claude-nomad 0.63.2 → 0.64.1

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/dist/nomad.mjs CHANGED
@@ -42,11 +42,21 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
42
42
  ));
43
43
 
44
44
  // src/config.never-sync.ts
45
+ function stripTrailingDotsAndWhitespace(name) {
46
+ let end = name.length;
47
+ while (end > 0 && (name[end - 1] === "." || /\s/.test(name[end - 1]))) end -= 1;
48
+ return name.slice(0, end);
49
+ }
45
50
  function isSecretFileName(name) {
46
- return SECRET_FILE_PATTERNS.some((re) => re.test(name));
51
+ const stripped = stripTrailingDotsAndWhitespace(name);
52
+ return SECRET_FILE_PATTERNS.some((re) => re.test(stripped));
47
53
  }
48
54
  function isDeniedName(blockSet, name) {
49
- return blockSet.has(name) || blockSet.has(name.toLowerCase()) || isSecretFileName(name);
55
+ const stripped = stripTrailingDotsAndWhitespace(name);
56
+ return blockSet.has(name) || blockSet.has(name.toLowerCase()) || blockSet.has(stripped) || blockSet.has(stripped.toLowerCase()) || isSecretFileName(name);
57
+ }
58
+ function isClaudeExtraName(name) {
59
+ return stripTrailingDotsAndWhitespace(name).toLowerCase() === ".claude";
50
60
  }
51
61
  var NEVER_SYNC, CLAUDE_EXTRA_NEVER_SYNC, SECRET_FILE_PATTERNS;
52
62
  var init_config_never_sync = __esm({
@@ -302,14 +312,67 @@ function assertSafeLogical(logical) {
302
312
  );
303
313
  }
304
314
  }
315
+ function validateSharedDirEntry(entry) {
316
+ if (typeof entry !== "string") {
317
+ return { reason: "not-a-string", message: "not a string" };
318
+ }
319
+ if (!SAFE_SEGMENT.test(entry) || entry === "." || entry === "..") {
320
+ return {
321
+ reason: "not-a-segment",
322
+ message: 'not a single path segment (contains a path separator, an unsupported character, "." or "..")'
323
+ };
324
+ }
325
+ const addressed = stripTrailingDots(entry);
326
+ const named = classifyDeniedName(addressed);
327
+ if (named !== null) {
328
+ return addressed === entry ? named : {
329
+ reason: named.reason,
330
+ message: `a trailing-dot name shadowing ${named.message}`
331
+ };
332
+ }
333
+ if (entry.endsWith(".")) {
334
+ return {
335
+ reason: "win32-alias",
336
+ message: 'a trailing-dot name (git for Windows refuses to create or check out a path ending in ".")'
337
+ };
338
+ }
339
+ return null;
340
+ }
341
+ function stripTrailingDots(name) {
342
+ let end = name.length;
343
+ while (end > 0 && name[end - 1] === ".") end -= 1;
344
+ return name.slice(0, end);
345
+ }
346
+ function classifyDeniedName(name) {
347
+ const folded = name.toLowerCase();
348
+ if (NEVER_SYNC.has(name) || NEVER_SYNC.has(folded)) {
349
+ return { reason: "never-sync", message: "a never-sync name" };
350
+ }
351
+ if (RESERVED_SHARED.has(name) || RESERVED_SHARED_FOLDED.has(folded)) {
352
+ return { reason: "reserved", message: "a reserved shared/ name" };
353
+ }
354
+ const dot = folded.indexOf(".");
355
+ const stem = dot === -1 ? folded : folded.slice(0, dot);
356
+ if (WIN32_DEVICE_NAMES.has(stem)) {
357
+ return { reason: "reserved", message: "a reserved Windows device name" };
358
+ }
359
+ if (isSecretFileName(name)) {
360
+ return {
361
+ reason: "secret-shaped",
362
+ message: "a credential-shaped filename (.env, id_rsa, credentials, *.pem, *.key and similar)"
363
+ };
364
+ }
365
+ return null;
366
+ }
367
+ function mayJoinRefusedEntry(entry, reason, remediable) {
368
+ if (UNJOINABLE_REASONS.has(reason)) return false;
369
+ if (entry.endsWith(".")) return true;
370
+ return remediable.has(reason);
371
+ }
305
372
  function isValidSharedDir(entry) {
306
- if (typeof entry !== "string") return false;
307
- if (!SAFE_SEGMENT.test(entry) || entry === "." || entry === "..") return false;
308
- if (NEVER_SYNC.has(entry)) return false;
309
- if (RESERVED_SHARED.has(entry)) return false;
310
- return true;
373
+ return validateSharedDirEntry(entry) === null;
311
374
  }
312
- var SAFE_LOGICAL, SAFE_SEGMENT, RESERVED_SHARED;
375
+ var SAFE_LOGICAL, SAFE_SEGMENT, RESERVED_SHARED, RESERVED_SHARED_FOLDED, WIN32_DEVICE_NAMES, UNJOINABLE_REASONS;
313
376
  var init_config_sharedDirs_guard = __esm({
314
377
  "src/config.sharedDirs.guard.ts"() {
315
378
  "use strict";
@@ -331,6 +394,19 @@ var init_config_sharedDirs_guard = __esm({
331
394
  "extras",
332
395
  "projects"
333
396
  ]);
397
+ RESERVED_SHARED_FOLDED = new Set([...RESERVED_SHARED].map((name) => name.toLowerCase()));
398
+ WIN32_DEVICE_NAMES = /* @__PURE__ */ new Set([
399
+ "con",
400
+ "prn",
401
+ "aux",
402
+ "nul",
403
+ ...Array.from({ length: 9 }, (_unused, i) => `com${i + 1}`),
404
+ ...Array.from({ length: 9 }, (_unused, i) => `lpt${i + 1}`)
405
+ ]);
406
+ UNJOINABLE_REASONS = /* @__PURE__ */ new Set([
407
+ "not-a-string",
408
+ "not-a-segment"
409
+ ]);
334
410
  }
335
411
  });
336
412
 
