claude-nomad 0.58.1 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/nomad.mjs CHANGED
@@ -445,6 +445,9 @@ var init_settings_keys = __esm({
445
445
  import { homedir, hostname } from "node:os";
446
446
  import { join, resolve } from "node:path";
447
447
  function home() {
448
+ if (process.platform === "win32") {
449
+ return process.env.USERPROFILE || process.env.HOME || homedir();
450
+ }
448
451
  return process.env.HOME || homedir();
449
452
  }
450
453
  function claudeHome() {
@@ -508,8 +511,10 @@ var init_config = __esm({
508
511
  "path-map.json",
509
512
  ".gitleaksignore",
510
513
  // written by nomad push Allow action
511
- ".gitleaks.overlay.toml"
514
+ ".gitleaks.overlay.toml",
512
515
  // user-owned gitleaks allowlist overlay layered on the bundled base
516
+ ".gitattributes"
517
+ // scaffolded by nomad init as the CRLF guard doctor recommends adding
513
518
  ];
514
519
  }
515
520
  });
@@ -596,10 +601,10 @@ var init_utils_json = __esm({
596
601
  // src/utils.fs.ts
597
602
  import {
598
603
  closeSync,
599
- cpSync,
600
- existsSync,
604
+ cpSync as cpSync3,
605
+ existsSync as existsSync3,
601
606
  fsyncSync,
602
- lstatSync,
607
+ lstatSync as lstatSync3,
603
608
  mkdirSync,
604
609
  openSync,
605
610
  renameSync,
@@ -607,9 +612,32 @@ import {
607
612
  symlinkSync,
608
613
  writeFileSync
609
614
  } from "node:fs";
610
- import { dirname, join as join2, relative, sep } from "node:path";
615
+ import { dirname, join as join4, relative as relative2, sep as sep2 } from "node:path";
616
+ function isRetryableRenameCode(code) {
617
+ return code === "EPERM" || code === "EBUSY";
618
+ }
619
+ function sleepSyncMs(ms) {
620
+ const view = new Int32Array(new SharedArrayBuffer(4));
621
+ Atomics.wait(view, 0, 0, ms);
622
+ }
623
+ function renameAtomicRetry(tmp, dst, renameFn = renameSync) {
624
+ if (process.platform !== "win32") {
625
+ renameFn(tmp, dst);
626
+ return;
627
+ }
628
+ for (let attempt = 1; attempt <= RENAME_RETRY_MAX_ATTEMPTS; attempt++) {
629
+ try {
630
+ renameFn(tmp, dst);
631
+ return;
632
+ } catch (e) {
633
+ const code = e.code;
634
+ if (!isRetryableRenameCode(code) || attempt === RENAME_RETRY_MAX_ATTEMPTS) throw e;
635
+ sleepSyncMs(RENAME_RETRY_BACKOFF_MS);
636
+ }
637
+ }
638
+ }
611
639
  function writeJsonAtomic(path, data) {
612
- const mode = existsSync(path) ? statSync(path).mode & 511 : 384;
640
+ const mode = existsSync3(path) ? statSync(path).mode & 511 : 384;
613
641
  const tmp = `${path}.tmp.${process.pid}`;
614
642
  const fd = openSync(tmp, "w", mode);
615
643
  try {
@@ -618,7 +646,7 @@ function writeJsonAtomic(path, data) {
618
646
  } finally {
619
647
  closeSync(fd);
620
648
  }
621
- renameSync(tmp, path);
649
+ renameAtomicRetry(tmp, path);
622
650
  const dirFd = openSync(dirname(path), "r");
623
651
  try {
624
652
  fsyncSync(dirFd);
@@ -635,14 +663,14 @@ function nowTimestamp() {
635
663
  }
636
664
  function freshBackupTs(backupRoot) {
637
665
  const base = nowTimestamp();
638
- if (!existsSync(join2(backupRoot, base))) return base;
666
+ if (!existsSync3(join4(backupRoot, base))) return base;
639
667
  let n = 1;
640
- while (existsSync(join2(backupRoot, `${base}-${n}`))) n++;
668
+ while (existsSync3(join4(backupRoot, `${base}-${n}`))) n++;
641
669
  return `${base}-${n}`;
642
670
  }
643
671
  function ensureSymlink(linkPath, target) {
644
- if (existsSync(linkPath)) {
645
- if (lstatSync(linkPath).isSymbolicLink()) return;
672
+ if (existsSync3(linkPath)) {
673
+ if (lstatSync3(linkPath).isSymbolicLink()) return;
646
674
  die(`${linkPath} exists and is not a symlink. Move it aside first.`);
647
675
  }
648
676
  mkdirSync(dirname(linkPath), { recursive: true });
@@ -650,39 +678,42 @@ function ensureSymlink(linkPath, target) {
650
678
  log(`linked ${linkPath} -> ${target}`);
651
679
  }
652
680
  function backupUnder(absPath, anchor, destRoot) {
653
- if (!existsSync(absPath)) return;
654
- const rel = relative(anchor, absPath);
655
- if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`)) return;
656
- const dst = join2(destRoot, rel);
681
+ if (!existsSync3(absPath)) return;
682
+ const rel = relative2(anchor, absPath);
683
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep2}`)) return;
684
+ const dst = join4(destRoot, rel);
657
685
  mkdirSync(dirname(dst), { recursive: true });
658
- cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
686
+ cpSync3(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
659
687
  }
660
688
  function backupBeforeWrite(absPath, ts) {
661
- backupUnder(absPath, claudeHome(), join2(backupBase(), ts));
689
+ backupUnder(absPath, claudeHome(), join4(backupBase(), ts));
662
690
  }
663
691
  function backupRepoWrite(absPath, ts, repoHome2) {
664
- backupUnder(absPath, repoHome2, join2(backupBase(), ts, "repo"));
692
+ backupUnder(absPath, repoHome2, join4(backupBase(), ts, "repo"));
665
693
  }
666
694
  function backupExtrasWrite(absPath, ts, projectRoot) {
667
- backupUnder(absPath, projectRoot, join2(backupBase(), ts, "extras", encodePath(projectRoot)));
695
+ backupUnder(absPath, projectRoot, join4(backupBase(), ts, "extras", encodePath(projectRoot)));
668
696
  }
697
+ var RENAME_RETRY_MAX_ATTEMPTS, RENAME_RETRY_BACKOFF_MS;
669
698
  var init_utils_fs = __esm({
670
699
  "src/utils.fs.ts"() {
671
700
  "use strict";
672
701
  init_config();
673
702
  init_utils_json();
674
703
  init_utils();
704
+ RENAME_RETRY_MAX_ATTEMPTS = 5;
705
+ RENAME_RETRY_BACKOFF_MS = 10;
675
706
  }
676
707
  });
677
708
 
678
709
  // src/commands.pull.wedge.ts
679
710
  import { execFileSync as execFileSync3 } from "node:child_process";
680
- import { existsSync as existsSync13 } from "node:fs";
681
- import { join as join15 } from "node:path";
711
+ import { existsSync as existsSync15 } from "node:fs";
712
+ import { join as join17 } from "node:path";
682
713
  function detectWedge(repo) {
683
- const g = join15(repo, ".git");
684
- if (existsSync13(join15(g, "rebase-merge")) || existsSync13(join15(g, "rebase-apply"))) return "rebase";
685
- if (existsSync13(join15(g, "MERGE_HEAD"))) return "merge";
714
+ const g = join17(repo, ".git");
715
+ if (existsSync15(join17(g, "rebase-merge")) || existsSync15(join17(g, "rebase-apply"))) return "rebase";
716
+ if (existsSync15(join17(g, "MERGE_HEAD"))) return "merge";
686
717
  return null;
687
718
  }
688
719
  function unmergedIndexPresent(repo) {
@@ -739,15 +770,15 @@ var init_commands_pull_wedge = __esm({
739
770
  });
740
771
 
741
772
  // src/push-gitleaks.config.ts
742
- import { existsSync as existsSync14, mkdtempSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
773
+ import { existsSync as existsSync16, mkdtempSync, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
743
774
  import { tmpdir } from "node:os";
744
- import { join as join16 } from "node:path";
775
+ import { join as join18 } from "node:path";
745
776
  import { fileURLToPath } from "node:url";
746
777
  function resolveTomlPath(repo = repoHome()) {
747
- const repoToml = join16(repo, ".gitleaks.toml");
748
- if (existsSync14(repoToml)) return repoToml;
778
+ const repoToml = join18(repo, ".gitleaks.toml");
779
+ if (existsSync16(repoToml)) return repoToml;
749
780
  const bundled = fileURLToPath(new URL("../.gitleaks.toml", import.meta.url));
750
- return existsSync14(bundled) ? bundled : null;
781
+ return existsSync16(bundled) ? bundled : null;
751
782
  }
752
783
  function assertOverlayAllowlistsScoped(overlayBody) {
753
784
  if (OVERLAY_INLINE_ALLOWLIST_RE.test(overlayBody)) {
@@ -785,17 +816,17 @@ function buildOverlayTempConfig(overlayBody, bundled) {
785
816
  path = ${JSON.stringify(bundled)}
786
817
 
787
818
  ${overlayBody}`;
788
- const tempPath = mkdtempSync(join16(tmpdir(), "nomad-gitleaks-cfg-"));
789
- const configPath = join16(tempPath, "config.toml");
819
+ const tempPath = mkdtempSync(join18(tmpdir(), "nomad-gitleaks-cfg-"));
820
+ const configPath = join18(tempPath, "config.toml");
790
821
  writeFileSync3(configPath, tempBody, { mode: 384, flag: "wx" });
791
822
  return { configPath, tempPath };
792
823
  }
793
824
  function resolveTomlConfig() {
794
825
  const repo = repoHome();
795
- const overlayPath = join16(repo, ".gitleaks.overlay.toml");
796
- const repoToml = join16(repo, ".gitleaks.toml");
826
+ const overlayPath = join18(repo, ".gitleaks.overlay.toml");
827
+ const repoToml = join18(repo, ".gitleaks.toml");
797
828
  const bundled = resolveTomlPath(repo);
798
- if (!existsSync14(overlayPath)) {
829
+ if (!existsSync16(overlayPath)) {
799
830
  return { path: bundled, tempPath: null };
800
831
  }
801
832
  if (bundled === repoToml) {
@@ -808,7 +839,7 @@ function resolveTomlConfig() {
808
839
  return { path: null, tempPath: null };
809
840
  }
810
841
  try {
811
- const overlayBody = readFileSync4(overlayPath, "utf8");
842
+ const overlayBody = readFileSync5(overlayPath, "utf8");
812
843
  if (OVERLAY_EXTEND_RE.test(overlayBody)) {
813
844
  throw new NomadFatal(
814
845
  ".gitleaks.overlay.toml must not contain an [extend] block; it is generated automatically. Remove the [extend] section and retry."
@@ -842,9 +873,9 @@ var init_push_gitleaks_config = __esm({
842
873
 
843
874
  // src/push-checks.ts
844
875
  import { execFileSync as execFileSync4 } from "node:child_process";
845
- import { readdirSync as readdirSync5, rmSync as rmSync5 } from "node:fs";
876
+ import { readdirSync as readdirSync7, rmSync as rmSync7 } from "node:fs";
846
877
  import { homedir as homedir2, platform } from "node:os";
847
- import { join as join17 } from "node:path";
878
+ import { delimiter, join as join19 } from "node:path";
848
879
  function gitleaksInstallHint() {
849
880
  const head = "gitleaks not on PATH (required for nomad push). Install:";
850
881
  const plat = platform();
@@ -852,6 +883,11 @@ function gitleaksInstallHint() {
852
883
  return `${head}
853
884
  brew install gitleaks`;
854
885
  }
886
+ if (plat === "win32") {
887
+ return `${head}
888
+ winget install gitleaks.gitleaks
889
+ (or: scoop install gitleaks)`;
890
+ }
855
891
  if (plat === "linux") {
856
892
  const archMap = { x64: "x64", arm64: "arm64", arm: "armv7" };
857
893
  const arch = archMap[process.arch];
@@ -865,7 +901,7 @@ function gitleaksInstallHint() {
865
901
  " ~/.local/bin/gitleaks version # verify"
866
902
  ];
867
903
  const localBin = `${homedir2()}/.local/bin`;
868
- const paths = (process.env.PATH ?? "").split(":");
904
+ const paths = (process.env.PATH ?? "").split(delimiter);
869
905
  if (!paths.includes(localBin)) {
870
906
  lines.push(
871
907
  " 3. ~/.local/bin is not on PATH; add to your shell rc:",
@@ -882,12 +918,12 @@ function findGitlinks(dir) {
882
918
  function walk(current) {
883
919
  let entries;
884
920
  try {
885
- entries = readdirSync5(current, { withFileTypes: true });
921
+ entries = readdirSync7(current, { withFileTypes: true });
886
922
  } catch {
887
923
  return;
888
924
  }
889
925
  for (const e of entries) {
890
- const p = join17(current, e.name);
926
+ const p = join19(current, e.name);
891
927
  if (e.name === ".git") {
892
928
  hits.push(p);
893
929
  continue;
@@ -909,7 +945,7 @@ function probeGitleaks() {
909
945
  if (e.code === "ENOENT") throw new NomadFatal(gitleaksInstallHint());
910
946
  throw new NomadFatal(`gitleaks --version failed: ${e.message}`);
911
947
  } finally {
912
- if (tempPath !== null) rmSync5(tempPath, { recursive: true, force: true });
948
+ if (tempPath !== null) rmSync7(tempPath, { recursive: true, force: true });
913
949
  }
914
950
  }
915
951
  function wedgePreflight(wedge) {
@@ -946,12 +982,12 @@ var init_push_checks = __esm({
946
982
 
947
983
  // src/push-gitleaks.scan.ts
948
984
  import { execFileSync as execFileSync8 } from "node:child_process";
949
- import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync6 } from "node:fs";
985
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, rmSync as rmSync8 } from "node:fs";
950
986
  import { homedir as homedir3 } from "node:os";
951
- import { join as join21 } from "node:path";
987
+ import { join as join23 } from "node:path";
952
988
  function readGitleaksReport(reportPath) {
953
989
  try {
954
- const raw = readFileSync5(reportPath, "utf8");
990
+ const raw = readFileSync6(reportPath, "utf8");
955
991
  const parsed = JSON.parse(raw);
956
992
  if (!Array.isArray(parsed)) return null;
957
993
  return parsed;
@@ -960,9 +996,9 @@ function readGitleaksReport(reportPath) {
960
996
  }
961
997
  }
962
998
  function scanStagedTree(repoDir, forwardStreams = false) {
963
- const cacheDir = join21(homedir3(), ".cache", "claude-nomad");
999
+ const cacheDir = join23(homedir3(), ".cache", "claude-nomad");
964
1000
  mkdirSync3(cacheDir, { recursive: true });
965
- const reportPath = join21(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
1001
+ const reportPath = join23(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
966
1002
  const { path: toml, tempPath } = resolveTomlConfig();
967
1003
  const args = [
968
1004
  "protect",
@@ -990,14 +1026,14 @@ function scanStagedTree(repoDir, forwardStreams = false) {
990
1026
  }
991
1027
  return report;
992
1028
  } finally {
993
- if (tempPath !== null) rmSync6(tempPath, { recursive: true, force: true });
994
- rmSync6(reportPath, { force: true });
1029
+ if (tempPath !== null) rmSync8(tempPath, { recursive: true, force: true });
1030
+ rmSync8(reportPath, { force: true });
995
1031
  }
996
1032
  }
997
1033
  function scanFile(filePath, forwardStreams = false) {
998
- const cacheDir = join21(homedir3(), ".cache", "claude-nomad");
1034
+ const cacheDir = join23(homedir3(), ".cache", "claude-nomad");
999
1035
  mkdirSync3(cacheDir, { recursive: true });
1000
- const reportPath = join21(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
1036
+ const reportPath = join23(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
1001
1037
  const { path: toml, tempPath } = resolveTomlConfig();
1002
1038
  const args = [
1003
1039
  "detect",
@@ -1025,8 +1061,8 @@ function scanFile(filePath, forwardStreams = false) {
1025
1061
  }
1026
1062
  return report;
1027
1063
  } finally {
1028
- if (tempPath !== null) rmSync6(tempPath, { recursive: true, force: true });
1029
- rmSync6(reportPath, { force: true });
1064
+ if (tempPath !== null) rmSync8(tempPath, { recursive: true, force: true });
1065
+ rmSync8(reportPath, { force: true });
1030
1066
  }
1031
1067
  }
1032
1068
  var init_push_gitleaks_scan = __esm({
@@ -1228,189 +1264,13 @@ var init_push_leak_verdict = __esm({
1228
1264
  // src/commands.adopt.ts
1229
1265
  init_config();
1230
1266
  init_config_sharedDirs_guard();
1231
- init_utils();
1232
- init_utils_fs();
1233
- init_utils_json();
1234
- import { cpSync as cpSync2, existsSync as existsSync2, lstatSync as lstatSync2, rmSync } from "node:fs";
1235
- import { join as join3 } from "node:path";
1236
- var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
1237
- function lexists(p) {
1238
- try {
1239
- lstatSync2(p);
1240
- return true;
1241
- } catch {
1242
- return false;
1243
- }
1244
- }
1245
- function readMapIfPresent(repoHome2) {
1246
- const mapPath = join3(repoHome2, "path-map.json");
1247
- return existsSync2(mapPath) ? readPathMap(mapPath) : { projects: {} };
1248
- }
1249
- function isConfiguredTarget(name, map) {
1250
- return SHARED_LINKS.includes(name) || (map.sharedDirs?.includes(name) ?? false);
1251
- }
1252
- function isValidAdoptName(name) {
1253
- if (SHARED_LINKS.includes(name)) return true;
1254
- return isValidSharedDir(name);
1255
- }
1256
- function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
1257
- const ts = freshBackupTs(backup);
1258
- backupBeforeWrite(linkPath, ts);
1259
- cpSync2(linkPath, sharedTarget, { recursive: true, force: true, preserveTimestamps: true });
1260
- rmSync(linkPath, { recursive: true, force: true });
1261
- ensureSymlink(linkPath, sharedTarget);
1262
- const rel = join3("shared", name);
1263
- gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
1264
- log(`adopted ${name}; ${ADOPT_PUSH_HINT}`);
1265
- }
1266
- function cmdAdopt(name, opts = {}) {
1267
- const dryRun = opts.dryRun === true;
1268
- if (!isValidAdoptName(name)) {
1269
- fail(`invalid name: ${JSON.stringify(name)}`);
1270
- process.exit(1);
1271
- }
1272
- const repo = repoHome();
1273
- const claude = claudeHome();
1274
- const backup = backupBase();
1275
- const map = readMapIfPresent(repo);
1276
- if (!isConfiguredTarget(name, map)) {
1277
- fail(
1278
- `${name}: not a configured shared target. Add it to sharedDirs in path-map.json first, then re-run adopt.`
1279
- );
1280
- process.exit(1);
1281
- }
1282
- const linkPath = join3(claude, name);
1283
- const sharedTarget = join3(repo, "shared", name);
1284
- if (!existsSync2(linkPath)) {
1285
- log(`${name}: nothing to adopt (not present in ~/.claude/)`);
1286
- return;
1287
- }
1288
- if (lstatSync2(linkPath).isSymbolicLink()) {
1289
- log(`${name}: already adopted (already a symlink)`);
1290
- return;
1291
- }
1292
- if (lexists(sharedTarget)) {
1293
- fail(`${name}: shared/${name} already exists; would clobber. Remove it first.`);
1294
- process.exit(1);
1295
- }
1296
- if (dryRun) {
1297
- const ts = freshBackupTs(backup);
1298
- log(`would backup: ${linkPath} -> backup/${ts}/${name}`);
1299
- log(`would move: ${linkPath} -> shared/${name}`);
1300
- log(`would stage: shared/${name}`);
1301
- return;
1302
- }
1303
- try {
1304
- performAdoptMove(name, linkPath, sharedTarget, repo, backup);
1305
- } catch (err) {
1306
- if (!(err instanceof NomadFatal)) throw err;
1307
- fail(err.message);
1308
- process.exitCode = 1;
1309
- }
1310
- }
1311
-
1312
- // src/commands.allow.ts
1313
- init_config();
1314
- import { existsSync as existsSync3 } from "node:fs";
1315
-
1316
- // src/commands.redact.core.ts
1317
- import { appendFileSync, readFileSync as readFileSync2 } from "node:fs";
1318
- import { join as join4 } from "node:path";
1319
- function collectMatchIntervals(content, findings) {
1320
- const intervals = [];
1321
- for (const f of findings) {
1322
- const match = f.Match;
1323
- if (match === "") continue;
1324
- let from = 0;
1325
- let pos = content.indexOf(match, from);
1326
- while (pos !== -1) {
1327
- intervals.push({ start: pos, end: pos + match.length, ruleId: f.RuleID });
1328
- from = pos + match.length;
1329
- pos = content.indexOf(match, from);
1330
- }
1331
- }
1332
- return intervals;
1333
- }
1334
- function mergeIntervals(intervals) {
1335
- if (intervals.length === 0) return [];
1336
- const sorted = [...intervals].sort((a, b) => a.start - b.start || b.end - a.end);
1337
- let last = { ...sorted[0] };
1338
- const merged = [last];
1339
- for (let i = 1; i < sorted.length; i++) {
1340
- const cur = sorted[i];
1341
- if (cur.start <= last.end) {
1342
- if (cur.end > last.end) last.end = cur.end;
1343
- } else {
1344
- last = { ...cur };
1345
- merged.push(last);
1346
- }
1347
- }
1348
- return merged;
1349
- }
1350
- function applyRedactions(content, findings) {
1351
- const raw = collectMatchIntervals(content, findings);
1352
- if (raw.length === 0) return content;
1353
- const merged = mergeIntervals(raw);
1354
- let result = content;
1355
- for (let i = merged.length - 1; i >= 0; i--) {
1356
- const { start, end, ruleId } = merged[i];
1357
- result = result.slice(0, start) + `[REDACTED:${ruleId}]` + result.slice(end);
1358
- }
1359
- return result;
1360
- }
1361
- function formatFingerprint(fingerprint) {
1362
- return fingerprint.replace(/[\r\n]/g, "") + "\n";
1363
- }
1364
- function isRecentlyModified(mtimeMs, nowMs, thresholdMs = 5 * 60 * 1e3) {
1365
- return nowMs - mtimeMs < thresholdMs;
1366
- }
1367
- function isValidFingerprint(fingerprint) {
1368
- return fingerprint.trim().length > 0 && fingerprint.length <= 512 && /^[^\r\n]+$/.test(fingerprint);
1369
- }
1370
- function isAlreadyPresent(line, lines) {
1371
- return lines.includes(line);
1372
- }
1373
- function appendGitleaksIgnore(fingerprint, repo) {
1374
- const sanitized = fingerprint.replace(/[\r\n]/g, "").trim();
1375
- if (sanitized.length === 0) return;
1376
- const ignPath = join4(repo, ".gitleaksignore");
1377
- let raw;
1378
- try {
1379
- raw = readFileSync2(ignPath, "utf8");
1380
- } catch {
1381
- raw = "";
1382
- }
1383
- const existing = raw.split("\n").filter((l) => l.length > 0);
1384
- if (isAlreadyPresent(sanitized, existing)) return;
1385
- const needsLeadingNewline = raw.length > 0 && !raw.endsWith("\n");
1386
- const prefix = needsLeadingNewline ? "\n" : "";
1387
- appendFileSync(ignPath, prefix + formatFingerprint(sanitized), "utf8");
1388
- }
1389
-
1390
- // src/commands.allow.ts
1391
- init_utils();
1392
- function cmdAllow(fingerprints) {
1393
- const repo = repoHome();
1394
- if (!existsSync3(repo)) die(`repo not cloned at ${repo}`);
1395
- for (const fp of fingerprints) {
1396
- if (!isValidFingerprint(fp)) {
1397
- const shown = fp.replaceAll("\r", String.raw`\r`).replaceAll("\n", String.raw`\n`);
1398
- fail(`invalid fingerprint: ${shown}`);
1399
- process.exit(1);
1400
- }
1401
- }
1402
- for (const fp of fingerprints) {
1403
- appendGitleaksIgnore(fp, repo);
1404
- item(`allowed: ${fp}`);
1405
- }
1406
- log(`allowed ${fingerprints.length} fingerprint(s)`);
1407
- }
1267
+ import { cpSync as cpSync4, existsSync as existsSync5, lstatSync as lstatSync5, rmSync as rmSync4 } from "node:fs";
1268
+ import { join as join6 } from "node:path";
1408
1269
 
1409
- // src/commands.capture-settings.ts
1270
+ // src/links.ts
1410
1271
  init_config();
1411
- import { existsSync as existsSync5 } from "node:fs";
1412
- import { join as join7 } from "node:path";
1413
- import { createInterface } from "node:readline/promises";
1272
+ import { existsSync as existsSync4, lstatSync as lstatSync4, rmSync as rmSync3 } from "node:fs";
1273
+ import { join as join5 } from "node:path";
1414
1274
 
1415
1275
  // src/hooks-filter.ts
1416
1276
  init_config();
@@ -1630,120 +1490,569 @@ function buildCaptureSubset(merged, settings, opts) {
1630
1490
  return out;
1631
1491
  }
1632
1492
 
1633
- // src/links.ts
1493
+ // src/extras-sync.core.ts
1634
1494
  init_config();
1635
- import { existsSync as existsSync4, lstatSync as lstatSync3, rmSync as rmSync2 } from "node:fs";
1636
- import { join as join5 } from "node:path";
1495
+ import { cpSync as cpSync2, existsSync as existsSync2, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
1496
+ import { basename as basename2, join as join3, relative } from "node:path";
1497
+
1498
+ // src/extras-sync.collision.ts
1637
1499
  init_utils();
1638
- init_utils_fs();
1639
- init_utils_json();
1640
- function emitAutoMove(onPreview, linkPath, ts, name) {
1641
- if (onPreview) {
1642
- onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
1643
- } else {
1644
- log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
1645
- }
1500
+ import { cpSync, existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
1501
+ import { basename, join as join2 } from "node:path";
1502
+ function quoted(p) {
1503
+ return `"${p}"`;
1646
1504
  }
1647
- function emitCreate(onPreview, from, to) {
1648
- if (onPreview) {
1649
- onPreview({ kind: "create", from, to });
1650
- } else {
1651
- log(`would create symlink: ${from} -> ${to}`);
1505
+ function stripCollidingDstSymlinks(src, dst, isExcluded) {
1506
+ if (!existsSync(dst)) return;
1507
+ for (const name of readdirSync(src)) {
1508
+ if (isExcluded(name)) continue;
1509
+ const dstPath = join2(dst, name);
1510
+ const dstStat = lstatSync(dstPath, { throwIfNoEntry: false });
1511
+ if (dstStat === void 0) continue;
1512
+ if (dstStat.isSymbolicLink()) {
1513
+ rmSync(dstPath, { recursive: true, force: true });
1514
+ } else if (dstStat.isDirectory() && lstatSync(join2(src, name)).isDirectory()) {
1515
+ stripCollidingDstSymlinks(join2(src, name), dstPath, isExcluded);
1516
+ }
1652
1517
  }
1653
1518
  }
1654
- function isAlreadySymlink(linkPath) {
1655
- return existsSync4(linkPath) && lstatSync3(linkPath).isSymbolicLink();
1519
+ function cpSyncGuarded(src, dst, filter, label) {
1520
+ try {
1521
+ cpSync(src, dst, { recursive: true, force: true, verbatimSymlinks: true, filter });
1522
+ } catch (err) {
1523
+ const e = err;
1524
+ if (e.code === "EINVAL" || e.code === "ENOTEMPTY" || e.code === "ERR_FS_CP_NON_DIR_TO_DIR" || e.code === "ERR_FS_CP_DIR_TO_NON_DIR" || process.platform === "win32" && e.errno === 145) {
1525
+ throw new NomadFatal(
1526
+ `${label}: type collision copying ${quoted(src)} -> ${quoted(dst)} (${e.path ?? "unknown path"}): a file/directory type changed upstream; run nomad pull --force-remote to recover`
1527
+ );
1528
+ }
1529
+ throw err;
1530
+ }
1656
1531
  }
1657
- function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
1658
- for (const name of linkNames) {
1659
- const linkPath = join5(claude, name);
1660
- const target = join5(repo, "shared", name);
1661
- if (!existsSync4(linkPath)) continue;
1662
- if (lstatSync3(linkPath).isSymbolicLink()) continue;
1663
- if (!existsSync4(target)) continue;
1664
- if (dryRun) {
1665
- emitAutoMove(onPreview, linkPath, ts, name);
1532
+ function prunePreservingBy(src, dst, isPreserved) {
1533
+ for (const name of readdirSync(dst)) {
1534
+ if (isPreserved(name)) continue;
1535
+ const dstPath = join2(dst, name);
1536
+ const srcStat = lstatSync(join2(src, name), { throwIfNoEntry: false });
1537
+ if (srcStat === void 0) {
1538
+ rmSync(dstPath, { recursive: true, force: true });
1666
1539
  continue;
1667
1540
  }
1668
- backupBeforeWrite(linkPath, ts);
1669
- rmSync2(linkPath, { recursive: true, force: true });
1541
+ const dstStat = lstatSync(dstPath);
1542
+ if (srcStat.isDirectory() && dstStat.isDirectory()) {
1543
+ prunePreservingBy(join2(src, name), dstPath, isPreserved);
1544
+ } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
1545
+ rmSync(dstPath, { recursive: true, force: true });
1546
+ }
1547
+ }
1548
+ }
1549
+ function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
1550
+ const dstStat = lstatSync(dst, { throwIfNoEntry: false });
1551
+ if (dstStat !== void 0) {
1552
+ if (dstStat.isDirectory()) {
1553
+ const srcStat = lstatSync(src, { throwIfNoEntry: false });
1554
+ if (srcStat !== void 0 && !srcStat.isDirectory()) {
1555
+ throw new NomadFatal(
1556
+ `copyExtrasFilteredPreservingBy: type mismatch copying ${quoted(src)} -> ${quoted(dst)}: the repo entry is a file but the local entry is a directory; run nomad pull --force-remote to recover`
1557
+ );
1558
+ }
1559
+ prunePreservingBy(src, dst, isPreserved);
1560
+ } else {
1561
+ rmSync(dst, { recursive: true, force: true });
1562
+ }
1563
+ }
1564
+ stripCollidingDstSymlinks(src, dst, isPreserved);
1565
+ cpSync(src, dst, {
1566
+ recursive: true,
1567
+ force: true,
1568
+ verbatimSymlinks: true,
1569
+ filter: (srcEntry) => srcEntry === src || !isPreserved(basename(srcEntry))
1570
+ });
1571
+ }
1572
+
1573
+ // src/extras-sync.guards.ts
1574
+ init_utils();
1575
+ init_config_sharedDirs_guard();
1576
+ import { isAbsolute, normalize, sep } from "node:path";
1577
+ function assertSafeLocalRoot(localRoot, logical) {
1578
+ if (!isAbsolute(localRoot)) {
1579
+ throw new NomadFatal(
1580
+ `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
1581
+ );
1670
1582
  }
1583
+ const canonical = process.platform === "win32" ? localRoot.split("/").join(sep) : localRoot;
1584
+ if (canonical !== normalize(canonical)) {
1585
+ throw new NomadFatal(
1586
+ `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
1587
+ );
1588
+ }
1589
+ }
1590
+
1591
+ // src/extras-sync.core.ts
1592
+ init_utils();
1593
+ init_utils_json();
1594
+ function loadValidatedExtras(opts) {
1595
+ const repo = repoHome();
1596
+ const mapPath = join3(repo, "path-map.json");
1597
+ const repoExtras = join3(repo, "shared", "extras");
1598
+ if (!existsSync2(mapPath) || opts.requireRepoExtras === true && !existsSync2(repoExtras)) {
1599
+ if (opts.missingMsg !== void 0) log(opts.missingMsg);
1600
+ return null;
1601
+ }
1602
+ const map = readPathMap(mapPath);
1603
+ const extrasMap = map.extras ?? {};
1604
+ if (Object.keys(extrasMap).length === 0) return null;
1605
+ for (const logical of Object.keys(extrasMap)) {
1606
+ assertSafeLogical(logical);
1607
+ const localRoot = map.projects[logical]?.[HOST];
1608
+ if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
1609
+ }
1610
+ return { map, extrasMap };
1611
+ }
1612
+ function* eachExtrasTarget(v, counts) {
1613
+ const whitelist = SUPPORTED_EXTRAS;
1614
+ for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
1615
+ const localRoot = v.map.projects[logical]?.[HOST];
1616
+ if (!localRoot || localRoot === "TBD") {
1617
+ counts.unmapped++;
1618
+ continue;
1619
+ }
1620
+ for (const dirname10 of dirnames) {
1621
+ if (!whitelist.includes(dirname10)) {
1622
+ counts.skipped++;
1623
+ continue;
1624
+ }
1625
+ yield { logical, localRoot, dirname: dirname10 };
1626
+ }
1627
+ }
1628
+ }
1629
+ function copyExtrasOverlayFiltered(src, dst, blockSet) {
1630
+ stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
1631
+ cpSyncGuarded(
1632
+ src,
1633
+ dst,
1634
+ (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry)),
1635
+ "copyExtrasOverlayFiltered"
1636
+ );
1637
+ }
1638
+ function copyExtrasOverlaySkipDiverged(src, dst, blockSet, divergedSet) {
1639
+ stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
1640
+ cpSyncGuarded(
1641
+ src,
1642
+ dst,
1643
+ (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry)) && !divergedSet.has(relative(src, srcEntry)),
1644
+ "copyExtrasOverlaySkipDiverged"
1645
+ );
1646
+ }
1647
+ function copyExtras(src, dst) {
1648
+ rmSync2(dst, { recursive: true, force: true });
1649
+ cpSync2(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
1650
+ }
1651
+ function copyExtrasFileSkipDiverged(src, dst) {
1652
+ if (existsSync2(dst)) {
1653
+ let dstBuf;
1654
+ try {
1655
+ dstBuf = readFileSync2(dst);
1656
+ } catch {
1657
+ return;
1658
+ }
1659
+ if (!readFileSync2(src).equals(dstBuf)) return;
1660
+ }
1661
+ copyExtras(src, dst);
1662
+ }
1663
+ function extrasDenySet(dirname10) {
1664
+ return dirname10 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
1665
+ }
1666
+ function copyExtrasFiltered(src, dst, blockSet) {
1667
+ rmSync2(dst, { recursive: true, force: true });
1668
+ cpSync2(src, dst, {
1669
+ recursive: true,
1670
+ force: true,
1671
+ verbatimSymlinks: true,
1672
+ filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry))
1673
+ });
1674
+ }
1675
+ function prunePreservingDenied(src, dst, blockSet) {
1676
+ for (const name of readdirSync2(dst)) {
1677
+ if (isDeniedName(blockSet, name)) continue;
1678
+ const dstPath = join3(dst, name);
1679
+ const srcStat = lstatSync2(join3(src, name), { throwIfNoEntry: false });
1680
+ if (srcStat === void 0) {
1681
+ rmSync2(dstPath, { recursive: true, force: true });
1682
+ continue;
1683
+ }
1684
+ const dstStat = lstatSync2(dstPath);
1685
+ if (srcStat.isDirectory() && dstStat.isDirectory()) {
1686
+ prunePreservingDenied(join3(src, name), dstPath, blockSet);
1687
+ } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
1688
+ rmSync2(dstPath, { recursive: true, force: true });
1689
+ }
1690
+ }
1691
+ }
1692
+ function copyExtrasFilteredPreserving(src, dst, blockSet) {
1693
+ const dstStat = lstatSync2(dst, { throwIfNoEntry: false });
1694
+ if (dstStat !== void 0) {
1695
+ if (dstStat.isDirectory()) prunePreservingDenied(src, dst, blockSet);
1696
+ else rmSync2(dst, { recursive: true, force: true });
1697
+ }
1698
+ stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
1699
+ cpSync2(src, dst, {
1700
+ recursive: true,
1701
+ force: true,
1702
+ verbatimSymlinks: true,
1703
+ filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry))
1704
+ });
1705
+ }
1706
+
1707
+ // src/links.ts
1708
+ init_utils();
1709
+ init_utils_fs();
1710
+ init_utils_json();
1711
+ function emitAutoMove(onPreview, linkPath, ts, name) {
1712
+ if (onPreview) {
1713
+ onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
1714
+ } else {
1715
+ log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
1716
+ }
1717
+ }
1718
+ function emitCreate(onPreview, from, to) {
1719
+ if (onPreview) {
1720
+ onPreview({ kind: "create", from, to });
1721
+ } else {
1722
+ log(`would create symlink: ${from} -> ${to}`);
1723
+ }
1724
+ }
1725
+ function emitCopy(onPreview, from, to) {
1726
+ if (onPreview) {
1727
+ onPreview({ kind: "copy", from, to });
1728
+ } else {
1729
+ log(`would copy: ${from} -> ${to}`);
1730
+ }
1731
+ }
1732
+ function isAlreadySymlink(linkPath) {
1733
+ return existsSync4(linkPath) && lstatSync4(linkPath).isSymbolicLink();
1734
+ }
1735
+ function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
1736
+ for (const name of linkNames) {
1737
+ const linkPath = join5(claude, name);
1738
+ const target = join5(repo, "shared", name);
1739
+ if (!existsSync4(linkPath)) continue;
1740
+ if (lstatSync4(linkPath).isSymbolicLink()) continue;
1741
+ if (!existsSync4(target)) continue;
1742
+ if (dryRun) {
1743
+ emitAutoMove(onPreview, linkPath, ts, name);
1744
+ continue;
1745
+ }
1746
+ backupBeforeWrite(linkPath, ts);
1747
+ rmSync3(linkPath, { recursive: true, force: true });
1748
+ }
1749
+ }
1750
+ function copySharedLinkPull(src, dst) {
1751
+ copyExtrasFilteredPreservingBy(src, dst, (name) => isDeniedName(ALWAYS_NEVER_SYNC, name));
1752
+ }
1753
+ function applySharedLinksWin32(linkNames, claude, repo, ts, dryRun, onPreview) {
1754
+ for (const name of linkNames) {
1755
+ const target = join5(repo, "shared", name);
1756
+ if (!existsSync4(target)) continue;
1757
+ const linkPath = join5(claude, name);
1758
+ if (dryRun) {
1759
+ emitCopy(onPreview, linkPath, target);
1760
+ continue;
1761
+ }
1762
+ const stat = lstatSync4(linkPath, { throwIfNoEntry: false });
1763
+ if (stat !== void 0) {
1764
+ backupBeforeWrite(linkPath, ts);
1765
+ if (stat.isSymbolicLink()) {
1766
+ rmSync3(linkPath, { recursive: true, force: true });
1767
+ }
1768
+ }
1769
+ copySharedLinkPull(target, linkPath);
1770
+ }
1771
+ }
1772
+ function applySharedLinks(ts, map, opts = {}) {
1773
+ const dryRun = opts.dryRun === true;
1774
+ const claude = claudeHome();
1775
+ const repo = repoHome();
1776
+ const linkNames = allSharedLinks(map);
1777
+ if (process.platform === "win32") {
1778
+ applySharedLinksWin32(linkNames, claude, repo, ts, dryRun, opts.onPreview);
1779
+ return;
1780
+ }
1781
+ runAutoMovePasses(linkNames, claude, repo, ts, dryRun, opts.onPreview);
1782
+ for (const name of linkNames) {
1783
+ const target = join5(repo, "shared", name);
1784
+ if (!existsSync4(target)) continue;
1785
+ const linkPath = join5(claude, name);
1786
+ if (isAlreadySymlink(linkPath)) continue;
1787
+ if (dryRun) {
1788
+ emitCreate(opts.onPreview, linkPath, target);
1789
+ continue;
1790
+ }
1791
+ ensureSymlink(linkPath, target);
1792
+ }
1793
+ }
1794
+ function syncSharedLinksPush(map) {
1795
+ if (process.platform !== "win32") return;
1796
+ if (map === null) return;
1797
+ const claude = claudeHome();
1798
+ const repo = repoHome();
1799
+ for (const name of allSharedLinks(map)) {
1800
+ const localPath = join5(claude, name);
1801
+ const stat = lstatSync4(localPath, { throwIfNoEntry: false });
1802
+ if (stat === void 0) continue;
1803
+ if (stat.isSymbolicLink()) continue;
1804
+ copyExtrasFiltered(localPath, join5(repo, "shared", name), ALWAYS_NEVER_SYNC);
1805
+ }
1806
+ }
1807
+ function regenerateSettings(ts, opts = {}) {
1808
+ const dryRun = opts.dryRun === true;
1809
+ const suppressDriftWarn = opts.suppressDriftWarn === true;
1810
+ const repo = repoHome();
1811
+ const claude = claudeHome();
1812
+ const basePath = join5(repo, "shared", "settings.base.json");
1813
+ const hostPath = join5(repo, "hosts", `${HOST}.json`);
1814
+ if (!existsSync4(basePath)) {
1815
+ die("repo not initialized; run 'nomad init' to scaffold");
1816
+ }
1817
+ const base = readJson(basePath);
1818
+ const hasOverrides = existsSync4(hostPath);
1819
+ const overrides = hasOverrides ? readJson(hostPath) : {};
1820
+ const merged = deepMerge(base, overrides);
1821
+ const settingsPath = join5(claude, "settings.json");
1822
+ if (!suppressDriftWarn && existsSync4(settingsPath)) {
1823
+ try {
1824
+ const existing = readJson(settingsPath);
1825
+ const drift = classifySettingsDrift(merged, existing);
1826
+ if (drift.behind.length > 0) {
1827
+ const { phrase, pronoun } = describeSettings(drift.behind);
1828
+ warn(
1829
+ `your settings.json is missing ${phrase} that the synced copy has; run 'nomad pull' to restore ${pronoun}.`
1830
+ );
1831
+ }
1832
+ const { promotable } = partitionByCaptureExclusion(drift.ahead);
1833
+ if (promotable.length > 0) {
1834
+ const { phrase, pronoun, verb } = describeSettings(promotable);
1835
+ warn(
1836
+ `your settings.json has ${phrase} that ${verb} not yet synced; run 'nomad capture-settings' to save ${pronoun} to the repo before the next pull overwrites ${pronoun}.`
1837
+ );
1838
+ }
1839
+ } catch {
1840
+ warn("existing settings.json is malformed; skipping drift-check and regenerating.");
1841
+ }
1842
+ }
1843
+ const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
1844
+ if (dryRun) {
1845
+ log(`would write settings.json (base + ${overrideLabel})`);
1846
+ return { label: overrideLabel };
1847
+ }
1848
+ backupBeforeWrite(settingsPath, ts);
1849
+ writeJsonAtomic(settingsPath, stripGsdHookEntries(merged));
1850
+ return { label: overrideLabel };
1851
+ }
1852
+
1853
+ // src/commands.adopt.ts
1854
+ init_utils();
1855
+ init_utils_fs();
1856
+ init_utils_json();
1857
+ var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
1858
+ function lexists(p) {
1859
+ try {
1860
+ lstatSync5(p);
1861
+ return true;
1862
+ } catch {
1863
+ return false;
1864
+ }
1865
+ }
1866
+ function readMapIfPresent(repoHome2) {
1867
+ const mapPath = join6(repoHome2, "path-map.json");
1868
+ return existsSync5(mapPath) ? readPathMap(mapPath) : { projects: {} };
1869
+ }
1870
+ function isConfiguredTarget(name, map) {
1871
+ return SHARED_LINKS.includes(name) || (map.sharedDirs?.includes(name) ?? false);
1872
+ }
1873
+ function isValidAdoptName(name) {
1874
+ if (SHARED_LINKS.includes(name)) return true;
1875
+ return isValidSharedDir(name);
1876
+ }
1877
+ function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
1878
+ const ts = freshBackupTs(backup);
1879
+ backupBeforeWrite(linkPath, ts);
1880
+ cpSync4(linkPath, sharedTarget, { recursive: true, force: true, preserveTimestamps: true });
1881
+ rmSync4(linkPath, { recursive: true, force: true });
1882
+ if (process.platform === "win32") {
1883
+ copySharedLinkPull(sharedTarget, linkPath);
1884
+ } else {
1885
+ ensureSymlink(linkPath, sharedTarget);
1886
+ }
1887
+ const rel = join6("shared", name);
1888
+ gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
1889
+ log(`adopted ${name}; ${ADOPT_PUSH_HINT}`);
1890
+ }
1891
+ function cmdAdopt(name, opts = {}) {
1892
+ const dryRun = opts.dryRun === true;
1893
+ if (!isValidAdoptName(name)) {
1894
+ fail(`invalid name: ${JSON.stringify(name)}`);
1895
+ process.exit(1);
1896
+ }
1897
+ const repo = repoHome();
1898
+ const claude = claudeHome();
1899
+ const backup = backupBase();
1900
+ const map = readMapIfPresent(repo);
1901
+ if (!isConfiguredTarget(name, map)) {
1902
+ fail(
1903
+ `${name}: not a configured shared target. Add it to sharedDirs in path-map.json first, then re-run adopt.`
1904
+ );
1905
+ process.exit(1);
1906
+ }
1907
+ const linkPath = join6(claude, name);
1908
+ const sharedTarget = join6(repo, "shared", name);
1909
+ if (!existsSync5(linkPath)) {
1910
+ log(`${name}: nothing to adopt (not present in ~/.claude/)`);
1911
+ return;
1912
+ }
1913
+ if (lstatSync5(linkPath).isSymbolicLink()) {
1914
+ log(`${name}: already adopted (already a symlink)`);
1915
+ return;
1916
+ }
1917
+ if (process.platform === "win32" && lexists(sharedTarget)) {
1918
+ log(`${name}: already adopted (win32 copy-sync); run \`nomad pull\` to refresh the local copy`);
1919
+ return;
1920
+ }
1921
+ if (lexists(sharedTarget)) {
1922
+ fail(`${name}: shared/${name} already exists; would clobber. Remove it first.`);
1923
+ process.exit(1);
1924
+ }
1925
+ if (dryRun) {
1926
+ const ts = freshBackupTs(backup);
1927
+ log(`would backup: ${linkPath} -> backup/${ts}/${name}`);
1928
+ log(`would move: ${linkPath} -> shared/${name}`);
1929
+ log(
1930
+ process.platform === "win32" ? `would copy back: shared/${name} -> ${linkPath} (win32 copy-sync)` : `would relink: ${linkPath} -> shared/${name}`
1931
+ );
1932
+ log(`would stage: shared/${name}`);
1933
+ return;
1934
+ }
1935
+ try {
1936
+ performAdoptMove(name, linkPath, sharedTarget, repo, backup);
1937
+ } catch (err) {
1938
+ if (!(err instanceof NomadFatal)) throw err;
1939
+ fail(err.message);
1940
+ process.exitCode = 1;
1941
+ }
1942
+ }
1943
+
1944
+ // src/commands.allow.ts
1945
+ init_config();
1946
+ import { existsSync as existsSync6 } from "node:fs";
1947
+
1948
+ // src/commands.redact.core.ts
1949
+ import { appendFileSync, readFileSync as readFileSync3 } from "node:fs";
1950
+ import { join as join7 } from "node:path";
1951
+ function collectMatchIntervals(content, findings) {
1952
+ const intervals = [];
1953
+ for (const f of findings) {
1954
+ const match = f.Match;
1955
+ if (match === "") continue;
1956
+ let from = 0;
1957
+ let pos = content.indexOf(match, from);
1958
+ while (pos !== -1) {
1959
+ intervals.push({ start: pos, end: pos + match.length, ruleId: f.RuleID });
1960
+ from = pos + match.length;
1961
+ pos = content.indexOf(match, from);
1962
+ }
1963
+ }
1964
+ return intervals;
1965
+ }
1966
+ function mergeIntervals(intervals) {
1967
+ if (intervals.length === 0) return [];
1968
+ const sorted = [...intervals].sort((a, b) => a.start - b.start || b.end - a.end);
1969
+ let last = { ...sorted[0] };
1970
+ const merged = [last];
1971
+ for (let i = 1; i < sorted.length; i++) {
1972
+ const cur = sorted[i];
1973
+ if (cur.start <= last.end) {
1974
+ if (cur.end > last.end) last.end = cur.end;
1975
+ } else {
1976
+ last = { ...cur };
1977
+ merged.push(last);
1978
+ }
1979
+ }
1980
+ return merged;
1981
+ }
1982
+ function applyRedactions(content, findings) {
1983
+ const raw = collectMatchIntervals(content, findings);
1984
+ if (raw.length === 0) return content;
1985
+ const merged = mergeIntervals(raw);
1986
+ let result = content;
1987
+ for (let i = merged.length - 1; i >= 0; i--) {
1988
+ const { start, end, ruleId } = merged[i];
1989
+ result = result.slice(0, start) + `[REDACTED:${ruleId}]` + result.slice(end);
1990
+ }
1991
+ return result;
1992
+ }
1993
+ function formatFingerprint(fingerprint) {
1994
+ return fingerprint.replace(/[\r\n]/g, "") + "\n";
1671
1995
  }
1672
- function applySharedLinks(ts, map, opts = {}) {
1673
- const dryRun = opts.dryRun === true;
1674
- const claude = claudeHome();
1675
- const repo = repoHome();
1676
- const linkNames = allSharedLinks(map);
1677
- runAutoMovePasses(linkNames, claude, repo, ts, dryRun, opts.onPreview);
1678
- for (const name of linkNames) {
1679
- const target = join5(repo, "shared", name);
1680
- if (!existsSync4(target)) continue;
1681
- const linkPath = join5(claude, name);
1682
- if (isAlreadySymlink(linkPath)) continue;
1683
- if (dryRun) {
1684
- emitCreate(opts.onPreview, linkPath, target);
1685
- continue;
1686
- }
1687
- ensureSymlink(linkPath, target);
1996
+ function isRecentlyModified(mtimeMs, nowMs, thresholdMs = 5 * 60 * 1e3) {
1997
+ return nowMs - mtimeMs < thresholdMs;
1998
+ }
1999
+ function isValidFingerprint(fingerprint) {
2000
+ return fingerprint.trim().length > 0 && fingerprint.length <= 512 && /^[^\r\n]+$/.test(fingerprint);
2001
+ }
2002
+ function isAlreadyPresent(line, lines) {
2003
+ return lines.includes(line);
2004
+ }
2005
+ function appendGitleaksIgnore(fingerprint, repo) {
2006
+ const sanitized = fingerprint.replace(/[\r\n]/g, "").trim();
2007
+ if (sanitized.length === 0) return;
2008
+ const ignPath = join7(repo, ".gitleaksignore");
2009
+ let raw;
2010
+ try {
2011
+ raw = readFileSync3(ignPath, "utf8");
2012
+ } catch {
2013
+ raw = "";
1688
2014
  }
2015
+ const existing = raw.split("\n").filter((l) => l.length > 0);
2016
+ if (isAlreadyPresent(sanitized, existing)) return;
2017
+ const needsLeadingNewline = raw.length > 0 && !raw.endsWith("\n");
2018
+ const prefix = needsLeadingNewline ? "\n" : "";
2019
+ appendFileSync(ignPath, prefix + formatFingerprint(sanitized), "utf8");
1689
2020
  }
1690
- function regenerateSettings(ts, opts = {}) {
1691
- const dryRun = opts.dryRun === true;
1692
- const suppressDriftWarn = opts.suppressDriftWarn === true;
2021
+
2022
+ // src/commands.allow.ts
2023
+ init_utils();
2024
+ function cmdAllow(fingerprints) {
1693
2025
  const repo = repoHome();
1694
- const claude = claudeHome();
1695
- const basePath = join5(repo, "shared", "settings.base.json");
1696
- const hostPath = join5(repo, "hosts", `${HOST}.json`);
1697
- if (!existsSync4(basePath)) {
1698
- die("repo not initialized; run 'nomad init' to scaffold");
1699
- }
1700
- const base = readJson(basePath);
1701
- const hasOverrides = existsSync4(hostPath);
1702
- const overrides = hasOverrides ? readJson(hostPath) : {};
1703
- const merged = deepMerge(base, overrides);
1704
- const settingsPath = join5(claude, "settings.json");
1705
- if (!suppressDriftWarn && existsSync4(settingsPath)) {
1706
- try {
1707
- const existing = readJson(settingsPath);
1708
- const drift = classifySettingsDrift(merged, existing);
1709
- if (drift.behind.length > 0) {
1710
- const { phrase, pronoun } = describeSettings(drift.behind);
1711
- warn(
1712
- `your settings.json is missing ${phrase} that the synced copy has; run 'nomad pull' to restore ${pronoun}.`
1713
- );
1714
- }
1715
- const { promotable } = partitionByCaptureExclusion(drift.ahead);
1716
- if (promotable.length > 0) {
1717
- const { phrase, pronoun, verb } = describeSettings(promotable);
1718
- warn(
1719
- `your settings.json has ${phrase} that ${verb} not yet synced; run 'nomad capture-settings' to save ${pronoun} to the repo before the next pull overwrites ${pronoun}.`
1720
- );
1721
- }
1722
- } catch {
1723
- warn("existing settings.json is malformed; skipping drift-check and regenerating.");
2026
+ if (!existsSync6(repo)) die(`repo not cloned at ${repo}`);
2027
+ for (const fp of fingerprints) {
2028
+ if (!isValidFingerprint(fp)) {
2029
+ const shown = fp.replaceAll("\r", String.raw`\r`).replaceAll("\n", String.raw`\n`);
2030
+ fail(`invalid fingerprint: ${shown}`);
2031
+ process.exit(1);
1724
2032
  }
1725
2033
  }
1726
- const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
1727
- if (dryRun) {
1728
- log(`would write settings.json (base + ${overrideLabel})`);
1729
- return { label: overrideLabel };
2034
+ for (const fp of fingerprints) {
2035
+ appendGitleaksIgnore(fp, repo);
2036
+ item(`allowed: ${fp}`);
1730
2037
  }
1731
- backupBeforeWrite(settingsPath, ts);
1732
- writeJsonAtomic(settingsPath, stripGsdHookEntries(merged));
1733
- return { label: overrideLabel };
2038
+ log(`allowed ${fingerprints.length} fingerprint(s)`);
1734
2039
  }
1735
2040
 
1736
2041
  // src/commands.capture-settings.ts
2042
+ init_config();
2043
+ import { existsSync as existsSync7 } from "node:fs";
2044
+ import { join as join9 } from "node:path";
2045
+ import { createInterface } from "node:readline/promises";
1737
2046
  init_utils_fs();
1738
2047
  init_utils_json();
1739
2048
 
1740
2049
  // src/utils.lockfile.ts
1741
2050
  init_config();
1742
2051
  init_utils();
1743
- import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
1744
- import { dirname as dirname2, join as join6 } from "node:path";
2052
+ import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2053
+ import { dirname as dirname2, join as join8 } from "node:path";
1745
2054
  function lockFilePath() {
1746
- return join6(home(), ".cache", "claude-nomad", "nomad.lock");
2055
+ return join8(home(), ".cache", "claude-nomad", "nomad.lock");
1747
2056
  }
1748
2057
  function acquireLock(verb) {
1749
2058
  const lp = lockFilePath();
@@ -1786,7 +2095,7 @@ function releaseLock(handle) {
1786
2095
  function unlinkIfSamePid(expectedPidStr, lp) {
1787
2096
  let current;
1788
2097
  try {
1789
- current = readFileSync3(lp, "utf8").trim();
2098
+ current = readFileSync4(lp, "utf8").trim();
1790
2099
  } catch {
1791
2100
  return false;
1792
2101
  }
@@ -1801,7 +2110,7 @@ function unlinkIfSamePid(expectedPidStr, lp) {
1801
2110
  function checkStaleAndRetry(verb, lp) {
1802
2111
  let pidStr;
1803
2112
  try {
1804
- pidStr = readFileSync3(lp, "utf8").trim();
2113
+ pidStr = readFileSync4(lp, "utf8").trim();
1805
2114
  } catch {
1806
2115
  pidStr = "";
1807
2116
  }
@@ -1869,30 +2178,30 @@ async function confirmCapture(destLabel, keys) {
1869
2178
  }
1870
2179
  }
1871
2180
  function resolveCaptureDestination(repo, useHost) {
1872
- const destPath = useHost ? join7(repo, "hosts", `${HOST}.json`) : join7(repo, "shared", "settings.base.json");
1873
- const existing = existsSync5(destPath) ? readJson(destPath) : {};
2181
+ const destPath = useHost ? join9(repo, "hosts", `${HOST}.json`) : join9(repo, "shared", "settings.base.json");
2182
+ const existing = existsSync7(destPath) ? readJson(destPath) : {};
1874
2183
  return { destPath, existing };
1875
2184
  }
1876
2185
  async function cmdCaptureSettings(opts) {
1877
2186
  const { host: useHost, dryRun } = opts;
1878
2187
  const repo = repoHome();
1879
- if (!existsSync5(repo)) die(`repo not cloned at ${repo}`);
2188
+ if (!existsSync7(repo)) die(`repo not cloned at ${repo}`);
1880
2189
  const handle = acquireLock("capture-settings");
1881
2190
  if (handle === null) process.exit(0);
1882
2191
  try {
1883
2192
  const claude = claudeHome();
1884
- const basePath = join7(repo, "shared", "settings.base.json");
1885
- if (!existsSync5(basePath)) {
2193
+ const basePath = join9(repo, "shared", "settings.base.json");
2194
+ if (!existsSync7(basePath)) {
1886
2195
  die("repo not initialized; run 'nomad init' to scaffold");
1887
2196
  }
1888
- const settingsPath = join7(claude, "settings.json");
1889
- if (!existsSync5(settingsPath)) {
2197
+ const settingsPath = join9(claude, "settings.json");
2198
+ if (!existsSync7(settingsPath)) {
1890
2199
  log("no ~/.claude/settings.json found; nothing to capture");
1891
2200
  return;
1892
2201
  }
1893
2202
  const base = readJson(basePath);
1894
- const hostPath = join7(repo, "hosts", `${HOST}.json`);
1895
- const overrides = existsSync5(hostPath) ? readJson(hostPath) : {};
2203
+ const hostPath = join9(repo, "hosts", `${HOST}.json`);
2204
+ const overrides = existsSync7(hostPath) ? readJson(hostPath) : {};
1896
2205
  const merged = deepMerge(base, overrides);
1897
2206
  const settings = readJson(settingsPath);
1898
2207
  const subset = buildCaptureSubset(merged, settings, { normalizeNodePath: !useHost });
@@ -1929,8 +2238,8 @@ async function cmdCaptureSettings(opts) {
1929
2238
  // src/commands.clean.ts
1930
2239
  init_config();
1931
2240
  init_utils();
1932
- import { existsSync as existsSync6, lstatSync as lstatSync4, readdirSync, rmSync as rmSync3, statSync as statSync2 } from "node:fs";
1933
- import { join as join8 } from "node:path";
2241
+ import { existsSync as existsSync8, lstatSync as lstatSync6, readdirSync as readdirSync3, rmSync as rmSync5, statSync as statSync2 } from "node:fs";
2242
+ import { join as join10 } from "node:path";
1934
2243
  var TS_SHAPE = /^\d{8}-\d{6}(-\d+)?$/;
1935
2244
  var DURATION_RE = /^(\d+)([dhm])$/;
1936
2245
  var UNIT_MS = { d: 864e5, h: 36e5, m: 6e4 };
@@ -1944,8 +2253,8 @@ function parseDuration(s) {
1944
2253
  return Number(m[1]) * UNIT_MS[m[2]];
1945
2254
  }
1946
2255
  function listBackupDirs(backupBase2) {
1947
- if (!existsSync6(backupBase2)) return [];
1948
- return readdirSync(backupBase2).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join8(backupBase2, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
2256
+ if (!existsSync8(backupBase2)) return [];
2257
+ return readdirSync3(backupBase2).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join10(backupBase2, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
1949
2258
  }
1950
2259
  function prunableByAge(dirs, olderThanMs, nowMs) {
1951
2260
  return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
@@ -1955,10 +2264,10 @@ function prunableByCount(dirs, keep) {
1955
2264
  }
1956
2265
  function safeDelete(backupBase2, name) {
1957
2266
  if (!isTsDir(name)) return;
1958
- const full = join8(backupBase2, name);
1959
- const st = lstatSync4(full, { throwIfNoEntry: false });
2267
+ const full = join10(backupBase2, name);
2268
+ const st = lstatSync6(full, { throwIfNoEntry: false });
1960
2269
  if (!st || st.isSymbolicLink() || !st.isDirectory()) return;
1961
- rmSync3(full, { recursive: true, force: true });
2270
+ rmSync5(full, { recursive: true, force: true });
1962
2271
  }
1963
2272
  function resolveTargets(dirs, olderThanMs, keep) {
1964
2273
  if (keep !== void 0) return prunableByCount(dirs, keep);
@@ -1993,9 +2302,10 @@ function cmdClean(opts, backupBase2 = backupBase()) {
1993
2302
  // src/commands.eject.ts
1994
2303
  init_config();
1995
2304
  init_utils();
2305
+ init_utils_fs();
1996
2306
  init_utils_json();
1997
- import { cpSync as cpSync3, existsSync as existsSync7, lstatSync as lstatSync5, realpathSync, renameSync as renameSync2, rmSync as rmSync4 } from "node:fs";
1998
- import { join as join9, sep as sep2 } from "node:path";
2307
+ import { cpSync as cpSync5, existsSync as existsSync9, lstatSync as lstatSync7, realpathSync, rmSync as rmSync6 } from "node:fs";
2308
+ import { join as join11, sep as sep3 } from "node:path";
1999
2309
  function ejectChecklist() {
2000
2310
  return [
2001
2311
  "Manual steps remaining to finish leaving claude-nomad on this host:",
@@ -2011,33 +2321,33 @@ function errMessage(err) {
2011
2321
  }
2012
2322
  function lexists2(p) {
2013
2323
  try {
2014
- lstatSync5(p);
2324
+ lstatSync7(p);
2015
2325
  return true;
2016
2326
  } catch {
2017
2327
  return false;
2018
2328
  }
2019
2329
  }
2020
2330
  function readMapIfPresent2(repoHome2) {
2021
- const mapPath = join9(repoHome2, "path-map.json");
2022
- return existsSync7(mapPath) ? readPathMap(mapPath) : { projects: {} };
2331
+ const mapPath = join11(repoHome2, "path-map.json");
2332
+ return existsSync9(mapPath) ? readPathMap(mapPath) : { projects: {} };
2023
2333
  }
2024
2334
  function classifyName(linkPath) {
2025
2335
  if (!lexists2(linkPath)) return "absent";
2026
- if (!lstatSync5(linkPath).isSymbolicLink()) return "skip-real";
2027
- if (!existsSync7(linkPath)) return "dangling";
2336
+ if (!lstatSync7(linkPath).isSymbolicLink()) return "skip-real";
2337
+ if (!existsSync9(linkPath)) return "dangling";
2028
2338
  return "materialize";
2029
2339
  }
2030
2340
  function resolveSharedRoot(repoHome2) {
2031
2341
  try {
2032
- return realpathSync(join9(repoHome2, "shared"));
2342
+ return realpathSync(join11(repoHome2, "shared"));
2033
2343
  } catch {
2034
2344
  return die(
2035
- `cannot resolve ${join9(repoHome2, "shared")} (repo checkout incomplete). run \`nomad pull\` first, then re-run \`nomad eject\``
2345
+ `cannot resolve ${join11(repoHome2, "shared")} (repo checkout incomplete). run \`nomad pull\` first, then re-run \`nomad eject\``
2036
2346
  );
2037
2347
  }
2038
2348
  }
2039
2349
  function isManagedTarget(target, sharedRoot) {
2040
- return target.startsWith(sharedRoot + sep2);
2350
+ return target.startsWith(sharedRoot + sep3);
2041
2351
  }
2042
2352
  function materializeOne(name, linkPath, sharedRoot) {
2043
2353
  const target = realpathSync(linkPath);
@@ -2047,33 +2357,36 @@ function materializeOne(name, linkPath, sharedRoot) {
2047
2357
  }
2048
2358
  const tmp = `${linkPath}.eject.tmp.${process.pid}.${Date.now()}`;
2049
2359
  try {
2050
- rmSync4(tmp, { recursive: true, force: true });
2051
- cpSync3(target, tmp, {
2360
+ rmSync6(tmp, { recursive: true, force: true });
2361
+ cpSync5(target, tmp, {
2052
2362
  recursive: true,
2053
2363
  force: true,
2054
2364
  dereference: true,
2055
2365
  preserveTimestamps: true
2056
2366
  });
2057
- rmSync4(linkPath, { force: true });
2058
- renameSync2(tmp, linkPath);
2367
+ rmSync6(linkPath, { force: true });
2368
+ renameAtomicRetry(tmp, linkPath);
2059
2369
  item(`ejected: ${name}`);
2060
2370
  return true;
2061
2371
  } catch (err) {
2062
2372
  try {
2063
- rmSync4(tmp, { recursive: true, force: true });
2373
+ rmSync6(tmp, { recursive: true, force: true });
2064
2374
  } catch {
2065
2375
  }
2066
2376
  throw err;
2067
2377
  }
2068
2378
  }
2379
+ function skipRealMessage(name) {
2380
+ return process.platform === "win32" ? `already a real copy (win32 copy-sync): ${name}` : `skipped (not a symlink): ${name}`;
2381
+ }
2069
2382
  function previewDryRun(names, classifications, claudeHome2, sharedRoot) {
2070
2383
  for (const name of names) {
2071
2384
  const cls = classifications.get(name);
2072
- const linkPath = join9(claudeHome2, name);
2385
+ const linkPath = join11(claudeHome2, name);
2073
2386
  if (cls === "absent") {
2074
2387
  item(`skipped (absent): ${name}`);
2075
2388
  } else if (cls === "skip-real") {
2076
- item(`skipped (not a symlink): ${name}`);
2389
+ item(skipRealMessage(name));
2077
2390
  } else {
2078
2391
  previewMaterialize(name, linkPath, sharedRoot);
2079
2392
  }
@@ -2099,12 +2412,12 @@ function runLiveEject(names, classifications, claudeHome2, sharedRoot) {
2099
2412
  let skipped = 0;
2100
2413
  for (const name of names) {
2101
2414
  const cls = classifications.get(name);
2102
- const linkPath = join9(claudeHome2, name);
2415
+ const linkPath = join11(claudeHome2, name);
2103
2416
  if (cls === "absent") {
2104
2417
  item(`skipped (absent): ${name}`);
2105
2418
  skipped++;
2106
2419
  } else if (cls === "skip-real") {
2107
- item(`skipped (not a symlink): ${name}`);
2420
+ item(skipRealMessage(name));
2108
2421
  skipped++;
2109
2422
  } else if (materializeOneOrDie(name, linkPath, sharedRoot, done)) {
2110
2423
  done.push(name);
@@ -2135,7 +2448,7 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
2135
2448
  const names = allSharedLinks(map);
2136
2449
  const classifications = /* @__PURE__ */ new Map();
2137
2450
  for (const name of names) {
2138
- classifications.set(name, classifyName(join9(claudeHome2, name)));
2451
+ classifications.set(name, classifyName(join11(claudeHome2, name)));
2139
2452
  }
2140
2453
  const dangling = names.filter((n) => classifications.get(n) === "dangling");
2141
2454
  if (dangling.length > 0) {
@@ -2158,14 +2471,14 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
2158
2471
  }
2159
2472
 
2160
2473
  // src/commands.doctor.ts
2161
- import { existsSync as existsSync32 } from "node:fs";
2162
- import { join as join38 } from "node:path";
2474
+ import { existsSync as existsSync34 } from "node:fs";
2475
+ import { join as join40 } from "node:path";
2163
2476
 
2164
2477
  // src/commands.doctor.checks.repo.ts
2165
2478
  init_color();
2166
2479
  init_config();
2167
- import { existsSync as existsSync9, lstatSync as lstatSync6, readdirSync as readdirSync2, statSync as statSync3 } from "node:fs";
2168
- import { join as join11 } from "node:path";
2480
+ import { existsSync as existsSync11, lstatSync as lstatSync8, readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
2481
+ import { join as join13 } from "node:path";
2169
2482
 
2170
2483
  // src/commands.doctor.format.ts
2171
2484
  init_color();
@@ -2243,15 +2556,15 @@ function readJsonSafe(path, label, section2) {
2243
2556
  // src/init.classify.ts
2244
2557
  init_config();
2245
2558
  init_utils_json();
2246
- import { existsSync as existsSync8 } from "node:fs";
2247
- import { join as join10 } from "node:path";
2559
+ import { existsSync as existsSync10 } from "node:fs";
2560
+ import { join as join12 } from "node:path";
2248
2561
  function classifyRepoState(repoHome2, host) {
2249
- const basePath = join10(repoHome2, "shared", "settings.base.json");
2250
- const mapPath = join10(repoHome2, "path-map.json");
2251
- const hostPath = join10(repoHome2, "hosts", `${host}.json`);
2252
- const hasBase = existsSync8(basePath);
2253
- const hasMap = existsSync8(mapPath);
2254
- const hasHost = existsSync8(hostPath);
2562
+ const basePath = join12(repoHome2, "shared", "settings.base.json");
2563
+ const mapPath = join12(repoHome2, "path-map.json");
2564
+ const hostPath = join12(repoHome2, "hosts", `${host}.json`);
2565
+ const hasBase = existsSync10(basePath);
2566
+ const hasMap = existsSync10(mapPath);
2567
+ const hasHost = existsSync10(hostPath);
2255
2568
  let mapEntryCount = 0;
2256
2569
  if (hasMap) {
2257
2570
  try {
@@ -2266,11 +2579,11 @@ function classifyRepoState(repoHome2, host) {
2266
2579
  return "partial";
2267
2580
  }
2268
2581
  function reasonForPartial(repoHome2, host) {
2269
- const basePath = join10(repoHome2, "shared", "settings.base.json");
2270
- const mapPath = join10(repoHome2, "path-map.json");
2271
- const hostPath = join10(repoHome2, "hosts", `${host}.json`);
2272
- if (!existsSync8(basePath)) return "- shared/settings.base.json missing";
2273
- if (!existsSync8(mapPath)) return "- path-map.json missing";
2582
+ const basePath = join12(repoHome2, "shared", "settings.base.json");
2583
+ const mapPath = join12(repoHome2, "path-map.json");
2584
+ const hostPath = join12(repoHome2, "hosts", `${host}.json`);
2585
+ if (!existsSync10(basePath)) return "- shared/settings.base.json missing";
2586
+ if (!existsSync10(mapPath)) return "- path-map.json missing";
2274
2587
  let mapEntryCount;
2275
2588
  try {
2276
2589
  const map = readJson(mapPath);
@@ -2279,7 +2592,7 @@ function reasonForPartial(repoHome2, host) {
2279
2592
  mapEntryCount = 0;
2280
2593
  }
2281
2594
  if (mapEntryCount === 0) return "- path-map.json.projects has no entries";
2282
- if (!existsSync8(hostPath)) return `- hosts/${host}.json missing`;
2595
+ if (!existsSync10(hostPath)) return `- hosts/${host}.json missing`;
2283
2596
  return "- partial state (unknown gap)";
2284
2597
  }
2285
2598
 
@@ -2296,15 +2609,15 @@ function reportHostAndPaths(section2) {
2296
2609
  if (isOverrideActive()) {
2297
2610
  addItem(section2, `${dim(infoGlyph)} NOMAD_REPO: ${blue(repo)}`);
2298
2611
  }
2299
- addItem(section2, `${existsSync9(repo) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(repo)}`);
2612
+ addItem(section2, `${existsSync11(repo) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(repo)}`);
2300
2613
  addItem(
2301
2614
  section2,
2302
- `${existsSync9(claude) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(claude)}`
2615
+ `${existsSync11(claude) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(claude)}`
2303
2616
  );
2304
2617
  }
2305
2618
  function pathMapHostKeys() {
2306
- const mapPath = join11(repoHome(), "path-map.json");
2307
- if (!existsSync9(mapPath)) return /* @__PURE__ */ new Set();
2619
+ const mapPath = join13(repoHome(), "path-map.json");
2620
+ if (!existsSync11(mapPath)) return /* @__PURE__ */ new Set();
2308
2621
  let raw;
2309
2622
  try {
2310
2623
  raw = readJson(mapPath);
@@ -2321,7 +2634,7 @@ function pathMapHostKeys() {
2321
2634
  function hostOverrideLabels() {
2322
2635
  let entries;
2323
2636
  try {
2324
- entries = readdirSync2(join11(repoHome(), "hosts"));
2637
+ entries = readdirSync4(join13(repoHome(), "hosts"));
2325
2638
  } catch {
2326
2639
  return /* @__PURE__ */ new Set();
2327
2640
  }
@@ -2362,12 +2675,12 @@ function reportRepoState(section2) {
2362
2675
  }
2363
2676
  }
2364
2677
  function repoHasSharedSource(name) {
2365
- return existsSync9(join11(repoHome(), "shared", name));
2678
+ return existsSync11(join13(repoHome(), "shared", name));
2366
2679
  }
2367
2680
  function classifySharedLink(name, p) {
2368
2681
  let stat;
2369
2682
  try {
2370
- stat = lstatSync6(p);
2683
+ stat = lstatSync8(p);
2371
2684
  } catch (err) {
2372
2685
  const code = err.code;
2373
2686
  if (code === "ENOENT") {
@@ -2379,6 +2692,12 @@ function classifySharedLink(name, p) {
2379
2692
  return { line: `${red(failGlyph)} ${name}: could not stat (${String(code)})`, fail: true };
2380
2693
  }
2381
2694
  if (!stat.isSymbolicLink()) {
2695
+ if (process.platform === "win32") {
2696
+ return {
2697
+ line: `${green(okGlyph)} ${name}: real copy (win32 copy-sync)`,
2698
+ fail: false
2699
+ };
2700
+ }
2382
2701
  return {
2383
2702
  line: `${red(failGlyph)} ${name}: NOT a symlink (blocks sync); run \`nomad adopt ${name}\` to fix`,
2384
2703
  fail: true
@@ -2410,7 +2729,7 @@ function classifySymlinkTarget(name, p) {
2410
2729
  function reportSharedLinks(section2, map) {
2411
2730
  const claude = claudeHome();
2412
2731
  for (const name of allSharedLinks(map)) {
2413
- const p = join11(claude, name);
2732
+ const p = join13(claude, name);
2414
2733
  const { line, fail: fail2 } = classifySharedLink(name, p);
2415
2734
  addItem(section2, line);
2416
2735
  if (fail2) process.exitCode = 1;
@@ -2419,10 +2738,10 @@ function reportSharedLinks(section2, map) {
2419
2738
  function reportDroppedNamesMigration(section2) {
2420
2739
  const claude = claudeHome();
2421
2740
  for (const name of GSD_DROPPED_NAMES) {
2422
- const p = join11(claude, name);
2741
+ const p = join13(claude, name);
2423
2742
  let stat;
2424
2743
  try {
2425
- stat = lstatSync6(p);
2744
+ stat = lstatSync8(p);
2426
2745
  } catch {
2427
2746
  continue;
2428
2747
  }
@@ -2437,11 +2756,11 @@ function reportDroppedNamesMigration(section2) {
2437
2756
  // src/commands.doctor.checks.settings.ts
2438
2757
  init_color();
2439
2758
  init_config();
2440
- import { existsSync as existsSync10, readdirSync as readdirSync3 } from "node:fs";
2441
- import { join as join12 } from "node:path";
2759
+ import { existsSync as existsSync12, readdirSync as readdirSync5 } from "node:fs";
2760
+ import { join as join14 } from "node:path";
2442
2761
  function loadBaseSettings(section2) {
2443
- const basePath = join12(repoHome(), "shared", "settings.base.json");
2444
- if (!existsSync10(basePath)) {
2762
+ const basePath = join14(repoHome(), "shared", "settings.base.json");
2763
+ if (!existsSync12(basePath)) {
2445
2764
  addItem(section2, `${red(failGlyph)} shared/settings.base.json missing at ${blue(basePath)}`);
2446
2765
  process.exitCode = 1;
2447
2766
  return null;
@@ -2449,8 +2768,8 @@ function loadBaseSettings(section2) {
2449
2768
  return readJsonSafe(basePath, basePath, section2);
2450
2769
  }
2451
2770
  function loadAndReportSettings(section2) {
2452
- const settingsPath = join12(claudeHome(), "settings.json");
2453
- if (!existsSync10(settingsPath)) return null;
2771
+ const settingsPath = join14(claudeHome(), "settings.json");
2772
+ if (!existsSync12(settingsPath)) return null;
2454
2773
  const settings = readJsonSafe(settingsPath, settingsPath, section2);
2455
2774
  if (settings === null) return null;
2456
2775
  const unknownKeys = Object.keys(settings).filter((k) => !KNOWN_SETTINGS_KEYS.has(k));
@@ -2466,13 +2785,13 @@ function loadAndReportSettings(section2) {
2466
2785
  }
2467
2786
  function reportHostOverrides(section2, base, settings) {
2468
2787
  const repo = repoHome();
2469
- const hostFile = join12(repo, "hosts", `${HOST}.json`);
2788
+ const hostFile = join14(repo, "hosts", `${HOST}.json`);
2470
2789
  let drift = [];
2471
2790
  if (base !== null && settings !== null) {
2472
2791
  const baseKeys = new Set(Object.keys(base));
2473
2792
  drift = Object.keys(settings).filter((k) => !baseKeys.has(k));
2474
2793
  }
2475
- if (existsSync10(hostFile)) {
2794
+ if (existsSync12(hostFile)) {
2476
2795
  if (readJsonSafe(hostFile, hostFile, section2) !== null) {
2477
2796
  addItem(section2, `${green(okGlyph)} host overrides: ${blue(hostFile)}`);
2478
2797
  }
@@ -2481,9 +2800,9 @@ function reportHostOverrides(section2, base, settings) {
2481
2800
  section2,
2482
2801
  `${red(failGlyph)} no hosts/${HOST}.json AND settings.json has unbased keys ${JSON.stringify(drift)}`
2483
2802
  );
2484
- const hostsDir = join12(repo, "hosts");
2485
- if (existsSync10(hostsDir)) {
2486
- const cands = readdirSync3(hostsDir).filter((f) => f.endsWith(".json"));
2803
+ const hostsDir = join14(repo, "hosts");
2804
+ if (existsSync12(hostsDir)) {
2805
+ const cands = readdirSync5(hostsDir).filter((f) => f.endsWith(".json"));
2487
2806
  if (cands.length > 0) addItem(section2, `${dim(infoGlyph)} candidates: ${cands.join(", ")}`);
2488
2807
  }
2489
2808
  process.exitCode = 1;
@@ -2498,8 +2817,8 @@ function reportHostOverrides(section2, base, settings) {
2498
2817
  // src/commands.doctor.checks.pathmap.ts
2499
2818
  init_color();
2500
2819
  init_config();
2501
- import { existsSync as existsSync11, readdirSync as readdirSync4 } from "node:fs";
2502
- import { join as join13 } from "node:path";
2820
+ import { existsSync as existsSync13, readdirSync as readdirSync6 } from "node:fs";
2821
+ import { join as join15 } from "node:path";
2503
2822
  init_utils_json();
2504
2823
  function reportMappedProjects(section2, map) {
2505
2824
  const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
@@ -2509,11 +2828,11 @@ function reportMappedProjects(section2, map) {
2509
2828
  }
2510
2829
  }
2511
2830
  function reportUnmappedProjects(section2, map) {
2512
- const localProjects = join13(claudeHome(), "projects");
2513
- if (!existsSync11(localProjects)) return;
2831
+ const localProjects = join15(claudeHome(), "projects");
2832
+ if (!existsSync13(localProjects)) return;
2514
2833
  let localDirs;
2515
2834
  try {
2516
- localDirs = readdirSync4(localProjects);
2835
+ localDirs = readdirSync6(localProjects);
2517
2836
  } catch {
2518
2837
  return;
2519
2838
  }
@@ -2531,7 +2850,7 @@ function reportCurrentHostPathsMissing(section2, map) {
2531
2850
  for (const [name, hosts] of Object.entries(map.projects)) {
2532
2851
  const abspath = hosts[HOST];
2533
2852
  if (!abspath || abspath === "TBD") continue;
2534
- if (!existsSync11(abspath)) {
2853
+ if (!existsSync13(abspath)) {
2535
2854
  addItem(
2536
2855
  section2,
2537
2856
  `${yellow(warnGlyph)} path-map: ${name} local path missing on ${HOST}: ${blue(abspath)}`
@@ -2562,8 +2881,8 @@ function reportPathCollisions(section2, map) {
2562
2881
  else addItem(section2, `${green(okGlyph)} path-encoding: no collisions`);
2563
2882
  }
2564
2883
  function reportPathMap(section2) {
2565
- const mapPath = join13(repoHome(), "path-map.json");
2566
- if (!existsSync11(mapPath)) {
2884
+ const mapPath = join15(repoHome(), "path-map.json");
2885
+ if (!existsSync13(mapPath)) {
2567
2886
  addItem(section2, `${red(failGlyph)} path-map.json missing at ${blue(mapPath)}`);
2568
2887
  process.exitCode = 1;
2569
2888
  return;
@@ -2592,21 +2911,19 @@ function reportNeverSync(section2) {
2592
2911
 
2593
2912
  // src/commands.doctor.checks.skills.ts
2594
2913
  init_color();
2595
- import { existsSync as existsSync12 } from "node:fs";
2596
- import { join as join14 } from "node:path";
2914
+ import { existsSync as existsSync14 } from "node:fs";
2915
+ import { join as join16 } from "node:path";
2597
2916
  init_config();
2598
2917
 
2599
2918
  // src/extras-sync.diff.ts
2600
2919
  init_utils();
2601
2920
  import { execFileSync as execFileSync2 } from "node:child_process";
2602
- import { relative as relative2 } from "node:path";
2921
+ import { relative as relative3 } from "node:path";
2603
2922
  function parseNameStatus(stdout) {
2923
+ const fields = stdout.split("\0").filter((f) => f.length > 0);
2604
2924
  const entries = [];
2605
- for (const line of stdout.split("\n")) {
2606
- if (line.length === 0) continue;
2607
- const tab = line.indexOf(" ");
2608
- if (tab === -1) continue;
2609
- entries.push({ status: line.slice(0, tab), path: line.slice(tab + 1) });
2925
+ for (let i = 0; i + 1 < fields.length; i += 2) {
2926
+ entries.push({ status: fields[i], path: fields[i + 1] });
2610
2927
  }
2611
2928
  return entries;
2612
2929
  }
@@ -2619,13 +2936,13 @@ function parseDiffOutput(stdout) {
2619
2936
  return parseNameStatus(stdout).map(labelEntry);
2620
2937
  }
2621
2938
  function parseModifiedPaths(stdout, a) {
2622
- return parseNameStatus(stdout).filter((entry) => entry.status === "M").map((entry) => relative2(a, entry.path));
2939
+ return parseNameStatus(stdout).filter((entry) => entry.status === "M").map((entry) => relative3(a, entry.path));
2623
2940
  }
2624
2941
  function runNameStatusDiff(a, b, parse) {
2625
2942
  try {
2626
2943
  const stdout = execFileSync2(
2627
2944
  "git",
2628
- ["diff", "--no-index", "--no-renames", "--name-status", a, b],
2945
+ ["diff", "--no-index", "--no-renames", "-z", "--name-status", a, b],
2629
2946
  {
2630
2947
  stdio: ["ignore", "pipe", "pipe"]
2631
2948
  }
@@ -2670,13 +2987,13 @@ function isGsdDiffLine(line, localBase, sharedBase) {
2670
2987
  return relative9.split("/")[0].startsWith(GSD_PREFIX);
2671
2988
  }
2672
2989
  function reportSkillsDivergence(section2) {
2673
- const sharedSkills = join14(repoHome(), "shared", "skills");
2674
- const localSkills = join14(claudeHome(), "skills");
2675
- if (!existsSync12(sharedSkills)) {
2990
+ const sharedSkills = join16(repoHome(), "shared", "skills");
2991
+ const localSkills = join16(claudeHome(), "skills");
2992
+ if (!existsSync14(sharedSkills)) {
2676
2993
  addItem(section2, `${dim(infoGlyph)} skills: no shared/skills/ to compare`);
2677
2994
  return;
2678
2995
  }
2679
- if (!existsSync12(localSkills)) {
2996
+ if (!existsSync14(localSkills)) {
2680
2997
  addItem(section2, `${dim(infoGlyph)} skills: no local skills/ to compare`);
2681
2998
  return;
2682
2999
  }
@@ -2699,8 +3016,8 @@ function reportSkillsDivergence(section2) {
2699
3016
  init_color();
2700
3017
  init_config();
2701
3018
  import { execFileSync as execFileSync5 } from "node:child_process";
2702
- import { existsSync as existsSync15 } from "node:fs";
2703
- import { join as join18, relative as relative3 } from "node:path";
3019
+ import { existsSync as existsSync17 } from "node:fs";
3020
+ import { join as join20, relative as relative4 } from "node:path";
2704
3021
  init_commands_pull_wedge();
2705
3022
  init_push_checks();
2706
3023
  init_utils();
@@ -2723,11 +3040,11 @@ function reportGitleaksProbe(section2) {
2723
3040
  }
2724
3041
  function reportGitlinks(section2) {
2725
3042
  const repo = repoHome();
2726
- const sharedDir = join18(repo, "shared");
2727
- if (existsSync15(sharedDir)) {
3043
+ const sharedDir = join20(repo, "shared");
3044
+ if (existsSync17(sharedDir)) {
2728
3045
  const gitlinks = findGitlinks(sharedDir);
2729
3046
  for (const p of gitlinks) {
2730
- const rel = relative3(repo, p);
3047
+ const rel = relative4(repo, p).replaceAll("\\", "/");
2731
3048
  addItem(
2732
3049
  section2,
2733
3050
  `${red(failGlyph)} gitlink: ${blue(rel)} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`
@@ -2822,13 +3139,13 @@ function reportGitIdentity(section2) {
2822
3139
 
2823
3140
  // src/commands.doctor.checks.backups.ts
2824
3141
  init_color();
2825
- import { existsSync as existsSync16, lstatSync as lstatSync7, readdirSync as readdirSync6 } from "node:fs";
2826
- import { join as join19 } from "node:path";
3142
+ import { existsSync as existsSync18, lstatSync as lstatSync9, readdirSync as readdirSync8 } from "node:fs";
3143
+ import { join as join21 } from "node:path";
2827
3144
  init_config();
2828
3145
  var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
2829
3146
  function safeReaddir(dir) {
2830
3147
  try {
2831
- return readdirSync6(dir);
3148
+ return readdirSync8(dir);
2832
3149
  } catch {
2833
3150
  return [];
2834
3151
  }
@@ -2839,8 +3156,8 @@ var BYTES_PER_MB = 1024 * 1024;
2839
3156
  function dirSizeBytes(dir) {
2840
3157
  let bytes = 0;
2841
3158
  for (const entry of safeReaddir(dir)) {
2842
- const full = join19(dir, entry);
2843
- const st = lstatSync7(full, { throwIfNoEntry: false });
3159
+ const full = join21(dir, entry);
3160
+ const st = lstatSync9(full, { throwIfNoEntry: false });
2844
3161
  if (!st) continue;
2845
3162
  if (st.isSymbolicLink()) continue;
2846
3163
  if (st.isDirectory()) bytes += dirSizeBytes(full);
@@ -2850,11 +3167,11 @@ function dirSizeBytes(dir) {
2850
3167
  }
2851
3168
  function totalSizeMb(backupBase2, dirs) {
2852
3169
  let bytes = 0;
2853
- for (const name of dirs) bytes += dirSizeBytes(join19(backupBase2, name));
3170
+ for (const name of dirs) bytes += dirSizeBytes(join21(backupBase2, name));
2854
3171
  return bytes / BYTES_PER_MB;
2855
3172
  }
2856
3173
  function reportBackupsCheck(section2, backupBase2 = backupBase()) {
2857
- if (!existsSync16(backupBase2)) return;
3174
+ if (!existsSync18(backupBase2)) return;
2858
3175
  const dirs = safeReaddir(backupBase2).filter((n) => TS_SHAPE2.test(n));
2859
3176
  const count = dirs.length;
2860
3177
  const sizeMb = totalSizeMb(backupBase2, dirs);
@@ -2935,8 +3252,8 @@ function reportCheckRemote(section2) {
2935
3252
 
2936
3253
  // src/commands.doctor.check-schema.ts
2937
3254
  init_color();
2938
- import { existsSync as existsSync17 } from "node:fs";
2939
- import { join as join20 } from "node:path";
3255
+ import { existsSync as existsSync19 } from "node:fs";
3256
+ import { join as join22 } from "node:path";
2940
3257
  init_config();
2941
3258
 
2942
3259
  // src/http-fetch.ts
@@ -2973,8 +3290,8 @@ function fetchSchemaKeys() {
2973
3290
  }
2974
3291
  }
2975
3292
  function reportCheckSchema(section2) {
2976
- const settingsPath = join20(claudeHome(), "settings.json");
2977
- if (!existsSync17(settingsPath)) {
3293
+ const settingsPath = join22(claudeHome(), "settings.json");
3294
+ if (!existsSync19(settingsPath)) {
2978
3295
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
2979
3296
  return;
2980
3297
  }
@@ -3004,314 +3321,107 @@ function reportCheckSchema(section2) {
3004
3321
  init_color();
3005
3322
  import { randomBytes } from "node:crypto";
3006
3323
  import { execFileSync as execFileSync9 } from "node:child_process";
3007
- import { existsSync as existsSync21, mkdirSync as mkdirSync6, readdirSync as readdirSync10, rmSync as rmSync9 } from "node:fs";
3324
+ import { existsSync as existsSync22, mkdirSync as mkdirSync6, readdirSync as readdirSync11, rmSync as rmSync10 } from "node:fs";
3008
3325
  import { homedir as homedir4 } from "node:os";
3009
- import { join as join26 } from "node:path";
3326
+ import { join as join27 } from "node:path";
3010
3327
 
3011
3328
  // src/commands.doctor.check-shared.scan.ts
3012
3329
  init_color();
3013
- import { join as join22 } from "node:path";
3330
+ import { join as join24 } from "node:path";
3014
3331
  init_config();
3015
3332
  init_push_gitleaks();
3016
3333
  function scrubPath(logical, sid, logicalToEncoded) {
3017
3334
  const encoded = logicalToEncoded.get(logical) ?? logical;
3018
- return join22(claudeHome(), "projects", encoded, `${sid}.jsonl`);
3335
+ return join24(claudeHome(), "projects", encoded, `${sid}.jsonl`);
3019
3336
  }
3020
3337
  function reportSessionFindings(section2, bySession) {
3021
3338
  for (const [sid, counts] of bySession) {
3022
3339
  const summary = [...counts.entries()].map(([rule, n]) => `${rule} (${n})`).join(", ");
3023
3340
  addItem(section2, `${red(failGlyph)} ${red(summary)} in session ${sid}`);
3024
3341
  }
3025
- process.exitCode = 1;
3026
- }
3027
- function reportOtherFindings(section2, other) {
3028
- for (const f of other) {
3029
- addItem(section2, `${red(failGlyph)} ${red(f.RuleID)} leak in ${f.File}`);
3030
- }
3031
- process.exitCode = 1;
3032
- }
3033
- function reportRemediation(section2, bySession, logicalBySession, logicalToEncoded) {
3034
- addItem(section2, "");
3035
- addItem(section2, bold("Remediation"));
3036
- for (const [sid] of bySession) {
3037
- const logical = logicalBySession.get(sid);
3038
- if (logical !== void 0) {
3039
- const rotateLine = dim(
3040
- `- rotate the credential, then scrub ${scrubPath(logical, sid, logicalToEncoded)}`
3041
- );
3042
- addItem(section2, ` ${rotateLine}`);
3043
- }
3044
- }
3045
- addItem(section2, ` ${dim("- false positive? add a pattern to .gitleaks.toml")}`);
3046
- }
3047
- var SESSION_PATH_LOGICAL = /^shared\/projects\/([^/]+)\/([^/]+)\.jsonl$/;
3048
- function emitClean(section2, staged) {
3049
- addItem(section2, `${green(okGlyph)} ${staged} project(s) scanned, no leaks`);
3050
- }
3051
- function buildLogicalBySession(findings) {
3052
- const logicalBySession = /* @__PURE__ */ new Map();
3053
- for (const f of findings) {
3054
- const m = SESSION_PATH_LOGICAL.exec(f.File);
3055
- if (m?.[2] !== void 0 && !logicalBySession.has(m[2])) {
3056
- logicalBySession.set(m[2], m[1] ?? "");
3057
- }
3058
- }
3059
- return logicalBySession;
3060
- }
3061
- function emitDescriptionLegend(section2, findings) {
3062
- const descByRule = /* @__PURE__ */ new Map();
3063
- for (const f of findings) {
3064
- if (f.Description && !descByRule.has(f.RuleID)) descByRule.set(f.RuleID, f.Description);
3065
- }
3066
- if (descByRule.size === 0) return;
3067
- addItem(section2, "");
3068
- addItem(section2, bold("Finding types"));
3069
- for (const [rule, desc] of descByRule) {
3070
- const ruleLabel = red(`- [${rule}]`);
3071
- addItem(section2, ` ${ruleLabel}: ${dim(desc)}`);
3072
- }
3073
- }
3074
- function scanAndReport(section2, tmpRoot, staged, logicalToEncoded) {
3075
- let findings;
3076
- try {
3077
- findings = scanStagedTree(tmpRoot);
3078
- } catch (err) {
3079
- addItem(section2, `${red(failGlyph)} scan failed: ${err.message}`);
3080
- process.exitCode = 1;
3081
- return;
3082
- }
3083
- if (findings === null) {
3084
- addItem(section2, `${red(failGlyph)} scan failed: no parseable gitleaks report`);
3085
- process.exitCode = 1;
3086
- return;
3087
- }
3088
- const { bySession, other } = partitionFindings(findings);
3089
- if (bySession.size === 0 && other.length === 0) {
3090
- emitClean(section2, staged);
3091
- return;
3092
- }
3093
- if (other.length > 0) reportOtherFindings(section2, other);
3094
- if (bySession.size > 0) reportSessionFindings(section2, bySession);
3095
- if (bySession.size > 0) {
3096
- reportRemediation(section2, bySession, buildLogicalBySession(findings), logicalToEncoded);
3097
- }
3098
- emitDescriptionLegend(section2, findings);
3099
- }
3100
-
3101
- // src/commands.doctor.check-shared.ts
3102
- init_config();
3103
-
3104
- // src/remap.ts
3105
- init_config_sharedDirs_guard();
3106
- import {
3107
- cpSync as cpSync5,
3108
- existsSync as existsSync20,
3109
- lstatSync as lstatSync9,
3110
- mkdirSync as mkdirSync5,
3111
- readdirSync as readdirSync9,
3112
- renameSync as renameSync3,
3113
- rmSync as rmSync8,
3114
- statSync as statSync5
3115
- } from "node:fs";
3116
- import { dirname as dirname4, join as join25, relative as relative5, sep as sep3 } from "node:path";
3117
-
3118
- // src/extras-sync.core.ts
3119
- init_config();
3120
- import { cpSync as cpSync4, existsSync as existsSync18, lstatSync as lstatSync8, readdirSync as readdirSync7, readFileSync as readFileSync6, rmSync as rmSync7 } from "node:fs";
3121
- import { basename, join as join23, relative as relative4 } from "node:path";
3122
-
3123
- // src/extras-sync.guards.ts
3124
- init_utils();
3125
- init_config_sharedDirs_guard();
3126
- import { isAbsolute, normalize } from "node:path";
3127
- function assertSafeLocalRoot(localRoot, logical) {
3128
- if (!isAbsolute(localRoot)) {
3129
- throw new NomadFatal(
3130
- `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
3131
- );
3132
- }
3133
- if (localRoot !== normalize(localRoot)) {
3134
- throw new NomadFatal(
3135
- `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
3136
- );
3137
- }
3138
- }
3139
-
3140
- // src/extras-sync.core.ts
3141
- init_utils();
3142
- init_utils_json();
3143
- function loadValidatedExtras(opts) {
3144
- const repo = repoHome();
3145
- const mapPath = join23(repo, "path-map.json");
3146
- const repoExtras = join23(repo, "shared", "extras");
3147
- if (!existsSync18(mapPath) || opts.requireRepoExtras === true && !existsSync18(repoExtras)) {
3148
- if (opts.missingMsg !== void 0) log(opts.missingMsg);
3149
- return null;
3150
- }
3151
- const map = readPathMap(mapPath);
3152
- const extrasMap = map.extras ?? {};
3153
- if (Object.keys(extrasMap).length === 0) return null;
3154
- for (const logical of Object.keys(extrasMap)) {
3155
- assertSafeLogical(logical);
3156
- const localRoot = map.projects[logical]?.[HOST];
3157
- if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
3158
- }
3159
- return { map, extrasMap };
3160
- }
3161
- function* eachExtrasTarget(v, counts) {
3162
- const whitelist = SUPPORTED_EXTRAS;
3163
- for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
3164
- const localRoot = v.map.projects[logical]?.[HOST];
3165
- if (!localRoot || localRoot === "TBD") {
3166
- counts.unmapped++;
3167
- continue;
3168
- }
3169
- for (const dirname10 of dirnames) {
3170
- if (!whitelist.includes(dirname10)) {
3171
- counts.skipped++;
3172
- continue;
3173
- }
3174
- yield { logical, localRoot, dirname: dirname10 };
3175
- }
3176
- }
3342
+ process.exitCode = 1;
3177
3343
  }
3178
- function stripCollidingDstSymlinks(src, dst, isExcluded) {
3179
- if (!existsSync18(dst)) return;
3180
- for (const name of readdirSync7(src)) {
3181
- if (isExcluded(name)) continue;
3182
- const dstPath = join23(dst, name);
3183
- const dstStat = lstatSync8(dstPath, { throwIfNoEntry: false });
3184
- if (dstStat === void 0) continue;
3185
- if (dstStat.isSymbolicLink()) {
3186
- rmSync7(dstPath, { recursive: true, force: true });
3187
- } else if (dstStat.isDirectory() && lstatSync8(join23(src, name)).isDirectory()) {
3188
- stripCollidingDstSymlinks(join23(src, name), dstPath, isExcluded);
3189
- }
3344
+ function reportOtherFindings(section2, other) {
3345
+ for (const f of other) {
3346
+ addItem(section2, `${red(failGlyph)} ${red(f.RuleID)} leak in ${f.File}`);
3190
3347
  }
3348
+ process.exitCode = 1;
3191
3349
  }
3192
- function cpSyncGuarded(src, dst, filter, label) {
3193
- try {
3194
- cpSync4(src, dst, { recursive: true, force: true, verbatimSymlinks: true, filter });
3195
- } catch (err) {
3196
- const e = err;
3197
- if (e.code === "EINVAL" || e.code === "ENOTEMPTY" || e.code === "ERR_FS_CP_NON_DIR_TO_DIR" || e.code === "ERR_FS_CP_DIR_TO_NON_DIR") {
3198
- throw new NomadFatal(
3199
- `${label}: type collision copying ${JSON.stringify(src)} -> ${JSON.stringify(dst)} (${e.path ?? "unknown path"}): a file/directory type changed upstream; run nomad pull --force-remote to recover`
3350
+ function reportRemediation(section2, bySession, logicalBySession, logicalToEncoded) {
3351
+ addItem(section2, "");
3352
+ addItem(section2, bold("Remediation"));
3353
+ for (const [sid] of bySession) {
3354
+ const logical = logicalBySession.get(sid);
3355
+ if (logical !== void 0) {
3356
+ const rotateLine = dim(
3357
+ `- rotate the credential, then scrub ${scrubPath(logical, sid, logicalToEncoded)}`
3200
3358
  );
3359
+ addItem(section2, ` ${rotateLine}`);
3201
3360
  }
3202
- throw err;
3203
3361
  }
3362
+ addItem(section2, ` ${dim("- false positive? add a pattern to .gitleaks.toml")}`);
3204
3363
  }
3205
- function copyExtrasOverlayFiltered(src, dst, blockSet) {
3206
- stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
3207
- cpSyncGuarded(
3208
- src,
3209
- dst,
3210
- (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry)),
3211
- "copyExtrasOverlayFiltered"
3212
- );
3213
- }
3214
- function copyExtrasOverlaySkipDiverged(src, dst, blockSet, divergedSet) {
3215
- stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
3216
- cpSyncGuarded(
3217
- src,
3218
- dst,
3219
- (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry)) && !divergedSet.has(relative4(src, srcEntry)),
3220
- "copyExtrasOverlaySkipDiverged"
3221
- );
3222
- }
3223
- function copyExtras(src, dst) {
3224
- rmSync7(dst, { recursive: true, force: true });
3225
- cpSync4(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
3364
+ var SESSION_PATH_LOGICAL = /^shared\/projects\/([^/]+)\/([^/]+)\.jsonl$/;
3365
+ function emitClean(section2, staged) {
3366
+ addItem(section2, `${green(okGlyph)} ${staged} project(s) scanned, no leaks`);
3226
3367
  }
3227
- function copyExtrasFileSkipDiverged(src, dst) {
3228
- if (existsSync18(dst)) {
3229
- let dstBuf;
3230
- try {
3231
- dstBuf = readFileSync6(dst);
3232
- } catch {
3233
- return;
3368
+ function buildLogicalBySession(findings) {
3369
+ const logicalBySession = /* @__PURE__ */ new Map();
3370
+ for (const f of findings) {
3371
+ const m = SESSION_PATH_LOGICAL.exec(f.File);
3372
+ if (m?.[2] !== void 0 && !logicalBySession.has(m[2])) {
3373
+ logicalBySession.set(m[2], m[1] ?? "");
3234
3374
  }
3235
- if (!readFileSync6(src).equals(dstBuf)) return;
3236
3375
  }
3237
- copyExtras(src, dst);
3238
- }
3239
- function extrasDenySet(dirname10) {
3240
- return dirname10 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
3241
- }
3242
- function copyExtrasFiltered(src, dst, blockSet) {
3243
- rmSync7(dst, { recursive: true, force: true });
3244
- cpSync4(src, dst, {
3245
- recursive: true,
3246
- force: true,
3247
- verbatimSymlinks: true,
3248
- filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
3249
- });
3376
+ return logicalBySession;
3250
3377
  }
3251
- function prunePreservingDenied(src, dst, blockSet) {
3252
- for (const name of readdirSync7(dst)) {
3253
- if (isDeniedName(blockSet, name)) continue;
3254
- const dstPath = join23(dst, name);
3255
- const srcStat = lstatSync8(join23(src, name), { throwIfNoEntry: false });
3256
- if (srcStat === void 0) {
3257
- rmSync7(dstPath, { recursive: true, force: true });
3258
- continue;
3259
- }
3260
- const dstStat = lstatSync8(dstPath);
3261
- if (srcStat.isDirectory() && dstStat.isDirectory()) {
3262
- prunePreservingDenied(join23(src, name), dstPath, blockSet);
3263
- } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
3264
- rmSync7(dstPath, { recursive: true, force: true });
3265
- }
3378
+ function emitDescriptionLegend(section2, findings) {
3379
+ const descByRule = /* @__PURE__ */ new Map();
3380
+ for (const f of findings) {
3381
+ if (f.Description && !descByRule.has(f.RuleID)) descByRule.set(f.RuleID, f.Description);
3266
3382
  }
3267
- }
3268
- function copyExtrasFilteredPreserving(src, dst, blockSet) {
3269
- const dstStat = lstatSync8(dst, { throwIfNoEntry: false });
3270
- if (dstStat !== void 0) {
3271
- if (dstStat.isDirectory()) prunePreservingDenied(src, dst, blockSet);
3272
- else rmSync7(dst, { recursive: true, force: true });
3383
+ if (descByRule.size === 0) return;
3384
+ addItem(section2, "");
3385
+ addItem(section2, bold("Finding types"));
3386
+ for (const [rule, desc] of descByRule) {
3387
+ const ruleLabel = red(`- [${rule}]`);
3388
+ addItem(section2, ` ${ruleLabel}: ${dim(desc)}`);
3273
3389
  }
3274
- stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
3275
- cpSync4(src, dst, {
3276
- recursive: true,
3277
- force: true,
3278
- verbatimSymlinks: true,
3279
- filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
3280
- });
3281
3390
  }
3282
- function prunePreservingBy(src, dst, isPreserved) {
3283
- for (const name of readdirSync7(dst)) {
3284
- if (isPreserved(name)) continue;
3285
- const dstPath = join23(dst, name);
3286
- const srcStat = lstatSync8(join23(src, name), { throwIfNoEntry: false });
3287
- if (srcStat === void 0) {
3288
- rmSync7(dstPath, { recursive: true, force: true });
3289
- continue;
3290
- }
3291
- const dstStat = lstatSync8(dstPath);
3292
- if (srcStat.isDirectory() && dstStat.isDirectory()) {
3293
- prunePreservingBy(join23(src, name), dstPath, isPreserved);
3294
- } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
3295
- rmSync7(dstPath, { recursive: true, force: true });
3296
- }
3391
+ function scanAndReport(section2, tmpRoot, staged, logicalToEncoded) {
3392
+ let findings;
3393
+ try {
3394
+ findings = scanStagedTree(tmpRoot);
3395
+ } catch (err) {
3396
+ addItem(section2, `${red(failGlyph)} scan failed: ${err.message}`);
3397
+ process.exitCode = 1;
3398
+ return;
3297
3399
  }
3298
- }
3299
- function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
3300
- const dstStat = lstatSync8(dst, { throwIfNoEntry: false });
3301
- if (dstStat !== void 0) {
3302
- if (dstStat.isDirectory()) prunePreservingBy(src, dst, isPreserved);
3303
- else rmSync7(dst, { recursive: true, force: true });
3400
+ if (findings === null) {
3401
+ addItem(section2, `${red(failGlyph)} scan failed: no parseable gitleaks report`);
3402
+ process.exitCode = 1;
3403
+ return;
3304
3404
  }
3305
- stripCollidingDstSymlinks(src, dst, isPreserved);
3306
- cpSync4(src, dst, {
3307
- recursive: true,
3308
- force: true,
3309
- verbatimSymlinks: true,
3310
- filter: (srcEntry) => srcEntry === src || !isPreserved(basename(srcEntry))
3311
- });
3405
+ const { bySession, other } = partitionFindings(findings);
3406
+ if (bySession.size === 0 && other.length === 0) {
3407
+ emitClean(section2, staged);
3408
+ return;
3409
+ }
3410
+ if (other.length > 0) reportOtherFindings(section2, other);
3411
+ if (bySession.size > 0) reportSessionFindings(section2, bySession);
3412
+ if (bySession.size > 0) {
3413
+ reportRemediation(section2, bySession, buildLogicalBySession(findings), logicalToEncoded);
3414
+ }
3415
+ emitDescriptionLegend(section2, findings);
3312
3416
  }
3313
3417
 
3418
+ // src/commands.doctor.check-shared.ts
3419
+ init_config();
3420
+
3314
3421
  // src/remap.ts
3422
+ init_config_sharedDirs_guard();
3423
+ import { cpSync as cpSync6, existsSync as existsSync21, lstatSync as lstatSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync10, rmSync as rmSync9, statSync as statSync5 } from "node:fs";
3424
+ import { dirname as dirname4, join as join26, relative as relative5, sep as sep4 } from "node:path";
3315
3425
  init_config();
3316
3426
 
3317
3427
  // src/push-manifest.ts
@@ -3319,8 +3429,8 @@ init_config();
3319
3429
  init_push_gitleaks_config();
3320
3430
  init_utils_fs();
3321
3431
  import { createHash } from "node:crypto";
3322
- import { existsSync as existsSync19, mkdirSync as mkdirSync4, readdirSync as readdirSync8, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
3323
- import { dirname as dirname3, join as join24 } from "node:path";
3432
+ import { existsSync as existsSync20, mkdirSync as mkdirSync4, readdirSync as readdirSync9, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
3433
+ import { dirname as dirname3, join as join25 } from "node:path";
3324
3434
  function isChanged(prev, cur, hash) {
3325
3435
  if (prev === void 0) return true;
3326
3436
  if (prev.size !== cur.size) return true;
@@ -3360,8 +3470,8 @@ function hashFile(absPath) {
3360
3470
  return createHash("sha256").update(readFileSync7(absPath)).digest("hex");
3361
3471
  }
3362
3472
  function enumerateDir(dir, results) {
3363
- for (const entry of readdirSync8(dir)) {
3364
- const fullPath = join24(dir, entry);
3473
+ for (const entry of readdirSync9(dir)) {
3474
+ const fullPath = join25(dir, entry);
3365
3475
  const st = statSync4(fullPath);
3366
3476
  if (st.isDirectory()) {
3367
3477
  enumerateDir(fullPath, results);
@@ -3372,8 +3482,8 @@ function enumerateDir(dir, results) {
3372
3482
  }
3373
3483
  function enumerateSourceFiles(projectDir) {
3374
3484
  const results = [];
3375
- for (const entry of readdirSync8(projectDir)) {
3376
- const fullPath = join24(projectDir, entry);
3485
+ for (const entry of readdirSync9(projectDir)) {
3486
+ const fullPath = join25(projectDir, entry);
3377
3487
  const st = statSync4(fullPath);
3378
3488
  if (st.isDirectory()) {
3379
3489
  enumerateDir(fullPath, results);
@@ -3385,15 +3495,15 @@ function enumerateSourceFiles(projectDir) {
3385
3495
  }
3386
3496
  function fileIdentity(p) {
3387
3497
  if (p === null) return "none::absent";
3388
- if (!existsSync19(p)) return `${p}::absent`;
3498
+ if (!existsSync20(p)) return `${p}::absent`;
3389
3499
  return `${p}::${createHash("sha256").update(readFileSync7(p)).digest("hex")}`;
3390
3500
  }
3391
3501
  function computeConfigHash() {
3392
3502
  const repo = repoHome();
3393
3503
  const parts = [
3394
3504
  fileIdentity(resolveTomlPath(repo)),
3395
- fileIdentity(join24(repo, ".gitleaks.overlay.toml")),
3396
- fileIdentity(join24(repo, ".gitleaksignore"))
3505
+ fileIdentity(join25(repo, ".gitleaks.overlay.toml")),
3506
+ fileIdentity(join25(repo, ".gitleaksignore"))
3397
3507
  ];
3398
3508
  return createHash("sha256").update(parts.join("\n")).digest("hex");
3399
3509
  }
@@ -3427,10 +3537,10 @@ init_utils_json();
3427
3537
  var TMP_SUFFIX = ".nomad-tmp";
3428
3538
  function atomicMirror(src, dst, options) {
3429
3539
  const tmp = `${dst}${TMP_SUFFIX}`;
3430
- rmSync8(tmp, { recursive: true, force: true });
3431
- cpSync5(src, tmp, options);
3432
- rmSync8(dst, { recursive: true, force: true });
3433
- renameSync3(tmp, dst);
3540
+ rmSync9(tmp, { recursive: true, force: true });
3541
+ cpSync6(src, tmp, options);
3542
+ rmSync9(dst, { recursive: true, force: true });
3543
+ renameAtomicRetry(tmp, dst);
3434
3544
  }
3435
3545
  function overlaySessionDir(src, dst) {
3436
3546
  stripCollidingDstSymlinks(src, dst, () => false);
@@ -3439,11 +3549,11 @@ function overlaySessionDir(src, dst) {
3439
3549
  function copyFileAtomic(src, dst) {
3440
3550
  mkdirSync5(dirname4(dst), { recursive: true });
3441
3551
  const tmp = `${dst}.tmp.${process.pid}`;
3442
- cpSync5(src, tmp, { force: true, preserveTimestamps: true });
3443
- renameSync3(tmp, dst);
3552
+ cpSync6(src, tmp, { force: true, preserveTimestamps: true });
3553
+ renameAtomicRetry(tmp, dst);
3444
3554
  }
3445
3555
  function hasDeltaForDir(sel, localDir) {
3446
- const prefix = `${localDir}${sep3}`;
3556
+ const prefix = `${localDir}${sep4}`;
3447
3557
  for (const p of sel.changed) if (p.startsWith(prefix)) return true;
3448
3558
  for (const p of sel.deleted) if (p.startsWith(prefix)) return true;
3449
3559
  return false;
@@ -3452,15 +3562,15 @@ function skipForSelection(sel, localDir) {
3452
3562
  return sel !== void 0 && !hasDeltaForDir(sel, localDir);
3453
3563
  }
3454
3564
  function applySelective(sel, localDir, repoDst) {
3455
- const prefix = `${localDir}${sep3}`;
3565
+ const prefix = `${localDir}${sep4}`;
3456
3566
  for (const src of sel.changed) {
3457
3567
  if (!src.startsWith(prefix)) continue;
3458
- copyFileAtomic(src, join25(repoDst, relative5(localDir, src)));
3568
+ copyFileAtomic(src, join26(repoDst, relative5(localDir, src)));
3459
3569
  }
3460
3570
  for (const src of sel.deleted) {
3461
3571
  if (!src.startsWith(prefix)) continue;
3462
- const dst = join25(repoDst, relative5(localDir, src));
3463
- if (existsSync20(dst)) rmSync8(dst);
3572
+ const dst = join26(repoDst, relative5(localDir, src));
3573
+ if (existsSync21(dst)) rmSync9(dst);
3464
3574
  }
3465
3575
  }
3466
3576
  function copyDirJsonlOnly(src, dst) {
@@ -3470,7 +3580,7 @@ function copyDirJsonlOnly(src, dst) {
3470
3580
  filter: (srcPath) => {
3471
3581
  const rel = relative5(src, srcPath);
3472
3582
  if (rel === "") return true;
3473
- if (rel.split(sep3).length > 1) return true;
3583
+ if (rel.split(sep4).length > 1) return true;
3474
3584
  if (statSync5(srcPath).isDirectory()) return true;
3475
3585
  if (srcPath.endsWith(".jsonl")) return true;
3476
3586
  item(`skip ${rel}: extension not in allowlist`);
@@ -3489,15 +3599,15 @@ function remapPull(ts, opts = {}) {
3489
3599
  const wouldPull = [];
3490
3600
  const repo = repoHome();
3491
3601
  const claude = claudeHome();
3492
- const mapPath = join25(repo, "path-map.json");
3493
- const repoProjects = join25(repo, "shared", "projects");
3494
- if (!existsSync20(mapPath) || !existsSync20(repoProjects)) {
3602
+ const mapPath = join26(repo, "path-map.json");
3603
+ const repoProjects = join26(repo, "shared", "projects");
3604
+ if (!existsSync21(mapPath) || !existsSync21(repoProjects)) {
3495
3605
  const text = "no path-map or repo projects dir; skipping session remap";
3496
3606
  emitPreview(opts.onPreview, { kind: "note", text }, text);
3497
3607
  return { unmapped: 0, pulled, wouldPull };
3498
3608
  }
3499
3609
  const map = readPathMap(mapPath);
3500
- const localProjects = join25(claude, "projects");
3610
+ const localProjects = join26(claude, "projects");
3501
3611
  if (!dryRun) mkdirSync5(localProjects, { recursive: true });
3502
3612
  for (const [logical, hosts] of Object.entries(map.projects)) {
3503
3613
  assertSafeLogical(logical);
@@ -3507,9 +3617,9 @@ function remapPull(ts, opts = {}) {
3507
3617
  continue;
3508
3618
  }
3509
3619
  assertSafeLocalRoot(localPath, logical);
3510
- const src = join25(repoProjects, logical);
3511
- if (!existsSync20(src)) continue;
3512
- const dst = join25(localProjects, encodePath(localPath));
3620
+ const src = join26(repoProjects, logical);
3621
+ if (!existsSync21(src)) continue;
3622
+ const dst = join26(localProjects, encodePath(localPath));
3513
3623
  if (dryRun) {
3514
3624
  wouldPull.push(logical);
3515
3625
  emitPreview(
@@ -3527,12 +3637,12 @@ function remapPull(ts, opts = {}) {
3527
3637
  }
3528
3638
  function countLocalOnly(src, dst) {
3529
3639
  let count = 0;
3530
- for (const name of readdirSync9(dst)) {
3531
- const dstPath = join25(dst, name);
3532
- const srcPath = join25(src, name);
3533
- if (lstatSync9(dstPath).isDirectory()) {
3640
+ for (const name of readdirSync10(dst)) {
3641
+ const dstPath = join26(dst, name);
3642
+ const srcPath = join26(src, name);
3643
+ if (lstatSync10(dstPath).isDirectory()) {
3534
3644
  count += countLocalOnly(srcPath, dstPath);
3535
- } else if (lstatSync9(srcPath, { throwIfNoEntry: false }) === void 0) {
3645
+ } else if (lstatSync10(srcPath, { throwIfNoEntry: false }) === void 0) {
3536
3646
  count++;
3537
3647
  }
3538
3648
  }
@@ -3541,20 +3651,20 @@ function countLocalOnly(src, dst) {
3541
3651
  function scanLocalOnly() {
3542
3652
  const repo = repoHome();
3543
3653
  const claude = claudeHome();
3544
- const mapPath = join25(repo, "path-map.json");
3545
- const repoProjects = join25(repo, "shared", "projects");
3546
- if (!existsSync20(mapPath) || !existsSync20(repoProjects)) return 0;
3654
+ const mapPath = join26(repo, "path-map.json");
3655
+ const repoProjects = join26(repo, "shared", "projects");
3656
+ if (!existsSync21(mapPath) || !existsSync21(repoProjects)) return 0;
3547
3657
  const map = readPathMap(mapPath);
3548
- const localProjects = join25(claude, "projects");
3658
+ const localProjects = join26(claude, "projects");
3549
3659
  let count = 0;
3550
3660
  for (const [logical, hosts] of Object.entries(map.projects)) {
3551
3661
  assertSafeLogical(logical);
3552
3662
  const localPath = hosts[HOST];
3553
3663
  if (!localPath || localPath === "TBD") continue;
3554
3664
  assertSafeLocalRoot(localPath, logical);
3555
- const dst = join25(localProjects, encodePath(localPath));
3556
- if (!existsSync20(dst)) continue;
3557
- count += countLocalOnly(join25(repoProjects, logical), dst);
3665
+ const dst = join26(localProjects, encodePath(localPath));
3666
+ if (!existsSync21(dst)) continue;
3667
+ count += countLocalOnly(join26(repoProjects, logical), dst);
3558
3668
  }
3559
3669
  return count;
3560
3670
  }
@@ -3589,26 +3699,26 @@ function remapPush(ts, opts = {}) {
3589
3699
  const wouldPush = [];
3590
3700
  const repo = repoHome();
3591
3701
  const claude = claudeHome();
3592
- const mapPath = join25(repo, "path-map.json");
3593
- if (!existsSync20(mapPath)) {
3702
+ const mapPath = join26(repo, "path-map.json");
3703
+ if (!existsSync21(mapPath)) {
3594
3704
  log("no path-map.json; skipping session export");
3595
3705
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
3596
3706
  }
3597
3707
  const map = readPathMap(mapPath);
3598
- const localProjects = join25(claude, "projects");
3599
- const repoProjects = join25(repo, "shared", "projects");
3708
+ const localProjects = join26(claude, "projects");
3709
+ const repoProjects = join26(repo, "shared", "projects");
3600
3710
  const reverse = buildReverseMap(map);
3601
- if (!existsSync20(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
3711
+ if (!existsSync21(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
3602
3712
  if (!dryRun) mkdirSync5(repoProjects, { recursive: true });
3603
- for (const dir of readdirSync9(localProjects)) {
3713
+ for (const dir of readdirSync10(localProjects)) {
3604
3714
  if (dir.endsWith(TMP_SUFFIX)) continue;
3605
3715
  const logical = reverse.get(dir);
3606
3716
  if (!logical) {
3607
3717
  unmapped++;
3608
3718
  continue;
3609
3719
  }
3610
- const localDir = join25(localProjects, dir);
3611
- const repoDst = join25(repoProjects, logical);
3720
+ const localDir = join26(localProjects, dir);
3721
+ const repoDst = join26(repoProjects, logical);
3612
3722
  if (skipForSelection(opts.selection, localDir)) continue;
3613
3723
  if (dryRun) {
3614
3724
  wouldPush.push(logical);
@@ -3632,8 +3742,8 @@ function buildScanTree(tmpRoot) {
3632
3742
  const logicalToEncoded = /* @__PURE__ */ new Map();
3633
3743
  let staged = 0;
3634
3744
  const repo = repoHome();
3635
- const mapPath = join26(repo, "path-map.json");
3636
- if (!existsSync21(mapPath)) return { logicalToEncoded, staged, malformed: false };
3745
+ const mapPath = join27(repo, "path-map.json");
3746
+ if (!existsSync22(mapPath)) return { logicalToEncoded, staged, malformed: false };
3637
3747
  let map;
3638
3748
  try {
3639
3749
  map = readJson(mapPath);
@@ -3650,12 +3760,12 @@ function buildScanTree(tmpRoot) {
3650
3760
  if (!p || p === "TBD") continue;
3651
3761
  reverse.set(encodePath(p), logical);
3652
3762
  }
3653
- const localProjects = join26(claudeHome(), "projects");
3654
- if (!existsSync21(localProjects)) return { logicalToEncoded, staged, malformed: false };
3655
- for (const dir of readdirSync10(localProjects)) {
3763
+ const localProjects = join27(claudeHome(), "projects");
3764
+ if (!existsSync22(localProjects)) return { logicalToEncoded, staged, malformed: false };
3765
+ for (const dir of readdirSync11(localProjects)) {
3656
3766
  const logical = reverse.get(dir);
3657
3767
  if (!logical) continue;
3658
- copyDirJsonlOnly(join26(localProjects, dir), join26(tmpRoot, "shared", "projects", logical));
3768
+ copyDirJsonlOnly(join27(localProjects, dir), join27(tmpRoot, "shared", "projects", logical));
3659
3769
  logicalToEncoded.set(logical, dir);
3660
3770
  staged++;
3661
3771
  }
@@ -3686,11 +3796,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
3686
3796
  }
3687
3797
  function reportCheckShared(section2, gitleaksReady) {
3688
3798
  if (!ensureGitleaksReady(section2, gitleaksReady)) return;
3689
- const cacheDir = join26(homedir4(), ".cache", "claude-nomad");
3799
+ const cacheDir = join27(homedir4(), ".cache", "claude-nomad");
3690
3800
  mkdirSync6(cacheDir, { recursive: true });
3691
3801
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes(4).toString("hex")}`;
3692
- const reportPath = join26(cacheDir, `check-shared-${stamp}.json`);
3693
- const tmpRoot = join26(cacheDir, `check-shared-tree-${stamp}`);
3802
+ const reportPath = join27(cacheDir, `check-shared-${stamp}.json`);
3803
+ const tmpRoot = join27(cacheDir, `check-shared-tree-${stamp}`);
3694
3804
  try {
3695
3805
  const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
3696
3806
  if (malformed) {
@@ -3704,15 +3814,15 @@ function reportCheckShared(section2, gitleaksReady) {
3704
3814
  }
3705
3815
  scanAndReport(section2, tmpRoot, staged, logicalToEncoded);
3706
3816
  } finally {
3707
- rmSync9(reportPath, { force: true });
3708
- rmSync9(tmpRoot, { recursive: true, force: true });
3817
+ rmSync10(reportPath, { force: true });
3818
+ rmSync10(tmpRoot, { recursive: true, force: true });
3709
3819
  }
3710
3820
  }
3711
3821
 
3712
3822
  // src/commands.doctor.checks.hooks.scope.ts
3713
3823
  init_color();
3714
- import { existsSync as existsSync22, readFileSync as readFileSync8, readdirSync as readdirSync11, realpathSync as realpathSync2 } from "node:fs";
3715
- import { dirname as dirname5, extname, join as join27 } from "node:path";
3824
+ import { existsSync as existsSync23, readFileSync as readFileSync8, readdirSync as readdirSync12, realpathSync as realpathSync2 } from "node:fs";
3825
+ import { dirname as dirname5, extname, join as join28 } from "node:path";
3716
3826
  init_config();
3717
3827
  function typeFromPackageJson(pkgPath) {
3718
3828
  try {
@@ -3734,8 +3844,8 @@ function effectiveType(hookPath) {
3734
3844
  }
3735
3845
  let dir = dirname5(real);
3736
3846
  for (; ; ) {
3737
- const pkg = join27(dir, "package.json");
3738
- if (existsSync22(pkg)) return typeFromPackageJson(pkg);
3847
+ const pkg = join28(dir, "package.json");
3848
+ if (existsSync23(pkg)) return typeFromPackageJson(pkg);
3739
3849
  const parent = dirname5(dir);
3740
3850
  if (parent === dir) return "cjs";
3741
3851
  dir = parent;
@@ -3764,7 +3874,7 @@ function remedy(family) {
3764
3874
  }
3765
3875
  function safeReaddir2(dir) {
3766
3876
  try {
3767
- return readdirSync11(dir);
3877
+ return readdirSync12(dir);
3768
3878
  } catch {
3769
3879
  return [];
3770
3880
  }
@@ -3777,15 +3887,15 @@ function safeRead(path) {
3777
3887
  }
3778
3888
  }
3779
3889
  function reportHookScopeCheck(section2) {
3780
- const hooksDir = join27(claudeHome(), "hooks");
3781
- if (!existsSync22(hooksDir)) {
3890
+ const hooksDir = join28(claudeHome(), "hooks");
3891
+ if (!existsSync23(hooksDir)) {
3782
3892
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
3783
3893
  return;
3784
3894
  }
3785
3895
  let anyWarn = false;
3786
3896
  for (const name of safeReaddir2(hooksDir)) {
3787
3897
  if (extname(name) !== ".js") continue;
3788
- const abs = join27(hooksDir, name);
3898
+ const abs = join28(hooksDir, name);
3789
3899
  const eff = effectiveType(abs);
3790
3900
  if (eff === null) continue;
3791
3901
  const src = safeRead(abs);
@@ -3805,8 +3915,8 @@ function reportHookScopeCheck(section2) {
3805
3915
 
3806
3916
  // src/commands.doctor.checks.hooks.ts
3807
3917
  init_color();
3808
- import { existsSync as existsSync23 } from "node:fs";
3809
- import { join as join28 } from "node:path";
3918
+ import { existsSync as existsSync24 } from "node:fs";
3919
+ import { join as join29 } from "node:path";
3810
3920
  init_config();
3811
3921
  function expandHome(token) {
3812
3922
  const h2 = home();
@@ -3820,11 +3930,11 @@ function stripShellPunctuation(token) {
3820
3930
  return stripped.slice(0, end);
3821
3931
  }
3822
3932
  function* claudePathsIn(command) {
3823
- const claudePrefix = `${claudeHome()}/`;
3933
+ const claudePrefix = `${claudeHome().replaceAll("\\", "/")}/`;
3824
3934
  for (const segment of command.split(/&&|\|\||;|\|/)) {
3825
3935
  for (const raw of segment.trim().split(/\s+/).filter(Boolean)) {
3826
3936
  const expanded = expandHome(stripShellPunctuation(raw));
3827
- if (expanded.startsWith(claudePrefix)) yield expanded;
3937
+ if (expanded.replaceAll("\\", "/").startsWith(claudePrefix)) yield expanded;
3828
3938
  }
3829
3939
  }
3830
3940
  }
@@ -3849,7 +3959,7 @@ function checkEventGroups(section2, event, groups) {
3849
3959
  for (const group of groups) {
3850
3960
  for (const cmd of commandsFromGroup(group)) {
3851
3961
  for (const resolved of claudePathsIn(cmd)) {
3852
- if (existsSync23(resolved)) continue;
3962
+ if (existsSync24(resolved)) continue;
3853
3963
  addItem(
3854
3964
  section2,
3855
3965
  `${red(failGlyph)} hooks/${event}: command target missing: ${resolved} (run nomad pull)`
@@ -3862,8 +3972,8 @@ function checkEventGroups(section2, event, groups) {
3862
3972
  return anyFail;
3863
3973
  }
3864
3974
  function reportHooksTargetCheck(section2) {
3865
- const settingsPath = join28(claudeHome(), "settings.json");
3866
- if (!existsSync23(settingsPath)) {
3975
+ const settingsPath = join29(claudeHome(), "settings.json");
3976
+ if (!existsSync24(settingsPath)) {
3867
3977
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
3868
3978
  return;
3869
3979
  }
@@ -3886,12 +3996,12 @@ function reportHooksTargetCheck(section2) {
3886
3996
 
3887
3997
  // src/commands.doctor.checks.hooks.preserve-symlinks.ts
3888
3998
  init_color();
3889
- import { existsSync as existsSync25, readFileSync as readFileSync9 } from "node:fs";
3890
- import { join as join30 } from "node:path";
3999
+ import { existsSync as existsSync26, readFileSync as readFileSync9 } from "node:fs";
4000
+ import { join as join31 } from "node:path";
3891
4001
 
3892
4002
  // src/commands.doctor.checks.hooks.preserve-symlinks.probe.ts
3893
- import { closeSync as closeSync3, existsSync as existsSync24, openSync as openSync3, readSync, realpathSync as realpathSync3 } from "node:fs";
3894
- import { dirname as dirname6, join as join29, resolve as resolve2 } from "node:path";
4003
+ import { closeSync as closeSync3, existsSync as existsSync25, openSync as openSync3, readSync, realpathSync as realpathSync3 } from "node:fs";
4004
+ import { dirname as dirname6, join as join30, resolve as resolve2 } from "node:path";
3895
4005
  function suppressedRanges(src) {
3896
4006
  const ranges = [];
3897
4007
  const re = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'[^']*'|"[^"]*"|`[^`]*`/g;
@@ -3923,12 +4033,12 @@ function topRelativeSpecifiers(src) {
3923
4033
  }
3924
4034
  function specifierIsMissing(specifier, baseDir) {
3925
4035
  const base = resolve2(baseDir, specifier);
3926
- if (existsSync24(base)) return false;
4036
+ if (existsSync25(base)) return false;
3927
4037
  for (const ext of [".js", ".cjs", ".mjs"]) {
3928
- if (existsSync24(base + ext)) return false;
4038
+ if (existsSync25(base + ext)) return false;
3929
4039
  }
3930
4040
  for (const idx of ["index.js", "index.cjs", "index.mjs"]) {
3931
- if (existsSync24(join29(base, idx))) return false;
4041
+ if (existsSync25(join30(base, idx))) return false;
3932
4042
  }
3933
4043
  return true;
3934
4044
  }
@@ -3983,8 +4093,8 @@ function commandTokens(command) {
3983
4093
  return tokens;
3984
4094
  }
3985
4095
  function readPathMapSafe() {
3986
- const mapPath = join30(repoHome(), "path-map.json");
3987
- if (!existsSync25(mapPath)) return { projects: {} };
4096
+ const mapPath = join31(repoHome(), "path-map.json");
4097
+ if (!existsSync26(mapPath)) return { projects: {} };
3988
4098
  try {
3989
4099
  return JSON.parse(readFileSync9(mapPath, "utf8"));
3990
4100
  } catch {
@@ -3992,9 +4102,10 @@ function readPathMapSafe() {
3992
4102
  }
3993
4103
  }
3994
4104
  function resolvesUnderSymlinkedShared(scriptPath, sharedLinkNames) {
4105
+ const normalizedScript = scriptPath.replaceAll("\\", "/");
3995
4106
  for (const name of sharedLinkNames) {
3996
- const prefix = `${claudeHome()}/${name}/`;
3997
- if (scriptPath.startsWith(prefix)) return true;
4107
+ const prefix = `${claudeHome().replaceAll("\\", "/")}/${name}/`;
4108
+ if (normalizedScript.startsWith(prefix)) return true;
3998
4109
  }
3999
4110
  return false;
4000
4111
  }
@@ -4057,8 +4168,8 @@ function checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames)
4057
4168
  return anyWarn;
4058
4169
  }
4059
4170
  function reportPreserveSymlinksCheck(section2) {
4060
- const settingsPath = join30(claudeHome(), "settings.json");
4061
- if (!existsSync25(settingsPath)) {
4171
+ const settingsPath = join31(claudeHome(), "settings.json");
4172
+ if (!existsSync26(settingsPath)) {
4062
4173
  addItem(
4063
4174
  section2,
4064
4175
  `${dim(infoGlyph)} no ~/.claude/settings.json; skipping preserve-symlinks-main check`
@@ -4091,8 +4202,8 @@ function reportPreserveSymlinksCheck(section2) {
4091
4202
 
4092
4203
  // src/commands.doctor.checks.settings-drift.ts
4093
4204
  init_color();
4094
- import { existsSync as existsSync26, readFileSync as readFileSync10 } from "node:fs";
4095
- import { join as join31 } from "node:path";
4205
+ import { existsSync as existsSync27, readFileSync as readFileSync10 } from "node:fs";
4206
+ import { join as join32 } from "node:path";
4096
4207
  init_config();
4097
4208
  init_utils_json();
4098
4209
  function diffMergedSettings(merged, settings) {
@@ -4110,7 +4221,7 @@ function tryReadJson(filePath) {
4110
4221
  }
4111
4222
  }
4112
4223
  function reportHooksBaseSelfCleanNote(section2) {
4113
- const basePath = join31(repoHome(), "shared", "settings.base.json");
4224
+ const basePath = join32(repoHome(), "shared", "settings.base.json");
4114
4225
  const base = tryReadJson(basePath);
4115
4226
  if (base === null) return;
4116
4227
  if (!baseHasGsdHookEntries(base)) return;
@@ -4123,14 +4234,14 @@ function reportSettingsDriftCheck(section2) {
4123
4234
  const claude = claudeHome();
4124
4235
  const repo = repoHome();
4125
4236
  const host = HOST;
4126
- const settingsPath = join31(claude, "settings.json");
4127
- const basePath = join31(repo, "shared", "settings.base.json");
4128
- const hostPath = join31(repo, "hosts", `${host}.json`);
4129
- if (!existsSync26(settingsPath)) {
4237
+ const settingsPath = join32(claude, "settings.json");
4238
+ const basePath = join32(repo, "shared", "settings.base.json");
4239
+ const hostPath = join32(repo, "hosts", `${host}.json`);
4240
+ if (!existsSync27(settingsPath)) {
4130
4241
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping merge-drift check`);
4131
4242
  return;
4132
4243
  }
4133
- if (!existsSync26(basePath)) {
4244
+ if (!existsSync27(basePath)) {
4134
4245
  addItem(
4135
4246
  section2,
4136
4247
  `${dim(infoGlyph)} shared/settings.base.json missing; skipping merge-drift check`
@@ -4149,7 +4260,7 @@ function reportSettingsDriftCheck(section2) {
4149
4260
  if (settings === null) {
4150
4261
  return;
4151
4262
  }
4152
- const hostExists = existsSync26(hostPath);
4263
+ const hostExists = existsSync27(hostPath);
4153
4264
  const hostObj = hostExists ? tryReadJson(hostPath) : null;
4154
4265
  if (hostExists && hostObj === null) {
4155
4266
  addItem(
@@ -4305,46 +4416,152 @@ function reportNodeEngineCheck(section2) {
4305
4416
  addItem(section2, `${green(okGlyph)} node: ${process.version}`);
4306
4417
  }
4307
4418
 
4419
+ // src/commands.doctor.checks.longpaths.ts
4420
+ init_color();
4421
+ import { execFileSync as execFileSync10 } from "node:child_process";
4422
+ init_config();
4423
+ var PROBE_TIMEOUT_MS = 3e3;
4424
+ var LONGPATHS_REG_KEY = String.raw`HKLM\SYSTEM\CurrentControlSet\Control\FileSystem`;
4425
+ var LONGPATHS_REG_VALUE = "LongPathsEnabled";
4426
+ function probeGitLongpaths(run) {
4427
+ try {
4428
+ const out = run("git", ["-C", repoHome(), "config", "--get", "core.longpaths"], {
4429
+ stdio: ["ignore", "pipe", "pipe"],
4430
+ timeout: PROBE_TIMEOUT_MS
4431
+ }).toString().trim();
4432
+ return out === "true" || out === "1";
4433
+ } catch {
4434
+ return false;
4435
+ }
4436
+ }
4437
+ function probeRegistryLongpaths(run) {
4438
+ try {
4439
+ const out = run("reg", ["query", LONGPATHS_REG_KEY, "/v", LONGPATHS_REG_VALUE], {
4440
+ stdio: ["ignore", "pipe", "pipe"],
4441
+ timeout: PROBE_TIMEOUT_MS
4442
+ }).toString();
4443
+ return /0x1\b/.test(out);
4444
+ } catch {
4445
+ return false;
4446
+ }
4447
+ }
4448
+ function addLongpathsRow(section2, label, enabled2, remediation) {
4449
+ if (enabled2) {
4450
+ addItem(section2, `${green(okGlyph)} ${label}: enabled`);
4451
+ return;
4452
+ }
4453
+ addItem(section2, `${yellow(warnGlyph)} ${label}: not enabled (${remediation})`);
4454
+ }
4455
+ function reportLongPathsCheck(section2, run = execFileSync10) {
4456
+ if (process.platform !== "win32") return;
4457
+ addLongpathsRow(
4458
+ section2,
4459
+ "git core.longpaths",
4460
+ probeGitLongpaths(run),
4461
+ "run `git config --global core.longpaths true`"
4462
+ );
4463
+ addLongpathsRow(
4464
+ section2,
4465
+ "OS long paths",
4466
+ probeRegistryLongpaths(run),
4467
+ "enable LongPathsEnabled in Local Group Policy or the registry (admin required)"
4468
+ );
4469
+ }
4470
+ function reportSyncModality(section2) {
4471
+ const modality = process.platform === "win32" ? "copy-sync (win32)" : "symlink (posix)";
4472
+ addItem(section2, `${dim(infoGlyph)} sync modality: ${modality}`);
4473
+ }
4474
+
4475
+ // src/commands.doctor.checks.crlf.ts
4476
+ init_color();
4477
+ import { execFileSync as execFileSync11 } from "node:child_process";
4478
+ import { existsSync as existsSync28, readFileSync as readFileSync13 } from "node:fs";
4479
+ import { join as join33 } from "node:path";
4480
+ init_config();
4481
+ var PROBE_TIMEOUT_MS2 = 3e3;
4482
+ var GUARD_LINE = /^\*\s+-text\b/;
4483
+ function hasGitattributesGuard(repo) {
4484
+ try {
4485
+ const path = join33(repo, ".gitattributes");
4486
+ if (!existsSync28(path)) return false;
4487
+ const content = readFileSync13(path, "utf8");
4488
+ return content.split("\n").map((line) => line.trim()).some((line) => line !== "" && !line.startsWith("#") && GUARD_LINE.test(line));
4489
+ } catch {
4490
+ return false;
4491
+ }
4492
+ }
4493
+ function probeAutocrlf(repo, run) {
4494
+ try {
4495
+ const out = run("git", ["-C", repo, "config", "core.autocrlf"], {
4496
+ stdio: ["ignore", "pipe", "pipe"],
4497
+ timeout: PROBE_TIMEOUT_MS2
4498
+ }).toString().trim();
4499
+ if (out === "true" || out === "input") return "active";
4500
+ if (out === "false") return "disabled";
4501
+ return "unset";
4502
+ } catch {
4503
+ return "unset";
4504
+ }
4505
+ }
4506
+ var AUTOCRLF_RISK = {
4507
+ active: "core.autocrlf is actively converting line endings on checkout",
4508
+ disabled: "guarded on this host by core.autocrlf=false, but no .gitattributes to protect other hosts",
4509
+ unset: "no .gitattributes guard and core.autocrlf is unset (latent risk)"
4510
+ };
4511
+ function addExposedRow(section2, repo, verdict) {
4512
+ const risk = AUTOCRLF_RISK[verdict];
4513
+ const remediation = `add a .gitattributes with a \`* -text\` line, or run \`git config core.autocrlf false\`, in ${repo}`;
4514
+ addItem(section2, `${yellow(warnGlyph)} CRLF guard: ${risk}; ${remediation}`);
4515
+ }
4516
+ function reportCrlfGuardCheck(section2, run = execFileSync11) {
4517
+ const repo = repoHome();
4518
+ if (hasGitattributesGuard(repo)) {
4519
+ addItem(section2, `${green(okGlyph)} CRLF guard: .gitattributes (* -text) present`);
4520
+ return;
4521
+ }
4522
+ addExposedRow(section2, repo, probeAutocrlf(repo, run));
4523
+ }
4524
+
4308
4525
  // src/spinner.ts
4309
4526
  init_color();
4310
- import { existsSync as existsSync30 } from "node:fs";
4527
+ import { existsSync as existsSync32 } from "node:fs";
4311
4528
  import { fileURLToPath as fileURLToPath4 } from "node:url";
4312
4529
  import { Worker } from "node:worker_threads";
4313
4530
 
4314
4531
  // src/commands.push.recovery.ts
4315
4532
  init_config();
4316
- import { readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
4317
- import { join as join36 } from "node:path";
4533
+ import { readFileSync as readFileSync16, rmSync as rmSync12, writeFileSync as writeFileSync5 } from "node:fs";
4534
+ import { join as join38 } from "node:path";
4318
4535
  import { createInterface as createInterface2 } from "node:readline/promises";
4319
4536
 
4320
4537
  // src/commands.push.recovery.actions.ts
4321
4538
  init_config();
4322
- import { readFileSync as readFileSync14 } from "node:fs";
4323
- import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep5 } from "node:path";
4539
+ import { readFileSync as readFileSync15 } from "node:fs";
4540
+ import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep6 } from "node:path";
4324
4541
 
4325
4542
  // src/commands.push.recovery.redact.ts
4326
4543
  init_config();
4327
4544
  init_config_sharedDirs_guard();
4328
- import { cpSync as cpSync6, existsSync as existsSync29, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
4329
- import { dirname as dirname8, join as join34, sep as sep4 } from "node:path";
4545
+ import { cpSync as cpSync7, existsSync as existsSync31, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
4546
+ import { dirname as dirname8, join as join36, sep as sep5 } from "node:path";
4330
4547
 
4331
4548
  // src/commands.redact.ts
4332
4549
  init_config();
4333
- import { existsSync as existsSync28, statSync as statSync7 } from "node:fs";
4334
- import { dirname as dirname7, join as join33 } from "node:path";
4550
+ import { existsSync as existsSync30, statSync as statSync7 } from "node:fs";
4551
+ import { dirname as dirname7, join as join35 } from "node:path";
4335
4552
 
4336
4553
  // src/commands.redact.subtree.ts
4337
- import { existsSync as existsSync27, lstatSync as lstatSync10, readFileSync as readFileSync13, readdirSync as readdirSync12, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
4338
- import { join as join32 } from "node:path";
4554
+ import { existsSync as existsSync29, lstatSync as lstatSync11, readFileSync as readFileSync14, readdirSync as readdirSync13, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
4555
+ import { join as join34 } from "node:path";
4339
4556
  init_utils_fs();
4340
4557
  init_utils();
4341
4558
  function collectFiles(dir, out) {
4342
- if (!existsSync27(dir)) return;
4343
- const st = lstatSync10(dir);
4559
+ if (!existsSync29(dir)) return;
4560
+ const st = lstatSync11(dir);
4344
4561
  if (!st.isDirectory()) return;
4345
- for (const entry of readdirSync12(dir)) {
4346
- const abs = join32(dir, entry);
4347
- const lst = lstatSync10(abs);
4562
+ for (const entry of readdirSync13(dir)) {
4563
+ const abs = join34(dir, entry);
4564
+ const lst = lstatSync11(abs);
4348
4565
  if (lst.isSymbolicLink()) continue;
4349
4566
  if (lst.isDirectory()) {
4350
4567
  collectFiles(abs, out);
@@ -4380,7 +4597,7 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
4380
4597
  if (!dryRun && total > 0) {
4381
4598
  for (const { path: filePath, findings } of dirty) {
4382
4599
  backupBeforeWrite(filePath, ts);
4383
- const before = readFileSync13(filePath, "utf8");
4600
+ const before = readFileSync14(filePath, "utf8");
4384
4601
  const after = applyRedactions(before, findings);
4385
4602
  if (after === before) {
4386
4603
  log(
@@ -4395,10 +4612,10 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
4395
4612
 
4396
4613
  // src/commands.pushed-history.ts
4397
4614
  init_utils();
4398
- import { execFileSync as execFileSync10 } from "node:child_process";
4615
+ import { execFileSync as execFileSync12 } from "node:child_process";
4399
4616
  function pushedRef(repo) {
4400
4617
  try {
4401
- const ref = execFileSync10("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
4618
+ const ref = execFileSync12("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
4402
4619
  cwd: repo,
4403
4620
  stdio: ["ignore", "pipe", "ignore"]
4404
4621
  }).toString().trim();
@@ -4411,7 +4628,7 @@ function sessionInPushedHistory(id, repo) {
4411
4628
  const ref = pushedRef(repo);
4412
4629
  if (ref === null) return false;
4413
4630
  try {
4414
- const out = execFileSync10(
4631
+ const out = execFileSync12(
4415
4632
  "git",
4416
4633
  [
4417
4634
  "log",
@@ -4448,15 +4665,15 @@ init_utils_json();
4448
4665
  init_utils();
4449
4666
  function resolveLiveTranscript(id) {
4450
4667
  try {
4451
- const mapPath = join33(repoHome(), "path-map.json");
4452
- if (!existsSync28(mapPath)) return null;
4668
+ const mapPath = join35(repoHome(), "path-map.json");
4669
+ if (!existsSync30(mapPath)) return null;
4453
4670
  const projects = readJson(mapPath).projects;
4454
4671
  const claude = claudeHome();
4455
4672
  for (const hostMap of Object.values(projects)) {
4456
4673
  const abs = hostMap[HOST];
4457
4674
  if (abs === void 0) continue;
4458
- const live = join33(claude, "projects", encodePath(abs), `${id}.jsonl`);
4459
- if (existsSync28(live)) return live;
4675
+ const live = join35(claude, "projects", encodePath(abs), `${id}.jsonl`);
4676
+ if (existsSync30(live)) return live;
4460
4677
  }
4461
4678
  return null;
4462
4679
  } catch {
@@ -4476,17 +4693,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
4476
4693
  }
4477
4694
  const repo = repoHome();
4478
4695
  const backup = backupBase();
4479
- if (!existsSync28(repo)) die(`repo not cloned at ${repo}`);
4696
+ if (!existsSync30(repo)) die(`repo not cloned at ${repo}`);
4480
4697
  const handle = acquireLock("redact");
4481
4698
  if (handle === null) process.exit(0);
4482
4699
  try {
4483
4700
  const localPath = resolveLiveTranscript(id);
4484
- if (localPath === null || !existsSync28(localPath)) {
4701
+ if (localPath === null || !existsSync30(localPath)) {
4485
4702
  fail(`could not resolve local transcript for session ${id} on this host`);
4486
4703
  process.exitCode = 1;
4487
4704
  return;
4488
4705
  }
4489
- const sessionDir = join33(dirname7(localPath), id);
4706
+ const sessionDir = join35(dirname7(localPath), id);
4490
4707
  const subtreeFiles = listSubtreeFiles(sessionDir);
4491
4708
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
4492
4709
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -4602,8 +4819,8 @@ function resolveStagedDir(localPath, map, claude, repo) {
4602
4819
  assertSafeLogical(logical);
4603
4820
  const abs = hostMap[HOST];
4604
4821
  if (abs === void 0) continue;
4605
- if (localPath.startsWith(join34(claude, "projects", encodePath(abs)) + sep4)) {
4606
- return join34(repo, "shared", "projects", logical);
4822
+ if (localPath.startsWith(join36(claude, "projects", encodePath(abs)) + sep5)) {
4823
+ return join36(repo, "shared", "projects", logical);
4607
4824
  }
4608
4825
  }
4609
4826
  return null;
@@ -4613,7 +4830,7 @@ function preflightRedactable(f, map, nowMs) {
4613
4830
  if (sid === null) return "a finding has no resolvable session id (not a session transcript)";
4614
4831
  const localPath = resolveLiveTranscript(sid);
4615
4832
  if (localPath === null) return `session ${sid}: local transcript not found`;
4616
- const sessionDir = join34(dirname8(localPath), sid);
4833
+ const sessionDir = join36(dirname8(localPath), sid);
4617
4834
  const subtreeFiles = listSubtreeFiles(sessionDir);
4618
4835
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
4619
4836
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -4643,7 +4860,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4643
4860
  `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
4644
4861
  );
4645
4862
  }
4646
- const sessionDir = join34(dirname8(localPath), sid);
4863
+ const sessionDir = join36(dirname8(localPath), sid);
4647
4864
  const subtreeFiles = listSubtreeFiles(sessionDir);
4648
4865
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
4649
4866
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -4677,26 +4894,26 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4677
4894
  );
4678
4895
  }
4679
4896
  mkdirSync7(stagedProjectDir, { recursive: true });
4680
- cpSync6(localPath, join34(stagedProjectDir, `${sid}.jsonl`), { force: true });
4681
- if (existsSync29(sessionDir)) {
4682
- cpSync6(sessionDir, join34(stagedProjectDir, sid), { force: true, recursive: true });
4897
+ cpSync7(localPath, join36(stagedProjectDir, `${sid}.jsonl`), { force: true });
4898
+ if (existsSync31(sessionDir)) {
4899
+ cpSync7(sessionDir, join36(stagedProjectDir, sid), { force: true, recursive: true });
4683
4900
  }
4684
4901
  return true;
4685
4902
  }
4686
4903
 
4687
4904
  // src/commands.push.recovery.drop.ts
4688
4905
  init_config();
4689
- import { rmSync as rmSync10 } from "node:fs";
4690
- import { join as join35 } from "node:path";
4906
+ import { rmSync as rmSync11 } from "node:fs";
4907
+ import { join as join37 } from "node:path";
4691
4908
  function dropSessionFromStaged(sid, map) {
4692
4909
  const logicals = Object.keys(map.projects);
4693
4910
  if (logicals.length === 0) return false;
4694
4911
  const repo = repoHome();
4695
4912
  for (const logical of logicals) {
4696
- const jsonl = join35(repo, "shared", "projects", logical, `${sid}.jsonl`);
4697
- const dir = join35(repo, "shared", "projects", logical, sid);
4698
- rmSync10(jsonl, { force: true });
4699
- rmSync10(dir, { recursive: true, force: true });
4913
+ const jsonl = join37(repo, "shared", "projects", logical, `${sid}.jsonl`);
4914
+ const dir = join37(repo, "shared", "projects", logical, sid);
4915
+ rmSync11(jsonl, { force: true });
4916
+ rmSync11(dir, { recursive: true, force: true });
4700
4917
  }
4701
4918
  return true;
4702
4919
  }
@@ -4727,10 +4944,10 @@ function makeDefaultReadLine(repo) {
4727
4944
  try {
4728
4945
  const repoRoot = resolve3(repo);
4729
4946
  const target = resolve3(repoRoot, file);
4730
- if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep5)) {
4947
+ if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep6)) {
4731
4948
  return null;
4732
4949
  }
4733
- const content = readFileSync14(target, "utf8");
4950
+ const content = readFileSync15(target, "utf8");
4734
4951
  const lines = content.split(/\r?\n/);
4735
4952
  const idx = line - 1;
4736
4953
  if (idx < 0 || idx >= lines.length) return null;
@@ -4851,10 +5068,10 @@ function applyThenRescan(scanVerdict, repoHome2) {
4851
5068
  return next;
4852
5069
  }
4853
5070
  function allowThenRescan(append, scanVerdict, repoHome2) {
4854
- const ignPath = join36(repoHome2, ".gitleaksignore");
5071
+ const ignPath = join38(repoHome2, ".gitleaksignore");
4855
5072
  let before;
4856
5073
  try {
4857
- before = readFileSync15(ignPath, "utf8");
5074
+ before = readFileSync16(ignPath, "utf8");
4858
5075
  } catch {
4859
5076
  before = null;
4860
5077
  }
@@ -4862,7 +5079,7 @@ function allowThenRescan(append, scanVerdict, repoHome2) {
4862
5079
  try {
4863
5080
  return applyThenRescan(scanVerdict, repoHome2);
4864
5081
  } catch (err) {
4865
- if (before === null) rmSync11(ignPath, { force: true });
5082
+ if (before === null) rmSync12(ignPath, { force: true });
4866
5083
  else writeFileSync5(ignPath, before, "utf8");
4867
5084
  throw err;
4868
5085
  }
@@ -4950,7 +5167,7 @@ function writeAnimatedDone(out, label, ms, useTTY) {
4950
5167
  `);
4951
5168
  }
4952
5169
  function resolveWorkerPath(deps = {}) {
4953
- const check = deps.existsSyncFn ?? existsSync30;
5170
+ const check = deps.existsSyncFn ?? existsSync32;
4954
5171
  const base = deps.baseUrl ?? import.meta.url;
4955
5172
  const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
4956
5173
  if (check(mjs)) return mjs;
@@ -5016,9 +5233,9 @@ function withSpinner(label, fn, deps) {
5016
5233
 
5017
5234
  // src/commands.doctor.gitleaks-version.ts
5018
5235
  init_color();
5019
- import { execFileSync as execFileSync11 } from "node:child_process";
5020
- import { existsSync as existsSync31 } from "node:fs";
5021
- import { join as join37 } from "node:path";
5236
+ import { execFileSync as execFileSync13 } from "node:child_process";
5237
+ import { existsSync as existsSync33 } from "node:fs";
5238
+ import { join as join39 } from "node:path";
5022
5239
  init_config();
5023
5240
  var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
5024
5241
  var GITLEAKS_TIMEOUT_MS = 5e3;
@@ -5027,7 +5244,7 @@ function majorMinorOf(value) {
5027
5244
  return m === null ? null : [m[1], m[2]];
5028
5245
  }
5029
5246
  function readGitleaksVersion(run, tomlExists) {
5030
- const tomlPath = join37(repoHome(), ".gitleaks.toml");
5247
+ const tomlPath = join39(repoHome(), ".gitleaks.toml");
5031
5248
  const args = ["version"];
5032
5249
  if (tomlExists(tomlPath)) args.push("--config", tomlPath);
5033
5250
  try {
@@ -5039,7 +5256,7 @@ function readGitleaksVersion(run, tomlExists) {
5039
5256
  return null;
5040
5257
  }
5041
5258
  }
5042
- function reportGitleaksVersionCheck(section2, run = execFileSync11, tomlExists = existsSync31) {
5259
+ function reportGitleaksVersionCheck(section2, run = execFileSync13, tomlExists = existsSync33) {
5043
5260
  const raw = readGitleaksVersion(run, tomlExists);
5044
5261
  if (raw === null) return;
5045
5262
  const local = majorMinorOf(raw);
@@ -5059,9 +5276,9 @@ function reportGitleaksVersionCheck(section2, run = execFileSync11, tomlExists =
5059
5276
 
5060
5277
  // src/commands.doctor.checks.deps.ts
5061
5278
  init_color();
5062
- import { execFileSync as execFileSync12 } from "node:child_process";
5279
+ import { execFileSync as execFileSync14 } from "node:child_process";
5063
5280
  var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
5064
- var PROBE_TIMEOUT_MS = 3e3;
5281
+ var PROBE_TIMEOUT_MS3 = 3e3;
5065
5282
  var FETCHER_BASE = "HTTP fetcher";
5066
5283
  function parseFirstVersion(line) {
5067
5284
  const m = VERSION_TOKEN.exec(line);
@@ -5071,7 +5288,7 @@ function probeOptionalDep(bin, run) {
5071
5288
  try {
5072
5289
  const firstLine = run(bin, ["--version"], {
5073
5290
  stdio: ["ignore", "pipe", "pipe"],
5074
- timeout: PROBE_TIMEOUT_MS
5291
+ timeout: PROBE_TIMEOUT_MS3
5075
5292
  }).toString().split("\n")[0].trim();
5076
5293
  const version = parseFirstVersion(firstLine);
5077
5294
  return { status: "present", version };
@@ -5096,7 +5313,7 @@ function reportFetcherRow(section2, run) {
5096
5313
  );
5097
5314
  }
5098
5315
  }
5099
- function reportOptionalDeps(section2, run = execFileSync12) {
5316
+ function reportOptionalDeps(section2, run = execFileSync14) {
5100
5317
  const gh = probeOptionalDep("gh", run);
5101
5318
  if (gh.status === "present") {
5102
5319
  addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
@@ -5111,11 +5328,11 @@ function reportOptionalDeps(section2, run = execFileSync12) {
5111
5328
 
5112
5329
  // src/commands.doctor.actions-drift.ts
5113
5330
  init_color();
5114
- import { execFileSync as execFileSync14 } from "node:child_process";
5331
+ import { execFileSync as execFileSync16 } from "node:child_process";
5115
5332
  init_config();
5116
5333
 
5117
5334
  // src/gh-actions.ts
5118
- import { execFileSync as execFileSync13 } from "node:child_process";
5335
+ import { execFileSync as execFileSync15 } from "node:child_process";
5119
5336
  var GH_TIMEOUT_MS = 5e3;
5120
5337
  function parseGitHubRemote(remoteUrl) {
5121
5338
  const normalized = remoteUrl.trim().replace(/\/$/, "");
@@ -5123,7 +5340,7 @@ function parseGitHubRemote(remoteUrl) {
5123
5340
  if (m === null) return null;
5124
5341
  return { owner: m[1], repo: m[2] };
5125
5342
  }
5126
- function ghAuthStatus(run = execFileSync13) {
5343
+ function ghAuthStatus(run = execFileSync15) {
5127
5344
  try {
5128
5345
  run("gh", ["auth", "status"], {
5129
5346
  stdio: ["ignore", "ignore", "ignore"],
@@ -5137,7 +5354,7 @@ function ghAuthStatus(run = execFileSync13) {
5137
5354
  return "gh-probe-error";
5138
5355
  }
5139
5356
  }
5140
- function isRepoPrivate(ref, run = execFileSync13) {
5357
+ function isRepoPrivate(ref, run = execFileSync15) {
5141
5358
  const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
5142
5359
  stdio: ["ignore", "pipe", "ignore"],
5143
5360
  timeout: GH_TIMEOUT_MS
@@ -5145,7 +5362,7 @@ function isRepoPrivate(ref, run = execFileSync13) {
5145
5362
  const parsed = JSON.parse(out);
5146
5363
  return parsed.isPrivate === true;
5147
5364
  }
5148
- function isActionsEnabled(ref, run = execFileSync13) {
5365
+ function isActionsEnabled(ref, run = execFileSync15) {
5149
5366
  const out = run(
5150
5367
  "gh",
5151
5368
  ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
@@ -5153,7 +5370,7 @@ function isActionsEnabled(ref, run = execFileSync13) {
5153
5370
  ).toString().trim();
5154
5371
  return out === "true";
5155
5372
  }
5156
- function disableActions(ref, run = execFileSync13) {
5373
+ function disableActions(ref, run = execFileSync15) {
5157
5374
  run(
5158
5375
  "gh",
5159
5376
  [
@@ -5167,7 +5384,7 @@ function disableActions(ref, run = execFileSync13) {
5167
5384
  { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
5168
5385
  );
5169
5386
  }
5170
- function readOriginRemote(cwd, run = execFileSync13) {
5387
+ function readOriginRemote(cwd, run = execFileSync15) {
5171
5388
  return run("git", ["remote", "get-url", "origin"], {
5172
5389
  cwd,
5173
5390
  stdio: ["ignore", "pipe", "ignore"]
@@ -5175,7 +5392,7 @@ function readOriginRemote(cwd, run = execFileSync13) {
5175
5392
  }
5176
5393
 
5177
5394
  // src/commands.doctor.actions-drift.ts
5178
- function reportActionsDrift(section2, run = execFileSync14) {
5395
+ function reportActionsDrift(section2, run = execFileSync16) {
5179
5396
  let remote;
5180
5397
  try {
5181
5398
  remote = readOriginRemote(repoHome(), run);
@@ -5261,9 +5478,12 @@ function gatherDoctorSections(opts) {
5261
5478
  reportHostAndPaths(host);
5262
5479
  reportHostKeyAlignment(host);
5263
5480
  reportRepoState(host);
5481
+ reportSyncModality(host);
5482
+ reportLongPathsCheck(host);
5483
+ reportCrlfGuardCheck(host);
5264
5484
  const links = section("Shared links");
5265
- const mapPath = join38(repoHome(), "path-map.json");
5266
- const rawMap = existsSync32(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
5485
+ const mapPath = join40(repoHome(), "path-map.json");
5486
+ const rawMap = existsSync34(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
5267
5487
  const map = rawMap ?? { projects: {} };
5268
5488
  reportSharedLinks(links, map);
5269
5489
  reportDroppedNamesMigration(links);
@@ -5359,15 +5579,15 @@ function parseDoctorArgs(args) {
5359
5579
 
5360
5580
  // src/commands.drop-session.ts
5361
5581
  init_config();
5362
- import { execFileSync as execFileSync16 } from "node:child_process";
5363
- import { existsSync as existsSync34, readdirSync as readdirSync13, statSync as statSync9 } from "node:fs";
5364
- import { join as join40, relative as relative6 } from "node:path";
5582
+ import { execFileSync as execFileSync18 } from "node:child_process";
5583
+ import { existsSync as existsSync36, readdirSync as readdirSync14, statSync as statSync9 } from "node:fs";
5584
+ import { join as join42, relative as relative6 } from "node:path";
5365
5585
 
5366
5586
  // src/commands.drop-session.git.ts
5367
- import { execFileSync as execFileSync15 } from "node:child_process";
5587
+ import { execFileSync as execFileSync17 } from "node:child_process";
5368
5588
  function expandStagedDir(dirRel, repo) {
5369
5589
  try {
5370
- const out = execFileSync15("git", ["ls-files", "-z", "--", dirRel], {
5590
+ const out = execFileSync17("git", ["ls-files", "-z", "--", dirRel], {
5371
5591
  cwd: repo,
5372
5592
  stdio: ["ignore", "pipe", "pipe"]
5373
5593
  });
@@ -5378,7 +5598,8 @@ function expandStagedDir(dirRel, repo) {
5378
5598
  }
5379
5599
  function isTrackedInHead(rel, repo) {
5380
5600
  try {
5381
- execFileSync15("git", ["cat-file", "-e", `HEAD:${rel}`], {
5601
+ const treePath = process.platform === "win32" ? rel.replaceAll("\\", "/") : rel;
5602
+ execFileSync17("git", ["cat-file", "-e", `HEAD:${treePath}`], {
5382
5603
  cwd: repo,
5383
5604
  stdio: ["ignore", "pipe", "pipe"]
5384
5605
  });
@@ -5389,7 +5610,7 @@ function isTrackedInHead(rel, repo) {
5389
5610
  }
5390
5611
  function isInIndex(rel, repo) {
5391
5612
  try {
5392
- const out = execFileSync15("git", ["ls-files", "--", rel], {
5613
+ const out = execFileSync17("git", ["ls-files", "--", rel], {
5393
5614
  cwd: repo,
5394
5615
  stdio: ["ignore", "pipe", "pipe"]
5395
5616
  });
@@ -5403,8 +5624,8 @@ function isInIndex(rel, repo) {
5403
5624
  init_config();
5404
5625
  init_utils();
5405
5626
  init_utils_json();
5406
- import { existsSync as existsSync33 } from "node:fs";
5407
- import { join as join39 } from "node:path";
5627
+ import { existsSync as existsSync35 } from "node:fs";
5628
+ import { join as join41 } from "node:path";
5408
5629
  var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
5409
5630
  function reportScrubHint(id, matches) {
5410
5631
  const live = resolveLiveTranscript2(id, matches);
@@ -5420,17 +5641,17 @@ function reportScrubHint(id, matches) {
5420
5641
  }
5421
5642
  function resolveLiveTranscript2(id, matches) {
5422
5643
  try {
5423
- const mapPath = join39(repoHome(), "path-map.json");
5424
- if (!existsSync33(mapPath)) return null;
5644
+ const mapPath = join41(repoHome(), "path-map.json");
5645
+ if (!existsSync35(mapPath)) return null;
5425
5646
  const projects = readJson(mapPath).projects;
5426
5647
  const claude = claudeHome();
5427
5648
  for (const rel of matches) {
5428
- const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
5649
+ const logical = SHARED_PROJECT_LOGICAL.exec(rel.replaceAll("\\", "/"))?.[1];
5429
5650
  if (logical === void 0) continue;
5430
5651
  const abs = projects[logical]?.[HOST];
5431
5652
  if (abs === void 0) continue;
5432
- const live = join39(claude, "projects", encodePath(abs), `${id}.jsonl`);
5433
- if (existsSync33(live)) return live;
5653
+ const live = join41(claude, "projects", encodePath(abs), `${id}.jsonl`);
5654
+ if (existsSync35(live)) return live;
5434
5655
  }
5435
5656
  return null;
5436
5657
  } catch {
@@ -5446,12 +5667,12 @@ function cmdDropSession(id) {
5446
5667
  process.exit(1);
5447
5668
  }
5448
5669
  const repo = repoHome();
5449
- if (!existsSync34(repo)) die(`repo not cloned at ${repo}`);
5670
+ if (!existsSync36(repo)) die(`repo not cloned at ${repo}`);
5450
5671
  const handle = acquireLock("drop-session");
5451
5672
  if (handle === null) process.exit(0);
5452
5673
  try {
5453
- const repoProjects = join40(repo, "shared", "projects");
5454
- if (!existsSync34(repoProjects)) {
5674
+ const repoProjects = join42(repo, "shared", "projects");
5675
+ if (!existsSync36(repoProjects)) {
5455
5676
  throw new NomadFatal(`no staged session matches ${id}`);
5456
5677
  }
5457
5678
  const matches = collectMatches(repoProjects, id, repo);
@@ -5473,13 +5694,13 @@ function cmdDropSession(id) {
5473
5694
  }
5474
5695
  function collectMatches(repoProjects, id, repo) {
5475
5696
  const matches = [];
5476
- for (const logical of readdirSync13(repoProjects)) {
5477
- const candidate = join40(repoProjects, logical, `${id}.jsonl`);
5478
- if (existsSync34(candidate)) {
5697
+ for (const logical of readdirSync14(repoProjects)) {
5698
+ const candidate = join42(repoProjects, logical, `${id}.jsonl`);
5699
+ if (existsSync36(candidate)) {
5479
5700
  matches.push(relative6(repo, candidate));
5480
5701
  }
5481
- const dir = join40(repoProjects, logical, id);
5482
- if (existsSync34(dir) && statSync9(dir).isDirectory()) {
5702
+ const dir = join42(repoProjects, logical, id);
5703
+ if (existsSync36(dir) && statSync9(dir).isDirectory()) {
5483
5704
  const dirRel = relative6(repo, dir);
5484
5705
  const staged = expandStagedDir(dirRel, repo);
5485
5706
  if (staged.length > 0) matches.push(...staged);
@@ -5495,12 +5716,12 @@ function unstageOne(rel, repo) {
5495
5716
  }
5496
5717
  try {
5497
5718
  if (isTrackedInHead(rel, repo)) {
5498
- execFileSync16("git", ["restore", "--staged", "--worktree", "--", rel], {
5719
+ execFileSync18("git", ["restore", "--staged", "--worktree", "--", rel], {
5499
5720
  cwd: repo,
5500
5721
  stdio: ["ignore", "pipe", "pipe"]
5501
5722
  });
5502
5723
  } else {
5503
- execFileSync16("git", ["rm", "--cached", "-f", "--", rel], {
5724
+ execFileSync18("git", ["rm", "--cached", "-f", "--", rel], {
5504
5725
  cwd: repo,
5505
5726
  stdio: ["ignore", "pipe", "pipe"]
5506
5727
  });
@@ -5514,8 +5735,8 @@ function unstageOne(rel, repo) {
5514
5735
  }
5515
5736
 
5516
5737
  // src/commands.pull.ts
5517
- import { existsSync as existsSync39, mkdirSync as mkdirSync10 } from "node:fs";
5518
- import { join as join46 } from "node:path";
5738
+ import { existsSync as existsSync41, mkdirSync as mkdirSync10 } from "node:fs";
5739
+ import { join as join48 } from "node:path";
5519
5740
 
5520
5741
  // src/commands.push.sections.ts
5521
5742
  init_color();
@@ -5617,18 +5838,18 @@ init_config();
5617
5838
 
5618
5839
  // src/extras-sync.ts
5619
5840
  init_config();
5620
- import { existsSync as existsSync36, statSync as statSync10 } from "node:fs";
5621
- import { join as join43 } from "node:path";
5841
+ import { existsSync as existsSync38, statSync as statSync10 } from "node:fs";
5842
+ import { join as join45 } from "node:path";
5622
5843
  init_utils();
5623
5844
  init_utils_json();
5624
5845
 
5625
5846
  // src/extras-sync.remap.ts
5626
5847
  init_config();
5627
- import { existsSync as existsSync35, mkdirSync as mkdirSync8, readdirSync as readdirSync14, readFileSync as readFileSync16, realpathSync as realpathSync4, rmSync as rmSync12 } from "node:fs";
5628
- import { dirname as dirname9, join as join42, sep as sep7 } from "node:path";
5848
+ import { existsSync as existsSync37, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync17, realpathSync as realpathSync4, rmSync as rmSync13 } from "node:fs";
5849
+ import { dirname as dirname9, join as join44, sep as sep8 } from "node:path";
5629
5850
 
5630
5851
  // src/extras-sync.planning-diff.ts
5631
- import { join as join41, normalize as normalize2, sep as sep6 } from "node:path";
5852
+ import { join as join43, normalize as normalize2, sep as sep7 } from "node:path";
5632
5853
  init_utils();
5633
5854
  function processRecord(fields, i, changed, deleted) {
5634
5855
  const status = fields[i];
@@ -5680,15 +5901,15 @@ function planningDeleteTargets(opts) {
5680
5901
  const { deleted } = parsePlanningDiff(raw);
5681
5902
  const logicalPrefix = "shared/extras/" + logical + "/";
5682
5903
  const prefix = logicalPrefix + ".planning/";
5683
- const planningRoot = join41(localRoot, ".planning");
5684
- const planningRootBoundary = planningRoot + sep6;
5904
+ const planningRoot = join43(localRoot, ".planning");
5905
+ const planningRootBoundary = planningRoot + sep7;
5685
5906
  const targets = [];
5686
5907
  for (const repoPath of deleted) {
5687
5908
  if (!repoPath.startsWith(prefix)) {
5688
5909
  continue;
5689
5910
  }
5690
5911
  const remainder = repoPath.slice(logicalPrefix.length);
5691
- const candidate = join41(localRoot, remainder);
5912
+ const candidate = join43(localRoot, remainder);
5692
5913
  const resolved = normalize2(candidate);
5693
5914
  if (!resolved.startsWith(planningRootBoundary)) {
5694
5915
  throw new NomadFatal(
@@ -5709,7 +5930,7 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
5709
5930
  const would = [];
5710
5931
  for (const t of eachExtrasTarget(v, counts)) {
5711
5932
  const { src, dst } = paths(t);
5712
- if (!existsSync35(src)) continue;
5933
+ if (!existsSync37(src)) continue;
5713
5934
  const item2 = `${t.logical}/${t.dirname}`;
5714
5935
  if (dryRun) {
5715
5936
  would.push(item2);
@@ -5723,10 +5944,10 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
5723
5944
  }
5724
5945
  function pruneEmptyAncestors(target, planningRoot) {
5725
5946
  let dir = dirname9(target);
5726
- while (dir !== planningRoot && dir.startsWith(planningRoot + sep7)) {
5947
+ while (dir !== planningRoot && dir.startsWith(planningRoot + sep8)) {
5727
5948
  try {
5728
- if (readdirSync14(dir).length > 0) break;
5729
- rmSync12(dir, { recursive: true, force: true });
5949
+ if (readdirSync15(dir).length > 0) break;
5950
+ rmSync13(dir, { recursive: true, force: true });
5730
5951
  } catch {
5731
5952
  break;
5732
5953
  }
@@ -5741,23 +5962,23 @@ function tryRealpath(dir) {
5741
5962
  }
5742
5963
  }
5743
5964
  function isInsidePlanningRoot(parentReal, rootReal) {
5744
- return parentReal === rootReal || parentReal.startsWith(rootReal + sep7);
5965
+ return parentReal === rootReal || parentReal.startsWith(rootReal + sep8);
5745
5966
  }
5746
5967
  function deletePlanningTarget(target, planningRoot, repoCounterpart) {
5747
- if (existsSync35(repoCounterpart)) return;
5968
+ if (existsSync37(repoCounterpart)) return;
5748
5969
  const parentReal = tryRealpath(dirname9(target));
5749
5970
  if (parentReal === void 0) return;
5750
5971
  const rootReal = tryRealpath(planningRoot);
5751
5972
  if (rootReal === void 0) return;
5752
5973
  if (!isInsidePlanningRoot(parentReal, rootReal)) return;
5753
- rmSync12(target, { recursive: true, force: true });
5974
+ rmSync13(target, { recursive: true, force: true });
5754
5975
  pruneEmptyAncestors(target, planningRoot);
5755
5976
  }
5756
5977
  function localDivergesFromPreDelete(target, pre, repoRel, repo) {
5757
- if (!existsSync35(target)) return false;
5978
+ if (!existsSync37(target)) return false;
5758
5979
  try {
5759
5980
  const preBlob = gitCaptureBuffer(["show", `${pre}:${repoRel}`], repo);
5760
- return !readFileSync16(target).equals(preBlob);
5981
+ return !readFileSync17(target).equals(preBlob);
5761
5982
  } catch {
5762
5983
  return true;
5763
5984
  }
@@ -5768,11 +5989,11 @@ function planningDiffArgs(pre, post, logical) {
5768
5989
  function deletePairsFor(t, raw) {
5769
5990
  return planningDeleteTargets({ raw, logical: t.logical, localRoot: t.localRoot }).map(
5770
5991
  (target) => {
5771
- const relToLocal = target.slice(t.localRoot.length + sep7.length);
5992
+ const relToLocal = target.slice(t.localRoot.length + sep8.length);
5772
5993
  return {
5773
5994
  target,
5774
5995
  relToLocal,
5775
- repoRel: `shared/extras/${t.logical}/${relToLocal.split(sep7).join("/")}`
5996
+ repoRel: `shared/extras/${t.logical}/${relToLocal.split(sep8).join("/")}`
5776
5997
  };
5777
5998
  }
5778
5999
  );
@@ -5799,7 +6020,7 @@ function keptDeletePreview(v, prePostHeads, repo) {
5799
6020
  return kept;
5800
6021
  }
5801
6022
  function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
5802
- const repoExtras = join42(repo, "shared", "extras");
6023
+ const repoExtras = join44(repo, "shared", "extras");
5803
6024
  for (const t of eachExtrasTarget(v, { unmapped: 0, skipped: 0 })) {
5804
6025
  if (t.dirname !== ".planning") continue;
5805
6026
  let raw;
@@ -5814,14 +6035,14 @@ function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
5814
6035
  }
5815
6036
  const pairs = deletePairsFor(t, raw);
5816
6037
  if (pairs.length === 0) continue;
5817
- backupExtrasWrite(join42(t.localRoot, t.dirname), ts, t.localRoot);
5818
- const planningRoot = join42(t.localRoot, ".planning");
6038
+ backupExtrasWrite(join44(t.localRoot, t.dirname), ts, t.localRoot);
6039
+ const planningRoot = join44(t.localRoot, ".planning");
5819
6040
  for (const { target, relToLocal, repoRel } of pairs) {
5820
6041
  if (localDivergesFromPreDelete(target, prePostHeads.pre, repoRel, repo)) {
5821
6042
  warn(keptDeleteWarnLine(t.logical, relToLocal));
5822
6043
  continue;
5823
6044
  }
5824
- deletePlanningTarget(target, planningRoot, join42(repoExtras, t.logical, relToLocal));
6045
+ deletePlanningTarget(target, planningRoot, join44(repoExtras, t.logical, relToLocal));
5825
6046
  }
5826
6047
  }
5827
6048
  }
@@ -5830,14 +6051,14 @@ function remapExtrasPush(ts, opts = {}) {
5830
6051
  const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
5831
6052
  if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
5832
6053
  const repo = repoHome();
5833
- const repoExtras = join42(repo, "shared", "extras");
6054
+ const repoExtras = join44(repo, "shared", "extras");
5834
6055
  if (!dryRun) mkdirSync8(repoExtras, { recursive: true });
5835
6056
  const { unmapped, skipped, done, would } = runExtrasOp(
5836
6057
  v,
5837
6058
  dryRun,
5838
6059
  ({ localRoot, logical, dirname: dirname10 }) => ({
5839
- src: join42(localRoot, dirname10),
5840
- dst: join42(repoExtras, logical, dirname10)
6060
+ src: join44(localRoot, dirname10),
6061
+ dst: join44(repoExtras, logical, dirname10)
5841
6062
  }),
5842
6063
  (dst) => backupRepoWrite(dst, ts, repo),
5843
6064
  // Push copy routing per extra type:
@@ -5866,8 +6087,8 @@ function remapExtrasPull(ts, opts = {}) {
5866
6087
  v,
5867
6088
  dryRun,
5868
6089
  ({ localRoot, logical, dirname: dirname10 }) => ({
5869
- src: join42(repo, "shared", "extras", logical, dirname10),
5870
- dst: join42(localRoot, dirname10)
6090
+ src: join44(repo, "shared", "extras", logical, dirname10),
6091
+ dst: join44(localRoot, dirname10)
5871
6092
  }),
5872
6093
  // Snapshot the host-side dst BEFORE the copy step touches it. Anchor on
5873
6094
  // localRoot so the backup tree mirrors the project layout.
@@ -5914,17 +6135,17 @@ function divergenceCheckExtras(ts, prePostHeads) {
5914
6135
  const v = loadValidatedExtras({});
5915
6136
  if (v === null) return 0;
5916
6137
  const counts = { unmapped: 0, skipped: 0 };
5917
- const backupRoot = join43(backupBase(), ts, "extras");
6138
+ const backupRoot = join45(backupBase(), ts, "extras");
5918
6139
  const repo = repoHome();
5919
6140
  let divergedCount = 0;
5920
6141
  for (const { logical, localRoot, dirname: dirname10 } of eachExtrasTarget(v, counts)) {
5921
- const local = join43(localRoot, dirname10);
5922
- const repoEntry = join43(repo, "shared", "extras", logical, dirname10);
5923
- if (!existsSync36(local) || !existsSync36(repoEntry)) continue;
6142
+ const local = join45(localRoot, dirname10);
6143
+ const repoEntry = join45(repo, "shared", "extras", logical, dirname10);
6144
+ if (!existsSync38(local) || !existsSync38(repoEntry)) continue;
5924
6145
  const modified = listDivergingModified(local, repoEntry);
5925
6146
  if (modified.length === 0) continue;
5926
6147
  divergedCount += modified.length;
5927
- const projectBackupRoot = join43(backupRoot, encodePath(localRoot));
6148
+ const projectBackupRoot = join45(backupRoot, encodePath(localRoot));
5928
6149
  warn(
5929
6150
  divergenceWarnLine({
5930
6151
  dirname: dirname10,
@@ -5946,8 +6167,8 @@ function divergenceCheckExtras(ts, prePostHeads) {
5946
6167
 
5947
6168
  // src/skills-sync.ts
5948
6169
  init_config();
5949
- import { existsSync as existsSync37, lstatSync as lstatSync11, mkdirSync as mkdirSync9, readdirSync as readdirSync15, rmSync as rmSync13 } from "node:fs";
5950
- import { join as join44 } from "node:path";
6170
+ import { existsSync as existsSync39, lstatSync as lstatSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync16, rmSync as rmSync14 } from "node:fs";
6171
+ import { join as join46 } from "node:path";
5951
6172
  init_utils_fs();
5952
6173
  function isGsdOwned(name) {
5953
6174
  return name.startsWith(GSD_PREFIX);
@@ -5956,7 +6177,7 @@ function isSkillExcluded(name) {
5956
6177
  return isGsdOwned(name) || isDeniedName(ALWAYS_NEVER_SYNC, name);
5957
6178
  }
5958
6179
  function copySkillsPush(src, dst) {
5959
- const srcNames = readdirSync15(src, { encoding: "utf8" });
6180
+ const srcNames = readdirSync16(src, { encoding: "utf8" });
5960
6181
  const blockSet = /* @__PURE__ */ new Set([
5961
6182
  ...srcNames.filter((n) => isGsdOwned(n)),
5962
6183
  ...ALWAYS_NEVER_SYNC
@@ -5967,30 +6188,30 @@ function copySkillsPull(src, dst) {
5967
6188
  copyExtrasFilteredPreservingBy(src, dst, isSkillExcluded);
5968
6189
  }
5969
6190
  function syncSkillsPull(ts) {
5970
- const sharedSkills = join44(repoHome(), "shared", "skills");
5971
- if (!existsSync37(sharedSkills)) return;
5972
- const localSkills = join44(claudeHome(), "skills");
5973
- const dstStat = lstatSync11(localSkills, { throwIfNoEntry: false });
6191
+ const sharedSkills = join46(repoHome(), "shared", "skills");
6192
+ if (!existsSync39(sharedSkills)) return;
6193
+ const localSkills = join46(claudeHome(), "skills");
6194
+ const dstStat = lstatSync12(localSkills, { throwIfNoEntry: false });
5974
6195
  if (dstStat?.isSymbolicLink() === true) {
5975
6196
  backupBeforeWrite(localSkills, ts);
5976
- rmSync13(localSkills, { recursive: true, force: true });
6197
+ rmSync14(localSkills, { recursive: true, force: true });
5977
6198
  mkdirSync9(localSkills, { recursive: true });
5978
6199
  }
5979
6200
  copySkillsPull(sharedSkills, localSkills);
5980
6201
  }
5981
6202
  function syncSkillsPush() {
5982
- const localSkills = join44(claudeHome(), "skills");
5983
- const stat = lstatSync11(localSkills, { throwIfNoEntry: false });
6203
+ const localSkills = join46(claudeHome(), "skills");
6204
+ const stat = lstatSync12(localSkills, { throwIfNoEntry: false });
5984
6205
  if (stat === void 0) return;
5985
6206
  if (stat.isSymbolicLink()) return;
5986
- const sharedSkills = join44(repoHome(), "shared", "skills");
6207
+ const sharedSkills = join46(repoHome(), "shared", "skills");
5987
6208
  copySkillsPush(localSkills, sharedSkills);
5988
6209
  }
5989
6210
 
5990
6211
  // src/preview.ts
5991
6212
  init_config();
5992
- import { existsSync as existsSync38 } from "node:fs";
5993
- import { join as join45 } from "node:path";
6213
+ import { existsSync as existsSync40 } from "node:fs";
6214
+ import { join as join47 } from "node:path";
5994
6215
 
5995
6216
  // node_modules/diff/libesm/diff/base.js
5996
6217
  var Diff = class {
@@ -6276,7 +6497,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
6276
6497
  return lines.join("\n");
6277
6498
  }
6278
6499
  function readJsonOrNull(path) {
6279
- if (!existsSync38(path)) return null;
6500
+ if (!existsSync40(path)) return null;
6280
6501
  try {
6281
6502
  return readJson(path);
6282
6503
  } catch {
@@ -6290,12 +6511,12 @@ function previewSettings(basePath, hostPath, settingsPath) {
6290
6511
  }
6291
6512
  const notes = [];
6292
6513
  const hostOverrides = readJsonOrNull(hostPath);
6293
- if (hostOverrides === null && existsSync38(hostPath)) {
6514
+ if (hostOverrides === null && existsSync40(hostPath)) {
6294
6515
  notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
6295
6516
  }
6296
6517
  const merged = stripGsdHookEntries(deepMerge(base, hostOverrides ?? {}));
6297
6518
  const current = readJsonOrNull(settingsPath);
6298
- if (current === null && existsSync38(settingsPath)) {
6519
+ if (current === null && existsSync40(settingsPath)) {
6299
6520
  return { diff: "", notes: [...notes, "malformed; skipping diff"] };
6300
6521
  }
6301
6522
  const strippedCurrent = stripGsdHookEntries(current ?? {});
@@ -6308,6 +6529,7 @@ function previewSettings(basePath, hostPath, settingsPath) {
6308
6529
  return { diff, notes };
6309
6530
  }
6310
6531
  function formatLinkRow(e) {
6532
+ if (e.kind === "copy") return `would copy ${e.from} -> ${e.to}`;
6311
6533
  return `${e.kind} ${e.from} -> ${e.to}`;
6312
6534
  }
6313
6535
  function formatSessionRow(e) {
@@ -6336,9 +6558,9 @@ function computePreview(ts, map, verb = "pull") {
6336
6558
  onPreview: (e) => addItem(links, formatLinkRow(e))
6337
6559
  });
6338
6560
  const settingsResult = previewSettings(
6339
- join45(repo, "shared", "settings.base.json"),
6340
- join45(repo, "hosts", `${HOST}.json`),
6341
- join45(claude, "settings.json")
6561
+ join47(repo, "shared", "settings.base.json"),
6562
+ join47(repo, "hosts", `${HOST}.json`),
6563
+ join47(claude, "settings.json")
6342
6564
  );
6343
6565
  const settingsSection = buildSettingsSectionForPreview(settingsResult);
6344
6566
  const sessions = section("Sessions");
@@ -6353,7 +6575,7 @@ function computePreview(ts, map, verb = "pull") {
6353
6575
  const extras = section("Extras");
6354
6576
  let extrasSkipped = 0;
6355
6577
  let extrasUnmapped = 0;
6356
- if (existsSync38(join45(repo, "path-map.json")) && existsSync38(join45(repo, "shared", "extras"))) {
6578
+ if (existsSync40(join47(repo, "path-map.json")) && existsSync40(join47(repo, "shared", "extras"))) {
6357
6579
  const extrasResult = remapExtrasPull(ts, { dryRun: true });
6358
6580
  for (const entry of extrasResult.wouldPull) {
6359
6581
  addItem(extras, entry);
@@ -6378,9 +6600,9 @@ init_config();
6378
6600
  init_commands_pull_wedge();
6379
6601
  init_utils();
6380
6602
  init_utils_fs();
6381
- import { execFileSync as execFileSync17 } from "node:child_process";
6603
+ import { execFileSync as execFileSync19 } from "node:child_process";
6382
6604
  function gitCapture(args, cwd) {
6383
- return execFileSync17("git", args, {
6605
+ return execFileSync19("git", args, {
6384
6606
  cwd,
6385
6607
  stdio: ["ignore", "pipe", "pipe"],
6386
6608
  maxBuffer: 64 * 1024 * 1024
@@ -6571,7 +6793,7 @@ function runPullCore(opts = {}) {
6571
6793
  const ts = freshBackupTs(backup);
6572
6794
  handleWedge(repo, forceRemote);
6573
6795
  if (!dryRun) {
6574
- const backupRoot = join46(backup, ts);
6796
+ const backupRoot = join48(backup, ts);
6575
6797
  try {
6576
6798
  mkdirSync10(backupRoot, { recursive: true });
6577
6799
  } catch (err) {
@@ -6584,8 +6806,8 @@ function runPullCore(opts = {}) {
6584
6806
  const prePostHeads = capturePrePostHeads(repo, () => {
6585
6807
  gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
6586
6808
  });
6587
- const mapPath = join46(repo, "path-map.json");
6588
- const map = existsSync39(mapPath) ? readPathMap(mapPath) : { projects: {} };
6809
+ const mapPath = join48(repo, "path-map.json");
6810
+ const map = existsSync41(mapPath) ? readPathMap(mapPath) : { projects: {} };
6589
6811
  const divergedKeptLocal = divergenceCheckExtras(ts, dryRun ? prePostHeads : void 0);
6590
6812
  if (dryRun) {
6591
6813
  computePreview(ts, map, "pull");
@@ -6597,8 +6819,8 @@ function runPullCore(opts = {}) {
6597
6819
  }
6598
6820
  function cmdPull(opts = {}) {
6599
6821
  const repo = repoHome();
6600
- if (!existsSync39(repo)) die(`repo not cloned at ${repo}`);
6601
- if (!existsSync39(join46(repo, "shared", "settings.base.json"))) {
6822
+ if (!existsSync41(repo)) die(`repo not cloned at ${repo}`);
6823
+ if (!existsSync41(join48(repo, "shared", "settings.base.json"))) {
6602
6824
  die("repo not initialized; run 'nomad init' to scaffold");
6603
6825
  }
6604
6826
  const handle = acquireLock("pull");
@@ -6620,13 +6842,13 @@ function cmdPull(opts = {}) {
6620
6842
 
6621
6843
  // src/commands.push.ts
6622
6844
  init_config();
6623
- import { existsSync as existsSync43 } from "node:fs";
6624
- import { join as join51 } from "node:path";
6845
+ import { existsSync as existsSync45 } from "node:fs";
6846
+ import { join as join53 } from "node:path";
6625
6847
 
6626
6848
  // src/commands.push.selection.ts
6627
6849
  init_config();
6628
- import { existsSync as existsSync40, statSync as statSync11 } from "node:fs";
6629
- import { join as join47 } from "node:path";
6850
+ import { existsSync as existsSync42, statSync as statSync11 } from "node:fs";
6851
+ import { join as join49 } from "node:path";
6630
6852
  init_utils_json();
6631
6853
  function buildCurrentMap(map) {
6632
6854
  const current = {};
@@ -6635,8 +6857,8 @@ function buildCurrentMap(map) {
6635
6857
  for (const [, hostMap] of Object.entries(map.projects)) {
6636
6858
  const localPath = hostMap[HOST];
6637
6859
  if (!localPath) continue;
6638
- const localDir = join47(claude, "projects", encodePath(localPath));
6639
- if (!existsSync40(localDir)) continue;
6860
+ const localDir = join49(claude, "projects", encodePath(localPath));
6861
+ if (!existsSync42(localDir)) continue;
6640
6862
  for (const f of enumerateSourceFiles(localDir)) {
6641
6863
  const st = statSync11(f);
6642
6864
  current[f] = { size: st.size, mtime: st.mtimeMs };
@@ -6667,7 +6889,7 @@ function computePushSelection(map, old, scannerVersion, configHash, fullScan) {
6667
6889
  };
6668
6890
  }
6669
6891
  function loadSelectionForPush(mapPath, old, scannerVersion, configHash, fullScan) {
6670
- const map = existsSync40(mapPath) ? readPathMap(mapPath) : null;
6892
+ const map = existsSync42(mapPath) ? readPathMap(mapPath) : null;
6671
6893
  const { selection, newManifest } = computePushSelection(
6672
6894
  map,
6673
6895
  old,
@@ -6769,14 +6991,14 @@ function enforceAllowList(statusPorcelain, map) {
6769
6991
 
6770
6992
  // src/commands.push.settings.ts
6771
6993
  init_config();
6772
- import { existsSync as existsSync41 } from "node:fs";
6773
- import { join as join48 } from "node:path";
6994
+ import { existsSync as existsSync43 } from "node:fs";
6995
+ import { join as join50 } from "node:path";
6774
6996
  init_utils();
6775
6997
  init_utils_fs();
6776
6998
  init_utils_json();
6777
6999
  function stripGsdHooksFromBase(repo, backup) {
6778
- const basePath = join48(repo, "shared", "settings.base.json");
6779
- if (!existsSync41(basePath)) return;
7000
+ const basePath = join50(repo, "shared", "settings.base.json");
7001
+ if (!existsSync43(basePath)) return;
6780
7002
  let base;
6781
7003
  try {
6782
7004
  base = readJson(basePath);
@@ -6790,14 +7012,14 @@ function stripGsdHooksFromBase(repo, backup) {
6790
7012
  writeJsonAtomic(basePath, stripped);
6791
7013
  }
6792
7014
  function reportSettingsAheadDrift(repo) {
6793
- const basePath = join48(repo, "shared", "settings.base.json");
6794
- if (!existsSync41(basePath)) return;
6795
- const settingsPath = join48(claudeHome(), "settings.json");
6796
- if (!existsSync41(settingsPath)) return;
7015
+ const basePath = join50(repo, "shared", "settings.base.json");
7016
+ if (!existsSync43(basePath)) return;
7017
+ const settingsPath = join50(claudeHome(), "settings.json");
7018
+ if (!existsSync43(settingsPath)) return;
6797
7019
  try {
6798
7020
  const base = readJson(basePath);
6799
- const hostPath = join48(repo, "hosts", `${HOST}.json`);
6800
- const overrides = existsSync41(hostPath) ? readJson(hostPath) : {};
7021
+ const hostPath = join50(repo, "hosts", `${HOST}.json`);
7022
+ const overrides = existsSync43(hostPath) ? readJson(hostPath) : {};
6801
7023
  const merged = deepMerge(base, overrides);
6802
7024
  const settings = readJson(settingsPath);
6803
7025
  const { ahead } = classifySettingsDrift(merged, settings);
@@ -6814,12 +7036,12 @@ function reportSettingsAheadDrift(repo) {
6814
7036
  // src/commands.push.guards.ts
6815
7037
  init_push_checks();
6816
7038
  init_utils();
6817
- import { join as join49, relative as relative7 } from "node:path";
7039
+ import { join as join51, relative as relative7 } from "node:path";
6818
7040
  function guardGitlinks(repo) {
6819
- const gitlinks = findGitlinks(join49(repo, "shared"));
7041
+ const gitlinks = findGitlinks(join51(repo, "shared"));
6820
7042
  if (gitlinks.length === 0) return;
6821
7043
  for (const p of gitlinks) {
6822
- const rel = relative7(repo, p);
7044
+ const rel = relative7(repo, p).replaceAll("\\", "/");
6823
7045
  fail(`gitlink: ${rel} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`);
6824
7046
  }
6825
7047
  const noun = gitlinks.length === 1 ? "entry" : "entries";
@@ -6848,7 +7070,7 @@ init_config();
6848
7070
 
6849
7071
  // src/push-global-config.ts
6850
7072
  init_config();
6851
- import { execFileSync as execFileSync18 } from "node:child_process";
7073
+ import { execFileSync as execFileSync20 } from "node:child_process";
6852
7074
  var STATUS_LABELS = {
6853
7075
  A: "add",
6854
7076
  M: "modify",
@@ -6888,7 +7110,7 @@ function isInScope(filePath, exactPrefixes, dirPrefixes) {
6888
7110
  }
6889
7111
  function collectGlobalConfigChanges(repoHome2, hostname2, opts) {
6890
7112
  const args = opts.staged ? ["diff", "--cached", "--name-status", "-z"] : ["diff", "HEAD", "--name-status", "-z"];
6891
- const raw = execFileSync18("git", args, {
7113
+ const raw = execFileSync20("git", args, {
6892
7114
  cwd: repoHome2,
6893
7115
  stdio: ["ignore", "pipe", "pipe"]
6894
7116
  }).toString();
@@ -6926,9 +7148,9 @@ init_color();
6926
7148
  init_config();
6927
7149
  init_config_sharedDirs_guard();
6928
7150
  import { randomBytes as randomBytes2 } from "node:crypto";
6929
- import { copyFileSync, existsSync as existsSync42, mkdirSync as mkdirSync11, readdirSync as readdirSync16, rmSync as rmSync14 } from "node:fs";
7151
+ import { copyFileSync, existsSync as existsSync44, mkdirSync as mkdirSync11, readdirSync as readdirSync17, rmSync as rmSync15 } from "node:fs";
6930
7152
  import { homedir as homedir5 } from "node:os";
6931
- import { join as join50, relative as relative8, sep as sep8 } from "node:path";
7153
+ import { join as join52, relative as relative8, sep as sep9 } from "node:path";
6932
7154
  init_push_leak_verdict();
6933
7155
  init_push_gitleaks();
6934
7156
  init_utils_fs();
@@ -6936,11 +7158,11 @@ init_utils_json();
6936
7158
  var NOTHING_TO_SCAN_ROW = `${dim(infoGlyph)} nothing to scan, no leaks`;
6937
7159
  function stageSessionDir(localDir, dstDir, changed) {
6938
7160
  if (changed !== void 0) {
6939
- const prefix = `${localDir}${sep8}`;
7161
+ const prefix = `${localDir}${sep9}`;
6940
7162
  const matching = [...changed].filter((p) => p.startsWith(prefix));
6941
7163
  if (matching.length === 0) return false;
6942
7164
  for (const src of matching) {
6943
- copyFileAtomic(src, join50(dstDir, relative8(localDir, src)));
7165
+ copyFileAtomic(src, join52(dstDir, relative8(localDir, src)));
6944
7166
  }
6945
7167
  return true;
6946
7168
  }
@@ -6956,14 +7178,14 @@ function stageSessions(tmpRoot, map, changed) {
6956
7178
  if (!p || p === "TBD") continue;
6957
7179
  reverse.set(encodePath(p), logical);
6958
7180
  }
6959
- const localProjects = join50(claudeHome(), "projects");
6960
- if (!existsSync42(localProjects)) return 0;
7181
+ const localProjects = join52(claudeHome(), "projects");
7182
+ if (!existsSync44(localProjects)) return 0;
6961
7183
  let staged = 0;
6962
- for (const dir of readdirSync16(localProjects)) {
7184
+ for (const dir of readdirSync17(localProjects)) {
6963
7185
  const logical = reverse.get(dir);
6964
7186
  if (!logical) continue;
6965
- const localDir = join50(localProjects, dir);
6966
- const dstDir = join50(tmpRoot, "shared", "projects", logical);
7187
+ const localDir = join52(localProjects, dir);
7188
+ const dstDir = join52(tmpRoot, "shared", "projects", logical);
6967
7189
  if (stageSessionDir(localDir, dstDir, changed)) staged++;
6968
7190
  }
6969
7191
  return staged;
@@ -6979,9 +7201,9 @@ function stageExtras(tmpRoot, map) {
6979
7201
  if (!localRoot || localRoot === "TBD") continue;
6980
7202
  for (const dirname10 of dirnames) {
6981
7203
  if (!whitelist.includes(dirname10)) continue;
6982
- const src = join50(localRoot, dirname10);
6983
- if (!existsSync42(src)) continue;
6984
- const dst = join50(tmpRoot, "shared", "extras", logical, dirname10);
7204
+ const src = join52(localRoot, dirname10);
7205
+ if (!existsSync44(src)) continue;
7206
+ const dst = join52(tmpRoot, "shared", "extras", logical, dirname10);
6985
7207
  copyExtras(src, dst);
6986
7208
  staged++;
6987
7209
  }
@@ -6989,19 +7211,19 @@ function stageExtras(tmpRoot, map) {
6989
7211
  return staged;
6990
7212
  }
6991
7213
  function previewPushLeaks(map, opts = {}) {
6992
- const cacheDir = join50(homedir5(), ".cache", "claude-nomad");
7214
+ const cacheDir = join52(homedir5(), ".cache", "claude-nomad");
6993
7215
  mkdirSync11(cacheDir, { recursive: true });
6994
7216
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
6995
- const tmpRoot = join50(cacheDir, `push-preview-tree-${stamp}`);
7217
+ const tmpRoot = join52(cacheDir, `push-preview-tree-${stamp}`);
6996
7218
  try {
6997
7219
  const sessionCount = stageSessions(tmpRoot, map, opts.selection?.changed);
6998
7220
  const extrasCount = stageExtras(tmpRoot, map);
6999
7221
  if (sessionCount + extrasCount === 0) {
7000
7222
  return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
7001
7223
  }
7002
- const ignoreFile = join50(repoHome(), ".gitleaksignore");
7003
- if (existsSync42(ignoreFile)) {
7004
- copyFileSync(ignoreFile, join50(tmpRoot, ".gitleaksignore"));
7224
+ const ignoreFile = join52(repoHome(), ".gitleaksignore");
7225
+ if (existsSync44(ignoreFile)) {
7226
+ copyFileSync(ignoreFile, join52(tmpRoot, ".gitleaksignore"));
7005
7227
  }
7006
7228
  let findings;
7007
7229
  try {
@@ -7014,7 +7236,7 @@ function previewPushLeaks(map, opts = {}) {
7014
7236
  }
7015
7237
  return verdictFromFindings(findings);
7016
7238
  } finally {
7017
- rmSync14(tmpRoot, { recursive: true, force: true });
7239
+ rmSync15(tmpRoot, { recursive: true, force: true });
7018
7240
  }
7019
7241
  }
7020
7242
 
@@ -7076,7 +7298,7 @@ async function runPushCore(opts = {}) {
7076
7298
  const scannerVersion = probeGitleaks();
7077
7299
  const configHash = computeConfigHash();
7078
7300
  const old = readManifest(manifestPath());
7079
- const mapPath = join51(repo, "path-map.json");
7301
+ const mapPath = join53(repo, "path-map.json");
7080
7302
  const { map, selection, newManifest } = loadSelectionForPush(
7081
7303
  mapPath,
7082
7304
  old,
@@ -7090,6 +7312,7 @@ async function runPushCore(opts = {}) {
7090
7312
  const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
7091
7313
  if (!dryRun) {
7092
7314
  syncSkillsPush();
7315
+ syncSharedLinksPush(map);
7093
7316
  stripGsdHooksFromBase(repo, backup);
7094
7317
  }
7095
7318
  const st = { dryRun, remap, extras, globalConfig: [] };
@@ -7129,7 +7352,7 @@ async function cmdPush(opts = {}) {
7129
7352
  const allowRule = opts.allowRule;
7130
7353
  guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
7131
7354
  const repo = repoHome();
7132
- if (!existsSync43(repo)) die(`repo not cloned at ${repo}`);
7355
+ if (!existsSync45(repo)) die(`repo not cloned at ${repo}`);
7133
7356
  const handle = acquireLock("push");
7134
7357
  if (handle === null) process.exit(0);
7135
7358
  try {
@@ -7147,8 +7370,8 @@ async function cmdPush(opts = {}) {
7147
7370
  }
7148
7371
 
7149
7372
  // src/commands.sync.ts
7150
- import { existsSync as existsSync44 } from "node:fs";
7151
- import { join as join52 } from "node:path";
7373
+ import { existsSync as existsSync46 } from "node:fs";
7374
+ import { join as join54 } from "node:path";
7152
7375
  init_config();
7153
7376
  init_utils();
7154
7377
  init_utils_fs();
@@ -7216,8 +7439,8 @@ async function runSyncWet() {
7216
7439
  }
7217
7440
  async function runSyncDryRun(repo, backup) {
7218
7441
  const ts = freshBackupTs(backup);
7219
- const mapPath = join52(repo, "path-map.json");
7220
- const map = existsSync44(mapPath) ? readPathMap(mapPath) : { projects: {} };
7442
+ const mapPath = join54(repo, "path-map.json");
7443
+ const map = existsSync46(mapPath) ? readPathMap(mapPath) : { projects: {} };
7221
7444
  computePreview(ts, map, "pull");
7222
7445
  log("push preview below is computed against pre-pull state (a real sync pushes after pull)");
7223
7446
  await runPushCore({ dryRun: true });
@@ -7226,8 +7449,8 @@ async function cmdSync(opts = {}) {
7226
7449
  const dryRun = opts.dryRun === true;
7227
7450
  const repo = repoHome();
7228
7451
  const backup = backupBase();
7229
- if (!existsSync44(repo)) die(`repo not cloned at ${repo}`);
7230
- if (!existsSync44(join52(repo, "shared", "settings.base.json"))) {
7452
+ if (!existsSync46(repo)) die(`repo not cloned at ${repo}`);
7453
+ if (!existsSync46(join54(repo, "shared", "settings.base.json"))) {
7231
7454
  die("repo not initialized; run 'nomad init' to scaffold");
7232
7455
  }
7233
7456
  const handle = acquireLock("sync");
@@ -7251,25 +7474,31 @@ async function cmdSync(opts = {}) {
7251
7474
  }
7252
7475
 
7253
7476
  // src/commands.update.ts
7254
- import { execFileSync as execFileSync19 } from "node:child_process";
7477
+ import { execFileSync as execFileSync21 } from "node:child_process";
7255
7478
  init_utils();
7256
- function readInstalledVersion(run = execFileSync19) {
7479
+ function readInstalledVersion(run = execFileSync21) {
7480
+ const isWin = process.platform === "win32";
7257
7481
  try {
7258
- return run("nomad", ["--version"], { encoding: "utf8" }).toString().trim() || null;
7482
+ return run(isWin ? "nomad.cmd" : "nomad", ["--version"], {
7483
+ encoding: "utf8",
7484
+ ...isWin ? { shell: true } : {}
7485
+ }).toString().trim() || null;
7259
7486
  } catch {
7260
7487
  return null;
7261
7488
  }
7262
7489
  }
7263
- function cmdUpdate(currentVersion, run = execFileSync19) {
7490
+ function cmdUpdate(currentVersion, run = execFileSync21) {
7264
7491
  console.log(`Updating claude-nomad v${currentVersion}...`);
7492
+ const isWin = process.platform === "win32";
7265
7493
  try {
7266
- run("npm", ["update", "-g", "claude-nomad"], {
7494
+ run(isWin ? "npm.cmd" : "npm", ["update", "-g", "claude-nomad"], {
7267
7495
  encoding: "utf8",
7268
7496
  stdio: ["ignore", "pipe", "pipe"],
7269
7497
  // Now that output is captured rather than inherited, execFileSync buffers
7270
7498
  // it; lift the default 1 MiB ceiling so a noisy-but-successful npm run
7271
7499
  // (deprecation/funding spam) cannot throw ENOBUFS.
7272
- maxBuffer: 64 * 1024 * 1024
7500
+ maxBuffer: 64 * 1024 * 1024,
7501
+ ...isWin ? { shell: true } : {}
7273
7502
  });
7274
7503
  } catch (err) {
7275
7504
  const e = err;
@@ -7294,18 +7523,18 @@ init_config();
7294
7523
 
7295
7524
  // src/diff.ts
7296
7525
  init_config();
7297
- import { existsSync as existsSync45 } from "node:fs";
7298
- import { join as join53 } from "node:path";
7526
+ import { existsSync as existsSync47 } from "node:fs";
7527
+ import { join as join55 } from "node:path";
7299
7528
  init_utils();
7300
7529
  init_utils_fs();
7301
7530
  init_utils_json();
7302
7531
  function cmdDiff() {
7303
7532
  try {
7304
7533
  const repo = repoHome();
7305
- if (!existsSync45(repo)) die(`repo not cloned at ${repo}`);
7534
+ if (!existsSync47(repo)) die(`repo not cloned at ${repo}`);
7306
7535
  const ts = freshBackupTs(backupBase());
7307
- const mapPath = join53(repo, "path-map.json");
7308
- const map = existsSync45(mapPath) ? readPathMap(mapPath) : { projects: {} };
7536
+ const mapPath = join55(repo, "path-map.json");
7537
+ const map = existsSync47(mapPath) ? readPathMap(mapPath) : { projects: {} };
7309
7538
  divergenceCheckExtras(ts);
7310
7539
  computePreview(ts, map, "diff");
7311
7540
  } catch (err) {
@@ -7320,19 +7549,19 @@ function cmdDiff() {
7320
7549
 
7321
7550
  // src/init.ts
7322
7551
  init_config();
7323
- import { existsSync as existsSync47, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "node:fs";
7324
- import { join as join55 } from "node:path";
7552
+ import { existsSync as existsSync49, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "node:fs";
7553
+ import { join as join57 } from "node:path";
7325
7554
 
7326
7555
  // src/init.gh-onboard.ts
7327
7556
  init_config();
7328
- import { execFileSync as execFileSync20 } from "node:child_process";
7557
+ import { execFileSync as execFileSync22 } from "node:child_process";
7329
7558
  init_utils();
7330
7559
  var DEFAULT_REPO_NAME = "claude-nomad-config";
7331
7560
  function isValidRepoName(name) {
7332
7561
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
7333
7562
  }
7334
7563
  var GH_NETWORK_TIMEOUT_MS = 3e4;
7335
- function ensureOriginRepo(repoName, run = execFileSync20) {
7564
+ function ensureOriginRepo(repoName, run = execFileSync22) {
7336
7565
  if (!isValidRepoName(repoName)) {
7337
7566
  die(
7338
7567
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -7403,33 +7632,33 @@ init_config();
7403
7632
  init_utils();
7404
7633
  init_utils_fs();
7405
7634
  init_utils_json();
7406
- import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync46, rmSync as rmSync15, statSync as statSync12 } from "node:fs";
7407
- import { join as join54 } from "node:path";
7635
+ import { copyFileSync as copyFileSync2, cpSync as cpSync8, existsSync as existsSync48, rmSync as rmSync16, statSync as statSync12 } from "node:fs";
7636
+ import { join as join56 } from "node:path";
7408
7637
  function snapshotIntoShared(map) {
7409
7638
  const repo = repoHome();
7410
7639
  const claude = claudeHome();
7411
7640
  for (const name of allSharedLinks(map)) {
7412
- const src = join54(claude, name);
7413
- if (!existsSync46(src)) continue;
7414
- const dst = join54(repo, "shared", name);
7641
+ const src = join56(claude, name);
7642
+ if (!existsSync48(src)) continue;
7643
+ const dst = join56(repo, "shared", name);
7415
7644
  if (statSync12(src).isDirectory()) {
7416
- const gk = join54(dst, ".gitkeep");
7417
- if (existsSync46(gk)) rmSync15(gk);
7418
- cpSync7(src, dst, { recursive: true, force: false, errorOnExist: true });
7645
+ const gk = join56(dst, ".gitkeep");
7646
+ if (existsSync48(gk)) rmSync16(gk);
7647
+ cpSync8(src, dst, { recursive: true, force: false, errorOnExist: true });
7419
7648
  } else {
7420
7649
  copyFileSync2(src, dst);
7421
7650
  }
7422
7651
  log(`snapshotted shared/${name} from ${src}`);
7423
7652
  }
7424
- const userSettings = join54(claude, "settings.json");
7425
- if (existsSync46(userSettings)) {
7653
+ const userSettings = join56(claude, "settings.json");
7654
+ if (existsSync48(userSettings)) {
7426
7655
  let parsed;
7427
7656
  try {
7428
7657
  parsed = readJson(userSettings);
7429
7658
  } catch (err) {
7430
7659
  return die(`malformed ${userSettings}: ${err.message}`);
7431
7660
  }
7432
- const hostFile = join54(repo, "hosts", `${HOST}.json`);
7661
+ const hostFile = join56(repo, "hosts", `${HOST}.json`);
7433
7662
  writeJsonAtomic(hostFile, parsed);
7434
7663
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
7435
7664
  }
@@ -7439,17 +7668,18 @@ function snapshotIntoShared(map) {
7439
7668
  init_utils();
7440
7669
  init_utils_fs();
7441
7670
  var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.claude/CLAUDE.md by nomad pull -->\n";
7671
+ var GITATTRIBUTES = "# nomad: sync content is byte-managed, disable all line-ending conversion\n* -text\n";
7442
7672
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
7443
7673
  function preflightConflict(repoHome2) {
7444
7674
  const candidates = [
7445
- join55(repoHome2, "shared", "settings.base.json"),
7446
- join55(repoHome2, "shared", "CLAUDE.md"),
7447
- join55(repoHome2, "path-map.json"),
7448
- join55(repoHome2, "hosts"),
7449
- join55(repoHome2, "shared")
7675
+ join57(repoHome2, "shared", "settings.base.json"),
7676
+ join57(repoHome2, "shared", "CLAUDE.md"),
7677
+ join57(repoHome2, "path-map.json"),
7678
+ join57(repoHome2, "hosts"),
7679
+ join57(repoHome2, "shared")
7450
7680
  ];
7451
7681
  for (const c of candidates) {
7452
- if (existsSync47(c)) return c;
7682
+ if (existsSync49(c)) return c;
7453
7683
  }
7454
7684
  return null;
7455
7685
  }
@@ -7467,26 +7697,28 @@ function cmdInit(opts = {}) {
7467
7697
  die(`already initialized; refusing to clobber ${conflict}`);
7468
7698
  }
7469
7699
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
7470
- mkdirSync12(join55(repo, "shared"), { recursive: true });
7471
- mkdirSync12(join55(repo, "hosts"), { recursive: true });
7700
+ mkdirSync12(join57(repo, "shared"), { recursive: true });
7701
+ mkdirSync12(join57(repo, "hosts"), { recursive: true });
7472
7702
  for (const name of SHARED_KEEP_DIRS) {
7473
- mkdirSync12(join55(repo, "shared", name), { recursive: true });
7703
+ mkdirSync12(join57(repo, "shared", name), { recursive: true });
7474
7704
  }
7475
- const userClaudeMd = join55(claude, "CLAUDE.md");
7476
- if (!snapshot || !existsSync47(userClaudeMd)) {
7477
- writeFileSync6(join55(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
7705
+ const userClaudeMd = join57(claude, "CLAUDE.md");
7706
+ if (!snapshot || !existsSync49(userClaudeMd)) {
7707
+ writeFileSync6(join57(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
7478
7708
  item("created shared/CLAUDE.md");
7479
7709
  }
7480
7710
  for (const name of SHARED_KEEP_DIRS) {
7481
- writeFileSync6(join55(repo, "shared", name, ".gitkeep"), "");
7711
+ writeFileSync6(join57(repo, "shared", name, ".gitkeep"), "");
7482
7712
  item(`created shared/${name}/.gitkeep`);
7483
7713
  }
7484
- writeFileSync6(join55(repo, "hosts", ".gitkeep"), "");
7714
+ writeFileSync6(join57(repo, "hosts", ".gitkeep"), "");
7485
7715
  item("created hosts/.gitkeep");
7486
- writeJsonAtomic(join55(repo, "shared", "settings.base.json"), {});
7716
+ writeJsonAtomic(join57(repo, "shared", "settings.base.json"), {});
7487
7717
  item("created shared/settings.base.json");
7488
- writeJsonAtomic(join55(repo, "path-map.json"), { projects: {} });
7718
+ writeJsonAtomic(join57(repo, "path-map.json"), { projects: {} });
7489
7719
  item("created path-map.json");
7720
+ writeFileSync6(join57(repo, ".gitattributes"), GITATTRIBUTES);
7721
+ item("created .gitattributes");
7490
7722
  if (snapshot) {
7491
7723
  snapshotIntoShared({ projects: {} });
7492
7724
  log(`snapshot staged in shared/; review, then 'nomad push' to share with other hosts.`);
@@ -7551,21 +7783,21 @@ function maybeDisableRepoActions(repoHome2, run) {
7551
7783
  // src/init.prompt.ts
7552
7784
  init_config();
7553
7785
  init_utils();
7554
- import { existsSync as existsSync48, readdirSync as readdirSync17, statSync as statSync13 } from "node:fs";
7555
- import { join as join56 } from "node:path";
7786
+ import { existsSync as existsSync50, readdirSync as readdirSync18, statSync as statSync13 } from "node:fs";
7787
+ import { join as join58 } from "node:path";
7556
7788
  import { createInterface as createInterface3 } from "node:readline/promises";
7557
7789
  function nonEmptyExists(path) {
7558
- if (!existsSync48(path)) return false;
7790
+ if (!existsSync50(path)) return false;
7559
7791
  try {
7560
- if (statSync13(path).isDirectory()) return readdirSync17(path).length > 0;
7792
+ if (statSync13(path).isDirectory()) return readdirSync18(path).length > 0;
7561
7793
  return true;
7562
7794
  } catch {
7563
7795
  return false;
7564
7796
  }
7565
7797
  }
7566
7798
  function hasExistingClaudeConfig(claudeHome2) {
7567
- if (existsSync48(join56(claudeHome2, "settings.json"))) return true;
7568
- return SHARED_LINKS.some((name) => nonEmptyExists(join56(claudeHome2, name)));
7799
+ if (existsSync50(join58(claudeHome2, "settings.json"))) return true;
7800
+ return SHARED_LINKS.some((name) => nonEmptyExists(join58(claudeHome2, name)));
7569
7801
  }
7570
7802
  async function confirmSnapshotDefault(claudeHome2) {
7571
7803
  if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
@@ -7869,7 +8101,7 @@ function parseSyncArgs(argv) {
7869
8101
  // package.json
7870
8102
  var package_default = {
7871
8103
  name: "claude-nomad",
7872
- version: "0.58.1",
8104
+ version: "0.59.0",
7873
8105
  type: "module",
7874
8106
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
7875
8107
  keywords: [
@@ -8105,15 +8337,15 @@ var DEFAULT_HELP = [
8105
8337
  init_config();
8106
8338
  init_utils();
8107
8339
  init_utils_json();
8108
- import { existsSync as existsSync49, readFileSync as readFileSync17, readdirSync as readdirSync18 } from "node:fs";
8109
- import { join as join57 } from "node:path";
8340
+ import { existsSync as existsSync51, readFileSync as readFileSync18, readdirSync as readdirSync19 } from "node:fs";
8341
+ import { join as join59 } from "node:path";
8110
8342
  function resumeCmd(sessionId) {
8111
8343
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
8112
8344
  fail(`invalid session id: ${sessionId}`);
8113
8345
  process.exit(1);
8114
8346
  }
8115
- const projectsRoot = join57(claudeHome(), "projects");
8116
- if (!existsSync49(projectsRoot)) {
8347
+ const projectsRoot = join59(claudeHome(), "projects");
8348
+ if (!existsSync51(projectsRoot)) {
8117
8349
  fail(`${projectsRoot} does not exist`);
8118
8350
  process.exit(1);
8119
8351
  }
@@ -8127,8 +8359,8 @@ function resumeCmd(sessionId) {
8127
8359
  fail(`no cwd field found in ${jsonlPath}`);
8128
8360
  process.exit(1);
8129
8361
  }
8130
- const mapPath = join57(repoHome(), "path-map.json");
8131
- if (!existsSync49(mapPath)) {
8362
+ const mapPath = join59(repoHome(), "path-map.json");
8363
+ if (!existsSync51(mapPath)) {
8132
8364
  fail("path-map.json missing");
8133
8365
  process.exit(1);
8134
8366
  }
@@ -8150,14 +8382,14 @@ function resumeCmd(sessionId) {
8150
8382
  console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
8151
8383
  }
8152
8384
  function findTranscriptPath(projectsRoot, sessionId) {
8153
- for (const dir of readdirSync18(projectsRoot)) {
8154
- const candidate = join57(projectsRoot, dir, `${sessionId}.jsonl`);
8155
- if (existsSync49(candidate)) return candidate;
8385
+ for (const dir of readdirSync19(projectsRoot)) {
8386
+ const candidate = join59(projectsRoot, dir, `${sessionId}.jsonl`);
8387
+ if (existsSync51(candidate)) return candidate;
8156
8388
  }
8157
8389
  return null;
8158
8390
  }
8159
8391
  function extractRecordedCwd(jsonlPath) {
8160
- for (const line of readFileSync17(jsonlPath, "utf8").split("\n")) {
8392
+ for (const line of readFileSync18(jsonlPath, "utf8").split("\n")) {
8161
8393
  if (!line.trim()) continue;
8162
8394
  try {
8163
8395
  const obj = JSON.parse(line);