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. 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 cbc8a2092..8f5eb45d8 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'); @@ -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}`); +}); 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;