@@ -421,7 +497,6 @@ var init_settings_keys = __esm({
421
497
  "includeGitInstructions",
422
498
  "inputNeededNotifEnabled",
423
499
  "language",
424
- "leftArrowOpensAgents",
425
500
  "managedMcpServers",
426
501
  "minimumVersion",
427
502
  "model",
@@ -539,19 +614,31 @@ function crashDir() {
539
614
  function manifestPath() {
540
615
  return join(home(), ".cache", "claude-nomad", `push-manifest-${encodeURIComponent(HOST)}.json`);
541
616
  }
542
- function allSharedLinks(map) {
617
+ function sharedBaselinePath() {
618
+ return join(home(), ".cache", "claude-nomad", `shared-baseline-${encodeURIComponent(HOST)}.json`);
619
+ }
620
+ function allSharedLinks(map, opts = {}) {
621
+ const emit = opts.quiet === true ? () => void 0 : warn;
622
+ const raw = map.sharedDirs;
623
+ if (raw !== void 0 && !Array.isArray(raw)) {
624
+ emit("sharedDirs in path-map.json is not an array; ignoring the whole field");
625
+ return [...SHARED_LINKS];
626
+ }
543
627
  const extras = [];
544
- for (const entry of map.sharedDirs ?? []) {
545
- if (isValidSharedDir(entry)) {
628
+ for (const entry of raw ?? []) {
629
+ const rejection = validateSharedDirEntry(entry);
630
+ if (rejection === null) {
546
631
  extras.push(entry);
547
632
  } else {
548
- warn(
549
- `sharedDirs entry ${JSON.stringify(entry)} is invalid (path separator, reserved name, or NEVER_SYNC); skipping`
550
- );
633
+ emit(`sharedDirs entry ${JSON.stringify(entry)} rejected: ${rejection.message}; skipping`);
551
634
  }
552
635
  }
553
636
  return [...SHARED_LINKS, ...extras];
554
637
  }
638
+ function sharedDirEntries(map) {
639
+ const raw = map.sharedDirs;
640
+ return Array.isArray(raw) ? raw : [];
641
+ }
555
642
  var SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, GITLEAKS_SCAN_TIMEOUT_MS, HOST, SHARED_LINKS, GSD_PREFIX, GSD_DROPPED_NAMES, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, PUSH_ALLOWED_STATIC;
556
643
  var init_config = __esm({
557
644
  "src/config.ts"() {
@@ -1420,6 +1507,7 @@ var init_push_leak_verdict = __esm({
1420
1507
  // src/commands.adopt.ts
1421
1508
  init_config();
1422
1509
  init_config_sharedDirs_guard();
1510
+ init_exit_codes();
1423
1511
  import { cpSync as cpSync4, existsSync as existsSync5, lstatSync as lstatSync5, rmSync as rmSync4 } from "node:fs";
1424
1512
  import { join as join6 } from "node:path";
1425
1513
 
@@ -1890,7 +1978,7 @@ function copyExtrasFileSkipDiverged(src, dst) {
1890
1978
  copyExtras(src, dst);
1891
1979
  }
1892
1980
  function extrasDenySet(dirname14) {
1893
- return dirname14 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
1981
+ return isClaudeExtraName(dirname14) ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
1894
1982
  }
1895
1983
  function copyExtrasFiltered(src, dst, blockSet) {
1896
1984
  rmSync2(dst, { recursive: true, force: true });
@@ -2131,7 +2219,7 @@ function readMapIfPresent(repoHome2) {
2131
2219
  return existsSync5(mapPath) ? readPathMap(mapPath) : { projects: {} };
2132
2220
  }
2133
2221
  function isConfiguredTarget(name, map) {
2134
- return SHARED_LINKS.includes(name) || (map.sharedDirs?.includes(name) ?? false);
2222
+ return SHARED_LINKS.includes(name) || sharedDirEntries(map).includes(name);
2135
2223
  }
2136
2224
  function isValidAdoptName(name) {
2137
2225
  if (SHARED_LINKS.includes(name)) return true;
@@ -2153,6 +2241,13 @@ function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
2153
2241
  }
2154
2242
  function cmdAdopt(name, opts = {}) {
2155
2243
  const dryRun = opts.dryRun === true;
2244
+ const rejection = validateSharedDirEntry(name);
2245
+ if (rejection !== null && rejection.reason === "secret-shaped") {
2246
+ throw new NomadFatal(
2247
+ `cannot adopt ${JSON.stringify(name)}: ${rejection.message}. If it is listed in sharedDirs in path-map.json, remove it there too.`,
2248
+ { code: EXIT.GENERIC_FAILURE }
2249
+ );
2250
+ }
2156
2251
  if (!isValidAdoptName(name)) {
2157
2252
  fail(`invalid name: ${JSON.stringify(name)}`);
2158
2253
  process.exit(1);
@@ -2566,6 +2661,7 @@ function cmdClean(opts, backupBase2 = backupBase()) {
2566
2661
 
2567
2662
  // src/commands.eject.ts
2568
2663
  init_config();
2664
+ init_config_sharedDirs_guard();
2569
2665
  init_utils();
2570
2666
  init_utils_fs();
2571
2667
  init_utils_json();
@@ -2703,6 +2799,19 @@ function materializeOneOrDie(name, linkPath, sharedRoot, done) {
2703
2799
  );
2704
2800
  }
2705
2801
  }
2802
+ var WIDENED_REASONS = /* @__PURE__ */ new Set([
2803
+ "never-sync",
2804
+ "reserved",
2805
+ "secret-shaped"
2806
+ ]);
2807
+ function ejectNames(map) {
2808
+ const alreadyMaterialized = sharedDirEntries(map).filter((entry) => {
2809
+ if (typeof entry !== "string") return false;
2810
+ const rejection = validateSharedDirEntry(entry);
2811
+ return rejection === null || mayJoinRefusedEntry(entry, rejection.reason, WIDENED_REASONS);
2812
+ });
2813
+ return [.../* @__PURE__ */ new Set([...allSharedLinks(map), ...alreadyMaterialized])];
2814
+ }
2706
2815
  function defaultEjectRoots() {
2707
2816
  return { claudeHome: claudeHome(), repoHome: repoHome() };
2708
2817
  }
@@ -2710,16 +2819,31 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
2710
2819
  const dryRun = opts.dryRun === true;
2711
2820
  const { claudeHome: claudeHome2, repoHome: repoHome2 } = roots;
2712
2821
  const map = readMapIfPresent2(repoHome2);
2713
- const names = allSharedLinks(map);
2822
+ const names = ejectNames(map);
2714
2823
  const classifications = /* @__PURE__ */ new Map();
2715
2824
  for (const name of names) {
2716
2825
  classifications.set(name, classifyName(join11(claudeHome2, name)));
2717
2826
  }
2827
+ const base = new Set(allSharedLinks(map, { quiet: true }));
2828
+ for (const name of names) {
2829
+ if (base.has(name)) continue;
2830
+ if (classifications.get(name) !== "materialize") continue;
2831
+ item(`processing rejected entry already present on this host: ${name}`);
2832
+ }
2718
2833
  const dangling = names.filter((n) => classifications.get(n) === "dangling");
2719
- if (dangling.length > 0) {
2834
+ const danglingBase = dangling.filter((n) => base.has(n));
2835
+ const danglingRefused = dangling.filter((n) => !base.has(n));
2836
+ if (danglingBase.length > 0) {
2837
+ fail(
2838
+ `dangling symlink(s): ${danglingBase.join(", ")}. run \`nomad pull\` first to restore the missing target, then re-run \`nomad eject\``
2839
+ );
2840
+ }
2841
+ if (danglingRefused.length > 0) {
2720
2842
  fail(
2721
- `dangling symlink(s): ${dangling.join(", ")}. run \`nomad pull\` first to restore the missing target, then re-run \`nomad eject\``
2843
+ `dangling symlink(s) for a refused name nomad cannot restore: ${danglingRefused.join(", ")}. recover the content by hand if you need it, then remove the dead link and re-run \`nomad eject\``
2722
2844
  );
2845
+ }
2846
+ if (dangling.length > 0) {
2723
2847
  process.exit(1);
2724
2848
  }
2725
2849
  const sharedRoot = resolveSharedRoot(repoHome2);
@@ -2743,7 +2867,7 @@ import { join as join45 } from "node:path";
2743
2867
  init_color();
2744
2868
  init_config();
2745
2869
  import { existsSync as existsSync11, lstatSync as lstatSync8, readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
2746
- import { join as join13 } from "node:path";
2870
+ import { basename as basename3, join as join13 } from "node:path";
2747
2871
 
2748
2872
  // src/commands.doctor.format.ts
2749
2873
  init_color();
@@ -2818,6 +2942,61 @@ function readJsonSafe(path, label, section2) {
2818
2942
  }
2819
2943
  }
2820
2944
 
2945
+ // src/extras-sync.diff.ts
2946
+ init_utils();
2947
+ import { execFileSync as execFileSync2 } from "node:child_process";
2948
+ import { relative as relative3 } from "node:path";
2949
+ var NAME_STATUS_DIFF_TIMEOUT_MS = 3e3;
2950
+ function parseNameStatus(stdout) {
2951
+ const fields = stdout.split("\0").filter((f) => f.length > 0);
2952
+ const entries = [];
2953
+ for (let i = 0; i + 1 < fields.length; i += 2) {
2954
+ entries.push({ status: fields[i], path: fields[i + 1] });
2955
+ }
2956
+ return entries;
2957
+ }
2958
+ function labelEntry(entry) {
2959
+ if (entry.status === "D") return `${entry.path} (local only)`;
2960
+ if (entry.status === "A") return `${entry.path} (repo only)`;
2961
+ return entry.path;
2962
+ }
2963
+ function parseDiffOutput(stdout) {
2964
+ return parseNameStatus(stdout).map(labelEntry);
2965
+ }
2966
+ function parseModifiedPaths(stdout, a) {
2967
+ return parseNameStatus(stdout).filter((entry) => entry.status === "M").map((entry) => relative3(a, entry.path));
2968
+ }
2969
+ function runNameStatusDiff(a, b, parse) {
2970
+ try {
2971
+ const stdout = execFileSync2(
2972
+ "git",
2973
+ ["diff", "--no-index", "--no-renames", "-z", "--name-status", a, b],
2974
+ {
2975
+ stdio: ["ignore", "pipe", "pipe"],
2976
+ timeout: NAME_STATUS_DIFF_TIMEOUT_MS
2977
+ }
2978
+ ).toString();
2979
+ return parse(stdout);
2980
+ } catch (err) {
2981
+ const e = err;
2982
+ if (e.status === 1 && e.stdout !== void 0) {
2983
+ return parse(e.stdout.toString());
2984
+ }
2985
+ if (e.code === "ENOENT") {
2986
+ warn(`git not on PATH; divergence check skipped for ${a}`);
2987
+ return [];
2988
+ }
2989
+ warn(`divergence check failed for ${a}: ${e.message ?? String(err)}`);
2990
+ return [];
2991
+ }
2992
+ }
2993
+ function listDivergingFiles(a, b) {
2994
+ return runNameStatusDiff(a, b, parseDiffOutput);
2995
+ }
2996
+ function listDivergingModified(a, b) {
2997
+ return runNameStatusDiff(a, b, (stdout) => parseModifiedPaths(stdout, a));
2998
+ }
2999
+
2821
3000
  // src/init.classify.ts
2822
3001
  init_config();
2823
3002
  init_utils_json();
@@ -2942,6 +3121,25 @@ function reportRepoState(section2) {
2942
3121
  function repoHasSharedSource(name) {
2943
3122
  return existsSync11(join13(repoHome(), "shared", name));
2944
3123
  }
3124
+ function win32CopyOkRow(name) {
3125
+ return { line: `${green(okGlyph)} ${name}: real copy (win32 copy-sync)`, fail: false };
3126
+ }
3127
+ function divergingBasename(line) {
3128
+ return basename3(line.replace(/ \((?:local|repo) only\)$/, ""));
3129
+ }
3130
+ function classifyWin32Copy(name, p) {
3131
+ const sharedPath = join13(repoHome(), "shared", name);
3132
+ if (!existsSync11(sharedPath)) return win32CopyOkRow(name);
3133
+ const diverging = listDivergingFiles(p, sharedPath).filter(
3134
+ (line) => !isDeniedName(ALWAYS_NEVER_SYNC, divergingBasename(line))
3135
+ );
3136
+ if (diverging.length === 0) return win32CopyOkRow(name);
3137
+ return {
3138
+ line: `${yellow(warnGlyph)} ${name}: ${diverging.length} file(s) diverge from shared/${name}`,
3139
+ fail: false,
3140
+ children: diverging
3141
+ };
3142
+ }
2945
3143
  function classifySharedLink(name, p) {
2946
3144
  let stat;
2947
3145
  try {
@@ -2958,10 +3156,7 @@ function classifySharedLink(name, p) {
2958
3156
  }
2959
3157
  if (!stat.isSymbolicLink()) {
2960
3158
  if (process.platform === "win32") {
2961
- return {
2962
- line: `${green(okGlyph)} ${name}: real copy (win32 copy-sync)`,
2963
- fail: false
2964
- };
3159
+ return classifyWin32Copy(name, p);
2965
3160
  }
2966
3161
  return {
2967
3162
  line: `${red(failGlyph)} ${name}: NOT a symlink (blocks sync); run \`nomad adopt ${name}\` to fix`,
@@ -2993,11 +3188,14 @@ function classifySymlinkTarget(name, p) {
2993
3188
  }
2994
3189
  function reportSharedLinks(section2, map) {
2995
3190
  const claude = claudeHome();
2996
- for (const name of allSharedLinks(map)) {
3191
+ for (const name of allSharedLinks(map, { quiet: true })) {
2997
3192
  const p = join13(claude, name);
2998
- const { line, fail: fail2 } = classifySharedLink(name, p);
3193
+ const { line, fail: fail2, children } = classifySharedLink(name, p);
2999
3194
  addItem(section2, line);
3000
3195
  if (fail2) process.exitCode = 1;
3196
+ if (children) {
3197
+ for (const child of children) addChildItem(section2, child);
3198
+ }
3001
3199
  }
3002
3200
  }
3003
3201
  function reportDroppedNamesMigration(section2) {
@@ -3082,8 +3280,9 @@ function reportHostOverrides(section2, base, settings) {
3082
3280
  // src/commands.doctor.checks.pathmap.ts
3083
3281
  init_color();
3084
3282
  init_config();
3085
- import { existsSync as existsSync13, readdirSync as readdirSync6 } from "node:fs";
3086
- import { join as join15 } from "node:path";
3283
+ init_config_sharedDirs_guard();
3284
+ import { existsSync as existsSync13, lstatSync as lstatSync9, readdirSync as readdirSync6, realpathSync as realpathSync2 } from "node:fs";
3285
+ import { join as join15, sep as sep4 } from "node:path";
3087
3286
  init_utils_json();
3088
3287
  function reportMappedProjects(section2, map) {
3089
3288
  const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
@@ -3123,6 +3322,95 @@ function reportCurrentHostPathsMissing(section2, map) {
3123
3322
  }
3124
3323
  }
3125
3324
  }
3325
+ function hasSharedLeftover(entry) {
3326
+ try {
3327
+ return lstatSync9(join15(repoHome(), "shared", entry), { throwIfNoEntry: false }) !== void 0;
3328
+ } catch {
3329
+ return false;
3330
+ }
3331
+ }
3332
+ var PROBEABLE_REASONS = /* @__PURE__ */ new Set([
3333
+ "never-sync",
3334
+ "secret-shaped"
3335
+ ]);
3336
+ function classifyLocalLink(entry) {
3337
+ const linkPath = join15(claudeHome(), entry);
3338
+ let stat;
3339
+ try {
3340
+ stat = lstatSync9(linkPath, { throwIfNoEntry: false });
3341
+ } catch {
3342
+ return "absent";
3343
+ }
3344
+ if (stat?.isSymbolicLink() !== true) return "absent";
3345
+ let target;
3346
+ try {
3347
+ target = realpathSync2(linkPath);
3348
+ } catch {
3349
+ return "dangling";
3350
+ }
3351
+ let root;
3352
+ try {
3353
+ root = realpathSync2(join15(repoHome(), "shared"));
3354
+ } catch {
3355
+ return "foreign";
3356
+ }
3357
+ return target.startsWith(root + sep4) ? "managed" : "foreign";
3358
+ }
3359
+ function reportRejectedSharedDirs(section2, map) {
3360
+ const raw = map.sharedDirs;
3361
+ if (raw !== void 0 && !Array.isArray(raw)) {
3362
+ addItem(
3363
+ section2,
3364
+ `${yellow(warnGlyph)} path-map: sharedDirs is not an array (${typeof raw}); the whole field is ignored`
3365
+ );
3366
+ return;
3367
+ }
3368
+ const entries = Array.isArray(raw) ? raw : [];
3369
+ const probable = [];
3370
+ for (const entry of entries) {
3371
+ const rejection = validateSharedDirEntry(entry);
3372
+ if (rejection === null) continue;
3373
+ if (typeof entry === "string" && mayJoinRefusedEntry(entry, rejection.reason, PROBEABLE_REASONS)) {
3374
+ probable.push(entry);
3375
+ }
3376
+ addItem(
3377
+ section2,
3378
+ `${yellow(warnGlyph)} path-map: sharedDirs entry ${JSON.stringify(entry)} rejected: ${rejection.message}; skipping`
3379
+ );
3380
+ }
3381
+ reportRejectedLeftovers(section2, probable);
3382
+ }
3383
+ function reportRejectedLeftovers(section2, probable) {
3384
+ for (const entry of probable) {
3385
+ const link = classifyLocalLink(entry);
3386
+ if (link === "managed") {
3387
+ addItem(
3388
+ section2,
3389
+ `${yellow(warnGlyph)} path-map: entry ${JSON.stringify(entry)} is a symlink under ~/.claude/ pointing into shared/; copy the content out first (cp -RL), then remove both. nomad will not do it for you`
3390
+ );
3391
+ continue;
3392
+ }
3393
+ if (link === "dangling") {
3394
+ addItem(
3395
+ section2,
3396
+ `${yellow(warnGlyph)} path-map: entry ${JSON.stringify(entry)} is a DANGLING symlink under ~/.claude/ (its target does not resolve); do not delete anything under shared/ yet, since a copy there may be the only one left. nomad cannot restore this name, because the entry is refused: recover the content by hand if you need it, then remove the dead link`
3397
+ );
3398
+ continue;
3399
+ }
3400
+ if (link === "foreign") {
3401
+ addItem(
3402
+ section2,
3403
+ `${yellow(warnGlyph)} path-map: entry ${JSON.stringify(entry)} is a symlink under ~/.claude/ pointing OUTSIDE shared/; nomad does not manage it, so leave it alone and just drop the entry from sharedDirs`
3404
+ );
3405
+ }
3406
+ if (hasSharedLeftover(entry)) {
3407
+ addItem(
3408
+ section2,
3409
+ `${yellow(warnGlyph)} path-map: shared/ entry ${JSON.stringify(entry)} exists in the repo working tree; remove it by hand, nomad will not delete it`
3410
+ );
3411
+ }
3412
+ }
3413
+ }
3126
3414
  function reportPathCollisions(section2, map) {
3127
3415
  const seen = /* @__PURE__ */ new Map();
3128
3416
  let collisionCount = 0;
@@ -3154,6 +3442,7 @@ function reportPathMap(section2) {
3154
3442
  }
3155
3443
  const map = readJsonSafe(mapPath, mapPath, section2);
3156
3444
  if (map === null) return;
3445
+ reportRejectedSharedDirs(section2, map);
3157
3446
  const shapeError = validatePathMapShape(map);
3158
3447
  if (shapeError !== null) {
3159
3448
  addItem(section2, `${red(failGlyph)} ${shapeError}`);
@@ -3179,61 +3468,6 @@ init_color();
3179
3468
  import { existsSync as existsSync14 } from "node:fs";
3180
3469
  import { join as join16 } from "node:path";
3181
3470
  init_config();
3182
-
3183
- // src/extras-sync.diff.ts
3184
- init_utils();
3185
- import { execFileSync as execFileSync2 } from "node:child_process";
3186
- import { relative as relative3 } from "node:path";
3187
- function parseNameStatus(stdout) {
3188
- const fields = stdout.split("\0").filter((f) => f.length > 0);
3189
- const entries = [];
3190
- for (let i = 0; i + 1 < fields.length; i += 2) {
3191
- entries.push({ status: fields[i], path: fields[i + 1] });
3192
- }
3193
- return entries;
3194
- }
3195
- function labelEntry(entry) {
3196
- if (entry.status === "D") return `${entry.path} (local only)`;
3197
- if (entry.status === "A") return `${entry.path} (repo only)`;
3198
- return entry.path;
3199
- }
3200
- function parseDiffOutput(stdout) {
3201
- return parseNameStatus(stdout).map(labelEntry);
3202
- }
3203
- function parseModifiedPaths(stdout, a) {
3204
- return parseNameStatus(stdout).filter((entry) => entry.status === "M").map((entry) => relative3(a, entry.path));
3205
- }
3206
- function runNameStatusDiff(a, b, parse) {
3207
- try {
3208
- const stdout = execFileSync2(
3209
- "git",
3210
- ["diff", "--no-index", "--no-renames", "-z", "--name-status", a, b],
3211
- {
3212
- stdio: ["ignore", "pipe", "pipe"]
3213
- }
3214
- ).toString();
3215
- return parse(stdout);
3216
- } catch (err) {
3217
- const e = err;
3218
- if (e.status === 1 && e.stdout !== void 0) {
3219
- return parse(e.stdout.toString());
3220
- }
3221
- if (e.code === "ENOENT") {
3222
- warn(`git not on PATH; divergence check skipped for ${a}`);
3223
- return [];
3224
- }
3225
- warn(`divergence check failed for ${a}: ${e.message ?? String(err)}`);
3226
- return [];
3227
- }
3228
- }
3229
- function listDivergingFiles(a, b) {
3230
- return runNameStatusDiff(a, b, parseDiffOutput);
3231
- }
3232
- function listDivergingModified(a, b) {
3233
- return runNameStatusDiff(a, b, (stdout) => parseModifiedPaths(stdout, a));
3234
- }
3235
-
3236
- // src/commands.doctor.checks.skills.ts
3237
3471
  function stripSideIndicator(line) {
3238
3472
  if (line.endsWith(" (local only)")) return line.slice(0, -" (local only)".length);
3239
3473
  if (line.endsWith(" (repo only)")) return line.slice(0, -" (repo only)".length);
@@ -3241,15 +3475,15 @@ function stripSideIndicator(line) {
3241
3475
  }
3242
3476
  function isGsdDiffLine(line, localBase, sharedBase) {
3243
3477
  const bare = stripSideIndicator(line);
3244
- let relative9;
3478
+ let relative11;
3245
3479
  if (bare.startsWith(localBase + "/")) {
3246
- relative9 = bare.slice(localBase.length + 1);
3480
+ relative11 = bare.slice(localBase.length + 1);
3247
3481
  } else if (bare.startsWith(sharedBase + "/")) {
3248
- relative9 = bare.slice(sharedBase.length + 1);
3482
+ relative11 = bare.slice(sharedBase.length + 1);
3249
3483
  } else {
3250
- relative9 = bare;
3484
+ relative11 = bare;
3251
3485
  }
3252
- return relative9.split("/")[0].startsWith(GSD_PREFIX);
3486
+ return relative11.split("/")[0].startsWith(GSD_PREFIX);
3253
3487
  }
3254
3488
  function reportSkillsDivergence(section2) {
3255
3489
  const sharedSkills = join16(repoHome(), "shared", "skills");
@@ -3404,7 +3638,7 @@ function reportGitIdentity(section2) {
3404
3638
 
3405
3639
  // src/commands.doctor.checks.backups.ts
3406
3640
  init_color();
3407
- import { existsSync as existsSync18, lstatSync as lstatSync9, readdirSync as readdirSync8 } from "node:fs";
3641
+ import { existsSync as existsSync18, lstatSync as lstatSync10, readdirSync as readdirSync8 } from "node:fs";
3408
3642
  import { join as join21 } from "node:path";
3409
3643
  init_config();
3410
3644
  var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
@@ -3422,7 +3656,7 @@ function dirSizeBytes(dir) {
3422
3656
  let bytes = 0;
3423
3657
  for (const entry of safeReaddir(dir)) {
3424
3658
  const full = join21(dir, entry);
3425
- const st = lstatSync9(full, { throwIfNoEntry: false });
3659
+ const st = lstatSync10(full, { throwIfNoEntry: false });
3426
3660
  if (!st) continue;
3427
3661
  if (st.isSymbolicLink()) continue;
3428
3662
  if (st.isDirectory()) bytes += dirSizeBytes(full);
@@ -3901,8 +4135,8 @@ init_config();
3901
4135
 
3902
4136
  // src/remap.ts
3903
4137
  init_config_sharedDirs_guard();
3904
- import { cpSync as cpSync6, existsSync as existsSync21, lstatSync as lstatSync10, mkdirSync as mkdirSync7, readdirSync as readdirSync10, rmSync as rmSync11, statSync as statSync5 } from "node:fs";
3905
- import { dirname as dirname6, join as join28, relative as relative5, sep as sep4 } from "node:path";
4138
+ import { cpSync as cpSync6, existsSync as existsSync21, lstatSync as lstatSync11, mkdirSync as mkdirSync7, readdirSync as readdirSync10, rmSync as rmSync11, statSync as statSync5 } from "node:fs";
4139
+ import { dirname as dirname6, join as join28, relative as relative5, sep as sep5 } from "node:path";
3906
4140
  init_config();
3907
4141
 
3908
4142
  // src/push-manifest.ts
@@ -4034,7 +4268,7 @@ function copyFileAtomic(src, dst) {
4034
4268
  renameAtomicRetry(tmp, dst);
4035
4269
  }
4036
4270
  function hasDeltaForDir(sel, localDir) {
4037
- const prefix = `${localDir}${sep4}`;
4271
+ const prefix = `${localDir}${sep5}`;
4038
4272
  for (const p of sel.changed) if (p.startsWith(prefix)) return true;
4039
4273
  for (const p of sel.deleted) if (p.startsWith(prefix)) return true;
4040
4274
  return false;
@@ -4043,7 +4277,7 @@ function skipForSelection(sel, localDir) {
4043
4277
  return sel !== void 0 && !hasDeltaForDir(sel, localDir);
4044
4278
  }
4045
4279
  function applySelective(sel, localDir, repoDst) {
4046
- const prefix = `${localDir}${sep4}`;
4280
+ const prefix = `${localDir}${sep5}`;
4047
4281
  for (const src of sel.changed) {
4048
4282
  if (!src.startsWith(prefix)) continue;
4049
4283
  copyFileAtomic(src, join28(repoDst, relative5(localDir, src)));
@@ -4061,7 +4295,7 @@ function copyDirJsonlOnly(src, dst) {
4061
4295
  filter: (srcPath) => {
4062
4296
  const rel = relative5(src, srcPath);
4063
4297
  if (rel === "") return true;
4064
- if (rel.split(sep4).length > 1) return true;
4298
+ if (rel.split(sep5).length > 1) return true;
4065
4299
  if (statSync5(srcPath).isDirectory()) return true;
4066
4300
  if (srcPath.endsWith(".jsonl")) return true;
4067
4301
  item(`skip ${rel}: extension not in allowlist`);
@@ -4121,9 +4355,9 @@ function countLocalOnly(src, dst) {
4121
4355
  for (const name of readdirSync10(dst)) {
4122
4356
  const dstPath = join28(dst, name);
4123
4357
  const srcPath = join28(src, name);
4124
- if (lstatSync10(dstPath).isDirectory()) {
4358
+ if (lstatSync11(dstPath).isDirectory()) {
4125
4359
  count += countLocalOnly(srcPath, dstPath);
4126
- } else if (lstatSync10(srcPath, { throwIfNoEntry: false }) === void 0) {
4360
+ } else if (lstatSync11(srcPath, { throwIfNoEntry: false }) === void 0) {
4127
4361
  count++;
4128
4362
  }
4129
4363
  }
@@ -4313,7 +4547,7 @@ function reportCheckShared(section2, gitleaksReady) {
4313
4547
 
4314
4548
  // src/commands.doctor.checks.hooks.scope.ts
4315
4549
  init_color();
4316
- import { existsSync as existsSync23, readFileSync as readFileSync8, readdirSync as readdirSync12, realpathSync as realpathSync2 } from "node:fs";
4550
+ import { existsSync as existsSync23, readFileSync as readFileSync8, readdirSync as readdirSync12, realpathSync as realpathSync3 } from "node:fs";
4317
4551
  import { dirname as dirname7, extname, join as join30 } from "node:path";
4318
4552
  init_config();
4319
4553
  function typeFromPackageJson(pkgPath) {
@@ -4330,7 +4564,7 @@ function effectiveType(hookPath) {
4330
4564
  if (ext === ".cjs") return "cjs";
4331
4565
  let real;
4332
4566
  try {
4333
- real = realpathSync2(hookPath);
4567
+ real = realpathSync3(hookPath);
4334
4568
  } catch {
4335
4569
  return null;
4336
4570
  }
@@ -4492,7 +4726,7 @@ import { existsSync as existsSync26, readFileSync as readFileSync9 } from "node:
4492
4726
  import { join as join33 } from "node:path";
4493
4727
 
4494
4728
  // src/commands.doctor.checks.hooks.preserve-symlinks.probe.ts
4495
- import { closeSync as closeSync3, existsSync as existsSync25, openSync as openSync3, readSync, realpathSync as realpathSync3 } from "node:fs";
4729
+ import { closeSync as closeSync3, existsSync as existsSync25, openSync as openSync3, readSync, realpathSync as realpathSync4 } from "node:fs";
4496
4730
  import { dirname as dirname8, join as join32, resolve as resolve2 } from "node:path";
4497
4731
  function suppressedRanges(src) {
4498
4732
  const ranges = [];
@@ -4537,7 +4771,7 @@ function specifierIsMissing(specifier, baseDir) {
4537
4771
  function relativeRequireTargetsBroken(scriptPath) {
4538
4772
  let realPath;
4539
4773
  try {
4540
- realPath = realpathSync3(scriptPath);
4774
+ realPath = realpathSync4(scriptPath);
4541
4775
  } catch {
4542
4776
  return false;
4543
4777
  }
@@ -4681,7 +4915,7 @@ function reportPreserveSymlinksCheck(section2) {
4681
4915
  return;
4682
4916
  }
4683
4917
  const map = readPathMapSafe();
4684
- const sharedLinkNames = allSharedLinks(map);
4918
+ const sharedLinkNames = allSharedLinks(map, { quiet: true });
4685
4919
  let anyWarn = false;
4686
4920
  for (const [event, groups] of Object.entries(hooks)) {
4687
4921
  if (!Array.isArray(groups)) continue;
@@ -5031,21 +5265,21 @@ import { createInterface as createInterface2 } from "node:readline/promises";
5031
5265
  // src/commands.push.recovery.actions.ts
5032
5266
  init_config();
5033
5267
  import { readFileSync as readFileSync17 } from "node:fs";
5034
- import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep8 } from "node:path";
5268
+ import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep9 } from "node:path";
5035
5269
 
5036
5270
  // src/commands.push.recovery.memory.ts
5037
5271
  init_config();
5038
5272
  init_config_sharedDirs_guard();
5039
5273
  import {
5040
5274
  cpSync as cpSync7,
5041
- lstatSync as lstatSync11,
5275
+ lstatSync as lstatSync12,
5042
5276
  mkdirSync as mkdirSync9,
5043
5277
  readFileSync as readFileSync14,
5044
- realpathSync as realpathSync4,
5278
+ realpathSync as realpathSync5,
5045
5279
  statSync as statSync6,
5046
5280
  writeFileSync as writeFileSync6
5047
5281
  } from "node:fs";
5048
- import { dirname as dirname9, join as join36, sep as sep5 } from "node:path";
5282
+ import { dirname as dirname9, join as join36, sep as sep6 } from "node:path";
5049
5283
  init_push_gitleaks_scan();
5050
5284
 
5051
5285
  // src/rel-path-guard.ts
@@ -5076,12 +5310,12 @@ function resolveMemoryLocalPath(logical, relPath, map) {
5076
5310
  if (abs === void 0) return null;
5077
5311
  const memoryRoot = join36(claudeHome(), "projects", encodePath(abs), "memory");
5078
5312
  const localPath = join36(memoryRoot, ...relPath.split("/"));
5079
- if (localPath !== memoryRoot && !localPath.startsWith(memoryRoot + sep5)) return null;
5313
+ if (localPath !== memoryRoot && !localPath.startsWith(memoryRoot + sep6)) return null;
5080
5314
  try {
5081
- if (lstatSync11(localPath).isSymbolicLink()) return null;
5082
- const realLocal = realpathSync4(localPath);
5083
- const realRoot = realpathSync4(memoryRoot);
5084
- if (!realLocal.startsWith(realRoot + sep5)) return null;
5315
+ if (lstatSync12(localPath).isSymbolicLink()) return null;
5316
+ const realLocal = realpathSync5(localPath);
5317
+ const realRoot = realpathSync5(memoryRoot);
5318
+ if (!realLocal.startsWith(realRoot + sep6)) return null;
5085
5319
  if (!statSync6(realLocal).isFile()) return null;
5086
5320
  } catch {
5087
5321
  return null;
@@ -5139,28 +5373,28 @@ function applyMemoryRedact(f, ts, map, scan = scanFile) {
5139
5373
  init_config();
5140
5374
  import {
5141
5375
  cpSync as cpSync8,
5142
- lstatSync as lstatSync13,
5376
+ lstatSync as lstatSync14,
5143
5377
  mkdirSync as mkdirSync11,
5144
5378
  readFileSync as readFileSync15,
5145
- realpathSync as realpathSync5,
5379
+ realpathSync as realpathSync6,
5146
5380
  statSync as statSync7,
5147
5381
  writeFileSync as writeFileSync7
5148
5382
  } from "node:fs";
5149
- import { dirname as dirname10, join as join38, sep as sep6 } from "node:path";
5383
+ import { dirname as dirname10, join as join38, sep as sep7 } from "node:path";
5150
5384
 
5151
5385
  // src/skills-sync.ts
5152
5386
  init_config();
5153
- import { existsSync as existsSync29, lstatSync as lstatSync12, mkdirSync as mkdirSync10, readdirSync as readdirSync13, rmSync as rmSync13 } from "node:fs";
5387
+ import { existsSync as existsSync29, lstatSync as lstatSync13, mkdirSync as mkdirSync10, readdirSync as readdirSync13, rmSync as rmSync13 } from "node:fs";
5154
5388
  import { join as join37 } from "node:path";
5155
5389
 
5156
5390
  // src/skills-sync.tracked.ts
5157
5391
  init_utils();
5158
- import { basename as basename3 } from "node:path";
5392
+ import { basename as basename4 } from "node:path";
5159
5393
  function trackedRootSkillsAt(ref, repo) {
5160
5394
  try {
5161
5395
  const raw = gitCaptureRaw(["ls-tree", "--name-only", "-z", ref, "--", "shared/skills/"], repo);
5162
5396
  const entries = raw.split("\0").filter((entry) => entry !== "");
5163
- return new Set(entries.map((entry) => basename3(entry)));
5397
+ return new Set(entries.map((entry) => basename4(entry)));
5164
5398
  } catch {
5165
5399
  return /* @__PURE__ */ new Set();
5166
5400
  }
@@ -5190,7 +5424,7 @@ function syncSkillsPull(ts, prePostHeads) {
5190
5424
  if (!existsSync29(sharedSkills)) return;
5191
5425
  const localSkills = join37(claudeHome(), "skills");
5192
5426
  backupBeforeWrite(localSkills, ts);
5193
- const dstStat = lstatSync12(localSkills, { throwIfNoEntry: false });
5427
+ const dstStat = lstatSync13(localSkills, { throwIfNoEntry: false });
5194
5428
  if (dstStat?.isSymbolicLink() === true) {
5195
5429
  rmSync13(localSkills, { recursive: true, force: true });
5196
5430
  mkdirSync10(localSkills, { recursive: true });
@@ -5200,7 +5434,7 @@ function syncSkillsPull(ts, prePostHeads) {
5200
5434
  }
5201
5435
  function syncSkillsPush() {
5202
5436
  const localSkills = join37(claudeHome(), "skills");
5203
- const stat = lstatSync12(localSkills, { throwIfNoEntry: false });
5437
+ const stat = lstatSync13(localSkills, { throwIfNoEntry: false });
5204
5438
  if (stat === void 0) return;
5205
5439
  if (stat.isSymbolicLink()) return;
5206
5440
  const sharedSkills = join37(repoHome(), "shared", "skills");
@@ -5229,12 +5463,12 @@ function resolveSkillLocalPath(name, relPath) {
5229
5463
  const skillsRoot = join38(claudeHome(), "skills");
5230
5464
  const skillRoot = join38(skillsRoot, name);
5231
5465
  const localPath = join38(skillRoot, ...relPath.split("/"));
5232
- if (localPath !== skillRoot && !localPath.startsWith(skillRoot + sep6)) return null;
5466
+ if (localPath !== skillRoot && !localPath.startsWith(skillRoot + sep7)) return null;
5233
5467
  try {
5234
- if (lstatSync13(localPath).isSymbolicLink()) return null;
5235
- const realLocal = realpathSync5(localPath);
5236
- const realRoot = realpathSync5(skillsRoot);
5237
- if (!realLocal.startsWith(realRoot + sep6)) return null;
5468
+ if (lstatSync14(localPath).isSymbolicLink()) return null;
5469
+ const realLocal = realpathSync6(localPath);
5470
+ const realRoot = realpathSync6(skillsRoot);
5471
+ if (!realLocal.startsWith(realRoot + sep7)) return null;
5238
5472
  if (!statSync7(realLocal).isFile()) return null;
5239
5473
  } catch {
5240
5474
  return null;
@@ -5359,7 +5593,7 @@ function applyDeferredAllows(findings, state, repo) {
5359
5593
  init_config();
5360
5594
  init_config_sharedDirs_guard();
5361
5595
  import { cpSync as cpSync9, existsSync as existsSync32, mkdirSync as mkdirSync12 } from "node:fs";
5362
- import { dirname as dirname12, join as join41, sep as sep7 } from "node:path";
5596
+ import { dirname as dirname12, join as join41, sep as sep8 } from "node:path";
5363
5597
 
5364
5598
  // src/commands.redact.ts
5365
5599
  init_config();
@@ -5367,17 +5601,17 @@ import { existsSync as existsSync31 } from "node:fs";
5367
5601
  import { dirname as dirname11, join as join40 } from "node:path";
5368
5602
 
5369
5603
  // src/commands.redact.subtree.ts
5370
- import { existsSync as existsSync30, lstatSync as lstatSync14, readFileSync as readFileSync16, readdirSync as readdirSync14, statSync as statSync8, writeFileSync as writeFileSync8 } from "node:fs";
5604
+ import { existsSync as existsSync30, lstatSync as lstatSync15, readFileSync as readFileSync16, readdirSync as readdirSync14, statSync as statSync8, writeFileSync as writeFileSync8 } from "node:fs";
5371
5605
  import { join as join39 } from "node:path";
5372
5606
  init_utils_fs();
5373
5607
  init_utils();
5374
5608
  function collectFiles(dir, out) {
5375
5609
  if (!existsSync30(dir)) return;
5376
- const st = lstatSync14(dir);
5610
+ const st = lstatSync15(dir);
5377
5611
  if (!st.isDirectory()) return;
5378
5612
  for (const entry of readdirSync14(dir)) {
5379
5613
  const abs = join39(dir, entry);
5380
- const lst = lstatSync14(abs);
5614
+ const lst = lstatSync15(abs);
5381
5615
  if (lst.isSymbolicLink()) continue;
5382
5616
  if (lst.isDirectory()) {
5383
5617
  collectFiles(abs, out);
@@ -5585,7 +5819,7 @@ function resolveStagedDir(localPath, map, claude, repo) {
5585
5819
  assertSafeLogical(logical);
5586
5820
  const abs = hostMap[HOST];
5587
5821
  if (abs === void 0) continue;
5588
- if (localPath.startsWith(join41(claude, "projects", encodePath(abs)) + sep7)) {
5822
+ if (localPath.startsWith(join41(claude, "projects", encodePath(abs)) + sep8)) {
5589
5823
  return join41(repo, "shared", "projects", logical);
5590
5824
  }
5591
5825
  }
@@ -5876,8 +6110,8 @@ function sessionSuffix(finding) {
5876
6110
  const sid = sessionIdFromFinding(finding);
5877
6111
  if (sid === null) return "";
5878
6112
  const rawBasename = finding.File.replace(/^.*\//, "");
5879
- const basename4 = rawBasename.endsWith(".jsonl") ? rawBasename.slice(0, -".jsonl".length) : rawBasename;
5880
- return basename4 === sid ? "" : ` (session: ${sid})`;
6113
+ const basename5 = rawBasename.endsWith(".jsonl") ? rawBasename.slice(0, -".jsonl".length) : rawBasename;
6114
+ return basename5 === sid ? "" : ` (session: ${sid})`;
5881
6115
  }
5882
6116
  function renderFindingBlock(finding, readLine, siblings = []) {
5883
6117
  const lines = [` file: ${finding.File}:${finding.StartLine}${sessionSuffix(finding)}`];
@@ -5949,7 +6183,7 @@ function makeDefaultReadLine(repo) {
5949
6183
  try {
5950
6184
  const repoRoot = resolve3(repo);
5951
6185
  const target = resolve3(repoRoot, file);
5952
- if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep8)) {
6186
+ if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep9)) {
5953
6187
  return null;
5954
6188
  }
5955
6189
  let lines = cache.get(target);
@@ -6843,8 +7077,8 @@ function unstageOne(rel, repo) {
6843
7077
 
6844
7078
  // src/commands.pull.ts
6845
7079
  init_autostash_guard();
6846
- import { existsSync as existsSync42, mkdirSync as mkdirSync14 } from "node:fs";
6847
- import { join as join53 } from "node:path";
7080
+ import { existsSync as existsSync46, mkdirSync as mkdirSync14 } from "node:fs";
7081
+ import { join as join58 } from "node:path";
6848
7082
 
6849
7083
  // src/commands.push.sections.ts
6850
7084
  init_color();
@@ -6959,11 +7193,11 @@ init_utils_json();
6959
7193
 
6960
7194
  // src/extras-sync.remap.ts
6961
7195
  init_config();
6962
- import { existsSync as existsSync38, mkdirSync as mkdirSync13, readdirSync as readdirSync16, readFileSync as readFileSync19, realpathSync as realpathSync6, rmSync as rmSync16 } from "node:fs";
6963
- import { dirname as dirname13, join as join49, sep as sep10 } from "node:path";
7196
+ import { existsSync as existsSync38, mkdirSync as mkdirSync13, readdirSync as readdirSync16, readFileSync as readFileSync19, realpathSync as realpathSync7, rmSync as rmSync16 } from "node:fs";
7197
+ import { dirname as dirname13, join as join49, sep as sep11 } from "node:path";
6964
7198
 
6965
7199
  // src/extras-sync.planning-diff.ts
6966
- import { join as join48, normalize as normalize2, sep as sep9 } from "node:path";
7200
+ import { join as join48, normalize as normalize2, sep as sep10 } from "node:path";
6967
7201
  init_utils();
6968
7202
  function processRecord(fields, i, changed, deleted) {
6969
7203
  const status = fields[i];
@@ -7016,7 +7250,7 @@ function planningDeleteTargets(opts) {
7016
7250
  const logicalPrefix = "shared/extras/" + logical + "/";
7017
7251
  const prefix = logicalPrefix + ".planning/";
7018
7252
  const planningRoot = join48(localRoot, ".planning");
7019
- const planningRootBoundary = planningRoot + sep9;
7253
+ const planningRootBoundary = planningRoot + sep10;
7020
7254
  const targets = [];
7021
7255
  for (const repoPath of deleted) {
7022
7256
  if (!repoPath.startsWith(prefix)) {
@@ -7058,7 +7292,7 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
7058
7292
  }
7059
7293
  function pruneEmptyAncestors(target, planningRoot) {
7060
7294
  let dir = dirname13(target);
7061
- while (dir !== planningRoot && dir.startsWith(planningRoot + sep10)) {
7295
+ while (dir !== planningRoot && dir.startsWith(planningRoot + sep11)) {
7062
7296
  try {
7063
7297
  if (readdirSync16(dir).length > 0) break;
7064
7298
  rmSync16(dir, { recursive: true, force: true });
@@ -7070,13 +7304,13 @@ function pruneEmptyAncestors(target, planningRoot) {
7070
7304
  }
7071
7305
  function tryRealpath(dir) {
7072
7306
  try {
7073
- return realpathSync6(dir);
7307
+ return realpathSync7(dir);
7074
7308
  } catch {
7075
7309
  return void 0;
7076
7310
  }
7077
7311
  }
7078
7312
  function isInsidePlanningRoot(parentReal, rootReal) {
7079
- return parentReal === rootReal || parentReal.startsWith(rootReal + sep10);
7313
+ return parentReal === rootReal || parentReal.startsWith(rootReal + sep11);
7080
7314
  }
7081
7315
  function deletePlanningTarget(target, planningRoot, repoCounterpart) {
7082
7316
  if (existsSync38(repoCounterpart)) return;
@@ -7103,11 +7337,11 @@ function planningDiffArgs(pre, post, logical) {
7103
7337
  function deletePairsFor(t, raw) {
7104
7338
  return planningDeleteTargets({ raw, logical: t.logical, localRoot: t.localRoot }).map(
7105
7339
  (target) => {
7106
- const relToLocal = target.slice(t.localRoot.length + sep10.length);
7340
+ const relToLocal = target.slice(t.localRoot.length + sep11.length);
7107
7341
  return {
7108
7342
  target,
7109
7343
  relToLocal,
7110
- repoRel: `shared/extras/${t.logical}/${relToLocal.split(sep10).join("/")}`
7344
+ repoRel: `shared/extras/${t.logical}/${relToLocal.split(sep11).join("/")}`
7111
7345
  };
7112
7346
  }
7113
7347
  );
@@ -7279,10 +7513,204 @@ function divergenceCheckExtras(ts, prePostHeads) {
7279
7513
  return divergedCount;
7280
7514
  }
7281
7515
 
7282
- // src/preview.ts
7516
+ // src/links.baseline.ts
7517
+ init_config();
7518
+ import { lstatSync as lstatSync16, readdirSync as readdirSync17 } from "node:fs";
7519
+ import { join as join51, relative as relative7, sep as sep12 } from "node:path";
7520
+ init_utils();
7521
+ var SHARED_BASELINE_KIND = "shared-links-baseline/1";
7522
+ var SHARED_BASELINE_CONFIG_HASH = "not-applicable";
7523
+ function readSharedBaseline() {
7524
+ const parsed = readManifest(sharedBaselinePath());
7525
+ if (parsed === null) return null;
7526
+ if (parsed.scannerVersion !== SHARED_BASELINE_KIND) return null;
7527
+ return parsed;
7528
+ }
7529
+ function baselineKey(claude, abs) {
7530
+ return relative7(claude, abs).split(sep12).join("/");
7531
+ }
7532
+ function addLocalPath(abs, claude, scan) {
7533
+ const key = baselineKey(claude, abs);
7534
+ let st;
7535
+ try {
7536
+ st = lstatSync16(abs, { throwIfNoEntry: false });
7537
+ } catch {
7538
+ scan.declined.push(key);
7539
+ return;
7540
+ }
7541
+ if (st === void 0) return;
7542
+ if (st.isSymbolicLink()) {
7543
+ scan.declined.push(key);
7544
+ return;
7545
+ }
7546
+ if (st.isDirectory()) {
7547
+ collectSharedFiles(abs, claude, scan);
7548
+ return;
7549
+ }
7550
+ scan.files[key] = { size: st.size, mtime: st.mtimeMs };
7551
+ }
7552
+ function collectSharedFiles(dir, claude, scan) {
7553
+ let entries;
7554
+ try {
7555
+ entries = readdirSync17(dir);
7556
+ } catch {
7557
+ scan.declined.push(baselineKey(claude, dir));
7558
+ return;
7559
+ }
7560
+ for (const entry of entries) {
7561
+ const abs = join51(dir, entry);
7562
+ if (isDeniedName(ALWAYS_NEVER_SYNC, entry)) {
7563
+ scan.declined.push(baselineKey(claude, abs));
7564
+ continue;
7565
+ }
7566
+ addLocalPath(abs, claude, scan);
7567
+ }
7568
+ }
7569
+ function enumerateLocalSharedScan(map) {
7570
+ const claude = claudeHome();
7571
+ const scan = { files: {}, declined: [] };
7572
+ for (const name of allSharedLinks(map)) {
7573
+ if (isDeniedName(ALWAYS_NEVER_SYNC, name)) {
7574
+ scan.declined.push(name);
7575
+ continue;
7576
+ }
7577
+ addLocalPath(join51(claude, name), claude, scan);
7578
+ }
7579
+ return scan;
7580
+ }
7581
+ function enumerateLocalSharedFiles(map) {
7582
+ return enumerateLocalSharedScan(map).files;
7583
+ }
7584
+ function buildSharedBaseline(map) {
7585
+ const claude = claudeHome();
7586
+ const files = {};
7587
+ for (const [key, st] of Object.entries(enumerateLocalSharedFiles(map))) {
7588
+ try {
7589
+ files[key] = { size: st.size, mtime: st.mtime, hash: hashFile(join51(claude, key)) };
7590
+ } catch {
7591
+ }
7592
+ }
7593
+ return buildManifest(files, SHARED_BASELINE_KIND, SHARED_BASELINE_CONFIG_HASH);
7594
+ }
7595
+ function writeSharedBaseline(map) {
7596
+ if (process.platform !== "win32") return;
7597
+ try {
7598
+ writeManifest(sharedBaselinePath(), buildSharedBaseline(map));
7599
+ } catch (err) {
7600
+ warn(`could not record the shared-config baseline: ${err.message}`);
7601
+ }
7602
+ }
7603
+
7604
+ // src/commands.pull.win32.ts
7283
7605
  init_config();
7284
- import { existsSync as existsSync41 } from "node:fs";
7606
+ import { existsSync as existsSync44 } from "node:fs";
7607
+ import { join as join56 } from "node:path";
7608
+
7609
+ // src/git-probe.ts
7610
+ import { execFileSync as execFileSync21 } from "node:child_process";
7611
+ var PROBE_TIMEOUT_MS4 = 1e4;
7612
+ var PROBE_MAX_BUFFER = 64 * 1024 * 1024;
7613
+ function gitProbe(args, repo) {
7614
+ try {
7615
+ return execFileSync21("git", args, {
7616
+ cwd: repo,
7617
+ stdio: ["ignore", "pipe", "pipe"],
7618
+ timeout: PROBE_TIMEOUT_MS4,
7619
+ maxBuffer: PROBE_MAX_BUFFER
7620
+ }).toString();
7621
+ } catch {
7622
+ return null;
7623
+ }
7624
+ }
7625
+
7626
+ // src/links.captures.ts
7627
+ init_config();
7628
+ import { existsSync as existsSync40, lstatSync as lstatSync17 } from "node:fs";
7285
7629
  import { join as join52 } from "node:path";
7630
+ function planSharedLinkCaptures(map) {
7631
+ if (process.platform !== "win32") return [];
7632
+ if (map === null) return [];
7633
+ const claude = claudeHome();
7634
+ const repo = repoHome();
7635
+ const plan = [];
7636
+ for (const name of allSharedLinks(map)) {
7637
+ const localPath = join52(claude, name);
7638
+ let stat;
7639
+ try {
7640
+ stat = lstatSync17(localPath, { throwIfNoEntry: false });
7641
+ } catch {
7642
+ continue;
7643
+ }
7644
+ if (stat === void 0) continue;
7645
+ if (stat.isSymbolicLink()) continue;
7646
+ const repoPath = join52(repo, "shared", name);
7647
+ if (!existsSync40(repoPath)) continue;
7648
+ plan.push({ name, localPath, repoPath });
7649
+ }
7650
+ return plan;
7651
+ }
7652
+
7653
+ // src/links.deletions.ts
7654
+ init_config();
7655
+ import { existsSync as existsSync41, lstatSync as lstatSync18, rmSync as rmSync17 } from "node:fs";
7656
+ import { join as join53, relative as relative8, resolve as resolve4, sep as sep13 } from "node:path";
7657
+ init_utils();
7658
+ init_utils_fs();
7659
+ function isUnknown(declined, key) {
7660
+ return declined.some((prefix) => key === prefix || key.startsWith(`${prefix}/`));
7661
+ }
7662
+ function deletionFor(key, names, claude, sharedRoot) {
7663
+ const segments = key.split("/");
7664
+ const name = segments[0];
7665
+ if (!names.has(name)) return null;
7666
+ if (!existsSync41(join53(claude, name))) return null;
7667
+ if (segments.some((segment) => isDeniedName(ALWAYS_NEVER_SYNC, segment))) return null;
7668
+ const repoPath = resolve4(sharedRoot, key);
7669
+ const rel = relative8(join53(sharedRoot, name), repoPath);
7670
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep13}`)) return null;
7671
+ if (segments.some((segment) => segment === "" || segment === "." || segment === ".."))
7672
+ return null;
7673
+ if (!existsSync41(repoPath)) return null;
7674
+ return { name, localPath: join53(claude, key), repoPath };
7675
+ }
7676
+ function planSharedLinkDeletions(map) {
7677
+ if (process.platform !== "win32") return [];
7678
+ if (map === null) return [];
7679
+ const baseline = readSharedBaseline();
7680
+ if (baseline === null) return [];
7681
+ const claude = claudeHome();
7682
+ const sharedRoot = join53(repoHome(), "shared");
7683
+ const scan = enumerateLocalSharedScan(map);
7684
+ const present = new Set(Object.keys(scan.files).map((key) => key.toLowerCase()));
7685
+ const declined = scan.declined.map((path) => path.toLowerCase());
7686
+ const names = new Set(allSharedLinks(map));
7687
+ const plan = [];
7688
+ for (const key of Object.keys(baseline.files)) {
7689
+ const folded = key.toLowerCase();
7690
+ if (present.has(folded)) continue;
7691
+ if (isUnknown(declined, folded)) continue;
7692
+ const entry = deletionFor(key, names, claude, sharedRoot);
7693
+ if (entry !== null) plan.push(entry);
7694
+ }
7695
+ return plan;
7696
+ }
7697
+ function applySharedLinkDeletions(map, ts) {
7698
+ const repo = repoHome();
7699
+ for (const entry of planSharedLinkDeletions(map)) {
7700
+ try {
7701
+ if (!lstatSync18(entry.repoPath).isFile()) continue;
7702
+ backupRepoWrite(entry.repoPath, ts, repo);
7703
+ rmSync17(entry.repoPath, { force: true });
7704
+ } catch (err) {
7705
+ warn(`could not remove ${entry.repoPath}: ${err.message}`);
7706
+ }
7707
+ }
7708
+ }
7709
+
7710
+ // src/preview.ts
7711
+ init_config();
7712
+ import { existsSync as existsSync43 } from "node:fs";
7713
+ import { join as join55 } from "node:path";
7286
7714
 
7287
7715
  // node_modules/diff/libesm/diff/base.js
7288
7716
  var Diff = class {
@@ -7557,16 +7985,16 @@ function diffLinesToUnified(oldStr, newStr) {
7557
7985
 
7558
7986
  // src/preview.skills.ts
7559
7987
  init_config();
7560
- import { existsSync as existsSync40, readdirSync as readdirSync17 } from "node:fs";
7561
- import { join as join51 } from "node:path";
7988
+ import { existsSync as existsSync42, readdirSync as readdirSync18 } from "node:fs";
7989
+ import { join as join54 } from "node:path";
7562
7990
  function buildSkillsPreviewSection() {
7563
7991
  const s = section("Skills");
7564
- const sharedSkills = join51(repoHome(), "shared", "skills");
7565
- if (!existsSync40(sharedSkills)) return s;
7566
- const sharedNames = readdirSync17(sharedSkills, { encoding: "utf8" }).filter((name) => !isSkillExcluded(name)).sort((a, b) => a.localeCompare(b, "en"));
7992
+ const sharedSkills = join54(repoHome(), "shared", "skills");
7993
+ if (!existsSync42(sharedSkills)) return s;
7994
+ const sharedNames = readdirSync18(sharedSkills, { encoding: "utf8" }).filter((name) => !isSkillExcluded(name)).sort((a, b) => a.localeCompare(b, "en"));
7567
7995
  for (const name of sharedNames) addItem(s, name);
7568
- const localSkills = join51(claudeHome(), "skills");
7569
- const localOnly = existsSync40(localSkills) ? readdirSync17(localSkills, { encoding: "utf8" }).filter(
7996
+ const localSkills = join54(claudeHome(), "skills");
7997
+ const localOnly = existsSync42(localSkills) ? readdirSync18(localSkills, { encoding: "utf8" }).filter(
7570
7998
  (name) => !isSkillExcluded(name) && !sharedNames.includes(name)
7571
7999
  ).length : 0;
7572
8000
  if (localOnly > 0) {
@@ -7588,7 +8016,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
7588
8016
  return lines.join("\n");
7589
8017
  }
7590
8018
  function readJsonOrNull(path) {
7591
- if (!existsSync41(path)) return null;
8019
+ if (!existsSync43(path)) return null;
7592
8020
  try {
7593
8021
  return readJson(path);
7594
8022
  } catch {
@@ -7602,12 +8030,12 @@ function previewSettings(basePath, hostPath, settingsPath) {
7602
8030
  }
7603
8031
  const notes = [];
7604
8032
  const hostOverrides = readJsonOrNull(hostPath);
7605
- if (hostOverrides === null && existsSync41(hostPath)) {
8033
+ if (hostOverrides === null && existsSync43(hostPath)) {
7606
8034
  notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
7607
8035
  }
7608
8036
  const merged = stripGsdHookEntries(deepMerge(base, hostOverrides ?? {}));
7609
8037
  const current = readJsonOrNull(settingsPath);
7610
- if (current === null && existsSync41(settingsPath)) {
8038
+ if (current === null && existsSync43(settingsPath)) {
7611
8039
  return { diff: "", notes: [...notes, "malformed; skipping diff"] };
7612
8040
  }
7613
8041
  const strippedCurrent = stripGsdHookEntries(current ?? {});
@@ -7623,6 +8051,12 @@ function formatLinkRow(e) {
7623
8051
  if (e.kind === "copy") return `would copy ${e.from} -> ${e.to}`;
7624
8052
  return `${e.kind} ${e.from} -> ${e.to}`;
7625
8053
  }
8054
+ function formatCaptureRow(c) {
8055
+ return `would capture ${c.localPath} -> ${c.repoPath}`;
8056
+ }
8057
+ function formatDeletionRow(d) {
8058
+ return `would remove ${d.repoPath} (gone from ${d.localPath})`;
8059
+ }
7626
8060
  function formatSessionRow(e) {
7627
8061
  return e.kind === "overwrite" ? `overwrite ${e.dst} (from ${e.src})` : e.text;
7628
8062
  }
@@ -7638,20 +8072,30 @@ function buildSettingsSectionForPreview(result) {
7638
8072
  }
7639
8073
  return s;
7640
8074
  }
7641
- function computePreview(ts, map, verb = "pull") {
8075
+ function computePreview(ts, map, verb = "pull", plans) {
7642
8076
  const repo = repoHome();
7643
8077
  const claude = claudeHome();
7644
8078
  console.log(`would pull on host=${HOST} (preview; nothing applied)`);
7645
8079
  console.log("");
7646
8080
  const links = section("Symlinks");
8081
+ const { captures, deletions } = plans ?? {
8082
+ captures: planSharedLinkCaptures(map),
8083
+ deletions: planSharedLinkDeletions(map)
8084
+ };
8085
+ for (const capture of captures) {
8086
+ addItem(links, formatCaptureRow(capture));
8087
+ }
8088
+ for (const deletion of deletions) {
8089
+ addItem(links, formatDeletionRow(deletion));
8090
+ }
7647
8091
  applySharedLinks(ts, map, {
7648
8092
  dryRun: true,
7649
8093
  onPreview: (e) => addItem(links, formatLinkRow(e))
7650
8094
  });
7651
8095
  const settingsResult = previewSettings(
7652
- join52(repo, "shared", "settings.base.json"),
7653
- join52(repo, "hosts", `${HOST}.json`),
7654
- join52(claude, "settings.json")
8096
+ join55(repo, "shared", "settings.base.json"),
8097
+ join55(repo, "hosts", `${HOST}.json`),
8098
+ join55(claude, "settings.json")
7655
8099
  );
7656
8100
  const settingsSection = buildSettingsSectionForPreview(settingsResult);
7657
8101
  const sessions = section("Sessions");
@@ -7667,7 +8111,7 @@ function computePreview(ts, map, verb = "pull") {
7667
8111
  const extras = section("Extras");
7668
8112
  let extrasSkipped = 0;
7669
8113
  let extrasUnmapped = 0;
7670
- if (existsSync41(join52(repo, "path-map.json")) && existsSync41(join52(repo, "shared", "extras"))) {
8114
+ if (existsSync43(join55(repo, "path-map.json")) && existsSync43(join55(repo, "shared", "extras"))) {
7671
8115
  const extrasResult = remapExtrasPull(ts, { dryRun: true });
7672
8116
  for (const entry of extrasResult.wouldPull) {
7673
8117
  addItem(extras, entry);
@@ -7684,6 +8128,135 @@ function computePreview(ts, map, verb = "pull") {
7684
8128
  return { unmapped: remapResult.unmapped, collisions: 0, localOnly };
7685
8129
  }
7686
8130
 
8131
+ // src/commands.pull.win32.ts
8132
+ init_utils();
8133
+ init_utils_json();
8134
+ function readMapForMirror(mapPath) {
8135
+ if (!existsSync44(mapPath)) return { projects: {} };
8136
+ try {
8137
+ return readPathMap(mapPath);
8138
+ } catch {
8139
+ return null;
8140
+ }
8141
+ }
8142
+ function untrackedUnderShared(repo) {
8143
+ const out = gitProbe(["ls-files", "--others", "--exclude-standard", "-z", "--", "shared/"], repo);
8144
+ if (out === null) return null;
8145
+ return new Set(out.split("\0").filter((p) => p !== ""));
8146
+ }
8147
+ function newlyUntracked(before, after) {
8148
+ if (before === null || after === null) return [];
8149
+ return [...after].filter((p) => !before.has(p));
8150
+ }
8151
+ function reconcileSharedLinksBeforePull(repo, ts) {
8152
+ if (process.platform !== "win32") return [];
8153
+ const before = untrackedUnderShared(repo);
8154
+ try {
8155
+ const map = readMapForMirror(join56(repo, "path-map.json"));
8156
+ stageLocalSharedEdits(map, ts);
8157
+ applySharedLinkDeletions(map, ts);
8158
+ } catch (err) {
8159
+ warn(`could not reconcile local shared edits before the pull: ${err.message}`);
8160
+ }
8161
+ return newlyUntracked(before, untrackedUnderShared(repo));
8162
+ }
8163
+ function planSharedReconcileBeforePull(repo) {
8164
+ const map = readMapForMirror(join56(repo, "path-map.json"));
8165
+ return { captures: planSharedLinkCaptures(map), deletions: planSharedLinkDeletions(map) };
8166
+ }
8167
+
8168
+ // src/commands.pull.collision.ts
8169
+ init_config();
8170
+ init_exit_codes();
8171
+ import { existsSync as existsSync45, lstatSync as lstatSync19, readFileSync as readFileSync20, rmSync as rmSync18 } from "node:fs";
8172
+ import { join as join57 } from "node:path";
8173
+ init_utils();
8174
+ var SHARED_PREFIX = /^shared\//;
8175
+ var EXTENSION = /(\.[^.]+)?$/;
8176
+ function localOriginal(repoRel) {
8177
+ return `~/.claude/${repoRel.replace(SHARED_PREFIX, "")}`;
8178
+ }
8179
+ function renameSuggestion(localPath) {
8180
+ const base = localPath.slice(localPath.lastIndexOf("/") + 1);
8181
+ return base.replace(EXTENSION, ".local$1");
8182
+ }
8183
+ function collisionSteps(locals) {
8184
+ const single = locals.length === 1;
8185
+ const target = single ? locals[0] : "each file listed above";
8186
+ const rename = single ? renameSuggestion(locals[0]) : "mine.md becomes mine.local.md";
8187
+ const combine = single ? "combine the two files" : "combine each pair";
8188
+ return `Keep the incoming version (yours is set aside, not deleted):
8189
+ 1. move ${target} outside ~/.claude/
8190
+ 2. nomad pull
8191
+
8192
+ Keep both and merge them yourself:
8193
+ 1. rename ${target} to a name the repo does not use (${rename})
8194
+ 2. nomad pull (brings in the other machine's version)
8195
+ 3. ${combine}, then nomad push`;
8196
+ }
8197
+ function collisionAdvice(single, cleared) {
8198
+ const copies = single ? "nomad's copy" : "nomad's copies";
8199
+ const them = single ? "it" : "them";
8200
+ const fate = cleared ? "which nomad has now removed for you" : `which nomad could not remove (the warning above says why), so delete ${them} by hand if the next pull stops here again`;
8201
+ const moving = single ? "Moving only that copy would not have cleared this anyway: the next pull re-copies your local file over it before fetching. The file to move is the one under ~/.claude/." : "Moving only those copies would not have cleared this anyway: the next pull re-copies your local files over them before fetching. The files to move are the ones under ~/.claude/.";
8202
+ return `Git's advice above refers to ${copies} inside the sync repo, ${fate}. ${moving}`;
8203
+ }
8204
+ function untrackedCollisionRunbookText(repoRelPaths, cleared) {
8205
+ const locals = repoRelPaths.map(localOriginal);
8206
+ const single = locals.length === 1;
8207
+ const claim = single ? "One of those copies has the same name as a file the incoming update also adds:" : "Each of the copies below has the same name as a file the incoming update also adds:";
8208
+ const intact = single ? "Your file is still exactly as you left it." : "Your files are still exactly as you left them.";
8209
+ const listed = locals.map((p) => ` ${p}`).join("\n");
8210
+ return `nomad pull could not fetch. Your machine had unpublished shared-config edits, so nomad copied them into the sync repo first, the way an edit on macOS or Linux is already in the repo before a pull starts. ${claim}
8211
+
8212
+ ${listed}
8213
+
8214
+ Nothing changed. Your ~/.claude/ config is untouched, the update did not land, and nothing was published. ${intact}
8215
+
8216
+ ${collisionAdvice(single, cleared)}
8217
+
8218
+ ` + collisionSteps(locals);
8219
+ }
8220
+ function addedByIncomingUpdate(repo, created) {
8221
+ return created.filter((rel) => gitProbe(["cat-file", "-e", `FETCH_HEAD:${rel}`], repo) !== null);
8222
+ }
8223
+ function isContainedMirrorPath(rel) {
8224
+ return rel.startsWith("shared/") && isSafeRelPath(rel);
8225
+ }
8226
+ function matchesLocalOriginal(repo, rel) {
8227
+ try {
8228
+ const local = join57(claudeHome(), rel.replace(SHARED_PREFIX, ""));
8229
+ return readFileSync20(join57(repo, rel)).equals(readFileSync20(local));
8230
+ } catch {
8231
+ return false;
8232
+ }
8233
+ }
8234
+ function removeMirroredCopies(repo, repoRelPaths) {
8235
+ for (const rel of repoRelPaths.filter(isContainedMirrorPath)) {
8236
+ const abs = join57(repo, rel);
8237
+ try {
8238
+ if (lstatSync19(abs, { throwIfNoEntry: false })?.isFile() !== true) continue;
8239
+ if (!matchesLocalOriginal(repo, rel)) continue;
8240
+ rmSync18(abs, { force: true });
8241
+ } catch (err) {
8242
+ warn(`could not remove nomad's copy at ${abs}: ${err.message}`);
8243
+ }
8244
+ }
8245
+ return repoRelPaths.every((rel) => !existsSync45(join57(repo, rel)));
8246
+ }
8247
+ function pullWithCollisionRunbook(repo, mirrored) {
8248
+ try {
8249
+ gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
8250
+ } catch (err) {
8251
+ const colliding = addedByIncomingUpdate(repo, mirrored);
8252
+ if (colliding.length === 0) throw err;
8253
+ const cleared = removeMirroredCopies(repo, colliding);
8254
+ throw new NomadFatal(untrackedCollisionRunbookText(colliding, cleared), {
8255
+ code: EXIT.GENERIC_FAILURE
8256
+ });
8257
+ }
8258
+ }
8259
+
7687
8260
  // src/commands.pull.ts
7688
8261
  init_commands_pull_wedge();
7689
8262
 
@@ -7694,9 +8267,9 @@ init_utils_fs();
7694
8267
 
7695
8268
  // src/commands.pull.recovery.git.ts
7696
8269
  init_utils();
7697
- import { execFileSync as execFileSync21 } from "node:child_process";
8270
+ import { execFileSync as execFileSync22 } from "node:child_process";
7698
8271
  function gitCapture(args, cwd) {
7699
- return execFileSync21("git", args, {
8272
+ return execFileSync22("git", args, {
7700
8273
  cwd,
7701
8274
  stdio: ["ignore", "pipe", "pipe"],
7702
8275
  maxBuffer: 64 * 1024 * 1024
@@ -7849,6 +8422,7 @@ function capturePrePostHeads(repo, rebase) {
7849
8422
  }
7850
8423
  function buildWetPullSections(ts, map, prePostHeads) {
7851
8424
  applySharedLinks(ts, map);
8425
+ writeSharedBaseline(map);
7852
8426
  const { label } = regenerateSettings(ts);
7853
8427
  syncSkillsPull(ts, prePostHeads);
7854
8428
  const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
@@ -7888,22 +8462,6 @@ function handleWedge(repo, forceRemote) {
7888
8462
  const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
7889
8463
  die(wedgeMarkerRunbookText(state), { code: EXIT.CONFLICT });
7890
8464
  }
7891
- function readMapForMirror(mapPath) {
7892
- if (!existsSync42(mapPath)) return { projects: {} };
7893
- try {
7894
- return readPathMap(mapPath);
7895
- } catch {
7896
- return null;
7897
- }
7898
- }
7899
- function mirrorSharedLinksBeforePull(repo, ts) {
7900
- if (process.platform !== "win32") return;
7901
- try {
7902
- stageLocalSharedEdits(readMapForMirror(join53(repo, "path-map.json")), ts);
7903
- } catch (err) {
7904
- warn(`could not stage local shared edits before the pull: ${err.message}`);
7905
- }
7906
- }
7907
8465
  function runPullCore(opts = {}) {
7908
8466
  const dryRun = opts.dryRun === true;
7909
8467
  const forceRemote = opts.forceRemote === true;
@@ -7913,7 +8471,7 @@ function runPullCore(opts = {}) {
7913
8471
  const ts = freshBackupTs(backup);
7914
8472
  handleWedge(repo, forceRemote);
7915
8473
  if (!dryRun) {
7916
- const backupRoot = join53(backup, ts);
8474
+ const backupRoot = join58(backup, ts);
7917
8475
  try {
7918
8476
  mkdirSync14(backupRoot, { recursive: true });
7919
8477
  } catch (err) {
@@ -7925,16 +8483,17 @@ function runPullCore(opts = {}) {
7925
8483
  dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
7926
8484
  );
7927
8485
  }
7928
- if (!dryRun && !forceRemote) mirrorSharedLinksBeforePull(repo, ts);
8486
+ const mirrored = !dryRun && !forceRemote ? reconcileSharedLinksBeforePull(repo, ts) : [];
8487
+ const sharedPlans = dryRun ? planSharedReconcileBeforePull(repo) : void 0;
7929
8488
  const prePostHeads = capturePrePostHeads(repo, () => {
7930
- gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
8489
+ pullWithCollisionRunbook(repo, mirrored);
7931
8490
  });
7932
8491
  assertNoAutostashConflict(repo, "nomad pull");
7933
- const mapPath = join53(repo, "path-map.json");
7934
- const map = existsSync42(mapPath) ? readPathMap(mapPath) : { projects: {} };
8492
+ const mapPath = join58(repo, "path-map.json");
8493
+ const map = existsSync46(mapPath) ? readPathMap(mapPath) : { projects: {} };
7935
8494
  const divergedKeptLocal = divergenceCheckExtras(ts, dryRun ? prePostHeads : void 0);
7936
8495
  if (dryRun) {
7937
- computePreview(ts, map, "pull");
8496
+ computePreview(ts, map, "pull", sharedPlans);
7938
8497
  return { tag: "dry" };
7939
8498
  }
7940
8499
  const { sections, localOnly, settingsLabel, unmapped, extrasSkipped } = buildWetPullSections(
@@ -7956,8 +8515,8 @@ function runPullCore(opts = {}) {
7956
8515
  }
7957
8516
  function cmdPull(opts = {}) {
7958
8517
  const repo = repoHome();
7959
- if (!existsSync42(repo)) die(`repo not cloned at ${repo}`);
7960
- if (!existsSync42(join53(repo, "shared", "settings.base.json"))) {
8518
+ if (!existsSync46(repo)) die(`repo not cloned at ${repo}`);
8519
+ if (!existsSync46(join58(repo, "shared", "settings.base.json"))) {
7961
8520
  die("repo not initialized; run 'nomad init' to scaffold");
7962
8521
  }
7963
8522
  const handle = acquireLock("pull");
@@ -7980,13 +8539,13 @@ function cmdPull(opts = {}) {
7980
8539
 
7981
8540
  // src/commands.push.ts
7982
8541
  init_config();
7983
- import { existsSync as existsSync46 } from "node:fs";
7984
- import { join as join58 } from "node:path";
8542
+ import { existsSync as existsSync50 } from "node:fs";
8543
+ import { join as join63 } from "node:path";
7985
8544
 
7986
8545
  // src/commands.push.selection.ts
7987
8546
  init_config();
7988
- import { existsSync as existsSync43, statSync as statSync11 } from "node:fs";
7989
- import { join as join54 } from "node:path";
8547
+ import { existsSync as existsSync47, statSync as statSync11 } from "node:fs";
8548
+ import { join as join59 } from "node:path";
7990
8549
  init_utils_json();
7991
8550
  function buildCurrentMap(map) {
7992
8551
  const current = {};
@@ -7995,8 +8554,8 @@ function buildCurrentMap(map) {
7995
8554
  for (const [, hostMap] of Object.entries(map.projects)) {
7996
8555
  const localPath = hostMap[HOST];
7997
8556
  if (!localPath) continue;
7998
- const localDir = join54(claude, "projects", encodePath(localPath));
7999
- if (!existsSync43(localDir)) continue;
8557
+ const localDir = join59(claude, "projects", encodePath(localPath));
8558
+ if (!existsSync47(localDir)) continue;
8000
8559
  for (const f of enumerateSourceFiles(localDir)) {
8001
8560
  const st = statSync11(f);
8002
8561
  current[f] = { size: st.size, mtime: st.mtimeMs };
@@ -8027,7 +8586,7 @@ function computePushSelection(map, old, scannerVersion, configHash, fullScan) {
8027
8586
  };
8028
8587
  }
8029
8588
  function loadSelectionForPush(mapPath, old, scannerVersion, configHash, fullScan) {
8030
- const map = existsSync43(mapPath) ? readPathMap(mapPath) : null;
8589
+ const map = existsSync47(mapPath) ? readPathMap(mapPath) : null;
8031
8590
  const { selection, newManifest } = computePushSelection(
8032
8591
  map,
8033
8592
  old,
@@ -8055,7 +8614,7 @@ function isAllowed(path, allowed) {
8055
8614
  }
8056
8615
  function blockSetFor(segments) {
8057
8616
  if (segments[0] !== "shared" || segments[1] !== "extras") return NEVER_SYNC;
8058
- return segments[3] === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
8617
+ return isClaudeExtraName(segments[3] ?? "") ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
8059
8618
  }
8060
8619
  function isNeverSync(path) {
8061
8620
  const segments = path.split("/");
@@ -8105,7 +8664,11 @@ function enforceAllowList(statusPorcelain, map) {
8105
8664
  ...Object.entries(map.extras ?? {}).flatMap(
8106
8665
  ([l, names]) => names.filter((n) => extrasWhitelist.includes(n)).flatMap((n) => [`shared/extras/${l}/${n}`, `shared/extras/${l}/${n}/`])
8107
8666
  ),
8108
- ...(map.sharedDirs ?? []).filter((d) => isValidSharedDir(d)).map((d) => `shared/${d}/`)
8667
+ // Predicate passed by reference, not wrapped in an arrow: `isValidSharedDir`
8668
+ // is a type guard, and an arrow returning its result is just a boolean, so
8669
+ // wrapping it would leave `d` as `unknown` and let a non-string reach the
8670
+ // template literal as "[object Object]".
8671
+ ...sharedDirEntries(map).filter(isValidSharedDir).map((d) => `shared/${d}/`)
8109
8672
  ];
8110
8673
  const neverSyncHits = [];
8111
8674
  const violations = [];
@@ -8129,14 +8692,14 @@ function enforceAllowList(statusPorcelain, map) {
8129
8692
 
8130
8693
  // src/commands.push.settings.ts
8131
8694
  init_config();
8132
- import { existsSync as existsSync44 } from "node:fs";
8133
- import { join as join55 } from "node:path";
8695
+ import { existsSync as existsSync48 } from "node:fs";
8696
+ import { join as join60 } from "node:path";
8134
8697
  init_utils();
8135
8698
  init_utils_fs();
8136
8699
  init_utils_json();
8137
8700
  function stripGsdHooksFromBase(repo, backup) {
8138
- const basePath = join55(repo, "shared", "settings.base.json");
8139
- if (!existsSync44(basePath)) return;
8701
+ const basePath = join60(repo, "shared", "settings.base.json");
8702
+ if (!existsSync48(basePath)) return;
8140
8703
  let base;
8141
8704
  try {
8142
8705
  base = readJson(basePath);
@@ -8150,14 +8713,14 @@ function stripGsdHooksFromBase(repo, backup) {
8150
8713
  writeJsonAtomic(basePath, stripped);
8151
8714
  }
8152
8715
  function reportSettingsAheadDrift(repo) {
8153
- const basePath = join55(repo, "shared", "settings.base.json");
8154
- if (!existsSync44(basePath)) return;
8155
- const settingsPath = join55(claudeHome(), "settings.json");
8156
- if (!existsSync44(settingsPath)) return;
8716
+ const basePath = join60(repo, "shared", "settings.base.json");
8717
+ if (!existsSync48(basePath)) return;
8718
+ const settingsPath = join60(claudeHome(), "settings.json");
8719
+ if (!existsSync48(settingsPath)) return;
8157
8720
  try {
8158
8721
  const base = readJson(basePath);
8159
- const hostPath = join55(repo, "hosts", `${HOST}.json`);
8160
- const overrides = existsSync44(hostPath) ? readJson(hostPath) : {};
8722
+ const hostPath = join60(repo, "hosts", `${HOST}.json`);
8723
+ const overrides = existsSync48(hostPath) ? readJson(hostPath) : {};
8161
8724
  const merged = deepMerge(base, overrides);
8162
8725
  const settings = readJson(settingsPath);
8163
8726
  const { ahead } = classifySettingsDrift(merged, settings);
@@ -8174,12 +8737,12 @@ function reportSettingsAheadDrift(repo) {
8174
8737
  // src/commands.push.guards.ts
8175
8738
  init_push_checks();
8176
8739
  init_utils();
8177
- import { join as join56, relative as relative7 } from "node:path";
8740
+ import { join as join61, relative as relative9 } from "node:path";
8178
8741
  function guardGitlinks(repo) {
8179
- const gitlinks = findGitlinks(join56(repo, "shared"));
8742
+ const gitlinks = findGitlinks(join61(repo, "shared"));
8180
8743
  if (gitlinks.length === 0) return;
8181
8744
  for (const p of gitlinks) {
8182
- const rel = relative7(repo, p).replaceAll("\\", "/");
8745
+ const rel = relative9(repo, p).replaceAll("\\", "/");
8183
8746
  fail(`gitlink: ${rel} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`);
8184
8747
  }
8185
8748
  const noun = gitlinks.length === 1 ? "entry" : "entries";
@@ -8208,7 +8771,7 @@ init_config();
8208
8771
 
8209
8772
  // src/push-global-config.ts
8210
8773
  init_config();
8211
- import { execFileSync as execFileSync22 } from "node:child_process";
8774
+ import { execFileSync as execFileSync23 } from "node:child_process";
8212
8775
  var STATUS_LABELS = {
8213
8776
  A: "add",
8214
8777
  M: "modify",
@@ -8248,7 +8811,7 @@ function isInScope(filePath, exactPrefixes, dirPrefixes) {
8248
8811
  }
8249
8812
  function collectGlobalConfigChanges(repoHome2, hostname2, opts) {
8250
8813
  const args = opts.staged ? ["diff", "--cached", "--name-status", "-z"] : ["diff", "HEAD", "--name-status", "-z"];
8251
- const raw = execFileSync22("git", args, {
8814
+ const raw = execFileSync23("git", args, {
8252
8815
  cwd: repoHome2,
8253
8816
  stdio: ["ignore", "pipe", "pipe"]
8254
8817
  }).toString();
@@ -8286,9 +8849,9 @@ init_color();
8286
8849
  init_config();
8287
8850
  init_config_sharedDirs_guard();
8288
8851
  import { randomBytes as randomBytes4 } from "node:crypto";
8289
- import { copyFileSync, existsSync as existsSync45, lstatSync as lstatSync15, mkdirSync as mkdirSync15, readdirSync as readdirSync18, rmSync as rmSync17 } from "node:fs";
8852
+ import { copyFileSync, existsSync as existsSync49, lstatSync as lstatSync20, mkdirSync as mkdirSync15, readdirSync as readdirSync19, rmSync as rmSync19 } from "node:fs";
8290
8853
  import { homedir as homedir7 } from "node:os";
8291
- import { join as join57, relative as relative8, sep as sep11 } from "node:path";
8854
+ import { join as join62, relative as relative10, sep as sep14 } from "node:path";
8292
8855
  init_push_leak_verdict();
8293
8856
  init_push_gitleaks();
8294
8857
  init_utils_fs();
@@ -8296,11 +8859,11 @@ init_utils_json();
8296
8859
  var NOTHING_TO_SCAN_ROW = `${dim(infoGlyph)} nothing to scan, no leaks`;
8297
8860
  function stageSessionDir(localDir, dstDir, changed) {
8298
8861
  if (changed !== void 0) {
8299
- const prefix = `${localDir}${sep11}`;
8862
+ const prefix = `${localDir}${sep14}`;
8300
8863
  const matching = [...changed].filter((p) => p.startsWith(prefix));
8301
8864
  if (matching.length === 0) return false;
8302
8865
  for (const src of matching) {
8303
- copyFileAtomic(src, join57(dstDir, relative8(localDir, src)));
8866
+ copyFileAtomic(src, join62(dstDir, relative10(localDir, src)));
8304
8867
  }
8305
8868
  return true;
8306
8869
  }
@@ -8316,14 +8879,14 @@ function stageSessions(tmpRoot, map, changed) {
8316
8879
  if (!p || p === "TBD") continue;
8317
8880
  reverse.set(encodePath(p), logical);
8318
8881
  }
8319
- const localProjects = join57(claudeHome(), "projects");
8320
- if (!existsSync45(localProjects)) return 0;
8882
+ const localProjects = join62(claudeHome(), "projects");
8883
+ if (!existsSync49(localProjects)) return 0;
8321
8884
  let staged = 0;
8322
- for (const dir of readdirSync18(localProjects)) {
8885
+ for (const dir of readdirSync19(localProjects)) {
8323
8886
  const logical = reverse.get(dir);
8324
8887
  if (!logical) continue;
8325
- const localDir = join57(localProjects, dir);
8326
- const dstDir = join57(tmpRoot, "shared", "projects", logical);
8888
+ const localDir = join62(localProjects, dir);
8889
+ const dstDir = join62(tmpRoot, "shared", "projects", logical);
8327
8890
  if (stageSessionDir(localDir, dstDir, changed)) staged++;
8328
8891
  }
8329
8892
  return staged;
@@ -8339,9 +8902,9 @@ function stageExtras(tmpRoot, map) {
8339
8902
  if (!localRoot || localRoot === "TBD") continue;
8340
8903
  for (const dirname14 of dirnames) {
8341
8904
  if (!whitelist.includes(dirname14)) continue;
8342
- const src = join57(localRoot, dirname14);
8343
- if (!existsSync45(src)) continue;
8344
- const dst = join57(tmpRoot, "shared", "extras", logical, dirname14);
8905
+ const src = join62(localRoot, dirname14);
8906
+ if (!existsSync49(src)) continue;
8907
+ const dst = join62(tmpRoot, "shared", "extras", logical, dirname14);
8345
8908
  copyExtras(src, dst);
8346
8909
  staged++;
8347
8910
  }
@@ -8349,19 +8912,19 @@ function stageExtras(tmpRoot, map) {
8349
8912
  return staged;
8350
8913
  }
8351
8914
  function stageSkills(tmpRoot) {
8352
- const localSkills = join57(claudeHome(), "skills");
8353
- const stat = lstatSync15(localSkills, { throwIfNoEntry: false });
8915
+ const localSkills = join62(claudeHome(), "skills");
8916
+ const stat = lstatSync20(localSkills, { throwIfNoEntry: false });
8354
8917
  if (stat === void 0 || stat.isSymbolicLink()) return 0;
8355
- const names = readdirSync18(localSkills, { encoding: "utf8" }).filter((n) => !isSkillExcluded(n));
8918
+ const names = readdirSync19(localSkills, { encoding: "utf8" }).filter((n) => !isSkillExcluded(n));
8356
8919
  if (names.length === 0) return 0;
8357
- copySkillsPush(localSkills, join57(tmpRoot, "shared", "skills"));
8920
+ copySkillsPush(localSkills, join62(tmpRoot, "shared", "skills"));
8358
8921
  return names.length;
8359
8922
  }
8360
8923
  function previewPushLeaks(map, opts = {}) {
8361
- const cacheDir = join57(homedir7(), ".cache", "claude-nomad");
8924
+ const cacheDir = join62(homedir7(), ".cache", "claude-nomad");
8362
8925
  mkdirSync15(cacheDir, { recursive: true });
8363
8926
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes4(4).toString("hex")}`;
8364
- const tmpRoot = join57(cacheDir, `push-preview-tree-${stamp}`);
8927
+ const tmpRoot = join62(cacheDir, `push-preview-tree-${stamp}`);
8365
8928
  try {
8366
8929
  const sessionCount = stageSessions(tmpRoot, map, opts.selection?.changed);
8367
8930
  const extrasCount = stageExtras(tmpRoot, map);
@@ -8369,9 +8932,9 @@ function previewPushLeaks(map, opts = {}) {
8369
8932
  if (sessionCount + extrasCount + skillCount === 0) {
8370
8933
  return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
8371
8934
  }
8372
- const ignoreFile = join57(repoHome(), ".gitleaksignore");
8373
- if (existsSync45(ignoreFile)) {
8374
- copyFileSync(ignoreFile, join57(tmpRoot, ".gitleaksignore"));
8935
+ const ignoreFile = join62(repoHome(), ".gitleaksignore");
8936
+ if (existsSync49(ignoreFile)) {
8937
+ copyFileSync(ignoreFile, join62(tmpRoot, ".gitleaksignore"));
8375
8938
  }
8376
8939
  let findings;
8377
8940
  try {
@@ -8384,7 +8947,7 @@ function previewPushLeaks(map, opts = {}) {
8384
8947
  }
8385
8948
  return verdictFromFindings(findings);
8386
8949
  } finally {
8387
- rmSync17(tmpRoot, { recursive: true, force: true });
8950
+ rmSync19(tmpRoot, { recursive: true, force: true });
8388
8951
  }
8389
8952
  }
8390
8953
 
@@ -8499,7 +9062,7 @@ async function runPushCore(opts = {}) {
8499
9062
  const scannerVersion = probeGitleaks();
8500
9063
  const configHash = computeConfigHash();
8501
9064
  const old = readManifest(manifestPath());
8502
- const mapPath = join58(repo, "path-map.json");
9065
+ const mapPath = join63(repo, "path-map.json");
8503
9066
  const { map, selection, newManifest } = loadSelectionForPush(
8504
9067
  mapPath,
8505
9068
  old,
@@ -8551,7 +9114,7 @@ async function cmdPush(opts = {}) {
8551
9114
  opts.allowRule
8552
9115
  );
8553
9116
  const repo = repoHome();
8554
- if (!existsSync46(repo)) die(`repo not cloned at ${repo}`);
9117
+ if (!existsSync50(repo)) die(`repo not cloned at ${repo}`);
8555
9118
  const handle = acquireLock("push");
8556
9119
  if (handle === null) process.exit(0);
8557
9120
  try {
@@ -8569,8 +9132,8 @@ async function cmdPush(opts = {}) {
8569
9132
  }
8570
9133
 
8571
9134
  // src/commands.sync.ts
8572
- import { existsSync as existsSync47 } from "node:fs";
8573
- import { join as join59 } from "node:path";
9135
+ import { existsSync as existsSync51 } from "node:fs";
9136
+ import { join as join64 } from "node:path";
8574
9137
  init_config();
8575
9138
  init_color();
8576
9139
  init_utils();
@@ -8704,8 +9267,8 @@ async function cmdSync(opts = {}) {
8704
9267
  const dryRun = opts.dryRun === true;
8705
9268
  const verbose = opts.verbose === true;
8706
9269
  const repo = repoHome();
8707
- if (!existsSync47(repo)) die(`repo not cloned at ${repo}`);
8708
- if (!existsSync47(join59(repo, "shared", "settings.base.json"))) {
9270
+ if (!existsSync51(repo)) die(`repo not cloned at ${repo}`);
9271
+ if (!existsSync51(join64(repo, "shared", "settings.base.json"))) {
8709
9272
  die("repo not initialized; run 'nomad init' to scaffold");
8710
9273
  }
8711
9274
  const handle = acquireLock("sync");
@@ -8729,9 +9292,9 @@ async function cmdSync(opts = {}) {
8729
9292
  }
8730
9293
 
8731
9294
  // src/commands.update.ts
8732
- import { execFileSync as execFileSync23 } from "node:child_process";
9295
+ import { execFileSync as execFileSync24 } from "node:child_process";
8733
9296
  init_utils();
8734
- function readInstalledVersion(run = execFileSync23) {
9297
+ function readInstalledVersion(run = execFileSync24) {
8735
9298
  const isWin = process.platform === "win32";
8736
9299
  try {
8737
9300
  return run(isWin ? "nomad.cmd" : "nomad", ["--version"], {
@@ -8742,7 +9305,7 @@ function readInstalledVersion(run = execFileSync23) {
8742
9305
  return null;
8743
9306
  }
8744
9307
  }
8745
- function cmdUpdate(currentVersion, run = execFileSync23) {
9308
+ function cmdUpdate(currentVersion, run = execFileSync24) {
8746
9309
  console.log(`Updating claude-nomad v${currentVersion}...`);
8747
9310
  const isWin = process.platform === "win32";
8748
9311
  try {
@@ -8781,8 +9344,8 @@ init_config();
8781
9344
  // src/crash-report.write.ts
8782
9345
  init_config();
8783
9346
  import { randomBytes as randomBytes5 } from "node:crypto";
8784
- import { mkdirSync as mkdirSync16, readdirSync as readdirSync19, statSync as statSync12, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "node:fs";
8785
- import { join as join61 } from "node:path";
9347
+ import { mkdirSync as mkdirSync16, readdirSync as readdirSync20, statSync as statSync12, unlinkSync as unlinkSync2, writeFileSync as writeFileSync11 } from "node:fs";
9348
+ import { join as join66 } from "node:path";
8786
9349
 
8787
9350
  // src/crash-report.ts
8788
9351
  var CRASH_MAX_STACK_LINES = 50;
@@ -8865,17 +9428,17 @@ function buildCrashReport(input) {
8865
9428
  }
8866
9429
 
8867
9430
  // src/crash-report.redact.ts
8868
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync18, writeFileSync as writeFileSync10 } from "node:fs";
9431
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync20, writeFileSync as writeFileSync10 } from "node:fs";
8869
9432
  import { tmpdir as tmpdir2 } from "node:os";
8870
- import { join as join60 } from "node:path";
9433
+ import { join as join65 } from "node:path";
8871
9434
  init_push_gitleaks_scan();
8872
9435
  var CRASH_SCAN_TIMEOUT_MS = 3e3;
8873
9436
  var SCAN_UNAVAILABLE_ADVISORY = "\n\n[gitleaks value-based scan unavailable; only structural redaction applied. Review before sharing this file publicly.]\n";
8874
9437
  function redactWithGitleaks(text, scan = scanFile) {
8875
9438
  let dir;
8876
9439
  try {
8877
- dir = mkdtempSync3(join60(tmpdir2(), "nomad-crash-scan-"));
8878
- const tmp = join60(dir, "crash.txt");
9440
+ dir = mkdtempSync3(join65(tmpdir2(), "nomad-crash-scan-"));
9441
+ const tmp = join65(dir, "crash.txt");
8879
9442
  writeFileSync10(tmp, text, { mode: 384 });
8880
9443
  const findings = scan(tmp, false, CRASH_SCAN_TIMEOUT_MS);
8881
9444
  if (findings === null) return text + SCAN_UNAVAILABLE_ADVISORY;
@@ -8885,7 +9448,7 @@ function redactWithGitleaks(text, scan = scanFile) {
8885
9448
  } finally {
8886
9449
  if (dir !== void 0) {
8887
9450
  try {
8888
- rmSync18(dir, { recursive: true, force: true });
9451
+ rmSync20(dir, { recursive: true, force: true });
8889
9452
  } catch {
8890
9453
  }
8891
9454
  }
@@ -8898,9 +9461,9 @@ init_utils();
8898
9461
  var CRASH_RETENTION_KEEP = 20;
8899
9462
  function listCrashFiles(dir = crashDir()) {
8900
9463
  try {
8901
- return readdirSync19(dir).flatMap((name) => {
9464
+ return readdirSync20(dir).flatMap((name) => {
8902
9465
  try {
8903
- return [{ name, mtimeMs: statSync12(join61(dir, name)).mtimeMs }];
9466
+ return [{ name, mtimeMs: statSync12(join66(dir, name)).mtimeMs }];
8904
9467
  } catch {
8905
9468
  return [];
8906
9469
  }
@@ -8914,7 +9477,7 @@ function pruneCrashDir(dir, keep = CRASH_RETENTION_KEEP) {
8914
9477
  const targets = prunableByCount(files, keep);
8915
9478
  for (const name of targets) {
8916
9479
  try {
8917
- unlinkSync2(join61(dir, name));
9480
+ unlinkSync2(join66(dir, name));
8918
9481
  } catch {
8919
9482
  }
8920
9483
  }
@@ -8922,7 +9485,7 @@ function pruneCrashDir(dir, keep = CRASH_RETENTION_KEEP) {
8922
9485
  function writeCrashReport(text, dir = crashDir()) {
8923
9486
  mkdirSync16(dir, { recursive: true, mode: 448 });
8924
9487
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes5(4).toString("hex")}`;
8925
- const path = join61(dir, `crash-${stamp}.txt`);
9488
+ const path = join66(dir, `crash-${stamp}.txt`);
8926
9489
  writeFileSync11(path, text, { mode: 384 });
8927
9490
  pruneCrashDir(dir);
8928
9491
  return path;
@@ -8954,18 +9517,18 @@ function handleCrash(err, argv, opts) {
8954
9517
 
8955
9518
  // src/diff.ts
8956
9519
  init_config();
8957
- import { existsSync as existsSync48 } from "node:fs";
8958
- import { join as join62 } from "node:path";
9520
+ import { existsSync as existsSync52 } from "node:fs";
9521
+ import { join as join67 } from "node:path";
8959
9522
  init_utils();
8960
9523
  init_utils_fs();
8961
9524
  init_utils_json();
8962
9525
  function cmdDiff() {
8963
9526
  try {
8964
9527
  const repo = repoHome();
8965
- if (!existsSync48(repo)) die(`repo not cloned at ${repo}`);
9528
+ if (!existsSync52(repo)) die(`repo not cloned at ${repo}`);
8966
9529
  const ts = freshBackupTs(backupBase());
8967
- const mapPath = join62(repo, "path-map.json");
8968
- const map = existsSync48(mapPath) ? readPathMap(mapPath) : { projects: {} };
9530
+ const mapPath = join67(repo, "path-map.json");
9531
+ const map = existsSync52(mapPath) ? readPathMap(mapPath) : { projects: {} };
8969
9532
  divergenceCheckExtras(ts);
8970
9533
  computePreview(ts, map, "diff");
8971
9534
  } catch (err) {
@@ -8980,19 +9543,19 @@ function cmdDiff() {
8980
9543
 
8981
9544
  // src/init.ts
8982
9545
  init_config();
8983
- import { existsSync as existsSync50, mkdirSync as mkdirSync17, writeFileSync as writeFileSync12 } from "node:fs";
8984
- import { join as join64 } from "node:path";
9546
+ import { existsSync as existsSync54, mkdirSync as mkdirSync17, writeFileSync as writeFileSync12 } from "node:fs";
9547
+ import { join as join69 } from "node:path";
8985
9548
 
8986
9549
  // src/init.gh-onboard.ts
8987
9550
  init_config();
8988
- import { execFileSync as execFileSync24 } from "node:child_process";
9551
+ import { execFileSync as execFileSync25 } from "node:child_process";
8989
9552
  init_utils();
8990
9553
  var DEFAULT_REPO_NAME = "claude-nomad-config";
8991
9554
  function isValidRepoName(name) {
8992
9555
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
8993
9556
  }
8994
9557
  var GH_NETWORK_TIMEOUT_MS = 3e4;
8995
- function ensureOriginRepo(repoName, run = execFileSync24) {
9558
+ function ensureOriginRepo(repoName, run = execFileSync25) {
8996
9559
  if (!isValidRepoName(repoName)) {
8997
9560
  die(
8998
9561
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -9063,33 +9626,33 @@ init_config();
9063
9626
  init_utils();
9064
9627
  init_utils_fs();
9065
9628
  init_utils_json();
9066
- import { copyFileSync as copyFileSync2, cpSync as cpSync10, existsSync as existsSync49, rmSync as rmSync19, statSync as statSync13 } from "node:fs";
9067
- import { join as join63 } from "node:path";
9629
+ import { copyFileSync as copyFileSync2, cpSync as cpSync10, existsSync as existsSync53, rmSync as rmSync21, statSync as statSync13 } from "node:fs";
9630
+ import { join as join68 } from "node:path";
9068
9631
  function snapshotIntoShared(map) {
9069
9632
  const repo = repoHome();
9070
9633
  const claude = claudeHome();
9071
9634
  for (const name of allSharedLinks(map)) {
9072
- const src = join63(claude, name);
9073
- if (!existsSync49(src)) continue;
9074
- const dst = join63(repo, "shared", name);
9635
+ const src = join68(claude, name);
9636
+ if (!existsSync53(src)) continue;
9637
+ const dst = join68(repo, "shared", name);
9075
9638
  if (statSync13(src).isDirectory()) {
9076
- const gk = join63(dst, ".gitkeep");
9077
- if (existsSync49(gk)) rmSync19(gk);
9639
+ const gk = join68(dst, ".gitkeep");
9640
+ if (existsSync53(gk)) rmSync21(gk);
9078
9641
  cpSync10(src, dst, { recursive: true, force: false, errorOnExist: true });
9079
9642
  } else {
9080
9643
  copyFileSync2(src, dst);
9081
9644
  }
9082
9645
  log(`snapshotted shared/${name} from ${src}`);
9083
9646
  }
9084
- const userSettings = join63(claude, "settings.json");
9085
- if (existsSync49(userSettings)) {
9647
+ const userSettings = join68(claude, "settings.json");
9648
+ if (existsSync53(userSettings)) {
9086
9649
  let parsed;
9087
9650
  try {
9088
9651
  parsed = readJson(userSettings);
9089
9652
  } catch (err) {
9090
9653
  return die(`malformed ${userSettings}: ${err.message}`);
9091
9654
  }
9092
- const hostFile = join63(repo, "hosts", `${HOST}.json`);
9655
+ const hostFile = join68(repo, "hosts", `${HOST}.json`);
9093
9656
  writeJsonAtomic(hostFile, parsed);
9094
9657
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
9095
9658
  }
@@ -9103,14 +9666,14 @@ var GITATTRIBUTES = "# nomad: sync content is byte-managed, disable all line-end
9103
9666
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
9104
9667
  function preflightConflict(repoHome2) {
9105
9668
  const candidates = [
9106
- join64(repoHome2, "shared", "settings.base.json"),
9107
- join64(repoHome2, "shared", "CLAUDE.md"),
9108
- join64(repoHome2, "path-map.json"),
9109
- join64(repoHome2, "hosts"),
9110
- join64(repoHome2, "shared")
9669
+ join69(repoHome2, "shared", "settings.base.json"),
9670
+ join69(repoHome2, "shared", "CLAUDE.md"),
9671
+ join69(repoHome2, "path-map.json"),
9672
+ join69(repoHome2, "hosts"),
9673
+ join69(repoHome2, "shared")
9111
9674
  ];
9112
9675
  for (const c of candidates) {
9113
- if (existsSync50(c)) return c;
9676
+ if (existsSync54(c)) return c;
9114
9677
  }
9115
9678
  return null;
9116
9679
  }
@@ -9128,27 +9691,27 @@ function cmdInit(opts = {}) {
9128
9691
  die(`already initialized; refusing to clobber ${conflict}`);
9129
9692
  }
9130
9693
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
9131
- mkdirSync17(join64(repo, "shared"), { recursive: true });
9132
- mkdirSync17(join64(repo, "hosts"), { recursive: true });
9694
+ mkdirSync17(join69(repo, "shared"), { recursive: true });
9695
+ mkdirSync17(join69(repo, "hosts"), { recursive: true });
9133
9696
  for (const name of SHARED_KEEP_DIRS) {
9134
- mkdirSync17(join64(repo, "shared", name), { recursive: true });
9697
+ mkdirSync17(join69(repo, "shared", name), { recursive: true });
9135
9698
  }
9136
- const userClaudeMd = join64(claude, "CLAUDE.md");
9137
- if (!snapshot || !existsSync50(userClaudeMd)) {
9138
- writeFileSync12(join64(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
9699
+ const userClaudeMd = join69(claude, "CLAUDE.md");
9700
+ if (!snapshot || !existsSync54(userClaudeMd)) {
9701
+ writeFileSync12(join69(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
9139
9702
  item("created shared/CLAUDE.md");
9140
9703
  }
9141
9704
  for (const name of SHARED_KEEP_DIRS) {
9142
- writeFileSync12(join64(repo, "shared", name, ".gitkeep"), "");
9705
+ writeFileSync12(join69(repo, "shared", name, ".gitkeep"), "");
9143
9706
  item(`created shared/${name}/.gitkeep`);
9144
9707
  }
9145
- writeFileSync12(join64(repo, "hosts", ".gitkeep"), "");
9708
+ writeFileSync12(join69(repo, "hosts", ".gitkeep"), "");
9146
9709
  item("created hosts/.gitkeep");
9147
- writeJsonAtomic(join64(repo, "shared", "settings.base.json"), {});
9710
+ writeJsonAtomic(join69(repo, "shared", "settings.base.json"), {});
9148
9711
  item("created shared/settings.base.json");
9149
- writeJsonAtomic(join64(repo, "path-map.json"), { projects: {} });
9712
+ writeJsonAtomic(join69(repo, "path-map.json"), { projects: {} });
9150
9713
  item("created path-map.json");
9151
- writeFileSync12(join64(repo, ".gitattributes"), GITATTRIBUTES);
9714
+ writeFileSync12(join69(repo, ".gitattributes"), GITATTRIBUTES);
9152
9715
  item("created .gitattributes");
9153
9716
  if (snapshot) {
9154
9717
  snapshotIntoShared({ projects: {} });
@@ -9214,21 +9777,21 @@ function maybeDisableRepoActions(repoHome2, run) {
9214
9777
  // src/init.prompt.ts
9215
9778
  init_config();
9216
9779
  init_utils();
9217
- import { existsSync as existsSync51, readdirSync as readdirSync20, statSync as statSync14 } from "node:fs";
9218
- import { join as join65 } from "node:path";
9780
+ import { existsSync as existsSync55, readdirSync as readdirSync21, statSync as statSync14 } from "node:fs";
9781
+ import { join as join70 } from "node:path";
9219
9782
  import { createInterface as createInterface3 } from "node:readline/promises";
9220
9783
  function nonEmptyExists(path) {
9221
- if (!existsSync51(path)) return false;
9784
+ if (!existsSync55(path)) return false;
9222
9785
  try {
9223
- if (statSync14(path).isDirectory()) return readdirSync20(path).length > 0;
9786
+ if (statSync14(path).isDirectory()) return readdirSync21(path).length > 0;
9224
9787
  return true;
9225
9788
  } catch {
9226
9789
  return false;
9227
9790
  }
9228
9791
  }
9229
9792
  function hasExistingClaudeConfig(claudeHome2) {
9230
- if (existsSync51(join65(claudeHome2, "settings.json"))) return true;
9231
- return SHARED_LINKS.some((name) => nonEmptyExists(join65(claudeHome2, name)));
9793
+ if (existsSync55(join70(claudeHome2, "settings.json"))) return true;
9794
+ return SHARED_LINKS.some((name) => nonEmptyExists(join70(claudeHome2, name)));
9232
9795
  }
9233
9796
  async function confirmSnapshotDefault(claudeHome2) {
9234
9797
  if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
@@ -9535,7 +10098,7 @@ function parseSyncArgs(argv) {
9535
10098
  // package.json
9536
10099
  var package_default = {
9537
10100
  name: "claude-nomad",
9538
- version: "0.63.2",
10101
+ version: "0.64.1",
9539
10102
  type: "module",
9540
10103
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
9541
10104
  keywords: [
@@ -9779,15 +10342,15 @@ var DEFAULT_HELP = [
9779
10342
  init_config();
9780
10343
  init_utils();
9781
10344
  init_utils_json();
9782
- import { existsSync as existsSync52, readFileSync as readFileSync20, readdirSync as readdirSync21 } from "node:fs";
9783
- import { join as join66 } from "node:path";
10345
+ import { existsSync as existsSync56, readFileSync as readFileSync21, readdirSync as readdirSync22 } from "node:fs";
10346
+ import { join as join71 } from "node:path";
9784
10347
  function resumeCmd(sessionId) {
9785
10348
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
9786
10349
  fail(`invalid session id: ${sessionId}`);
9787
10350
  process.exit(1);
9788
10351
  }
9789
- const projectsRoot = join66(claudeHome(), "projects");
9790
- if (!existsSync52(projectsRoot)) {
10352
+ const projectsRoot = join71(claudeHome(), "projects");
10353
+ if (!existsSync56(projectsRoot)) {
9791
10354
  fail(`${projectsRoot} does not exist`);
9792
10355
  process.exit(1);
9793
10356
  }
@@ -9801,8 +10364,8 @@ function resumeCmd(sessionId) {
9801
10364
  fail(`no cwd field found in ${jsonlPath}`);
9802
10365
  process.exit(1);
9803
10366
  }
9804
- const mapPath = join66(repoHome(), "path-map.json");
9805
- if (!existsSync52(mapPath)) {
10367
+ const mapPath = join71(repoHome(), "path-map.json");
10368
+ if (!existsSync56(mapPath)) {
9806
10369
  fail("path-map.json missing");
9807
10370
  process.exit(1);
9808
10371
  }
@@ -9824,14 +10387,14 @@ function resumeCmd(sessionId) {
9824
10387
  console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
9825
10388
  }
9826
10389
  function findTranscriptPath(projectsRoot, sessionId) {
9827
- for (const dir of readdirSync21(projectsRoot)) {
9828
- const candidate = join66(projectsRoot, dir, `${sessionId}.jsonl`);
9829
- if (existsSync52(candidate)) return candidate;
10390
+ for (const dir of readdirSync22(projectsRoot)) {
10391
+ const candidate = join71(projectsRoot, dir, `${sessionId}.jsonl`);
10392
+ if (existsSync56(candidate)) return candidate;
9830
10393
  }
9831
10394
  return null;
9832
10395
  }
9833
10396
  function extractRecordedCwd(jsonlPath) {
9834
- for (const line of readFileSync20(jsonlPath, "utf8").split("\n")) {
10397
+ for (const line of readFileSync21(jsonlPath, "utf8").split("\n")) {
9835
10398
  if (!line.trim()) continue;
9836
10399
  try {
9837
10400
  const obj = JSON.parse(line);