claude-nomad 0.41.0 → 0.42.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) {
@@ -617,7 +628,7 @@ var init_push_gitleaks_config = __esm({
617
628
  import { execFileSync as execFileSync2 } from "node:child_process";
618
629
  import { readdirSync as readdirSync3, 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();
@@ -660,7 +671,7 @@ function findGitlinks(dir) {
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 existsSync19 } from "node:fs";
1224
+ import { join as join23 } from "node:path";
1214
1225
 
1215
1226
  // src/commands.doctor.checks.repo.ts
1216
1227
  init_color();
@@ -1551,8 +1562,20 @@ function reportNeverSync(section2) {
1551
1562
  init_color();
1552
1563
  init_config();
1553
1564
  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";
1565
+ import { existsSync as existsSync11 } from "node:fs";
1566
+ import { join as join13, relative as relative2 } from "node:path";
1567
+
1568
+ // src/commands.pull.wedge.ts
1569
+ import { existsSync as existsSync9 } from "node:fs";
1570
+ import { join as join10 } from "node:path";
1571
+ function detectWedge(repo) {
1572
+ const g = join10(repo, ".git");
1573
+ if (existsSync9(join10(g, "rebase-merge")) || existsSync9(join10(g, "rebase-apply"))) return "rebase";
1574
+ if (existsSync9(join10(g, "MERGE_HEAD"))) return "merge";
1575
+ return null;
1576
+ }
1577
+
1578
+ // src/commands.doctor.checks.repository.ts
1556
1579
  init_push_checks();
1557
1580
  init_utils();
1558
1581
  function reportGitleaksProbe(section2) {
@@ -1571,8 +1594,8 @@ function reportGitleaksProbe(section2) {
1571
1594
  }
1572
1595
  }
1573
1596
  function reportGitlinks(section2) {
1574
- const sharedDir = join12(REPO_HOME, "shared");
1575
- if (existsSync10(sharedDir)) {
1597
+ const sharedDir = join13(REPO_HOME, "shared");
1598
+ if (existsSync11(sharedDir)) {
1576
1599
  const gitlinks = findGitlinks(sharedDir);
1577
1600
  for (const p of gitlinks) {
1578
1601
  const rel = relative2(REPO_HOME, p);
@@ -1611,11 +1634,25 @@ function reportRebaseClean(section2) {
1611
1634
  } catch {
1612
1635
  }
1613
1636
  }
1637
+ function reportRebaseState(section2) {
1638
+ try {
1639
+ const wedge = detectWedge(REPO_HOME);
1640
+ if (wedge !== null) {
1641
+ const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
1642
+ addItem(
1643
+ section2,
1644
+ `${red(failGlyph)} repo is ${state}: run 'nomad pull --force-remote' to auto-recover`
1645
+ );
1646
+ process.exitCode = 1;
1647
+ }
1648
+ } catch {
1649
+ }
1650
+ }
1614
1651
 
1615
1652
  // src/commands.doctor.checks.backups.ts
1616
1653
  init_color();
1617
- import { existsSync as existsSync11, lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
1618
- import { join as join13 } from "node:path";
1654
+ import { existsSync as existsSync12, lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
1655
+ import { join as join14 } from "node:path";
1619
1656
  init_config();
1620
1657
  var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
1621
1658
  function safeReaddir(dir) {
@@ -1631,7 +1668,7 @@ var BYTES_PER_MB = 1024 * 1024;
1631
1668
  function dirSizeBytes(dir) {
1632
1669
  let bytes = 0;
1633
1670
  for (const entry of safeReaddir(dir)) {
1634
- const full = join13(dir, entry);
1671
+ const full = join14(dir, entry);
1635
1672
  const st = lstatSync5(full, { throwIfNoEntry: false });
1636
1673
  if (!st) continue;
1637
1674
  if (st.isSymbolicLink()) continue;
@@ -1642,11 +1679,11 @@ function dirSizeBytes(dir) {
1642
1679
  }
1643
1680
  function totalSizeMb(backupBase, dirs) {
1644
1681
  let bytes = 0;
1645
- for (const name of dirs) bytes += dirSizeBytes(join13(backupBase, name));
1682
+ for (const name of dirs) bytes += dirSizeBytes(join14(backupBase, name));
1646
1683
  return bytes / BYTES_PER_MB;
1647
1684
  }
1648
1685
  function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
1649
- if (!existsSync11(backupBase)) return;
1686
+ if (!existsSync12(backupBase)) return;
1650
1687
  const dirs = safeReaddir(backupBase).filter((n) => TS_SHAPE2.test(n));
1651
1688
  const count = dirs.length;
1652
1689
  const sizeMb = totalSizeMb(backupBase, dirs);
@@ -1660,8 +1697,8 @@ function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
1660
1697
 
1661
1698
  // src/commands.doctor.check-schema.ts
1662
1699
  init_color();
1663
- import { existsSync as existsSync12 } from "node:fs";
1664
- import { join as join14 } from "node:path";
1700
+ import { existsSync as existsSync13 } from "node:fs";
1701
+ import { join as join15 } from "node:path";
1665
1702
  init_config();
1666
1703
 
1667
1704
  // src/http-fetch.ts
@@ -1698,8 +1735,8 @@ function fetchSchemaKeys() {
1698
1735
  }
1699
1736
  }
1700
1737
  function reportCheckSchema(section2) {
1701
- const settingsPath = join14(CLAUDE_HOME, "settings.json");
1702
- if (!existsSync12(settingsPath)) {
1738
+ const settingsPath = join15(CLAUDE_HOME, "settings.json");
1739
+ if (!existsSync13(settingsPath)) {
1703
1740
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
1704
1741
  return;
1705
1742
  }
@@ -1729,18 +1766,18 @@ function reportCheckSchema(section2) {
1729
1766
  init_color();
1730
1767
  import { randomBytes } from "node:crypto";
1731
1768
  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";
1769
+ import { existsSync as existsSync15, mkdirSync as mkdirSync4, readdirSync as readdirSync6, rmSync as rmSync6 } from "node:fs";
1733
1770
  import { homedir as homedir4 } from "node:os";
1734
- import { join as join18 } from "node:path";
1771
+ import { join as join19 } from "node:path";
1735
1772
 
1736
1773
  // src/commands.doctor.check-shared.scan.ts
1737
1774
  init_color();
1738
- import { join as join16 } from "node:path";
1775
+ import { join as join17 } from "node:path";
1739
1776
  init_config();
1740
1777
  init_push_gitleaks();
1741
1778
  function scrubPath(logical, sid, logicalToEncoded) {
1742
1779
  const encoded = logicalToEncoded.get(logical) ?? logical;
1743
- return join16(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
1780
+ return join17(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
1744
1781
  }
1745
1782
  function reportSessionFindings(section2, bySession) {
1746
1783
  for (const [sid, counts] of bySession) {
@@ -1832,8 +1869,8 @@ init_config();
1832
1869
  init_utils();
1833
1870
  init_utils_fs();
1834
1871
  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";
1872
+ import { cpSync as cpSync3, existsSync as existsSync14, mkdirSync as mkdirSync3, readdirSync as readdirSync5, rmSync as rmSync5, statSync as statSync4 } from "node:fs";
1873
+ import { join as join18, relative as relative3, sep } from "node:path";
1837
1874
  function copyDir(src, dst) {
1838
1875
  rmSync5(dst, { recursive: true, force: true });
1839
1876
  cpSync3(src, dst, { recursive: true, force: true });
@@ -1863,15 +1900,15 @@ function remapPull(ts, opts = {}) {
1863
1900
  let unmapped = 0;
1864
1901
  const pulled = [];
1865
1902
  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)) {
1903
+ const mapPath = join18(REPO_HOME, "path-map.json");
1904
+ const repoProjects = join18(REPO_HOME, "shared", "projects");
1905
+ if (!existsSync14(mapPath) || !existsSync14(repoProjects)) {
1869
1906
  const text = "no path-map or repo projects dir; skipping session remap";
1870
1907
  emitPreview(opts.onPreview, { kind: "note", text }, text);
1871
1908
  return { unmapped: 0, pulled, wouldPull };
1872
1909
  }
1873
1910
  const map = readJson(mapPath);
1874
- const localProjects = join17(CLAUDE_HOME, "projects");
1911
+ const localProjects = join18(CLAUDE_HOME, "projects");
1875
1912
  if (!dryRun) mkdirSync3(localProjects, { recursive: true });
1876
1913
  for (const [logical, hosts] of Object.entries(map.projects)) {
1877
1914
  assertSafeLogical(logical);
@@ -1880,9 +1917,9 @@ function remapPull(ts, opts = {}) {
1880
1917
  unmapped++;
1881
1918
  continue;
1882
1919
  }
1883
- const src = join17(repoProjects, logical);
1884
- if (!existsSync13(src)) continue;
1885
- const dst = join17(localProjects, encodePath(localPath));
1920
+ const src = join18(repoProjects, logical);
1921
+ if (!existsSync14(src)) continue;
1922
+ const dst = join18(localProjects, encodePath(localPath));
1886
1923
  if (dryRun) {
1887
1924
  wouldPull.push(logical);
1888
1925
  emitPreview(
@@ -1927,16 +1964,16 @@ function remapPush(ts, opts = {}) {
1927
1964
  let unmapped = 0;
1928
1965
  const pushed = [];
1929
1966
  const wouldPush = [];
1930
- const mapPath = join17(REPO_HOME, "path-map.json");
1931
- if (!existsSync13(mapPath)) {
1967
+ const mapPath = join18(REPO_HOME, "path-map.json");
1968
+ if (!existsSync14(mapPath)) {
1932
1969
  log("no path-map.json; skipping session export");
1933
1970
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
1934
1971
  }
1935
1972
  const map = readJson(mapPath);
1936
- const localProjects = join17(CLAUDE_HOME, "projects");
1937
- const repoProjects = join17(REPO_HOME, "shared", "projects");
1973
+ const localProjects = join18(CLAUDE_HOME, "projects");
1974
+ const repoProjects = join18(REPO_HOME, "shared", "projects");
1938
1975
  const reverse = buildReverseMap(map);
1939
- if (!existsSync13(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
1976
+ if (!existsSync14(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
1940
1977
  if (!dryRun) mkdirSync3(repoProjects, { recursive: true });
1941
1978
  for (const dir of readdirSync5(localProjects)) {
1942
1979
  const logical = reverse.get(dir);
@@ -1944,13 +1981,13 @@ function remapPush(ts, opts = {}) {
1944
1981
  unmapped++;
1945
1982
  continue;
1946
1983
  }
1947
- const repoDst = join17(repoProjects, logical);
1984
+ const repoDst = join18(repoProjects, logical);
1948
1985
  if (dryRun) {
1949
1986
  wouldPush.push(logical);
1950
1987
  continue;
1951
1988
  }
1952
1989
  backupRepoWrite(repoDst, ts, REPO_HOME);
1953
- copyDirJsonlOnly(join17(localProjects, dir), repoDst);
1990
+ copyDirJsonlOnly(join18(localProjects, dir), repoDst);
1954
1991
  pushed.push(logical);
1955
1992
  }
1956
1993
  return { unmapped, collisions: 0, pushed, wouldPush };
@@ -1962,8 +1999,8 @@ init_utils_json();
1962
1999
  function buildScanTree(tmpRoot) {
1963
2000
  const logicalToEncoded = /* @__PURE__ */ new Map();
1964
2001
  let staged = 0;
1965
- const mapPath = join18(REPO_HOME, "path-map.json");
1966
- if (!existsSync14(mapPath)) return { logicalToEncoded, staged, malformed: false };
2002
+ const mapPath = join19(REPO_HOME, "path-map.json");
2003
+ if (!existsSync15(mapPath)) return { logicalToEncoded, staged, malformed: false };
1967
2004
  let map;
1968
2005
  try {
1969
2006
  map = readJson(mapPath);
@@ -1980,12 +2017,12 @@ function buildScanTree(tmpRoot) {
1980
2017
  if (!p || p === "TBD") continue;
1981
2018
  reverse.set(encodePath(p), logical);
1982
2019
  }
1983
- const localProjects = join18(CLAUDE_HOME, "projects");
1984
- if (!existsSync14(localProjects)) return { logicalToEncoded, staged, malformed: false };
2020
+ const localProjects = join19(CLAUDE_HOME, "projects");
2021
+ if (!existsSync15(localProjects)) return { logicalToEncoded, staged, malformed: false };
1985
2022
  for (const dir of readdirSync6(localProjects)) {
1986
2023
  const logical = reverse.get(dir);
1987
2024
  if (!logical) continue;
1988
- copyDirJsonlOnly(join18(localProjects, dir), join18(tmpRoot, "shared", "projects", logical));
2025
+ copyDirJsonlOnly(join19(localProjects, dir), join19(tmpRoot, "shared", "projects", logical));
1989
2026
  logicalToEncoded.set(logical, dir);
1990
2027
  staged++;
1991
2028
  }
@@ -2016,11 +2053,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
2016
2053
  }
2017
2054
  function reportCheckShared(section2, gitleaksReady) {
2018
2055
  if (!ensureGitleaksReady(section2, gitleaksReady)) return;
2019
- const cacheDir = join18(homedir4(), ".cache", "claude-nomad");
2056
+ const cacheDir = join19(homedir4(), ".cache", "claude-nomad");
2020
2057
  mkdirSync4(cacheDir, { recursive: true });
2021
2058
  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}`);
2059
+ const reportPath = join19(cacheDir, `check-shared-${stamp}.json`);
2060
+ const tmpRoot = join19(cacheDir, `check-shared-tree-${stamp}`);
2024
2061
  try {
2025
2062
  const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
2026
2063
  if (malformed) {
@@ -2041,8 +2078,8 @@ function reportCheckShared(section2, gitleaksReady) {
2041
2078
 
2042
2079
  // src/commands.doctor.checks.hooks.scope.ts
2043
2080
  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";
2081
+ import { existsSync as existsSync16, readFileSync as readFileSync5, readdirSync as readdirSync7, realpathSync } from "node:fs";
2082
+ import { dirname as dirname2, extname, join as join20 } from "node:path";
2046
2083
  init_config();
2047
2084
  function typeFromPackageJson(pkgPath) {
2048
2085
  try {
@@ -2064,8 +2101,8 @@ function effectiveType(hookPath) {
2064
2101
  }
2065
2102
  let dir = dirname2(real);
2066
2103
  for (; ; ) {
2067
- const pkg = join19(dir, "package.json");
2068
- if (existsSync15(pkg)) return typeFromPackageJson(pkg);
2104
+ const pkg = join20(dir, "package.json");
2105
+ if (existsSync16(pkg)) return typeFromPackageJson(pkg);
2069
2106
  const parent = dirname2(dir);
2070
2107
  if (parent === dir) return "cjs";
2071
2108
  dir = parent;
@@ -2107,15 +2144,15 @@ function safeRead(path) {
2107
2144
  }
2108
2145
  }
2109
2146
  function reportHookScopeCheck(section2) {
2110
- const hooksDir = join19(CLAUDE_HOME, "hooks");
2111
- if (!existsSync15(hooksDir)) {
2147
+ const hooksDir = join20(CLAUDE_HOME, "hooks");
2148
+ if (!existsSync16(hooksDir)) {
2112
2149
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
2113
2150
  return;
2114
2151
  }
2115
2152
  let anyWarn = false;
2116
2153
  for (const name of safeReaddir2(hooksDir)) {
2117
2154
  if (extname(name) !== ".js") continue;
2118
- const abs = join19(hooksDir, name);
2155
+ const abs = join20(hooksDir, name);
2119
2156
  const eff = effectiveType(abs);
2120
2157
  if (eff === null) continue;
2121
2158
  const src = safeRead(abs);
@@ -2135,8 +2172,8 @@ function reportHookScopeCheck(section2) {
2135
2172
 
2136
2173
  // src/commands.doctor.checks.hooks.ts
2137
2174
  init_color();
2138
- import { existsSync as existsSync16 } from "node:fs";
2139
- import { join as join20 } from "node:path";
2175
+ import { existsSync as existsSync17 } from "node:fs";
2176
+ import { join as join21 } from "node:path";
2140
2177
  init_config();
2141
2178
  function expandHome(token) {
2142
2179
  return token.replace(/^\$\{HOME\}/, HOME).replace(/^\$HOME/, HOME).replace(/^~/, HOME);
@@ -2174,7 +2211,7 @@ function checkEventGroups(section2, event, groups) {
2174
2211
  for (const group of groups) {
2175
2212
  for (const cmd of commandsFromGroup(group)) {
2176
2213
  for (const resolved of claudePathsIn(cmd)) {
2177
- if (existsSync16(resolved)) continue;
2214
+ if (existsSync17(resolved)) continue;
2178
2215
  addItem(section2, `${red(failGlyph)} hooks/${event}: command target missing: ${resolved}`);
2179
2216
  process.exitCode = 1;
2180
2217
  anyFail = true;
@@ -2184,8 +2221,8 @@ function checkEventGroups(section2, event, groups) {
2184
2221
  return anyFail;
2185
2222
  }
2186
2223
  function reportHooksTargetCheck(section2) {
2187
- const settingsPath = join20(CLAUDE_HOME, "settings.json");
2188
- if (!existsSync16(settingsPath)) {
2224
+ const settingsPath = join21(CLAUDE_HOME, "settings.json");
2225
+ if (!existsSync17(settingsPath)) {
2189
2226
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
2190
2227
  return;
2191
2228
  }
@@ -2315,8 +2352,8 @@ function reportNodeEngineCheck(section2) {
2315
2352
  // src/commands.doctor.gitleaks-version.ts
2316
2353
  init_color();
2317
2354
  import { execFileSync as execFileSync7 } from "node:child_process";
2318
- import { existsSync as existsSync17 } from "node:fs";
2319
- import { join as join21 } from "node:path";
2355
+ import { existsSync as existsSync18 } from "node:fs";
2356
+ import { join as join22 } from "node:path";
2320
2357
  init_config();
2321
2358
  var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
2322
2359
  var GITLEAKS_TIMEOUT_MS = 5e3;
@@ -2325,7 +2362,7 @@ function majorMinorOf(value) {
2325
2362
  return m === null ? null : [m[1], m[2]];
2326
2363
  }
2327
2364
  function readGitleaksVersion(run, tomlExists) {
2328
- const tomlPath = join21(REPO_HOME, ".gitleaks.toml");
2365
+ const tomlPath = join22(REPO_HOME, ".gitleaks.toml");
2329
2366
  const args = ["version"];
2330
2367
  if (tomlExists(tomlPath)) args.push("--config", tomlPath);
2331
2368
  try {
@@ -2337,7 +2374,7 @@ function readGitleaksVersion(run, tomlExists) {
2337
2374
  return null;
2338
2375
  }
2339
2376
  }
2340
- function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync17) {
2377
+ function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync18) {
2341
2378
  const raw = readGitleaksVersion(run, tomlExists);
2342
2379
  if (raw === null) return;
2343
2380
  const local = majorMinorOf(raw);
@@ -2510,8 +2547,8 @@ function cmdDoctor(opts = {}) {
2510
2547
  reportHostAndPaths(host);
2511
2548
  reportRepoState(host);
2512
2549
  const links = section("Shared links");
2513
- const mapPath = join22(REPO_HOME, "path-map.json");
2514
- const rawMap = existsSync18(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
2550
+ const mapPath = join23(REPO_HOME, "path-map.json");
2551
+ const rawMap = existsSync19(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
2515
2552
  const map = rawMap ?? { projects: {} };
2516
2553
  reportSharedLinks(links, map);
2517
2554
  const hooksScan = section("Hook targets");
@@ -2530,6 +2567,7 @@ function cmdDoctor(opts = {}) {
2530
2567
  reportGitlinks(repository);
2531
2568
  reportRemote(repository);
2532
2569
  reportRebaseClean(repository);
2570
+ reportRebaseState(repository);
2533
2571
  reportActionsDrift(repository);
2534
2572
  const nomadVersion = section("Nomad Version");
2535
2573
  reportVersionCheck(nomadVersion);
@@ -2560,8 +2598,8 @@ function cmdDoctor(opts = {}) {
2560
2598
  // src/commands.drop-session.ts
2561
2599
  init_config();
2562
2600
  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";
2601
+ import { existsSync as existsSync21, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
2602
+ import { join as join26, relative as relative4 } from "node:path";
2565
2603
 
2566
2604
  // src/commands.drop-session.git.ts
2567
2605
  init_config();
@@ -2604,8 +2642,8 @@ function isInIndex(rel) {
2604
2642
  init_config();
2605
2643
  init_utils();
2606
2644
  init_utils_json();
2607
- import { existsSync as existsSync19 } from "node:fs";
2608
- import { join as join23 } from "node:path";
2645
+ import { existsSync as existsSync20 } from "node:fs";
2646
+ import { join as join24 } from "node:path";
2609
2647
  var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
2610
2648
  function reportScrubHint(id, matches) {
2611
2649
  const live = resolveLiveTranscript(id, matches);
@@ -2621,16 +2659,16 @@ function reportScrubHint(id, matches) {
2621
2659
  }
2622
2660
  function resolveLiveTranscript(id, matches) {
2623
2661
  try {
2624
- const mapPath = join23(REPO_HOME, "path-map.json");
2625
- if (!existsSync19(mapPath)) return null;
2662
+ const mapPath = join24(REPO_HOME, "path-map.json");
2663
+ if (!existsSync20(mapPath)) return null;
2626
2664
  const projects = readJson(mapPath).projects;
2627
2665
  for (const rel of matches) {
2628
2666
  const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
2629
2667
  if (logical === void 0) continue;
2630
2668
  const abs = projects[logical]?.[HOST];
2631
2669
  if (abs === void 0) continue;
2632
- const live = join23(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2633
- if (existsSync19(live)) return live;
2670
+ const live = join24(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2671
+ if (existsSync20(live)) return live;
2634
2672
  }
2635
2673
  return null;
2636
2674
  } catch {
@@ -2645,8 +2683,8 @@ init_utils();
2645
2683
  init_config();
2646
2684
  init_utils();
2647
2685
  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");
2686
+ import { dirname as dirname3, join as join25 } from "node:path";
2687
+ var LOCK_PATH = join25(HOME, ".cache", "claude-nomad", "nomad.lock");
2650
2688
  function acquireLock(verb) {
2651
2689
  mkdirSync5(dirname3(LOCK_PATH), { recursive: true });
2652
2690
  try {
@@ -2756,12 +2794,12 @@ function cmdDropSession(id) {
2756
2794
  fail(`invalid session id: ${id}`);
2757
2795
  process.exit(1);
2758
2796
  }
2759
- if (!existsSync20(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2797
+ if (!existsSync21(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2760
2798
  const handle = acquireLock("drop-session");
2761
2799
  if (handle === null) process.exit(0);
2762
2800
  try {
2763
- const repoProjects = join25(REPO_HOME, "shared", "projects");
2764
- if (!existsSync20(repoProjects)) {
2801
+ const repoProjects = join26(REPO_HOME, "shared", "projects");
2802
+ if (!existsSync21(repoProjects)) {
2765
2803
  throw new NomadFatal(`no staged session matches ${id}`);
2766
2804
  }
2767
2805
  const matches = collectMatches(repoProjects, id);
@@ -2783,12 +2821,12 @@ function cmdDropSession(id) {
2783
2821
  function collectMatches(repoProjects, id) {
2784
2822
  const matches = [];
2785
2823
  for (const logical of readdirSync8(repoProjects)) {
2786
- const candidate = join25(repoProjects, logical, `${id}.jsonl`);
2787
- if (existsSync20(candidate)) {
2824
+ const candidate = join26(repoProjects, logical, `${id}.jsonl`);
2825
+ if (existsSync21(candidate)) {
2788
2826
  matches.push(relative4(REPO_HOME, candidate));
2789
2827
  }
2790
- const dir = join25(repoProjects, logical, id);
2791
- if (existsSync20(dir) && statSync5(dir).isDirectory()) {
2828
+ const dir = join26(repoProjects, logical, id);
2829
+ if (existsSync21(dir) && statSync5(dir).isDirectory()) {
2792
2830
  const dirRel = relative4(REPO_HOME, dir);
2793
2831
  const staged = expandStagedDir(dirRel);
2794
2832
  if (staged.length > 0) matches.push(...staged);
@@ -2824,19 +2862,19 @@ function unstageOne(rel) {
2824
2862
 
2825
2863
  // src/commands.redact.ts
2826
2864
  init_config();
2827
- import { existsSync as existsSync22, statSync as statSync7 } from "node:fs";
2828
- import { dirname as dirname4, join as join27 } from "node:path";
2865
+ import { existsSync as existsSync23, statSync as statSync7 } from "node:fs";
2866
+ import { dirname as dirname4, join as join28 } from "node:path";
2829
2867
 
2830
2868
  // 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";
2869
+ import { existsSync as existsSync22, lstatSync as lstatSync6, readFileSync as readFileSync9, readdirSync as readdirSync9, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
2870
+ import { join as join27 } from "node:path";
2833
2871
  init_utils_fs();
2834
2872
  function collectFiles(dir, out) {
2835
- if (!existsSync21(dir)) return;
2873
+ if (!existsSync22(dir)) return;
2836
2874
  const st = lstatSync6(dir);
2837
2875
  if (!st.isDirectory()) return;
2838
2876
  for (const entry of readdirSync9(dir)) {
2839
- const abs = join26(dir, entry);
2877
+ const abs = join27(dir, entry);
2840
2878
  const lst = lstatSync6(abs);
2841
2879
  if (lst.isSymbolicLink()) continue;
2842
2880
  if (lst.isDirectory()) {
@@ -2886,14 +2924,14 @@ init_utils_json();
2886
2924
  init_utils();
2887
2925
  function resolveLiveTranscript2(id) {
2888
2926
  try {
2889
- const mapPath = join27(REPO_HOME, "path-map.json");
2890
- if (!existsSync22(mapPath)) return null;
2927
+ const mapPath = join28(REPO_HOME, "path-map.json");
2928
+ if (!existsSync23(mapPath)) return null;
2891
2929
  const projects = readJson(mapPath).projects;
2892
2930
  for (const hostMap of Object.values(projects)) {
2893
2931
  const abs = hostMap[HOST];
2894
2932
  if (abs === void 0) continue;
2895
- const live = join27(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2896
- if (existsSync22(live)) return live;
2933
+ const live = join28(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2934
+ if (existsSync23(live)) return live;
2897
2935
  }
2898
2936
  return null;
2899
2937
  } catch {
@@ -2911,17 +2949,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
2911
2949
  fail(`invalid session id: ${id}`);
2912
2950
  process.exit(1);
2913
2951
  }
2914
- if (!existsSync22(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2952
+ if (!existsSync23(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2915
2953
  const handle = acquireLock("redact");
2916
2954
  if (handle === null) process.exit(0);
2917
2955
  try {
2918
2956
  const localPath = resolveLiveTranscript2(id);
2919
- if (localPath === null || !existsSync22(localPath)) {
2957
+ if (localPath === null || !existsSync23(localPath)) {
2920
2958
  fail(`could not resolve local transcript for session ${id} on this host`);
2921
2959
  process.exitCode = 1;
2922
2960
  return;
2923
2961
  }
2924
- const sessionDir = join27(dirname4(localPath), id);
2962
+ const sessionDir = join28(dirname4(localPath), id);
2925
2963
  const subtreeFiles = listSubtreeFiles(sessionDir);
2926
2964
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
2927
2965
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -2974,8 +3012,8 @@ ${lines}`);
2974
3012
  }
2975
3013
 
2976
3014
  // src/commands.pull.ts
2977
- import { existsSync as existsSync30, mkdirSync as mkdirSync8 } from "node:fs";
2978
- import { join as join36 } from "node:path";
3015
+ import { existsSync as existsSync31, mkdirSync as mkdirSync8 } from "node:fs";
3016
+ import { join as join37 } from "node:path";
2979
3017
 
2980
3018
  // src/commands.push.sections.ts
2981
3019
  init_color();
@@ -3063,22 +3101,34 @@ init_config();
3063
3101
 
3064
3102
  // src/extras-sync.ts
3065
3103
  init_config();
3066
- import { existsSync as existsSync25 } from "node:fs";
3067
- import { join as join30 } from "node:path";
3104
+ import { existsSync as existsSync26 } from "node:fs";
3105
+ import { join as join31 } from "node:path";
3068
3106
 
3069
3107
  // src/extras-sync.diff.ts
3070
3108
  init_utils();
3071
3109
  import { execFileSync as execFileSync13 } from "node:child_process";
3110
+ function labelDiffLine(line) {
3111
+ const tab = line.indexOf(" ");
3112
+ if (tab === -1) return line;
3113
+ const status = line.slice(0, tab);
3114
+ const path = line.slice(tab + 1);
3115
+ if (status === "D") return `${path} (local only)`;
3116
+ if (status === "A") return `${path} (repo only)`;
3117
+ return path;
3118
+ }
3119
+ function parseDiffOutput(stdout) {
3120
+ return stdout.split("\n").filter((line) => line.length > 0).map(labelDiffLine);
3121
+ }
3072
3122
  function listDivergingFiles(a, b) {
3073
3123
  try {
3074
- const stdout = execFileSync13("git", ["diff", "--no-index", "--name-only", a, b], {
3124
+ const stdout = execFileSync13("git", ["diff", "--no-index", "--name-status", a, b], {
3075
3125
  stdio: ["ignore", "pipe", "pipe"]
3076
3126
  }).toString();
3077
- return stdout.split("\n").filter((line) => line.length > 0);
3127
+ return parseDiffOutput(stdout);
3078
3128
  } catch (err) {
3079
3129
  const e = err;
3080
3130
  if (e.status === 1 && e.stdout !== void 0) {
3081
- return e.stdout.toString().split("\n").filter((line) => line.length > 0);
3131
+ return parseDiffOutput(e.stdout.toString());
3082
3132
  }
3083
3133
  if (e.code === "ENOENT") {
3084
3134
  warn(`git not on PATH; divergence check skipped for ${a}`);
@@ -3091,8 +3141,8 @@ function listDivergingFiles(a, b) {
3091
3141
 
3092
3142
  // src/extras-sync.core.ts
3093
3143
  init_config();
3094
- import { cpSync as cpSync4, existsSync as existsSync23, rmSync as rmSync7 } from "node:fs";
3095
- import { join as join28 } from "node:path";
3144
+ import { cpSync as cpSync4, existsSync as existsSync24, rmSync as rmSync7 } from "node:fs";
3145
+ import { join as join29 } from "node:path";
3096
3146
 
3097
3147
  // src/extras-sync.guards.ts
3098
3148
  init_utils();
@@ -3115,9 +3165,9 @@ function assertSafeLocalRoot(localRoot, logical) {
3115
3165
  init_utils();
3116
3166
  init_utils_json();
3117
3167
  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)) {
3168
+ const mapPath = join29(REPO_HOME, "path-map.json");
3169
+ const repoExtras = join29(REPO_HOME, "shared", "extras");
3170
+ if (!existsSync24(mapPath) || opts.requireRepoExtras === true && !existsSync24(repoExtras)) {
3121
3171
  if (opts.missingMsg !== void 0) log(opts.missingMsg);
3122
3172
  return null;
3123
3173
  }
@@ -3159,8 +3209,8 @@ init_utils_json();
3159
3209
 
3160
3210
  // src/extras-sync.remap.ts
3161
3211
  init_config();
3162
- import { existsSync as existsSync24, mkdirSync as mkdirSync6 } from "node:fs";
3163
- import { join as join29 } from "node:path";
3212
+ import { existsSync as existsSync25, mkdirSync as mkdirSync6 } from "node:fs";
3213
+ import { join as join30 } from "node:path";
3164
3214
  init_utils_fs();
3165
3215
  function runExtrasOp(v, dryRun, paths, backup) {
3166
3216
  const counts = { unmapped: 0, skipped: 0 };
@@ -3168,7 +3218,7 @@ function runExtrasOp(v, dryRun, paths, backup) {
3168
3218
  const would = [];
3169
3219
  for (const t of eachExtrasTarget(v, counts)) {
3170
3220
  const { src, dst } = paths(t);
3171
- if (!existsSync24(src)) continue;
3221
+ if (!existsSync25(src)) continue;
3172
3222
  const item = `${t.logical}/${t.dirname}`;
3173
3223
  if (dryRun) {
3174
3224
  would.push(item);
@@ -3184,14 +3234,14 @@ function remapExtrasPush(ts, opts = {}) {
3184
3234
  const dryRun = opts.dryRun === true;
3185
3235
  const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
3186
3236
  if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
3187
- const repoExtras = join29(REPO_HOME, "shared", "extras");
3237
+ const repoExtras = join30(REPO_HOME, "shared", "extras");
3188
3238
  if (!dryRun) mkdirSync6(repoExtras, { recursive: true });
3189
3239
  const { unmapped, skipped, done, would } = runExtrasOp(
3190
3240
  v,
3191
3241
  dryRun,
3192
3242
  ({ localRoot, logical, dirname: dirname6 }) => ({
3193
- src: join29(localRoot, dirname6),
3194
- dst: join29(repoExtras, logical, dirname6)
3243
+ src: join30(localRoot, dirname6),
3244
+ dst: join30(repoExtras, logical, dirname6)
3195
3245
  }),
3196
3246
  (dst) => backupRepoWrite(dst, ts, REPO_HOME)
3197
3247
  );
@@ -3204,13 +3254,13 @@ function remapExtrasPull(ts, opts = {}) {
3204
3254
  missingMsg: "no path-map or repo extras dir; skipping extras remap"
3205
3255
  });
3206
3256
  if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
3207
- const repoExtras = join29(REPO_HOME, "shared", "extras");
3257
+ const repoExtras = join30(REPO_HOME, "shared", "extras");
3208
3258
  const { unmapped, skipped, done, would } = runExtrasOp(
3209
3259
  v,
3210
3260
  dryRun,
3211
3261
  ({ localRoot, logical, dirname: dirname6 }) => ({
3212
- src: join29(repoExtras, logical, dirname6),
3213
- dst: join29(localRoot, dirname6)
3262
+ src: join30(repoExtras, logical, dirname6),
3263
+ dst: join30(localRoot, dirname6)
3214
3264
  }),
3215
3265
  // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
3216
3266
  // localRoot so the backup tree mirrors the project layout.
@@ -3224,14 +3274,14 @@ function divergenceCheckExtras(ts) {
3224
3274
  const v = loadValidatedExtras({});
3225
3275
  if (v === null) return;
3226
3276
  const counts = { unmapped: 0, skipped: 0 };
3227
- const backupRoot = join30(BACKUP_BASE, ts, "extras");
3277
+ const backupRoot = join31(BACKUP_BASE, ts, "extras");
3228
3278
  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;
3279
+ const local = join31(localRoot, dirname6);
3280
+ const repo = join31(REPO_HOME, "shared", "extras", logical, dirname6);
3281
+ if (!existsSync26(local) || !existsSync26(repo)) continue;
3232
3282
  const diff = listDivergingFiles(local, repo);
3233
3283
  if (diff.length === 0) continue;
3234
- const projectBackupRoot = join30(backupRoot, encodePath(localRoot));
3284
+ const projectBackupRoot = join31(backupRoot, encodePath(localRoot));
3235
3285
  warn(
3236
3286
  `local ${dirname6} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
3237
3287
  );
@@ -3244,8 +3294,8 @@ init_config();
3244
3294
  init_utils();
3245
3295
  init_utils_fs();
3246
3296
  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";
3297
+ import { existsSync as existsSync27, lstatSync as lstatSync7, rmSync as rmSync8 } from "node:fs";
3298
+ import { join as join32 } from "node:path";
3249
3299
  function emitAutoMove(onPreview, linkPath, ts, name) {
3250
3300
  if (onPreview) {
3251
3301
  onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
@@ -3264,11 +3314,11 @@ function applySharedLinks(ts, map, opts = {}) {
3264
3314
  const dryRun = opts.dryRun === true;
3265
3315
  const linkNames = allSharedLinks(map);
3266
3316
  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;
3317
+ const linkPath = join32(CLAUDE_HOME, name);
3318
+ const target = join32(REPO_HOME, "shared", name);
3319
+ if (!existsSync27(linkPath)) continue;
3270
3320
  if (lstatSync7(linkPath).isSymbolicLink()) continue;
3271
- if (!existsSync26(target)) continue;
3321
+ if (!existsSync27(target)) continue;
3272
3322
  if (dryRun) {
3273
3323
  emitAutoMove(opts.onPreview, linkPath, ts, name);
3274
3324
  continue;
@@ -3277,28 +3327,28 @@ function applySharedLinks(ts, map, opts = {}) {
3277
3327
  rmSync8(linkPath, { recursive: true, force: true });
3278
3328
  }
3279
3329
  for (const name of linkNames) {
3280
- const target = join31(REPO_HOME, "shared", name);
3281
- if (!existsSync26(target)) continue;
3330
+ const target = join32(REPO_HOME, "shared", name);
3331
+ if (!existsSync27(target)) continue;
3282
3332
  if (dryRun) {
3283
- emitCreate(opts.onPreview, join31(CLAUDE_HOME, name), target);
3333
+ emitCreate(opts.onPreview, join32(CLAUDE_HOME, name), target);
3284
3334
  continue;
3285
3335
  }
3286
- ensureSymlink(join31(CLAUDE_HOME, name), target);
3336
+ ensureSymlink(join32(CLAUDE_HOME, name), target);
3287
3337
  }
3288
3338
  }
3289
3339
  function regenerateSettings(ts, opts = {}) {
3290
3340
  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)) {
3341
+ const basePath = join32(REPO_HOME, "shared", "settings.base.json");
3342
+ const hostPath = join32(REPO_HOME, "hosts", `${HOST}.json`);
3343
+ if (!existsSync27(basePath)) {
3294
3344
  die("repo not initialized; run 'nomad init' to scaffold");
3295
3345
  }
3296
3346
  const base = readJson(basePath);
3297
- const hasOverrides = existsSync26(hostPath);
3347
+ const hasOverrides = existsSync27(hostPath);
3298
3348
  const overrides = hasOverrides ? readJson(hostPath) : {};
3299
3349
  const merged = deepMerge(base, overrides);
3300
- const settingsPath = join31(CLAUDE_HOME, "settings.json");
3301
- if (!hasOverrides && existsSync26(settingsPath)) {
3350
+ const settingsPath = join32(CLAUDE_HOME, "settings.json");
3351
+ if (!hasOverrides && existsSync27(settingsPath)) {
3302
3352
  try {
3303
3353
  const existing = readJson(settingsPath);
3304
3354
  const baseKeys = new Set(Object.keys(base));
@@ -3324,8 +3374,8 @@ function regenerateSettings(ts, opts = {}) {
3324
3374
 
3325
3375
  // src/preview.ts
3326
3376
  init_config();
3327
- import { existsSync as existsSync27 } from "node:fs";
3328
- import { join as join32 } from "node:path";
3377
+ import { existsSync as existsSync28 } from "node:fs";
3378
+ import { join as join33 } from "node:path";
3329
3379
 
3330
3380
  // node_modules/diff/libesm/diff/base.js
3331
3381
  var Diff = class {
@@ -3600,6 +3650,7 @@ function diffLinesToUnified(oldStr, newStr) {
3600
3650
 
3601
3651
  // src/preview.ts
3602
3652
  init_utils_json();
3653
+ var CANONICAL_ORDER_NOTE = "settings.json will be rewritten in canonical key order; no value changes";
3603
3654
  function diffJsonStrings(currentJsonText, newJsonText) {
3604
3655
  if (currentJsonText === newJsonText) return "";
3605
3656
  const lines = [
@@ -3610,7 +3661,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
3610
3661
  return lines.join("\n");
3611
3662
  }
3612
3663
  function readJsonOrNull(path) {
3613
- if (!existsSync27(path)) return null;
3664
+ if (!existsSync28(path)) return null;
3614
3665
  try {
3615
3666
  return readJson(path);
3616
3667
  } catch {
@@ -3624,18 +3675,20 @@ function previewSettings(basePath, hostPath, settingsPath) {
3624
3675
  }
3625
3676
  const notes = [];
3626
3677
  const hostOverrides = readJsonOrNull(hostPath);
3627
- if (hostOverrides === null && existsSync27(hostPath)) {
3678
+ if (hostOverrides === null && existsSync28(hostPath)) {
3628
3679
  notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
3629
3680
  }
3630
3681
  const merged = deepMerge(base, hostOverrides ?? {});
3631
3682
  const current = readJsonOrNull(settingsPath);
3632
- if (current === null && existsSync27(settingsPath)) {
3683
+ if (current === null && existsSync28(settingsPath)) {
3633
3684
  return { diff: "", notes: [...notes, "malformed; skipping diff"] };
3634
3685
  }
3686
+ const rawEqual = JSON.stringify(current ?? {}, null, 2) === JSON.stringify(merged, null, 2);
3635
3687
  const diff = diffJsonStrings(
3636
- JSON.stringify(current ?? {}, null, 2),
3637
- JSON.stringify(merged, null, 2)
3688
+ JSON.stringify(sortKeysDeep(current ?? {}), null, 2),
3689
+ JSON.stringify(sortKeysDeep(merged), null, 2)
3638
3690
  );
3691
+ if (diff === "" && !rawEqual) notes.push(CANONICAL_ORDER_NOTE);
3639
3692
  return { diff, notes };
3640
3693
  }
3641
3694
  function formatLinkRow(e) {
@@ -3665,9 +3718,9 @@ function computePreview(ts, map, verb = "pull") {
3665
3718
  onPreview: (e) => addItem(links, formatLinkRow(e))
3666
3719
  });
3667
3720
  const settingsResult = previewSettings(
3668
- join32(REPO_HOME, "shared", "settings.base.json"),
3669
- join32(REPO_HOME, "hosts", `${HOST}.json`),
3670
- join32(CLAUDE_HOME, "settings.json")
3721
+ join33(REPO_HOME, "shared", "settings.base.json"),
3722
+ join33(REPO_HOME, "hosts", `${HOST}.json`),
3723
+ join33(CLAUDE_HOME, "settings.json")
3671
3724
  );
3672
3725
  const settingsSection = buildSettingsSectionForPreview(settingsResult);
3673
3726
  const sessions = section("Sessions");
@@ -3683,21 +3736,21 @@ function computePreview(ts, map, verb = "pull") {
3683
3736
 
3684
3737
  // src/spinner.ts
3685
3738
  init_color();
3686
- import { existsSync as existsSync29 } from "node:fs";
3739
+ import { existsSync as existsSync30 } from "node:fs";
3687
3740
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3688
3741
  import { Worker } from "node:worker_threads";
3689
3742
 
3690
3743
  // src/commands.push.recovery.ts
3691
3744
  init_config();
3692
3745
  import { readFileSync as readFileSync10, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
3693
- import { join as join35 } from "node:path";
3746
+ import { join as join36 } from "node:path";
3694
3747
  import { createInterface } from "node:readline/promises";
3695
3748
 
3696
3749
  // src/commands.push.recovery.redact.ts
3697
3750
  init_config();
3698
3751
  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";
3752
+ import { cpSync as cpSync5, existsSync as existsSync29, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
3753
+ import { dirname as dirname5, join as join34, sep as sep2 } from "node:path";
3701
3754
  init_push_gitleaks_scan();
3702
3755
  init_utils_json();
3703
3756
  init_utils();
@@ -3728,8 +3781,8 @@ function resolveStagedDir(localPath, map) {
3728
3781
  assertSafeLogical(logical);
3729
3782
  const abs = hostMap[HOST];
3730
3783
  if (abs === void 0) continue;
3731
- if (localPath.startsWith(join33(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
3732
- return join33(REPO_HOME, "shared", "projects", logical);
3784
+ if (localPath.startsWith(join34(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
3785
+ return join34(REPO_HOME, "shared", "projects", logical);
3733
3786
  }
3734
3787
  }
3735
3788
  return null;
@@ -3751,7 +3804,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3751
3804
  `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
3752
3805
  );
3753
3806
  }
3754
- const sessionDir = join33(dirname5(localPath), sid);
3807
+ const sessionDir = join34(dirname5(localPath), sid);
3755
3808
  const subtreeFiles = listSubtreeFiles(sessionDir);
3756
3809
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
3757
3810
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -3785,9 +3838,9 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3785
3838
  );
3786
3839
  }
3787
3840
  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 });
3841
+ cpSync5(localPath, join34(stagedProjectDir, `${sid}.jsonl`), { force: true });
3842
+ if (existsSync29(sessionDir)) {
3843
+ cpSync5(sessionDir, join34(stagedProjectDir, sid), { force: true, recursive: true });
3791
3844
  }
3792
3845
  return true;
3793
3846
  }
@@ -3795,13 +3848,13 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3795
3848
  // src/commands.push.recovery.drop.ts
3796
3849
  init_config();
3797
3850
  import { rmSync as rmSync9 } from "node:fs";
3798
- import { join as join34 } from "node:path";
3851
+ import { join as join35 } from "node:path";
3799
3852
  function dropSessionFromStaged(sid, map) {
3800
3853
  const logicals = Object.keys(map.projects);
3801
3854
  if (logicals.length === 0) return false;
3802
3855
  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);
3856
+ const jsonl = join35(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
3857
+ const dir = join35(REPO_HOME, "shared", "projects", logical, sid);
3805
3858
  rmSync9(jsonl, { force: true });
3806
3859
  rmSync9(dir, { recursive: true, force: true });
3807
3860
  }
@@ -3919,7 +3972,7 @@ function applyThenRescan(scanVerdict, repoHome) {
3919
3972
  return next;
3920
3973
  }
3921
3974
  function allowThenRescan(append, scanVerdict, repoHome) {
3922
- const ignPath = join35(repoHome, ".gitleaksignore");
3975
+ const ignPath = join36(repoHome, ".gitleaksignore");
3923
3976
  let before;
3924
3977
  try {
3925
3978
  before = readFileSync10(ignPath, "utf8");
@@ -4017,7 +4070,7 @@ function writeAnimatedDone(out, label, ms, useTTY) {
4017
4070
  `);
4018
4071
  }
4019
4072
  function resolveWorkerPath(deps = {}) {
4020
- const check = deps.existsSyncFn ?? existsSync29;
4073
+ const check = deps.existsSyncFn ?? existsSync30;
4021
4074
  const base = deps.baseUrl ?? import.meta.url;
4022
4075
  const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
4023
4076
  if (check(mjs)) return mjs;
@@ -4081,6 +4134,112 @@ function withSpinner(label, fn, deps) {
4081
4134
  }
4082
4135
  }
4083
4136
 
4137
+ // src/commands.pull.recovery.ts
4138
+ init_config();
4139
+ import { execFileSync as execFileSync14 } from "node:child_process";
4140
+ init_utils();
4141
+ init_utils_fs();
4142
+ function gitCapture(args, cwd) {
4143
+ return execFileSync14("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
4144
+ }
4145
+ function isSyncedConfig(path) {
4146
+ return PUSH_ALLOWED_STATIC.some(
4147
+ (entry) => entry.endsWith("/") ? path.startsWith(entry) : path === entry
4148
+ );
4149
+ }
4150
+ function classifyTouched(touched) {
4151
+ const synced = [];
4152
+ const toolSource = [];
4153
+ for (const p of touched) {
4154
+ if (isSyncedConfig(p)) {
4155
+ synced.push(p);
4156
+ } else {
4157
+ toolSource.push(p);
4158
+ }
4159
+ }
4160
+ return { synced, toolSource };
4161
+ }
4162
+ function parsePorcelainZ(raw) {
4163
+ const tracked = [];
4164
+ const untracked = [];
4165
+ if (!raw) return { tracked, untracked };
4166
+ const records = raw.split("\0");
4167
+ for (let i = 0; i < records.length; i++) {
4168
+ const record = records[i];
4169
+ if (record.length < 3) continue;
4170
+ const xy = record.slice(0, 2);
4171
+ const filePath = record.slice(3);
4172
+ if (xy === "??") {
4173
+ untracked.push(filePath);
4174
+ continue;
4175
+ }
4176
+ tracked.push(filePath);
4177
+ if (xy.startsWith("R") || xy.startsWith("C")) {
4178
+ const src = records[i + 1];
4179
+ if (src) {
4180
+ tracked.push(src);
4181
+ i++;
4182
+ }
4183
+ }
4184
+ }
4185
+ return { tracked, untracked };
4186
+ }
4187
+ function parseDirtyPaths(repo) {
4188
+ return parsePorcelainZ(gitStatusPorcelainZ(repo));
4189
+ }
4190
+ function buildRecoverySummary(branchName, strandedLog, untracked) {
4191
+ const strandedLines = strandedLog.split("\n").filter(Boolean).map((l) => ` ${l}`).join("\n");
4192
+ const parts = [`parked stranded commits on ${branchName}`];
4193
+ if (strandedLines) parts.push(`stranded:
4194
+ ${strandedLines}`);
4195
+ if (untracked.length > 0) parts.push(`untracked files preserved: ${untracked.join(", ")}`);
4196
+ parts.push("continuing with normal pull");
4197
+ return parts.join("; ");
4198
+ }
4199
+ function freshStrandedBranch(repo) {
4200
+ const base = `nomad/stranded-${nowTimestamp()}`;
4201
+ const exists = (name) => {
4202
+ try {
4203
+ gitCapture(["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], repo);
4204
+ return true;
4205
+ } catch {
4206
+ return false;
4207
+ }
4208
+ };
4209
+ if (!exists(base)) return base;
4210
+ let n = 1;
4211
+ while (exists(`${base}-${n}`)) n++;
4212
+ return `${base}-${n}`;
4213
+ }
4214
+ function recoverForceRemote(mode, repo) {
4215
+ if (mode === "merge") {
4216
+ gitOrFatal(["merge", "--abort"], "git merge --abort", repo);
4217
+ } else {
4218
+ gitOrFatal(["rebase", "--abort"], "git rebase --abort", repo);
4219
+ }
4220
+ gitOrFatal(["fetch", "origin", "main"], "git fetch origin main", repo);
4221
+ try {
4222
+ gitCapture(["rev-parse", "--verify", "origin/main"], repo);
4223
+ } catch {
4224
+ die("origin/main not found after fetch; check your remote configuration");
4225
+ }
4226
+ const committedRaw = gitCapture(["diff", "--name-only", "-z", "origin/main", "HEAD"], repo);
4227
+ const committedTouched = committedRaw.split("\0").filter(Boolean);
4228
+ const { tracked: dirtyTracked, untracked } = parseDirtyPaths(repo);
4229
+ const allTouched = [...committedTouched, ...dirtyTracked];
4230
+ const { synced } = classifyTouched(allTouched);
4231
+ if (synced.length > 0) {
4232
+ die(
4233
+ "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."
4234
+ );
4235
+ }
4236
+ const branchName = freshStrandedBranch(repo);
4237
+ gitOrFatal(["branch", branchName, "HEAD"], "park stranded commits", repo);
4238
+ gitOrFatal(["reset", "--hard", "origin/main"], "reset to origin/main", repo);
4239
+ const strandedLog = gitCapture(["log", "--oneline", `origin/main..${branchName}`], repo);
4240
+ log(buildRecoverySummary(branchName, strandedLog, untracked));
4241
+ }
4242
+
4084
4243
  // src/commands.pull.ts
4085
4244
  init_utils();
4086
4245
  init_utils_fs();
@@ -4102,18 +4261,32 @@ function applyWetPull(ts, map) {
4102
4261
  summary
4103
4262
  ]);
4104
4263
  }
4264
+ function handleWedge(repo, forceRemote) {
4265
+ const wedge = detectWedge(repo);
4266
+ if (wedge === null) return;
4267
+ if (forceRemote) {
4268
+ recoverForceRemote(wedge, repo);
4269
+ return;
4270
+ }
4271
+ const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
4272
+ die(
4273
+ `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")`
4274
+ );
4275
+ }
4105
4276
  function cmdPull(opts = {}) {
4106
4277
  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"))) {
4278
+ const forceRemote = opts.forceRemote === true;
4279
+ if (!existsSync31(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4280
+ if (!existsSync31(join37(REPO_HOME, "shared", "settings.base.json"))) {
4109
4281
  die("repo not initialized; run 'nomad init' to scaffold");
4110
4282
  }
4111
4283
  const handle = acquireLock("pull");
4112
4284
  if (handle === null) process.exit(0);
4113
4285
  try {
4114
4286
  const ts = freshBackupTs(BACKUP_BASE);
4287
+ handleWedge(REPO_HOME, forceRemote);
4115
4288
  if (!dryRun) {
4116
- const backupRoot = join36(BACKUP_BASE, ts);
4289
+ const backupRoot = join37(BACKUP_BASE, ts);
4117
4290
  try {
4118
4291
  mkdirSync8(backupRoot, { recursive: true });
4119
4292
  } catch (err) {
@@ -4124,8 +4297,8 @@ function cmdPull(opts = {}) {
4124
4297
  dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
4125
4298
  );
4126
4299
  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: {} };
4300
+ const mapPath = join37(REPO_HOME, "path-map.json");
4301
+ const map = existsSync31(mapPath) ? readPathMap(mapPath) : { projects: {} };
4129
4302
  divergenceCheckExtras(ts);
4130
4303
  if (dryRun) {
4131
4304
  computePreview(ts, map, "pull");
@@ -4147,8 +4320,8 @@ function cmdPull(opts = {}) {
4147
4320
 
4148
4321
  // src/commands.push.ts
4149
4322
  init_config();
4150
- import { existsSync as existsSync32 } from "node:fs";
4151
- import { join as join38, relative as relative5 } from "node:path";
4323
+ import { existsSync as existsSync33 } from "node:fs";
4324
+ import { join as join39, relative as relative5 } from "node:path";
4152
4325
 
4153
4326
  // src/commands.push.allowlist.ts
4154
4327
  init_config();
@@ -4172,7 +4345,7 @@ function isNeverSync(path) {
4172
4345
  }
4173
4346
  return false;
4174
4347
  }
4175
- function parsePorcelainZ(statusPorcelain) {
4348
+ function parsePorcelainZ2(statusPorcelain) {
4176
4349
  const records = statusPorcelain.split("\0");
4177
4350
  const paths = [];
4178
4351
  for (let i = 0; i < records.length; i++) {
@@ -4202,7 +4375,7 @@ function enforceAllowList(statusPorcelain, map) {
4202
4375
  ];
4203
4376
  const neverSyncHits = [];
4204
4377
  const violations = [];
4205
- for (const path of parsePorcelainZ(statusPorcelain)) {
4378
+ for (const path of parsePorcelainZ2(statusPorcelain)) {
4206
4379
  if (isNeverSync(path)) {
4207
4380
  neverSyncHits.push(path);
4208
4381
  } else if (!isAllowed(path, allowed)) {
@@ -4228,9 +4401,9 @@ init_color();
4228
4401
  init_config();
4229
4402
  init_config_sharedDirs_guard();
4230
4403
  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";
4404
+ import { copyFileSync, existsSync as existsSync32, mkdirSync as mkdirSync9, readdirSync as readdirSync10, rmSync as rmSync11 } from "node:fs";
4232
4405
  import { homedir as homedir5 } from "node:os";
4233
- import { join as join37 } from "node:path";
4406
+ import { join as join38 } from "node:path";
4234
4407
  init_push_leak_verdict();
4235
4408
  init_push_gitleaks();
4236
4409
  init_utils_fs();
@@ -4245,13 +4418,13 @@ function stageSessions(tmpRoot, map) {
4245
4418
  if (!p || p === "TBD") continue;
4246
4419
  reverse.set(encodePath(p), logical);
4247
4420
  }
4248
- const localProjects = join37(CLAUDE_HOME, "projects");
4249
- if (!existsSync31(localProjects)) return 0;
4421
+ const localProjects = join38(CLAUDE_HOME, "projects");
4422
+ if (!existsSync32(localProjects)) return 0;
4250
4423
  let staged = 0;
4251
4424
  for (const dir of readdirSync10(localProjects)) {
4252
4425
  const logical = reverse.get(dir);
4253
4426
  if (!logical) continue;
4254
- copyDirJsonlOnly(join37(localProjects, dir), join37(tmpRoot, "shared", "projects", logical));
4427
+ copyDirJsonlOnly(join38(localProjects, dir), join38(tmpRoot, "shared", "projects", logical));
4255
4428
  staged++;
4256
4429
  }
4257
4430
  return staged;
@@ -4267,9 +4440,9 @@ function stageExtras(tmpRoot, map) {
4267
4440
  if (!localRoot || localRoot === "TBD") continue;
4268
4441
  for (const dirname6 of dirnames) {
4269
4442
  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);
4443
+ const src = join38(localRoot, dirname6);
4444
+ if (!existsSync32(src)) continue;
4445
+ const dst = join38(tmpRoot, "shared", "extras", logical, dirname6);
4273
4446
  copyExtras(src, dst);
4274
4447
  staged++;
4275
4448
  }
@@ -4277,19 +4450,19 @@ function stageExtras(tmpRoot, map) {
4277
4450
  return staged;
4278
4451
  }
4279
4452
  function previewPushLeaks(map) {
4280
- const cacheDir = join37(homedir5(), ".cache", "claude-nomad");
4453
+ const cacheDir = join38(homedir5(), ".cache", "claude-nomad");
4281
4454
  mkdirSync9(cacheDir, { recursive: true });
4282
4455
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
4283
- const tmpRoot = join37(cacheDir, `push-preview-tree-${stamp}`);
4456
+ const tmpRoot = join38(cacheDir, `push-preview-tree-${stamp}`);
4284
4457
  try {
4285
4458
  const sessionCount = stageSessions(tmpRoot, map);
4286
4459
  const extrasCount = stageExtras(tmpRoot, map);
4287
4460
  if (sessionCount + extrasCount === 0) {
4288
4461
  return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
4289
4462
  }
4290
- const ignoreFile = join37(REPO_HOME, ".gitleaksignore");
4291
- if (existsSync31(ignoreFile)) {
4292
- copyFileSync(ignoreFile, join37(tmpRoot, ".gitleaksignore"));
4463
+ const ignoreFile = join38(REPO_HOME, ".gitleaksignore");
4464
+ if (existsSync32(ignoreFile)) {
4465
+ copyFileSync(ignoreFile, join38(tmpRoot, ".gitleaksignore"));
4293
4466
  }
4294
4467
  let findings;
4295
4468
  try {
@@ -4311,7 +4484,7 @@ init_utils();
4311
4484
  init_utils_fs();
4312
4485
  init_utils_json();
4313
4486
  function guardGitlinks() {
4314
- const gitlinks = findGitlinks(join38(REPO_HOME, "shared"));
4487
+ const gitlinks = findGitlinks(join39(REPO_HOME, "shared"));
4315
4488
  if (gitlinks.length === 0) return;
4316
4489
  for (const p of gitlinks) {
4317
4490
  const rel = relative5(REPO_HOME, p);
@@ -4363,7 +4536,7 @@ async function cmdPush(opts = {}) {
4363
4536
  const allowAll = opts.allowAll === true;
4364
4537
  const allowRule = opts.allowRule;
4365
4538
  guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
4366
- if (!existsSync32(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4539
+ if (!existsSync33(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4367
4540
  const handle = acquireLock("push");
4368
4541
  if (handle === null) process.exit(0);
4369
4542
  try {
@@ -4381,8 +4554,8 @@ async function cmdPush(opts = {}) {
4381
4554
  renderNoScanTree(st);
4382
4555
  return;
4383
4556
  }
4384
- const mapPath = join38(REPO_HOME, "path-map.json");
4385
- if (!existsSync32(mapPath)) {
4557
+ const mapPath = join39(REPO_HOME, "path-map.json");
4558
+ if (!existsSync33(mapPath)) {
4386
4559
  if (dryRun) return runDryRunPreview(st, null);
4387
4560
  die("path-map.json missing, cannot enforce push allow-list");
4388
4561
  }
@@ -4403,9 +4576,9 @@ async function cmdPush(opts = {}) {
4403
4576
  }
4404
4577
 
4405
4578
  // src/commands.update.ts
4406
- import { execFileSync as execFileSync14 } from "node:child_process";
4579
+ import { execFileSync as execFileSync15 } from "node:child_process";
4407
4580
  init_utils();
4408
- function cmdUpdate(run = execFileSync14) {
4581
+ function cmdUpdate(run = execFileSync15) {
4409
4582
  try {
4410
4583
  run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
4411
4584
  } catch (err) {
@@ -4422,17 +4595,17 @@ init_config();
4422
4595
 
4423
4596
  // src/diff.ts
4424
4597
  init_config();
4425
- import { existsSync as existsSync33 } from "node:fs";
4426
- import { join as join39 } from "node:path";
4598
+ import { existsSync as existsSync34 } from "node:fs";
4599
+ import { join as join40 } from "node:path";
4427
4600
  init_utils();
4428
4601
  init_utils_fs();
4429
4602
  init_utils_json();
4430
4603
  function cmdDiff() {
4431
4604
  try {
4432
- if (!existsSync33(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4605
+ if (!existsSync34(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4433
4606
  const ts = freshBackupTs(BACKUP_BASE);
4434
- const mapPath = join39(REPO_HOME, "path-map.json");
4435
- const map = existsSync33(mapPath) ? readPathMap(mapPath) : { projects: {} };
4607
+ const mapPath = join40(REPO_HOME, "path-map.json");
4608
+ const map = existsSync34(mapPath) ? readPathMap(mapPath) : { projects: {} };
4436
4609
  computePreview(ts, map, "diff");
4437
4610
  } catch (err) {
4438
4611
  if (err instanceof NomadFatal) {
@@ -4446,19 +4619,19 @@ function cmdDiff() {
4446
4619
 
4447
4620
  // src/init.ts
4448
4621
  init_config();
4449
- import { existsSync as existsSync35, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
4450
- import { join as join41 } from "node:path";
4622
+ import { existsSync as existsSync36, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
4623
+ import { join as join42 } from "node:path";
4451
4624
 
4452
4625
  // src/init.gh-onboard.ts
4453
4626
  init_config();
4454
- import { execFileSync as execFileSync15 } from "node:child_process";
4627
+ import { execFileSync as execFileSync16 } from "node:child_process";
4455
4628
  init_utils();
4456
4629
  var DEFAULT_REPO_NAME = "claude-nomad-config";
4457
4630
  function isValidRepoName(name) {
4458
4631
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
4459
4632
  }
4460
4633
  var GH_NETWORK_TIMEOUT_MS = 3e4;
4461
- function ensureOriginRepo(repoName, run = execFileSync15) {
4634
+ function ensureOriginRepo(repoName, run = execFileSync16) {
4462
4635
  if (!isValidRepoName(repoName)) {
4463
4636
  die(
4464
4637
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -4528,31 +4701,31 @@ init_config();
4528
4701
  init_utils();
4529
4702
  init_utils_fs();
4530
4703
  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";
4704
+ import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync35, rmSync as rmSync12, statSync as statSync9 } from "node:fs";
4705
+ import { join as join41 } from "node:path";
4533
4706
  function snapshotIntoShared(map) {
4534
4707
  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);
4708
+ const src = join41(CLAUDE_HOME, name);
4709
+ if (!existsSync35(src)) continue;
4710
+ const dst = join41(REPO_HOME, "shared", name);
4538
4711
  if (statSync9(src).isDirectory()) {
4539
- const gk = join40(dst, ".gitkeep");
4540
- if (existsSync34(gk)) rmSync12(gk);
4712
+ const gk = join41(dst, ".gitkeep");
4713
+ if (existsSync35(gk)) rmSync12(gk);
4541
4714
  cpSync6(src, dst, { recursive: true, force: false, errorOnExist: true });
4542
4715
  } else {
4543
4716
  copyFileSync2(src, dst);
4544
4717
  }
4545
4718
  log(`snapshotted shared/${name} from ${src}`);
4546
4719
  }
4547
- const userSettings = join40(CLAUDE_HOME, "settings.json");
4548
- if (existsSync34(userSettings)) {
4720
+ const userSettings = join41(CLAUDE_HOME, "settings.json");
4721
+ if (existsSync35(userSettings)) {
4549
4722
  let parsed;
4550
4723
  try {
4551
4724
  parsed = readJson(userSettings);
4552
4725
  } catch (err) {
4553
4726
  return die(`malformed ${userSettings}: ${err.message}`);
4554
4727
  }
4555
- const hostFile = join40(REPO_HOME, "hosts", `${HOST}.json`);
4728
+ const hostFile = join41(REPO_HOME, "hosts", `${HOST}.json`);
4556
4729
  writeJsonAtomic(hostFile, parsed);
4557
4730
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
4558
4731
  }
@@ -4565,14 +4738,14 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
4565
4738
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
4566
4739
  function preflightConflict(repoHome) {
4567
4740
  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")
4741
+ join42(repoHome, "shared", "settings.base.json"),
4742
+ join42(repoHome, "shared", "CLAUDE.md"),
4743
+ join42(repoHome, "path-map.json"),
4744
+ join42(repoHome, "hosts"),
4745
+ join42(repoHome, "shared")
4573
4746
  ];
4574
4747
  for (const c of candidates) {
4575
- if (existsSync35(c)) return c;
4748
+ if (existsSync36(c)) return c;
4576
4749
  }
4577
4750
  return null;
4578
4751
  }
@@ -4585,25 +4758,25 @@ function cmdInit(opts = {}) {
4585
4758
  die(`already initialized; refusing to clobber ${conflict}`);
4586
4759
  }
4587
4760
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
4588
- mkdirSync10(join41(REPO_HOME, "shared"), { recursive: true });
4589
- mkdirSync10(join41(REPO_HOME, "hosts"), { recursive: true });
4761
+ mkdirSync10(join42(REPO_HOME, "shared"), { recursive: true });
4762
+ mkdirSync10(join42(REPO_HOME, "hosts"), { recursive: true });
4590
4763
  for (const name of SHARED_KEEP_DIRS) {
4591
- mkdirSync10(join41(REPO_HOME, "shared", name), { recursive: true });
4764
+ mkdirSync10(join42(REPO_HOME, "shared", name), { recursive: true });
4592
4765
  }
4593
- const userClaudeMd = join41(CLAUDE_HOME, "CLAUDE.md");
4594
- if (!snapshot || !existsSync35(userClaudeMd)) {
4595
- writeFileSync6(join41(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
4766
+ const userClaudeMd = join42(CLAUDE_HOME, "CLAUDE.md");
4767
+ if (!snapshot || !existsSync36(userClaudeMd)) {
4768
+ writeFileSync6(join42(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
4596
4769
  log("created shared/CLAUDE.md");
4597
4770
  }
4598
4771
  for (const name of SHARED_KEEP_DIRS) {
4599
- writeFileSync6(join41(REPO_HOME, "shared", name, ".gitkeep"), "");
4772
+ writeFileSync6(join42(REPO_HOME, "shared", name, ".gitkeep"), "");
4600
4773
  log(`created shared/${name}/.gitkeep`);
4601
4774
  }
4602
- writeFileSync6(join41(REPO_HOME, "hosts", ".gitkeep"), "");
4775
+ writeFileSync6(join42(REPO_HOME, "hosts", ".gitkeep"), "");
4603
4776
  log("created hosts/.gitkeep");
4604
- writeJsonAtomic(join41(REPO_HOME, "shared", "settings.base.json"), {});
4777
+ writeJsonAtomic(join42(REPO_HOME, "shared", "settings.base.json"), {});
4605
4778
  log("created shared/settings.base.json");
4606
- writeJsonAtomic(join41(REPO_HOME, "path-map.json"), { projects: {} });
4779
+ writeJsonAtomic(join42(REPO_HOME, "path-map.json"), { projects: {} });
4607
4780
  log("created path-map.json");
4608
4781
  if (snapshot) {
4609
4782
  snapshotIntoShared({ projects: {} });
@@ -4807,6 +4980,28 @@ function parseAllowArgs(argv) {
4807
4980
  return positionals;
4808
4981
  }
4809
4982
 
4983
+ // src/nomad.dispatch.pull.ts
4984
+ function parsePullArgs(argv) {
4985
+ let dryRun = false;
4986
+ let forceRemote = false;
4987
+ let i = 3;
4988
+ while (i < argv.length) {
4989
+ const token = argv[i];
4990
+ if (token === "--dry-run") {
4991
+ if (dryRun) return null;
4992
+ dryRun = true;
4993
+ } else if (token === "--force-remote") {
4994
+ if (forceRemote) return null;
4995
+ forceRemote = true;
4996
+ } else {
4997
+ return null;
4998
+ }
4999
+ i++;
5000
+ }
5001
+ if (dryRun && forceRemote) return null;
5002
+ return { dryRun, forceRemote };
5003
+ }
5004
+
4810
5005
  // src/nomad.dispatch.push.ts
4811
5006
  var REJECT2 = { ok: false, advance: 0 };
4812
5007
  function applyBool2(seen, set) {
@@ -4865,7 +5060,7 @@ function parsePushArgs(argv) {
4865
5060
  // package.json
4866
5061
  var package_default = {
4867
5062
  name: "claude-nomad",
4868
- version: "0.41.0",
5063
+ version: "0.42.0",
4869
5064
  type: "module",
4870
5065
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
4871
5066
  keywords: [
@@ -4963,6 +5158,11 @@ var DEFAULT_HELP = [
4963
5158
  "Commands:",
4964
5159
  row(" pull", "Sync ~/.claude/ from the shared repo (settings, symlinks, sessions)."),
4965
5160
  row(" --dry-run", "Run lock + git pull, then preview every mutation without writing."),
5161
+ row(" --force-remote", "Recover from a wedged repo (stuck mid-rebase or mid-merge):"),
5162
+ cont("abort the in-progress rebase/merge, park stranded commits on"),
5163
+ cont("nomad/stranded-<ts>, reset to origin/main, and re-pull. Refuses"),
5164
+ cont("if stranded or dirty tracked changes touch synced config (shared/,"),
5165
+ cont("hosts/, path-map.json). Cannot combine with --dry-run."),
4966
5166
  "",
4967
5167
  row(" push", "Rebase, run safety checks (gitleaks, gitlinks, allow-list), commit, push."),
4968
5168
  row(" --dry-run", "Run pre-checks (rebase, gitleaks probe, gitlink scan) and preview"),
@@ -5055,15 +5255,15 @@ var DEFAULT_HELP = [
5055
5255
  init_config();
5056
5256
  init_utils();
5057
5257
  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";
5258
+ import { existsSync as existsSync37, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "node:fs";
5259
+ import { join as join43 } from "node:path";
5060
5260
  function resumeCmd(sessionId) {
5061
5261
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
5062
5262
  fail(`invalid session id: ${sessionId}`);
5063
5263
  process.exit(1);
5064
5264
  }
5065
- const projectsRoot = join42(CLAUDE_HOME, "projects");
5066
- if (!existsSync36(projectsRoot)) {
5265
+ const projectsRoot = join43(CLAUDE_HOME, "projects");
5266
+ if (!existsSync37(projectsRoot)) {
5067
5267
  fail(`${projectsRoot} does not exist`);
5068
5268
  process.exit(1);
5069
5269
  }
@@ -5077,8 +5277,8 @@ function resumeCmd(sessionId) {
5077
5277
  fail(`no cwd field found in ${jsonlPath}`);
5078
5278
  process.exit(1);
5079
5279
  }
5080
- const mapPath = join42(REPO_HOME, "path-map.json");
5081
- if (!existsSync36(mapPath)) {
5280
+ const mapPath = join43(REPO_HOME, "path-map.json");
5281
+ if (!existsSync37(mapPath)) {
5082
5282
  fail("path-map.json missing");
5083
5283
  process.exit(1);
5084
5284
  }
@@ -5101,8 +5301,8 @@ function resumeCmd(sessionId) {
5101
5301
  }
5102
5302
  function findTranscriptPath(projectsRoot, sessionId) {
5103
5303
  for (const dir of readdirSync11(projectsRoot)) {
5104
- const candidate = join42(projectsRoot, dir, `${sessionId}.jsonl`);
5105
- if (existsSync36(candidate)) return candidate;
5304
+ const candidate = join43(projectsRoot, dir, `${sessionId}.jsonl`);
5305
+ if (existsSync37(candidate)) return candidate;
5106
5306
  }
5107
5307
  return null;
5108
5308
  }
@@ -5174,15 +5374,12 @@ try {
5174
5374
  console.log(package_default.version);
5175
5375
  break;
5176
5376
  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]");
5377
+ const pullArgs = parsePullArgs(process.argv);
5378
+ if (pullArgs === null) {
5379
+ console.error("usage: nomad pull [--dry-run] [--force-remote]");
5184
5380
  process.exit(1);
5185
5381
  }
5382
+ cmdPull({ dryRun: pullArgs.dryRun, forceRemote: pullArgs.forceRemote });
5186
5383
  break;
5187
5384
  }
5188
5385
  case "push": {