@nemus-cli/nemus 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.15.0] - 2026-09-02
11
+
12
+ ### Added
13
+
14
+ - **XDG base-directory support for config/state (Linux).** A fresh install now
15
+ stores its config/state under `$XDG_CONFIG_HOME/nemus` (default
16
+ `~/.config/nemus`) on Linux, making Nemus a better Linux citizen. Precedence
17
+ (first match wins): `NEMUS_CACHE_DIR`/`WORKSPACE_MANAGER_CACHE_DIR` override →
18
+ an existing `~/.nemus` with state (kept on every platform, never moved) →
19
+ an absolute `XDG_CONFIG_HOME` → Linux default `~/.config/nemus` → `~/.nemus`
20
+ (macOS/Windows). Existing `~/.nemus` installs are untouched. (#77)
21
+
10
22
  ## [0.14.0] - 2026-09-02
11
23
 
12
24
  ### Changed
package/README.md CHANGED
@@ -284,7 +284,8 @@ Everything Nemus reads from the environment (all optional):
284
284
  | Variable | Effect |
285
285
  | --- | --- |
286
286
  | `NEMUS_DIR` | Override where workspaces are created (also `WORKSPACE_MANAGER_DIR`). |
287
- | `NEMUS_CACHE_DIR` | Override the cache/config/state dir, default `~/.nemus` (also `WORKSPACE_MANAGER_CACHE_DIR`). |
287
+ | `NEMUS_CACHE_DIR` | Override the config/state dir. Default: `~/.nemus`, or `$XDG_CONFIG_HOME/nemus` ( `~/.config/nemus`) on Linux. Also `WORKSPACE_MANAGER_CACHE_DIR`. |
288
+ | `XDG_CONFIG_HOME` | On Linux (or when set explicitly), a fresh install stores config/state under `$XDG_CONFIG_HOME/nemus`. An existing `~/.nemus` is always kept as-is. |
288
289
  | `NEMUS_JUDGE_MODEL` | Model for the `reflect` judge (overrides `--model`'s default). |
289
290
  | `NEMUS_JUDGE_THINKING` | Thinking level for the `reflect` judge on pi (`off`…`max`). |
290
291
  | `NEMUS_JUDGE_TIMEOUT_MS` | Timeout for the `reflect` judge call. |
@@ -426,7 +427,19 @@ export NEMUS_CACHE_DIR="$HOME/.cache/nemus" # default: ~/.nemus
426
427
 
427
428
  (The legacy `WORKSPACE_MANAGER_DIR` / `WORKSPACE_MANAGER_CACHE_DIR` names still
428
429
  work as fallbacks. On first run, state from the old `~/.workspace-manager-cache`
429
- location is migrated to `~/.nemus` automatically.)
430
+ location is migrated to the resolved config dir automatically.)
431
+
432
+ **Config/state location (XDG-aware).** Resolution order, first match wins —
433
+ designed so existing installs are never silently moved:
434
+
435
+ 1. `NEMUS_CACHE_DIR` / `WORKSPACE_MANAGER_CACHE_DIR` (explicit override).
436
+ 2. An existing `~/.nemus` that holds durable state (anything beyond regenerable
437
+ caches like `repos-cache.json` / `last-version-check.json` and the
438
+ `shell-integration.sh` file — e.g. `config.json`, `suites.json`, the
439
+ `reflect/` reports) — kept on every platform, never moved.
440
+ 3. `XDG_CONFIG_HOME/nemus`, when `XDG_CONFIG_HOME` is set to an absolute path.
441
+ 4. Linux with no prior install → `~/.config/nemus` (the XDG default).
442
+ 5. Otherwise (macOS/Windows, fresh install) → `~/.nemus`.
430
443
 
431
444
  Key settings: `githubOrg` (which org's repos to list — leave empty to list your
432
445
  own), `cloneProtocol` (`ssh`|`https`), `aiAgent`, `primaryAgent`, `installMcp`.
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.config = exports.CLONE_MAX_BUFFER = exports.CLONE_TIMEOUT_MS = exports.CONFIG_PATH = exports.META_FILENAME = exports.SUITES_FILE = exports.HISTORY_FILE = exports.CACHE_DIR = exports.WORKSPACES_DIR = exports.CONFIG_DEFAULTS = void 0;
37
+ exports.resolveNemusHome = resolveNemusHome;
37
38
  exports.getUserConfig = getUserConfig;
38
39
  exports.getPackageVersion = getPackageVersion;
39
40
  exports.getCloneUrl = getCloneUrl;
@@ -42,11 +43,62 @@ const os = __importStar(require("os"));
42
43
  const path = __importStar(require("path"));
43
44
  const fs = __importStar(require("fs"));
44
45
  const HOME_DIR = os.homedir();
45
- // Cache/config/state directory. Prefer the branded NEMUS_* env vars; fall back
46
- // to the legacy WORKSPACE_MANAGER_* names for backward compatibility; else the
47
- // default ~/.nemus home.
48
- const DEFAULT_CACHE_DIR = path.join(HOME_DIR, '.nemus');
49
46
  const LEGACY_CACHE_DIR = path.join(HOME_DIR, '.workspace-manager-cache');
47
+ // Entries in ~/.nemus that do NOT make it a "real install": regenerable caches
48
+ // and the shell-integration artifact. Anything else (config.json, suites.json,
49
+ // history.jsonl, the reflect/ report dir, or any future state) marks the dir as
50
+ // an existing install to keep in place. Denylisting the throwaways — a small,
51
+ // stable set — is exhaustive for durable state by construction, which is safer
52
+ // than allowlisting state files (that could miss e.g. reflect/ and wrongly
53
+ // relocate a real install to XDG on upgrade).
54
+ const REGENERABLE_ENTRIES = new Set([
55
+ 'last-version-check.json', // update-check cache
56
+ 'repos-cache.json', // GitHub repo-list cache
57
+ 'last-error.json', // last error, for report-bug
58
+ 'shell-integration.sh', // shell installer artifact — not Nemus state
59
+ '.DS_Store',
60
+ ]);
61
+ /** True if ~/.nemus holds anything beyond regenerable caches / the shell artifact. */
62
+ function hasDurableState(dir, readDir) {
63
+ let entries;
64
+ try {
65
+ entries = readDir(dir);
66
+ }
67
+ catch {
68
+ return false; // dir doesn't exist
69
+ }
70
+ return entries.some((e) => !REGENERABLE_ENTRIES.has(e));
71
+ }
72
+ /**
73
+ * Resolve the directory that holds Nemus config + state. Precedence (first
74
+ * match wins), designed so existing users are never silently moved:
75
+ *
76
+ * 1. Explicit override — NEMUS_CACHE_DIR (or legacy WORKSPACE_MANAGER_CACHE_DIR).
77
+ * 2. An existing ~/.nemus that holds durable state (anything beyond
78
+ * regenerable caches / the shell-integration file) — kept on every platform.
79
+ * 3. XDG_CONFIG_HOME, when set to an absolute path — $XDG_CONFIG_HOME/nemus.
80
+ * 4. Linux with no prior install — the XDG default ~/.config/nemus.
81
+ * 5. Otherwise (macOS/Windows, fresh install) — ~/.nemus.
82
+ *
83
+ * Config + state are kept together in one dir (a later change may split cache
84
+ * out under XDG_CACHE_HOME). The dir is chosen under XDG_CONFIG_HOME, not
85
+ * XDG_CACHE_HOME, because it holds real config (config.json) — not disposable
86
+ * cache a cleaner may wipe. A relative XDG_CONFIG_HOME is ignored per the spec.
87
+ */
88
+ function resolveNemusHome({ env, home, platform, readDir }) {
89
+ const explicit = env.NEMUS_CACHE_DIR || env.WORKSPACE_MANAGER_CACHE_DIR;
90
+ if (explicit)
91
+ return explicit;
92
+ const branded = path.join(home, '.nemus');
93
+ if (hasDurableState(branded, readDir))
94
+ return branded;
95
+ const xdg = env.XDG_CONFIG_HOME;
96
+ if (xdg && path.isAbsolute(xdg))
97
+ return path.join(xdg, 'nemus');
98
+ if (platform === 'linux')
99
+ return path.join(home, '.config', 'nemus');
100
+ return branded;
101
+ }
50
102
  /**
51
103
  * One-time, best-effort migration of state from the pre-0.2.2 cache location
52
104
  * (~/.workspace-manager-cache) to ~/.nemus. Runs only when using the default
@@ -58,11 +110,15 @@ const LEGACY_CACHE_DIR = path.join(HOME_DIR, '.workspace-manager-cache');
58
110
  * shared by another tool whose "latest version" is unrelated to nemus, and
59
111
  * copying it would make the update check report a wrong version until the entry
60
112
  * expires. Skipping it just forces one fresh lookup.
113
+ *
114
+ * Skipped when an explicit env override is in effect (don't copy into a path the
115
+ * user deliberately chose). Runs into whatever default location was resolved,
116
+ * including a new XDG dir on Linux.
61
117
  */
62
118
  const MIGRATION_SKIP = new Set(['last-version-check.json']);
63
- function migrateLegacyCacheDir(targetDir) {
119
+ function migrateLegacyCacheDir(targetDir, isExplicitOverride) {
64
120
  try {
65
- if (targetDir === DEFAULT_CACHE_DIR && !fs.existsSync(targetDir) && fs.existsSync(LEGACY_CACHE_DIR)) {
121
+ if (!isExplicitOverride && !fs.existsSync(targetDir) && fs.existsSync(LEGACY_CACHE_DIR)) {
66
122
  fs.cpSync(LEGACY_CACHE_DIR, targetDir, {
67
123
  recursive: true,
68
124
  filter: (src) => !MIGRATION_SKIP.has(path.basename(src)),
@@ -73,8 +129,14 @@ function migrateLegacyCacheDir(targetDir) {
73
129
  // best-effort — ignore and let the dir be created lazily
74
130
  }
75
131
  }
76
- const CACHE_DIR_RESOLVED = process.env.NEMUS_CACHE_DIR || process.env.WORKSPACE_MANAGER_CACHE_DIR || DEFAULT_CACHE_DIR;
77
- migrateLegacyCacheDir(CACHE_DIR_RESOLVED);
132
+ const EXPLICIT_OVERRIDE = !!(process.env.NEMUS_CACHE_DIR || process.env.WORKSPACE_MANAGER_CACHE_DIR);
133
+ const CACHE_DIR_RESOLVED = resolveNemusHome({
134
+ env: process.env,
135
+ home: HOME_DIR,
136
+ platform: process.platform,
137
+ readDir: (p) => fs.readdirSync(p),
138
+ });
139
+ migrateLegacyCacheDir(CACHE_DIR_RESOLVED, EXPLICIT_OVERRIDE);
78
140
  // Config file lives inside the cache dir.
79
141
  const CONFIG_FILE = path.join(CACHE_DIR_RESOLVED, 'config.json');
80
142
  exports.CONFIG_DEFAULTS = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nemus-cli/nemus",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "workspaces": [
5
5
  "packages/*"
6
6
  ],
@@ -202,12 +202,21 @@ describe('config', () => {
202
202
  });
203
203
 
204
204
  describe('cache dir location + legacy migration', () => {
205
- it('defaults CACHE_DIR to ~/.nemus when no env override is set', async () => {
205
+ // With XDG support a fresh install resolves to ~/.config/nemus on Linux and
206
+ // ~/.nemus elsewhere. Delete XDG_CONFIG_HOME so resolution is homedir-based
207
+ // (and stays inside the tempDir sandbox on any CI host).
208
+ const defaultHome = () =>
209
+ process.platform === 'linux'
210
+ ? path.join(tempDir, '.config', 'nemus')
211
+ : path.join(tempDir, '.nemus');
212
+
213
+ it('defaults CACHE_DIR to the platform default when no env override is set', async () => {
206
214
  delete process.env.NEMUS_CACHE_DIR;
207
215
  delete process.env.WORKSPACE_MANAGER_CACHE_DIR;
216
+ delete process.env.XDG_CONFIG_HOME;
208
217
  vi.resetModules();
209
218
  const { CACHE_DIR } = await loadModule();
210
- expect(CACHE_DIR).toBe(path.join(tempDir, '.nemus'));
219
+ expect(CACHE_DIR).toBe(defaultHome());
211
220
  });
212
221
 
213
222
  it('prefers NEMUS_CACHE_DIR, then legacy WORKSPACE_MANAGER_CACHE_DIR', async () => {
@@ -222,41 +231,122 @@ describe('config', () => {
222
231
  expect((await loadModule()).CACHE_DIR).toBe(path.join(tempDir, '.workspace-manager-cache'));
223
232
  });
224
233
 
225
- it('migrates state from ~/.workspace-manager-cache to ~/.nemus on first run', async () => {
234
+ it('migrates state from ~/.workspace-manager-cache to the default dir on first run', async () => {
226
235
  delete process.env.NEMUS_CACHE_DIR;
227
236
  delete process.env.WORKSPACE_MANAGER_CACHE_DIR;
237
+ delete process.env.XDG_CONFIG_HOME;
228
238
  // legacy dir (created by beforeEach) holds prior state; new dir absent
229
239
  fs.writeFileSync(path.join(tempDir, '.workspace-manager-cache', 'suites.json'), '{"x":1}');
230
- expect(fs.existsSync(path.join(tempDir, '.nemus'))).toBe(false);
240
+ expect(fs.existsSync(defaultHome())).toBe(false);
231
241
  vi.resetModules();
232
242
  const { CACHE_DIR } = await loadModule();
233
- expect(CACHE_DIR).toBe(path.join(tempDir, '.nemus'));
243
+ expect(CACHE_DIR).toBe(defaultHome());
234
244
  // copied, not moved: file exists in the new dir AND the legacy dir survives
235
- expect(fs.readFileSync(path.join(tempDir, '.nemus', 'suites.json'), 'utf-8')).toBe('{"x":1}');
245
+ expect(fs.readFileSync(path.join(defaultHome(), 'suites.json'), 'utf-8')).toBe('{"x":1}');
236
246
  expect(fs.existsSync(path.join(tempDir, '.workspace-manager-cache', 'suites.json'))).toBe(true);
237
247
  });
238
248
 
239
249
  it('does not migrate last-version-check.json (avoids inheriting a foreign latest)', async () => {
240
250
  delete process.env.NEMUS_CACHE_DIR;
241
251
  delete process.env.WORKSPACE_MANAGER_CACHE_DIR;
252
+ delete process.env.XDG_CONFIG_HOME;
242
253
  const legacy = path.join(tempDir, '.workspace-manager-cache');
243
254
  fs.writeFileSync(path.join(legacy, 'suites.json'), '{"x":1}');
244
255
  fs.writeFileSync(path.join(legacy, 'last-version-check.json'), '{"latestVersion":"99.0.0"}');
245
256
  vi.resetModules();
246
257
  await loadModule();
247
258
  // other state migrates, but the version-check cache is left behind
248
- expect(fs.existsSync(path.join(tempDir, '.nemus', 'suites.json'))).toBe(true);
249
- expect(fs.existsSync(path.join(tempDir, '.nemus', 'last-version-check.json'))).toBe(false);
259
+ expect(fs.existsSync(path.join(defaultHome(), 'suites.json'))).toBe(true);
260
+ expect(fs.existsSync(path.join(defaultHome(), 'last-version-check.json'))).toBe(false);
250
261
  });
251
262
 
252
263
  it('does not migrate when the new dir already exists', async () => {
253
264
  delete process.env.NEMUS_CACHE_DIR;
254
265
  delete process.env.WORKSPACE_MANAGER_CACHE_DIR;
255
- fs.mkdirSync(path.join(tempDir, '.nemus'), { recursive: true });
266
+ delete process.env.XDG_CONFIG_HOME;
267
+ fs.mkdirSync(defaultHome(), { recursive: true });
256
268
  fs.writeFileSync(path.join(tempDir, '.workspace-manager-cache', 'suites.json'), '{"x":1}');
257
269
  vi.resetModules();
258
270
  await loadModule();
259
- expect(fs.existsSync(path.join(tempDir, '.nemus', 'suites.json'))).toBe(false);
271
+ expect(fs.existsSync(path.join(defaultHome(), 'suites.json'))).toBe(false);
272
+ });
273
+
274
+ it('keeps an existing ~/.nemus install even on Linux (no surprise XDG move)', async () => {
275
+ delete process.env.NEMUS_CACHE_DIR;
276
+ delete process.env.WORKSPACE_MANAGER_CACHE_DIR;
277
+ delete process.env.XDG_CONFIG_HOME;
278
+ const branded = path.join(tempDir, '.nemus');
279
+ fs.mkdirSync(branded, { recursive: true });
280
+ fs.writeFileSync(path.join(branded, 'config.json'), '{}'); // real prior state
281
+ vi.resetModules();
282
+ const { CACHE_DIR } = await loadModule();
283
+ expect(CACHE_DIR).toBe(branded);
284
+ });
285
+ });
286
+
287
+ describe('resolveNemusHome precedence', () => {
288
+ const home = '/home/u';
289
+ const branded = path.join(home, '.nemus');
290
+ // A readDir that reports the given entries for ~/.nemus and "absent" elsewhere.
291
+ const brandedHas = (...entries: string[]) => (p: string): string[] => {
292
+ if (p === branded) return entries;
293
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
294
+ };
295
+ const empty = (): string[] => {
296
+ throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
297
+ };
298
+
299
+ it('explicit NEMUS_CACHE_DIR / legacy override wins over everything', async () => {
300
+ const { resolveNemusHome } = await loadModule();
301
+ expect(
302
+ resolveNemusHome({ env: { NEMUS_CACHE_DIR: '/x', XDG_CONFIG_HOME: '/c' }, home, platform: 'linux', readDir: brandedHas('config.json') }),
303
+ ).toBe('/x');
304
+ expect(
305
+ resolveNemusHome({ env: { WORKSPACE_MANAGER_CACHE_DIR: '/y' }, home, platform: 'linux', readDir: brandedHas('config.json') }),
306
+ ).toBe('/y');
307
+ });
308
+
309
+ it('an existing ~/.nemus with durable state wins over XDG, on any platform', async () => {
310
+ const { resolveNemusHome } = await loadModule();
311
+ expect(resolveNemusHome({ env: { XDG_CONFIG_HOME: '/c' }, home, platform: 'linux', readDir: brandedHas('config.json') })).toBe(branded);
312
+ });
313
+
314
+ it('counts reflect/ (and any non-cache entry) as durable state — not just the classic 3', async () => {
315
+ const { resolveNemusHome } = await loadModule();
316
+ // a reflect-only user has no config.json/suites.json/history.jsonl
317
+ expect(resolveNemusHome({ env: {}, home, platform: 'linux', readDir: brandedHas('reflect') })).toBe(branded);
318
+ });
319
+
320
+ it('a ~/.nemus holding only regenerable caches / the shell artifact does NOT count', async () => {
321
+ const { resolveNemusHome } = await loadModule();
322
+ expect(
323
+ resolveNemusHome({
324
+ env: {},
325
+ home,
326
+ platform: 'linux',
327
+ readDir: brandedHas('shell-integration.sh', 'last-version-check.json', 'repos-cache.json', 'last-error.json', '.DS_Store'),
328
+ }),
329
+ ).toBe(path.join(home, '.config', 'nemus'));
330
+ });
331
+
332
+ it('honors an absolute XDG_CONFIG_HOME on a fresh install (any platform)', async () => {
333
+ const { resolveNemusHome } = await loadModule();
334
+ expect(resolveNemusHome({ env: { XDG_CONFIG_HOME: '/cfg' }, home, platform: 'linux', readDir: empty })).toBe('/cfg/nemus');
335
+ expect(resolveNemusHome({ env: { XDG_CONFIG_HOME: '/cfg' }, home, platform: 'darwin', readDir: empty })).toBe('/cfg/nemus');
336
+ });
337
+
338
+ it('ignores a relative XDG_CONFIG_HOME (per spec)', async () => {
339
+ const { resolveNemusHome } = await loadModule();
340
+ expect(resolveNemusHome({ env: { XDG_CONFIG_HOME: 'rel/path' }, home, platform: 'linux', readDir: empty })).toBe(
341
+ path.join(home, '.config', 'nemus'),
342
+ );
343
+ });
344
+
345
+ it('fresh-install default: Linux -> ~/.config/nemus, macOS/Windows -> ~/.nemus', async () => {
346
+ const { resolveNemusHome } = await loadModule();
347
+ expect(resolveNemusHome({ env: {}, home, platform: 'linux', readDir: empty })).toBe(path.join(home, '.config', 'nemus'));
348
+ expect(resolveNemusHome({ env: {}, home, platform: 'darwin', readDir: empty })).toBe(branded);
349
+ expect(resolveNemusHome({ env: {}, home, platform: 'win32', readDir: empty })).toBe(branded);
260
350
  });
261
351
  });
262
352
  });
@@ -4,12 +4,73 @@ import * as fs from 'fs';
4
4
 
5
5
  const HOME_DIR = os.homedir();
6
6
 
7
- // Cache/config/state directory. Prefer the branded NEMUS_* env vars; fall back
8
- // to the legacy WORKSPACE_MANAGER_* names for backward compatibility; else the
9
- // default ~/.nemus home.
10
- const DEFAULT_CACHE_DIR = path.join(HOME_DIR, '.nemus');
11
7
  const LEGACY_CACHE_DIR = path.join(HOME_DIR, '.workspace-manager-cache');
12
8
 
9
+ // Entries in ~/.nemus that do NOT make it a "real install": regenerable caches
10
+ // and the shell-integration artifact. Anything else (config.json, suites.json,
11
+ // history.jsonl, the reflect/ report dir, or any future state) marks the dir as
12
+ // an existing install to keep in place. Denylisting the throwaways — a small,
13
+ // stable set — is exhaustive for durable state by construction, which is safer
14
+ // than allowlisting state files (that could miss e.g. reflect/ and wrongly
15
+ // relocate a real install to XDG on upgrade).
16
+ const REGENERABLE_ENTRIES = new Set([
17
+ 'last-version-check.json', // update-check cache
18
+ 'repos-cache.json', // GitHub repo-list cache
19
+ 'last-error.json', // last error, for report-bug
20
+ 'shell-integration.sh', // shell installer artifact — not Nemus state
21
+ '.DS_Store',
22
+ ]);
23
+
24
+ export interface NemusHomeEnv {
25
+ env: NodeJS.ProcessEnv;
26
+ home: string;
27
+ platform: NodeJS.Platform;
28
+ /** List a directory's entries; throw (or return []) when it doesn't exist. */
29
+ readDir: (p: string) => string[];
30
+ }
31
+
32
+ /** True if ~/.nemus holds anything beyond regenerable caches / the shell artifact. */
33
+ function hasDurableState(dir: string, readDir: (p: string) => string[]): boolean {
34
+ let entries: string[];
35
+ try {
36
+ entries = readDir(dir);
37
+ } catch {
38
+ return false; // dir doesn't exist
39
+ }
40
+ return entries.some((e) => !REGENERABLE_ENTRIES.has(e));
41
+ }
42
+
43
+ /**
44
+ * Resolve the directory that holds Nemus config + state. Precedence (first
45
+ * match wins), designed so existing users are never silently moved:
46
+ *
47
+ * 1. Explicit override — NEMUS_CACHE_DIR (or legacy WORKSPACE_MANAGER_CACHE_DIR).
48
+ * 2. An existing ~/.nemus that holds durable state (anything beyond
49
+ * regenerable caches / the shell-integration file) — kept on every platform.
50
+ * 3. XDG_CONFIG_HOME, when set to an absolute path — $XDG_CONFIG_HOME/nemus.
51
+ * 4. Linux with no prior install — the XDG default ~/.config/nemus.
52
+ * 5. Otherwise (macOS/Windows, fresh install) — ~/.nemus.
53
+ *
54
+ * Config + state are kept together in one dir (a later change may split cache
55
+ * out under XDG_CACHE_HOME). The dir is chosen under XDG_CONFIG_HOME, not
56
+ * XDG_CACHE_HOME, because it holds real config (config.json) — not disposable
57
+ * cache a cleaner may wipe. A relative XDG_CONFIG_HOME is ignored per the spec.
58
+ */
59
+ export function resolveNemusHome({ env, home, platform, readDir }: NemusHomeEnv): string {
60
+ const explicit = env.NEMUS_CACHE_DIR || env.WORKSPACE_MANAGER_CACHE_DIR;
61
+ if (explicit) return explicit;
62
+
63
+ const branded = path.join(home, '.nemus');
64
+ if (hasDurableState(branded, readDir)) return branded;
65
+
66
+ const xdg = env.XDG_CONFIG_HOME;
67
+ if (xdg && path.isAbsolute(xdg)) return path.join(xdg, 'nemus');
68
+
69
+ if (platform === 'linux') return path.join(home, '.config', 'nemus');
70
+
71
+ return branded;
72
+ }
73
+
13
74
  /**
14
75
  * One-time, best-effort migration of state from the pre-0.2.2 cache location
15
76
  * (~/.workspace-manager-cache) to ~/.nemus. Runs only when using the default
@@ -21,11 +82,15 @@ const LEGACY_CACHE_DIR = path.join(HOME_DIR, '.workspace-manager-cache');
21
82
  * shared by another tool whose "latest version" is unrelated to nemus, and
22
83
  * copying it would make the update check report a wrong version until the entry
23
84
  * expires. Skipping it just forces one fresh lookup.
85
+ *
86
+ * Skipped when an explicit env override is in effect (don't copy into a path the
87
+ * user deliberately chose). Runs into whatever default location was resolved,
88
+ * including a new XDG dir on Linux.
24
89
  */
25
90
  const MIGRATION_SKIP = new Set(['last-version-check.json']);
26
- function migrateLegacyCacheDir(targetDir: string): void {
91
+ function migrateLegacyCacheDir(targetDir: string, isExplicitOverride: boolean): void {
27
92
  try {
28
- if (targetDir === DEFAULT_CACHE_DIR && !fs.existsSync(targetDir) && fs.existsSync(LEGACY_CACHE_DIR)) {
93
+ if (!isExplicitOverride && !fs.existsSync(targetDir) && fs.existsSync(LEGACY_CACHE_DIR)) {
29
94
  fs.cpSync(LEGACY_CACHE_DIR, targetDir, {
30
95
  recursive: true,
31
96
  filter: (src) => !MIGRATION_SKIP.has(path.basename(src)),
@@ -36,9 +101,14 @@ function migrateLegacyCacheDir(targetDir: string): void {
36
101
  }
37
102
  }
38
103
 
39
- const CACHE_DIR_RESOLVED =
40
- process.env.NEMUS_CACHE_DIR || process.env.WORKSPACE_MANAGER_CACHE_DIR || DEFAULT_CACHE_DIR;
41
- migrateLegacyCacheDir(CACHE_DIR_RESOLVED);
104
+ const EXPLICIT_OVERRIDE = !!(process.env.NEMUS_CACHE_DIR || process.env.WORKSPACE_MANAGER_CACHE_DIR);
105
+ const CACHE_DIR_RESOLVED = resolveNemusHome({
106
+ env: process.env,
107
+ home: HOME_DIR,
108
+ platform: process.platform,
109
+ readDir: (p) => fs.readdirSync(p),
110
+ });
111
+ migrateLegacyCacheDir(CACHE_DIR_RESOLVED, EXPLICIT_OVERRIDE);
42
112
 
43
113
  // Config file lives inside the cache dir.
44
114
  const CONFIG_FILE = path.join(CACHE_DIR_RESOLVED, 'config.json');