From d0b29e974d2df117b3e9c1c1ba5eb914c1739b7c Mon Sep 17 00:00:00 2001 From: Tushar Date: Mon, 17 Aug 2026 17:03:25 +0530 Subject: [PATCH 1/3] fix: use os.homedir() for models cache path on Windows process.env.HOME is undefined on Windows, causing path.join('~', '.cache') to create a literal ~ directory. Use os.homedir() which resolves correctly on all platforms. --- source/models/models-cache.spec.ts | 6 +++--- source/models/models-cache.ts | 5 +++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/source/models/models-cache.spec.ts b/source/models/models-cache.spec.ts index cbc8a2092..7f9d842e7 100644 --- a/source/models/models-cache.spec.ts +++ b/source/models/models-cache.spec.ts @@ -2,7 +2,7 @@ import test from 'ava'; import { unlink, writeFile, readFile, mkdir } from 'node:fs/promises'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; -import { tmpdir } from 'node:os'; +import { homedir, tmpdir } from 'node:os'; import type { CachedModelsData } from './models-types.js'; import { readCache, @@ -20,8 +20,8 @@ async function getCacheFilePath(): Promise { const DEFAULT_CACHE_DIR = process.platform === 'darwin' - ? path.join(process.env.HOME || '~', 'Library', 'Caches') - : path.join(process.env.HOME || '~', '.cache'); + ? path.join(homedir(), 'Library', 'Caches') + : path.join(homedir(), '.cache'); const cacheBase = xdgCache || DEFAULT_CACHE_DIR; return path.join(cacheBase, 'nanocoder', 'models.json'); diff --git a/source/models/models-cache.ts b/source/models/models-cache.ts index 524268d14..822dd61ac 100644 --- a/source/models/models-cache.ts +++ b/source/models/models-cache.ts @@ -5,6 +5,7 @@ import {constants} from 'node:fs'; import {access, mkdir, readFile, writeFile} from 'node:fs/promises'; +import * as os from 'node:os'; import * as path from 'node:path'; import {xdgCache} from 'xdg-basedir'; import {CACHE_MODELS_EXPIRATION_MS} from '@/constants'; @@ -14,8 +15,8 @@ import type {CachedModelsData, ModelsDevDatabase} from './models-types.js'; const DEFAULT_CACHE_DIR = process.platform === 'darwin' - ? path.join(process.env.HOME || '~', 'Library', 'Caches') - : path.join(process.env.HOME || '~', '.cache'); + ? path.join(os.homedir(), 'Library', 'Caches') + : path.join(os.homedir(), '.cache'); function getCacheDir(): string { const cacheBase = xdgCache || DEFAULT_CACHE_DIR; From a42b1b757112c6b1c3bbd9b8458757ac23e73871 Mon Sep 17 00:00:00 2001 From: Tushar Date: Mon, 17 Aug 2026 17:06:09 +0530 Subject: [PATCH 2/3] chore: add changeset for Windows models cache fix --- .changeset/fix-windows-models-cache-path.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-windows-models-cache-path.md diff --git a/.changeset/fix-windows-models-cache-path.md b/.changeset/fix-windows-models-cache-path.md new file mode 100644 index 000000000..2b471e863 --- /dev/null +++ b/.changeset/fix-windows-models-cache-path.md @@ -0,0 +1,5 @@ +--- +"@nanocollective/nanocoder": patch +--- + +Fixed models cache path on Windows by using `os.homedir()` instead of `process.env.HOME`, which is undefined on Windows. The cache now writes to the correct location instead of creating a literal `~` folder. From bb6d5f2e6766d9cce78efdc8a118cf728dd4de6f Mon Sep 17 00:00:00 2001 From: Tushar Date: Wed, 19 Aug 2026 20:00:32 +0530 Subject: [PATCH 3/3] fix: use os.homedir() in fetch-models.js and add Windows regression tests - Fix scripts/fetch-models.js to use os.homedir() instead of process.env.HOME || '~' which produces a literal ~ directory on Windows - Add regression tests that verify the old pattern produces a literal ~ while os.homedir() resolves the real home directory --- scripts/fetch-models.js | 5 +-- source/models/models-cache.spec.ts | 50 ++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/scripts/fetch-models.js b/scripts/fetch-models.js index ad3460b3f..34bd57c6a 100755 --- a/scripts/fetch-models.js +++ b/scripts/fetch-models.js @@ -6,6 +6,7 @@ */ import * as fs from 'node:fs'; +import * as os from 'node:os'; import * as path from 'node:path'; import {request} from 'undici'; import {xdgCache} from 'xdg-basedir'; @@ -16,8 +17,8 @@ const CACHE_EXPIRATION_MS = 7 * 24 * 60 * 60 * 1000; // 7 days // Get cache directory const DEFAULT_CACHE_DIR = process.platform === 'darwin' - ? path.join(process.env.HOME || '~', 'Library', 'Caches') - : path.join(process.env.HOME || '~', '.cache'); + ? path.join(os.homedir(), 'Library', 'Caches') + : path.join(os.homedir(), '.cache'); const cacheBase = xdgCache || DEFAULT_CACHE_DIR; const cacheDir = path.join(cacheBase, 'nanocoder'); diff --git a/source/models/models-cache.spec.ts b/source/models/models-cache.spec.ts index 7f9d842e7..8f5eb45d8 100644 --- a/source/models/models-cache.spec.ts +++ b/source/models/models-cache.spec.ts @@ -212,3 +212,53 @@ test('writeCache overwrites existing cache', async t => { t.truthy(readResult); t.deepEqual(readResult?.data, secondData); }); + +// ============================================================================ +// Windows regression: cache path must use os.homedir(), not process.env.HOME +// Regression for https://github.com/Nano-Collective/nanocoder/pull/886 +// When process.env.HOME is undefined (Windows default), the old code produced +// a literal "~" directory instead of resolving the user's home directory. +// ============================================================================ + +test('os.homedir() resolves correctly when HOME env is undefined (unlike process.env.HOME)', t => { + const savedHome = process.env.HOME; + delete process.env.HOME; + + try { + // The old broken pattern: process.env.HOME || '~' + const brokenPath = join(process.env.HOME || '~', '.cache'); + // The fixed pattern: os.homedir() + const fixedPath = join(homedir(), '.cache'); + + // On Windows, process.env.HOME is undefined, so old code yields literal "~" + t.true( + brokenPath.startsWith('~'), + `Old code produces literal ~ directory: ${brokenPath}`, + ); + // os.homedir() always resolves to the real home directory + t.false( + fixedPath.startsWith('~'), + `Fixed code resolves real home: ${fixedPath}`, + ); + t.not(brokenPath, fixedPath, 'Fix must produce a different path than the old code'); + } finally { + if (savedHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = savedHome; + } + } +}); + +test('actual cache path does not contain literal ~', async t => { + const {xdgCache} = await import('xdg-basedir'); + const DEFAULT_CACHE_DIR = + process.platform === 'darwin' + ? join(homedir(), 'Library', 'Caches') + : join(homedir(), '.cache'); + const cacheBase = xdgCache || DEFAULT_CACHE_DIR; + const cachePath = join(cacheBase, 'nanocoder', 'models.json'); + + t.false(cachePath.includes('~'), `Cache path must not contain literal ~: ${cachePath}`); + t.true(cachePath.startsWith(homedir()), `Cache path must be under home directory: ${cachePath}`); +});