Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-windows-models-cache-path.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 3 additions & 2 deletions scripts/fetch-models.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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');
Expand Down
56 changes: 53 additions & 3 deletions source/models/models-cache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -20,8 +20,8 @@ async function getCacheFilePath(): Promise<string> {

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');
Expand Down Expand Up @@ -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}`);
});
5 changes: 3 additions & 2 deletions source/models/models-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand Down
Loading