claude-nomad 0.41.0 → 0.43.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
@@ -449,6 +449,17 @@ function deepMerge(target, source) {
449
449
  }
450
450
  return out;
451
451
  }
452
+ function sortKeysDeep(value) {
453
+ if (Array.isArray(value)) return value.map(sortKeysDeep);
454
+ if (value !== null && typeof value === "object") {
455
+ const out = {};
456
+ for (const key of Object.keys(value).sort((a, b) => a.localeCompare(b, "en"))) {
457
+ out[key] = sortKeysDeep(value[key]);
458
+ }
459
+ return out;
460
+ }
461
+ return value;
462
+ }
452
463
  var encodePath;
453
464
  var init_utils_json = __esm({
454
465
  "src/utils.json.ts"() {
@@ -550,31 +561,31 @@ var init_utils_fs = __esm({
550
561
  });
551
562
 
552
563
  // src/push-gitleaks.config.ts
553
- import { existsSync as existsSync9, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
564
+ import { existsSync as existsSync10, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
554
565
  import { tmpdir } from "node:os";
555
- import { join as join10 } from "node:path";
566
+ import { join as join11 } from "node:path";
556
567
  import { fileURLToPath } from "node:url";
557
568
  function resolveTomlPath() {
558
- const repoToml = join10(REPO_HOME, ".gitleaks.toml");
559
- if (existsSync9(repoToml)) return repoToml;
569
+ const repoToml = join11(REPO_HOME, ".gitleaks.toml");
570
+ if (existsSync10(repoToml)) return repoToml;
560
571
  const bundled = fileURLToPath(new URL("../.gitleaks.toml", import.meta.url));
561
- return existsSync9(bundled) ? bundled : null;
572
+ return existsSync10(bundled) ? bundled : null;
562
573
  }
563
574
  function buildOverlayTempConfig(overlayBody, bundled) {
564
575
  const tempBody = `[extend]
565
576
  path = ${JSON.stringify(bundled)}
566
577
 
567
578
  ${overlayBody}`;
568
- const tempPath = mkdtempSync(join10(tmpdir(), "nomad-gitleaks-cfg-"));
569
- const configPath = join10(tempPath, "config.toml");
579
+ const tempPath = mkdtempSync(join11(tmpdir(), "nomad-gitleaks-cfg-"));
580
+ const configPath = join11(tempPath, "config.toml");
570
581
  writeFileSync2(configPath, tempBody, { mode: 384, flag: "wx" });
571
582
  return { configPath, tempPath };
572
583
  }
573
584
  function resolveTomlConfig() {
574
- const overlayPath = join10(REPO_HOME, ".gitleaks.overlay.toml");
575
- const repoToml = join10(REPO_HOME, ".gitleaks.toml");
585
+ const overlayPath = join11(REPO_HOME, ".gitleaks.overlay.toml");
586
+ const repoToml = join11(REPO_HOME, ".gitleaks.toml");
576
587
  const bundled = resolveTomlPath();
577
- if (!existsSync9(overlayPath)) {
588
+ if (!existsSync10(overlayPath)) {
578
589
  return { path: bundled, tempPath: null };
579
590
  }
580
591
  if (bundled === repoToml) {
@@ -615,9 +626,9 @@ var init_push_gitleaks_config = __esm({
615
626
 
616
627
  // src/push-checks.ts
617
628
  import { execFileSync as execFileSync2 } from "node:child_process";
618
- import { readdirSync as readdirSync3, rmSync as rmSync3 } from "node:fs";
629
+ import { readdirSync as readdirSync4, rmSync as rmSync3 } from "node:fs";
619
630
  import { homedir as homedir2, platform } from "node:os";
620
- import { join as join11 } from "node:path";
631
+ import { join as join12 } from "node:path";
621
632
  function gitleaksInstallHint() {
622
633
  const head = "gitleaks not on PATH (required for nomad push). Install:";
623
634
  const plat = platform();
@@ -655,12 +666,12 @@ function findGitlinks(dir) {
655
666
  function walk(current) {
656
667
  let entries;
657
668
  try {
658
- entries = readdirSync3(current, { withFileTypes: true });
669
+ entries = readdirSync4(current, { withFileTypes: true });
659
670
  } catch {
660
671
  return;
661
672
  }
662
673
  for (const e of entries) {
663
- const p = join11(current, e.name);
674
+ const p = join12(current, e.name);
664
675
  if (e.name === ".git") {
665
676
  hits.push(p);
666
677
  continue;
@@ -712,7 +723,7 @@ var init_push_checks = __esm({
712
723
  import { execFileSync as execFileSync5 } from "node:child_process";
713
724
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync4 } from "node:fs";
714
725
  import { homedir as homedir3 } from "node:os";
715
- import { join as join15 } from "node:path";
726
+ import { join as join16 } from "node:path";
716
727
  function readGitleaksReport(reportPath) {
717
728
  try {
718
729
  const raw = readFileSync4(reportPath, "utf8");
@@ -724,9 +735,9 @@ function readGitleaksReport(reportPath) {
724
735
  }
725
736
  }
726
737
  function scanStagedTree(repoDir, forwardStreams = false) {
727
- const cacheDir = join15(homedir3(), ".cache", "claude-nomad");
738
+ const cacheDir = join16(homedir3(), ".cache", "claude-nomad");
728
739
  mkdirSync2(cacheDir, { recursive: true });
729
- const reportPath = join15(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
740
+ const reportPath = join16(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
730
741
  const { path: toml, tempPath } = resolveTomlConfig();
731
742
  const args = [
732
743
  "protect",
@@ -758,9 +769,9 @@ function scanStagedTree(repoDir, forwardStreams = false) {
758
769
  }
759
770
  }
760
771
  function scanFile(filePath, forwardStreams = false) {
761
- const cacheDir = join15(homedir3(), ".cache", "claude-nomad");
772
+ const cacheDir = join16(homedir3(), ".cache", "claude-nomad");
762
773
  mkdirSync2(cacheDir, { recursive: true });
763
- const reportPath = join15(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
774
+ const reportPath = join16(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
764
775
  const { path: toml, tempPath } = resolveTomlConfig();
765
776
  const args = [
766
777
  "detect",
@@ -1209,8 +1220,8 @@ function cmdClean(opts, backupBase = BACKUP_BASE) {
1209
1220
  }
1210
1221
 
1211
1222
  // src/commands.doctor.ts
1212
- import { existsSync as existsSync18 } from "node:fs";
1213
- import { join as join22 } from "node:path";
1223
+ import { existsSync as existsSync21 } from "node:fs";
1224
+ import { join as join25 } from "node:path";
1214
1225
 
1215
1226
  // src/commands.doctor.checks.repo.ts
1216
1227
  init_color();
@@ -1230,6 +1241,9 @@ function section(header, raw = false) {
1230
1241
  function addItem(s, text) {
1231
1242
  s.items.push(text);
1232
1243
  }
1244
+ function addChildItem(s, text) {
1245
+ s.items.push(` ${text}`);
1246
+ }
1233
1247
  function sectionFailed(s) {
1234
1248
  return s.items.some((line) => line.includes(failGlyph));
1235
1249
  }
@@ -1246,12 +1260,27 @@ function renderSection(s) {
1246
1260
  }
1247
1261
  const header = sectionFailed(s) ? `${red(FAIL_GLYPH_BARE)} ${s.header}` : s.header;
1248
1262
  console.log(header);
1249
- const lastContent = s.items.reduce((acc, item, j) => item === "" ? acc : j, -1);
1263
+ const lastContent = s.items.reduce(
1264
+ (acc, item, j) => item === "" || isChild(item) ? acc : j,
1265
+ -1
1266
+ );
1250
1267
  for (let j = 0; j < s.items.length; j++) {
1251
- if (s.items[j] === "") console.log("");
1252
- else console.log(`${j === lastContent ? " \u2514 " : " \u251C "}${s.items[j]}`);
1268
+ const item = s.items[j];
1269
+ if (item === "") console.log("");
1270
+ else if (isChild(item)) console.log(renderChildLine(s.items, j));
1271
+ else console.log(`${j === lastContent ? " \u2514 " : " \u251C "}${item}`);
1253
1272
  }
1254
1273
  }
1274
+ function isChild(item) {
1275
+ return item.startsWith(" ");
1276
+ }
1277
+ function renderChildLine(items, j) {
1278
+ const parentContinues = items.some((it, k) => k > j && it !== "" && !isChild(it));
1279
+ const gutter = parentContinues ? " \u2502 " : " ";
1280
+ const next = items[j + 1];
1281
+ const elbow = next === void 0 || !isChild(next) ? "\u2514 " : "\u251C ";
1282
+ return `${gutter} ${elbow}${items[j].slice(1)}`;
1283
+ }
1255
1284
  function renderTree(sections) {
1256
1285
  const visible = sections.filter((s) => s.items.length > 0);
1257
1286
  for (let i = 0; i < visible.length; i++) {
@@ -1321,7 +1350,11 @@ function isOverrideActive() {
1321
1350
  return Boolean(process.env.NOMAD_REPO);
1322
1351
  }
1323
1352
  function reportHostAndPaths(section2) {
1324
- addItem(section2, `${dim(infoGlyph)} host: ${cyan(HOST)}`);
1353
+ const unsetHint = process.env.NOMAD_HOST ? "" : dim(" (env unset, using hostname)");
1354
+ addItem(section2, `${dim(infoGlyph)} NOMAD_HOST: ${cyan(HOST)}${unsetHint}`);
1355
+ if (isOverrideActive()) {
1356
+ addItem(section2, `${dim(infoGlyph)} NOMAD_REPO: ${blue(REPO_HOME)}`);
1357
+ }
1325
1358
  addItem(
1326
1359
  section2,
1327
1360
  `${existsSync6(REPO_HOME) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(REPO_HOME)}`
@@ -1427,7 +1460,7 @@ function loadAndReportSettings(section2) {
1427
1460
  if (unknownKeys.length > 0) {
1428
1461
  addItem(
1429
1462
  section2,
1430
- `${yellow(warnGlyph)} settings.json has unknown keys (schema drift?): ${unknownKeys.join(", ")}`
1463
+ `${yellow(warnGlyph)} settings.json has unknown keys (schema drift?): ${unknownKeys.join(", ")} (verify: nomad doctor --check-schema)`
1431
1464
  );
1432
1465
  } else {
1433
1466
  addItem(section2, `${green(okGlyph)} settings.json schema: known keys only`);
@@ -1467,17 +1500,33 @@ function reportHostOverrides(section2, base, settings) {
1467
1500
  // src/commands.doctor.checks.pathmap.ts
1468
1501
  init_color();
1469
1502
  init_config();
1470
- import { existsSync as existsSync8 } from "node:fs";
1503
+ import { existsSync as existsSync8, readdirSync as readdirSync3 } from "node:fs";
1471
1504
  import { join as join9 } from "node:path";
1472
1505
  init_utils_json();
1473
1506
  function reportMappedProjects(section2, map) {
1474
1507
  const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
1475
- addItem(
1476
- section2,
1477
- `${dim(infoGlyph)} mapped projects for ${cyan(HOST)}: ${dim(String(mapped.length))}`
1478
- );
1508
+ addItem(section2, `Mapped projects for ${cyan(HOST)}: ${dim(String(mapped.length))}`);
1479
1509
  for (const [name, hosts] of mapped) {
1480
- addItem(section2, ` ${name} -> ${blue(hosts[HOST])}`);
1510
+ addChildItem(section2, `${name} -> ${blue(hosts[HOST])}`);
1511
+ }
1512
+ }
1513
+ function reportUnmappedProjects(section2, map) {
1514
+ const localProjects = join9(CLAUDE_HOME, "projects");
1515
+ if (!existsSync8(localProjects)) return;
1516
+ let localDirs;
1517
+ try {
1518
+ localDirs = readdirSync3(localProjects);
1519
+ } catch {
1520
+ return;
1521
+ }
1522
+ const mappedEncodings = new Set(
1523
+ Object.values(map.projects).map((hosts) => hosts[HOST]).filter(Boolean).map((abspath) => encodePath(abspath))
1524
+ );
1525
+ const unmapped = localDirs.filter((dir) => !mappedEncodings.has(dir));
1526
+ if (unmapped.length === 0) return;
1527
+ addItem(section2, `Unmapped local projects (not synced): ${dim(String(unmapped.length))}`);
1528
+ for (const dir of unmapped) {
1529
+ addChildItem(section2, dim(dir));
1481
1530
  }
1482
1531
  }
1483
1532
  function reportPathCollisions(section2, map) {
@@ -1541,28 +1590,48 @@ function reportPathMap(section2) {
1541
1590
  }
1542
1591
  }
1543
1592
  reportMappedProjects(section2, map);
1593
+ reportUnmappedProjects(section2, map);
1544
1594
  reportPathCollisions(section2, map);
1545
1595
  }
1546
1596
  function reportNeverSync(section2) {
1547
- addItem(section2, `${dim(infoGlyph)} never-sync items: ${[...NEVER_SYNC].join(", ")}`);
1597
+ addItem(
1598
+ section2,
1599
+ `${dim(infoGlyph)} never-sync items: ${NEVER_SYNC.size} protected ${dim(
1600
+ "(https://funkadelic.github.io/claude-nomad/how-it-works/)"
1601
+ )}`
1602
+ );
1548
1603
  }
1549
1604
 
1550
1605
  // src/commands.doctor.checks.repository.ts
1551
1606
  init_color();
1552
1607
  init_config();
1553
1608
  import { execFileSync as execFileSync3 } from "node:child_process";
1554
- import { existsSync as existsSync10 } from "node:fs";
1555
- import { join as join12, relative as relative2 } from "node:path";
1609
+ import { existsSync as existsSync11 } from "node:fs";
1610
+ import { join as join13, relative as relative2 } from "node:path";
1611
+
1612
+ // src/commands.pull.wedge.ts
1613
+ import { existsSync as existsSync9 } from "node:fs";
1614
+ import { join as join10 } from "node:path";
1615
+ function detectWedge(repo) {
1616
+ const g = join10(repo, ".git");
1617
+ if (existsSync9(join10(g, "rebase-merge")) || existsSync9(join10(g, "rebase-apply"))) return "rebase";
1618
+ if (existsSync9(join10(g, "MERGE_HEAD"))) return "merge";
1619
+ return null;
1620
+ }
1621
+
1622
+ // src/commands.doctor.checks.repository.ts
1556
1623
  init_push_checks();
1557
1624
  init_utils();
1558
1625
  function reportGitleaksProbe(section2) {
1559
1626
  try {
1560
- const v = execFileSync3("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
1561
- addItem(section2, `${green(okGlyph)} gitleaks: ${dim(v)}`);
1627
+ execFileSync3("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
1562
1628
  return true;
1563
1629
  } catch (err) {
1564
1630
  if (err.code === "ENOENT") {
1565
- addItem(section2, `${yellow(warnGlyph)} gitleaks: not on PATH (required for nomad push)`);
1631
+ addItem(
1632
+ section2,
1633
+ `${yellow(warnGlyph)} gitleaks: not on PATH (required for nomad push; install: https://github.com/gitleaks/gitleaks)`
1634
+ );
1566
1635
  } else {
1567
1636
  addItem(section2, `${red(failGlyph)} gitleaks: probe failed: ${err.message}`);
1568
1637
  process.exitCode = 1;
@@ -1571,8 +1640,8 @@ function reportGitleaksProbe(section2) {
1571
1640
  }
1572
1641
  }
1573
1642
  function reportGitlinks(section2) {
1574
- const sharedDir = join12(REPO_HOME, "shared");
1575
- if (existsSync10(sharedDir)) {
1643
+ const sharedDir = join13(REPO_HOME, "shared");
1644
+ if (existsSync11(sharedDir)) {
1576
1645
  const gitlinks = findGitlinks(sharedDir);
1577
1646
  for (const p of gitlinks) {
1578
1647
  const rel = relative2(REPO_HOME, p);
@@ -1611,16 +1680,30 @@ function reportRebaseClean(section2) {
1611
1680
  } catch {
1612
1681
  }
1613
1682
  }
1683
+ function reportRebaseState(section2) {
1684
+ try {
1685
+ const wedge = detectWedge(REPO_HOME);
1686
+ if (wedge !== null) {
1687
+ const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
1688
+ addItem(
1689
+ section2,
1690
+ `${red(failGlyph)} repo is ${state}: run 'nomad pull --force-remote' to auto-recover`
1691
+ );
1692
+ process.exitCode = 1;
1693
+ }
1694
+ } catch {
1695
+ }
1696
+ }
1614
1697
 
1615
1698
  // src/commands.doctor.checks.backups.ts
1616
1699
  init_color();
1617
- import { existsSync as existsSync11, lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
1618
- import { join as join13 } from "node:path";
1700
+ import { existsSync as existsSync12, lstatSync as lstatSync5, readdirSync as readdirSync5 } from "node:fs";
1701
+ import { join as join14 } from "node:path";
1619
1702
  init_config();
1620
1703
  var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
1621
1704
  function safeReaddir(dir) {
1622
1705
  try {
1623
- return readdirSync4(dir);
1706
+ return readdirSync5(dir);
1624
1707
  } catch {
1625
1708
  return [];
1626
1709
  }
@@ -1631,7 +1714,7 @@ var BYTES_PER_MB = 1024 * 1024;
1631
1714
  function dirSizeBytes(dir) {
1632
1715
  let bytes = 0;
1633
1716
  for (const entry of safeReaddir(dir)) {
1634
- const full = join13(dir, entry);
1717
+ const full = join14(dir, entry);
1635
1718
  const st = lstatSync5(full, { throwIfNoEntry: false });
1636
1719
  if (!st) continue;
1637
1720
  if (st.isSymbolicLink()) continue;
@@ -1642,11 +1725,11 @@ function dirSizeBytes(dir) {
1642
1725
  }
1643
1726
  function totalSizeMb(backupBase, dirs) {
1644
1727
  let bytes = 0;
1645
- for (const name of dirs) bytes += dirSizeBytes(join13(backupBase, name));
1728
+ for (const name of dirs) bytes += dirSizeBytes(join14(backupBase, name));
1646
1729
  return bytes / BYTES_PER_MB;
1647
1730
  }
1648
1731
  function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
1649
- if (!existsSync11(backupBase)) return;
1732
+ if (!existsSync12(backupBase)) return;
1650
1733
  const dirs = safeReaddir(backupBase).filter((n) => TS_SHAPE2.test(n));
1651
1734
  const count = dirs.length;
1652
1735
  const sizeMb = totalSizeMb(backupBase, dirs);
@@ -1660,8 +1743,8 @@ function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
1660
1743
 
1661
1744
  // src/commands.doctor.check-schema.ts
1662
1745
  init_color();
1663
- import { existsSync as existsSync12 } from "node:fs";
1664
- import { join as join14 } from "node:path";
1746
+ import { existsSync as existsSync13 } from "node:fs";
1747
+ import { join as join15 } from "node:path";
1665
1748
  init_config();
1666
1749
 
1667
1750
  // src/http-fetch.ts
@@ -1698,8 +1781,8 @@ function fetchSchemaKeys() {
1698
1781
  }
1699
1782
  }
1700
1783
  function reportCheckSchema(section2) {
1701
- const settingsPath = join14(CLAUDE_HOME, "settings.json");
1702
- if (!existsSync12(settingsPath)) {
1784
+ const settingsPath = join15(CLAUDE_HOME, "settings.json");
1785
+ if (!existsSync13(settingsPath)) {
1703
1786
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
1704
1787
  return;
1705
1788
  }
@@ -1729,18 +1812,18 @@ function reportCheckSchema(section2) {
1729
1812
  init_color();
1730
1813
  import { randomBytes } from "node:crypto";
1731
1814
  import { execFileSync as execFileSync6 } from "node:child_process";
1732
- import { existsSync as existsSync14, mkdirSync as mkdirSync4, readdirSync as readdirSync6, rmSync as rmSync6 } from "node:fs";
1815
+ import { existsSync as existsSync15, mkdirSync as mkdirSync4, readdirSync as readdirSync7, rmSync as rmSync6 } from "node:fs";
1733
1816
  import { homedir as homedir4 } from "node:os";
1734
- import { join as join18 } from "node:path";
1817
+ import { join as join19 } from "node:path";
1735
1818
 
1736
1819
  // src/commands.doctor.check-shared.scan.ts
1737
1820
  init_color();
1738
- import { join as join16 } from "node:path";
1821
+ import { join as join17 } from "node:path";
1739
1822
  init_config();
1740
1823
  init_push_gitleaks();
1741
1824
  function scrubPath(logical, sid, logicalToEncoded) {
1742
1825
  const encoded = logicalToEncoded.get(logical) ?? logical;
1743
- return join16(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
1826
+ return join17(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
1744
1827
  }
1745
1828
  function reportSessionFindings(section2, bySession) {
1746
1829
  for (const [sid, counts] of bySession) {
@@ -1832,8 +1915,8 @@ init_config();
1832
1915
  init_utils();
1833
1916
  init_utils_fs();
1834
1917
  init_utils_json();
1835
- import { cpSync as cpSync3, existsSync as existsSync13, mkdirSync as mkdirSync3, readdirSync as readdirSync5, rmSync as rmSync5, statSync as statSync4 } from "node:fs";
1836
- import { join as join17, relative as relative3, sep } from "node:path";
1918
+ import { cpSync as cpSync3, existsSync as existsSync14, mkdirSync as mkdirSync3, readdirSync as readdirSync6, rmSync as rmSync5, statSync as statSync4 } from "node:fs";
1919
+ import { join as join18, relative as relative3, sep } from "node:path";
1837
1920
  function copyDir(src, dst) {
1838
1921
  rmSync5(dst, { recursive: true, force: true });
1839
1922
  cpSync3(src, dst, { recursive: true, force: true });
@@ -1863,15 +1946,15 @@ function remapPull(ts, opts = {}) {
1863
1946
  let unmapped = 0;
1864
1947
  const pulled = [];
1865
1948
  const wouldPull = [];
1866
- const mapPath = join17(REPO_HOME, "path-map.json");
1867
- const repoProjects = join17(REPO_HOME, "shared", "projects");
1868
- if (!existsSync13(mapPath) || !existsSync13(repoProjects)) {
1949
+ const mapPath = join18(REPO_HOME, "path-map.json");
1950
+ const repoProjects = join18(REPO_HOME, "shared", "projects");
1951
+ if (!existsSync14(mapPath) || !existsSync14(repoProjects)) {
1869
1952
  const text = "no path-map or repo projects dir; skipping session remap";
1870
1953
  emitPreview(opts.onPreview, { kind: "note", text }, text);
1871
1954
  return { unmapped: 0, pulled, wouldPull };
1872
1955
  }
1873
1956
  const map = readJson(mapPath);
1874
- const localProjects = join17(CLAUDE_HOME, "projects");
1957
+ const localProjects = join18(CLAUDE_HOME, "projects");
1875
1958
  if (!dryRun) mkdirSync3(localProjects, { recursive: true });
1876
1959
  for (const [logical, hosts] of Object.entries(map.projects)) {
1877
1960
  assertSafeLogical(logical);
@@ -1880,9 +1963,9 @@ function remapPull(ts, opts = {}) {
1880
1963
  unmapped++;
1881
1964
  continue;
1882
1965
  }
1883
- const src = join17(repoProjects, logical);
1884
- if (!existsSync13(src)) continue;
1885
- const dst = join17(localProjects, encodePath(localPath));
1966
+ const src = join18(repoProjects, logical);
1967
+ if (!existsSync14(src)) continue;
1968
+ const dst = join18(localProjects, encodePath(localPath));
1886
1969
  if (dryRun) {
1887
1970
  wouldPull.push(logical);
1888
1971
  emitPreview(
@@ -1927,30 +2010,30 @@ function remapPush(ts, opts = {}) {
1927
2010
  let unmapped = 0;
1928
2011
  const pushed = [];
1929
2012
  const wouldPush = [];
1930
- const mapPath = join17(REPO_HOME, "path-map.json");
1931
- if (!existsSync13(mapPath)) {
2013
+ const mapPath = join18(REPO_HOME, "path-map.json");
2014
+ if (!existsSync14(mapPath)) {
1932
2015
  log("no path-map.json; skipping session export");
1933
2016
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
1934
2017
  }
1935
2018
  const map = readJson(mapPath);
1936
- const localProjects = join17(CLAUDE_HOME, "projects");
1937
- const repoProjects = join17(REPO_HOME, "shared", "projects");
2019
+ const localProjects = join18(CLAUDE_HOME, "projects");
2020
+ const repoProjects = join18(REPO_HOME, "shared", "projects");
1938
2021
  const reverse = buildReverseMap(map);
1939
- if (!existsSync13(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
2022
+ if (!existsSync14(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
1940
2023
  if (!dryRun) mkdirSync3(repoProjects, { recursive: true });
1941
- for (const dir of readdirSync5(localProjects)) {
2024
+ for (const dir of readdirSync6(localProjects)) {
1942
2025
  const logical = reverse.get(dir);
1943
2026
  if (!logical) {
1944
2027
  unmapped++;
1945
2028
  continue;
1946
2029
  }
1947
- const repoDst = join17(repoProjects, logical);
2030
+ const repoDst = join18(repoProjects, logical);
1948
2031
  if (dryRun) {
1949
2032
  wouldPush.push(logical);
1950
2033
  continue;
1951
2034
  }
1952
2035
  backupRepoWrite(repoDst, ts, REPO_HOME);
1953
- copyDirJsonlOnly(join17(localProjects, dir), repoDst);
2036
+ copyDirJsonlOnly(join18(localProjects, dir), repoDst);
1954
2037
  pushed.push(logical);
1955
2038
  }
1956
2039
  return { unmapped, collisions: 0, pushed, wouldPush };
@@ -1962,8 +2045,8 @@ init_utils_json();
1962
2045
  function buildScanTree(tmpRoot) {
1963
2046
  const logicalToEncoded = /* @__PURE__ */ new Map();
1964
2047
  let staged = 0;
1965
- const mapPath = join18(REPO_HOME, "path-map.json");
1966
- if (!existsSync14(mapPath)) return { logicalToEncoded, staged, malformed: false };
2048
+ const mapPath = join19(REPO_HOME, "path-map.json");
2049
+ if (!existsSync15(mapPath)) return { logicalToEncoded, staged, malformed: false };
1967
2050
  let map;
1968
2051
  try {
1969
2052
  map = readJson(mapPath);
@@ -1980,12 +2063,12 @@ function buildScanTree(tmpRoot) {
1980
2063
  if (!p || p === "TBD") continue;
1981
2064
  reverse.set(encodePath(p), logical);
1982
2065
  }
1983
- const localProjects = join18(CLAUDE_HOME, "projects");
1984
- if (!existsSync14(localProjects)) return { logicalToEncoded, staged, malformed: false };
1985
- for (const dir of readdirSync6(localProjects)) {
2066
+ const localProjects = join19(CLAUDE_HOME, "projects");
2067
+ if (!existsSync15(localProjects)) return { logicalToEncoded, staged, malformed: false };
2068
+ for (const dir of readdirSync7(localProjects)) {
1986
2069
  const logical = reverse.get(dir);
1987
2070
  if (!logical) continue;
1988
- copyDirJsonlOnly(join18(localProjects, dir), join18(tmpRoot, "shared", "projects", logical));
2071
+ copyDirJsonlOnly(join19(localProjects, dir), join19(tmpRoot, "shared", "projects", logical));
1989
2072
  logicalToEncoded.set(logical, dir);
1990
2073
  staged++;
1991
2074
  }
@@ -2016,11 +2099,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
2016
2099
  }
2017
2100
  function reportCheckShared(section2, gitleaksReady) {
2018
2101
  if (!ensureGitleaksReady(section2, gitleaksReady)) return;
2019
- const cacheDir = join18(homedir4(), ".cache", "claude-nomad");
2102
+ const cacheDir = join19(homedir4(), ".cache", "claude-nomad");
2020
2103
  mkdirSync4(cacheDir, { recursive: true });
2021
2104
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes(4).toString("hex")}`;
2022
- const reportPath = join18(cacheDir, `check-shared-${stamp}.json`);
2023
- const tmpRoot = join18(cacheDir, `check-shared-tree-${stamp}`);
2105
+ const reportPath = join19(cacheDir, `check-shared-${stamp}.json`);
2106
+ const tmpRoot = join19(cacheDir, `check-shared-tree-${stamp}`);
2024
2107
  try {
2025
2108
  const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
2026
2109
  if (malformed) {
@@ -2041,8 +2124,8 @@ function reportCheckShared(section2, gitleaksReady) {
2041
2124
 
2042
2125
  // src/commands.doctor.checks.hooks.scope.ts
2043
2126
  init_color();
2044
- import { existsSync as existsSync15, readFileSync as readFileSync5, readdirSync as readdirSync7, realpathSync } from "node:fs";
2045
- import { dirname as dirname2, extname, join as join19 } from "node:path";
2127
+ import { existsSync as existsSync16, readFileSync as readFileSync5, readdirSync as readdirSync8, realpathSync } from "node:fs";
2128
+ import { dirname as dirname2, extname, join as join20 } from "node:path";
2046
2129
  init_config();
2047
2130
  function typeFromPackageJson(pkgPath) {
2048
2131
  try {
@@ -2064,8 +2147,8 @@ function effectiveType(hookPath) {
2064
2147
  }
2065
2148
  let dir = dirname2(real);
2066
2149
  for (; ; ) {
2067
- const pkg = join19(dir, "package.json");
2068
- if (existsSync15(pkg)) return typeFromPackageJson(pkg);
2150
+ const pkg = join20(dir, "package.json");
2151
+ if (existsSync16(pkg)) return typeFromPackageJson(pkg);
2069
2152
  const parent = dirname2(dir);
2070
2153
  if (parent === dir) return "cjs";
2071
2154
  dir = parent;
@@ -2094,7 +2177,7 @@ function remedy(family) {
2094
2177
  }
2095
2178
  function safeReaddir2(dir) {
2096
2179
  try {
2097
- return readdirSync7(dir);
2180
+ return readdirSync8(dir);
2098
2181
  } catch {
2099
2182
  return [];
2100
2183
  }
@@ -2107,15 +2190,15 @@ function safeRead(path) {
2107
2190
  }
2108
2191
  }
2109
2192
  function reportHookScopeCheck(section2) {
2110
- const hooksDir = join19(CLAUDE_HOME, "hooks");
2111
- if (!existsSync15(hooksDir)) {
2193
+ const hooksDir = join20(CLAUDE_HOME, "hooks");
2194
+ if (!existsSync16(hooksDir)) {
2112
2195
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
2113
2196
  return;
2114
2197
  }
2115
2198
  let anyWarn = false;
2116
2199
  for (const name of safeReaddir2(hooksDir)) {
2117
2200
  if (extname(name) !== ".js") continue;
2118
- const abs = join19(hooksDir, name);
2201
+ const abs = join20(hooksDir, name);
2119
2202
  const eff = effectiveType(abs);
2120
2203
  if (eff === null) continue;
2121
2204
  const src = safeRead(abs);
@@ -2135,8 +2218,8 @@ function reportHookScopeCheck(section2) {
2135
2218
 
2136
2219
  // src/commands.doctor.checks.hooks.ts
2137
2220
  init_color();
2138
- import { existsSync as existsSync16 } from "node:fs";
2139
- import { join as join20 } from "node:path";
2221
+ import { existsSync as existsSync17 } from "node:fs";
2222
+ import { join as join21 } from "node:path";
2140
2223
  init_config();
2141
2224
  function expandHome(token) {
2142
2225
  return token.replace(/^\$\{HOME\}/, HOME).replace(/^\$HOME/, HOME).replace(/^~/, HOME);
@@ -2174,8 +2257,11 @@ function checkEventGroups(section2, event, groups) {
2174
2257
  for (const group of groups) {
2175
2258
  for (const cmd of commandsFromGroup(group)) {
2176
2259
  for (const resolved of claudePathsIn(cmd)) {
2177
- if (existsSync16(resolved)) continue;
2178
- addItem(section2, `${red(failGlyph)} hooks/${event}: command target missing: ${resolved}`);
2260
+ if (existsSync17(resolved)) continue;
2261
+ addItem(
2262
+ section2,
2263
+ `${red(failGlyph)} hooks/${event}: command target missing: ${resolved} (run nomad pull)`
2264
+ );
2179
2265
  process.exitCode = 1;
2180
2266
  anyFail = true;
2181
2267
  }
@@ -2184,8 +2270,8 @@ function checkEventGroups(section2, event, groups) {
2184
2270
  return anyFail;
2185
2271
  }
2186
2272
  function reportHooksTargetCheck(section2) {
2187
- const settingsPath = join20(CLAUDE_HOME, "settings.json");
2188
- if (!existsSync16(settingsPath)) {
2273
+ const settingsPath = join21(CLAUDE_HOME, "settings.json");
2274
+ if (!existsSync17(settingsPath)) {
2189
2275
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
2190
2276
  return;
2191
2277
  }
@@ -2206,17 +2292,221 @@ function reportHooksTargetCheck(section2) {
2206
2292
  }
2207
2293
  }
2208
2294
 
2295
+ // src/commands.doctor.checks.hooks.preserve-symlinks.ts
2296
+ init_color();
2297
+ import { existsSync as existsSync19, readFileSync as readFileSync6 } from "node:fs";
2298
+ import { join as join23 } from "node:path";
2299
+
2300
+ // src/commands.doctor.checks.hooks.preserve-symlinks.probe.ts
2301
+ import { closeSync as closeSync2, existsSync as existsSync18, openSync as openSync2, readSync, realpathSync as realpathSync2 } from "node:fs";
2302
+ import { dirname as dirname3, join as join22, resolve as resolve2 } from "node:path";
2303
+ function suppressedRanges(src) {
2304
+ const ranges = [];
2305
+ const re = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'[^']*'|"[^"]*"|`[^`]*`/g;
2306
+ let m;
2307
+ while ((m = re.exec(src)) !== null) {
2308
+ ranges.push([m.index, m.index + m[0].length]);
2309
+ }
2310
+ return ranges;
2311
+ }
2312
+ function inSuppressedRange(pos, ranges) {
2313
+ for (const [start, end] of ranges) {
2314
+ if (pos >= start && pos < end) return true;
2315
+ }
2316
+ return false;
2317
+ }
2318
+ function topRelativeSpecifiers(src) {
2319
+ const ranges = suppressedRanges(src);
2320
+ const specifiers = [];
2321
+ const reqRe = /\brequire\s*\(\s*(['"])(\.\.?\/[^'"]*)\1\s*\)/g;
2322
+ let m;
2323
+ while ((m = reqRe.exec(src)) !== null) {
2324
+ if (!inSuppressedRange(m.index, ranges)) specifiers.push(m[2]);
2325
+ }
2326
+ const impRe = /\bfrom\s+(['"])(\.\.?\/[^'"]*)\1/g;
2327
+ while ((m = impRe.exec(src)) !== null) {
2328
+ if (!inSuppressedRange(m.index, ranges)) specifiers.push(m[2]);
2329
+ }
2330
+ return specifiers;
2331
+ }
2332
+ function specifierIsMissing(specifier, baseDir) {
2333
+ const base = resolve2(baseDir, specifier);
2334
+ if (existsSync18(base)) return false;
2335
+ for (const ext of [".js", ".cjs", ".mjs"]) {
2336
+ if (existsSync18(base + ext)) return false;
2337
+ }
2338
+ for (const idx of ["index.js", "index.cjs", "index.mjs"]) {
2339
+ if (existsSync18(join22(base, idx))) return false;
2340
+ }
2341
+ return true;
2342
+ }
2343
+ function relativeRequireTargetsBroken(scriptPath) {
2344
+ let realPath;
2345
+ try {
2346
+ realPath = realpathSync2(scriptPath);
2347
+ } catch {
2348
+ return false;
2349
+ }
2350
+ let raw;
2351
+ try {
2352
+ const fd = openSync2(realPath, "r");
2353
+ try {
2354
+ const buf = Buffer.alloc(65536);
2355
+ const bytesRead = readSync(fd, buf, 0, 65536, 0);
2356
+ raw = buf.toString("utf8", 0, bytesRead);
2357
+ } finally {
2358
+ closeSync2(fd);
2359
+ }
2360
+ } catch {
2361
+ return false;
2362
+ }
2363
+ const specifiers = topRelativeSpecifiers(raw);
2364
+ if (specifiers.length === 0) return false;
2365
+ const baseDir = dirname3(realPath);
2366
+ for (const spec of specifiers) {
2367
+ if (specifierIsMissing(spec, baseDir)) return true;
2368
+ }
2369
+ return false;
2370
+ }
2371
+
2372
+ // src/commands.doctor.checks.hooks.preserve-symlinks.ts
2373
+ init_config();
2374
+ function expandHome2(token) {
2375
+ return token.replace(/^\$\{HOME\}/, HOME).replace(/^\$HOME/, HOME).replace(/^~/, HOME);
2376
+ }
2377
+ function stripShellPunct(token) {
2378
+ const head = token.replace(/^['"]+/, "");
2379
+ let end = head.length;
2380
+ while (end > 0 && "'\"`;)|&>".includes(head[end - 1])) end--;
2381
+ return head.slice(0, end);
2382
+ }
2383
+ function commandTokens(command) {
2384
+ const tokens = [];
2385
+ for (const seg of command.split(/&&|\|\||;|\|/)) {
2386
+ for (const raw of seg.trim().split(/\s+/).filter(Boolean)) {
2387
+ tokens.push(expandHome2(stripShellPunct(raw)));
2388
+ }
2389
+ }
2390
+ return tokens;
2391
+ }
2392
+ function readPathMapSafe() {
2393
+ const mapPath = join23(REPO_HOME, "path-map.json");
2394
+ if (!existsSync19(mapPath)) return { projects: {} };
2395
+ try {
2396
+ return JSON.parse(readFileSync6(mapPath, "utf8"));
2397
+ } catch {
2398
+ return { projects: {} };
2399
+ }
2400
+ }
2401
+ function resolvesUnderSymlinkedShared(scriptPath, sharedLinkNames) {
2402
+ for (const name of sharedLinkNames) {
2403
+ const prefix = `${CLAUDE_HOME}/${name}/`;
2404
+ if (scriptPath.startsWith(prefix)) return true;
2405
+ }
2406
+ return false;
2407
+ }
2408
+ function nodeScriptArg(tokens, nodeIdx) {
2409
+ for (let i = nodeIdx + 1; i < tokens.length; i++) {
2410
+ const t = tokens[i];
2411
+ if (t.startsWith("-")) continue;
2412
+ if (t.endsWith(".js") || t.endsWith(".cjs")) return t;
2413
+ break;
2414
+ }
2415
+ return null;
2416
+ }
2417
+ function hasPreserveSymlinksMain(tokens, nodeIdx) {
2418
+ for (let i = nodeIdx + 1; i < tokens.length; i++) {
2419
+ if (tokens[i] === "--preserve-symlinks-main") return true;
2420
+ if (!tokens[i].startsWith("-")) break;
2421
+ }
2422
+ return false;
2423
+ }
2424
+ function* commandsFromFlatEntries(entries) {
2425
+ for (const entry of entries) {
2426
+ if (typeof entry !== "object" || entry === null) continue;
2427
+ const e = entry;
2428
+ if (e.type === "command" && typeof e.command === "string") yield e.command;
2429
+ }
2430
+ }
2431
+ function* commandsFromOneGroup(group) {
2432
+ if (typeof group !== "object" || group === null) return;
2433
+ const g = group;
2434
+ if (Array.isArray(g.hooks)) {
2435
+ yield* commandsFromFlatEntries(g.hooks);
2436
+ return;
2437
+ }
2438
+ if (g.type === "command" && typeof g.command === "string") yield g.command;
2439
+ }
2440
+ function flaggedScript(command, sharedLinkNames) {
2441
+ const tokens = commandTokens(command);
2442
+ const nodeIdx = tokens.indexOf("node");
2443
+ if (nodeIdx < 0) return null;
2444
+ if (hasPreserveSymlinksMain(tokens, nodeIdx)) return null;
2445
+ const script = nodeScriptArg(tokens, nodeIdx);
2446
+ if (script === null) return null;
2447
+ if (!resolvesUnderSymlinkedShared(script, sharedLinkNames)) return null;
2448
+ if (!relativeRequireTargetsBroken(script)) return null;
2449
+ return script;
2450
+ }
2451
+ function checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames) {
2452
+ let anyWarn = false;
2453
+ for (const group of groups) {
2454
+ for (const cmd of commandsFromOneGroup(group)) {
2455
+ const script = flaggedScript(cmd, sharedLinkNames);
2456
+ if (script === null) continue;
2457
+ addItem(
2458
+ section2,
2459
+ `${yellow(warnGlyph)} hooks/${event}: node ${script} needs --preserve-symlinks-main (add it to the hook command in shared/settings.base.json)`
2460
+ );
2461
+ anyWarn = true;
2462
+ }
2463
+ }
2464
+ return anyWarn;
2465
+ }
2466
+ function reportPreserveSymlinksCheck(section2) {
2467
+ const settingsPath = join23(CLAUDE_HOME, "settings.json");
2468
+ if (!existsSync19(settingsPath)) {
2469
+ addItem(
2470
+ section2,
2471
+ `${dim(infoGlyph)} no ~/.claude/settings.json; skipping preserve-symlinks-main check`
2472
+ );
2473
+ return;
2474
+ }
2475
+ let settings;
2476
+ try {
2477
+ settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
2478
+ } catch {
2479
+ return;
2480
+ }
2481
+ if (settings === null) return;
2482
+ const hooks = settings.hooks;
2483
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
2484
+ addItem(section2, `${green(okGlyph)} hooks: preserve-symlinks-main not needed`);
2485
+ return;
2486
+ }
2487
+ const map = readPathMapSafe();
2488
+ const sharedLinkNames = allSharedLinks(map);
2489
+ let anyWarn = false;
2490
+ for (const [event, groups] of Object.entries(hooks)) {
2491
+ if (!Array.isArray(groups)) continue;
2492
+ if (checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames)) anyWarn = true;
2493
+ }
2494
+ if (!anyWarn) {
2495
+ addItem(section2, `${green(okGlyph)} hooks: preserve-symlinks-main not needed`);
2496
+ }
2497
+ }
2498
+
2209
2499
  // src/commands.doctor.ts
2210
2500
  init_config();
2211
2501
 
2212
2502
  // src/commands.doctor.engine.ts
2213
2503
  init_color();
2214
- import { readFileSync as readFileSync7 } from "node:fs";
2504
+ import { readFileSync as readFileSync8 } from "node:fs";
2215
2505
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2216
2506
 
2217
2507
  // src/commands.doctor.version.ts
2218
2508
  init_color();
2219
- import { readFileSync as readFileSync6 } from "node:fs";
2509
+ import { readFileSync as readFileSync7 } from "node:fs";
2220
2510
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2221
2511
  init_config();
2222
2512
  var STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
@@ -2233,7 +2523,7 @@ function compareSemver(a, b) {
2233
2523
  function readLocalVersion() {
2234
2524
  try {
2235
2525
  const pkgPath = fileURLToPath2(new URL("../package.json", import.meta.url));
2236
- const parsed = JSON.parse(readFileSync6(pkgPath, "utf8"));
2526
+ const parsed = JSON.parse(readFileSync7(pkgPath, "utf8"));
2237
2527
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
2238
2528
  return parsed.version;
2239
2529
  }
@@ -2260,7 +2550,13 @@ function reportVersionCheck(section2) {
2260
2550
  const localPure = STRICT_SEMVER_PREFIX.exec(local)?.[1] ?? null;
2261
2551
  if (localPure === null) return;
2262
2552
  const latest = fetchLatestVersion();
2263
- if (latest === null) return;
2553
+ if (latest === null) {
2554
+ addItem(
2555
+ section2,
2556
+ `${dim(infoGlyph)} claude-nomad: ${local} (version check skipped: could not determine latest version)`
2557
+ );
2558
+ return;
2559
+ }
2264
2560
  const cmp = compareSemver(localPure, latest);
2265
2561
  if (cmp === 0) {
2266
2562
  addItem(section2, `${green(okGlyph)} claude-nomad: ${local} (latest)`);
@@ -2286,7 +2582,7 @@ function parseMinVersion(spec) {
2286
2582
  function readEnginesNode() {
2287
2583
  try {
2288
2584
  const pkgPath = fileURLToPath3(new URL("../package.json", import.meta.url));
2289
- const parsed = JSON.parse(readFileSync7(pkgPath, "utf8"));
2585
+ const parsed = JSON.parse(readFileSync8(pkgPath, "utf8"));
2290
2586
  const node = parsed.engines?.node;
2291
2587
  if (typeof node === "string" && node.length > 0) return node;
2292
2588
  return null;
@@ -2315,8 +2611,8 @@ function reportNodeEngineCheck(section2) {
2315
2611
  // src/commands.doctor.gitleaks-version.ts
2316
2612
  init_color();
2317
2613
  import { execFileSync as execFileSync7 } from "node:child_process";
2318
- import { existsSync as existsSync17 } from "node:fs";
2319
- import { join as join21 } from "node:path";
2614
+ import { existsSync as existsSync20 } from "node:fs";
2615
+ import { join as join24 } from "node:path";
2320
2616
  init_config();
2321
2617
  var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
2322
2618
  var GITLEAKS_TIMEOUT_MS = 5e3;
@@ -2325,7 +2621,7 @@ function majorMinorOf(value) {
2325
2621
  return m === null ? null : [m[1], m[2]];
2326
2622
  }
2327
2623
  function readGitleaksVersion(run, tomlExists) {
2328
- const tomlPath = join21(REPO_HOME, ".gitleaks.toml");
2624
+ const tomlPath = join24(REPO_HOME, ".gitleaks.toml");
2329
2625
  const args = ["version"];
2330
2626
  if (tomlExists(tomlPath)) args.push("--config", tomlPath);
2331
2627
  try {
@@ -2337,7 +2633,7 @@ function readGitleaksVersion(run, tomlExists) {
2337
2633
  return null;
2338
2634
  }
2339
2635
  }
2340
- function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync17) {
2636
+ function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync20) {
2341
2637
  const raw = readGitleaksVersion(run, tomlExists);
2342
2638
  if (raw === null) return;
2343
2639
  const local = majorMinorOf(raw);
@@ -2504,19 +2800,47 @@ function reportActionsDrift(section2, run = execFileSync10) {
2504
2800
  );
2505
2801
  }
2506
2802
 
2803
+ // src/commands.doctor.verdict.ts
2804
+ init_color();
2805
+ function isFailLine(item) {
2806
+ return item.includes(failGlyph);
2807
+ }
2808
+ function isWarnLine(item) {
2809
+ return !isFailLine(item) && item.includes(warnGlyph);
2810
+ }
2811
+ function buildVerdictSection(sections) {
2812
+ const summary = section("Summary");
2813
+ const lines = sections.flatMap((s) => s.items).map((item) => item.replace(/^\t/, ""));
2814
+ const failures = lines.filter(isFailLine);
2815
+ const warnings = lines.filter(isWarnLine);
2816
+ for (const line of [...failures, ...warnings]) addItem(summary, line);
2817
+ if (failures.length > 0) {
2818
+ addItem(
2819
+ summary,
2820
+ `${red(failGlyph)} ${failures.length} failure(s), ${warnings.length} warning(s)`
2821
+ );
2822
+ } else if (warnings.length > 0) {
2823
+ addItem(summary, `${yellow(warnGlyph)} ${warnings.length} warning(s)`);
2824
+ } else {
2825
+ addItem(summary, `${green(okGlyph)} healthy`);
2826
+ }
2827
+ return summary;
2828
+ }
2829
+
2507
2830
  // src/commands.doctor.ts
2508
2831
  function cmdDoctor(opts = {}) {
2509
- const host = section("Host");
2832
+ const host = section("Environment");
2510
2833
  reportHostAndPaths(host);
2511
2834
  reportRepoState(host);
2512
2835
  const links = section("Shared links");
2513
- const mapPath = join22(REPO_HOME, "path-map.json");
2514
- const rawMap = existsSync18(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
2836
+ const mapPath = join25(REPO_HOME, "path-map.json");
2837
+ const rawMap = existsSync21(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
2515
2838
  const map = rawMap ?? { projects: {} };
2516
2839
  reportSharedLinks(links, map);
2517
2840
  const hooksScan = section("Hook targets");
2518
2841
  reportHooksTargetCheck(hooksScan);
2519
2842
  reportHookScopeCheck(hooksScan);
2843
+ reportPreserveSymlinksCheck(hooksScan);
2520
2844
  const settings = section("Settings");
2521
2845
  const base = loadBaseSettings(settings);
2522
2846
  const parsedSettings = loadAndReportSettings(settings);
@@ -2530,10 +2854,12 @@ function cmdDoctor(opts = {}) {
2530
2854
  reportGitlinks(repository);
2531
2855
  reportRemote(repository);
2532
2856
  reportRebaseClean(repository);
2857
+ reportRebaseState(repository);
2533
2858
  reportActionsDrift(repository);
2534
2859
  const nomadVersion = section("Nomad Version");
2535
2860
  reportVersionCheck(nomadVersion);
2536
- reportBackupsCheck(nomadVersion);
2861
+ const housekeeping = section("Housekeeping");
2862
+ reportBackupsCheck(housekeeping);
2537
2863
  const depVersions = section("Dependency Versions");
2538
2864
  reportNodeEngineCheck(depVersions);
2539
2865
  reportGitleaksVersionCheck(depVersions);
@@ -2542,7 +2868,7 @@ function cmdDoctor(opts = {}) {
2542
2868
  if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
2543
2869
  const schemaScan = section("Schema scan");
2544
2870
  if (opts.checkSchema === true) reportCheckSchema(schemaScan);
2545
- renderDoctor([
2871
+ const body = [
2546
2872
  nomadVersion,
2547
2873
  depVersions,
2548
2874
  host,
@@ -2552,16 +2878,18 @@ function cmdDoctor(opts = {}) {
2552
2878
  pathMap,
2553
2879
  neverSync,
2554
2880
  repository,
2881
+ housekeeping,
2555
2882
  sharedScan,
2556
2883
  schemaScan
2557
- ]);
2884
+ ];
2885
+ renderDoctor([...body, buildVerdictSection(body)]);
2558
2886
  }
2559
2887
 
2560
2888
  // src/commands.drop-session.ts
2561
2889
  init_config();
2562
2890
  import { execFileSync as execFileSync12 } from "node:child_process";
2563
- import { existsSync as existsSync20, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
2564
- import { join as join25, relative as relative4 } from "node:path";
2891
+ import { existsSync as existsSync23, readdirSync as readdirSync9, statSync as statSync5 } from "node:fs";
2892
+ import { join as join28, relative as relative4 } from "node:path";
2565
2893
 
2566
2894
  // src/commands.drop-session.git.ts
2567
2895
  init_config();
@@ -2604,8 +2932,8 @@ function isInIndex(rel) {
2604
2932
  init_config();
2605
2933
  init_utils();
2606
2934
  init_utils_json();
2607
- import { existsSync as existsSync19 } from "node:fs";
2608
- import { join as join23 } from "node:path";
2935
+ import { existsSync as existsSync22 } from "node:fs";
2936
+ import { join as join26 } from "node:path";
2609
2937
  var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
2610
2938
  function reportScrubHint(id, matches) {
2611
2939
  const live = resolveLiveTranscript(id, matches);
@@ -2621,16 +2949,16 @@ function reportScrubHint(id, matches) {
2621
2949
  }
2622
2950
  function resolveLiveTranscript(id, matches) {
2623
2951
  try {
2624
- const mapPath = join23(REPO_HOME, "path-map.json");
2625
- if (!existsSync19(mapPath)) return null;
2952
+ const mapPath = join26(REPO_HOME, "path-map.json");
2953
+ if (!existsSync22(mapPath)) return null;
2626
2954
  const projects = readJson(mapPath).projects;
2627
2955
  for (const rel of matches) {
2628
2956
  const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
2629
2957
  if (logical === void 0) continue;
2630
2958
  const abs = projects[logical]?.[HOST];
2631
2959
  if (abs === void 0) continue;
2632
- const live = join23(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2633
- if (existsSync19(live)) return live;
2960
+ const live = join26(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2961
+ if (existsSync22(live)) return live;
2634
2962
  }
2635
2963
  return null;
2636
2964
  } catch {
@@ -2644,18 +2972,18 @@ init_utils();
2644
2972
  // src/utils.lockfile.ts
2645
2973
  init_config();
2646
2974
  init_utils();
2647
- import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
2648
- import { dirname as dirname3, join as join24 } from "node:path";
2649
- var LOCK_PATH = join24(HOME, ".cache", "claude-nomad", "nomad.lock");
2975
+ import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
2976
+ import { dirname as dirname4, join as join27 } from "node:path";
2977
+ var LOCK_PATH = join27(HOME, ".cache", "claude-nomad", "nomad.lock");
2650
2978
  function acquireLock(verb) {
2651
- mkdirSync5(dirname3(LOCK_PATH), { recursive: true });
2979
+ mkdirSync5(dirname4(LOCK_PATH), { recursive: true });
2652
2980
  try {
2653
- const fd = openSync2(LOCK_PATH, "wx");
2981
+ const fd = openSync3(LOCK_PATH, "wx");
2654
2982
  try {
2655
2983
  writeFileSync3(fd, String(process.pid));
2656
2984
  } catch (writeErr) {
2657
2985
  try {
2658
- closeSync2(fd);
2986
+ closeSync3(fd);
2659
2987
  } catch {
2660
2988
  }
2661
2989
  try {
@@ -2674,7 +3002,7 @@ function acquireLock(verb) {
2674
3002
  function releaseLock(handle) {
2675
3003
  if (handle === null) return;
2676
3004
  try {
2677
- closeSync2(handle.fd);
3005
+ closeSync3(handle.fd);
2678
3006
  } catch {
2679
3007
  }
2680
3008
  try {
@@ -2686,7 +3014,7 @@ function releaseLock(handle) {
2686
3014
  function unlinkIfSamePid(expectedPidStr) {
2687
3015
  let current;
2688
3016
  try {
2689
- current = readFileSync8(LOCK_PATH, "utf8").trim();
3017
+ current = readFileSync9(LOCK_PATH, "utf8").trim();
2690
3018
  } catch {
2691
3019
  return false;
2692
3020
  }
@@ -2701,7 +3029,7 @@ function unlinkIfSamePid(expectedPidStr) {
2701
3029
  function checkStaleAndRetry(verb) {
2702
3030
  let pidStr;
2703
3031
  try {
2704
- pidStr = readFileSync8(LOCK_PATH, "utf8").trim();
3032
+ pidStr = readFileSync9(LOCK_PATH, "utf8").trim();
2705
3033
  } catch {
2706
3034
  pidStr = "";
2707
3035
  }
@@ -2728,12 +3056,12 @@ function checkStaleAndRetry(verb) {
2728
3056
  }
2729
3057
  function retryOnce(verb) {
2730
3058
  try {
2731
- const fd = openSync2(LOCK_PATH, "wx");
3059
+ const fd = openSync3(LOCK_PATH, "wx");
2732
3060
  try {
2733
3061
  writeFileSync3(fd, String(process.pid));
2734
3062
  } catch {
2735
3063
  try {
2736
- closeSync2(fd);
3064
+ closeSync3(fd);
2737
3065
  } catch {
2738
3066
  }
2739
3067
  try {
@@ -2756,12 +3084,12 @@ function cmdDropSession(id) {
2756
3084
  fail(`invalid session id: ${id}`);
2757
3085
  process.exit(1);
2758
3086
  }
2759
- if (!existsSync20(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3087
+ if (!existsSync23(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2760
3088
  const handle = acquireLock("drop-session");
2761
3089
  if (handle === null) process.exit(0);
2762
3090
  try {
2763
- const repoProjects = join25(REPO_HOME, "shared", "projects");
2764
- if (!existsSync20(repoProjects)) {
3091
+ const repoProjects = join28(REPO_HOME, "shared", "projects");
3092
+ if (!existsSync23(repoProjects)) {
2765
3093
  throw new NomadFatal(`no staged session matches ${id}`);
2766
3094
  }
2767
3095
  const matches = collectMatches(repoProjects, id);
@@ -2782,13 +3110,13 @@ function cmdDropSession(id) {
2782
3110
  }
2783
3111
  function collectMatches(repoProjects, id) {
2784
3112
  const matches = [];
2785
- for (const logical of readdirSync8(repoProjects)) {
2786
- const candidate = join25(repoProjects, logical, `${id}.jsonl`);
2787
- if (existsSync20(candidate)) {
3113
+ for (const logical of readdirSync9(repoProjects)) {
3114
+ const candidate = join28(repoProjects, logical, `${id}.jsonl`);
3115
+ if (existsSync23(candidate)) {
2788
3116
  matches.push(relative4(REPO_HOME, candidate));
2789
3117
  }
2790
- const dir = join25(repoProjects, logical, id);
2791
- if (existsSync20(dir) && statSync5(dir).isDirectory()) {
3118
+ const dir = join28(repoProjects, logical, id);
3119
+ if (existsSync23(dir) && statSync5(dir).isDirectory()) {
2792
3120
  const dirRel = relative4(REPO_HOME, dir);
2793
3121
  const staged = expandStagedDir(dirRel);
2794
3122
  if (staged.length > 0) matches.push(...staged);
@@ -2824,19 +3152,19 @@ function unstageOne(rel) {
2824
3152
 
2825
3153
  // src/commands.redact.ts
2826
3154
  init_config();
2827
- import { existsSync as existsSync22, statSync as statSync7 } from "node:fs";
2828
- import { dirname as dirname4, join as join27 } from "node:path";
3155
+ import { existsSync as existsSync25, statSync as statSync7 } from "node:fs";
3156
+ import { dirname as dirname5, join as join30 } from "node:path";
2829
3157
 
2830
3158
  // src/commands.redact.subtree.ts
2831
- import { existsSync as existsSync21, lstatSync as lstatSync6, readFileSync as readFileSync9, readdirSync as readdirSync9, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
2832
- import { join as join26 } from "node:path";
3159
+ import { existsSync as existsSync24, lstatSync as lstatSync6, readFileSync as readFileSync10, readdirSync as readdirSync10, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
3160
+ import { join as join29 } from "node:path";
2833
3161
  init_utils_fs();
2834
3162
  function collectFiles(dir, out) {
2835
- if (!existsSync21(dir)) return;
3163
+ if (!existsSync24(dir)) return;
2836
3164
  const st = lstatSync6(dir);
2837
3165
  if (!st.isDirectory()) return;
2838
- for (const entry of readdirSync9(dir)) {
2839
- const abs = join26(dir, entry);
3166
+ for (const entry of readdirSync10(dir)) {
3167
+ const abs = join29(dir, entry);
2840
3168
  const lst = lstatSync6(abs);
2841
3169
  if (lst.isSymbolicLink()) continue;
2842
3170
  if (lst.isDirectory()) {
@@ -2873,7 +3201,7 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
2873
3201
  if (!dryRun && total > 0) {
2874
3202
  for (const { path: filePath, findings } of dirty) {
2875
3203
  backupBeforeWrite(filePath, ts);
2876
- writeFileSync4(filePath, applyRedactions(readFileSync9(filePath, "utf8"), findings), "utf8");
3204
+ writeFileSync4(filePath, applyRedactions(readFileSync10(filePath, "utf8"), findings), "utf8");
2877
3205
  }
2878
3206
  }
2879
3207
  return { total, dirty };
@@ -2886,14 +3214,14 @@ init_utils_json();
2886
3214
  init_utils();
2887
3215
  function resolveLiveTranscript2(id) {
2888
3216
  try {
2889
- const mapPath = join27(REPO_HOME, "path-map.json");
2890
- if (!existsSync22(mapPath)) return null;
3217
+ const mapPath = join30(REPO_HOME, "path-map.json");
3218
+ if (!existsSync25(mapPath)) return null;
2891
3219
  const projects = readJson(mapPath).projects;
2892
3220
  for (const hostMap of Object.values(projects)) {
2893
3221
  const abs = hostMap[HOST];
2894
3222
  if (abs === void 0) continue;
2895
- const live = join27(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2896
- if (existsSync22(live)) return live;
3223
+ const live = join30(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
3224
+ if (existsSync25(live)) return live;
2897
3225
  }
2898
3226
  return null;
2899
3227
  } catch {
@@ -2911,17 +3239,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
2911
3239
  fail(`invalid session id: ${id}`);
2912
3240
  process.exit(1);
2913
3241
  }
2914
- if (!existsSync22(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3242
+ if (!existsSync25(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2915
3243
  const handle = acquireLock("redact");
2916
3244
  if (handle === null) process.exit(0);
2917
3245
  try {
2918
3246
  const localPath = resolveLiveTranscript2(id);
2919
- if (localPath === null || !existsSync22(localPath)) {
3247
+ if (localPath === null || !existsSync25(localPath)) {
2920
3248
  fail(`could not resolve local transcript for session ${id} on this host`);
2921
3249
  process.exitCode = 1;
2922
3250
  return;
2923
3251
  }
2924
- const sessionDir = join27(dirname4(localPath), id);
3252
+ const sessionDir = join30(dirname5(localPath), id);
2925
3253
  const subtreeFiles = listSubtreeFiles(sessionDir);
2926
3254
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
2927
3255
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -2974,8 +3302,8 @@ ${lines}`);
2974
3302
  }
2975
3303
 
2976
3304
  // src/commands.pull.ts
2977
- import { existsSync as existsSync30, mkdirSync as mkdirSync8 } from "node:fs";
2978
- import { join as join36 } from "node:path";
3305
+ import { existsSync as existsSync33, mkdirSync as mkdirSync8 } from "node:fs";
3306
+ import { join as join39 } from "node:path";
2979
3307
 
2980
3308
  // src/commands.push.sections.ts
2981
3309
  init_color();
@@ -3063,22 +3391,34 @@ init_config();
3063
3391
 
3064
3392
  // src/extras-sync.ts
3065
3393
  init_config();
3066
- import { existsSync as existsSync25 } from "node:fs";
3067
- import { join as join30 } from "node:path";
3394
+ import { existsSync as existsSync28 } from "node:fs";
3395
+ import { join as join33 } from "node:path";
3068
3396
 
3069
3397
  // src/extras-sync.diff.ts
3070
3398
  init_utils();
3071
3399
  import { execFileSync as execFileSync13 } from "node:child_process";
3400
+ function labelDiffLine(line) {
3401
+ const tab = line.indexOf(" ");
3402
+ if (tab === -1) return line;
3403
+ const status = line.slice(0, tab);
3404
+ const path = line.slice(tab + 1);
3405
+ if (status === "D") return `${path} (local only)`;
3406
+ if (status === "A") return `${path} (repo only)`;
3407
+ return path;
3408
+ }
3409
+ function parseDiffOutput(stdout) {
3410
+ return stdout.split("\n").filter((line) => line.length > 0).map(labelDiffLine);
3411
+ }
3072
3412
  function listDivergingFiles(a, b) {
3073
3413
  try {
3074
- const stdout = execFileSync13("git", ["diff", "--no-index", "--name-only", a, b], {
3414
+ const stdout = execFileSync13("git", ["diff", "--no-index", "--name-status", a, b], {
3075
3415
  stdio: ["ignore", "pipe", "pipe"]
3076
3416
  }).toString();
3077
- return stdout.split("\n").filter((line) => line.length > 0);
3417
+ return parseDiffOutput(stdout);
3078
3418
  } catch (err) {
3079
3419
  const e = err;
3080
3420
  if (e.status === 1 && e.stdout !== void 0) {
3081
- return e.stdout.toString().split("\n").filter((line) => line.length > 0);
3421
+ return parseDiffOutput(e.stdout.toString());
3082
3422
  }
3083
3423
  if (e.code === "ENOENT") {
3084
3424
  warn(`git not on PATH; divergence check skipped for ${a}`);
@@ -3091,8 +3431,8 @@ function listDivergingFiles(a, b) {
3091
3431
 
3092
3432
  // src/extras-sync.core.ts
3093
3433
  init_config();
3094
- import { cpSync as cpSync4, existsSync as existsSync23, rmSync as rmSync7 } from "node:fs";
3095
- import { join as join28 } from "node:path";
3434
+ import { cpSync as cpSync4, existsSync as existsSync26, rmSync as rmSync7 } from "node:fs";
3435
+ import { join as join31 } from "node:path";
3096
3436
 
3097
3437
  // src/extras-sync.guards.ts
3098
3438
  init_utils();
@@ -3115,9 +3455,9 @@ function assertSafeLocalRoot(localRoot, logical) {
3115
3455
  init_utils();
3116
3456
  init_utils_json();
3117
3457
  function loadValidatedExtras(opts) {
3118
- const mapPath = join28(REPO_HOME, "path-map.json");
3119
- const repoExtras = join28(REPO_HOME, "shared", "extras");
3120
- if (!existsSync23(mapPath) || opts.requireRepoExtras === true && !existsSync23(repoExtras)) {
3458
+ const mapPath = join31(REPO_HOME, "path-map.json");
3459
+ const repoExtras = join31(REPO_HOME, "shared", "extras");
3460
+ if (!existsSync26(mapPath) || opts.requireRepoExtras === true && !existsSync26(repoExtras)) {
3121
3461
  if (opts.missingMsg !== void 0) log(opts.missingMsg);
3122
3462
  return null;
3123
3463
  }
@@ -3139,12 +3479,12 @@ function* eachExtrasTarget(v, counts) {
3139
3479
  counts.unmapped++;
3140
3480
  continue;
3141
3481
  }
3142
- for (const dirname6 of dirnames) {
3143
- if (!whitelist.includes(dirname6)) {
3482
+ for (const dirname7 of dirnames) {
3483
+ if (!whitelist.includes(dirname7)) {
3144
3484
  counts.skipped++;
3145
3485
  continue;
3146
3486
  }
3147
- yield { logical, localRoot, dirname: dirname6 };
3487
+ yield { logical, localRoot, dirname: dirname7 };
3148
3488
  }
3149
3489
  }
3150
3490
  }
@@ -3159,8 +3499,8 @@ init_utils_json();
3159
3499
 
3160
3500
  // src/extras-sync.remap.ts
3161
3501
  init_config();
3162
- import { existsSync as existsSync24, mkdirSync as mkdirSync6 } from "node:fs";
3163
- import { join as join29 } from "node:path";
3502
+ import { existsSync as existsSync27, mkdirSync as mkdirSync6 } from "node:fs";
3503
+ import { join as join32 } from "node:path";
3164
3504
  init_utils_fs();
3165
3505
  function runExtrasOp(v, dryRun, paths, backup) {
3166
3506
  const counts = { unmapped: 0, skipped: 0 };
@@ -3168,7 +3508,7 @@ function runExtrasOp(v, dryRun, paths, backup) {
3168
3508
  const would = [];
3169
3509
  for (const t of eachExtrasTarget(v, counts)) {
3170
3510
  const { src, dst } = paths(t);
3171
- if (!existsSync24(src)) continue;
3511
+ if (!existsSync27(src)) continue;
3172
3512
  const item = `${t.logical}/${t.dirname}`;
3173
3513
  if (dryRun) {
3174
3514
  would.push(item);
@@ -3184,14 +3524,14 @@ function remapExtrasPush(ts, opts = {}) {
3184
3524
  const dryRun = opts.dryRun === true;
3185
3525
  const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
3186
3526
  if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
3187
- const repoExtras = join29(REPO_HOME, "shared", "extras");
3527
+ const repoExtras = join32(REPO_HOME, "shared", "extras");
3188
3528
  if (!dryRun) mkdirSync6(repoExtras, { recursive: true });
3189
3529
  const { unmapped, skipped, done, would } = runExtrasOp(
3190
3530
  v,
3191
3531
  dryRun,
3192
- ({ localRoot, logical, dirname: dirname6 }) => ({
3193
- src: join29(localRoot, dirname6),
3194
- dst: join29(repoExtras, logical, dirname6)
3532
+ ({ localRoot, logical, dirname: dirname7 }) => ({
3533
+ src: join32(localRoot, dirname7),
3534
+ dst: join32(repoExtras, logical, dirname7)
3195
3535
  }),
3196
3536
  (dst) => backupRepoWrite(dst, ts, REPO_HOME)
3197
3537
  );
@@ -3204,13 +3544,13 @@ function remapExtrasPull(ts, opts = {}) {
3204
3544
  missingMsg: "no path-map or repo extras dir; skipping extras remap"
3205
3545
  });
3206
3546
  if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
3207
- const repoExtras = join29(REPO_HOME, "shared", "extras");
3547
+ const repoExtras = join32(REPO_HOME, "shared", "extras");
3208
3548
  const { unmapped, skipped, done, would } = runExtrasOp(
3209
3549
  v,
3210
3550
  dryRun,
3211
- ({ localRoot, logical, dirname: dirname6 }) => ({
3212
- src: join29(repoExtras, logical, dirname6),
3213
- dst: join29(localRoot, dirname6)
3551
+ ({ localRoot, logical, dirname: dirname7 }) => ({
3552
+ src: join32(repoExtras, logical, dirname7),
3553
+ dst: join32(localRoot, dirname7)
3214
3554
  }),
3215
3555
  // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
3216
3556
  // localRoot so the backup tree mirrors the project layout.
@@ -3224,16 +3564,16 @@ function divergenceCheckExtras(ts) {
3224
3564
  const v = loadValidatedExtras({});
3225
3565
  if (v === null) return;
3226
3566
  const counts = { unmapped: 0, skipped: 0 };
3227
- const backupRoot = join30(BACKUP_BASE, ts, "extras");
3228
- for (const { logical, localRoot, dirname: dirname6 } of eachExtrasTarget(v, counts)) {
3229
- const local = join30(localRoot, dirname6);
3230
- const repo = join30(REPO_HOME, "shared", "extras", logical, dirname6);
3231
- if (!existsSync25(local) || !existsSync25(repo)) continue;
3567
+ const backupRoot = join33(BACKUP_BASE, ts, "extras");
3568
+ for (const { logical, localRoot, dirname: dirname7 } of eachExtrasTarget(v, counts)) {
3569
+ const local = join33(localRoot, dirname7);
3570
+ const repo = join33(REPO_HOME, "shared", "extras", logical, dirname7);
3571
+ if (!existsSync28(local) || !existsSync28(repo)) continue;
3232
3572
  const diff = listDivergingFiles(local, repo);
3233
3573
  if (diff.length === 0) continue;
3234
- const projectBackupRoot = join30(backupRoot, encodePath(localRoot));
3574
+ const projectBackupRoot = join33(backupRoot, encodePath(localRoot));
3235
3575
  warn(
3236
- `local ${dirname6} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
3576
+ `local ${dirname7} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
3237
3577
  );
3238
3578
  for (const f of diff) warn(` ${f}`);
3239
3579
  }
@@ -3244,8 +3584,8 @@ init_config();
3244
3584
  init_utils();
3245
3585
  init_utils_fs();
3246
3586
  init_utils_json();
3247
- import { existsSync as existsSync26, lstatSync as lstatSync7, rmSync as rmSync8 } from "node:fs";
3248
- import { join as join31 } from "node:path";
3587
+ import { existsSync as existsSync29, lstatSync as lstatSync7, rmSync as rmSync8 } from "node:fs";
3588
+ import { join as join34 } from "node:path";
3249
3589
  function emitAutoMove(onPreview, linkPath, ts, name) {
3250
3590
  if (onPreview) {
3251
3591
  onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
@@ -3264,11 +3604,11 @@ function applySharedLinks(ts, map, opts = {}) {
3264
3604
  const dryRun = opts.dryRun === true;
3265
3605
  const linkNames = allSharedLinks(map);
3266
3606
  for (const name of linkNames) {
3267
- const linkPath = join31(CLAUDE_HOME, name);
3268
- const target = join31(REPO_HOME, "shared", name);
3269
- if (!existsSync26(linkPath)) continue;
3607
+ const linkPath = join34(CLAUDE_HOME, name);
3608
+ const target = join34(REPO_HOME, "shared", name);
3609
+ if (!existsSync29(linkPath)) continue;
3270
3610
  if (lstatSync7(linkPath).isSymbolicLink()) continue;
3271
- if (!existsSync26(target)) continue;
3611
+ if (!existsSync29(target)) continue;
3272
3612
  if (dryRun) {
3273
3613
  emitAutoMove(opts.onPreview, linkPath, ts, name);
3274
3614
  continue;
@@ -3277,28 +3617,28 @@ function applySharedLinks(ts, map, opts = {}) {
3277
3617
  rmSync8(linkPath, { recursive: true, force: true });
3278
3618
  }
3279
3619
  for (const name of linkNames) {
3280
- const target = join31(REPO_HOME, "shared", name);
3281
- if (!existsSync26(target)) continue;
3620
+ const target = join34(REPO_HOME, "shared", name);
3621
+ if (!existsSync29(target)) continue;
3282
3622
  if (dryRun) {
3283
- emitCreate(opts.onPreview, join31(CLAUDE_HOME, name), target);
3623
+ emitCreate(opts.onPreview, join34(CLAUDE_HOME, name), target);
3284
3624
  continue;
3285
3625
  }
3286
- ensureSymlink(join31(CLAUDE_HOME, name), target);
3626
+ ensureSymlink(join34(CLAUDE_HOME, name), target);
3287
3627
  }
3288
3628
  }
3289
3629
  function regenerateSettings(ts, opts = {}) {
3290
3630
  const dryRun = opts.dryRun === true;
3291
- const basePath = join31(REPO_HOME, "shared", "settings.base.json");
3292
- const hostPath = join31(REPO_HOME, "hosts", `${HOST}.json`);
3293
- if (!existsSync26(basePath)) {
3631
+ const basePath = join34(REPO_HOME, "shared", "settings.base.json");
3632
+ const hostPath = join34(REPO_HOME, "hosts", `${HOST}.json`);
3633
+ if (!existsSync29(basePath)) {
3294
3634
  die("repo not initialized; run 'nomad init' to scaffold");
3295
3635
  }
3296
3636
  const base = readJson(basePath);
3297
- const hasOverrides = existsSync26(hostPath);
3637
+ const hasOverrides = existsSync29(hostPath);
3298
3638
  const overrides = hasOverrides ? readJson(hostPath) : {};
3299
3639
  const merged = deepMerge(base, overrides);
3300
- const settingsPath = join31(CLAUDE_HOME, "settings.json");
3301
- if (!hasOverrides && existsSync26(settingsPath)) {
3640
+ const settingsPath = join34(CLAUDE_HOME, "settings.json");
3641
+ if (!hasOverrides && existsSync29(settingsPath)) {
3302
3642
  try {
3303
3643
  const existing = readJson(settingsPath);
3304
3644
  const baseKeys = new Set(Object.keys(base));
@@ -3324,8 +3664,8 @@ function regenerateSettings(ts, opts = {}) {
3324
3664
 
3325
3665
  // src/preview.ts
3326
3666
  init_config();
3327
- import { existsSync as existsSync27 } from "node:fs";
3328
- import { join as join32 } from "node:path";
3667
+ import { existsSync as existsSync30 } from "node:fs";
3668
+ import { join as join35 } from "node:path";
3329
3669
 
3330
3670
  // node_modules/diff/libesm/diff/base.js
3331
3671
  var Diff = class {
@@ -3600,6 +3940,7 @@ function diffLinesToUnified(oldStr, newStr) {
3600
3940
 
3601
3941
  // src/preview.ts
3602
3942
  init_utils_json();
3943
+ var CANONICAL_ORDER_NOTE = "settings.json will be rewritten in canonical key order; no value changes";
3603
3944
  function diffJsonStrings(currentJsonText, newJsonText) {
3604
3945
  if (currentJsonText === newJsonText) return "";
3605
3946
  const lines = [
@@ -3610,7 +3951,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
3610
3951
  return lines.join("\n");
3611
3952
  }
3612
3953
  function readJsonOrNull(path) {
3613
- if (!existsSync27(path)) return null;
3954
+ if (!existsSync30(path)) return null;
3614
3955
  try {
3615
3956
  return readJson(path);
3616
3957
  } catch {
@@ -3624,18 +3965,20 @@ function previewSettings(basePath, hostPath, settingsPath) {
3624
3965
  }
3625
3966
  const notes = [];
3626
3967
  const hostOverrides = readJsonOrNull(hostPath);
3627
- if (hostOverrides === null && existsSync27(hostPath)) {
3968
+ if (hostOverrides === null && existsSync30(hostPath)) {
3628
3969
  notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
3629
3970
  }
3630
3971
  const merged = deepMerge(base, hostOverrides ?? {});
3631
3972
  const current = readJsonOrNull(settingsPath);
3632
- if (current === null && existsSync27(settingsPath)) {
3973
+ if (current === null && existsSync30(settingsPath)) {
3633
3974
  return { diff: "", notes: [...notes, "malformed; skipping diff"] };
3634
3975
  }
3976
+ const rawEqual = JSON.stringify(current ?? {}, null, 2) === JSON.stringify(merged, null, 2);
3635
3977
  const diff = diffJsonStrings(
3636
- JSON.stringify(current ?? {}, null, 2),
3637
- JSON.stringify(merged, null, 2)
3978
+ JSON.stringify(sortKeysDeep(current ?? {}), null, 2),
3979
+ JSON.stringify(sortKeysDeep(merged), null, 2)
3638
3980
  );
3981
+ if (diff === "" && !rawEqual) notes.push(CANONICAL_ORDER_NOTE);
3639
3982
  return { diff, notes };
3640
3983
  }
3641
3984
  function formatLinkRow(e) {
@@ -3665,9 +4008,9 @@ function computePreview(ts, map, verb = "pull") {
3665
4008
  onPreview: (e) => addItem(links, formatLinkRow(e))
3666
4009
  });
3667
4010
  const settingsResult = previewSettings(
3668
- join32(REPO_HOME, "shared", "settings.base.json"),
3669
- join32(REPO_HOME, "hosts", `${HOST}.json`),
3670
- join32(CLAUDE_HOME, "settings.json")
4011
+ join35(REPO_HOME, "shared", "settings.base.json"),
4012
+ join35(REPO_HOME, "hosts", `${HOST}.json`),
4013
+ join35(CLAUDE_HOME, "settings.json")
3671
4014
  );
3672
4015
  const settingsSection = buildSettingsSectionForPreview(settingsResult);
3673
4016
  const sessions = section("Sessions");
@@ -3683,21 +4026,21 @@ function computePreview(ts, map, verb = "pull") {
3683
4026
 
3684
4027
  // src/spinner.ts
3685
4028
  init_color();
3686
- import { existsSync as existsSync29 } from "node:fs";
4029
+ import { existsSync as existsSync32 } from "node:fs";
3687
4030
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3688
4031
  import { Worker } from "node:worker_threads";
3689
4032
 
3690
4033
  // src/commands.push.recovery.ts
3691
4034
  init_config();
3692
- import { readFileSync as readFileSync10, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
3693
- import { join as join35 } from "node:path";
4035
+ import { readFileSync as readFileSync11, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
4036
+ import { join as join38 } from "node:path";
3694
4037
  import { createInterface } from "node:readline/promises";
3695
4038
 
3696
4039
  // src/commands.push.recovery.redact.ts
3697
4040
  init_config();
3698
4041
  init_config_sharedDirs_guard();
3699
- import { cpSync as cpSync5, existsSync as existsSync28, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
3700
- import { dirname as dirname5, join as join33, sep as sep2 } from "node:path";
4042
+ import { cpSync as cpSync5, existsSync as existsSync31, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
4043
+ import { dirname as dirname6, join as join36, sep as sep2 } from "node:path";
3701
4044
  init_push_gitleaks_scan();
3702
4045
  init_utils_json();
3703
4046
  init_utils();
@@ -3728,8 +4071,8 @@ function resolveStagedDir(localPath, map) {
3728
4071
  assertSafeLogical(logical);
3729
4072
  const abs = hostMap[HOST];
3730
4073
  if (abs === void 0) continue;
3731
- if (localPath.startsWith(join33(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
3732
- return join33(REPO_HOME, "shared", "projects", logical);
4074
+ if (localPath.startsWith(join36(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
4075
+ return join36(REPO_HOME, "shared", "projects", logical);
3733
4076
  }
3734
4077
  }
3735
4078
  return null;
@@ -3751,7 +4094,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3751
4094
  `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
3752
4095
  );
3753
4096
  }
3754
- const sessionDir = join33(dirname5(localPath), sid);
4097
+ const sessionDir = join36(dirname6(localPath), sid);
3755
4098
  const subtreeFiles = listSubtreeFiles(sessionDir);
3756
4099
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
3757
4100
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -3785,9 +4128,9 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3785
4128
  );
3786
4129
  }
3787
4130
  mkdirSync7(stagedProjectDir, { recursive: true });
3788
- cpSync5(localPath, join33(stagedProjectDir, `${sid}.jsonl`), { force: true });
3789
- if (existsSync28(sessionDir)) {
3790
- cpSync5(sessionDir, join33(stagedProjectDir, sid), { force: true, recursive: true });
4131
+ cpSync5(localPath, join36(stagedProjectDir, `${sid}.jsonl`), { force: true });
4132
+ if (existsSync31(sessionDir)) {
4133
+ cpSync5(sessionDir, join36(stagedProjectDir, sid), { force: true, recursive: true });
3791
4134
  }
3792
4135
  return true;
3793
4136
  }
@@ -3795,13 +4138,13 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3795
4138
  // src/commands.push.recovery.drop.ts
3796
4139
  init_config();
3797
4140
  import { rmSync as rmSync9 } from "node:fs";
3798
- import { join as join34 } from "node:path";
4141
+ import { join as join37 } from "node:path";
3799
4142
  function dropSessionFromStaged(sid, map) {
3800
4143
  const logicals = Object.keys(map.projects);
3801
4144
  if (logicals.length === 0) return false;
3802
4145
  for (const logical of logicals) {
3803
- const jsonl = join34(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
3804
- const dir = join34(REPO_HOME, "shared", "projects", logical, sid);
4146
+ const jsonl = join37(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
4147
+ const dir = join37(REPO_HOME, "shared", "projects", logical, sid);
3805
4148
  rmSync9(jsonl, { force: true });
3806
4149
  rmSync9(dir, { recursive: true, force: true });
3807
4150
  }
@@ -3919,10 +4262,10 @@ function applyThenRescan(scanVerdict, repoHome) {
3919
4262
  return next;
3920
4263
  }
3921
4264
  function allowThenRescan(append, scanVerdict, repoHome) {
3922
- const ignPath = join35(repoHome, ".gitleaksignore");
4265
+ const ignPath = join38(repoHome, ".gitleaksignore");
3923
4266
  let before;
3924
4267
  try {
3925
- before = readFileSync10(ignPath, "utf8");
4268
+ before = readFileSync11(ignPath, "utf8");
3926
4269
  } catch {
3927
4270
  before = null;
3928
4271
  }
@@ -4017,7 +4360,7 @@ function writeAnimatedDone(out, label, ms, useTTY) {
4017
4360
  `);
4018
4361
  }
4019
4362
  function resolveWorkerPath(deps = {}) {
4020
- const check = deps.existsSyncFn ?? existsSync29;
4363
+ const check = deps.existsSyncFn ?? existsSync32;
4021
4364
  const base = deps.baseUrl ?? import.meta.url;
4022
4365
  const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
4023
4366
  if (check(mjs)) return mjs;
@@ -4081,6 +4424,112 @@ function withSpinner(label, fn, deps) {
4081
4424
  }
4082
4425
  }
4083
4426
 
4427
+ // src/commands.pull.recovery.ts
4428
+ init_config();
4429
+ import { execFileSync as execFileSync14 } from "node:child_process";
4430
+ init_utils();
4431
+ init_utils_fs();
4432
+ function gitCapture(args, cwd) {
4433
+ return execFileSync14("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
4434
+ }
4435
+ function isSyncedConfig(path) {
4436
+ return PUSH_ALLOWED_STATIC.some(
4437
+ (entry) => entry.endsWith("/") ? path.startsWith(entry) : path === entry
4438
+ );
4439
+ }
4440
+ function classifyTouched(touched) {
4441
+ const synced = [];
4442
+ const toolSource = [];
4443
+ for (const p of touched) {
4444
+ if (isSyncedConfig(p)) {
4445
+ synced.push(p);
4446
+ } else {
4447
+ toolSource.push(p);
4448
+ }
4449
+ }
4450
+ return { synced, toolSource };
4451
+ }
4452
+ function parsePorcelainZ(raw) {
4453
+ const tracked = [];
4454
+ const untracked = [];
4455
+ if (!raw) return { tracked, untracked };
4456
+ const records = raw.split("\0");
4457
+ for (let i = 0; i < records.length; i++) {
4458
+ const record = records[i];
4459
+ if (record.length < 3) continue;
4460
+ const xy = record.slice(0, 2);
4461
+ const filePath = record.slice(3);
4462
+ if (xy === "??") {
4463
+ untracked.push(filePath);
4464
+ continue;
4465
+ }
4466
+ tracked.push(filePath);
4467
+ if (xy.startsWith("R") || xy.startsWith("C")) {
4468
+ const src = records[i + 1];
4469
+ if (src) {
4470
+ tracked.push(src);
4471
+ i++;
4472
+ }
4473
+ }
4474
+ }
4475
+ return { tracked, untracked };
4476
+ }
4477
+ function parseDirtyPaths(repo) {
4478
+ return parsePorcelainZ(gitStatusPorcelainZ(repo));
4479
+ }
4480
+ function buildRecoverySummary(branchName, strandedLog, untracked) {
4481
+ const strandedLines = strandedLog.split("\n").filter(Boolean).map((l) => ` ${l}`).join("\n");
4482
+ const parts = [`parked stranded commits on ${branchName}`];
4483
+ if (strandedLines) parts.push(`stranded:
4484
+ ${strandedLines}`);
4485
+ if (untracked.length > 0) parts.push(`untracked files preserved: ${untracked.join(", ")}`);
4486
+ parts.push("continuing with normal pull");
4487
+ return parts.join("; ");
4488
+ }
4489
+ function freshStrandedBranch(repo) {
4490
+ const base = `nomad/stranded-${nowTimestamp()}`;
4491
+ const exists = (name) => {
4492
+ try {
4493
+ gitCapture(["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], repo);
4494
+ return true;
4495
+ } catch {
4496
+ return false;
4497
+ }
4498
+ };
4499
+ if (!exists(base)) return base;
4500
+ let n = 1;
4501
+ while (exists(`${base}-${n}`)) n++;
4502
+ return `${base}-${n}`;
4503
+ }
4504
+ function recoverForceRemote(mode, repo) {
4505
+ if (mode === "merge") {
4506
+ gitOrFatal(["merge", "--abort"], "git merge --abort", repo);
4507
+ } else {
4508
+ gitOrFatal(["rebase", "--abort"], "git rebase --abort", repo);
4509
+ }
4510
+ gitOrFatal(["fetch", "origin", "main"], "git fetch origin main", repo);
4511
+ try {
4512
+ gitCapture(["rev-parse", "--verify", "origin/main"], repo);
4513
+ } catch {
4514
+ die("origin/main not found after fetch; check your remote configuration");
4515
+ }
4516
+ const committedRaw = gitCapture(["diff", "--name-only", "-z", "origin/main", "HEAD"], repo);
4517
+ const committedTouched = committedRaw.split("\0").filter(Boolean);
4518
+ const { tracked: dirtyTracked, untracked } = parseDirtyPaths(repo);
4519
+ const allTouched = [...committedTouched, ...dirtyTracked];
4520
+ const { synced } = classifyTouched(allTouched);
4521
+ if (synced.length > 0) {
4522
+ die(
4523
+ "force-remote refused: stranded or dirty tracked changes touch synced config.\nAt-risk paths:\n" + synced.map((p) => ` ${p}`).join("\n") + "\nCopy or cherry-pick those changes out before retrying."
4524
+ );
4525
+ }
4526
+ const branchName = freshStrandedBranch(repo);
4527
+ gitOrFatal(["branch", branchName, "HEAD"], "park stranded commits", repo);
4528
+ gitOrFatal(["reset", "--hard", "origin/main"], "reset to origin/main", repo);
4529
+ const strandedLog = gitCapture(["log", "--oneline", `origin/main..${branchName}`], repo);
4530
+ log(buildRecoverySummary(branchName, strandedLog, untracked));
4531
+ }
4532
+
4084
4533
  // src/commands.pull.ts
4085
4534
  init_utils();
4086
4535
  init_utils_fs();
@@ -4102,18 +4551,32 @@ function applyWetPull(ts, map) {
4102
4551
  summary
4103
4552
  ]);
4104
4553
  }
4554
+ function handleWedge(repo, forceRemote) {
4555
+ const wedge = detectWedge(repo);
4556
+ if (wedge === null) return;
4557
+ if (forceRemote) {
4558
+ recoverForceRemote(wedge, repo);
4559
+ return;
4560
+ }
4561
+ const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
4562
+ die(
4563
+ `repo is ${state} from a previous failed pull; run 'nomad pull --force-remote' to auto-recover, or resolve manually (see FAQ: "Every pull fails with unmerged files")`
4564
+ );
4565
+ }
4105
4566
  function cmdPull(opts = {}) {
4106
4567
  const dryRun = opts.dryRun === true;
4107
- if (!existsSync30(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4108
- if (!existsSync30(join36(REPO_HOME, "shared", "settings.base.json"))) {
4568
+ const forceRemote = opts.forceRemote === true;
4569
+ if (!existsSync33(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4570
+ if (!existsSync33(join39(REPO_HOME, "shared", "settings.base.json"))) {
4109
4571
  die("repo not initialized; run 'nomad init' to scaffold");
4110
4572
  }
4111
4573
  const handle = acquireLock("pull");
4112
4574
  if (handle === null) process.exit(0);
4113
4575
  try {
4114
4576
  const ts = freshBackupTs(BACKUP_BASE);
4577
+ handleWedge(REPO_HOME, forceRemote);
4115
4578
  if (!dryRun) {
4116
- const backupRoot = join36(BACKUP_BASE, ts);
4579
+ const backupRoot = join39(BACKUP_BASE, ts);
4117
4580
  try {
4118
4581
  mkdirSync8(backupRoot, { recursive: true });
4119
4582
  } catch (err) {
@@ -4124,8 +4587,8 @@ function cmdPull(opts = {}) {
4124
4587
  dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
4125
4588
  );
4126
4589
  gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
4127
- const mapPath = join36(REPO_HOME, "path-map.json");
4128
- const map = existsSync30(mapPath) ? readPathMap(mapPath) : { projects: {} };
4590
+ const mapPath = join39(REPO_HOME, "path-map.json");
4591
+ const map = existsSync33(mapPath) ? readPathMap(mapPath) : { projects: {} };
4129
4592
  divergenceCheckExtras(ts);
4130
4593
  if (dryRun) {
4131
4594
  computePreview(ts, map, "pull");
@@ -4147,8 +4610,8 @@ function cmdPull(opts = {}) {
4147
4610
 
4148
4611
  // src/commands.push.ts
4149
4612
  init_config();
4150
- import { existsSync as existsSync32 } from "node:fs";
4151
- import { join as join38, relative as relative5 } from "node:path";
4613
+ import { existsSync as existsSync35 } from "node:fs";
4614
+ import { join as join41, relative as relative5 } from "node:path";
4152
4615
 
4153
4616
  // src/commands.push.allowlist.ts
4154
4617
  init_config();
@@ -4172,7 +4635,7 @@ function isNeverSync(path) {
4172
4635
  }
4173
4636
  return false;
4174
4637
  }
4175
- function parsePorcelainZ(statusPorcelain) {
4638
+ function parsePorcelainZ2(statusPorcelain) {
4176
4639
  const records = statusPorcelain.split("\0");
4177
4640
  const paths = [];
4178
4641
  for (let i = 0; i < records.length; i++) {
@@ -4202,7 +4665,7 @@ function enforceAllowList(statusPorcelain, map) {
4202
4665
  ];
4203
4666
  const neverSyncHits = [];
4204
4667
  const violations = [];
4205
- for (const path of parsePorcelainZ(statusPorcelain)) {
4668
+ for (const path of parsePorcelainZ2(statusPorcelain)) {
4206
4669
  if (isNeverSync(path)) {
4207
4670
  neverSyncHits.push(path);
4208
4671
  } else if (!isAllowed(path, allowed)) {
@@ -4228,9 +4691,9 @@ init_color();
4228
4691
  init_config();
4229
4692
  init_config_sharedDirs_guard();
4230
4693
  import { randomBytes as randomBytes2 } from "node:crypto";
4231
- import { copyFileSync, existsSync as existsSync31, mkdirSync as mkdirSync9, readdirSync as readdirSync10, rmSync as rmSync11 } from "node:fs";
4694
+ import { copyFileSync, existsSync as existsSync34, mkdirSync as mkdirSync9, readdirSync as readdirSync11, rmSync as rmSync11 } from "node:fs";
4232
4695
  import { homedir as homedir5 } from "node:os";
4233
- import { join as join37 } from "node:path";
4696
+ import { join as join40 } from "node:path";
4234
4697
  init_push_leak_verdict();
4235
4698
  init_push_gitleaks();
4236
4699
  init_utils_fs();
@@ -4245,13 +4708,13 @@ function stageSessions(tmpRoot, map) {
4245
4708
  if (!p || p === "TBD") continue;
4246
4709
  reverse.set(encodePath(p), logical);
4247
4710
  }
4248
- const localProjects = join37(CLAUDE_HOME, "projects");
4249
- if (!existsSync31(localProjects)) return 0;
4711
+ const localProjects = join40(CLAUDE_HOME, "projects");
4712
+ if (!existsSync34(localProjects)) return 0;
4250
4713
  let staged = 0;
4251
- for (const dir of readdirSync10(localProjects)) {
4714
+ for (const dir of readdirSync11(localProjects)) {
4252
4715
  const logical = reverse.get(dir);
4253
4716
  if (!logical) continue;
4254
- copyDirJsonlOnly(join37(localProjects, dir), join37(tmpRoot, "shared", "projects", logical));
4717
+ copyDirJsonlOnly(join40(localProjects, dir), join40(tmpRoot, "shared", "projects", logical));
4255
4718
  staged++;
4256
4719
  }
4257
4720
  return staged;
@@ -4265,11 +4728,11 @@ function stageExtras(tmpRoot, map) {
4265
4728
  assertSafeLogical(logical);
4266
4729
  const localRoot = map.projects[logical]?.[HOST];
4267
4730
  if (!localRoot || localRoot === "TBD") continue;
4268
- for (const dirname6 of dirnames) {
4269
- if (!whitelist.includes(dirname6)) continue;
4270
- const src = join37(localRoot, dirname6);
4271
- if (!existsSync31(src)) continue;
4272
- const dst = join37(tmpRoot, "shared", "extras", logical, dirname6);
4731
+ for (const dirname7 of dirnames) {
4732
+ if (!whitelist.includes(dirname7)) continue;
4733
+ const src = join40(localRoot, dirname7);
4734
+ if (!existsSync34(src)) continue;
4735
+ const dst = join40(tmpRoot, "shared", "extras", logical, dirname7);
4273
4736
  copyExtras(src, dst);
4274
4737
  staged++;
4275
4738
  }
@@ -4277,19 +4740,19 @@ function stageExtras(tmpRoot, map) {
4277
4740
  return staged;
4278
4741
  }
4279
4742
  function previewPushLeaks(map) {
4280
- const cacheDir = join37(homedir5(), ".cache", "claude-nomad");
4743
+ const cacheDir = join40(homedir5(), ".cache", "claude-nomad");
4281
4744
  mkdirSync9(cacheDir, { recursive: true });
4282
4745
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
4283
- const tmpRoot = join37(cacheDir, `push-preview-tree-${stamp}`);
4746
+ const tmpRoot = join40(cacheDir, `push-preview-tree-${stamp}`);
4284
4747
  try {
4285
4748
  const sessionCount = stageSessions(tmpRoot, map);
4286
4749
  const extrasCount = stageExtras(tmpRoot, map);
4287
4750
  if (sessionCount + extrasCount === 0) {
4288
4751
  return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
4289
4752
  }
4290
- const ignoreFile = join37(REPO_HOME, ".gitleaksignore");
4291
- if (existsSync31(ignoreFile)) {
4292
- copyFileSync(ignoreFile, join37(tmpRoot, ".gitleaksignore"));
4753
+ const ignoreFile = join40(REPO_HOME, ".gitleaksignore");
4754
+ if (existsSync34(ignoreFile)) {
4755
+ copyFileSync(ignoreFile, join40(tmpRoot, ".gitleaksignore"));
4293
4756
  }
4294
4757
  let findings;
4295
4758
  try {
@@ -4311,7 +4774,7 @@ init_utils();
4311
4774
  init_utils_fs();
4312
4775
  init_utils_json();
4313
4776
  function guardGitlinks() {
4314
- const gitlinks = findGitlinks(join38(REPO_HOME, "shared"));
4777
+ const gitlinks = findGitlinks(join41(REPO_HOME, "shared"));
4315
4778
  if (gitlinks.length === 0) return;
4316
4779
  for (const p of gitlinks) {
4317
4780
  const rel = relative5(REPO_HOME, p);
@@ -4363,7 +4826,7 @@ async function cmdPush(opts = {}) {
4363
4826
  const allowAll = opts.allowAll === true;
4364
4827
  const allowRule = opts.allowRule;
4365
4828
  guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
4366
- if (!existsSync32(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4829
+ if (!existsSync35(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4367
4830
  const handle = acquireLock("push");
4368
4831
  if (handle === null) process.exit(0);
4369
4832
  try {
@@ -4381,8 +4844,8 @@ async function cmdPush(opts = {}) {
4381
4844
  renderNoScanTree(st);
4382
4845
  return;
4383
4846
  }
4384
- const mapPath = join38(REPO_HOME, "path-map.json");
4385
- if (!existsSync32(mapPath)) {
4847
+ const mapPath = join41(REPO_HOME, "path-map.json");
4848
+ if (!existsSync35(mapPath)) {
4386
4849
  if (dryRun) return runDryRunPreview(st, null);
4387
4850
  die("path-map.json missing, cannot enforce push allow-list");
4388
4851
  }
@@ -4403,9 +4866,9 @@ async function cmdPush(opts = {}) {
4403
4866
  }
4404
4867
 
4405
4868
  // src/commands.update.ts
4406
- import { execFileSync as execFileSync14 } from "node:child_process";
4869
+ import { execFileSync as execFileSync15 } from "node:child_process";
4407
4870
  init_utils();
4408
- function cmdUpdate(run = execFileSync14) {
4871
+ function cmdUpdate(run = execFileSync15) {
4409
4872
  try {
4410
4873
  run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
4411
4874
  } catch (err) {
@@ -4422,17 +4885,17 @@ init_config();
4422
4885
 
4423
4886
  // src/diff.ts
4424
4887
  init_config();
4425
- import { existsSync as existsSync33 } from "node:fs";
4426
- import { join as join39 } from "node:path";
4888
+ import { existsSync as existsSync36 } from "node:fs";
4889
+ import { join as join42 } from "node:path";
4427
4890
  init_utils();
4428
4891
  init_utils_fs();
4429
4892
  init_utils_json();
4430
4893
  function cmdDiff() {
4431
4894
  try {
4432
- if (!existsSync33(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4895
+ if (!existsSync36(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4433
4896
  const ts = freshBackupTs(BACKUP_BASE);
4434
- const mapPath = join39(REPO_HOME, "path-map.json");
4435
- const map = existsSync33(mapPath) ? readPathMap(mapPath) : { projects: {} };
4897
+ const mapPath = join42(REPO_HOME, "path-map.json");
4898
+ const map = existsSync36(mapPath) ? readPathMap(mapPath) : { projects: {} };
4436
4899
  computePreview(ts, map, "diff");
4437
4900
  } catch (err) {
4438
4901
  if (err instanceof NomadFatal) {
@@ -4446,19 +4909,19 @@ function cmdDiff() {
4446
4909
 
4447
4910
  // src/init.ts
4448
4911
  init_config();
4449
- import { existsSync as existsSync35, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
4450
- import { join as join41 } from "node:path";
4912
+ import { existsSync as existsSync38, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
4913
+ import { join as join44 } from "node:path";
4451
4914
 
4452
4915
  // src/init.gh-onboard.ts
4453
4916
  init_config();
4454
- import { execFileSync as execFileSync15 } from "node:child_process";
4917
+ import { execFileSync as execFileSync16 } from "node:child_process";
4455
4918
  init_utils();
4456
4919
  var DEFAULT_REPO_NAME = "claude-nomad-config";
4457
4920
  function isValidRepoName(name) {
4458
4921
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
4459
4922
  }
4460
4923
  var GH_NETWORK_TIMEOUT_MS = 3e4;
4461
- function ensureOriginRepo(repoName, run = execFileSync15) {
4924
+ function ensureOriginRepo(repoName, run = execFileSync16) {
4462
4925
  if (!isValidRepoName(repoName)) {
4463
4926
  die(
4464
4927
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -4528,31 +4991,31 @@ init_config();
4528
4991
  init_utils();
4529
4992
  init_utils_fs();
4530
4993
  init_utils_json();
4531
- import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync34, rmSync as rmSync12, statSync as statSync9 } from "node:fs";
4532
- import { join as join40 } from "node:path";
4994
+ import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync37, rmSync as rmSync12, statSync as statSync9 } from "node:fs";
4995
+ import { join as join43 } from "node:path";
4533
4996
  function snapshotIntoShared(map) {
4534
4997
  for (const name of allSharedLinks(map)) {
4535
- const src = join40(CLAUDE_HOME, name);
4536
- if (!existsSync34(src)) continue;
4537
- const dst = join40(REPO_HOME, "shared", name);
4998
+ const src = join43(CLAUDE_HOME, name);
4999
+ if (!existsSync37(src)) continue;
5000
+ const dst = join43(REPO_HOME, "shared", name);
4538
5001
  if (statSync9(src).isDirectory()) {
4539
- const gk = join40(dst, ".gitkeep");
4540
- if (existsSync34(gk)) rmSync12(gk);
5002
+ const gk = join43(dst, ".gitkeep");
5003
+ if (existsSync37(gk)) rmSync12(gk);
4541
5004
  cpSync6(src, dst, { recursive: true, force: false, errorOnExist: true });
4542
5005
  } else {
4543
5006
  copyFileSync2(src, dst);
4544
5007
  }
4545
5008
  log(`snapshotted shared/${name} from ${src}`);
4546
5009
  }
4547
- const userSettings = join40(CLAUDE_HOME, "settings.json");
4548
- if (existsSync34(userSettings)) {
5010
+ const userSettings = join43(CLAUDE_HOME, "settings.json");
5011
+ if (existsSync37(userSettings)) {
4549
5012
  let parsed;
4550
5013
  try {
4551
5014
  parsed = readJson(userSettings);
4552
5015
  } catch (err) {
4553
5016
  return die(`malformed ${userSettings}: ${err.message}`);
4554
5017
  }
4555
- const hostFile = join40(REPO_HOME, "hosts", `${HOST}.json`);
5018
+ const hostFile = join43(REPO_HOME, "hosts", `${HOST}.json`);
4556
5019
  writeJsonAtomic(hostFile, parsed);
4557
5020
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
4558
5021
  }
@@ -4565,14 +5028,14 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
4565
5028
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
4566
5029
  function preflightConflict(repoHome) {
4567
5030
  const candidates = [
4568
- join41(repoHome, "shared", "settings.base.json"),
4569
- join41(repoHome, "shared", "CLAUDE.md"),
4570
- join41(repoHome, "path-map.json"),
4571
- join41(repoHome, "hosts"),
4572
- join41(repoHome, "shared")
5031
+ join44(repoHome, "shared", "settings.base.json"),
5032
+ join44(repoHome, "shared", "CLAUDE.md"),
5033
+ join44(repoHome, "path-map.json"),
5034
+ join44(repoHome, "hosts"),
5035
+ join44(repoHome, "shared")
4573
5036
  ];
4574
5037
  for (const c of candidates) {
4575
- if (existsSync35(c)) return c;
5038
+ if (existsSync38(c)) return c;
4576
5039
  }
4577
5040
  return null;
4578
5041
  }
@@ -4585,25 +5048,25 @@ function cmdInit(opts = {}) {
4585
5048
  die(`already initialized; refusing to clobber ${conflict}`);
4586
5049
  }
4587
5050
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
4588
- mkdirSync10(join41(REPO_HOME, "shared"), { recursive: true });
4589
- mkdirSync10(join41(REPO_HOME, "hosts"), { recursive: true });
5051
+ mkdirSync10(join44(REPO_HOME, "shared"), { recursive: true });
5052
+ mkdirSync10(join44(REPO_HOME, "hosts"), { recursive: true });
4590
5053
  for (const name of SHARED_KEEP_DIRS) {
4591
- mkdirSync10(join41(REPO_HOME, "shared", name), { recursive: true });
5054
+ mkdirSync10(join44(REPO_HOME, "shared", name), { recursive: true });
4592
5055
  }
4593
- const userClaudeMd = join41(CLAUDE_HOME, "CLAUDE.md");
4594
- if (!snapshot || !existsSync35(userClaudeMd)) {
4595
- writeFileSync6(join41(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
5056
+ const userClaudeMd = join44(CLAUDE_HOME, "CLAUDE.md");
5057
+ if (!snapshot || !existsSync38(userClaudeMd)) {
5058
+ writeFileSync6(join44(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
4596
5059
  log("created shared/CLAUDE.md");
4597
5060
  }
4598
5061
  for (const name of SHARED_KEEP_DIRS) {
4599
- writeFileSync6(join41(REPO_HOME, "shared", name, ".gitkeep"), "");
5062
+ writeFileSync6(join44(REPO_HOME, "shared", name, ".gitkeep"), "");
4600
5063
  log(`created shared/${name}/.gitkeep`);
4601
5064
  }
4602
- writeFileSync6(join41(REPO_HOME, "hosts", ".gitkeep"), "");
5065
+ writeFileSync6(join44(REPO_HOME, "hosts", ".gitkeep"), "");
4603
5066
  log("created hosts/.gitkeep");
4604
- writeJsonAtomic(join41(REPO_HOME, "shared", "settings.base.json"), {});
5067
+ writeJsonAtomic(join44(REPO_HOME, "shared", "settings.base.json"), {});
4605
5068
  log("created shared/settings.base.json");
4606
- writeJsonAtomic(join41(REPO_HOME, "path-map.json"), { projects: {} });
5069
+ writeJsonAtomic(join44(REPO_HOME, "path-map.json"), { projects: {} });
4607
5070
  log("created path-map.json");
4608
5071
  if (snapshot) {
4609
5072
  snapshotIntoShared({ projects: {} });
@@ -4807,6 +5270,28 @@ function parseAllowArgs(argv) {
4807
5270
  return positionals;
4808
5271
  }
4809
5272
 
5273
+ // src/nomad.dispatch.pull.ts
5274
+ function parsePullArgs(argv) {
5275
+ let dryRun = false;
5276
+ let forceRemote = false;
5277
+ let i = 3;
5278
+ while (i < argv.length) {
5279
+ const token = argv[i];
5280
+ if (token === "--dry-run") {
5281
+ if (dryRun) return null;
5282
+ dryRun = true;
5283
+ } else if (token === "--force-remote") {
5284
+ if (forceRemote) return null;
5285
+ forceRemote = true;
5286
+ } else {
5287
+ return null;
5288
+ }
5289
+ i++;
5290
+ }
5291
+ if (dryRun && forceRemote) return null;
5292
+ return { dryRun, forceRemote };
5293
+ }
5294
+
4810
5295
  // src/nomad.dispatch.push.ts
4811
5296
  var REJECT2 = { ok: false, advance: 0 };
4812
5297
  function applyBool2(seen, set) {
@@ -4865,7 +5350,7 @@ function parsePushArgs(argv) {
4865
5350
  // package.json
4866
5351
  var package_default = {
4867
5352
  name: "claude-nomad",
4868
- version: "0.41.0",
5353
+ version: "0.43.0",
4869
5354
  type: "module",
4870
5355
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
4871
5356
  keywords: [
@@ -4963,6 +5448,11 @@ var DEFAULT_HELP = [
4963
5448
  "Commands:",
4964
5449
  row(" pull", "Sync ~/.claude/ from the shared repo (settings, symlinks, sessions)."),
4965
5450
  row(" --dry-run", "Run lock + git pull, then preview every mutation without writing."),
5451
+ row(" --force-remote", "Recover from a wedged repo (stuck mid-rebase or mid-merge):"),
5452
+ cont("abort the in-progress rebase/merge, park stranded commits on"),
5453
+ cont("nomad/stranded-<ts>, reset to origin/main, and re-pull. Refuses"),
5454
+ cont("if stranded or dirty tracked changes touch synced config (shared/,"),
5455
+ cont("hosts/, path-map.json). Cannot combine with --dry-run."),
4966
5456
  "",
4967
5457
  row(" push", "Rebase, run safety checks (gitleaks, gitlinks, allow-list), commit, push."),
4968
5458
  row(" --dry-run", "Run pre-checks (rebase, gitleaks probe, gitlink scan) and preview"),
@@ -5055,15 +5545,15 @@ var DEFAULT_HELP = [
5055
5545
  init_config();
5056
5546
  init_utils();
5057
5547
  init_utils_json();
5058
- import { existsSync as existsSync36, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "node:fs";
5059
- import { join as join42 } from "node:path";
5548
+ import { existsSync as existsSync39, readFileSync as readFileSync12, readdirSync as readdirSync12 } from "node:fs";
5549
+ import { join as join45 } from "node:path";
5060
5550
  function resumeCmd(sessionId) {
5061
5551
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
5062
5552
  fail(`invalid session id: ${sessionId}`);
5063
5553
  process.exit(1);
5064
5554
  }
5065
- const projectsRoot = join42(CLAUDE_HOME, "projects");
5066
- if (!existsSync36(projectsRoot)) {
5555
+ const projectsRoot = join45(CLAUDE_HOME, "projects");
5556
+ if (!existsSync39(projectsRoot)) {
5067
5557
  fail(`${projectsRoot} does not exist`);
5068
5558
  process.exit(1);
5069
5559
  }
@@ -5077,8 +5567,8 @@ function resumeCmd(sessionId) {
5077
5567
  fail(`no cwd field found in ${jsonlPath}`);
5078
5568
  process.exit(1);
5079
5569
  }
5080
- const mapPath = join42(REPO_HOME, "path-map.json");
5081
- if (!existsSync36(mapPath)) {
5570
+ const mapPath = join45(REPO_HOME, "path-map.json");
5571
+ if (!existsSync39(mapPath)) {
5082
5572
  fail("path-map.json missing");
5083
5573
  process.exit(1);
5084
5574
  }
@@ -5100,14 +5590,14 @@ function resumeCmd(sessionId) {
5100
5590
  console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
5101
5591
  }
5102
5592
  function findTranscriptPath(projectsRoot, sessionId) {
5103
- for (const dir of readdirSync11(projectsRoot)) {
5104
- const candidate = join42(projectsRoot, dir, `${sessionId}.jsonl`);
5105
- if (existsSync36(candidate)) return candidate;
5593
+ for (const dir of readdirSync12(projectsRoot)) {
5594
+ const candidate = join45(projectsRoot, dir, `${sessionId}.jsonl`);
5595
+ if (existsSync39(candidate)) return candidate;
5106
5596
  }
5107
5597
  return null;
5108
5598
  }
5109
5599
  function extractRecordedCwd(jsonlPath) {
5110
- for (const line of readFileSync11(jsonlPath, "utf8").split("\n")) {
5600
+ for (const line of readFileSync12(jsonlPath, "utf8").split("\n")) {
5111
5601
  if (!line.trim()) continue;
5112
5602
  try {
5113
5603
  const obj = JSON.parse(line);
@@ -5174,15 +5664,12 @@ try {
5174
5664
  console.log(package_default.version);
5175
5665
  break;
5176
5666
  case "pull": {
5177
- const sub = process.argv[3];
5178
- if (sub === void 0) {
5179
- cmdPull();
5180
- } else if (sub === "--dry-run" && process.argv.length === 4) {
5181
- cmdPull({ dryRun: true });
5182
- } else {
5183
- console.error("usage: nomad pull [--dry-run]");
5667
+ const pullArgs = parsePullArgs(process.argv);
5668
+ if (pullArgs === null) {
5669
+ console.error("usage: nomad pull [--dry-run] [--force-remote]");
5184
5670
  process.exit(1);
5185
5671
  }
5672
+ cmdPull({ dryRun: pullArgs.dryRun, forceRemote: pullArgs.forceRemote });
5186
5673
  break;
5187
5674
  }
5188
5675
  case "push": {