claude-nomad 0.58.1 → 0.60.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();
@@ -1493,6 +1353,75 @@ function stripGsdHookEntries(settings) {
1493
1353
  }
1494
1354
  return out;
1495
1355
  }
1356
+ function keepMatcherEntry(entry) {
1357
+ if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null;
1358
+ const entryObj = entry;
1359
+ if (!Array.isArray(entryObj.hooks)) return null;
1360
+ const innerHooks = entryObj.hooks;
1361
+ const kept = innerHooks.filter((h2) => {
1362
+ if (h2 === null || typeof h2 !== "object" || Array.isArray(h2)) return false;
1363
+ const hookObj = h2;
1364
+ const cmd = hookObj.command;
1365
+ return isGsdHookEntry(typeof cmd === "string" ? cmd : "");
1366
+ });
1367
+ if (kept.length === 0) return null;
1368
+ return { ...entryObj, hooks: kept };
1369
+ }
1370
+ function keepEventMatchers(matchers) {
1371
+ if (!Array.isArray(matchers)) return null;
1372
+ const kept = [];
1373
+ for (const entry of matchers) {
1374
+ const result = keepMatcherEntry(entry);
1375
+ if (result !== null) kept.push(result);
1376
+ }
1377
+ return kept.length === 0 ? null : kept;
1378
+ }
1379
+ function keepGsdHookEntries(settings) {
1380
+ const hooksVal = settings.hooks;
1381
+ if (hooksVal === null || typeof hooksVal !== "object" || Array.isArray(hooksVal)) return {};
1382
+ const hooksObj = hooksVal;
1383
+ const keptHooks = {};
1384
+ for (const [event, matchers] of Object.entries(hooksObj)) {
1385
+ const kept = keepEventMatchers(matchers);
1386
+ if (kept !== null) keptHooks[event] = kept;
1387
+ }
1388
+ return Object.keys(keptHooks).length === 0 ? {} : { hooks: keptHooks };
1389
+ }
1390
+ function asPlainObject(value) {
1391
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
1392
+ return value;
1393
+ }
1394
+ function canonicalMatcherKey(m) {
1395
+ const obj = asPlainObject(m);
1396
+ if (obj === null) return JSON.stringify(m);
1397
+ return JSON.stringify(
1398
+ Object.keys(obj).sort((a, b) => a.localeCompare(b)).map((k) => [k, obj[k]])
1399
+ );
1400
+ }
1401
+ function unionMatcherArrays(baseMatchers, gsdMatchers) {
1402
+ const seen = new Set(baseMatchers.map(canonicalMatcherKey));
1403
+ const merged = [...baseMatchers];
1404
+ for (const m of gsdMatchers) {
1405
+ const key = canonicalMatcherKey(m);
1406
+ if (!seen.has(key)) {
1407
+ merged.push(m);
1408
+ seen.add(key);
1409
+ }
1410
+ }
1411
+ return merged;
1412
+ }
1413
+ function graftGsdHookEntries(base, gsdOnly) {
1414
+ const gsdHooks = asPlainObject(gsdOnly.hooks);
1415
+ if (gsdHooks === null || Object.keys(gsdHooks).length === 0) return base;
1416
+ const baseHooks = asPlainObject(base.hooks);
1417
+ const mergedHooks = baseHooks ? { ...baseHooks } : {};
1418
+ for (const [event, gsdMatchers] of Object.entries(gsdHooks)) {
1419
+ if (!Array.isArray(gsdMatchers)) continue;
1420
+ const baseMatchers = mergedHooks[event];
1421
+ mergedHooks[event] = Array.isArray(baseMatchers) ? unionMatcherArrays(baseMatchers, gsdMatchers) : gsdMatchers;
1422
+ }
1423
+ return { ...base, hooks: mergedHooks };
1424
+ }
1496
1425
  function matcherHasGsdEntry(entry) {
1497
1426
  if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return false;
1498
1427
  const entryObj = entry;
@@ -1630,50 +1559,294 @@ function buildCaptureSubset(merged, settings, opts) {
1630
1559
  return out;
1631
1560
  }
1632
1561
 
1633
- // src/links.ts
1562
+ // src/extras-sync.core.ts
1634
1563
  init_config();
1635
- import { existsSync as existsSync4, lstatSync as lstatSync3, rmSync as rmSync2 } from "node:fs";
1636
- import { join as join5 } from "node:path";
1564
+ import { cpSync as cpSync2, existsSync as existsSync2, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
1565
+ import { basename as basename2, join as join3, relative } from "node:path";
1566
+
1567
+ // src/extras-sync.collision.ts
1637
1568
  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
- }
1569
+ import { cpSync, existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
1570
+ import { basename, join as join2 } from "node:path";
1571
+ function quoted(p) {
1572
+ return `"${p}"`;
1646
1573
  }
1647
- function emitCreate(onPreview, from, to) {
1648
- if (onPreview) {
1649
- onPreview({ kind: "create", from, to });
1650
- } else {
1651
- log(`would create symlink: ${from} -> ${to}`);
1574
+ function stripCollidingDstSymlinks(src, dst, isExcluded) {
1575
+ if (!existsSync(dst)) return;
1576
+ for (const name of readdirSync(src)) {
1577
+ if (isExcluded(name)) continue;
1578
+ const dstPath = join2(dst, name);
1579
+ const dstStat = lstatSync(dstPath, { throwIfNoEntry: false });
1580
+ if (dstStat === void 0) continue;
1581
+ if (dstStat.isSymbolicLink()) {
1582
+ rmSync(dstPath, { recursive: true, force: true });
1583
+ } else if (dstStat.isDirectory() && lstatSync(join2(src, name)).isDirectory()) {
1584
+ stripCollidingDstSymlinks(join2(src, name), dstPath, isExcluded);
1585
+ }
1652
1586
  }
1653
1587
  }
1654
- function isAlreadySymlink(linkPath) {
1655
- return existsSync4(linkPath) && lstatSync3(linkPath).isSymbolicLink();
1656
- }
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);
1666
- continue;
1588
+ function cpSyncGuarded(src, dst, filter, label) {
1589
+ try {
1590
+ cpSync(src, dst, { recursive: true, force: true, verbatimSymlinks: true, filter });
1591
+ } catch (err) {
1592
+ const e = err;
1593
+ 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) {
1594
+ throw new NomadFatal(
1595
+ `${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`
1596
+ );
1667
1597
  }
1668
- backupBeforeWrite(linkPath, ts);
1669
- rmSync2(linkPath, { recursive: true, force: true });
1598
+ throw err;
1670
1599
  }
1671
1600
  }
1672
- function applySharedLinks(ts, map, opts = {}) {
1673
- const dryRun = opts.dryRun === true;
1674
- const claude = claudeHome();
1675
- const repo = repoHome();
1601
+ function prunePreservingBy(src, dst, isPreserved) {
1602
+ for (const name of readdirSync(dst)) {
1603
+ if (isPreserved(name)) continue;
1604
+ const dstPath = join2(dst, name);
1605
+ const srcStat = lstatSync(join2(src, name), { throwIfNoEntry: false });
1606
+ if (srcStat === void 0) {
1607
+ rmSync(dstPath, { recursive: true, force: true });
1608
+ continue;
1609
+ }
1610
+ const dstStat = lstatSync(dstPath);
1611
+ if (srcStat.isDirectory() && dstStat.isDirectory()) {
1612
+ prunePreservingBy(join2(src, name), dstPath, isPreserved);
1613
+ } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
1614
+ rmSync(dstPath, { recursive: true, force: true });
1615
+ }
1616
+ }
1617
+ }
1618
+ function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
1619
+ const dstStat = lstatSync(dst, { throwIfNoEntry: false });
1620
+ if (dstStat !== void 0) {
1621
+ if (dstStat.isDirectory()) {
1622
+ const srcStat = lstatSync(src, { throwIfNoEntry: false });
1623
+ if (srcStat !== void 0 && !srcStat.isDirectory()) {
1624
+ throw new NomadFatal(
1625
+ `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`
1626
+ );
1627
+ }
1628
+ prunePreservingBy(src, dst, isPreserved);
1629
+ } else {
1630
+ rmSync(dst, { recursive: true, force: true });
1631
+ }
1632
+ }
1633
+ stripCollidingDstSymlinks(src, dst, isPreserved);
1634
+ cpSync(src, dst, {
1635
+ recursive: true,
1636
+ force: true,
1637
+ verbatimSymlinks: true,
1638
+ filter: (srcEntry) => srcEntry === src || !isPreserved(basename(srcEntry))
1639
+ });
1640
+ }
1641
+
1642
+ // src/extras-sync.guards.ts
1643
+ init_utils();
1644
+ init_config_sharedDirs_guard();
1645
+ import { isAbsolute, normalize, sep } from "node:path";
1646
+ function assertSafeLocalRoot(localRoot, logical) {
1647
+ if (!isAbsolute(localRoot)) {
1648
+ throw new NomadFatal(
1649
+ `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
1650
+ );
1651
+ }
1652
+ const canonical = process.platform === "win32" ? localRoot.split("/").join(sep) : localRoot;
1653
+ if (canonical !== normalize(canonical)) {
1654
+ throw new NomadFatal(
1655
+ `invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
1656
+ );
1657
+ }
1658
+ }
1659
+
1660
+ // src/extras-sync.core.ts
1661
+ init_utils();
1662
+ init_utils_json();
1663
+ function loadValidatedExtras(opts) {
1664
+ const repo = repoHome();
1665
+ const mapPath = join3(repo, "path-map.json");
1666
+ const repoExtras = join3(repo, "shared", "extras");
1667
+ if (!existsSync2(mapPath) || opts.requireRepoExtras === true && !existsSync2(repoExtras)) {
1668
+ if (opts.missingMsg !== void 0) log(opts.missingMsg);
1669
+ return null;
1670
+ }
1671
+ const map = readPathMap(mapPath);
1672
+ const extrasMap = map.extras ?? {};
1673
+ if (Object.keys(extrasMap).length === 0) return null;
1674
+ for (const logical of Object.keys(extrasMap)) {
1675
+ assertSafeLogical(logical);
1676
+ const localRoot = map.projects[logical]?.[HOST];
1677
+ if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
1678
+ }
1679
+ return { map, extrasMap };
1680
+ }
1681
+ function* eachExtrasTarget(v, counts) {
1682
+ const whitelist = SUPPORTED_EXTRAS;
1683
+ for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
1684
+ const localRoot = v.map.projects[logical]?.[HOST];
1685
+ if (!localRoot || localRoot === "TBD") {
1686
+ counts.unmapped++;
1687
+ continue;
1688
+ }
1689
+ for (const dirname10 of dirnames) {
1690
+ if (!whitelist.includes(dirname10)) {
1691
+ counts.skipped++;
1692
+ continue;
1693
+ }
1694
+ yield { logical, localRoot, dirname: dirname10 };
1695
+ }
1696
+ }
1697
+ }
1698
+ function copyExtrasOverlayFiltered(src, dst, blockSet) {
1699
+ stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
1700
+ cpSyncGuarded(
1701
+ src,
1702
+ dst,
1703
+ (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry)),
1704
+ "copyExtrasOverlayFiltered"
1705
+ );
1706
+ }
1707
+ function copyExtrasOverlaySkipDiverged(src, dst, blockSet, divergedSet) {
1708
+ stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
1709
+ cpSyncGuarded(
1710
+ src,
1711
+ dst,
1712
+ (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry)) && !divergedSet.has(relative(src, srcEntry)),
1713
+ "copyExtrasOverlaySkipDiverged"
1714
+ );
1715
+ }
1716
+ function copyExtras(src, dst) {
1717
+ rmSync2(dst, { recursive: true, force: true });
1718
+ cpSync2(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
1719
+ }
1720
+ function copyExtrasFileSkipDiverged(src, dst) {
1721
+ if (existsSync2(dst)) {
1722
+ let dstBuf;
1723
+ try {
1724
+ dstBuf = readFileSync2(dst);
1725
+ } catch {
1726
+ return;
1727
+ }
1728
+ if (!readFileSync2(src).equals(dstBuf)) return;
1729
+ }
1730
+ copyExtras(src, dst);
1731
+ }
1732
+ function extrasDenySet(dirname10) {
1733
+ return dirname10 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
1734
+ }
1735
+ function copyExtrasFiltered(src, dst, blockSet) {
1736
+ rmSync2(dst, { recursive: true, force: true });
1737
+ cpSync2(src, dst, {
1738
+ recursive: true,
1739
+ force: true,
1740
+ verbatimSymlinks: true,
1741
+ filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry))
1742
+ });
1743
+ }
1744
+ function prunePreservingDenied(src, dst, blockSet) {
1745
+ for (const name of readdirSync2(dst)) {
1746
+ if (isDeniedName(blockSet, name)) continue;
1747
+ const dstPath = join3(dst, name);
1748
+ const srcStat = lstatSync2(join3(src, name), { throwIfNoEntry: false });
1749
+ if (srcStat === void 0) {
1750
+ rmSync2(dstPath, { recursive: true, force: true });
1751
+ continue;
1752
+ }
1753
+ const dstStat = lstatSync2(dstPath);
1754
+ if (srcStat.isDirectory() && dstStat.isDirectory()) {
1755
+ prunePreservingDenied(join3(src, name), dstPath, blockSet);
1756
+ } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
1757
+ rmSync2(dstPath, { recursive: true, force: true });
1758
+ }
1759
+ }
1760
+ }
1761
+ function copyExtrasFilteredPreserving(src, dst, blockSet) {
1762
+ const dstStat = lstatSync2(dst, { throwIfNoEntry: false });
1763
+ if (dstStat !== void 0) {
1764
+ if (dstStat.isDirectory()) prunePreservingDenied(src, dst, blockSet);
1765
+ else rmSync2(dst, { recursive: true, force: true });
1766
+ }
1767
+ stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
1768
+ cpSync2(src, dst, {
1769
+ recursive: true,
1770
+ force: true,
1771
+ verbatimSymlinks: true,
1772
+ filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry))
1773
+ });
1774
+ }
1775
+
1776
+ // src/links.ts
1777
+ init_utils();
1778
+ init_utils_fs();
1779
+ init_utils_json();
1780
+ function emitAutoMove(onPreview, linkPath, ts, name) {
1781
+ if (onPreview) {
1782
+ onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
1783
+ } else {
1784
+ log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
1785
+ }
1786
+ }
1787
+ function emitCreate(onPreview, from, to) {
1788
+ if (onPreview) {
1789
+ onPreview({ kind: "create", from, to });
1790
+ } else {
1791
+ log(`would create symlink: ${from} -> ${to}`);
1792
+ }
1793
+ }
1794
+ function emitCopy(onPreview, from, to) {
1795
+ if (onPreview) {
1796
+ onPreview({ kind: "copy", from, to });
1797
+ } else {
1798
+ log(`would copy: ${from} -> ${to}`);
1799
+ }
1800
+ }
1801
+ function isAlreadySymlink(linkPath) {
1802
+ return existsSync4(linkPath) && lstatSync4(linkPath).isSymbolicLink();
1803
+ }
1804
+ function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
1805
+ for (const name of linkNames) {
1806
+ const linkPath = join5(claude, name);
1807
+ const target = join5(repo, "shared", name);
1808
+ if (!existsSync4(linkPath)) continue;
1809
+ if (lstatSync4(linkPath).isSymbolicLink()) continue;
1810
+ if (!existsSync4(target)) continue;
1811
+ if (dryRun) {
1812
+ emitAutoMove(onPreview, linkPath, ts, name);
1813
+ continue;
1814
+ }
1815
+ backupBeforeWrite(linkPath, ts);
1816
+ rmSync3(linkPath, { recursive: true, force: true });
1817
+ }
1818
+ }
1819
+ function copySharedLinkPull(src, dst) {
1820
+ copyExtrasFilteredPreservingBy(src, dst, (name) => isDeniedName(ALWAYS_NEVER_SYNC, name));
1821
+ }
1822
+ function applySharedLinksWin32(linkNames, claude, repo, ts, dryRun, onPreview) {
1823
+ for (const name of linkNames) {
1824
+ const target = join5(repo, "shared", name);
1825
+ if (!existsSync4(target)) continue;
1826
+ const linkPath = join5(claude, name);
1827
+ if (dryRun) {
1828
+ emitCopy(onPreview, linkPath, target);
1829
+ continue;
1830
+ }
1831
+ const stat = lstatSync4(linkPath, { throwIfNoEntry: false });
1832
+ if (stat !== void 0) {
1833
+ backupBeforeWrite(linkPath, ts);
1834
+ if (stat.isSymbolicLink()) {
1835
+ rmSync3(linkPath, { recursive: true, force: true });
1836
+ }
1837
+ }
1838
+ copySharedLinkPull(target, linkPath);
1839
+ }
1840
+ }
1841
+ function applySharedLinks(ts, map, opts = {}) {
1842
+ const dryRun = opts.dryRun === true;
1843
+ const claude = claudeHome();
1844
+ const repo = repoHome();
1676
1845
  const linkNames = allSharedLinks(map);
1846
+ if (process.platform === "win32") {
1847
+ applySharedLinksWin32(linkNames, claude, repo, ts, dryRun, opts.onPreview);
1848
+ return;
1849
+ }
1677
1850
  runAutoMovePasses(linkNames, claude, repo, ts, dryRun, opts.onPreview);
1678
1851
  for (const name of linkNames) {
1679
1852
  const target = join5(repo, "shared", name);
@@ -1687,63 +1860,286 @@ function applySharedLinks(ts, map, opts = {}) {
1687
1860
  ensureSymlink(linkPath, target);
1688
1861
  }
1689
1862
  }
1690
- function regenerateSettings(ts, opts = {}) {
1691
- const dryRun = opts.dryRun === true;
1692
- const suppressDriftWarn = opts.suppressDriftWarn === true;
1863
+ function syncSharedLinksPush(map) {
1864
+ if (process.platform !== "win32") return;
1865
+ if (map === null) return;
1866
+ const claude = claudeHome();
1867
+ const repo = repoHome();
1868
+ for (const name of allSharedLinks(map)) {
1869
+ const localPath = join5(claude, name);
1870
+ const stat = lstatSync4(localPath, { throwIfNoEntry: false });
1871
+ if (stat === void 0) continue;
1872
+ if (stat.isSymbolicLink()) continue;
1873
+ copyExtrasFiltered(localPath, join5(repo, "shared", name), ALWAYS_NEVER_SYNC);
1874
+ }
1875
+ }
1876
+ function readExistingSettings(settingsPath) {
1877
+ if (!existsSync4(settingsPath)) return { existing: {}, present: false, malformed: false };
1878
+ try {
1879
+ const parsed = readJson(settingsPath);
1880
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
1881
+ return { existing: {}, present: true, malformed: true };
1882
+ }
1883
+ return { existing: parsed, present: true, malformed: false };
1884
+ } catch {
1885
+ return { existing: {}, present: true, malformed: true };
1886
+ }
1887
+ }
1888
+ function emitDriftWarnings(merged, existing) {
1889
+ const drift = classifySettingsDrift(merged, existing);
1890
+ if (drift.behind.length > 0) {
1891
+ const { phrase, pronoun } = describeSettings(drift.behind);
1892
+ warn(
1893
+ `your settings.json is missing ${phrase} that the synced copy has; run 'nomad pull' to restore ${pronoun}.`
1894
+ );
1895
+ }
1896
+ const { promotable } = partitionByCaptureExclusion(drift.ahead);
1897
+ if (promotable.length > 0) {
1898
+ const { phrase, pronoun, verb } = describeSettings(promotable);
1899
+ warn(
1900
+ `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}.`
1901
+ );
1902
+ }
1903
+ }
1904
+ function regenerateSettings(ts, opts = {}) {
1905
+ const dryRun = opts.dryRun === true;
1906
+ const suppressDriftWarn = opts.suppressDriftWarn === true;
1907
+ const repo = repoHome();
1908
+ const claude = claudeHome();
1909
+ const basePath = join5(repo, "shared", "settings.base.json");
1910
+ const hostPath = join5(repo, "hosts", `${HOST}.json`);
1911
+ if (!existsSync4(basePath)) {
1912
+ die("repo not initialized; run 'nomad init' to scaffold");
1913
+ }
1914
+ const base = readJson(basePath);
1915
+ const hasOverrides = existsSync4(hostPath);
1916
+ const overrides = hasOverrides ? readJson(hostPath) : {};
1917
+ const merged = deepMerge(base, overrides);
1918
+ const settingsPath = join5(claude, "settings.json");
1919
+ const { existing, present, malformed } = readExistingSettings(settingsPath);
1920
+ if (!suppressDriftWarn && present) {
1921
+ if (malformed) {
1922
+ warn("existing settings.json is malformed; skipping drift-check and regenerating.");
1923
+ } else {
1924
+ emitDriftWarnings(merged, existing);
1925
+ }
1926
+ }
1927
+ const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
1928
+ if (dryRun) {
1929
+ log(`would write settings.json (base + ${overrideLabel})`);
1930
+ return { label: overrideLabel };
1931
+ }
1932
+ backupBeforeWrite(settingsPath, ts);
1933
+ writeJsonAtomic(
1934
+ settingsPath,
1935
+ graftGsdHookEntries(stripGsdHookEntries(merged), keepGsdHookEntries(existing))
1936
+ );
1937
+ return { label: overrideLabel };
1938
+ }
1939
+
1940
+ // src/commands.adopt.ts
1941
+ init_utils();
1942
+ init_utils_fs();
1943
+ init_utils_json();
1944
+ var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
1945
+ function lexists(p) {
1946
+ try {
1947
+ lstatSync5(p);
1948
+ return true;
1949
+ } catch {
1950
+ return false;
1951
+ }
1952
+ }
1953
+ function readMapIfPresent(repoHome2) {
1954
+ const mapPath = join6(repoHome2, "path-map.json");
1955
+ return existsSync5(mapPath) ? readPathMap(mapPath) : { projects: {} };
1956
+ }
1957
+ function isConfiguredTarget(name, map) {
1958
+ return SHARED_LINKS.includes(name) || (map.sharedDirs?.includes(name) ?? false);
1959
+ }
1960
+ function isValidAdoptName(name) {
1961
+ if (SHARED_LINKS.includes(name)) return true;
1962
+ return isValidSharedDir(name);
1963
+ }
1964
+ function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
1965
+ const ts = freshBackupTs(backup);
1966
+ backupBeforeWrite(linkPath, ts);
1967
+ cpSync4(linkPath, sharedTarget, { recursive: true, force: true, preserveTimestamps: true });
1968
+ rmSync4(linkPath, { recursive: true, force: true });
1969
+ if (process.platform === "win32") {
1970
+ copySharedLinkPull(sharedTarget, linkPath);
1971
+ } else {
1972
+ ensureSymlink(linkPath, sharedTarget);
1973
+ }
1974
+ const rel = join6("shared", name);
1975
+ gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
1976
+ log(`adopted ${name}; ${ADOPT_PUSH_HINT}`);
1977
+ }
1978
+ function cmdAdopt(name, opts = {}) {
1979
+ const dryRun = opts.dryRun === true;
1980
+ if (!isValidAdoptName(name)) {
1981
+ fail(`invalid name: ${JSON.stringify(name)}`);
1982
+ process.exit(1);
1983
+ }
1984
+ const repo = repoHome();
1985
+ const claude = claudeHome();
1986
+ const backup = backupBase();
1987
+ const map = readMapIfPresent(repo);
1988
+ if (!isConfiguredTarget(name, map)) {
1989
+ fail(
1990
+ `${name}: not a configured shared target. Add it to sharedDirs in path-map.json first, then re-run adopt.`
1991
+ );
1992
+ process.exit(1);
1993
+ }
1994
+ const linkPath = join6(claude, name);
1995
+ const sharedTarget = join6(repo, "shared", name);
1996
+ if (!existsSync5(linkPath)) {
1997
+ log(`${name}: nothing to adopt (not present in ~/.claude/)`);
1998
+ return;
1999
+ }
2000
+ if (lstatSync5(linkPath).isSymbolicLink()) {
2001
+ log(`${name}: already adopted (already a symlink)`);
2002
+ return;
2003
+ }
2004
+ if (process.platform === "win32" && lexists(sharedTarget)) {
2005
+ log(`${name}: already adopted (win32 copy-sync); run \`nomad pull\` to refresh the local copy`);
2006
+ return;
2007
+ }
2008
+ if (lexists(sharedTarget)) {
2009
+ fail(`${name}: shared/${name} already exists; would clobber. Remove it first.`);
2010
+ process.exit(1);
2011
+ }
2012
+ if (dryRun) {
2013
+ const ts = freshBackupTs(backup);
2014
+ log(`would backup: ${linkPath} -> backup/${ts}/${name}`);
2015
+ log(`would move: ${linkPath} -> shared/${name}`);
2016
+ log(
2017
+ process.platform === "win32" ? `would copy back: shared/${name} -> ${linkPath} (win32 copy-sync)` : `would relink: ${linkPath} -> shared/${name}`
2018
+ );
2019
+ log(`would stage: shared/${name}`);
2020
+ return;
2021
+ }
2022
+ try {
2023
+ performAdoptMove(name, linkPath, sharedTarget, repo, backup);
2024
+ } catch (err) {
2025
+ if (!(err instanceof NomadFatal)) throw err;
2026
+ fail(err.message);
2027
+ process.exitCode = 1;
2028
+ }
2029
+ }
2030
+
2031
+ // src/commands.allow.ts
2032
+ init_config();
2033
+ import { existsSync as existsSync6 } from "node:fs";
2034
+
2035
+ // src/commands.redact.core.ts
2036
+ import { appendFileSync, readFileSync as readFileSync3 } from "node:fs";
2037
+ import { join as join7 } from "node:path";
2038
+ function collectMatchIntervals(content, findings) {
2039
+ const intervals = [];
2040
+ for (const f of findings) {
2041
+ const match = f.Match;
2042
+ if (match === "") continue;
2043
+ let from = 0;
2044
+ let pos = content.indexOf(match, from);
2045
+ while (pos !== -1) {
2046
+ intervals.push({ start: pos, end: pos + match.length, ruleId: f.RuleID });
2047
+ from = pos + match.length;
2048
+ pos = content.indexOf(match, from);
2049
+ }
2050
+ }
2051
+ return intervals;
2052
+ }
2053
+ function mergeIntervals(intervals) {
2054
+ if (intervals.length === 0) return [];
2055
+ const sorted = [...intervals].sort((a, b) => a.start - b.start || b.end - a.end);
2056
+ let last = { ...sorted[0] };
2057
+ const merged = [last];
2058
+ for (let i = 1; i < sorted.length; i++) {
2059
+ const cur = sorted[i];
2060
+ if (cur.start <= last.end) {
2061
+ if (cur.end > last.end) last.end = cur.end;
2062
+ } else {
2063
+ last = { ...cur };
2064
+ merged.push(last);
2065
+ }
2066
+ }
2067
+ return merged;
2068
+ }
2069
+ function applyRedactions(content, findings) {
2070
+ const raw = collectMatchIntervals(content, findings);
2071
+ if (raw.length === 0) return content;
2072
+ const merged = mergeIntervals(raw);
2073
+ let result = content;
2074
+ for (let i = merged.length - 1; i >= 0; i--) {
2075
+ const { start, end, ruleId } = merged[i];
2076
+ result = result.slice(0, start) + `[REDACTED:${ruleId}]` + result.slice(end);
2077
+ }
2078
+ return result;
2079
+ }
2080
+ function formatFingerprint(fingerprint) {
2081
+ return fingerprint.replace(/[\r\n]/g, "") + "\n";
2082
+ }
2083
+ function isRecentlyModified(mtimeMs, nowMs, thresholdMs = 5 * 60 * 1e3) {
2084
+ return nowMs - mtimeMs < thresholdMs;
2085
+ }
2086
+ function isValidFingerprint(fingerprint) {
2087
+ return fingerprint.trim().length > 0 && fingerprint.length <= 512 && /^[^\r\n]+$/.test(fingerprint);
2088
+ }
2089
+ function isAlreadyPresent(line, lines) {
2090
+ return lines.includes(line);
2091
+ }
2092
+ function appendGitleaksIgnore(fingerprint, repo) {
2093
+ const sanitized = fingerprint.replace(/[\r\n]/g, "").trim();
2094
+ if (sanitized.length === 0) return;
2095
+ const ignPath = join7(repo, ".gitleaksignore");
2096
+ let raw;
2097
+ try {
2098
+ raw = readFileSync3(ignPath, "utf8");
2099
+ } catch {
2100
+ raw = "";
2101
+ }
2102
+ const existing = raw.split("\n").filter((l) => l.length > 0);
2103
+ if (isAlreadyPresent(sanitized, existing)) return;
2104
+ const needsLeadingNewline = raw.length > 0 && !raw.endsWith("\n");
2105
+ const prefix = needsLeadingNewline ? "\n" : "";
2106
+ appendFileSync(ignPath, prefix + formatFingerprint(sanitized), "utf8");
2107
+ }
2108
+
2109
+ // src/commands.allow.ts
2110
+ init_utils();
2111
+ function cmdAllow(fingerprints) {
1693
2112
  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.");
2113
+ if (!existsSync6(repo)) die(`repo not cloned at ${repo}`);
2114
+ for (const fp of fingerprints) {
2115
+ if (!isValidFingerprint(fp)) {
2116
+ const shown = fp.replaceAll("\r", String.raw`\r`).replaceAll("\n", String.raw`\n`);
2117
+ fail(`invalid fingerprint: ${shown}`);
2118
+ process.exit(1);
1724
2119
  }
1725
2120
  }
1726
- const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
1727
- if (dryRun) {
1728
- log(`would write settings.json (base + ${overrideLabel})`);
1729
- return { label: overrideLabel };
2121
+ for (const fp of fingerprints) {
2122
+ appendGitleaksIgnore(fp, repo);
2123
+ item(`allowed: ${fp}`);
1730
2124
  }
1731
- backupBeforeWrite(settingsPath, ts);
1732
- writeJsonAtomic(settingsPath, stripGsdHookEntries(merged));
1733
- return { label: overrideLabel };
2125
+ log(`allowed ${fingerprints.length} fingerprint(s)`);
1734
2126
  }
1735
2127
 
1736
2128
  // src/commands.capture-settings.ts
2129
+ init_config();
2130
+ import { existsSync as existsSync7 } from "node:fs";
2131
+ import { join as join9 } from "node:path";
2132
+ import { createInterface } from "node:readline/promises";
1737
2133
  init_utils_fs();
1738
2134
  init_utils_json();
1739
2135
 
1740
2136
  // src/utils.lockfile.ts
1741
2137
  init_config();
1742
2138
  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";
2139
+ import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
2140
+ import { dirname as dirname2, join as join8 } from "node:path";
1745
2141
  function lockFilePath() {
1746
- return join6(home(), ".cache", "claude-nomad", "nomad.lock");
2142
+ return join8(home(), ".cache", "claude-nomad", "nomad.lock");
1747
2143
  }
1748
2144
  function acquireLock(verb) {
1749
2145
  const lp = lockFilePath();
@@ -1786,7 +2182,7 @@ function releaseLock(handle) {
1786
2182
  function unlinkIfSamePid(expectedPidStr, lp) {
1787
2183
  let current;
1788
2184
  try {
1789
- current = readFileSync3(lp, "utf8").trim();
2185
+ current = readFileSync4(lp, "utf8").trim();
1790
2186
  } catch {
1791
2187
  return false;
1792
2188
  }
@@ -1801,7 +2197,7 @@ function unlinkIfSamePid(expectedPidStr, lp) {
1801
2197
  function checkStaleAndRetry(verb, lp) {
1802
2198
  let pidStr;
1803
2199
  try {
1804
- pidStr = readFileSync3(lp, "utf8").trim();
2200
+ pidStr = readFileSync4(lp, "utf8").trim();
1805
2201
  } catch {
1806
2202
  pidStr = "";
1807
2203
  }
@@ -1869,30 +2265,30 @@ async function confirmCapture(destLabel, keys) {
1869
2265
  }
1870
2266
  }
1871
2267
  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) : {};
2268
+ const destPath = useHost ? join9(repo, "hosts", `${HOST}.json`) : join9(repo, "shared", "settings.base.json");
2269
+ const existing = existsSync7(destPath) ? readJson(destPath) : {};
1874
2270
  return { destPath, existing };
1875
2271
  }
1876
2272
  async function cmdCaptureSettings(opts) {
1877
2273
  const { host: useHost, dryRun } = opts;
1878
2274
  const repo = repoHome();
1879
- if (!existsSync5(repo)) die(`repo not cloned at ${repo}`);
2275
+ if (!existsSync7(repo)) die(`repo not cloned at ${repo}`);
1880
2276
  const handle = acquireLock("capture-settings");
1881
2277
  if (handle === null) process.exit(0);
1882
2278
  try {
1883
2279
  const claude = claudeHome();
1884
- const basePath = join7(repo, "shared", "settings.base.json");
1885
- if (!existsSync5(basePath)) {
2280
+ const basePath = join9(repo, "shared", "settings.base.json");
2281
+ if (!existsSync7(basePath)) {
1886
2282
  die("repo not initialized; run 'nomad init' to scaffold");
1887
2283
  }
1888
- const settingsPath = join7(claude, "settings.json");
1889
- if (!existsSync5(settingsPath)) {
2284
+ const settingsPath = join9(claude, "settings.json");
2285
+ if (!existsSync7(settingsPath)) {
1890
2286
  log("no ~/.claude/settings.json found; nothing to capture");
1891
2287
  return;
1892
2288
  }
1893
2289
  const base = readJson(basePath);
1894
- const hostPath = join7(repo, "hosts", `${HOST}.json`);
1895
- const overrides = existsSync5(hostPath) ? readJson(hostPath) : {};
2290
+ const hostPath = join9(repo, "hosts", `${HOST}.json`);
2291
+ const overrides = existsSync7(hostPath) ? readJson(hostPath) : {};
1896
2292
  const merged = deepMerge(base, overrides);
1897
2293
  const settings = readJson(settingsPath);
1898
2294
  const subset = buildCaptureSubset(merged, settings, { normalizeNodePath: !useHost });
@@ -1929,8 +2325,8 @@ async function cmdCaptureSettings(opts) {
1929
2325
  // src/commands.clean.ts
1930
2326
  init_config();
1931
2327
  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";
2328
+ import { existsSync as existsSync8, lstatSync as lstatSync6, readdirSync as readdirSync3, rmSync as rmSync5, statSync as statSync2 } from "node:fs";
2329
+ import { join as join10 } from "node:path";
1934
2330
  var TS_SHAPE = /^\d{8}-\d{6}(-\d+)?$/;
1935
2331
  var DURATION_RE = /^(\d+)([dhm])$/;
1936
2332
  var UNIT_MS = { d: 864e5, h: 36e5, m: 6e4 };
@@ -1944,8 +2340,8 @@ function parseDuration(s) {
1944
2340
  return Number(m[1]) * UNIT_MS[m[2]];
1945
2341
  }
1946
2342
  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);
2343
+ if (!existsSync8(backupBase2)) return [];
2344
+ return readdirSync3(backupBase2).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join10(backupBase2, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
1949
2345
  }
1950
2346
  function prunableByAge(dirs, olderThanMs, nowMs) {
1951
2347
  return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
@@ -1955,10 +2351,10 @@ function prunableByCount(dirs, keep) {
1955
2351
  }
1956
2352
  function safeDelete(backupBase2, name) {
1957
2353
  if (!isTsDir(name)) return;
1958
- const full = join8(backupBase2, name);
1959
- const st = lstatSync4(full, { throwIfNoEntry: false });
2354
+ const full = join10(backupBase2, name);
2355
+ const st = lstatSync6(full, { throwIfNoEntry: false });
1960
2356
  if (!st || st.isSymbolicLink() || !st.isDirectory()) return;
1961
- rmSync3(full, { recursive: true, force: true });
2357
+ rmSync5(full, { recursive: true, force: true });
1962
2358
  }
1963
2359
  function resolveTargets(dirs, olderThanMs, keep) {
1964
2360
  if (keep !== void 0) return prunableByCount(dirs, keep);
@@ -1993,9 +2389,10 @@ function cmdClean(opts, backupBase2 = backupBase()) {
1993
2389
  // src/commands.eject.ts
1994
2390
  init_config();
1995
2391
  init_utils();
2392
+ init_utils_fs();
1996
2393
  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";
2394
+ import { cpSync as cpSync5, existsSync as existsSync9, lstatSync as lstatSync7, realpathSync, rmSync as rmSync6 } from "node:fs";
2395
+ import { join as join11, sep as sep3 } from "node:path";
1999
2396
  function ejectChecklist() {
2000
2397
  return [
2001
2398
  "Manual steps remaining to finish leaving claude-nomad on this host:",
@@ -2011,33 +2408,33 @@ function errMessage(err) {
2011
2408
  }
2012
2409
  function lexists2(p) {
2013
2410
  try {
2014
- lstatSync5(p);
2411
+ lstatSync7(p);
2015
2412
  return true;
2016
2413
  } catch {
2017
2414
  return false;
2018
2415
  }
2019
2416
  }
2020
2417
  function readMapIfPresent2(repoHome2) {
2021
- const mapPath = join9(repoHome2, "path-map.json");
2022
- return existsSync7(mapPath) ? readPathMap(mapPath) : { projects: {} };
2418
+ const mapPath = join11(repoHome2, "path-map.json");
2419
+ return existsSync9(mapPath) ? readPathMap(mapPath) : { projects: {} };
2023
2420
  }
2024
2421
  function classifyName(linkPath) {
2025
2422
  if (!lexists2(linkPath)) return "absent";
2026
- if (!lstatSync5(linkPath).isSymbolicLink()) return "skip-real";
2027
- if (!existsSync7(linkPath)) return "dangling";
2423
+ if (!lstatSync7(linkPath).isSymbolicLink()) return "skip-real";
2424
+ if (!existsSync9(linkPath)) return "dangling";
2028
2425
  return "materialize";
2029
2426
  }
2030
2427
  function resolveSharedRoot(repoHome2) {
2031
2428
  try {
2032
- return realpathSync(join9(repoHome2, "shared"));
2429
+ return realpathSync(join11(repoHome2, "shared"));
2033
2430
  } catch {
2034
2431
  return die(
2035
- `cannot resolve ${join9(repoHome2, "shared")} (repo checkout incomplete). run \`nomad pull\` first, then re-run \`nomad eject\``
2432
+ `cannot resolve ${join11(repoHome2, "shared")} (repo checkout incomplete). run \`nomad pull\` first, then re-run \`nomad eject\``
2036
2433
  );
2037
2434
  }
2038
2435
  }
2039
2436
  function isManagedTarget(target, sharedRoot) {
2040
- return target.startsWith(sharedRoot + sep2);
2437
+ return target.startsWith(sharedRoot + sep3);
2041
2438
  }
2042
2439
  function materializeOne(name, linkPath, sharedRoot) {
2043
2440
  const target = realpathSync(linkPath);
@@ -2047,33 +2444,36 @@ function materializeOne(name, linkPath, sharedRoot) {
2047
2444
  }
2048
2445
  const tmp = `${linkPath}.eject.tmp.${process.pid}.${Date.now()}`;
2049
2446
  try {
2050
- rmSync4(tmp, { recursive: true, force: true });
2051
- cpSync3(target, tmp, {
2447
+ rmSync6(tmp, { recursive: true, force: true });
2448
+ cpSync5(target, tmp, {
2052
2449
  recursive: true,
2053
2450
  force: true,
2054
2451
  dereference: true,
2055
2452
  preserveTimestamps: true
2056
2453
  });
2057
- rmSync4(linkPath, { force: true });
2058
- renameSync2(tmp, linkPath);
2454
+ rmSync6(linkPath, { force: true });
2455
+ renameAtomicRetry(tmp, linkPath);
2059
2456
  item(`ejected: ${name}`);
2060
2457
  return true;
2061
2458
  } catch (err) {
2062
2459
  try {
2063
- rmSync4(tmp, { recursive: true, force: true });
2460
+ rmSync6(tmp, { recursive: true, force: true });
2064
2461
  } catch {
2065
2462
  }
2066
2463
  throw err;
2067
2464
  }
2068
2465
  }
2466
+ function skipRealMessage(name) {
2467
+ return process.platform === "win32" ? `already a real copy (win32 copy-sync): ${name}` : `skipped (not a symlink): ${name}`;
2468
+ }
2069
2469
  function previewDryRun(names, classifications, claudeHome2, sharedRoot) {
2070
2470
  for (const name of names) {
2071
2471
  const cls = classifications.get(name);
2072
- const linkPath = join9(claudeHome2, name);
2472
+ const linkPath = join11(claudeHome2, name);
2073
2473
  if (cls === "absent") {
2074
2474
  item(`skipped (absent): ${name}`);
2075
2475
  } else if (cls === "skip-real") {
2076
- item(`skipped (not a symlink): ${name}`);
2476
+ item(skipRealMessage(name));
2077
2477
  } else {
2078
2478
  previewMaterialize(name, linkPath, sharedRoot);
2079
2479
  }
@@ -2099,12 +2499,12 @@ function runLiveEject(names, classifications, claudeHome2, sharedRoot) {
2099
2499
  let skipped = 0;
2100
2500
  for (const name of names) {
2101
2501
  const cls = classifications.get(name);
2102
- const linkPath = join9(claudeHome2, name);
2502
+ const linkPath = join11(claudeHome2, name);
2103
2503
  if (cls === "absent") {
2104
2504
  item(`skipped (absent): ${name}`);
2105
2505
  skipped++;
2106
2506
  } else if (cls === "skip-real") {
2107
- item(`skipped (not a symlink): ${name}`);
2507
+ item(skipRealMessage(name));
2108
2508
  skipped++;
2109
2509
  } else if (materializeOneOrDie(name, linkPath, sharedRoot, done)) {
2110
2510
  done.push(name);
@@ -2135,7 +2535,7 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
2135
2535
  const names = allSharedLinks(map);
2136
2536
  const classifications = /* @__PURE__ */ new Map();
2137
2537
  for (const name of names) {
2138
- classifications.set(name, classifyName(join9(claudeHome2, name)));
2538
+ classifications.set(name, classifyName(join11(claudeHome2, name)));
2139
2539
  }
2140
2540
  const dangling = names.filter((n) => classifications.get(n) === "dangling");
2141
2541
  if (dangling.length > 0) {
@@ -2158,14 +2558,14 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
2158
2558
  }
2159
2559
 
2160
2560
  // src/commands.doctor.ts
2161
- import { existsSync as existsSync32 } from "node:fs";
2162
- import { join as join38 } from "node:path";
2561
+ import { existsSync as existsSync34 } from "node:fs";
2562
+ import { join as join40 } from "node:path";
2163
2563
 
2164
2564
  // src/commands.doctor.checks.repo.ts
2165
2565
  init_color();
2166
2566
  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";
2567
+ import { existsSync as existsSync11, lstatSync as lstatSync8, readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
2568
+ import { join as join13 } from "node:path";
2169
2569
 
2170
2570
  // src/commands.doctor.format.ts
2171
2571
  init_color();
@@ -2243,15 +2643,15 @@ function readJsonSafe(path, label, section2) {
2243
2643
  // src/init.classify.ts
2244
2644
  init_config();
2245
2645
  init_utils_json();
2246
- import { existsSync as existsSync8 } from "node:fs";
2247
- import { join as join10 } from "node:path";
2646
+ import { existsSync as existsSync10 } from "node:fs";
2647
+ import { join as join12 } from "node:path";
2248
2648
  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);
2649
+ const basePath = join12(repoHome2, "shared", "settings.base.json");
2650
+ const mapPath = join12(repoHome2, "path-map.json");
2651
+ const hostPath = join12(repoHome2, "hosts", `${host}.json`);
2652
+ const hasBase = existsSync10(basePath);
2653
+ const hasMap = existsSync10(mapPath);
2654
+ const hasHost = existsSync10(hostPath);
2255
2655
  let mapEntryCount = 0;
2256
2656
  if (hasMap) {
2257
2657
  try {
@@ -2266,11 +2666,11 @@ function classifyRepoState(repoHome2, host) {
2266
2666
  return "partial";
2267
2667
  }
2268
2668
  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";
2669
+ const basePath = join12(repoHome2, "shared", "settings.base.json");
2670
+ const mapPath = join12(repoHome2, "path-map.json");
2671
+ const hostPath = join12(repoHome2, "hosts", `${host}.json`);
2672
+ if (!existsSync10(basePath)) return "- shared/settings.base.json missing";
2673
+ if (!existsSync10(mapPath)) return "- path-map.json missing";
2274
2674
  let mapEntryCount;
2275
2675
  try {
2276
2676
  const map = readJson(mapPath);
@@ -2279,7 +2679,7 @@ function reasonForPartial(repoHome2, host) {
2279
2679
  mapEntryCount = 0;
2280
2680
  }
2281
2681
  if (mapEntryCount === 0) return "- path-map.json.projects has no entries";
2282
- if (!existsSync8(hostPath)) return `- hosts/${host}.json missing`;
2682
+ if (!existsSync10(hostPath)) return `- hosts/${host}.json missing`;
2283
2683
  return "- partial state (unknown gap)";
2284
2684
  }
2285
2685
 
@@ -2296,15 +2696,15 @@ function reportHostAndPaths(section2) {
2296
2696
  if (isOverrideActive()) {
2297
2697
  addItem(section2, `${dim(infoGlyph)} NOMAD_REPO: ${blue(repo)}`);
2298
2698
  }
2299
- addItem(section2, `${existsSync9(repo) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(repo)}`);
2699
+ addItem(section2, `${existsSync11(repo) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(repo)}`);
2300
2700
  addItem(
2301
2701
  section2,
2302
- `${existsSync9(claude) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(claude)}`
2702
+ `${existsSync11(claude) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(claude)}`
2303
2703
  );
2304
2704
  }
2305
2705
  function pathMapHostKeys() {
2306
- const mapPath = join11(repoHome(), "path-map.json");
2307
- if (!existsSync9(mapPath)) return /* @__PURE__ */ new Set();
2706
+ const mapPath = join13(repoHome(), "path-map.json");
2707
+ if (!existsSync11(mapPath)) return /* @__PURE__ */ new Set();
2308
2708
  let raw;
2309
2709
  try {
2310
2710
  raw = readJson(mapPath);
@@ -2321,7 +2721,7 @@ function pathMapHostKeys() {
2321
2721
  function hostOverrideLabels() {
2322
2722
  let entries;
2323
2723
  try {
2324
- entries = readdirSync2(join11(repoHome(), "hosts"));
2724
+ entries = readdirSync4(join13(repoHome(), "hosts"));
2325
2725
  } catch {
2326
2726
  return /* @__PURE__ */ new Set();
2327
2727
  }
@@ -2362,12 +2762,12 @@ function reportRepoState(section2) {
2362
2762
  }
2363
2763
  }
2364
2764
  function repoHasSharedSource(name) {
2365
- return existsSync9(join11(repoHome(), "shared", name));
2765
+ return existsSync11(join13(repoHome(), "shared", name));
2366
2766
  }
2367
2767
  function classifySharedLink(name, p) {
2368
2768
  let stat;
2369
2769
  try {
2370
- stat = lstatSync6(p);
2770
+ stat = lstatSync8(p);
2371
2771
  } catch (err) {
2372
2772
  const code = err.code;
2373
2773
  if (code === "ENOENT") {
@@ -2379,6 +2779,12 @@ function classifySharedLink(name, p) {
2379
2779
  return { line: `${red(failGlyph)} ${name}: could not stat (${String(code)})`, fail: true };
2380
2780
  }
2381
2781
  if (!stat.isSymbolicLink()) {
2782
+ if (process.platform === "win32") {
2783
+ return {
2784
+ line: `${green(okGlyph)} ${name}: real copy (win32 copy-sync)`,
2785
+ fail: false
2786
+ };
2787
+ }
2382
2788
  return {
2383
2789
  line: `${red(failGlyph)} ${name}: NOT a symlink (blocks sync); run \`nomad adopt ${name}\` to fix`,
2384
2790
  fail: true
@@ -2410,7 +2816,7 @@ function classifySymlinkTarget(name, p) {
2410
2816
  function reportSharedLinks(section2, map) {
2411
2817
  const claude = claudeHome();
2412
2818
  for (const name of allSharedLinks(map)) {
2413
- const p = join11(claude, name);
2819
+ const p = join13(claude, name);
2414
2820
  const { line, fail: fail2 } = classifySharedLink(name, p);
2415
2821
  addItem(section2, line);
2416
2822
  if (fail2) process.exitCode = 1;
@@ -2419,10 +2825,10 @@ function reportSharedLinks(section2, map) {
2419
2825
  function reportDroppedNamesMigration(section2) {
2420
2826
  const claude = claudeHome();
2421
2827
  for (const name of GSD_DROPPED_NAMES) {
2422
- const p = join11(claude, name);
2828
+ const p = join13(claude, name);
2423
2829
  let stat;
2424
2830
  try {
2425
- stat = lstatSync6(p);
2831
+ stat = lstatSync8(p);
2426
2832
  } catch {
2427
2833
  continue;
2428
2834
  }
@@ -2437,11 +2843,11 @@ function reportDroppedNamesMigration(section2) {
2437
2843
  // src/commands.doctor.checks.settings.ts
2438
2844
  init_color();
2439
2845
  init_config();
2440
- import { existsSync as existsSync10, readdirSync as readdirSync3 } from "node:fs";
2441
- import { join as join12 } from "node:path";
2846
+ import { existsSync as existsSync12, readdirSync as readdirSync5 } from "node:fs";
2847
+ import { join as join14 } from "node:path";
2442
2848
  function loadBaseSettings(section2) {
2443
- const basePath = join12(repoHome(), "shared", "settings.base.json");
2444
- if (!existsSync10(basePath)) {
2849
+ const basePath = join14(repoHome(), "shared", "settings.base.json");
2850
+ if (!existsSync12(basePath)) {
2445
2851
  addItem(section2, `${red(failGlyph)} shared/settings.base.json missing at ${blue(basePath)}`);
2446
2852
  process.exitCode = 1;
2447
2853
  return null;
@@ -2449,8 +2855,8 @@ function loadBaseSettings(section2) {
2449
2855
  return readJsonSafe(basePath, basePath, section2);
2450
2856
  }
2451
2857
  function loadAndReportSettings(section2) {
2452
- const settingsPath = join12(claudeHome(), "settings.json");
2453
- if (!existsSync10(settingsPath)) return null;
2858
+ const settingsPath = join14(claudeHome(), "settings.json");
2859
+ if (!existsSync12(settingsPath)) return null;
2454
2860
  const settings = readJsonSafe(settingsPath, settingsPath, section2);
2455
2861
  if (settings === null) return null;
2456
2862
  const unknownKeys = Object.keys(settings).filter((k) => !KNOWN_SETTINGS_KEYS.has(k));
@@ -2466,13 +2872,13 @@ function loadAndReportSettings(section2) {
2466
2872
  }
2467
2873
  function reportHostOverrides(section2, base, settings) {
2468
2874
  const repo = repoHome();
2469
- const hostFile = join12(repo, "hosts", `${HOST}.json`);
2875
+ const hostFile = join14(repo, "hosts", `${HOST}.json`);
2470
2876
  let drift = [];
2471
2877
  if (base !== null && settings !== null) {
2472
2878
  const baseKeys = new Set(Object.keys(base));
2473
2879
  drift = Object.keys(settings).filter((k) => !baseKeys.has(k));
2474
2880
  }
2475
- if (existsSync10(hostFile)) {
2881
+ if (existsSync12(hostFile)) {
2476
2882
  if (readJsonSafe(hostFile, hostFile, section2) !== null) {
2477
2883
  addItem(section2, `${green(okGlyph)} host overrides: ${blue(hostFile)}`);
2478
2884
  }
@@ -2481,9 +2887,9 @@ function reportHostOverrides(section2, base, settings) {
2481
2887
  section2,
2482
2888
  `${red(failGlyph)} no hosts/${HOST}.json AND settings.json has unbased keys ${JSON.stringify(drift)}`
2483
2889
  );
2484
- const hostsDir = join12(repo, "hosts");
2485
- if (existsSync10(hostsDir)) {
2486
- const cands = readdirSync3(hostsDir).filter((f) => f.endsWith(".json"));
2890
+ const hostsDir = join14(repo, "hosts");
2891
+ if (existsSync12(hostsDir)) {
2892
+ const cands = readdirSync5(hostsDir).filter((f) => f.endsWith(".json"));
2487
2893
  if (cands.length > 0) addItem(section2, `${dim(infoGlyph)} candidates: ${cands.join(", ")}`);
2488
2894
  }
2489
2895
  process.exitCode = 1;
@@ -2498,8 +2904,8 @@ function reportHostOverrides(section2, base, settings) {
2498
2904
  // src/commands.doctor.checks.pathmap.ts
2499
2905
  init_color();
2500
2906
  init_config();
2501
- import { existsSync as existsSync11, readdirSync as readdirSync4 } from "node:fs";
2502
- import { join as join13 } from "node:path";
2907
+ import { existsSync as existsSync13, readdirSync as readdirSync6 } from "node:fs";
2908
+ import { join as join15 } from "node:path";
2503
2909
  init_utils_json();
2504
2910
  function reportMappedProjects(section2, map) {
2505
2911
  const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
@@ -2509,11 +2915,11 @@ function reportMappedProjects(section2, map) {
2509
2915
  }
2510
2916
  }
2511
2917
  function reportUnmappedProjects(section2, map) {
2512
- const localProjects = join13(claudeHome(), "projects");
2513
- if (!existsSync11(localProjects)) return;
2918
+ const localProjects = join15(claudeHome(), "projects");
2919
+ if (!existsSync13(localProjects)) return;
2514
2920
  let localDirs;
2515
2921
  try {
2516
- localDirs = readdirSync4(localProjects);
2922
+ localDirs = readdirSync6(localProjects);
2517
2923
  } catch {
2518
2924
  return;
2519
2925
  }
@@ -2531,7 +2937,7 @@ function reportCurrentHostPathsMissing(section2, map) {
2531
2937
  for (const [name, hosts] of Object.entries(map.projects)) {
2532
2938
  const abspath = hosts[HOST];
2533
2939
  if (!abspath || abspath === "TBD") continue;
2534
- if (!existsSync11(abspath)) {
2940
+ if (!existsSync13(abspath)) {
2535
2941
  addItem(
2536
2942
  section2,
2537
2943
  `${yellow(warnGlyph)} path-map: ${name} local path missing on ${HOST}: ${blue(abspath)}`
@@ -2562,8 +2968,8 @@ function reportPathCollisions(section2, map) {
2562
2968
  else addItem(section2, `${green(okGlyph)} path-encoding: no collisions`);
2563
2969
  }
2564
2970
  function reportPathMap(section2) {
2565
- const mapPath = join13(repoHome(), "path-map.json");
2566
- if (!existsSync11(mapPath)) {
2971
+ const mapPath = join15(repoHome(), "path-map.json");
2972
+ if (!existsSync13(mapPath)) {
2567
2973
  addItem(section2, `${red(failGlyph)} path-map.json missing at ${blue(mapPath)}`);
2568
2974
  process.exitCode = 1;
2569
2975
  return;
@@ -2592,21 +2998,19 @@ function reportNeverSync(section2) {
2592
2998
 
2593
2999
  // src/commands.doctor.checks.skills.ts
2594
3000
  init_color();
2595
- import { existsSync as existsSync12 } from "node:fs";
2596
- import { join as join14 } from "node:path";
3001
+ import { existsSync as existsSync14 } from "node:fs";
3002
+ import { join as join16 } from "node:path";
2597
3003
  init_config();
2598
3004
 
2599
3005
  // src/extras-sync.diff.ts
2600
3006
  init_utils();
2601
3007
  import { execFileSync as execFileSync2 } from "node:child_process";
2602
- import { relative as relative2 } from "node:path";
3008
+ import { relative as relative3 } from "node:path";
2603
3009
  function parseNameStatus(stdout) {
3010
+ const fields = stdout.split("\0").filter((f) => f.length > 0);
2604
3011
  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) });
3012
+ for (let i = 0; i + 1 < fields.length; i += 2) {
3013
+ entries.push({ status: fields[i], path: fields[i + 1] });
2610
3014
  }
2611
3015
  return entries;
2612
3016
  }
@@ -2619,13 +3023,13 @@ function parseDiffOutput(stdout) {
2619
3023
  return parseNameStatus(stdout).map(labelEntry);
2620
3024
  }
2621
3025
  function parseModifiedPaths(stdout, a) {
2622
- return parseNameStatus(stdout).filter((entry) => entry.status === "M").map((entry) => relative2(a, entry.path));
3026
+ return parseNameStatus(stdout).filter((entry) => entry.status === "M").map((entry) => relative3(a, entry.path));
2623
3027
  }
2624
3028
  function runNameStatusDiff(a, b, parse) {
2625
3029
  try {
2626
3030
  const stdout = execFileSync2(
2627
3031
  "git",
2628
- ["diff", "--no-index", "--no-renames", "--name-status", a, b],
3032
+ ["diff", "--no-index", "--no-renames", "-z", "--name-status", a, b],
2629
3033
  {
2630
3034
  stdio: ["ignore", "pipe", "pipe"]
2631
3035
  }
@@ -2670,13 +3074,13 @@ function isGsdDiffLine(line, localBase, sharedBase) {
2670
3074
  return relative9.split("/")[0].startsWith(GSD_PREFIX);
2671
3075
  }
2672
3076
  function reportSkillsDivergence(section2) {
2673
- const sharedSkills = join14(repoHome(), "shared", "skills");
2674
- const localSkills = join14(claudeHome(), "skills");
2675
- if (!existsSync12(sharedSkills)) {
3077
+ const sharedSkills = join16(repoHome(), "shared", "skills");
3078
+ const localSkills = join16(claudeHome(), "skills");
3079
+ if (!existsSync14(sharedSkills)) {
2676
3080
  addItem(section2, `${dim(infoGlyph)} skills: no shared/skills/ to compare`);
2677
3081
  return;
2678
3082
  }
2679
- if (!existsSync12(localSkills)) {
3083
+ if (!existsSync14(localSkills)) {
2680
3084
  addItem(section2, `${dim(infoGlyph)} skills: no local skills/ to compare`);
2681
3085
  return;
2682
3086
  }
@@ -2699,8 +3103,8 @@ function reportSkillsDivergence(section2) {
2699
3103
  init_color();
2700
3104
  init_config();
2701
3105
  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";
3106
+ import { existsSync as existsSync17 } from "node:fs";
3107
+ import { join as join20, relative as relative4 } from "node:path";
2704
3108
  init_commands_pull_wedge();
2705
3109
  init_push_checks();
2706
3110
  init_utils();
@@ -2723,11 +3127,11 @@ function reportGitleaksProbe(section2) {
2723
3127
  }
2724
3128
  function reportGitlinks(section2) {
2725
3129
  const repo = repoHome();
2726
- const sharedDir = join18(repo, "shared");
2727
- if (existsSync15(sharedDir)) {
3130
+ const sharedDir = join20(repo, "shared");
3131
+ if (existsSync17(sharedDir)) {
2728
3132
  const gitlinks = findGitlinks(sharedDir);
2729
3133
  for (const p of gitlinks) {
2730
- const rel = relative3(repo, p);
3134
+ const rel = relative4(repo, p).replaceAll("\\", "/");
2731
3135
  addItem(
2732
3136
  section2,
2733
3137
  `${red(failGlyph)} gitlink: ${blue(rel)} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`
@@ -2822,13 +3226,13 @@ function reportGitIdentity(section2) {
2822
3226
 
2823
3227
  // src/commands.doctor.checks.backups.ts
2824
3228
  init_color();
2825
- import { existsSync as existsSync16, lstatSync as lstatSync7, readdirSync as readdirSync6 } from "node:fs";
2826
- import { join as join19 } from "node:path";
3229
+ import { existsSync as existsSync18, lstatSync as lstatSync9, readdirSync as readdirSync8 } from "node:fs";
3230
+ import { join as join21 } from "node:path";
2827
3231
  init_config();
2828
3232
  var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
2829
3233
  function safeReaddir(dir) {
2830
3234
  try {
2831
- return readdirSync6(dir);
3235
+ return readdirSync8(dir);
2832
3236
  } catch {
2833
3237
  return [];
2834
3238
  }
@@ -2839,8 +3243,8 @@ var BYTES_PER_MB = 1024 * 1024;
2839
3243
  function dirSizeBytes(dir) {
2840
3244
  let bytes = 0;
2841
3245
  for (const entry of safeReaddir(dir)) {
2842
- const full = join19(dir, entry);
2843
- const st = lstatSync7(full, { throwIfNoEntry: false });
3246
+ const full = join21(dir, entry);
3247
+ const st = lstatSync9(full, { throwIfNoEntry: false });
2844
3248
  if (!st) continue;
2845
3249
  if (st.isSymbolicLink()) continue;
2846
3250
  if (st.isDirectory()) bytes += dirSizeBytes(full);
@@ -2850,11 +3254,11 @@ function dirSizeBytes(dir) {
2850
3254
  }
2851
3255
  function totalSizeMb(backupBase2, dirs) {
2852
3256
  let bytes = 0;
2853
- for (const name of dirs) bytes += dirSizeBytes(join19(backupBase2, name));
3257
+ for (const name of dirs) bytes += dirSizeBytes(join21(backupBase2, name));
2854
3258
  return bytes / BYTES_PER_MB;
2855
3259
  }
2856
3260
  function reportBackupsCheck(section2, backupBase2 = backupBase()) {
2857
- if (!existsSync16(backupBase2)) return;
3261
+ if (!existsSync18(backupBase2)) return;
2858
3262
  const dirs = safeReaddir(backupBase2).filter((n) => TS_SHAPE2.test(n));
2859
3263
  const count = dirs.length;
2860
3264
  const sizeMb = totalSizeMb(backupBase2, dirs);
@@ -2935,8 +3339,8 @@ function reportCheckRemote(section2) {
2935
3339
 
2936
3340
  // src/commands.doctor.check-schema.ts
2937
3341
  init_color();
2938
- import { existsSync as existsSync17 } from "node:fs";
2939
- import { join as join20 } from "node:path";
3342
+ import { existsSync as existsSync19 } from "node:fs";
3343
+ import { join as join22 } from "node:path";
2940
3344
  init_config();
2941
3345
 
2942
3346
  // src/http-fetch.ts
@@ -2973,8 +3377,8 @@ function fetchSchemaKeys() {
2973
3377
  }
2974
3378
  }
2975
3379
  function reportCheckSchema(section2) {
2976
- const settingsPath = join20(claudeHome(), "settings.json");
2977
- if (!existsSync17(settingsPath)) {
3380
+ const settingsPath = join22(claudeHome(), "settings.json");
3381
+ if (!existsSync19(settingsPath)) {
2978
3382
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
2979
3383
  return;
2980
3384
  }
@@ -3004,18 +3408,18 @@ function reportCheckSchema(section2) {
3004
3408
  init_color();
3005
3409
  import { randomBytes } from "node:crypto";
3006
3410
  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";
3411
+ import { existsSync as existsSync22, mkdirSync as mkdirSync6, readdirSync as readdirSync11, rmSync as rmSync10 } from "node:fs";
3008
3412
  import { homedir as homedir4 } from "node:os";
3009
- import { join as join26 } from "node:path";
3413
+ import { join as join27 } from "node:path";
3010
3414
 
3011
3415
  // src/commands.doctor.check-shared.scan.ts
3012
3416
  init_color();
3013
- import { join as join22 } from "node:path";
3417
+ import { join as join24 } from "node:path";
3014
3418
  init_config();
3015
3419
  init_push_gitleaks();
3016
3420
  function scrubPath(logical, sid, logicalToEncoded) {
3017
3421
  const encoded = logicalToEncoded.get(logical) ?? logical;
3018
- return join22(claudeHome(), "projects", encoded, `${sid}.jsonl`);
3422
+ return join24(claudeHome(), "projects", encoded, `${sid}.jsonl`);
3019
3423
  }
3020
3424
  function reportSessionFindings(section2, bySession) {
3021
3425
  for (const [sid, counts] of bySession) {
@@ -3044,274 +3448,67 @@ function reportRemediation(section2, bySession, logicalBySession, logicalToEncod
3044
3448
  }
3045
3449
  addItem(section2, ` ${dim("- false positive? add a pattern to .gitleaks.toml")}`);
3046
3450
  }
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
- }
3177
- }
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
- }
3190
- }
3191
- }
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`
3200
- );
3201
- }
3202
- throw err;
3203
- }
3204
- }
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 });
3226
- }
3227
- function copyExtrasFileSkipDiverged(src, dst) {
3228
- if (existsSync18(dst)) {
3229
- let dstBuf;
3230
- try {
3231
- dstBuf = readFileSync6(dst);
3232
- } catch {
3233
- return;
3234
- }
3235
- if (!readFileSync6(src).equals(dstBuf)) return;
3236
- }
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
- });
3250
- }
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 });
3451
+ var SESSION_PATH_LOGICAL = /^shared\/projects\/([^/]+)\/([^/]+)\.jsonl$/;
3452
+ function emitClean(section2, staged) {
3453
+ addItem(section2, `${green(okGlyph)} ${staged} project(s) scanned, no leaks`);
3454
+ }
3455
+ function buildLogicalBySession(findings) {
3456
+ const logicalBySession = /* @__PURE__ */ new Map();
3457
+ for (const f of findings) {
3458
+ const m = SESSION_PATH_LOGICAL.exec(f.File);
3459
+ if (m?.[2] !== void 0 && !logicalBySession.has(m[2])) {
3460
+ logicalBySession.set(m[2], m[1] ?? "");
3265
3461
  }
3266
3462
  }
3463
+ return logicalBySession;
3267
3464
  }
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 });
3465
+ function emitDescriptionLegend(section2, findings) {
3466
+ const descByRule = /* @__PURE__ */ new Map();
3467
+ for (const f of findings) {
3468
+ if (f.Description && !descByRule.has(f.RuleID)) descByRule.set(f.RuleID, f.Description);
3273
3469
  }
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
- }
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
- }
3470
+ if (descByRule.size === 0) return;
3471
+ addItem(section2, "");
3472
+ addItem(section2, bold("Finding types"));
3473
+ for (const [rule, desc] of descByRule) {
3474
+ const ruleLabel = red(`- [${rule}]`);
3475
+ addItem(section2, ` ${ruleLabel}: ${dim(desc)}`);
3297
3476
  }
3298
3477
  }
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 });
3478
+ function scanAndReport(section2, tmpRoot, staged, logicalToEncoded) {
3479
+ let findings;
3480
+ try {
3481
+ findings = scanStagedTree(tmpRoot);
3482
+ } catch (err) {
3483
+ addItem(section2, `${red(failGlyph)} scan failed: ${err.message}`);
3484
+ process.exitCode = 1;
3485
+ return;
3304
3486
  }
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
- });
3487
+ if (findings === null) {
3488
+ addItem(section2, `${red(failGlyph)} scan failed: no parseable gitleaks report`);
3489
+ process.exitCode = 1;
3490
+ return;
3491
+ }
3492
+ const { bySession, other } = partitionFindings(findings);
3493
+ if (bySession.size === 0 && other.length === 0) {
3494
+ emitClean(section2, staged);
3495
+ return;
3496
+ }
3497
+ if (other.length > 0) reportOtherFindings(section2, other);
3498
+ if (bySession.size > 0) reportSessionFindings(section2, bySession);
3499
+ if (bySession.size > 0) {
3500
+ reportRemediation(section2, bySession, buildLogicalBySession(findings), logicalToEncoded);
3501
+ }
3502
+ emitDescriptionLegend(section2, findings);
3312
3503
  }
3313
3504
 
3505
+ // src/commands.doctor.check-shared.ts
3506
+ init_config();
3507
+
3314
3508
  // src/remap.ts
3509
+ init_config_sharedDirs_guard();
3510
+ 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";
3511
+ import { dirname as dirname4, join as join26, relative as relative5, sep as sep4 } from "node:path";
3315
3512
  init_config();
3316
3513
 
3317
3514
  // src/push-manifest.ts
@@ -3319,8 +3516,8 @@ init_config();
3319
3516
  init_push_gitleaks_config();
3320
3517
  init_utils_fs();
3321
3518
  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";
3519
+ import { existsSync as existsSync20, mkdirSync as mkdirSync4, readdirSync as readdirSync9, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
3520
+ import { dirname as dirname3, join as join25 } from "node:path";
3324
3521
  function isChanged(prev, cur, hash) {
3325
3522
  if (prev === void 0) return true;
3326
3523
  if (prev.size !== cur.size) return true;
@@ -3360,8 +3557,8 @@ function hashFile(absPath) {
3360
3557
  return createHash("sha256").update(readFileSync7(absPath)).digest("hex");
3361
3558
  }
3362
3559
  function enumerateDir(dir, results) {
3363
- for (const entry of readdirSync8(dir)) {
3364
- const fullPath = join24(dir, entry);
3560
+ for (const entry of readdirSync9(dir)) {
3561
+ const fullPath = join25(dir, entry);
3365
3562
  const st = statSync4(fullPath);
3366
3563
  if (st.isDirectory()) {
3367
3564
  enumerateDir(fullPath, results);
@@ -3372,8 +3569,8 @@ function enumerateDir(dir, results) {
3372
3569
  }
3373
3570
  function enumerateSourceFiles(projectDir) {
3374
3571
  const results = [];
3375
- for (const entry of readdirSync8(projectDir)) {
3376
- const fullPath = join24(projectDir, entry);
3572
+ for (const entry of readdirSync9(projectDir)) {
3573
+ const fullPath = join25(projectDir, entry);
3377
3574
  const st = statSync4(fullPath);
3378
3575
  if (st.isDirectory()) {
3379
3576
  enumerateDir(fullPath, results);
@@ -3385,15 +3582,15 @@ function enumerateSourceFiles(projectDir) {
3385
3582
  }
3386
3583
  function fileIdentity(p) {
3387
3584
  if (p === null) return "none::absent";
3388
- if (!existsSync19(p)) return `${p}::absent`;
3585
+ if (!existsSync20(p)) return `${p}::absent`;
3389
3586
  return `${p}::${createHash("sha256").update(readFileSync7(p)).digest("hex")}`;
3390
3587
  }
3391
3588
  function computeConfigHash() {
3392
3589
  const repo = repoHome();
3393
3590
  const parts = [
3394
3591
  fileIdentity(resolveTomlPath(repo)),
3395
- fileIdentity(join24(repo, ".gitleaks.overlay.toml")),
3396
- fileIdentity(join24(repo, ".gitleaksignore"))
3592
+ fileIdentity(join25(repo, ".gitleaks.overlay.toml")),
3593
+ fileIdentity(join25(repo, ".gitleaksignore"))
3397
3594
  ];
3398
3595
  return createHash("sha256").update(parts.join("\n")).digest("hex");
3399
3596
  }
@@ -3427,10 +3624,10 @@ init_utils_json();
3427
3624
  var TMP_SUFFIX = ".nomad-tmp";
3428
3625
  function atomicMirror(src, dst, options) {
3429
3626
  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);
3627
+ rmSync9(tmp, { recursive: true, force: true });
3628
+ cpSync6(src, tmp, options);
3629
+ rmSync9(dst, { recursive: true, force: true });
3630
+ renameAtomicRetry(tmp, dst);
3434
3631
  }
3435
3632
  function overlaySessionDir(src, dst) {
3436
3633
  stripCollidingDstSymlinks(src, dst, () => false);
@@ -3439,11 +3636,11 @@ function overlaySessionDir(src, dst) {
3439
3636
  function copyFileAtomic(src, dst) {
3440
3637
  mkdirSync5(dirname4(dst), { recursive: true });
3441
3638
  const tmp = `${dst}.tmp.${process.pid}`;
3442
- cpSync5(src, tmp, { force: true, preserveTimestamps: true });
3443
- renameSync3(tmp, dst);
3639
+ cpSync6(src, tmp, { force: true, preserveTimestamps: true });
3640
+ renameAtomicRetry(tmp, dst);
3444
3641
  }
3445
3642
  function hasDeltaForDir(sel, localDir) {
3446
- const prefix = `${localDir}${sep3}`;
3643
+ const prefix = `${localDir}${sep4}`;
3447
3644
  for (const p of sel.changed) if (p.startsWith(prefix)) return true;
3448
3645
  for (const p of sel.deleted) if (p.startsWith(prefix)) return true;
3449
3646
  return false;
@@ -3452,15 +3649,15 @@ function skipForSelection(sel, localDir) {
3452
3649
  return sel !== void 0 && !hasDeltaForDir(sel, localDir);
3453
3650
  }
3454
3651
  function applySelective(sel, localDir, repoDst) {
3455
- const prefix = `${localDir}${sep3}`;
3652
+ const prefix = `${localDir}${sep4}`;
3456
3653
  for (const src of sel.changed) {
3457
3654
  if (!src.startsWith(prefix)) continue;
3458
- copyFileAtomic(src, join25(repoDst, relative5(localDir, src)));
3655
+ copyFileAtomic(src, join26(repoDst, relative5(localDir, src)));
3459
3656
  }
3460
3657
  for (const src of sel.deleted) {
3461
3658
  if (!src.startsWith(prefix)) continue;
3462
- const dst = join25(repoDst, relative5(localDir, src));
3463
- if (existsSync20(dst)) rmSync8(dst);
3659
+ const dst = join26(repoDst, relative5(localDir, src));
3660
+ if (existsSync21(dst)) rmSync9(dst);
3464
3661
  }
3465
3662
  }
3466
3663
  function copyDirJsonlOnly(src, dst) {
@@ -3470,7 +3667,7 @@ function copyDirJsonlOnly(src, dst) {
3470
3667
  filter: (srcPath) => {
3471
3668
  const rel = relative5(src, srcPath);
3472
3669
  if (rel === "") return true;
3473
- if (rel.split(sep3).length > 1) return true;
3670
+ if (rel.split(sep4).length > 1) return true;
3474
3671
  if (statSync5(srcPath).isDirectory()) return true;
3475
3672
  if (srcPath.endsWith(".jsonl")) return true;
3476
3673
  item(`skip ${rel}: extension not in allowlist`);
@@ -3489,15 +3686,15 @@ function remapPull(ts, opts = {}) {
3489
3686
  const wouldPull = [];
3490
3687
  const repo = repoHome();
3491
3688
  const claude = claudeHome();
3492
- const mapPath = join25(repo, "path-map.json");
3493
- const repoProjects = join25(repo, "shared", "projects");
3494
- if (!existsSync20(mapPath) || !existsSync20(repoProjects)) {
3689
+ const mapPath = join26(repo, "path-map.json");
3690
+ const repoProjects = join26(repo, "shared", "projects");
3691
+ if (!existsSync21(mapPath) || !existsSync21(repoProjects)) {
3495
3692
  const text = "no path-map or repo projects dir; skipping session remap";
3496
3693
  emitPreview(opts.onPreview, { kind: "note", text }, text);
3497
3694
  return { unmapped: 0, pulled, wouldPull };
3498
3695
  }
3499
3696
  const map = readPathMap(mapPath);
3500
- const localProjects = join25(claude, "projects");
3697
+ const localProjects = join26(claude, "projects");
3501
3698
  if (!dryRun) mkdirSync5(localProjects, { recursive: true });
3502
3699
  for (const [logical, hosts] of Object.entries(map.projects)) {
3503
3700
  assertSafeLogical(logical);
@@ -3507,9 +3704,9 @@ function remapPull(ts, opts = {}) {
3507
3704
  continue;
3508
3705
  }
3509
3706
  assertSafeLocalRoot(localPath, logical);
3510
- const src = join25(repoProjects, logical);
3511
- if (!existsSync20(src)) continue;
3512
- const dst = join25(localProjects, encodePath(localPath));
3707
+ const src = join26(repoProjects, logical);
3708
+ if (!existsSync21(src)) continue;
3709
+ const dst = join26(localProjects, encodePath(localPath));
3513
3710
  if (dryRun) {
3514
3711
  wouldPull.push(logical);
3515
3712
  emitPreview(
@@ -3527,12 +3724,12 @@ function remapPull(ts, opts = {}) {
3527
3724
  }
3528
3725
  function countLocalOnly(src, dst) {
3529
3726
  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()) {
3727
+ for (const name of readdirSync10(dst)) {
3728
+ const dstPath = join26(dst, name);
3729
+ const srcPath = join26(src, name);
3730
+ if (lstatSync10(dstPath).isDirectory()) {
3534
3731
  count += countLocalOnly(srcPath, dstPath);
3535
- } else if (lstatSync9(srcPath, { throwIfNoEntry: false }) === void 0) {
3732
+ } else if (lstatSync10(srcPath, { throwIfNoEntry: false }) === void 0) {
3536
3733
  count++;
3537
3734
  }
3538
3735
  }
@@ -3541,20 +3738,20 @@ function countLocalOnly(src, dst) {
3541
3738
  function scanLocalOnly() {
3542
3739
  const repo = repoHome();
3543
3740
  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;
3741
+ const mapPath = join26(repo, "path-map.json");
3742
+ const repoProjects = join26(repo, "shared", "projects");
3743
+ if (!existsSync21(mapPath) || !existsSync21(repoProjects)) return 0;
3547
3744
  const map = readPathMap(mapPath);
3548
- const localProjects = join25(claude, "projects");
3745
+ const localProjects = join26(claude, "projects");
3549
3746
  let count = 0;
3550
3747
  for (const [logical, hosts] of Object.entries(map.projects)) {
3551
3748
  assertSafeLogical(logical);
3552
3749
  const localPath = hosts[HOST];
3553
3750
  if (!localPath || localPath === "TBD") continue;
3554
3751
  assertSafeLocalRoot(localPath, logical);
3555
- const dst = join25(localProjects, encodePath(localPath));
3556
- if (!existsSync20(dst)) continue;
3557
- count += countLocalOnly(join25(repoProjects, logical), dst);
3752
+ const dst = join26(localProjects, encodePath(localPath));
3753
+ if (!existsSync21(dst)) continue;
3754
+ count += countLocalOnly(join26(repoProjects, logical), dst);
3558
3755
  }
3559
3756
  return count;
3560
3757
  }
@@ -3589,26 +3786,26 @@ function remapPush(ts, opts = {}) {
3589
3786
  const wouldPush = [];
3590
3787
  const repo = repoHome();
3591
3788
  const claude = claudeHome();
3592
- const mapPath = join25(repo, "path-map.json");
3593
- if (!existsSync20(mapPath)) {
3789
+ const mapPath = join26(repo, "path-map.json");
3790
+ if (!existsSync21(mapPath)) {
3594
3791
  log("no path-map.json; skipping session export");
3595
3792
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
3596
3793
  }
3597
3794
  const map = readPathMap(mapPath);
3598
- const localProjects = join25(claude, "projects");
3599
- const repoProjects = join25(repo, "shared", "projects");
3795
+ const localProjects = join26(claude, "projects");
3796
+ const repoProjects = join26(repo, "shared", "projects");
3600
3797
  const reverse = buildReverseMap(map);
3601
- if (!existsSync20(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
3798
+ if (!existsSync21(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
3602
3799
  if (!dryRun) mkdirSync5(repoProjects, { recursive: true });
3603
- for (const dir of readdirSync9(localProjects)) {
3800
+ for (const dir of readdirSync10(localProjects)) {
3604
3801
  if (dir.endsWith(TMP_SUFFIX)) continue;
3605
3802
  const logical = reverse.get(dir);
3606
3803
  if (!logical) {
3607
3804
  unmapped++;
3608
3805
  continue;
3609
3806
  }
3610
- const localDir = join25(localProjects, dir);
3611
- const repoDst = join25(repoProjects, logical);
3807
+ const localDir = join26(localProjects, dir);
3808
+ const repoDst = join26(repoProjects, logical);
3612
3809
  if (skipForSelection(opts.selection, localDir)) continue;
3613
3810
  if (dryRun) {
3614
3811
  wouldPush.push(logical);
@@ -3632,8 +3829,8 @@ function buildScanTree(tmpRoot) {
3632
3829
  const logicalToEncoded = /* @__PURE__ */ new Map();
3633
3830
  let staged = 0;
3634
3831
  const repo = repoHome();
3635
- const mapPath = join26(repo, "path-map.json");
3636
- if (!existsSync21(mapPath)) return { logicalToEncoded, staged, malformed: false };
3832
+ const mapPath = join27(repo, "path-map.json");
3833
+ if (!existsSync22(mapPath)) return { logicalToEncoded, staged, malformed: false };
3637
3834
  let map;
3638
3835
  try {
3639
3836
  map = readJson(mapPath);
@@ -3650,12 +3847,12 @@ function buildScanTree(tmpRoot) {
3650
3847
  if (!p || p === "TBD") continue;
3651
3848
  reverse.set(encodePath(p), logical);
3652
3849
  }
3653
- const localProjects = join26(claudeHome(), "projects");
3654
- if (!existsSync21(localProjects)) return { logicalToEncoded, staged, malformed: false };
3655
- for (const dir of readdirSync10(localProjects)) {
3850
+ const localProjects = join27(claudeHome(), "projects");
3851
+ if (!existsSync22(localProjects)) return { logicalToEncoded, staged, malformed: false };
3852
+ for (const dir of readdirSync11(localProjects)) {
3656
3853
  const logical = reverse.get(dir);
3657
3854
  if (!logical) continue;
3658
- copyDirJsonlOnly(join26(localProjects, dir), join26(tmpRoot, "shared", "projects", logical));
3855
+ copyDirJsonlOnly(join27(localProjects, dir), join27(tmpRoot, "shared", "projects", logical));
3659
3856
  logicalToEncoded.set(logical, dir);
3660
3857
  staged++;
3661
3858
  }
@@ -3686,11 +3883,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
3686
3883
  }
3687
3884
  function reportCheckShared(section2, gitleaksReady) {
3688
3885
  if (!ensureGitleaksReady(section2, gitleaksReady)) return;
3689
- const cacheDir = join26(homedir4(), ".cache", "claude-nomad");
3886
+ const cacheDir = join27(homedir4(), ".cache", "claude-nomad");
3690
3887
  mkdirSync6(cacheDir, { recursive: true });
3691
3888
  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}`);
3889
+ const reportPath = join27(cacheDir, `check-shared-${stamp}.json`);
3890
+ const tmpRoot = join27(cacheDir, `check-shared-tree-${stamp}`);
3694
3891
  try {
3695
3892
  const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
3696
3893
  if (malformed) {
@@ -3704,15 +3901,15 @@ function reportCheckShared(section2, gitleaksReady) {
3704
3901
  }
3705
3902
  scanAndReport(section2, tmpRoot, staged, logicalToEncoded);
3706
3903
  } finally {
3707
- rmSync9(reportPath, { force: true });
3708
- rmSync9(tmpRoot, { recursive: true, force: true });
3904
+ rmSync10(reportPath, { force: true });
3905
+ rmSync10(tmpRoot, { recursive: true, force: true });
3709
3906
  }
3710
3907
  }
3711
3908
 
3712
3909
  // src/commands.doctor.checks.hooks.scope.ts
3713
3910
  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";
3911
+ import { existsSync as existsSync23, readFileSync as readFileSync8, readdirSync as readdirSync12, realpathSync as realpathSync2 } from "node:fs";
3912
+ import { dirname as dirname5, extname, join as join28 } from "node:path";
3716
3913
  init_config();
3717
3914
  function typeFromPackageJson(pkgPath) {
3718
3915
  try {
@@ -3734,8 +3931,8 @@ function effectiveType(hookPath) {
3734
3931
  }
3735
3932
  let dir = dirname5(real);
3736
3933
  for (; ; ) {
3737
- const pkg = join27(dir, "package.json");
3738
- if (existsSync22(pkg)) return typeFromPackageJson(pkg);
3934
+ const pkg = join28(dir, "package.json");
3935
+ if (existsSync23(pkg)) return typeFromPackageJson(pkg);
3739
3936
  const parent = dirname5(dir);
3740
3937
  if (parent === dir) return "cjs";
3741
3938
  dir = parent;
@@ -3764,7 +3961,7 @@ function remedy(family) {
3764
3961
  }
3765
3962
  function safeReaddir2(dir) {
3766
3963
  try {
3767
- return readdirSync11(dir);
3964
+ return readdirSync12(dir);
3768
3965
  } catch {
3769
3966
  return [];
3770
3967
  }
@@ -3777,15 +3974,15 @@ function safeRead(path) {
3777
3974
  }
3778
3975
  }
3779
3976
  function reportHookScopeCheck(section2) {
3780
- const hooksDir = join27(claudeHome(), "hooks");
3781
- if (!existsSync22(hooksDir)) {
3977
+ const hooksDir = join28(claudeHome(), "hooks");
3978
+ if (!existsSync23(hooksDir)) {
3782
3979
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
3783
3980
  return;
3784
3981
  }
3785
3982
  let anyWarn = false;
3786
3983
  for (const name of safeReaddir2(hooksDir)) {
3787
3984
  if (extname(name) !== ".js") continue;
3788
- const abs = join27(hooksDir, name);
3985
+ const abs = join28(hooksDir, name);
3789
3986
  const eff = effectiveType(abs);
3790
3987
  if (eff === null) continue;
3791
3988
  const src = safeRead(abs);
@@ -3805,8 +4002,8 @@ function reportHookScopeCheck(section2) {
3805
4002
 
3806
4003
  // src/commands.doctor.checks.hooks.ts
3807
4004
  init_color();
3808
- import { existsSync as existsSync23 } from "node:fs";
3809
- import { join as join28 } from "node:path";
4005
+ import { existsSync as existsSync24 } from "node:fs";
4006
+ import { join as join29 } from "node:path";
3810
4007
  init_config();
3811
4008
  function expandHome(token) {
3812
4009
  const h2 = home();
@@ -3820,11 +4017,11 @@ function stripShellPunctuation(token) {
3820
4017
  return stripped.slice(0, end);
3821
4018
  }
3822
4019
  function* claudePathsIn(command) {
3823
- const claudePrefix = `${claudeHome()}/`;
4020
+ const claudePrefix = `${claudeHome().replaceAll("\\", "/")}/`;
3824
4021
  for (const segment of command.split(/&&|\|\||;|\|/)) {
3825
4022
  for (const raw of segment.trim().split(/\s+/).filter(Boolean)) {
3826
4023
  const expanded = expandHome(stripShellPunctuation(raw));
3827
- if (expanded.startsWith(claudePrefix)) yield expanded;
4024
+ if (expanded.replaceAll("\\", "/").startsWith(claudePrefix)) yield expanded;
3828
4025
  }
3829
4026
  }
3830
4027
  }
@@ -3849,7 +4046,7 @@ function checkEventGroups(section2, event, groups) {
3849
4046
  for (const group of groups) {
3850
4047
  for (const cmd of commandsFromGroup(group)) {
3851
4048
  for (const resolved of claudePathsIn(cmd)) {
3852
- if (existsSync23(resolved)) continue;
4049
+ if (existsSync24(resolved)) continue;
3853
4050
  addItem(
3854
4051
  section2,
3855
4052
  `${red(failGlyph)} hooks/${event}: command target missing: ${resolved} (run nomad pull)`
@@ -3862,8 +4059,8 @@ function checkEventGroups(section2, event, groups) {
3862
4059
  return anyFail;
3863
4060
  }
3864
4061
  function reportHooksTargetCheck(section2) {
3865
- const settingsPath = join28(claudeHome(), "settings.json");
3866
- if (!existsSync23(settingsPath)) {
4062
+ const settingsPath = join29(claudeHome(), "settings.json");
4063
+ if (!existsSync24(settingsPath)) {
3867
4064
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
3868
4065
  return;
3869
4066
  }
@@ -3886,12 +4083,12 @@ function reportHooksTargetCheck(section2) {
3886
4083
 
3887
4084
  // src/commands.doctor.checks.hooks.preserve-symlinks.ts
3888
4085
  init_color();
3889
- import { existsSync as existsSync25, readFileSync as readFileSync9 } from "node:fs";
3890
- import { join as join30 } from "node:path";
4086
+ import { existsSync as existsSync26, readFileSync as readFileSync9 } from "node:fs";
4087
+ import { join as join31 } from "node:path";
3891
4088
 
3892
4089
  // 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";
4090
+ import { closeSync as closeSync3, existsSync as existsSync25, openSync as openSync3, readSync, realpathSync as realpathSync3 } from "node:fs";
4091
+ import { dirname as dirname6, join as join30, resolve as resolve2 } from "node:path";
3895
4092
  function suppressedRanges(src) {
3896
4093
  const ranges = [];
3897
4094
  const re = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'[^']*'|"[^"]*"|`[^`]*`/g;
@@ -3923,12 +4120,12 @@ function topRelativeSpecifiers(src) {
3923
4120
  }
3924
4121
  function specifierIsMissing(specifier, baseDir) {
3925
4122
  const base = resolve2(baseDir, specifier);
3926
- if (existsSync24(base)) return false;
4123
+ if (existsSync25(base)) return false;
3927
4124
  for (const ext of [".js", ".cjs", ".mjs"]) {
3928
- if (existsSync24(base + ext)) return false;
4125
+ if (existsSync25(base + ext)) return false;
3929
4126
  }
3930
4127
  for (const idx of ["index.js", "index.cjs", "index.mjs"]) {
3931
- if (existsSync24(join29(base, idx))) return false;
4128
+ if (existsSync25(join30(base, idx))) return false;
3932
4129
  }
3933
4130
  return true;
3934
4131
  }
@@ -3983,8 +4180,8 @@ function commandTokens(command) {
3983
4180
  return tokens;
3984
4181
  }
3985
4182
  function readPathMapSafe() {
3986
- const mapPath = join30(repoHome(), "path-map.json");
3987
- if (!existsSync25(mapPath)) return { projects: {} };
4183
+ const mapPath = join31(repoHome(), "path-map.json");
4184
+ if (!existsSync26(mapPath)) return { projects: {} };
3988
4185
  try {
3989
4186
  return JSON.parse(readFileSync9(mapPath, "utf8"));
3990
4187
  } catch {
@@ -3992,9 +4189,10 @@ function readPathMapSafe() {
3992
4189
  }
3993
4190
  }
3994
4191
  function resolvesUnderSymlinkedShared(scriptPath, sharedLinkNames) {
4192
+ const normalizedScript = scriptPath.replaceAll("\\", "/");
3995
4193
  for (const name of sharedLinkNames) {
3996
- const prefix = `${claudeHome()}/${name}/`;
3997
- if (scriptPath.startsWith(prefix)) return true;
4194
+ const prefix = `${claudeHome().replaceAll("\\", "/")}/${name}/`;
4195
+ if (normalizedScript.startsWith(prefix)) return true;
3998
4196
  }
3999
4197
  return false;
4000
4198
  }
@@ -4057,8 +4255,8 @@ function checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames)
4057
4255
  return anyWarn;
4058
4256
  }
4059
4257
  function reportPreserveSymlinksCheck(section2) {
4060
- const settingsPath = join30(claudeHome(), "settings.json");
4061
- if (!existsSync25(settingsPath)) {
4258
+ const settingsPath = join31(claudeHome(), "settings.json");
4259
+ if (!existsSync26(settingsPath)) {
4062
4260
  addItem(
4063
4261
  section2,
4064
4262
  `${dim(infoGlyph)} no ~/.claude/settings.json; skipping preserve-symlinks-main check`
@@ -4091,8 +4289,8 @@ function reportPreserveSymlinksCheck(section2) {
4091
4289
 
4092
4290
  // src/commands.doctor.checks.settings-drift.ts
4093
4291
  init_color();
4094
- import { existsSync as existsSync26, readFileSync as readFileSync10 } from "node:fs";
4095
- import { join as join31 } from "node:path";
4292
+ import { existsSync as existsSync27, readFileSync as readFileSync10 } from "node:fs";
4293
+ import { join as join32 } from "node:path";
4096
4294
  init_config();
4097
4295
  init_utils_json();
4098
4296
  function diffMergedSettings(merged, settings) {
@@ -4110,7 +4308,7 @@ function tryReadJson(filePath) {
4110
4308
  }
4111
4309
  }
4112
4310
  function reportHooksBaseSelfCleanNote(section2) {
4113
- const basePath = join31(repoHome(), "shared", "settings.base.json");
4311
+ const basePath = join32(repoHome(), "shared", "settings.base.json");
4114
4312
  const base = tryReadJson(basePath);
4115
4313
  if (base === null) return;
4116
4314
  if (!baseHasGsdHookEntries(base)) return;
@@ -4123,14 +4321,14 @@ function reportSettingsDriftCheck(section2) {
4123
4321
  const claude = claudeHome();
4124
4322
  const repo = repoHome();
4125
4323
  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)) {
4324
+ const settingsPath = join32(claude, "settings.json");
4325
+ const basePath = join32(repo, "shared", "settings.base.json");
4326
+ const hostPath = join32(repo, "hosts", `${host}.json`);
4327
+ if (!existsSync27(settingsPath)) {
4130
4328
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping merge-drift check`);
4131
4329
  return;
4132
4330
  }
4133
- if (!existsSync26(basePath)) {
4331
+ if (!existsSync27(basePath)) {
4134
4332
  addItem(
4135
4333
  section2,
4136
4334
  `${dim(infoGlyph)} shared/settings.base.json missing; skipping merge-drift check`
@@ -4149,7 +4347,7 @@ function reportSettingsDriftCheck(section2) {
4149
4347
  if (settings === null) {
4150
4348
  return;
4151
4349
  }
4152
- const hostExists = existsSync26(hostPath);
4350
+ const hostExists = existsSync27(hostPath);
4153
4351
  const hostObj = hostExists ? tryReadJson(hostPath) : null;
4154
4352
  if (hostExists && hostObj === null) {
4155
4353
  addItem(
@@ -4305,46 +4503,152 @@ function reportNodeEngineCheck(section2) {
4305
4503
  addItem(section2, `${green(okGlyph)} node: ${process.version}`);
4306
4504
  }
4307
4505
 
4506
+ // src/commands.doctor.checks.longpaths.ts
4507
+ init_color();
4508
+ import { execFileSync as execFileSync10 } from "node:child_process";
4509
+ init_config();
4510
+ var PROBE_TIMEOUT_MS = 3e3;
4511
+ var LONGPATHS_REG_KEY = String.raw`HKLM\SYSTEM\CurrentControlSet\Control\FileSystem`;
4512
+ var LONGPATHS_REG_VALUE = "LongPathsEnabled";
4513
+ function probeGitLongpaths(run) {
4514
+ try {
4515
+ const out = run("git", ["-C", repoHome(), "config", "--get", "core.longpaths"], {
4516
+ stdio: ["ignore", "pipe", "pipe"],
4517
+ timeout: PROBE_TIMEOUT_MS
4518
+ }).toString().trim();
4519
+ return out === "true" || out === "1";
4520
+ } catch {
4521
+ return false;
4522
+ }
4523
+ }
4524
+ function probeRegistryLongpaths(run) {
4525
+ try {
4526
+ const out = run("reg", ["query", LONGPATHS_REG_KEY, "/v", LONGPATHS_REG_VALUE], {
4527
+ stdio: ["ignore", "pipe", "pipe"],
4528
+ timeout: PROBE_TIMEOUT_MS
4529
+ }).toString();
4530
+ return /0x1\b/.test(out);
4531
+ } catch {
4532
+ return false;
4533
+ }
4534
+ }
4535
+ function addLongpathsRow(section2, label, enabled2, remediation) {
4536
+ if (enabled2) {
4537
+ addItem(section2, `${green(okGlyph)} ${label}: enabled`);
4538
+ return;
4539
+ }
4540
+ addItem(section2, `${yellow(warnGlyph)} ${label}: not enabled (${remediation})`);
4541
+ }
4542
+ function reportLongPathsCheck(section2, run = execFileSync10) {
4543
+ if (process.platform !== "win32") return;
4544
+ addLongpathsRow(
4545
+ section2,
4546
+ "git core.longpaths",
4547
+ probeGitLongpaths(run),
4548
+ "run `git config --global core.longpaths true`"
4549
+ );
4550
+ addLongpathsRow(
4551
+ section2,
4552
+ "OS long paths",
4553
+ probeRegistryLongpaths(run),
4554
+ "enable LongPathsEnabled in Local Group Policy or the registry (admin required)"
4555
+ );
4556
+ }
4557
+ function reportSyncModality(section2) {
4558
+ const modality = process.platform === "win32" ? "copy-sync (win32)" : "symlink (posix)";
4559
+ addItem(section2, `${dim(infoGlyph)} sync modality: ${modality}`);
4560
+ }
4561
+
4562
+ // src/commands.doctor.checks.crlf.ts
4563
+ init_color();
4564
+ import { execFileSync as execFileSync11 } from "node:child_process";
4565
+ import { existsSync as existsSync28, readFileSync as readFileSync13 } from "node:fs";
4566
+ import { join as join33 } from "node:path";
4567
+ init_config();
4568
+ var PROBE_TIMEOUT_MS2 = 3e3;
4569
+ var GUARD_LINE = /^\*\s+-text\b/;
4570
+ function hasGitattributesGuard(repo) {
4571
+ try {
4572
+ const path = join33(repo, ".gitattributes");
4573
+ if (!existsSync28(path)) return false;
4574
+ const content = readFileSync13(path, "utf8");
4575
+ return content.split("\n").map((line) => line.trim()).some((line) => line !== "" && !line.startsWith("#") && GUARD_LINE.test(line));
4576
+ } catch {
4577
+ return false;
4578
+ }
4579
+ }
4580
+ function probeAutocrlf(repo, run) {
4581
+ try {
4582
+ const out = run("git", ["-C", repo, "config", "core.autocrlf"], {
4583
+ stdio: ["ignore", "pipe", "pipe"],
4584
+ timeout: PROBE_TIMEOUT_MS2
4585
+ }).toString().trim();
4586
+ if (out === "true" || out === "input") return "active";
4587
+ if (out === "false") return "disabled";
4588
+ return "unset";
4589
+ } catch {
4590
+ return "unset";
4591
+ }
4592
+ }
4593
+ var AUTOCRLF_RISK = {
4594
+ active: "core.autocrlf is actively converting line endings on checkout",
4595
+ disabled: "guarded on this host by core.autocrlf=false, but no .gitattributes to protect other hosts",
4596
+ unset: "no .gitattributes guard and core.autocrlf is unset (latent risk)"
4597
+ };
4598
+ function addExposedRow(section2, repo, verdict) {
4599
+ const risk = AUTOCRLF_RISK[verdict];
4600
+ const remediation = `add a .gitattributes with a \`* -text\` line, or run \`git config core.autocrlf false\`, in ${repo}`;
4601
+ addItem(section2, `${yellow(warnGlyph)} CRLF guard: ${risk}; ${remediation}`);
4602
+ }
4603
+ function reportCrlfGuardCheck(section2, run = execFileSync11) {
4604
+ const repo = repoHome();
4605
+ if (hasGitattributesGuard(repo)) {
4606
+ addItem(section2, `${green(okGlyph)} CRLF guard: .gitattributes (* -text) present`);
4607
+ return;
4608
+ }
4609
+ addExposedRow(section2, repo, probeAutocrlf(repo, run));
4610
+ }
4611
+
4308
4612
  // src/spinner.ts
4309
4613
  init_color();
4310
- import { existsSync as existsSync30 } from "node:fs";
4614
+ import { existsSync as existsSync32 } from "node:fs";
4311
4615
  import { fileURLToPath as fileURLToPath4 } from "node:url";
4312
4616
  import { Worker } from "node:worker_threads";
4313
4617
 
4314
4618
  // src/commands.push.recovery.ts
4315
4619
  init_config();
4316
- import { readFileSync as readFileSync15, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
4317
- import { join as join36 } from "node:path";
4620
+ import { readFileSync as readFileSync16, rmSync as rmSync12, writeFileSync as writeFileSync5 } from "node:fs";
4621
+ import { join as join38 } from "node:path";
4318
4622
  import { createInterface as createInterface2 } from "node:readline/promises";
4319
4623
 
4320
4624
  // src/commands.push.recovery.actions.ts
4321
4625
  init_config();
4322
- import { readFileSync as readFileSync14 } from "node:fs";
4323
- import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep5 } from "node:path";
4626
+ import { readFileSync as readFileSync15 } from "node:fs";
4627
+ import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep6 } from "node:path";
4324
4628
 
4325
4629
  // src/commands.push.recovery.redact.ts
4326
4630
  init_config();
4327
4631
  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";
4632
+ import { cpSync as cpSync7, existsSync as existsSync31, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
4633
+ import { dirname as dirname8, join as join36, sep as sep5 } from "node:path";
4330
4634
 
4331
4635
  // src/commands.redact.ts
4332
4636
  init_config();
4333
- import { existsSync as existsSync28, statSync as statSync7 } from "node:fs";
4334
- import { dirname as dirname7, join as join33 } from "node:path";
4637
+ import { existsSync as existsSync30, statSync as statSync7 } from "node:fs";
4638
+ import { dirname as dirname7, join as join35 } from "node:path";
4335
4639
 
4336
4640
  // 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";
4641
+ import { existsSync as existsSync29, lstatSync as lstatSync11, readFileSync as readFileSync14, readdirSync as readdirSync13, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
4642
+ import { join as join34 } from "node:path";
4339
4643
  init_utils_fs();
4340
4644
  init_utils();
4341
4645
  function collectFiles(dir, out) {
4342
- if (!existsSync27(dir)) return;
4343
- const st = lstatSync10(dir);
4646
+ if (!existsSync29(dir)) return;
4647
+ const st = lstatSync11(dir);
4344
4648
  if (!st.isDirectory()) return;
4345
- for (const entry of readdirSync12(dir)) {
4346
- const abs = join32(dir, entry);
4347
- const lst = lstatSync10(abs);
4649
+ for (const entry of readdirSync13(dir)) {
4650
+ const abs = join34(dir, entry);
4651
+ const lst = lstatSync11(abs);
4348
4652
  if (lst.isSymbolicLink()) continue;
4349
4653
  if (lst.isDirectory()) {
4350
4654
  collectFiles(abs, out);
@@ -4380,7 +4684,7 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
4380
4684
  if (!dryRun && total > 0) {
4381
4685
  for (const { path: filePath, findings } of dirty) {
4382
4686
  backupBeforeWrite(filePath, ts);
4383
- const before = readFileSync13(filePath, "utf8");
4687
+ const before = readFileSync14(filePath, "utf8");
4384
4688
  const after = applyRedactions(before, findings);
4385
4689
  if (after === before) {
4386
4690
  log(
@@ -4395,10 +4699,10 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
4395
4699
 
4396
4700
  // src/commands.pushed-history.ts
4397
4701
  init_utils();
4398
- import { execFileSync as execFileSync10 } from "node:child_process";
4702
+ import { execFileSync as execFileSync12 } from "node:child_process";
4399
4703
  function pushedRef(repo) {
4400
4704
  try {
4401
- const ref = execFileSync10("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
4705
+ const ref = execFileSync12("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
4402
4706
  cwd: repo,
4403
4707
  stdio: ["ignore", "pipe", "ignore"]
4404
4708
  }).toString().trim();
@@ -4411,7 +4715,7 @@ function sessionInPushedHistory(id, repo) {
4411
4715
  const ref = pushedRef(repo);
4412
4716
  if (ref === null) return false;
4413
4717
  try {
4414
- const out = execFileSync10(
4718
+ const out = execFileSync12(
4415
4719
  "git",
4416
4720
  [
4417
4721
  "log",
@@ -4448,15 +4752,15 @@ init_utils_json();
4448
4752
  init_utils();
4449
4753
  function resolveLiveTranscript(id) {
4450
4754
  try {
4451
- const mapPath = join33(repoHome(), "path-map.json");
4452
- if (!existsSync28(mapPath)) return null;
4755
+ const mapPath = join35(repoHome(), "path-map.json");
4756
+ if (!existsSync30(mapPath)) return null;
4453
4757
  const projects = readJson(mapPath).projects;
4454
4758
  const claude = claudeHome();
4455
4759
  for (const hostMap of Object.values(projects)) {
4456
4760
  const abs = hostMap[HOST];
4457
4761
  if (abs === void 0) continue;
4458
- const live = join33(claude, "projects", encodePath(abs), `${id}.jsonl`);
4459
- if (existsSync28(live)) return live;
4762
+ const live = join35(claude, "projects", encodePath(abs), `${id}.jsonl`);
4763
+ if (existsSync30(live)) return live;
4460
4764
  }
4461
4765
  return null;
4462
4766
  } catch {
@@ -4476,17 +4780,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
4476
4780
  }
4477
4781
  const repo = repoHome();
4478
4782
  const backup = backupBase();
4479
- if (!existsSync28(repo)) die(`repo not cloned at ${repo}`);
4783
+ if (!existsSync30(repo)) die(`repo not cloned at ${repo}`);
4480
4784
  const handle = acquireLock("redact");
4481
4785
  if (handle === null) process.exit(0);
4482
4786
  try {
4483
4787
  const localPath = resolveLiveTranscript(id);
4484
- if (localPath === null || !existsSync28(localPath)) {
4788
+ if (localPath === null || !existsSync30(localPath)) {
4485
4789
  fail(`could not resolve local transcript for session ${id} on this host`);
4486
4790
  process.exitCode = 1;
4487
4791
  return;
4488
4792
  }
4489
- const sessionDir = join33(dirname7(localPath), id);
4793
+ const sessionDir = join35(dirname7(localPath), id);
4490
4794
  const subtreeFiles = listSubtreeFiles(sessionDir);
4491
4795
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
4492
4796
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -4602,8 +4906,8 @@ function resolveStagedDir(localPath, map, claude, repo) {
4602
4906
  assertSafeLogical(logical);
4603
4907
  const abs = hostMap[HOST];
4604
4908
  if (abs === void 0) continue;
4605
- if (localPath.startsWith(join34(claude, "projects", encodePath(abs)) + sep4)) {
4606
- return join34(repo, "shared", "projects", logical);
4909
+ if (localPath.startsWith(join36(claude, "projects", encodePath(abs)) + sep5)) {
4910
+ return join36(repo, "shared", "projects", logical);
4607
4911
  }
4608
4912
  }
4609
4913
  return null;
@@ -4613,7 +4917,7 @@ function preflightRedactable(f, map, nowMs) {
4613
4917
  if (sid === null) return "a finding has no resolvable session id (not a session transcript)";
4614
4918
  const localPath = resolveLiveTranscript(sid);
4615
4919
  if (localPath === null) return `session ${sid}: local transcript not found`;
4616
- const sessionDir = join34(dirname8(localPath), sid);
4920
+ const sessionDir = join36(dirname8(localPath), sid);
4617
4921
  const subtreeFiles = listSubtreeFiles(sessionDir);
4618
4922
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
4619
4923
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -4643,7 +4947,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4643
4947
  `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
4644
4948
  );
4645
4949
  }
4646
- const sessionDir = join34(dirname8(localPath), sid);
4950
+ const sessionDir = join36(dirname8(localPath), sid);
4647
4951
  const subtreeFiles = listSubtreeFiles(sessionDir);
4648
4952
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
4649
4953
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -4677,26 +4981,26 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4677
4981
  );
4678
4982
  }
4679
4983
  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 });
4984
+ cpSync7(localPath, join36(stagedProjectDir, `${sid}.jsonl`), { force: true });
4985
+ if (existsSync31(sessionDir)) {
4986
+ cpSync7(sessionDir, join36(stagedProjectDir, sid), { force: true, recursive: true });
4683
4987
  }
4684
4988
  return true;
4685
4989
  }
4686
4990
 
4687
4991
  // src/commands.push.recovery.drop.ts
4688
4992
  init_config();
4689
- import { rmSync as rmSync10 } from "node:fs";
4690
- import { join as join35 } from "node:path";
4993
+ import { rmSync as rmSync11 } from "node:fs";
4994
+ import { join as join37 } from "node:path";
4691
4995
  function dropSessionFromStaged(sid, map) {
4692
4996
  const logicals = Object.keys(map.projects);
4693
4997
  if (logicals.length === 0) return false;
4694
4998
  const repo = repoHome();
4695
4999
  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 });
5000
+ const jsonl = join37(repo, "shared", "projects", logical, `${sid}.jsonl`);
5001
+ const dir = join37(repo, "shared", "projects", logical, sid);
5002
+ rmSync11(jsonl, { force: true });
5003
+ rmSync11(dir, { recursive: true, force: true });
4700
5004
  }
4701
5005
  return true;
4702
5006
  }
@@ -4727,10 +5031,10 @@ function makeDefaultReadLine(repo) {
4727
5031
  try {
4728
5032
  const repoRoot = resolve3(repo);
4729
5033
  const target = resolve3(repoRoot, file);
4730
- if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep5)) {
5034
+ if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep6)) {
4731
5035
  return null;
4732
5036
  }
4733
- const content = readFileSync14(target, "utf8");
5037
+ const content = readFileSync15(target, "utf8");
4734
5038
  const lines = content.split(/\r?\n/);
4735
5039
  const idx = line - 1;
4736
5040
  if (idx < 0 || idx >= lines.length) return null;
@@ -4851,10 +5155,10 @@ function applyThenRescan(scanVerdict, repoHome2) {
4851
5155
  return next;
4852
5156
  }
4853
5157
  function allowThenRescan(append, scanVerdict, repoHome2) {
4854
- const ignPath = join36(repoHome2, ".gitleaksignore");
5158
+ const ignPath = join38(repoHome2, ".gitleaksignore");
4855
5159
  let before;
4856
5160
  try {
4857
- before = readFileSync15(ignPath, "utf8");
5161
+ before = readFileSync16(ignPath, "utf8");
4858
5162
  } catch {
4859
5163
  before = null;
4860
5164
  }
@@ -4862,7 +5166,7 @@ function allowThenRescan(append, scanVerdict, repoHome2) {
4862
5166
  try {
4863
5167
  return applyThenRescan(scanVerdict, repoHome2);
4864
5168
  } catch (err) {
4865
- if (before === null) rmSync11(ignPath, { force: true });
5169
+ if (before === null) rmSync12(ignPath, { force: true });
4866
5170
  else writeFileSync5(ignPath, before, "utf8");
4867
5171
  throw err;
4868
5172
  }
@@ -4950,7 +5254,7 @@ function writeAnimatedDone(out, label, ms, useTTY) {
4950
5254
  `);
4951
5255
  }
4952
5256
  function resolveWorkerPath(deps = {}) {
4953
- const check = deps.existsSyncFn ?? existsSync30;
5257
+ const check = deps.existsSyncFn ?? existsSync32;
4954
5258
  const base = deps.baseUrl ?? import.meta.url;
4955
5259
  const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
4956
5260
  if (check(mjs)) return mjs;
@@ -5016,9 +5320,9 @@ function withSpinner(label, fn, deps) {
5016
5320
 
5017
5321
  // src/commands.doctor.gitleaks-version.ts
5018
5322
  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";
5323
+ import { execFileSync as execFileSync13 } from "node:child_process";
5324
+ import { existsSync as existsSync33 } from "node:fs";
5325
+ import { join as join39 } from "node:path";
5022
5326
  init_config();
5023
5327
  var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
5024
5328
  var GITLEAKS_TIMEOUT_MS = 5e3;
@@ -5027,7 +5331,7 @@ function majorMinorOf(value) {
5027
5331
  return m === null ? null : [m[1], m[2]];
5028
5332
  }
5029
5333
  function readGitleaksVersion(run, tomlExists) {
5030
- const tomlPath = join37(repoHome(), ".gitleaks.toml");
5334
+ const tomlPath = join39(repoHome(), ".gitleaks.toml");
5031
5335
  const args = ["version"];
5032
5336
  if (tomlExists(tomlPath)) args.push("--config", tomlPath);
5033
5337
  try {
@@ -5039,7 +5343,7 @@ function readGitleaksVersion(run, tomlExists) {
5039
5343
  return null;
5040
5344
  }
5041
5345
  }
5042
- function reportGitleaksVersionCheck(section2, run = execFileSync11, tomlExists = existsSync31) {
5346
+ function reportGitleaksVersionCheck(section2, run = execFileSync13, tomlExists = existsSync33) {
5043
5347
  const raw = readGitleaksVersion(run, tomlExists);
5044
5348
  if (raw === null) return;
5045
5349
  const local = majorMinorOf(raw);
@@ -5059,9 +5363,9 @@ function reportGitleaksVersionCheck(section2, run = execFileSync11, tomlExists =
5059
5363
 
5060
5364
  // src/commands.doctor.checks.deps.ts
5061
5365
  init_color();
5062
- import { execFileSync as execFileSync12 } from "node:child_process";
5366
+ import { execFileSync as execFileSync14 } from "node:child_process";
5063
5367
  var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
5064
- var PROBE_TIMEOUT_MS = 3e3;
5368
+ var PROBE_TIMEOUT_MS3 = 3e3;
5065
5369
  var FETCHER_BASE = "HTTP fetcher";
5066
5370
  function parseFirstVersion(line) {
5067
5371
  const m = VERSION_TOKEN.exec(line);
@@ -5071,7 +5375,7 @@ function probeOptionalDep(bin, run) {
5071
5375
  try {
5072
5376
  const firstLine = run(bin, ["--version"], {
5073
5377
  stdio: ["ignore", "pipe", "pipe"],
5074
- timeout: PROBE_TIMEOUT_MS
5378
+ timeout: PROBE_TIMEOUT_MS3
5075
5379
  }).toString().split("\n")[0].trim();
5076
5380
  const version = parseFirstVersion(firstLine);
5077
5381
  return { status: "present", version };
@@ -5096,7 +5400,7 @@ function reportFetcherRow(section2, run) {
5096
5400
  );
5097
5401
  }
5098
5402
  }
5099
- function reportOptionalDeps(section2, run = execFileSync12) {
5403
+ function reportOptionalDeps(section2, run = execFileSync14) {
5100
5404
  const gh = probeOptionalDep("gh", run);
5101
5405
  if (gh.status === "present") {
5102
5406
  addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
@@ -5111,11 +5415,11 @@ function reportOptionalDeps(section2, run = execFileSync12) {
5111
5415
 
5112
5416
  // src/commands.doctor.actions-drift.ts
5113
5417
  init_color();
5114
- import { execFileSync as execFileSync14 } from "node:child_process";
5418
+ import { execFileSync as execFileSync16 } from "node:child_process";
5115
5419
  init_config();
5116
5420
 
5117
5421
  // src/gh-actions.ts
5118
- import { execFileSync as execFileSync13 } from "node:child_process";
5422
+ import { execFileSync as execFileSync15 } from "node:child_process";
5119
5423
  var GH_TIMEOUT_MS = 5e3;
5120
5424
  function parseGitHubRemote(remoteUrl) {
5121
5425
  const normalized = remoteUrl.trim().replace(/\/$/, "");
@@ -5123,7 +5427,7 @@ function parseGitHubRemote(remoteUrl) {
5123
5427
  if (m === null) return null;
5124
5428
  return { owner: m[1], repo: m[2] };
5125
5429
  }
5126
- function ghAuthStatus(run = execFileSync13) {
5430
+ function ghAuthStatus(run = execFileSync15) {
5127
5431
  try {
5128
5432
  run("gh", ["auth", "status"], {
5129
5433
  stdio: ["ignore", "ignore", "ignore"],
@@ -5137,7 +5441,7 @@ function ghAuthStatus(run = execFileSync13) {
5137
5441
  return "gh-probe-error";
5138
5442
  }
5139
5443
  }
5140
- function isRepoPrivate(ref, run = execFileSync13) {
5444
+ function isRepoPrivate(ref, run = execFileSync15) {
5141
5445
  const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
5142
5446
  stdio: ["ignore", "pipe", "ignore"],
5143
5447
  timeout: GH_TIMEOUT_MS
@@ -5145,7 +5449,7 @@ function isRepoPrivate(ref, run = execFileSync13) {
5145
5449
  const parsed = JSON.parse(out);
5146
5450
  return parsed.isPrivate === true;
5147
5451
  }
5148
- function isActionsEnabled(ref, run = execFileSync13) {
5452
+ function isActionsEnabled(ref, run = execFileSync15) {
5149
5453
  const out = run(
5150
5454
  "gh",
5151
5455
  ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
@@ -5153,7 +5457,7 @@ function isActionsEnabled(ref, run = execFileSync13) {
5153
5457
  ).toString().trim();
5154
5458
  return out === "true";
5155
5459
  }
5156
- function disableActions(ref, run = execFileSync13) {
5460
+ function disableActions(ref, run = execFileSync15) {
5157
5461
  run(
5158
5462
  "gh",
5159
5463
  [
@@ -5167,7 +5471,7 @@ function disableActions(ref, run = execFileSync13) {
5167
5471
  { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
5168
5472
  );
5169
5473
  }
5170
- function readOriginRemote(cwd, run = execFileSync13) {
5474
+ function readOriginRemote(cwd, run = execFileSync15) {
5171
5475
  return run("git", ["remote", "get-url", "origin"], {
5172
5476
  cwd,
5173
5477
  stdio: ["ignore", "pipe", "ignore"]
@@ -5175,7 +5479,7 @@ function readOriginRemote(cwd, run = execFileSync13) {
5175
5479
  }
5176
5480
 
5177
5481
  // src/commands.doctor.actions-drift.ts
5178
- function reportActionsDrift(section2, run = execFileSync14) {
5482
+ function reportActionsDrift(section2, run = execFileSync16) {
5179
5483
  let remote;
5180
5484
  try {
5181
5485
  remote = readOriginRemote(repoHome(), run);
@@ -5261,9 +5565,12 @@ function gatherDoctorSections(opts) {
5261
5565
  reportHostAndPaths(host);
5262
5566
  reportHostKeyAlignment(host);
5263
5567
  reportRepoState(host);
5568
+ reportSyncModality(host);
5569
+ reportLongPathsCheck(host);
5570
+ reportCrlfGuardCheck(host);
5264
5571
  const links = section("Shared links");
5265
- const mapPath = join38(repoHome(), "path-map.json");
5266
- const rawMap = existsSync32(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
5572
+ const mapPath = join40(repoHome(), "path-map.json");
5573
+ const rawMap = existsSync34(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
5267
5574
  const map = rawMap ?? { projects: {} };
5268
5575
  reportSharedLinks(links, map);
5269
5576
  reportDroppedNamesMigration(links);
@@ -5359,15 +5666,15 @@ function parseDoctorArgs(args) {
5359
5666
 
5360
5667
  // src/commands.drop-session.ts
5361
5668
  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";
5669
+ import { execFileSync as execFileSync18 } from "node:child_process";
5670
+ import { existsSync as existsSync36, readdirSync as readdirSync14, statSync as statSync9 } from "node:fs";
5671
+ import { join as join42, relative as relative6 } from "node:path";
5365
5672
 
5366
5673
  // src/commands.drop-session.git.ts
5367
- import { execFileSync as execFileSync15 } from "node:child_process";
5674
+ import { execFileSync as execFileSync17 } from "node:child_process";
5368
5675
  function expandStagedDir(dirRel, repo) {
5369
5676
  try {
5370
- const out = execFileSync15("git", ["ls-files", "-z", "--", dirRel], {
5677
+ const out = execFileSync17("git", ["ls-files", "-z", "--", dirRel], {
5371
5678
  cwd: repo,
5372
5679
  stdio: ["ignore", "pipe", "pipe"]
5373
5680
  });
@@ -5378,7 +5685,8 @@ function expandStagedDir(dirRel, repo) {
5378
5685
  }
5379
5686
  function isTrackedInHead(rel, repo) {
5380
5687
  try {
5381
- execFileSync15("git", ["cat-file", "-e", `HEAD:${rel}`], {
5688
+ const treePath = process.platform === "win32" ? rel.replaceAll("\\", "/") : rel;
5689
+ execFileSync17("git", ["cat-file", "-e", `HEAD:${treePath}`], {
5382
5690
  cwd: repo,
5383
5691
  stdio: ["ignore", "pipe", "pipe"]
5384
5692
  });
@@ -5389,7 +5697,7 @@ function isTrackedInHead(rel, repo) {
5389
5697
  }
5390
5698
  function isInIndex(rel, repo) {
5391
5699
  try {
5392
- const out = execFileSync15("git", ["ls-files", "--", rel], {
5700
+ const out = execFileSync17("git", ["ls-files", "--", rel], {
5393
5701
  cwd: repo,
5394
5702
  stdio: ["ignore", "pipe", "pipe"]
5395
5703
  });
@@ -5403,8 +5711,8 @@ function isInIndex(rel, repo) {
5403
5711
  init_config();
5404
5712
  init_utils();
5405
5713
  init_utils_json();
5406
- import { existsSync as existsSync33 } from "node:fs";
5407
- import { join as join39 } from "node:path";
5714
+ import { existsSync as existsSync35 } from "node:fs";
5715
+ import { join as join41 } from "node:path";
5408
5716
  var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
5409
5717
  function reportScrubHint(id, matches) {
5410
5718
  const live = resolveLiveTranscript2(id, matches);
@@ -5420,17 +5728,17 @@ function reportScrubHint(id, matches) {
5420
5728
  }
5421
5729
  function resolveLiveTranscript2(id, matches) {
5422
5730
  try {
5423
- const mapPath = join39(repoHome(), "path-map.json");
5424
- if (!existsSync33(mapPath)) return null;
5731
+ const mapPath = join41(repoHome(), "path-map.json");
5732
+ if (!existsSync35(mapPath)) return null;
5425
5733
  const projects = readJson(mapPath).projects;
5426
5734
  const claude = claudeHome();
5427
5735
  for (const rel of matches) {
5428
- const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
5736
+ const logical = SHARED_PROJECT_LOGICAL.exec(rel.replaceAll("\\", "/"))?.[1];
5429
5737
  if (logical === void 0) continue;
5430
5738
  const abs = projects[logical]?.[HOST];
5431
5739
  if (abs === void 0) continue;
5432
- const live = join39(claude, "projects", encodePath(abs), `${id}.jsonl`);
5433
- if (existsSync33(live)) return live;
5740
+ const live = join41(claude, "projects", encodePath(abs), `${id}.jsonl`);
5741
+ if (existsSync35(live)) return live;
5434
5742
  }
5435
5743
  return null;
5436
5744
  } catch {
@@ -5446,12 +5754,12 @@ function cmdDropSession(id) {
5446
5754
  process.exit(1);
5447
5755
  }
5448
5756
  const repo = repoHome();
5449
- if (!existsSync34(repo)) die(`repo not cloned at ${repo}`);
5757
+ if (!existsSync36(repo)) die(`repo not cloned at ${repo}`);
5450
5758
  const handle = acquireLock("drop-session");
5451
5759
  if (handle === null) process.exit(0);
5452
5760
  try {
5453
- const repoProjects = join40(repo, "shared", "projects");
5454
- if (!existsSync34(repoProjects)) {
5761
+ const repoProjects = join42(repo, "shared", "projects");
5762
+ if (!existsSync36(repoProjects)) {
5455
5763
  throw new NomadFatal(`no staged session matches ${id}`);
5456
5764
  }
5457
5765
  const matches = collectMatches(repoProjects, id, repo);
@@ -5473,13 +5781,13 @@ function cmdDropSession(id) {
5473
5781
  }
5474
5782
  function collectMatches(repoProjects, id, repo) {
5475
5783
  const matches = [];
5476
- for (const logical of readdirSync13(repoProjects)) {
5477
- const candidate = join40(repoProjects, logical, `${id}.jsonl`);
5478
- if (existsSync34(candidate)) {
5784
+ for (const logical of readdirSync14(repoProjects)) {
5785
+ const candidate = join42(repoProjects, logical, `${id}.jsonl`);
5786
+ if (existsSync36(candidate)) {
5479
5787
  matches.push(relative6(repo, candidate));
5480
5788
  }
5481
- const dir = join40(repoProjects, logical, id);
5482
- if (existsSync34(dir) && statSync9(dir).isDirectory()) {
5789
+ const dir = join42(repoProjects, logical, id);
5790
+ if (existsSync36(dir) && statSync9(dir).isDirectory()) {
5483
5791
  const dirRel = relative6(repo, dir);
5484
5792
  const staged = expandStagedDir(dirRel, repo);
5485
5793
  if (staged.length > 0) matches.push(...staged);
@@ -5495,12 +5803,12 @@ function unstageOne(rel, repo) {
5495
5803
  }
5496
5804
  try {
5497
5805
  if (isTrackedInHead(rel, repo)) {
5498
- execFileSync16("git", ["restore", "--staged", "--worktree", "--", rel], {
5806
+ execFileSync18("git", ["restore", "--staged", "--worktree", "--", rel], {
5499
5807
  cwd: repo,
5500
5808
  stdio: ["ignore", "pipe", "pipe"]
5501
5809
  });
5502
5810
  } else {
5503
- execFileSync16("git", ["rm", "--cached", "-f", "--", rel], {
5811
+ execFileSync18("git", ["rm", "--cached", "-f", "--", rel], {
5504
5812
  cwd: repo,
5505
5813
  stdio: ["ignore", "pipe", "pipe"]
5506
5814
  });
@@ -5514,8 +5822,8 @@ function unstageOne(rel, repo) {
5514
5822
  }
5515
5823
 
5516
5824
  // src/commands.pull.ts
5517
- import { existsSync as existsSync39, mkdirSync as mkdirSync10 } from "node:fs";
5518
- import { join as join46 } from "node:path";
5825
+ import { existsSync as existsSync41, mkdirSync as mkdirSync10 } from "node:fs";
5826
+ import { join as join48 } from "node:path";
5519
5827
 
5520
5828
  // src/commands.push.sections.ts
5521
5829
  init_color();
@@ -5597,19 +5905,25 @@ function summarySection(st) {
5597
5905
  addItem(s, summaryRow("push", unmapped, st.remap.collisions, st.extras.skipped));
5598
5906
  return s;
5599
5907
  }
5600
- function renderPushTree(st, verdict) {
5908
+ function buildPushTreeSections(st, verdict) {
5601
5909
  const leakScan = section("Leak scan");
5602
5910
  addItem(leakScan, verdict.verdictRow);
5603
- renderTree([...syncedSections(st), leakScan, summarySection(st)]);
5911
+ return [...syncedSections(st), leakScan, summarySection(st)];
5604
5912
  }
5605
- function renderNoScanTree(st, opts = {}) {
5913
+ function renderPushTree(st, verdict) {
5914
+ renderTree(buildPushTreeSections(st, verdict));
5915
+ }
5916
+ function buildNoScanSections(st, opts = {}) {
5606
5917
  const sections = [];
5607
5918
  if (opts.noMapHint === true) {
5608
5919
  const pathMap = section("Path map");
5609
5920
  addItem(pathMap, `${dim(infoGlyph)} no path-map.json (nothing to preview)`);
5610
5921
  sections.push(pathMap);
5611
5922
  }
5612
- renderTree([...sections, ...syncedSections(st), summarySection(st)]);
5923
+ return [...sections, ...syncedSections(st), summarySection(st)];
5924
+ }
5925
+ function renderNoScanTree(st, opts = {}) {
5926
+ renderTree(buildNoScanSections(st, opts));
5613
5927
  }
5614
5928
 
5615
5929
  // src/commands.pull.ts
@@ -5617,18 +5931,18 @@ init_config();
5617
5931
 
5618
5932
  // src/extras-sync.ts
5619
5933
  init_config();
5620
- import { existsSync as existsSync36, statSync as statSync10 } from "node:fs";
5621
- import { join as join43 } from "node:path";
5934
+ import { existsSync as existsSync38, statSync as statSync10 } from "node:fs";
5935
+ import { join as join45 } from "node:path";
5622
5936
  init_utils();
5623
5937
  init_utils_json();
5624
5938
 
5625
5939
  // src/extras-sync.remap.ts
5626
5940
  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";
5941
+ import { existsSync as existsSync37, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync17, realpathSync as realpathSync4, rmSync as rmSync13 } from "node:fs";
5942
+ import { dirname as dirname9, join as join44, sep as sep8 } from "node:path";
5629
5943
 
5630
5944
  // src/extras-sync.planning-diff.ts
5631
- import { join as join41, normalize as normalize2, sep as sep6 } from "node:path";
5945
+ import { join as join43, normalize as normalize2, sep as sep7 } from "node:path";
5632
5946
  init_utils();
5633
5947
  function processRecord(fields, i, changed, deleted) {
5634
5948
  const status = fields[i];
@@ -5680,15 +5994,15 @@ function planningDeleteTargets(opts) {
5680
5994
  const { deleted } = parsePlanningDiff(raw);
5681
5995
  const logicalPrefix = "shared/extras/" + logical + "/";
5682
5996
  const prefix = logicalPrefix + ".planning/";
5683
- const planningRoot = join41(localRoot, ".planning");
5684
- const planningRootBoundary = planningRoot + sep6;
5997
+ const planningRoot = join43(localRoot, ".planning");
5998
+ const planningRootBoundary = planningRoot + sep7;
5685
5999
  const targets = [];
5686
6000
  for (const repoPath of deleted) {
5687
6001
  if (!repoPath.startsWith(prefix)) {
5688
6002
  continue;
5689
6003
  }
5690
6004
  const remainder = repoPath.slice(logicalPrefix.length);
5691
- const candidate = join41(localRoot, remainder);
6005
+ const candidate = join43(localRoot, remainder);
5692
6006
  const resolved = normalize2(candidate);
5693
6007
  if (!resolved.startsWith(planningRootBoundary)) {
5694
6008
  throw new NomadFatal(
@@ -5709,7 +6023,7 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
5709
6023
  const would = [];
5710
6024
  for (const t of eachExtrasTarget(v, counts)) {
5711
6025
  const { src, dst } = paths(t);
5712
- if (!existsSync35(src)) continue;
6026
+ if (!existsSync37(src)) continue;
5713
6027
  const item2 = `${t.logical}/${t.dirname}`;
5714
6028
  if (dryRun) {
5715
6029
  would.push(item2);
@@ -5723,10 +6037,10 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
5723
6037
  }
5724
6038
  function pruneEmptyAncestors(target, planningRoot) {
5725
6039
  let dir = dirname9(target);
5726
- while (dir !== planningRoot && dir.startsWith(planningRoot + sep7)) {
6040
+ while (dir !== planningRoot && dir.startsWith(planningRoot + sep8)) {
5727
6041
  try {
5728
- if (readdirSync14(dir).length > 0) break;
5729
- rmSync12(dir, { recursive: true, force: true });
6042
+ if (readdirSync15(dir).length > 0) break;
6043
+ rmSync13(dir, { recursive: true, force: true });
5730
6044
  } catch {
5731
6045
  break;
5732
6046
  }
@@ -5741,23 +6055,23 @@ function tryRealpath(dir) {
5741
6055
  }
5742
6056
  }
5743
6057
  function isInsidePlanningRoot(parentReal, rootReal) {
5744
- return parentReal === rootReal || parentReal.startsWith(rootReal + sep7);
6058
+ return parentReal === rootReal || parentReal.startsWith(rootReal + sep8);
5745
6059
  }
5746
6060
  function deletePlanningTarget(target, planningRoot, repoCounterpart) {
5747
- if (existsSync35(repoCounterpart)) return;
6061
+ if (existsSync37(repoCounterpart)) return;
5748
6062
  const parentReal = tryRealpath(dirname9(target));
5749
6063
  if (parentReal === void 0) return;
5750
6064
  const rootReal = tryRealpath(planningRoot);
5751
6065
  if (rootReal === void 0) return;
5752
6066
  if (!isInsidePlanningRoot(parentReal, rootReal)) return;
5753
- rmSync12(target, { recursive: true, force: true });
6067
+ rmSync13(target, { recursive: true, force: true });
5754
6068
  pruneEmptyAncestors(target, planningRoot);
5755
6069
  }
5756
6070
  function localDivergesFromPreDelete(target, pre, repoRel, repo) {
5757
- if (!existsSync35(target)) return false;
6071
+ if (!existsSync37(target)) return false;
5758
6072
  try {
5759
6073
  const preBlob = gitCaptureBuffer(["show", `${pre}:${repoRel}`], repo);
5760
- return !readFileSync16(target).equals(preBlob);
6074
+ return !readFileSync17(target).equals(preBlob);
5761
6075
  } catch {
5762
6076
  return true;
5763
6077
  }
@@ -5768,11 +6082,11 @@ function planningDiffArgs(pre, post, logical) {
5768
6082
  function deletePairsFor(t, raw) {
5769
6083
  return planningDeleteTargets({ raw, logical: t.logical, localRoot: t.localRoot }).map(
5770
6084
  (target) => {
5771
- const relToLocal = target.slice(t.localRoot.length + sep7.length);
6085
+ const relToLocal = target.slice(t.localRoot.length + sep8.length);
5772
6086
  return {
5773
6087
  target,
5774
6088
  relToLocal,
5775
- repoRel: `shared/extras/${t.logical}/${relToLocal.split(sep7).join("/")}`
6089
+ repoRel: `shared/extras/${t.logical}/${relToLocal.split(sep8).join("/")}`
5776
6090
  };
5777
6091
  }
5778
6092
  );
@@ -5799,7 +6113,7 @@ function keptDeletePreview(v, prePostHeads, repo) {
5799
6113
  return kept;
5800
6114
  }
5801
6115
  function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
5802
- const repoExtras = join42(repo, "shared", "extras");
6116
+ const repoExtras = join44(repo, "shared", "extras");
5803
6117
  for (const t of eachExtrasTarget(v, { unmapped: 0, skipped: 0 })) {
5804
6118
  if (t.dirname !== ".planning") continue;
5805
6119
  let raw;
@@ -5814,14 +6128,14 @@ function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
5814
6128
  }
5815
6129
  const pairs = deletePairsFor(t, raw);
5816
6130
  if (pairs.length === 0) continue;
5817
- backupExtrasWrite(join42(t.localRoot, t.dirname), ts, t.localRoot);
5818
- const planningRoot = join42(t.localRoot, ".planning");
6131
+ backupExtrasWrite(join44(t.localRoot, t.dirname), ts, t.localRoot);
6132
+ const planningRoot = join44(t.localRoot, ".planning");
5819
6133
  for (const { target, relToLocal, repoRel } of pairs) {
5820
6134
  if (localDivergesFromPreDelete(target, prePostHeads.pre, repoRel, repo)) {
5821
6135
  warn(keptDeleteWarnLine(t.logical, relToLocal));
5822
6136
  continue;
5823
6137
  }
5824
- deletePlanningTarget(target, planningRoot, join42(repoExtras, t.logical, relToLocal));
6138
+ deletePlanningTarget(target, planningRoot, join44(repoExtras, t.logical, relToLocal));
5825
6139
  }
5826
6140
  }
5827
6141
  }
@@ -5830,14 +6144,14 @@ function remapExtrasPush(ts, opts = {}) {
5830
6144
  const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
5831
6145
  if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
5832
6146
  const repo = repoHome();
5833
- const repoExtras = join42(repo, "shared", "extras");
6147
+ const repoExtras = join44(repo, "shared", "extras");
5834
6148
  if (!dryRun) mkdirSync8(repoExtras, { recursive: true });
5835
6149
  const { unmapped, skipped, done, would } = runExtrasOp(
5836
6150
  v,
5837
6151
  dryRun,
5838
6152
  ({ localRoot, logical, dirname: dirname10 }) => ({
5839
- src: join42(localRoot, dirname10),
5840
- dst: join42(repoExtras, logical, dirname10)
6153
+ src: join44(localRoot, dirname10),
6154
+ dst: join44(repoExtras, logical, dirname10)
5841
6155
  }),
5842
6156
  (dst) => backupRepoWrite(dst, ts, repo),
5843
6157
  // Push copy routing per extra type:
@@ -5866,8 +6180,8 @@ function remapExtrasPull(ts, opts = {}) {
5866
6180
  v,
5867
6181
  dryRun,
5868
6182
  ({ localRoot, logical, dirname: dirname10 }) => ({
5869
- src: join42(repo, "shared", "extras", logical, dirname10),
5870
- dst: join42(localRoot, dirname10)
6183
+ src: join44(repo, "shared", "extras", logical, dirname10),
6184
+ dst: join44(localRoot, dirname10)
5871
6185
  }),
5872
6186
  // Snapshot the host-side dst BEFORE the copy step touches it. Anchor on
5873
6187
  // localRoot so the backup tree mirrors the project layout.
@@ -5914,17 +6228,17 @@ function divergenceCheckExtras(ts, prePostHeads) {
5914
6228
  const v = loadValidatedExtras({});
5915
6229
  if (v === null) return 0;
5916
6230
  const counts = { unmapped: 0, skipped: 0 };
5917
- const backupRoot = join43(backupBase(), ts, "extras");
6231
+ const backupRoot = join45(backupBase(), ts, "extras");
5918
6232
  const repo = repoHome();
5919
6233
  let divergedCount = 0;
5920
6234
  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;
6235
+ const local = join45(localRoot, dirname10);
6236
+ const repoEntry = join45(repo, "shared", "extras", logical, dirname10);
6237
+ if (!existsSync38(local) || !existsSync38(repoEntry)) continue;
5924
6238
  const modified = listDivergingModified(local, repoEntry);
5925
6239
  if (modified.length === 0) continue;
5926
6240
  divergedCount += modified.length;
5927
- const projectBackupRoot = join43(backupRoot, encodePath(localRoot));
6241
+ const projectBackupRoot = join45(backupRoot, encodePath(localRoot));
5928
6242
  warn(
5929
6243
  divergenceWarnLine({
5930
6244
  dirname: dirname10,
@@ -5946,8 +6260,8 @@ function divergenceCheckExtras(ts, prePostHeads) {
5946
6260
 
5947
6261
  // src/skills-sync.ts
5948
6262
  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";
6263
+ import { existsSync as existsSync39, lstatSync as lstatSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync16, rmSync as rmSync14 } from "node:fs";
6264
+ import { join as join46 } from "node:path";
5951
6265
  init_utils_fs();
5952
6266
  function isGsdOwned(name) {
5953
6267
  return name.startsWith(GSD_PREFIX);
@@ -5956,7 +6270,7 @@ function isSkillExcluded(name) {
5956
6270
  return isGsdOwned(name) || isDeniedName(ALWAYS_NEVER_SYNC, name);
5957
6271
  }
5958
6272
  function copySkillsPush(src, dst) {
5959
- const srcNames = readdirSync15(src, { encoding: "utf8" });
6273
+ const srcNames = readdirSync16(src, { encoding: "utf8" });
5960
6274
  const blockSet = /* @__PURE__ */ new Set([
5961
6275
  ...srcNames.filter((n) => isGsdOwned(n)),
5962
6276
  ...ALWAYS_NEVER_SYNC
@@ -5967,30 +6281,30 @@ function copySkillsPull(src, dst) {
5967
6281
  copyExtrasFilteredPreservingBy(src, dst, isSkillExcluded);
5968
6282
  }
5969
6283
  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 });
6284
+ const sharedSkills = join46(repoHome(), "shared", "skills");
6285
+ if (!existsSync39(sharedSkills)) return;
6286
+ const localSkills = join46(claudeHome(), "skills");
6287
+ const dstStat = lstatSync12(localSkills, { throwIfNoEntry: false });
5974
6288
  if (dstStat?.isSymbolicLink() === true) {
5975
6289
  backupBeforeWrite(localSkills, ts);
5976
- rmSync13(localSkills, { recursive: true, force: true });
6290
+ rmSync14(localSkills, { recursive: true, force: true });
5977
6291
  mkdirSync9(localSkills, { recursive: true });
5978
6292
  }
5979
6293
  copySkillsPull(sharedSkills, localSkills);
5980
6294
  }
5981
6295
  function syncSkillsPush() {
5982
- const localSkills = join44(claudeHome(), "skills");
5983
- const stat = lstatSync11(localSkills, { throwIfNoEntry: false });
6296
+ const localSkills = join46(claudeHome(), "skills");
6297
+ const stat = lstatSync12(localSkills, { throwIfNoEntry: false });
5984
6298
  if (stat === void 0) return;
5985
6299
  if (stat.isSymbolicLink()) return;
5986
- const sharedSkills = join44(repoHome(), "shared", "skills");
6300
+ const sharedSkills = join46(repoHome(), "shared", "skills");
5987
6301
  copySkillsPush(localSkills, sharedSkills);
5988
6302
  }
5989
6303
 
5990
6304
  // src/preview.ts
5991
6305
  init_config();
5992
- import { existsSync as existsSync38 } from "node:fs";
5993
- import { join as join45 } from "node:path";
6306
+ import { existsSync as existsSync40 } from "node:fs";
6307
+ import { join as join47 } from "node:path";
5994
6308
 
5995
6309
  // node_modules/diff/libesm/diff/base.js
5996
6310
  var Diff = class {
@@ -6276,7 +6590,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
6276
6590
  return lines.join("\n");
6277
6591
  }
6278
6592
  function readJsonOrNull(path) {
6279
- if (!existsSync38(path)) return null;
6593
+ if (!existsSync40(path)) return null;
6280
6594
  try {
6281
6595
  return readJson(path);
6282
6596
  } catch {
@@ -6290,12 +6604,12 @@ function previewSettings(basePath, hostPath, settingsPath) {
6290
6604
  }
6291
6605
  const notes = [];
6292
6606
  const hostOverrides = readJsonOrNull(hostPath);
6293
- if (hostOverrides === null && existsSync38(hostPath)) {
6607
+ if (hostOverrides === null && existsSync40(hostPath)) {
6294
6608
  notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
6295
6609
  }
6296
6610
  const merged = stripGsdHookEntries(deepMerge(base, hostOverrides ?? {}));
6297
6611
  const current = readJsonOrNull(settingsPath);
6298
- if (current === null && existsSync38(settingsPath)) {
6612
+ if (current === null && existsSync40(settingsPath)) {
6299
6613
  return { diff: "", notes: [...notes, "malformed; skipping diff"] };
6300
6614
  }
6301
6615
  const strippedCurrent = stripGsdHookEntries(current ?? {});
@@ -6308,6 +6622,7 @@ function previewSettings(basePath, hostPath, settingsPath) {
6308
6622
  return { diff, notes };
6309
6623
  }
6310
6624
  function formatLinkRow(e) {
6625
+ if (e.kind === "copy") return `would copy ${e.from} -> ${e.to}`;
6311
6626
  return `${e.kind} ${e.from} -> ${e.to}`;
6312
6627
  }
6313
6628
  function formatSessionRow(e) {
@@ -6336,9 +6651,9 @@ function computePreview(ts, map, verb = "pull") {
6336
6651
  onPreview: (e) => addItem(links, formatLinkRow(e))
6337
6652
  });
6338
6653
  const settingsResult = previewSettings(
6339
- join45(repo, "shared", "settings.base.json"),
6340
- join45(repo, "hosts", `${HOST}.json`),
6341
- join45(claude, "settings.json")
6654
+ join47(repo, "shared", "settings.base.json"),
6655
+ join47(repo, "hosts", `${HOST}.json`),
6656
+ join47(claude, "settings.json")
6342
6657
  );
6343
6658
  const settingsSection = buildSettingsSectionForPreview(settingsResult);
6344
6659
  const sessions = section("Sessions");
@@ -6353,7 +6668,7 @@ function computePreview(ts, map, verb = "pull") {
6353
6668
  const extras = section("Extras");
6354
6669
  let extrasSkipped = 0;
6355
6670
  let extrasUnmapped = 0;
6356
- if (existsSync38(join45(repo, "path-map.json")) && existsSync38(join45(repo, "shared", "extras"))) {
6671
+ if (existsSync40(join47(repo, "path-map.json")) && existsSync40(join47(repo, "shared", "extras"))) {
6357
6672
  const extrasResult = remapExtrasPull(ts, { dryRun: true });
6358
6673
  for (const entry of extrasResult.wouldPull) {
6359
6674
  addItem(extras, entry);
@@ -6378,9 +6693,9 @@ init_config();
6378
6693
  init_commands_pull_wedge();
6379
6694
  init_utils();
6380
6695
  init_utils_fs();
6381
- import { execFileSync as execFileSync17 } from "node:child_process";
6696
+ import { execFileSync as execFileSync19 } from "node:child_process";
6382
6697
  function gitCapture(args, cwd) {
6383
- return execFileSync17("git", args, {
6698
+ return execFileSync19("git", args, {
6384
6699
  cwd,
6385
6700
  stdio: ["ignore", "pipe", "pipe"],
6386
6701
  maxBuffer: 64 * 1024 * 1024
@@ -6524,17 +6839,9 @@ function buildWetPullSections(ts, map, prePostHeads) {
6524
6839
  const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
6525
6840
  const extrasResult = remapExtrasPull(ts, { prePostHeads });
6526
6841
  const localOnly = scanLocalOnly();
6842
+ const unmapped = remapResult.unmapped + extrasResult.unmapped;
6527
6843
  const summary = section(PULL_SUMMARY_HEADER);
6528
- addItem(
6529
- summary,
6530
- summaryRow(
6531
- "pull",
6532
- remapResult.unmapped + extrasResult.unmapped,
6533
- 0,
6534
- extrasResult.skipped,
6535
- localOnly
6536
- )
6537
- );
6844
+ addItem(summary, summaryRow("pull", unmapped, 0, extrasResult.skipped, localOnly));
6538
6845
  return {
6539
6846
  sections: [
6540
6847
  buildSettingsSection(label),
@@ -6542,7 +6849,10 @@ function buildWetPullSections(ts, map, prePostHeads) {
6542
6849
  buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
6543
6850
  summary
6544
6851
  ],
6545
- localOnly
6852
+ localOnly,
6853
+ settingsLabel: label,
6854
+ unmapped,
6855
+ extrasSkipped: extrasResult.skipped
6546
6856
  };
6547
6857
  }
6548
6858
  function handleWedge(repo, forceRemote) {
@@ -6566,39 +6876,56 @@ function handleWedge(repo, forceRemote) {
6566
6876
  function runPullCore(opts = {}) {
6567
6877
  const dryRun = opts.dryRun === true;
6568
6878
  const forceRemote = opts.forceRemote === true;
6879
+ const compose = opts.compose === true;
6569
6880
  const repo = repoHome();
6570
6881
  const backup = backupBase();
6571
6882
  const ts = freshBackupTs(backup);
6572
6883
  handleWedge(repo, forceRemote);
6573
6884
  if (!dryRun) {
6574
- const backupRoot = join46(backup, ts);
6885
+ const backupRoot = join48(backup, ts);
6575
6886
  try {
6576
6887
  mkdirSync10(backupRoot, { recursive: true });
6577
6888
  } catch (err) {
6578
6889
  die(`could not create backup dir: ${err.message}`);
6579
6890
  }
6580
6891
  }
6581
- log(
6582
- dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
6583
- );
6892
+ if (!compose) {
6893
+ log(
6894
+ dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
6895
+ );
6896
+ }
6584
6897
  const prePostHeads = capturePrePostHeads(repo, () => {
6585
6898
  gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
6586
6899
  });
6587
- const mapPath = join46(repo, "path-map.json");
6588
- const map = existsSync39(mapPath) ? readPathMap(mapPath) : { projects: {} };
6900
+ const mapPath = join48(repo, "path-map.json");
6901
+ const map = existsSync41(mapPath) ? readPathMap(mapPath) : { projects: {} };
6589
6902
  const divergedKeptLocal = divergenceCheckExtras(ts, dryRun ? prePostHeads : void 0);
6590
6903
  if (dryRun) {
6591
6904
  computePreview(ts, map, "pull");
6592
6905
  log("dry-run complete; no mutation");
6593
6906
  return { tag: "dry" };
6594
6907
  }
6595
- const { sections, localOnly } = buildWetPullSections(ts, map, prePostHeads);
6596
- return { tag: "wet", sections, localOnly, divergedKeptLocal };
6908
+ const { sections, localOnly, settingsLabel, unmapped, extrasSkipped } = buildWetPullSections(
6909
+ ts,
6910
+ map,
6911
+ prePostHeads
6912
+ );
6913
+ const incomingChanges = prePostHeads === void 0 ? true : prePostHeads.pre !== prePostHeads.post;
6914
+ return {
6915
+ tag: "wet",
6916
+ sections,
6917
+ localOnly,
6918
+ divergedKeptLocal,
6919
+ incomingChanges,
6920
+ settingsLabel,
6921
+ unmapped,
6922
+ extrasSkipped
6923
+ };
6597
6924
  }
6598
6925
  function cmdPull(opts = {}) {
6599
6926
  const repo = repoHome();
6600
- if (!existsSync39(repo)) die(`repo not cloned at ${repo}`);
6601
- if (!existsSync39(join46(repo, "shared", "settings.base.json"))) {
6927
+ if (!existsSync41(repo)) die(`repo not cloned at ${repo}`);
6928
+ if (!existsSync41(join48(repo, "shared", "settings.base.json"))) {
6602
6929
  die("repo not initialized; run 'nomad init' to scaffold");
6603
6930
  }
6604
6931
  const handle = acquireLock("pull");
@@ -6620,13 +6947,13 @@ function cmdPull(opts = {}) {
6620
6947
 
6621
6948
  // src/commands.push.ts
6622
6949
  init_config();
6623
- import { existsSync as existsSync43 } from "node:fs";
6624
- import { join as join51 } from "node:path";
6950
+ import { existsSync as existsSync45 } from "node:fs";
6951
+ import { join as join53 } from "node:path";
6625
6952
 
6626
6953
  // src/commands.push.selection.ts
6627
6954
  init_config();
6628
- import { existsSync as existsSync40, statSync as statSync11 } from "node:fs";
6629
- import { join as join47 } from "node:path";
6955
+ import { existsSync as existsSync42, statSync as statSync11 } from "node:fs";
6956
+ import { join as join49 } from "node:path";
6630
6957
  init_utils_json();
6631
6958
  function buildCurrentMap(map) {
6632
6959
  const current = {};
@@ -6635,8 +6962,8 @@ function buildCurrentMap(map) {
6635
6962
  for (const [, hostMap] of Object.entries(map.projects)) {
6636
6963
  const localPath = hostMap[HOST];
6637
6964
  if (!localPath) continue;
6638
- const localDir = join47(claude, "projects", encodePath(localPath));
6639
- if (!existsSync40(localDir)) continue;
6965
+ const localDir = join49(claude, "projects", encodePath(localPath));
6966
+ if (!existsSync42(localDir)) continue;
6640
6967
  for (const f of enumerateSourceFiles(localDir)) {
6641
6968
  const st = statSync11(f);
6642
6969
  current[f] = { size: st.size, mtime: st.mtimeMs };
@@ -6667,7 +6994,7 @@ function computePushSelection(map, old, scannerVersion, configHash, fullScan) {
6667
6994
  };
6668
6995
  }
6669
6996
  function loadSelectionForPush(mapPath, old, scannerVersion, configHash, fullScan) {
6670
- const map = existsSync40(mapPath) ? readPathMap(mapPath) : null;
6997
+ const map = existsSync42(mapPath) ? readPathMap(mapPath) : null;
6671
6998
  const { selection, newManifest } = computePushSelection(
6672
6999
  map,
6673
7000
  old,
@@ -6769,14 +7096,14 @@ function enforceAllowList(statusPorcelain, map) {
6769
7096
 
6770
7097
  // src/commands.push.settings.ts
6771
7098
  init_config();
6772
- import { existsSync as existsSync41 } from "node:fs";
6773
- import { join as join48 } from "node:path";
7099
+ import { existsSync as existsSync43 } from "node:fs";
7100
+ import { join as join50 } from "node:path";
6774
7101
  init_utils();
6775
7102
  init_utils_fs();
6776
7103
  init_utils_json();
6777
7104
  function stripGsdHooksFromBase(repo, backup) {
6778
- const basePath = join48(repo, "shared", "settings.base.json");
6779
- if (!existsSync41(basePath)) return;
7105
+ const basePath = join50(repo, "shared", "settings.base.json");
7106
+ if (!existsSync43(basePath)) return;
6780
7107
  let base;
6781
7108
  try {
6782
7109
  base = readJson(basePath);
@@ -6790,14 +7117,14 @@ function stripGsdHooksFromBase(repo, backup) {
6790
7117
  writeJsonAtomic(basePath, stripped);
6791
7118
  }
6792
7119
  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;
7120
+ const basePath = join50(repo, "shared", "settings.base.json");
7121
+ if (!existsSync43(basePath)) return;
7122
+ const settingsPath = join50(claudeHome(), "settings.json");
7123
+ if (!existsSync43(settingsPath)) return;
6797
7124
  try {
6798
7125
  const base = readJson(basePath);
6799
- const hostPath = join48(repo, "hosts", `${HOST}.json`);
6800
- const overrides = existsSync41(hostPath) ? readJson(hostPath) : {};
7126
+ const hostPath = join50(repo, "hosts", `${HOST}.json`);
7127
+ const overrides = existsSync43(hostPath) ? readJson(hostPath) : {};
6801
7128
  const merged = deepMerge(base, overrides);
6802
7129
  const settings = readJson(settingsPath);
6803
7130
  const { ahead } = classifySettingsDrift(merged, settings);
@@ -6814,12 +7141,12 @@ function reportSettingsAheadDrift(repo) {
6814
7141
  // src/commands.push.guards.ts
6815
7142
  init_push_checks();
6816
7143
  init_utils();
6817
- import { join as join49, relative as relative7 } from "node:path";
7144
+ import { join as join51, relative as relative7 } from "node:path";
6818
7145
  function guardGitlinks(repo) {
6819
- const gitlinks = findGitlinks(join49(repo, "shared"));
7146
+ const gitlinks = findGitlinks(join51(repo, "shared"));
6820
7147
  if (gitlinks.length === 0) return;
6821
7148
  for (const p of gitlinks) {
6822
- const rel = relative7(repo, p);
7149
+ const rel = relative7(repo, p).replaceAll("\\", "/");
6823
7150
  fail(`gitlink: ${rel} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`);
6824
7151
  }
6825
7152
  const noun = gitlinks.length === 1 ? "entry" : "entries";
@@ -6848,7 +7175,7 @@ init_config();
6848
7175
 
6849
7176
  // src/push-global-config.ts
6850
7177
  init_config();
6851
- import { execFileSync as execFileSync18 } from "node:child_process";
7178
+ import { execFileSync as execFileSync20 } from "node:child_process";
6852
7179
  var STATUS_LABELS = {
6853
7180
  A: "add",
6854
7181
  M: "modify",
@@ -6888,7 +7215,7 @@ function isInScope(filePath, exactPrefixes, dirPrefixes) {
6888
7215
  }
6889
7216
  function collectGlobalConfigChanges(repoHome2, hostname2, opts) {
6890
7217
  const args = opts.staged ? ["diff", "--cached", "--name-status", "-z"] : ["diff", "HEAD", "--name-status", "-z"];
6891
- const raw = execFileSync18("git", args, {
7218
+ const raw = execFileSync20("git", args, {
6892
7219
  cwd: repoHome2,
6893
7220
  stdio: ["ignore", "pipe", "pipe"]
6894
7221
  }).toString();
@@ -6926,9 +7253,9 @@ init_color();
6926
7253
  init_config();
6927
7254
  init_config_sharedDirs_guard();
6928
7255
  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";
7256
+ import { copyFileSync, existsSync as existsSync44, mkdirSync as mkdirSync11, readdirSync as readdirSync17, rmSync as rmSync15 } from "node:fs";
6930
7257
  import { homedir as homedir5 } from "node:os";
6931
- import { join as join50, relative as relative8, sep as sep8 } from "node:path";
7258
+ import { join as join52, relative as relative8, sep as sep9 } from "node:path";
6932
7259
  init_push_leak_verdict();
6933
7260
  init_push_gitleaks();
6934
7261
  init_utils_fs();
@@ -6936,11 +7263,11 @@ init_utils_json();
6936
7263
  var NOTHING_TO_SCAN_ROW = `${dim(infoGlyph)} nothing to scan, no leaks`;
6937
7264
  function stageSessionDir(localDir, dstDir, changed) {
6938
7265
  if (changed !== void 0) {
6939
- const prefix = `${localDir}${sep8}`;
7266
+ const prefix = `${localDir}${sep9}`;
6940
7267
  const matching = [...changed].filter((p) => p.startsWith(prefix));
6941
7268
  if (matching.length === 0) return false;
6942
7269
  for (const src of matching) {
6943
- copyFileAtomic(src, join50(dstDir, relative8(localDir, src)));
7270
+ copyFileAtomic(src, join52(dstDir, relative8(localDir, src)));
6944
7271
  }
6945
7272
  return true;
6946
7273
  }
@@ -6956,14 +7283,14 @@ function stageSessions(tmpRoot, map, changed) {
6956
7283
  if (!p || p === "TBD") continue;
6957
7284
  reverse.set(encodePath(p), logical);
6958
7285
  }
6959
- const localProjects = join50(claudeHome(), "projects");
6960
- if (!existsSync42(localProjects)) return 0;
7286
+ const localProjects = join52(claudeHome(), "projects");
7287
+ if (!existsSync44(localProjects)) return 0;
6961
7288
  let staged = 0;
6962
- for (const dir of readdirSync16(localProjects)) {
7289
+ for (const dir of readdirSync17(localProjects)) {
6963
7290
  const logical = reverse.get(dir);
6964
7291
  if (!logical) continue;
6965
- const localDir = join50(localProjects, dir);
6966
- const dstDir = join50(tmpRoot, "shared", "projects", logical);
7292
+ const localDir = join52(localProjects, dir);
7293
+ const dstDir = join52(tmpRoot, "shared", "projects", logical);
6967
7294
  if (stageSessionDir(localDir, dstDir, changed)) staged++;
6968
7295
  }
6969
7296
  return staged;
@@ -6979,9 +7306,9 @@ function stageExtras(tmpRoot, map) {
6979
7306
  if (!localRoot || localRoot === "TBD") continue;
6980
7307
  for (const dirname10 of dirnames) {
6981
7308
  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);
7309
+ const src = join52(localRoot, dirname10);
7310
+ if (!existsSync44(src)) continue;
7311
+ const dst = join52(tmpRoot, "shared", "extras", logical, dirname10);
6985
7312
  copyExtras(src, dst);
6986
7313
  staged++;
6987
7314
  }
@@ -6989,19 +7316,19 @@ function stageExtras(tmpRoot, map) {
6989
7316
  return staged;
6990
7317
  }
6991
7318
  function previewPushLeaks(map, opts = {}) {
6992
- const cacheDir = join50(homedir5(), ".cache", "claude-nomad");
7319
+ const cacheDir = join52(homedir5(), ".cache", "claude-nomad");
6993
7320
  mkdirSync11(cacheDir, { recursive: true });
6994
7321
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
6995
- const tmpRoot = join50(cacheDir, `push-preview-tree-${stamp}`);
7322
+ const tmpRoot = join52(cacheDir, `push-preview-tree-${stamp}`);
6996
7323
  try {
6997
7324
  const sessionCount = stageSessions(tmpRoot, map, opts.selection?.changed);
6998
7325
  const extrasCount = stageExtras(tmpRoot, map);
6999
7326
  if (sessionCount + extrasCount === 0) {
7000
7327
  return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
7001
7328
  }
7002
- const ignoreFile = join50(repoHome(), ".gitleaksignore");
7003
- if (existsSync42(ignoreFile)) {
7004
- copyFileSync(ignoreFile, join50(tmpRoot, ".gitleaksignore"));
7329
+ const ignoreFile = join52(repoHome(), ".gitleaksignore");
7330
+ if (existsSync44(ignoreFile)) {
7331
+ copyFileSync(ignoreFile, join52(tmpRoot, ".gitleaksignore"));
7005
7332
  }
7006
7333
  let findings;
7007
7334
  try {
@@ -7014,13 +7341,13 @@ function previewPushLeaks(map, opts = {}) {
7014
7341
  }
7015
7342
  return verdictFromFindings(findings);
7016
7343
  } finally {
7017
- rmSync14(tmpRoot, { recursive: true, force: true });
7344
+ rmSync15(tmpRoot, { recursive: true, force: true });
7018
7345
  }
7019
7346
  }
7020
7347
 
7021
7348
  // src/commands.push.steps.ts
7022
7349
  init_utils();
7023
- async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
7350
+ async function commitAndPush(st, ts, map, resolution, repo, newManifest, render) {
7024
7351
  gitOrFatal(["add", "-A"], "git add", repo);
7025
7352
  const staged = parsePorcelainZ2(gitStatusPorcelainZ(repo));
7026
7353
  const toDrop = staged.filter((p) => isGsdDropped(p));
@@ -7028,13 +7355,18 @@ async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
7028
7355
  gitOrFatal(["restore", "--staged", "--", ...toDrop], "git restore --staged", repo);
7029
7356
  }
7030
7357
  if (staged.length === toDrop.length) {
7031
- log("nothing to commit");
7032
- renderNoScanTree(st);
7033
- return "nothing";
7358
+ const sections2 = buildNoScanSections(st);
7359
+ if (render) {
7360
+ log("nothing to commit");
7361
+ renderTree(sections2);
7362
+ }
7363
+ return { outcome: "nothing", sections: sections2 };
7034
7364
  }
7035
7365
  st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: true });
7036
7366
  let verdict = withSpinner("Scanning for secrets", () => scanPushVerdict(repo));
7367
+ const hadLeak = verdict.leak;
7037
7368
  if (verdict.leak) {
7369
+ if (!render) log("push (leak recovery)");
7038
7370
  renderPushTree(st, verdict);
7039
7371
  verdict = await resolveLeakFindings(verdict, ts, map, resolution);
7040
7372
  }
@@ -7045,8 +7377,13 @@ async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
7045
7377
  } catch (err) {
7046
7378
  warn(`could not write push manifest (next push will full-rescan): ${String(err)}`);
7047
7379
  }
7048
- renderPushTree(st, verdict);
7049
- return "pushed";
7380
+ if (hadLeak) {
7381
+ renderPushTree(st, verdict);
7382
+ return { outcome: "pushed", sections: [] };
7383
+ }
7384
+ const sections = buildPushTreeSections(st, verdict);
7385
+ if (render) renderTree(sections);
7386
+ return { outcome: "pushed", sections };
7050
7387
  }
7051
7388
  function runDryRunPreview(st, map, repo, selection) {
7052
7389
  st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: false });
@@ -7063,20 +7400,61 @@ function runDryRunPreview(st, map, repo, selection) {
7063
7400
  init_push_checks();
7064
7401
  init_utils();
7065
7402
  init_utils_fs();
7403
+ function aheadOfUpstream(repo) {
7404
+ try {
7405
+ const raw = gitCaptureRaw(["rev-list", "--count", "@{u}..HEAD"], repo);
7406
+ return Number.parseInt(raw.trim(), 10) > 0;
7407
+ } catch {
7408
+ return false;
7409
+ }
7410
+ }
7411
+ function emptyStatusResult(st, compose, repo) {
7412
+ if (compose) {
7413
+ return {
7414
+ tag: "nothing",
7415
+ sections: buildNoScanSections(st),
7416
+ aheadOfOrigin: aheadOfUpstream(repo),
7417
+ globalConfigCount: st.globalConfig.length,
7418
+ collisions: st.remap.collisions
7419
+ };
7420
+ }
7421
+ log("nothing to commit");
7422
+ renderNoScanTree(st);
7423
+ return { tag: "nothing" };
7424
+ }
7425
+ function toPushCoreResult(outcome, sections, compose, repo, st) {
7426
+ if (!compose) return { tag: outcome };
7427
+ const globalConfigCount = st.globalConfig.length;
7428
+ const collisions = st.remap.collisions;
7429
+ if (outcome === "nothing") {
7430
+ return {
7431
+ tag: "nothing",
7432
+ sections,
7433
+ aheadOfOrigin: aheadOfUpstream(repo),
7434
+ globalConfigCount,
7435
+ collisions
7436
+ };
7437
+ }
7438
+ return { tag: "pushed", sections, globalConfigCount, collisions };
7439
+ }
7066
7440
  async function runPushCore(opts = {}) {
7067
7441
  const dryRun = opts.dryRun === true;
7068
7442
  const redactAll = opts.redactAll === true;
7069
7443
  const allowAll = opts.allowAll === true;
7070
7444
  const allowRule = opts.allowRule;
7071
7445
  const fullScan = opts.fullScan === true;
7446
+ const compose = opts.compose === true;
7447
+ const render = !compose;
7072
7448
  const repo = repoHome();
7073
7449
  const backup = backupBase();
7074
- console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
7450
+ if (!compose) {
7451
+ console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
7452
+ }
7075
7453
  reportSettingsAheadDrift(repo);
7076
7454
  const scannerVersion = probeGitleaks();
7077
7455
  const configHash = computeConfigHash();
7078
7456
  const old = readManifest(manifestPath());
7079
- const mapPath = join51(repo, "path-map.json");
7457
+ const mapPath = join53(repo, "path-map.json");
7080
7458
  const { map, selection, newManifest } = loadSelectionForPush(
7081
7459
  mapPath,
7082
7460
  old,
@@ -7090,16 +7468,13 @@ async function runPushCore(opts = {}) {
7090
7468
  const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
7091
7469
  if (!dryRun) {
7092
7470
  syncSkillsPush();
7471
+ syncSharedLinksPush(map);
7093
7472
  stripGsdHooksFromBase(repo, backup);
7094
7473
  }
7095
7474
  const st = { dryRun, remap, extras, globalConfig: [] };
7096
7475
  guardGitlinks(repo);
7097
7476
  const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
7098
- if (!dryRun && !status) {
7099
- log("nothing to commit");
7100
- renderNoScanTree(st);
7101
- return { tag: "nothing" };
7102
- }
7477
+ if (!dryRun && !status) return emptyStatusResult(st, compose, repo);
7103
7478
  if (map === null) {
7104
7479
  if (dryRun) {
7105
7480
  runDryRunPreview(st, null, repo, selection);
@@ -7112,15 +7487,16 @@ async function runPushCore(opts = {}) {
7112
7487
  runDryRunPreview(st, map, repo, selection);
7113
7488
  return { tag: "dry" };
7114
7489
  }
7115
- const outcome = await commitAndPush(
7490
+ const { outcome, sections } = await commitAndPush(
7116
7491
  st,
7117
7492
  ts,
7118
7493
  map,
7119
7494
  { redactAll, allowAll, allowRule },
7120
7495
  repo,
7121
- newManifest
7496
+ newManifest,
7497
+ render
7122
7498
  );
7123
- return outcome === "nothing" ? { tag: "nothing" } : { tag: "pushed" };
7499
+ return toPushCoreResult(outcome, sections, compose, repo, st);
7124
7500
  }
7125
7501
  async function cmdPush(opts = {}) {
7126
7502
  const dryRun = opts.dryRun === true;
@@ -7129,7 +7505,7 @@ async function cmdPush(opts = {}) {
7129
7505
  const allowRule = opts.allowRule;
7130
7506
  guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
7131
7507
  const repo = repoHome();
7132
- if (!existsSync43(repo)) die(`repo not cloned at ${repo}`);
7508
+ if (!existsSync45(repo)) die(`repo not cloned at ${repo}`);
7133
7509
  const handle = acquireLock("push");
7134
7510
  if (handle === null) process.exit(0);
7135
7511
  try {
@@ -7147,36 +7523,59 @@ async function cmdPush(opts = {}) {
7147
7523
  }
7148
7524
 
7149
7525
  // src/commands.sync.ts
7150
- import { existsSync as existsSync44 } from "node:fs";
7151
- import { join as join52 } from "node:path";
7526
+ import { existsSync as existsSync46 } from "node:fs";
7527
+ import { join as join54 } from "node:path";
7152
7528
  init_config();
7529
+ init_color();
7153
7530
  init_utils();
7154
7531
  init_utils_fs();
7155
7532
  init_utils_json();
7156
- function pullHasNoSyncedItems(sections) {
7157
- return sections.every(
7158
- (s) => s.header === "Settings" || s.header === PULL_SUMMARY_HEADER || s.items.length === 0
7159
- );
7160
- }
7161
7533
  function isNoopSync(pull, pushOutcome) {
7162
7534
  if (!pushOutcome.ok) return false;
7163
7535
  if (pull.localOnly !== 0 || pull.divergedKeptLocal !== 0) return false;
7164
7536
  if (pushOutcome.result.tag !== "nothing") return false;
7165
- return pullHasNoSyncedItems(pull.sections);
7537
+ if (pushOutcome.result.aheadOfOrigin === true) return false;
7538
+ return !pull.incomingChanges;
7166
7539
  }
7167
- function pullPhrase(pull) {
7168
- const summary = pull.sections.find((s) => s.header === PULL_SUMMARY_HEADER);
7169
- return summary?.items[0] ?? "applied";
7540
+ function buildPullSummaryRow(pull) {
7541
+ const applied = pull.incomingChanges ? "upstream changes applied" : "no upstream changes";
7542
+ return `pull: ${applied}; settings regenerated (base + ${pull.settingsLabel})`;
7170
7543
  }
7171
- function reconciledNotes(pull) {
7172
- const notes = [];
7544
+ function countNoun(n, singular, plural) {
7545
+ return `${n} ${n === 1 ? singular : plural}`;
7546
+ }
7547
+ function pushedParenParts(pull, globalConfigCount) {
7548
+ const parts = [];
7549
+ if (pull.localOnly > 0) {
7550
+ parts.push(countNoun(pull.localOnly, "local-only session", "local-only sessions"));
7551
+ }
7173
7552
  if (pull.divergedKeptLocal > 0) {
7174
- notes.push(`${pull.divergedKeptLocal} diverged files kept local and pushed`);
7553
+ parts.push(countNoun(pull.divergedKeptLocal, "diverged extras file", "diverged extras files"));
7175
7554
  }
7176
- if (pull.localOnly > 0) {
7177
- notes.push(`${pull.localOnly} local-only sessions pushed`);
7555
+ if (globalConfigCount > 0) {
7556
+ parts.push(countNoun(globalConfigCount, "config file", "config files"));
7178
7557
  }
7179
- return notes;
7558
+ return parts;
7559
+ }
7560
+ function buildPushSummaryRow(pull, result) {
7561
+ if (result.tag !== "pushed") return "push: nothing to push";
7562
+ const parts = pushedParenParts(pull, result.globalConfigCount ?? 0);
7563
+ return parts.length > 0 ? `push: pushed (${parts.join(", ")})` : "push: pushed";
7564
+ }
7565
+ function buildSkipAndCollisionRows(pull, result) {
7566
+ const rows = [];
7567
+ if (pull.unmapped > 0) {
7568
+ rows.push(`${dim(infoGlyph)} ${pull.unmapped} not in path-map (run nomad doctor to list)`);
7569
+ }
7570
+ if (pull.extrasSkipped > 0) {
7571
+ rows.push(`${dim(infoGlyph)} ${pull.extrasSkipped} extras skipped`);
7572
+ }
7573
+ const collisions = result.tag === "dry" ? void 0 : result.collisions;
7574
+ if (collisions !== void 0 && collisions > 0) {
7575
+ const phrase = countNoun(collisions, "collision", "collisions");
7576
+ rows.push(`${yellow(warnGlyph)} ${phrase} (run nomad doctor to list)`);
7577
+ }
7578
+ return rows;
7180
7579
  }
7181
7580
  function buildSyncSummarySection(pull, pushOutcome) {
7182
7581
  const s = section("Sync summary");
@@ -7184,22 +7583,59 @@ function buildSyncSummarySection(pull, pushOutcome) {
7184
7583
  addItem(s, `pull: applied, push: failed (${pushOutcome.message})`);
7185
7584
  return s;
7186
7585
  }
7187
- addItem(s, `pull: ${pullPhrase(pull)}`);
7188
- addItem(s, `push: ${pushOutcome.result.tag === "pushed" ? "pushed" : "nothing to push"}`);
7189
- for (const note of reconciledNotes(pull)) addItem(s, note);
7586
+ const { result } = pushOutcome;
7587
+ addItem(s, buildPullSummaryRow(pull));
7588
+ addItem(s, buildPushSummaryRow(pull, result));
7589
+ if (result.tag === "nothing" && result.aheadOfOrigin === true) {
7590
+ addItem(s, "sync repo has unpushed commits");
7591
+ }
7592
+ for (const row2 of buildSkipAndCollisionRows(pull, result)) addItem(s, row2);
7190
7593
  return s;
7191
7594
  }
7192
- function renderWetSync(pull, pushOutcome) {
7595
+ var SYNC_SECTION_ORDER = ["Settings", "Global config", "Sessions", "Extras", "Leak scan"];
7596
+ function isDroppedSummaryHeader(header) {
7597
+ return header === PULL_SUMMARY_HEADER || header === "Push summary";
7598
+ }
7599
+ function mergeSyncSections(pullSections, pushSections) {
7600
+ const byHeader = new Map(
7601
+ SYNC_SECTION_ORDER.map((header) => [header, section(header)])
7602
+ );
7603
+ for (const s of [...pullSections, ...pushSections]) {
7604
+ if (isDroppedSummaryHeader(s.header)) continue;
7605
+ let target = byHeader.get(s.header);
7606
+ if (target === void 0) {
7607
+ target = section(s.header);
7608
+ byHeader.set(s.header, target);
7609
+ }
7610
+ for (const item2 of s.items) {
7611
+ if (!target.items.includes(item2)) addItem(target, item2);
7612
+ }
7613
+ }
7614
+ return [...byHeader.values()].filter((s) => s.items.length > 0);
7615
+ }
7616
+ function pushSectionsOf(pushOutcome) {
7617
+ if (!pushOutcome.ok) return [];
7618
+ const result = pushOutcome.result;
7619
+ if (result.tag === "dry") return [];
7620
+ return result.sections ?? [];
7621
+ }
7622
+ function renderWetSync(pull, pushOutcome, verbose) {
7193
7623
  if (isNoopSync(pull, pushOutcome)) {
7194
7624
  ok("already in sync");
7195
7625
  return;
7196
7626
  }
7197
- renderTree(pull.sections);
7198
- renderTree([buildSyncSummarySection(pull, pushOutcome)]);
7627
+ log(`sync on host=${HOST}`);
7628
+ const summary = buildSyncSummarySection(pull, pushOutcome);
7629
+ if (!verbose) {
7630
+ renderTree([summary]);
7631
+ return;
7632
+ }
7633
+ const merged = mergeSyncSections(pull.sections, pushSectionsOf(pushOutcome));
7634
+ renderTree([...merged, summary]);
7199
7635
  }
7200
7636
  async function runSyncPushHalf() {
7201
7637
  try {
7202
- const result = await runPushCore();
7638
+ const result = await runPushCore({ compose: true });
7203
7639
  return { ok: true, result };
7204
7640
  } catch (err) {
7205
7641
  if (err instanceof NomadFatal) {
@@ -7209,25 +7645,26 @@ async function runSyncPushHalf() {
7209
7645
  throw err;
7210
7646
  }
7211
7647
  }
7212
- async function runSyncWet() {
7213
- const pull = runPullCore();
7648
+ async function runSyncWet(verbose) {
7649
+ const pull = runPullCore({ compose: true });
7214
7650
  const pushOutcome = await runSyncPushHalf();
7215
- renderWetSync(pull, pushOutcome);
7651
+ renderWetSync(pull, pushOutcome, verbose);
7216
7652
  }
7217
7653
  async function runSyncDryRun(repo, backup) {
7218
7654
  const ts = freshBackupTs(backup);
7219
- const mapPath = join52(repo, "path-map.json");
7220
- const map = existsSync44(mapPath) ? readPathMap(mapPath) : { projects: {} };
7655
+ const mapPath = join54(repo, "path-map.json");
7656
+ const map = existsSync46(mapPath) ? readPathMap(mapPath) : { projects: {} };
7221
7657
  computePreview(ts, map, "pull");
7222
7658
  log("push preview below is computed against pre-pull state (a real sync pushes after pull)");
7223
7659
  await runPushCore({ dryRun: true });
7224
7660
  }
7225
7661
  async function cmdSync(opts = {}) {
7226
7662
  const dryRun = opts.dryRun === true;
7663
+ const verbose = opts.verbose === true;
7227
7664
  const repo = repoHome();
7228
7665
  const backup = backupBase();
7229
- if (!existsSync44(repo)) die(`repo not cloned at ${repo}`);
7230
- if (!existsSync44(join52(repo, "shared", "settings.base.json"))) {
7666
+ if (!existsSync46(repo)) die(`repo not cloned at ${repo}`);
7667
+ if (!existsSync46(join54(repo, "shared", "settings.base.json"))) {
7231
7668
  die("repo not initialized; run 'nomad init' to scaffold");
7232
7669
  }
7233
7670
  const handle = acquireLock("sync");
@@ -7236,7 +7673,7 @@ async function cmdSync(opts = {}) {
7236
7673
  if (dryRun) {
7237
7674
  await runSyncDryRun(repo, backup);
7238
7675
  } else {
7239
- await runSyncWet();
7676
+ await runSyncWet(verbose);
7240
7677
  }
7241
7678
  } catch (err) {
7242
7679
  if (err instanceof NomadFatal) {
@@ -7251,25 +7688,31 @@ async function cmdSync(opts = {}) {
7251
7688
  }
7252
7689
 
7253
7690
  // src/commands.update.ts
7254
- import { execFileSync as execFileSync19 } from "node:child_process";
7691
+ import { execFileSync as execFileSync21 } from "node:child_process";
7255
7692
  init_utils();
7256
- function readInstalledVersion(run = execFileSync19) {
7693
+ function readInstalledVersion(run = execFileSync21) {
7694
+ const isWin = process.platform === "win32";
7257
7695
  try {
7258
- return run("nomad", ["--version"], { encoding: "utf8" }).toString().trim() || null;
7696
+ return run(isWin ? "nomad.cmd" : "nomad", ["--version"], {
7697
+ encoding: "utf8",
7698
+ ...isWin ? { shell: true } : {}
7699
+ }).toString().trim() || null;
7259
7700
  } catch {
7260
7701
  return null;
7261
7702
  }
7262
7703
  }
7263
- function cmdUpdate(currentVersion, run = execFileSync19) {
7704
+ function cmdUpdate(currentVersion, run = execFileSync21) {
7264
7705
  console.log(`Updating claude-nomad v${currentVersion}...`);
7706
+ const isWin = process.platform === "win32";
7265
7707
  try {
7266
- run("npm", ["update", "-g", "claude-nomad"], {
7708
+ run(isWin ? "npm.cmd" : "npm", ["update", "-g", "claude-nomad"], {
7267
7709
  encoding: "utf8",
7268
7710
  stdio: ["ignore", "pipe", "pipe"],
7269
7711
  // Now that output is captured rather than inherited, execFileSync buffers
7270
7712
  // it; lift the default 1 MiB ceiling so a noisy-but-successful npm run
7271
7713
  // (deprecation/funding spam) cannot throw ENOBUFS.
7272
- maxBuffer: 64 * 1024 * 1024
7714
+ maxBuffer: 64 * 1024 * 1024,
7715
+ ...isWin ? { shell: true } : {}
7273
7716
  });
7274
7717
  } catch (err) {
7275
7718
  const e = err;
@@ -7294,18 +7737,18 @@ init_config();
7294
7737
 
7295
7738
  // src/diff.ts
7296
7739
  init_config();
7297
- import { existsSync as existsSync45 } from "node:fs";
7298
- import { join as join53 } from "node:path";
7740
+ import { existsSync as existsSync47 } from "node:fs";
7741
+ import { join as join55 } from "node:path";
7299
7742
  init_utils();
7300
7743
  init_utils_fs();
7301
7744
  init_utils_json();
7302
7745
  function cmdDiff() {
7303
7746
  try {
7304
7747
  const repo = repoHome();
7305
- if (!existsSync45(repo)) die(`repo not cloned at ${repo}`);
7748
+ if (!existsSync47(repo)) die(`repo not cloned at ${repo}`);
7306
7749
  const ts = freshBackupTs(backupBase());
7307
- const mapPath = join53(repo, "path-map.json");
7308
- const map = existsSync45(mapPath) ? readPathMap(mapPath) : { projects: {} };
7750
+ const mapPath = join55(repo, "path-map.json");
7751
+ const map = existsSync47(mapPath) ? readPathMap(mapPath) : { projects: {} };
7309
7752
  divergenceCheckExtras(ts);
7310
7753
  computePreview(ts, map, "diff");
7311
7754
  } catch (err) {
@@ -7320,19 +7763,19 @@ function cmdDiff() {
7320
7763
 
7321
7764
  // src/init.ts
7322
7765
  init_config();
7323
- import { existsSync as existsSync47, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "node:fs";
7324
- import { join as join55 } from "node:path";
7766
+ import { existsSync as existsSync49, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "node:fs";
7767
+ import { join as join57 } from "node:path";
7325
7768
 
7326
7769
  // src/init.gh-onboard.ts
7327
7770
  init_config();
7328
- import { execFileSync as execFileSync20 } from "node:child_process";
7771
+ import { execFileSync as execFileSync22 } from "node:child_process";
7329
7772
  init_utils();
7330
7773
  var DEFAULT_REPO_NAME = "claude-nomad-config";
7331
7774
  function isValidRepoName(name) {
7332
7775
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
7333
7776
  }
7334
7777
  var GH_NETWORK_TIMEOUT_MS = 3e4;
7335
- function ensureOriginRepo(repoName, run = execFileSync20) {
7778
+ function ensureOriginRepo(repoName, run = execFileSync22) {
7336
7779
  if (!isValidRepoName(repoName)) {
7337
7780
  die(
7338
7781
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -7403,33 +7846,33 @@ init_config();
7403
7846
  init_utils();
7404
7847
  init_utils_fs();
7405
7848
  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";
7849
+ import { copyFileSync as copyFileSync2, cpSync as cpSync8, existsSync as existsSync48, rmSync as rmSync16, statSync as statSync12 } from "node:fs";
7850
+ import { join as join56 } from "node:path";
7408
7851
  function snapshotIntoShared(map) {
7409
7852
  const repo = repoHome();
7410
7853
  const claude = claudeHome();
7411
7854
  for (const name of allSharedLinks(map)) {
7412
- const src = join54(claude, name);
7413
- if (!existsSync46(src)) continue;
7414
- const dst = join54(repo, "shared", name);
7855
+ const src = join56(claude, name);
7856
+ if (!existsSync48(src)) continue;
7857
+ const dst = join56(repo, "shared", name);
7415
7858
  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 });
7859
+ const gk = join56(dst, ".gitkeep");
7860
+ if (existsSync48(gk)) rmSync16(gk);
7861
+ cpSync8(src, dst, { recursive: true, force: false, errorOnExist: true });
7419
7862
  } else {
7420
7863
  copyFileSync2(src, dst);
7421
7864
  }
7422
7865
  log(`snapshotted shared/${name} from ${src}`);
7423
7866
  }
7424
- const userSettings = join54(claude, "settings.json");
7425
- if (existsSync46(userSettings)) {
7867
+ const userSettings = join56(claude, "settings.json");
7868
+ if (existsSync48(userSettings)) {
7426
7869
  let parsed;
7427
7870
  try {
7428
7871
  parsed = readJson(userSettings);
7429
7872
  } catch (err) {
7430
7873
  return die(`malformed ${userSettings}: ${err.message}`);
7431
7874
  }
7432
- const hostFile = join54(repo, "hosts", `${HOST}.json`);
7875
+ const hostFile = join56(repo, "hosts", `${HOST}.json`);
7433
7876
  writeJsonAtomic(hostFile, parsed);
7434
7877
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
7435
7878
  }
@@ -7439,17 +7882,18 @@ function snapshotIntoShared(map) {
7439
7882
  init_utils();
7440
7883
  init_utils_fs();
7441
7884
  var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.claude/CLAUDE.md by nomad pull -->\n";
7885
+ var GITATTRIBUTES = "# nomad: sync content is byte-managed, disable all line-ending conversion\n* -text\n";
7442
7886
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
7443
7887
  function preflightConflict(repoHome2) {
7444
7888
  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")
7889
+ join57(repoHome2, "shared", "settings.base.json"),
7890
+ join57(repoHome2, "shared", "CLAUDE.md"),
7891
+ join57(repoHome2, "path-map.json"),
7892
+ join57(repoHome2, "hosts"),
7893
+ join57(repoHome2, "shared")
7450
7894
  ];
7451
7895
  for (const c of candidates) {
7452
- if (existsSync47(c)) return c;
7896
+ if (existsSync49(c)) return c;
7453
7897
  }
7454
7898
  return null;
7455
7899
  }
@@ -7467,26 +7911,28 @@ function cmdInit(opts = {}) {
7467
7911
  die(`already initialized; refusing to clobber ${conflict}`);
7468
7912
  }
7469
7913
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
7470
- mkdirSync12(join55(repo, "shared"), { recursive: true });
7471
- mkdirSync12(join55(repo, "hosts"), { recursive: true });
7914
+ mkdirSync12(join57(repo, "shared"), { recursive: true });
7915
+ mkdirSync12(join57(repo, "hosts"), { recursive: true });
7472
7916
  for (const name of SHARED_KEEP_DIRS) {
7473
- mkdirSync12(join55(repo, "shared", name), { recursive: true });
7917
+ mkdirSync12(join57(repo, "shared", name), { recursive: true });
7474
7918
  }
7475
- const userClaudeMd = join55(claude, "CLAUDE.md");
7476
- if (!snapshot || !existsSync47(userClaudeMd)) {
7477
- writeFileSync6(join55(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
7919
+ const userClaudeMd = join57(claude, "CLAUDE.md");
7920
+ if (!snapshot || !existsSync49(userClaudeMd)) {
7921
+ writeFileSync6(join57(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
7478
7922
  item("created shared/CLAUDE.md");
7479
7923
  }
7480
7924
  for (const name of SHARED_KEEP_DIRS) {
7481
- writeFileSync6(join55(repo, "shared", name, ".gitkeep"), "");
7925
+ writeFileSync6(join57(repo, "shared", name, ".gitkeep"), "");
7482
7926
  item(`created shared/${name}/.gitkeep`);
7483
7927
  }
7484
- writeFileSync6(join55(repo, "hosts", ".gitkeep"), "");
7928
+ writeFileSync6(join57(repo, "hosts", ".gitkeep"), "");
7485
7929
  item("created hosts/.gitkeep");
7486
- writeJsonAtomic(join55(repo, "shared", "settings.base.json"), {});
7930
+ writeJsonAtomic(join57(repo, "shared", "settings.base.json"), {});
7487
7931
  item("created shared/settings.base.json");
7488
- writeJsonAtomic(join55(repo, "path-map.json"), { projects: {} });
7932
+ writeJsonAtomic(join57(repo, "path-map.json"), { projects: {} });
7489
7933
  item("created path-map.json");
7934
+ writeFileSync6(join57(repo, ".gitattributes"), GITATTRIBUTES);
7935
+ item("created .gitattributes");
7490
7936
  if (snapshot) {
7491
7937
  snapshotIntoShared({ projects: {} });
7492
7938
  log(`snapshot staged in shared/; review, then 'nomad push' to share with other hosts.`);
@@ -7551,21 +7997,21 @@ function maybeDisableRepoActions(repoHome2, run) {
7551
7997
  // src/init.prompt.ts
7552
7998
  init_config();
7553
7999
  init_utils();
7554
- import { existsSync as existsSync48, readdirSync as readdirSync17, statSync as statSync13 } from "node:fs";
7555
- import { join as join56 } from "node:path";
8000
+ import { existsSync as existsSync50, readdirSync as readdirSync18, statSync as statSync13 } from "node:fs";
8001
+ import { join as join58 } from "node:path";
7556
8002
  import { createInterface as createInterface3 } from "node:readline/promises";
7557
8003
  function nonEmptyExists(path) {
7558
- if (!existsSync48(path)) return false;
8004
+ if (!existsSync50(path)) return false;
7559
8005
  try {
7560
- if (statSync13(path).isDirectory()) return readdirSync17(path).length > 0;
8006
+ if (statSync13(path).isDirectory()) return readdirSync18(path).length > 0;
7561
8007
  return true;
7562
8008
  } catch {
7563
8009
  return false;
7564
8010
  }
7565
8011
  }
7566
8012
  function hasExistingClaudeConfig(claudeHome2) {
7567
- if (existsSync48(join56(claudeHome2, "settings.json"))) return true;
7568
- return SHARED_LINKS.some((name) => nonEmptyExists(join56(claudeHome2, name)));
8013
+ if (existsSync50(join58(claudeHome2, "settings.json"))) return true;
8014
+ return SHARED_LINKS.some((name) => nonEmptyExists(join58(claudeHome2, name)));
7569
8015
  }
7570
8016
  async function confirmSnapshotDefault(claudeHome2) {
7571
8017
  if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
@@ -7852,24 +8298,27 @@ function parsePushArgs(argv) {
7852
8298
  // src/nomad.dispatch.sync.ts
7853
8299
  function parseSyncArgs(argv) {
7854
8300
  let dryRun = false;
8301
+ let verbose = false;
7855
8302
  let i = 3;
7856
8303
  while (i < argv.length) {
7857
8304
  const token = argv[i];
7858
8305
  if (token === "--dry-run") {
7859
8306
  if (dryRun) return null;
7860
8307
  dryRun = true;
8308
+ } else if (token === "--verbose" || token === "--all" || token === "-v") {
8309
+ verbose = true;
7861
8310
  } else {
7862
8311
  return null;
7863
8312
  }
7864
8313
  i++;
7865
8314
  }
7866
- return { dryRun };
8315
+ return { dryRun, verbose };
7867
8316
  }
7868
8317
 
7869
8318
  // package.json
7870
8319
  var package_default = {
7871
8320
  name: "claude-nomad",
7872
- version: "0.58.1",
8321
+ version: "0.60.0",
7873
8322
  type: "module",
7874
8323
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
7875
8324
  keywords: [
@@ -8004,10 +8453,15 @@ var DEFAULT_HELP = [
8004
8453
  cont("Mutually exclusive with --redact-all/--allow. Cannot combine with --dry-run."),
8005
8454
  "",
8006
8455
  row(" sync", "Pull (retain-merge), then push, under one lock. One-shot; hides ordering."),
8456
+ cont("Compact by default: prints only the Sync summary."),
8007
8457
  row(" --dry-run", "Stack the pull preview then the push preview; mutates nothing."),
8008
8458
  cont("Fails fast on a pull-half error; a wedged repo hints at nomad pull --force-remote"),
8009
8459
  cont("(sync itself takes no --force-remote). Mid-push leak recovery behaves exactly"),
8010
8460
  cont("like nomad push."),
8461
+ row(
8462
+ " --verbose, --all, -v",
8463
+ "Show the full merged tree; default output is the Sync summary only."
8464
+ ),
8011
8465
  "",
8012
8466
  row(" diff", "Offline preview of what `pull` would change against local repo state."),
8013
8467
  cont("No git pull, no lock acquired."),
@@ -8105,15 +8559,15 @@ var DEFAULT_HELP = [
8105
8559
  init_config();
8106
8560
  init_utils();
8107
8561
  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";
8562
+ import { existsSync as existsSync51, readFileSync as readFileSync18, readdirSync as readdirSync19 } from "node:fs";
8563
+ import { join as join59 } from "node:path";
8110
8564
  function resumeCmd(sessionId) {
8111
8565
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
8112
8566
  fail(`invalid session id: ${sessionId}`);
8113
8567
  process.exit(1);
8114
8568
  }
8115
- const projectsRoot = join57(claudeHome(), "projects");
8116
- if (!existsSync49(projectsRoot)) {
8569
+ const projectsRoot = join59(claudeHome(), "projects");
8570
+ if (!existsSync51(projectsRoot)) {
8117
8571
  fail(`${projectsRoot} does not exist`);
8118
8572
  process.exit(1);
8119
8573
  }
@@ -8127,8 +8581,8 @@ function resumeCmd(sessionId) {
8127
8581
  fail(`no cwd field found in ${jsonlPath}`);
8128
8582
  process.exit(1);
8129
8583
  }
8130
- const mapPath = join57(repoHome(), "path-map.json");
8131
- if (!existsSync49(mapPath)) {
8584
+ const mapPath = join59(repoHome(), "path-map.json");
8585
+ if (!existsSync51(mapPath)) {
8132
8586
  fail("path-map.json missing");
8133
8587
  process.exit(1);
8134
8588
  }
@@ -8150,14 +8604,14 @@ function resumeCmd(sessionId) {
8150
8604
  console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
8151
8605
  }
8152
8606
  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;
8607
+ for (const dir of readdirSync19(projectsRoot)) {
8608
+ const candidate = join59(projectsRoot, dir, `${sessionId}.jsonl`);
8609
+ if (existsSync51(candidate)) return candidate;
8156
8610
  }
8157
8611
  return null;
8158
8612
  }
8159
8613
  function extractRecordedCwd(jsonlPath) {
8160
- for (const line of readFileSync17(jsonlPath, "utf8").split("\n")) {
8614
+ for (const line of readFileSync18(jsonlPath, "utf8").split("\n")) {
8161
8615
  if (!line.trim()) continue;
8162
8616
  try {
8163
8617
  const obj = JSON.parse(line);
@@ -8233,10 +8687,10 @@ try {
8233
8687
  case "sync": {
8234
8688
  const syncArgs = parseSyncArgs(process.argv);
8235
8689
  if (syncArgs === null) {
8236
- console.error("usage: nomad sync [--dry-run]");
8690
+ console.error("usage: nomad sync [--dry-run] [--verbose|--all|-v]");
8237
8691
  process.exit(1);
8238
8692
  }
8239
- await cmdSync({ dryRun: syncArgs.dryRun });
8693
+ await cmdSync({ dryRun: syncArgs.dryRun, verbose: syncArgs.verbose });
8240
8694
  break;
8241
8695
  }
8242
8696
  case "init": {