claude-nomad 0.40.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
@@ -170,13 +170,12 @@ function gitOrFatal(args, context, cwd) {
170
170
  throw new NomadFatal(`${context} failed`);
171
171
  }
172
172
  }
173
- var log, ok, warn, fail, NomadFatal, die, gitStatusPorcelainZ;
173
+ var log, warn, fail, NomadFatal, die, gitStatusPorcelainZ;
174
174
  var init_utils = __esm({
175
175
  "src/utils.ts"() {
176
176
  "use strict";
177
177
  init_color();
178
178
  log = (msg) => console.log(`${dim(infoGlyph)} ${msg}`);
179
- ok = (msg) => console.log(`${green(okGlyph)} ${msg}`);
180
179
  warn = (msg) => {
181
180
  console.error(`${yellow(warnGlyph)} ${msg}`);
182
181
  };
@@ -450,6 +449,17 @@ function deepMerge(target, source) {
450
449
  }
451
450
  return out;
452
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
+ }
453
463
  var encodePath;
454
464
  var init_utils_json = __esm({
455
465
  "src/utils.json.ts"() {
@@ -551,31 +561,31 @@ var init_utils_fs = __esm({
551
561
  });
552
562
 
553
563
  // src/push-gitleaks.config.ts
554
- 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";
555
565
  import { tmpdir } from "node:os";
556
- import { join as join10 } from "node:path";
566
+ import { join as join11 } from "node:path";
557
567
  import { fileURLToPath } from "node:url";
558
568
  function resolveTomlPath() {
559
- const repoToml = join10(REPO_HOME, ".gitleaks.toml");
560
- if (existsSync9(repoToml)) return repoToml;
569
+ const repoToml = join11(REPO_HOME, ".gitleaks.toml");
570
+ if (existsSync10(repoToml)) return repoToml;
561
571
  const bundled = fileURLToPath(new URL("../.gitleaks.toml", import.meta.url));
562
- return existsSync9(bundled) ? bundled : null;
572
+ return existsSync10(bundled) ? bundled : null;
563
573
  }
564
574
  function buildOverlayTempConfig(overlayBody, bundled) {
565
575
  const tempBody = `[extend]
566
576
  path = ${JSON.stringify(bundled)}
567
577
 
568
578
  ${overlayBody}`;
569
- const tempPath = mkdtempSync(join10(tmpdir(), "nomad-gitleaks-cfg-"));
570
- const configPath = join10(tempPath, "config.toml");
579
+ const tempPath = mkdtempSync(join11(tmpdir(), "nomad-gitleaks-cfg-"));
580
+ const configPath = join11(tempPath, "config.toml");
571
581
  writeFileSync2(configPath, tempBody, { mode: 384, flag: "wx" });
572
582
  return { configPath, tempPath };
573
583
  }
574
584
  function resolveTomlConfig() {
575
- const overlayPath = join10(REPO_HOME, ".gitleaks.overlay.toml");
576
- const repoToml = join10(REPO_HOME, ".gitleaks.toml");
585
+ const overlayPath = join11(REPO_HOME, ".gitleaks.overlay.toml");
586
+ const repoToml = join11(REPO_HOME, ".gitleaks.toml");
577
587
  const bundled = resolveTomlPath();
578
- if (!existsSync9(overlayPath)) {
588
+ if (!existsSync10(overlayPath)) {
579
589
  return { path: bundled, tempPath: null };
580
590
  }
581
591
  if (bundled === repoToml) {
@@ -618,7 +628,7 @@ var init_push_gitleaks_config = __esm({
618
628
  import { execFileSync as execFileSync2 } from "node:child_process";
619
629
  import { readdirSync as readdirSync3, rmSync as rmSync3 } from "node:fs";
620
630
  import { homedir as homedir2, platform } from "node:os";
621
- import { join as join11 } from "node:path";
631
+ import { join as join12 } from "node:path";
622
632
  function gitleaksInstallHint() {
623
633
  const head = "gitleaks not on PATH (required for nomad push). Install:";
624
634
  const plat = platform();
@@ -661,7 +671,7 @@ function findGitlinks(dir) {
661
671
  return;
662
672
  }
663
673
  for (const e of entries) {
664
- const p = join11(current, e.name);
674
+ const p = join12(current, e.name);
665
675
  if (e.name === ".git") {
666
676
  hits.push(p);
667
677
  continue;
@@ -713,7 +723,7 @@ var init_push_checks = __esm({
713
723
  import { execFileSync as execFileSync5 } from "node:child_process";
714
724
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync4 } from "node:fs";
715
725
  import { homedir as homedir3 } from "node:os";
716
- import { join as join15 } from "node:path";
726
+ import { join as join16 } from "node:path";
717
727
  function readGitleaksReport(reportPath) {
718
728
  try {
719
729
  const raw = readFileSync4(reportPath, "utf8");
@@ -725,9 +735,9 @@ function readGitleaksReport(reportPath) {
725
735
  }
726
736
  }
727
737
  function scanStagedTree(repoDir, forwardStreams = false) {
728
- const cacheDir = join15(homedir3(), ".cache", "claude-nomad");
738
+ const cacheDir = join16(homedir3(), ".cache", "claude-nomad");
729
739
  mkdirSync2(cacheDir, { recursive: true });
730
- const reportPath = join15(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
740
+ const reportPath = join16(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
731
741
  const { path: toml, tempPath } = resolveTomlConfig();
732
742
  const args = [
733
743
  "protect",
@@ -759,9 +769,9 @@ function scanStagedTree(repoDir, forwardStreams = false) {
759
769
  }
760
770
  }
761
771
  function scanFile(filePath, forwardStreams = false) {
762
- const cacheDir = join15(homedir3(), ".cache", "claude-nomad");
772
+ const cacheDir = join16(homedir3(), ".cache", "claude-nomad");
763
773
  mkdirSync2(cacheDir, { recursive: true });
764
- const reportPath = join15(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
774
+ const reportPath = join16(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
765
775
  const { path: toml, tempPath } = resolveTomlConfig();
766
776
  const args = [
767
777
  "detect",
@@ -1210,8 +1220,8 @@ function cmdClean(opts, backupBase = BACKUP_BASE) {
1210
1220
  }
1211
1221
 
1212
1222
  // src/commands.doctor.ts
1213
- import { existsSync as existsSync18 } from "node:fs";
1214
- import { join as join22 } from "node:path";
1223
+ import { existsSync as existsSync19 } from "node:fs";
1224
+ import { join as join23 } from "node:path";
1215
1225
 
1216
1226
  // src/commands.doctor.checks.repo.ts
1217
1227
  init_color();
@@ -1225,8 +1235,8 @@ init_color();
1225
1235
  // src/output-tree.ts
1226
1236
  init_color();
1227
1237
  var FAIL_GLYPH_BARE = "\u2717";
1228
- function section(header) {
1229
- return { header, items: [] };
1238
+ function section(header, raw = false) {
1239
+ return { header, items: [], raw };
1230
1240
  }
1231
1241
  function addItem(s, text) {
1232
1242
  s.items.push(text);
@@ -1234,7 +1244,17 @@ function addItem(s, text) {
1234
1244
  function sectionFailed(s) {
1235
1245
  return s.items.some((line) => line.includes(failGlyph));
1236
1246
  }
1247
+ function renderRawItems(items) {
1248
+ for (const item of items) {
1249
+ console.log(item === "" ? "" : ` ${item}`);
1250
+ }
1251
+ }
1237
1252
  function renderSection(s) {
1253
+ if (s.raw) {
1254
+ console.log(s.header);
1255
+ renderRawItems(s.items);
1256
+ return;
1257
+ }
1238
1258
  const header = sectionFailed(s) ? `${red(FAIL_GLYPH_BARE)} ${s.header}` : s.header;
1239
1259
  console.log(header);
1240
1260
  const lastContent = s.items.reduce((acc, item, j) => item === "" ? acc : j, -1);
@@ -1542,8 +1562,20 @@ function reportNeverSync(section2) {
1542
1562
  init_color();
1543
1563
  init_config();
1544
1564
  import { execFileSync as execFileSync3 } from "node:child_process";
1545
- import { existsSync as existsSync10 } from "node:fs";
1546
- 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
1547
1579
  init_push_checks();
1548
1580
  init_utils();
1549
1581
  function reportGitleaksProbe(section2) {
@@ -1562,8 +1594,8 @@ function reportGitleaksProbe(section2) {
1562
1594
  }
1563
1595
  }
1564
1596
  function reportGitlinks(section2) {
1565
- const sharedDir = join12(REPO_HOME, "shared");
1566
- if (existsSync10(sharedDir)) {
1597
+ const sharedDir = join13(REPO_HOME, "shared");
1598
+ if (existsSync11(sharedDir)) {
1567
1599
  const gitlinks = findGitlinks(sharedDir);
1568
1600
  for (const p of gitlinks) {
1569
1601
  const rel = relative2(REPO_HOME, p);
@@ -1602,11 +1634,25 @@ function reportRebaseClean(section2) {
1602
1634
  } catch {
1603
1635
  }
1604
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
+ }
1605
1651
 
1606
1652
  // src/commands.doctor.checks.backups.ts
1607
1653
  init_color();
1608
- import { existsSync as existsSync11, lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
1609
- 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";
1610
1656
  init_config();
1611
1657
  var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
1612
1658
  function safeReaddir(dir) {
@@ -1622,7 +1668,7 @@ var BYTES_PER_MB = 1024 * 1024;
1622
1668
  function dirSizeBytes(dir) {
1623
1669
  let bytes = 0;
1624
1670
  for (const entry of safeReaddir(dir)) {
1625
- const full = join13(dir, entry);
1671
+ const full = join14(dir, entry);
1626
1672
  const st = lstatSync5(full, { throwIfNoEntry: false });
1627
1673
  if (!st) continue;
1628
1674
  if (st.isSymbolicLink()) continue;
@@ -1633,11 +1679,11 @@ function dirSizeBytes(dir) {
1633
1679
  }
1634
1680
  function totalSizeMb(backupBase, dirs) {
1635
1681
  let bytes = 0;
1636
- for (const name of dirs) bytes += dirSizeBytes(join13(backupBase, name));
1682
+ for (const name of dirs) bytes += dirSizeBytes(join14(backupBase, name));
1637
1683
  return bytes / BYTES_PER_MB;
1638
1684
  }
1639
1685
  function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
1640
- if (!existsSync11(backupBase)) return;
1686
+ if (!existsSync12(backupBase)) return;
1641
1687
  const dirs = safeReaddir(backupBase).filter((n) => TS_SHAPE2.test(n));
1642
1688
  const count = dirs.length;
1643
1689
  const sizeMb = totalSizeMb(backupBase, dirs);
@@ -1651,8 +1697,8 @@ function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
1651
1697
 
1652
1698
  // src/commands.doctor.check-schema.ts
1653
1699
  init_color();
1654
- import { existsSync as existsSync12 } from "node:fs";
1655
- import { join as join14 } from "node:path";
1700
+ import { existsSync as existsSync13 } from "node:fs";
1701
+ import { join as join15 } from "node:path";
1656
1702
  init_config();
1657
1703
 
1658
1704
  // src/http-fetch.ts
@@ -1689,8 +1735,8 @@ function fetchSchemaKeys() {
1689
1735
  }
1690
1736
  }
1691
1737
  function reportCheckSchema(section2) {
1692
- const settingsPath = join14(CLAUDE_HOME, "settings.json");
1693
- if (!existsSync12(settingsPath)) {
1738
+ const settingsPath = join15(CLAUDE_HOME, "settings.json");
1739
+ if (!existsSync13(settingsPath)) {
1694
1740
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
1695
1741
  return;
1696
1742
  }
@@ -1720,18 +1766,18 @@ function reportCheckSchema(section2) {
1720
1766
  init_color();
1721
1767
  import { randomBytes } from "node:crypto";
1722
1768
  import { execFileSync as execFileSync6 } from "node:child_process";
1723
- 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";
1724
1770
  import { homedir as homedir4 } from "node:os";
1725
- import { join as join18 } from "node:path";
1771
+ import { join as join19 } from "node:path";
1726
1772
 
1727
1773
  // src/commands.doctor.check-shared.scan.ts
1728
1774
  init_color();
1729
- import { join as join16 } from "node:path";
1775
+ import { join as join17 } from "node:path";
1730
1776
  init_config();
1731
1777
  init_push_gitleaks();
1732
1778
  function scrubPath(logical, sid, logicalToEncoded) {
1733
1779
  const encoded = logicalToEncoded.get(logical) ?? logical;
1734
- return join16(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
1780
+ return join17(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
1735
1781
  }
1736
1782
  function reportSessionFindings(section2, bySession) {
1737
1783
  for (const [sid, counts] of bySession) {
@@ -1823,8 +1869,8 @@ init_config();
1823
1869
  init_utils();
1824
1870
  init_utils_fs();
1825
1871
  init_utils_json();
1826
- import { cpSync as cpSync3, existsSync as existsSync13, mkdirSync as mkdirSync3, readdirSync as readdirSync5, rmSync as rmSync5, statSync as statSync4 } from "node:fs";
1827
- 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";
1828
1874
  function copyDir(src, dst) {
1829
1875
  rmSync5(dst, { recursive: true, force: true });
1830
1876
  cpSync3(src, dst, { recursive: true, force: true });
@@ -1845,19 +1891,24 @@ function copyDirJsonlOnly(src, dst) {
1845
1891
  }
1846
1892
  });
1847
1893
  }
1894
+ function emitPreview(onPreview, event, fallback) {
1895
+ if (onPreview) onPreview(event);
1896
+ else log(fallback);
1897
+ }
1848
1898
  function remapPull(ts, opts = {}) {
1849
1899
  const dryRun = opts.dryRun === true;
1850
1900
  let unmapped = 0;
1851
1901
  const pulled = [];
1852
1902
  const wouldPull = [];
1853
- const mapPath = join17(REPO_HOME, "path-map.json");
1854
- const repoProjects = join17(REPO_HOME, "shared", "projects");
1855
- if (!existsSync13(mapPath) || !existsSync13(repoProjects)) {
1856
- log("no path-map or repo projects dir; skipping session remap");
1903
+ const mapPath = join18(REPO_HOME, "path-map.json");
1904
+ const repoProjects = join18(REPO_HOME, "shared", "projects");
1905
+ if (!existsSync14(mapPath) || !existsSync14(repoProjects)) {
1906
+ const text = "no path-map or repo projects dir; skipping session remap";
1907
+ emitPreview(opts.onPreview, { kind: "note", text }, text);
1857
1908
  return { unmapped: 0, pulled, wouldPull };
1858
1909
  }
1859
1910
  const map = readJson(mapPath);
1860
- const localProjects = join17(CLAUDE_HOME, "projects");
1911
+ const localProjects = join18(CLAUDE_HOME, "projects");
1861
1912
  if (!dryRun) mkdirSync3(localProjects, { recursive: true });
1862
1913
  for (const [logical, hosts] of Object.entries(map.projects)) {
1863
1914
  assertSafeLogical(logical);
@@ -1866,12 +1917,16 @@ function remapPull(ts, opts = {}) {
1866
1917
  unmapped++;
1867
1918
  continue;
1868
1919
  }
1869
- const src = join17(repoProjects, logical);
1870
- if (!existsSync13(src)) continue;
1871
- const dst = join17(localProjects, encodePath(localPath));
1920
+ const src = join18(repoProjects, logical);
1921
+ if (!existsSync14(src)) continue;
1922
+ const dst = join18(localProjects, encodePath(localPath));
1872
1923
  if (dryRun) {
1873
1924
  wouldPull.push(logical);
1874
- log(`would overwrite: ${dst} (from ${src})`);
1925
+ emitPreview(
1926
+ opts.onPreview,
1927
+ { kind: "overwrite", dst, src },
1928
+ `would overwrite: ${dst} (from ${src})`
1929
+ );
1875
1930
  continue;
1876
1931
  }
1877
1932
  backupBeforeWrite(dst, ts);
@@ -1909,16 +1964,16 @@ function remapPush(ts, opts = {}) {
1909
1964
  let unmapped = 0;
1910
1965
  const pushed = [];
1911
1966
  const wouldPush = [];
1912
- const mapPath = join17(REPO_HOME, "path-map.json");
1913
- if (!existsSync13(mapPath)) {
1967
+ const mapPath = join18(REPO_HOME, "path-map.json");
1968
+ if (!existsSync14(mapPath)) {
1914
1969
  log("no path-map.json; skipping session export");
1915
1970
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
1916
1971
  }
1917
1972
  const map = readJson(mapPath);
1918
- const localProjects = join17(CLAUDE_HOME, "projects");
1919
- const repoProjects = join17(REPO_HOME, "shared", "projects");
1973
+ const localProjects = join18(CLAUDE_HOME, "projects");
1974
+ const repoProjects = join18(REPO_HOME, "shared", "projects");
1920
1975
  const reverse = buildReverseMap(map);
1921
- if (!existsSync13(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
1976
+ if (!existsSync14(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
1922
1977
  if (!dryRun) mkdirSync3(repoProjects, { recursive: true });
1923
1978
  for (const dir of readdirSync5(localProjects)) {
1924
1979
  const logical = reverse.get(dir);
@@ -1926,13 +1981,13 @@ function remapPush(ts, opts = {}) {
1926
1981
  unmapped++;
1927
1982
  continue;
1928
1983
  }
1929
- const repoDst = join17(repoProjects, logical);
1984
+ const repoDst = join18(repoProjects, logical);
1930
1985
  if (dryRun) {
1931
1986
  wouldPush.push(logical);
1932
1987
  continue;
1933
1988
  }
1934
1989
  backupRepoWrite(repoDst, ts, REPO_HOME);
1935
- copyDirJsonlOnly(join17(localProjects, dir), repoDst);
1990
+ copyDirJsonlOnly(join18(localProjects, dir), repoDst);
1936
1991
  pushed.push(logical);
1937
1992
  }
1938
1993
  return { unmapped, collisions: 0, pushed, wouldPush };
@@ -1944,8 +1999,8 @@ init_utils_json();
1944
1999
  function buildScanTree(tmpRoot) {
1945
2000
  const logicalToEncoded = /* @__PURE__ */ new Map();
1946
2001
  let staged = 0;
1947
- const mapPath = join18(REPO_HOME, "path-map.json");
1948
- 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 };
1949
2004
  let map;
1950
2005
  try {
1951
2006
  map = readJson(mapPath);
@@ -1962,12 +2017,12 @@ function buildScanTree(tmpRoot) {
1962
2017
  if (!p || p === "TBD") continue;
1963
2018
  reverse.set(encodePath(p), logical);
1964
2019
  }
1965
- const localProjects = join18(CLAUDE_HOME, "projects");
1966
- if (!existsSync14(localProjects)) return { logicalToEncoded, staged, malformed: false };
2020
+ const localProjects = join19(CLAUDE_HOME, "projects");
2021
+ if (!existsSync15(localProjects)) return { logicalToEncoded, staged, malformed: false };
1967
2022
  for (const dir of readdirSync6(localProjects)) {
1968
2023
  const logical = reverse.get(dir);
1969
2024
  if (!logical) continue;
1970
- copyDirJsonlOnly(join18(localProjects, dir), join18(tmpRoot, "shared", "projects", logical));
2025
+ copyDirJsonlOnly(join19(localProjects, dir), join19(tmpRoot, "shared", "projects", logical));
1971
2026
  logicalToEncoded.set(logical, dir);
1972
2027
  staged++;
1973
2028
  }
@@ -1998,11 +2053,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
1998
2053
  }
1999
2054
  function reportCheckShared(section2, gitleaksReady) {
2000
2055
  if (!ensureGitleaksReady(section2, gitleaksReady)) return;
2001
- const cacheDir = join18(homedir4(), ".cache", "claude-nomad");
2056
+ const cacheDir = join19(homedir4(), ".cache", "claude-nomad");
2002
2057
  mkdirSync4(cacheDir, { recursive: true });
2003
2058
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes(4).toString("hex")}`;
2004
- const reportPath = join18(cacheDir, `check-shared-${stamp}.json`);
2005
- 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}`);
2006
2061
  try {
2007
2062
  const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
2008
2063
  if (malformed) {
@@ -2023,8 +2078,8 @@ function reportCheckShared(section2, gitleaksReady) {
2023
2078
 
2024
2079
  // src/commands.doctor.checks.hooks.scope.ts
2025
2080
  init_color();
2026
- import { existsSync as existsSync15, readFileSync as readFileSync5, readdirSync as readdirSync7, realpathSync } from "node:fs";
2027
- 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";
2028
2083
  init_config();
2029
2084
  function typeFromPackageJson(pkgPath) {
2030
2085
  try {
@@ -2046,8 +2101,8 @@ function effectiveType(hookPath) {
2046
2101
  }
2047
2102
  let dir = dirname2(real);
2048
2103
  for (; ; ) {
2049
- const pkg = join19(dir, "package.json");
2050
- if (existsSync15(pkg)) return typeFromPackageJson(pkg);
2104
+ const pkg = join20(dir, "package.json");
2105
+ if (existsSync16(pkg)) return typeFromPackageJson(pkg);
2051
2106
  const parent = dirname2(dir);
2052
2107
  if (parent === dir) return "cjs";
2053
2108
  dir = parent;
@@ -2089,15 +2144,15 @@ function safeRead(path) {
2089
2144
  }
2090
2145
  }
2091
2146
  function reportHookScopeCheck(section2) {
2092
- const hooksDir = join19(CLAUDE_HOME, "hooks");
2093
- if (!existsSync15(hooksDir)) {
2147
+ const hooksDir = join20(CLAUDE_HOME, "hooks");
2148
+ if (!existsSync16(hooksDir)) {
2094
2149
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
2095
2150
  return;
2096
2151
  }
2097
2152
  let anyWarn = false;
2098
2153
  for (const name of safeReaddir2(hooksDir)) {
2099
2154
  if (extname(name) !== ".js") continue;
2100
- const abs = join19(hooksDir, name);
2155
+ const abs = join20(hooksDir, name);
2101
2156
  const eff = effectiveType(abs);
2102
2157
  if (eff === null) continue;
2103
2158
  const src = safeRead(abs);
@@ -2117,8 +2172,8 @@ function reportHookScopeCheck(section2) {
2117
2172
 
2118
2173
  // src/commands.doctor.checks.hooks.ts
2119
2174
  init_color();
2120
- import { existsSync as existsSync16 } from "node:fs";
2121
- import { join as join20 } from "node:path";
2175
+ import { existsSync as existsSync17 } from "node:fs";
2176
+ import { join as join21 } from "node:path";
2122
2177
  init_config();
2123
2178
  function expandHome(token) {
2124
2179
  return token.replace(/^\$\{HOME\}/, HOME).replace(/^\$HOME/, HOME).replace(/^~/, HOME);
@@ -2156,7 +2211,7 @@ function checkEventGroups(section2, event, groups) {
2156
2211
  for (const group of groups) {
2157
2212
  for (const cmd of commandsFromGroup(group)) {
2158
2213
  for (const resolved of claudePathsIn(cmd)) {
2159
- if (existsSync16(resolved)) continue;
2214
+ if (existsSync17(resolved)) continue;
2160
2215
  addItem(section2, `${red(failGlyph)} hooks/${event}: command target missing: ${resolved}`);
2161
2216
  process.exitCode = 1;
2162
2217
  anyFail = true;
@@ -2166,8 +2221,8 @@ function checkEventGroups(section2, event, groups) {
2166
2221
  return anyFail;
2167
2222
  }
2168
2223
  function reportHooksTargetCheck(section2) {
2169
- const settingsPath = join20(CLAUDE_HOME, "settings.json");
2170
- if (!existsSync16(settingsPath)) {
2224
+ const settingsPath = join21(CLAUDE_HOME, "settings.json");
2225
+ if (!existsSync17(settingsPath)) {
2171
2226
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
2172
2227
  return;
2173
2228
  }
@@ -2297,8 +2352,8 @@ function reportNodeEngineCheck(section2) {
2297
2352
  // src/commands.doctor.gitleaks-version.ts
2298
2353
  init_color();
2299
2354
  import { execFileSync as execFileSync7 } from "node:child_process";
2300
- import { existsSync as existsSync17 } from "node:fs";
2301
- import { join as join21 } from "node:path";
2355
+ import { existsSync as existsSync18 } from "node:fs";
2356
+ import { join as join22 } from "node:path";
2302
2357
  init_config();
2303
2358
  var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
2304
2359
  var GITLEAKS_TIMEOUT_MS = 5e3;
@@ -2307,7 +2362,7 @@ function majorMinorOf(value) {
2307
2362
  return m === null ? null : [m[1], m[2]];
2308
2363
  }
2309
2364
  function readGitleaksVersion(run, tomlExists) {
2310
- const tomlPath = join21(REPO_HOME, ".gitleaks.toml");
2365
+ const tomlPath = join22(REPO_HOME, ".gitleaks.toml");
2311
2366
  const args = ["version"];
2312
2367
  if (tomlExists(tomlPath)) args.push("--config", tomlPath);
2313
2368
  try {
@@ -2319,7 +2374,7 @@ function readGitleaksVersion(run, tomlExists) {
2319
2374
  return null;
2320
2375
  }
2321
2376
  }
2322
- function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync17) {
2377
+ function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync18) {
2323
2378
  const raw = readGitleaksVersion(run, tomlExists);
2324
2379
  if (raw === null) return;
2325
2380
  const local = majorMinorOf(raw);
@@ -2492,8 +2547,8 @@ function cmdDoctor(opts = {}) {
2492
2547
  reportHostAndPaths(host);
2493
2548
  reportRepoState(host);
2494
2549
  const links = section("Shared links");
2495
- const mapPath = join22(REPO_HOME, "path-map.json");
2496
- 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;
2497
2552
  const map = rawMap ?? { projects: {} };
2498
2553
  reportSharedLinks(links, map);
2499
2554
  const hooksScan = section("Hook targets");
@@ -2512,6 +2567,7 @@ function cmdDoctor(opts = {}) {
2512
2567
  reportGitlinks(repository);
2513
2568
  reportRemote(repository);
2514
2569
  reportRebaseClean(repository);
2570
+ reportRebaseState(repository);
2515
2571
  reportActionsDrift(repository);
2516
2572
  const nomadVersion = section("Nomad Version");
2517
2573
  reportVersionCheck(nomadVersion);
@@ -2542,8 +2598,8 @@ function cmdDoctor(opts = {}) {
2542
2598
  // src/commands.drop-session.ts
2543
2599
  init_config();
2544
2600
  import { execFileSync as execFileSync12 } from "node:child_process";
2545
- import { existsSync as existsSync20, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
2546
- 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";
2547
2603
 
2548
2604
  // src/commands.drop-session.git.ts
2549
2605
  init_config();
@@ -2586,8 +2642,8 @@ function isInIndex(rel) {
2586
2642
  init_config();
2587
2643
  init_utils();
2588
2644
  init_utils_json();
2589
- import { existsSync as existsSync19 } from "node:fs";
2590
- import { join as join23 } from "node:path";
2645
+ import { existsSync as existsSync20 } from "node:fs";
2646
+ import { join as join24 } from "node:path";
2591
2647
  var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
2592
2648
  function reportScrubHint(id, matches) {
2593
2649
  const live = resolveLiveTranscript(id, matches);
@@ -2603,16 +2659,16 @@ function reportScrubHint(id, matches) {
2603
2659
  }
2604
2660
  function resolveLiveTranscript(id, matches) {
2605
2661
  try {
2606
- const mapPath = join23(REPO_HOME, "path-map.json");
2607
- if (!existsSync19(mapPath)) return null;
2662
+ const mapPath = join24(REPO_HOME, "path-map.json");
2663
+ if (!existsSync20(mapPath)) return null;
2608
2664
  const projects = readJson(mapPath).projects;
2609
2665
  for (const rel of matches) {
2610
2666
  const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
2611
2667
  if (logical === void 0) continue;
2612
2668
  const abs = projects[logical]?.[HOST];
2613
2669
  if (abs === void 0) continue;
2614
- const live = join23(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2615
- if (existsSync19(live)) return live;
2670
+ const live = join24(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2671
+ if (existsSync20(live)) return live;
2616
2672
  }
2617
2673
  return null;
2618
2674
  } catch {
@@ -2627,8 +2683,8 @@ init_utils();
2627
2683
  init_config();
2628
2684
  init_utils();
2629
2685
  import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync8, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
2630
- import { dirname as dirname3, join as join24 } from "node:path";
2631
- 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");
2632
2688
  function acquireLock(verb) {
2633
2689
  mkdirSync5(dirname3(LOCK_PATH), { recursive: true });
2634
2690
  try {
@@ -2738,12 +2794,12 @@ function cmdDropSession(id) {
2738
2794
  fail(`invalid session id: ${id}`);
2739
2795
  process.exit(1);
2740
2796
  }
2741
- if (!existsSync20(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2797
+ if (!existsSync21(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2742
2798
  const handle = acquireLock("drop-session");
2743
2799
  if (handle === null) process.exit(0);
2744
2800
  try {
2745
- const repoProjects = join25(REPO_HOME, "shared", "projects");
2746
- if (!existsSync20(repoProjects)) {
2801
+ const repoProjects = join26(REPO_HOME, "shared", "projects");
2802
+ if (!existsSync21(repoProjects)) {
2747
2803
  throw new NomadFatal(`no staged session matches ${id}`);
2748
2804
  }
2749
2805
  const matches = collectMatches(repoProjects, id);
@@ -2765,12 +2821,12 @@ function cmdDropSession(id) {
2765
2821
  function collectMatches(repoProjects, id) {
2766
2822
  const matches = [];
2767
2823
  for (const logical of readdirSync8(repoProjects)) {
2768
- const candidate = join25(repoProjects, logical, `${id}.jsonl`);
2769
- if (existsSync20(candidate)) {
2824
+ const candidate = join26(repoProjects, logical, `${id}.jsonl`);
2825
+ if (existsSync21(candidate)) {
2770
2826
  matches.push(relative4(REPO_HOME, candidate));
2771
2827
  }
2772
- const dir = join25(repoProjects, logical, id);
2773
- if (existsSync20(dir) && statSync5(dir).isDirectory()) {
2828
+ const dir = join26(repoProjects, logical, id);
2829
+ if (existsSync21(dir) && statSync5(dir).isDirectory()) {
2774
2830
  const dirRel = relative4(REPO_HOME, dir);
2775
2831
  const staged = expandStagedDir(dirRel);
2776
2832
  if (staged.length > 0) matches.push(...staged);
@@ -2806,19 +2862,19 @@ function unstageOne(rel) {
2806
2862
 
2807
2863
  // src/commands.redact.ts
2808
2864
  init_config();
2809
- import { existsSync as existsSync22, statSync as statSync7 } from "node:fs";
2810
- 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";
2811
2867
 
2812
2868
  // src/commands.redact.subtree.ts
2813
- import { existsSync as existsSync21, lstatSync as lstatSync6, readFileSync as readFileSync9, readdirSync as readdirSync9, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
2814
- 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";
2815
2871
  init_utils_fs();
2816
2872
  function collectFiles(dir, out) {
2817
- if (!existsSync21(dir)) return;
2873
+ if (!existsSync22(dir)) return;
2818
2874
  const st = lstatSync6(dir);
2819
2875
  if (!st.isDirectory()) return;
2820
2876
  for (const entry of readdirSync9(dir)) {
2821
- const abs = join26(dir, entry);
2877
+ const abs = join27(dir, entry);
2822
2878
  const lst = lstatSync6(abs);
2823
2879
  if (lst.isSymbolicLink()) continue;
2824
2880
  if (lst.isDirectory()) {
@@ -2868,14 +2924,14 @@ init_utils_json();
2868
2924
  init_utils();
2869
2925
  function resolveLiveTranscript2(id) {
2870
2926
  try {
2871
- const mapPath = join27(REPO_HOME, "path-map.json");
2872
- if (!existsSync22(mapPath)) return null;
2927
+ const mapPath = join28(REPO_HOME, "path-map.json");
2928
+ if (!existsSync23(mapPath)) return null;
2873
2929
  const projects = readJson(mapPath).projects;
2874
2930
  for (const hostMap of Object.values(projects)) {
2875
2931
  const abs = hostMap[HOST];
2876
2932
  if (abs === void 0) continue;
2877
- const live = join27(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2878
- if (existsSync22(live)) return live;
2933
+ const live = join28(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2934
+ if (existsSync23(live)) return live;
2879
2935
  }
2880
2936
  return null;
2881
2937
  } catch {
@@ -2893,17 +2949,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
2893
2949
  fail(`invalid session id: ${id}`);
2894
2950
  process.exit(1);
2895
2951
  }
2896
- if (!existsSync22(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2952
+ if (!existsSync23(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2897
2953
  const handle = acquireLock("redact");
2898
2954
  if (handle === null) process.exit(0);
2899
2955
  try {
2900
2956
  const localPath = resolveLiveTranscript2(id);
2901
- if (localPath === null || !existsSync22(localPath)) {
2957
+ if (localPath === null || !existsSync23(localPath)) {
2902
2958
  fail(`could not resolve local transcript for session ${id} on this host`);
2903
2959
  process.exitCode = 1;
2904
2960
  return;
2905
2961
  }
2906
- const sessionDir = join27(dirname4(localPath), id);
2962
+ const sessionDir = join28(dirname4(localPath), id);
2907
2963
  const subtreeFiles = listSubtreeFiles(sessionDir);
2908
2964
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
2909
2965
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -2956,8 +3012,8 @@ ${lines}`);
2956
3012
  }
2957
3013
 
2958
3014
  // src/commands.pull.ts
2959
- import { existsSync as existsSync30, mkdirSync as mkdirSync8 } from "node:fs";
2960
- 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";
2961
3017
 
2962
3018
  // src/commands.push.sections.ts
2963
3019
  init_color();
@@ -2986,14 +3042,6 @@ function summaryRow(verb, unmapped, collisions = 0, extrasSkipped = 0) {
2986
3042
  const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
2987
3043
  return clean ? `${green(okGlyph)} ${text}` : `${yellow(warnGlyph)} ${text}`;
2988
3044
  }
2989
- function emitSummary(verb, unmapped, collisions = 0, extrasSkipped = 0) {
2990
- const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
2991
- if (clean) {
2992
- ok(text);
2993
- return;
2994
- }
2995
- warn(text);
2996
- }
2997
3045
 
2998
3046
  // src/commands.push.sections.ts
2999
3047
  function collapsedSkipRow(n, noun) {
@@ -3053,22 +3101,34 @@ init_config();
3053
3101
 
3054
3102
  // src/extras-sync.ts
3055
3103
  init_config();
3056
- import { existsSync as existsSync25 } from "node:fs";
3057
- import { join as join30 } from "node:path";
3104
+ import { existsSync as existsSync26 } from "node:fs";
3105
+ import { join as join31 } from "node:path";
3058
3106
 
3059
3107
  // src/extras-sync.diff.ts
3060
3108
  init_utils();
3061
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
+ }
3062
3122
  function listDivergingFiles(a, b) {
3063
3123
  try {
3064
- const stdout = execFileSync13("git", ["diff", "--no-index", "--name-only", a, b], {
3124
+ const stdout = execFileSync13("git", ["diff", "--no-index", "--name-status", a, b], {
3065
3125
  stdio: ["ignore", "pipe", "pipe"]
3066
3126
  }).toString();
3067
- return stdout.split("\n").filter((line) => line.length > 0);
3127
+ return parseDiffOutput(stdout);
3068
3128
  } catch (err) {
3069
3129
  const e = err;
3070
3130
  if (e.status === 1 && e.stdout !== void 0) {
3071
- return e.stdout.toString().split("\n").filter((line) => line.length > 0);
3131
+ return parseDiffOutput(e.stdout.toString());
3072
3132
  }
3073
3133
  if (e.code === "ENOENT") {
3074
3134
  warn(`git not on PATH; divergence check skipped for ${a}`);
@@ -3081,8 +3141,8 @@ function listDivergingFiles(a, b) {
3081
3141
 
3082
3142
  // src/extras-sync.core.ts
3083
3143
  init_config();
3084
- import { cpSync as cpSync4, existsSync as existsSync23, rmSync as rmSync7 } from "node:fs";
3085
- 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";
3086
3146
 
3087
3147
  // src/extras-sync.guards.ts
3088
3148
  init_utils();
@@ -3105,9 +3165,9 @@ function assertSafeLocalRoot(localRoot, logical) {
3105
3165
  init_utils();
3106
3166
  init_utils_json();
3107
3167
  function loadValidatedExtras(opts) {
3108
- const mapPath = join28(REPO_HOME, "path-map.json");
3109
- const repoExtras = join28(REPO_HOME, "shared", "extras");
3110
- 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)) {
3111
3171
  if (opts.missingMsg !== void 0) log(opts.missingMsg);
3112
3172
  return null;
3113
3173
  }
@@ -3149,8 +3209,8 @@ init_utils_json();
3149
3209
 
3150
3210
  // src/extras-sync.remap.ts
3151
3211
  init_config();
3152
- import { existsSync as existsSync24, mkdirSync as mkdirSync6 } from "node:fs";
3153
- 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";
3154
3214
  init_utils_fs();
3155
3215
  function runExtrasOp(v, dryRun, paths, backup) {
3156
3216
  const counts = { unmapped: 0, skipped: 0 };
@@ -3158,7 +3218,7 @@ function runExtrasOp(v, dryRun, paths, backup) {
3158
3218
  const would = [];
3159
3219
  for (const t of eachExtrasTarget(v, counts)) {
3160
3220
  const { src, dst } = paths(t);
3161
- if (!existsSync24(src)) continue;
3221
+ if (!existsSync25(src)) continue;
3162
3222
  const item = `${t.logical}/${t.dirname}`;
3163
3223
  if (dryRun) {
3164
3224
  would.push(item);
@@ -3174,14 +3234,14 @@ function remapExtrasPush(ts, opts = {}) {
3174
3234
  const dryRun = opts.dryRun === true;
3175
3235
  const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
3176
3236
  if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
3177
- const repoExtras = join29(REPO_HOME, "shared", "extras");
3237
+ const repoExtras = join30(REPO_HOME, "shared", "extras");
3178
3238
  if (!dryRun) mkdirSync6(repoExtras, { recursive: true });
3179
3239
  const { unmapped, skipped, done, would } = runExtrasOp(
3180
3240
  v,
3181
3241
  dryRun,
3182
3242
  ({ localRoot, logical, dirname: dirname6 }) => ({
3183
- src: join29(localRoot, dirname6),
3184
- dst: join29(repoExtras, logical, dirname6)
3243
+ src: join30(localRoot, dirname6),
3244
+ dst: join30(repoExtras, logical, dirname6)
3185
3245
  }),
3186
3246
  (dst) => backupRepoWrite(dst, ts, REPO_HOME)
3187
3247
  );
@@ -3194,13 +3254,13 @@ function remapExtrasPull(ts, opts = {}) {
3194
3254
  missingMsg: "no path-map or repo extras dir; skipping extras remap"
3195
3255
  });
3196
3256
  if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
3197
- const repoExtras = join29(REPO_HOME, "shared", "extras");
3257
+ const repoExtras = join30(REPO_HOME, "shared", "extras");
3198
3258
  const { unmapped, skipped, done, would } = runExtrasOp(
3199
3259
  v,
3200
3260
  dryRun,
3201
3261
  ({ localRoot, logical, dirname: dirname6 }) => ({
3202
- src: join29(repoExtras, logical, dirname6),
3203
- dst: join29(localRoot, dirname6)
3262
+ src: join30(repoExtras, logical, dirname6),
3263
+ dst: join30(localRoot, dirname6)
3204
3264
  }),
3205
3265
  // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
3206
3266
  // localRoot so the backup tree mirrors the project layout.
@@ -3214,14 +3274,14 @@ function divergenceCheckExtras(ts) {
3214
3274
  const v = loadValidatedExtras({});
3215
3275
  if (v === null) return;
3216
3276
  const counts = { unmapped: 0, skipped: 0 };
3217
- const backupRoot = join30(BACKUP_BASE, ts, "extras");
3277
+ const backupRoot = join31(BACKUP_BASE, ts, "extras");
3218
3278
  for (const { logical, localRoot, dirname: dirname6 } of eachExtrasTarget(v, counts)) {
3219
- const local = join30(localRoot, dirname6);
3220
- const repo = join30(REPO_HOME, "shared", "extras", logical, dirname6);
3221
- 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;
3222
3282
  const diff = listDivergingFiles(local, repo);
3223
3283
  if (diff.length === 0) continue;
3224
- const projectBackupRoot = join30(backupRoot, encodePath(localRoot));
3284
+ const projectBackupRoot = join31(backupRoot, encodePath(localRoot));
3225
3285
  warn(
3226
3286
  `local ${dirname6} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
3227
3287
  );
@@ -3234,47 +3294,61 @@ init_config();
3234
3294
  init_utils();
3235
3295
  init_utils_fs();
3236
3296
  init_utils_json();
3237
- import { existsSync as existsSync26, lstatSync as lstatSync7, rmSync as rmSync8 } from "node:fs";
3238
- 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";
3299
+ function emitAutoMove(onPreview, linkPath, ts, name) {
3300
+ if (onPreview) {
3301
+ onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
3302
+ } else {
3303
+ log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
3304
+ }
3305
+ }
3306
+ function emitCreate(onPreview, from, to) {
3307
+ if (onPreview) {
3308
+ onPreview({ kind: "create", from, to });
3309
+ } else {
3310
+ log(`would create symlink: ${from} -> ${to}`);
3311
+ }
3312
+ }
3239
3313
  function applySharedLinks(ts, map, opts = {}) {
3240
3314
  const dryRun = opts.dryRun === true;
3241
3315
  const linkNames = allSharedLinks(map);
3242
3316
  for (const name of linkNames) {
3243
- const linkPath = join31(CLAUDE_HOME, name);
3244
- const target = join31(REPO_HOME, "shared", name);
3245
- if (!existsSync26(linkPath)) continue;
3317
+ const linkPath = join32(CLAUDE_HOME, name);
3318
+ const target = join32(REPO_HOME, "shared", name);
3319
+ if (!existsSync27(linkPath)) continue;
3246
3320
  if (lstatSync7(linkPath).isSymbolicLink()) continue;
3247
- if (!existsSync26(target)) continue;
3321
+ if (!existsSync27(target)) continue;
3248
3322
  if (dryRun) {
3249
- log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
3323
+ emitAutoMove(opts.onPreview, linkPath, ts, name);
3250
3324
  continue;
3251
3325
  }
3252
3326
  backupBeforeWrite(linkPath, ts);
3253
3327
  rmSync8(linkPath, { recursive: true, force: true });
3254
3328
  }
3255
3329
  for (const name of linkNames) {
3256
- const target = join31(REPO_HOME, "shared", name);
3257
- if (!existsSync26(target)) continue;
3330
+ const target = join32(REPO_HOME, "shared", name);
3331
+ if (!existsSync27(target)) continue;
3258
3332
  if (dryRun) {
3259
- log(`would create symlink: ${join31(CLAUDE_HOME, name)} -> ${target}`);
3333
+ emitCreate(opts.onPreview, join32(CLAUDE_HOME, name), target);
3260
3334
  continue;
3261
3335
  }
3262
- ensureSymlink(join31(CLAUDE_HOME, name), target);
3336
+ ensureSymlink(join32(CLAUDE_HOME, name), target);
3263
3337
  }
3264
3338
  }
3265
3339
  function regenerateSettings(ts, opts = {}) {
3266
3340
  const dryRun = opts.dryRun === true;
3267
- const basePath = join31(REPO_HOME, "shared", "settings.base.json");
3268
- const hostPath = join31(REPO_HOME, "hosts", `${HOST}.json`);
3269
- 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)) {
3270
3344
  die("repo not initialized; run 'nomad init' to scaffold");
3271
3345
  }
3272
3346
  const base = readJson(basePath);
3273
- const hasOverrides = existsSync26(hostPath);
3347
+ const hasOverrides = existsSync27(hostPath);
3274
3348
  const overrides = hasOverrides ? readJson(hostPath) : {};
3275
3349
  const merged = deepMerge(base, overrides);
3276
- const settingsPath = join31(CLAUDE_HOME, "settings.json");
3277
- if (!hasOverrides && existsSync26(settingsPath)) {
3350
+ const settingsPath = join32(CLAUDE_HOME, "settings.json");
3351
+ if (!hasOverrides && existsSync27(settingsPath)) {
3278
3352
  try {
3279
3353
  const existing = readJson(settingsPath);
3280
3354
  const baseKeys = new Set(Object.keys(base));
@@ -3300,8 +3374,8 @@ function regenerateSettings(ts, opts = {}) {
3300
3374
 
3301
3375
  // src/preview.ts
3302
3376
  init_config();
3303
- import { existsSync as existsSync27 } from "node:fs";
3304
- import { join as join32 } from "node:path";
3377
+ import { existsSync as existsSync28 } from "node:fs";
3378
+ import { join as join33 } from "node:path";
3305
3379
 
3306
3380
  // node_modules/diff/libesm/diff/base.js
3307
3381
  var Diff = class {
@@ -3575,8 +3649,8 @@ function diffLinesToUnified(oldStr, newStr) {
3575
3649
  }
3576
3650
 
3577
3651
  // src/preview.ts
3578
- init_utils();
3579
3652
  init_utils_json();
3653
+ var CANONICAL_ORDER_NOTE = "settings.json will be rewritten in canonical key order; no value changes";
3580
3654
  function diffJsonStrings(currentJsonText, newJsonText) {
3581
3655
  if (currentJsonText === newJsonText) return "";
3582
3656
  const lines = [
@@ -3587,7 +3661,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
3587
3661
  return lines.join("\n");
3588
3662
  }
3589
3663
  function readJsonOrNull(path) {
3590
- if (!existsSync27(path)) return null;
3664
+ if (!existsSync28(path)) return null;
3591
3665
  try {
3592
3666
  return readJson(path);
3593
3667
  } catch {
@@ -3597,59 +3671,86 @@ function readJsonOrNull(path) {
3597
3671
  function previewSettings(basePath, hostPath, settingsPath) {
3598
3672
  const base = readJsonOrNull(basePath);
3599
3673
  if (base === null) {
3600
- log("settings.json: section skipped (base or current missing)");
3601
- return;
3674
+ return { diff: "", notes: ["section skipped (base or current missing)"] };
3602
3675
  }
3676
+ const notes = [];
3603
3677
  const hostOverrides = readJsonOrNull(hostPath);
3604
- if (hostOverrides === null && existsSync27(hostPath)) {
3605
- log(`settings.json: malformed hosts/${HOST}.json; ignoring overrides`);
3678
+ if (hostOverrides === null && existsSync28(hostPath)) {
3679
+ notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
3606
3680
  }
3607
3681
  const merged = deepMerge(base, hostOverrides ?? {});
3608
3682
  const current = readJsonOrNull(settingsPath);
3609
- if (current === null && existsSync27(settingsPath)) {
3610
- log("settings.json: malformed; skipping diff");
3611
- return;
3683
+ if (current === null && existsSync28(settingsPath)) {
3684
+ return { diff: "", notes: [...notes, "malformed; skipping diff"] };
3612
3685
  }
3686
+ const rawEqual = JSON.stringify(current ?? {}, null, 2) === JSON.stringify(merged, null, 2);
3613
3687
  const diff = diffJsonStrings(
3614
- JSON.stringify(current ?? {}, null, 2),
3615
- JSON.stringify(merged, null, 2)
3688
+ JSON.stringify(sortKeysDeep(current ?? {}), null, 2),
3689
+ JSON.stringify(sortKeysDeep(merged), null, 2)
3616
3690
  );
3617
- if (diff === "") {
3618
- log("settings.json: no changes");
3619
- } else {
3620
- log("settings.json:");
3621
- for (const line of diff.split("\n")) log(line);
3691
+ if (diff === "" && !rawEqual) notes.push(CANONICAL_ORDER_NOTE);
3692
+ return { diff, notes };
3693
+ }
3694
+ function formatLinkRow(e) {
3695
+ return `${e.kind} ${e.from} -> ${e.to}`;
3696
+ }
3697
+ function formatSessionRow(e) {
3698
+ return e.kind === "overwrite" ? `overwrite ${e.dst} (from ${e.src})` : e.text;
3699
+ }
3700
+ function buildSettingsSectionForPreview(result) {
3701
+ const s = section("settings.json", true);
3702
+ if (result.diff !== "") {
3703
+ for (const line of result.diff.split("\n")) {
3704
+ addItem(s, line);
3705
+ }
3622
3706
  }
3707
+ for (const note of result.notes) {
3708
+ addItem(s, `note: ${note}`);
3709
+ }
3710
+ return s;
3623
3711
  }
3624
- function computePreview(ts, map) {
3625
- log(`would pull on host=${HOST} (dry-run; no mutation)`);
3626
- applySharedLinks(ts, map, { dryRun: true });
3627
- previewSettings(
3628
- join32(REPO_HOME, "shared", "settings.base.json"),
3629
- join32(REPO_HOME, "hosts", `${HOST}.json`),
3630
- join32(CLAUDE_HOME, "settings.json")
3712
+ function computePreview(ts, map, verb = "pull") {
3713
+ console.log(`would pull on host=${HOST} (dry-run; no mutation)`);
3714
+ console.log("");
3715
+ const links = section("Symlinks");
3716
+ applySharedLinks(ts, map, {
3717
+ dryRun: true,
3718
+ onPreview: (e) => addItem(links, formatLinkRow(e))
3719
+ });
3720
+ const settingsResult = previewSettings(
3721
+ join33(REPO_HOME, "shared", "settings.base.json"),
3722
+ join33(REPO_HOME, "hosts", `${HOST}.json`),
3723
+ join33(CLAUDE_HOME, "settings.json")
3631
3724
  );
3632
- const remapResult = remapPull(ts, { dryRun: true });
3725
+ const settingsSection = buildSettingsSectionForPreview(settingsResult);
3726
+ const sessions = section("Sessions");
3727
+ const remapResult = remapPull(ts, {
3728
+ dryRun: true,
3729
+ onPreview: (e) => addItem(sessions, formatSessionRow(e))
3730
+ });
3731
+ const summary = section("Summary");
3732
+ addItem(summary, summaryRow(verb, remapResult.unmapped));
3733
+ renderTree([links, settingsSection, sessions, summary]);
3633
3734
  return { unmapped: remapResult.unmapped, collisions: 0 };
3634
3735
  }
3635
3736
 
3636
3737
  // src/spinner.ts
3637
3738
  init_color();
3638
- import { existsSync as existsSync29 } from "node:fs";
3739
+ import { existsSync as existsSync30 } from "node:fs";
3639
3740
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3640
3741
  import { Worker } from "node:worker_threads";
3641
3742
 
3642
3743
  // src/commands.push.recovery.ts
3643
3744
  init_config();
3644
3745
  import { readFileSync as readFileSync10, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
3645
- import { join as join35 } from "node:path";
3746
+ import { join as join36 } from "node:path";
3646
3747
  import { createInterface } from "node:readline/promises";
3647
3748
 
3648
3749
  // src/commands.push.recovery.redact.ts
3649
3750
  init_config();
3650
3751
  init_config_sharedDirs_guard();
3651
- import { cpSync as cpSync5, existsSync as existsSync28, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
3652
- 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";
3653
3754
  init_push_gitleaks_scan();
3654
3755
  init_utils_json();
3655
3756
  init_utils();
@@ -3680,8 +3781,8 @@ function resolveStagedDir(localPath, map) {
3680
3781
  assertSafeLogical(logical);
3681
3782
  const abs = hostMap[HOST];
3682
3783
  if (abs === void 0) continue;
3683
- if (localPath.startsWith(join33(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
3684
- 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);
3685
3786
  }
3686
3787
  }
3687
3788
  return null;
@@ -3703,7 +3804,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3703
3804
  `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
3704
3805
  );
3705
3806
  }
3706
- const sessionDir = join33(dirname5(localPath), sid);
3807
+ const sessionDir = join34(dirname5(localPath), sid);
3707
3808
  const subtreeFiles = listSubtreeFiles(sessionDir);
3708
3809
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
3709
3810
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -3737,9 +3838,9 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3737
3838
  );
3738
3839
  }
3739
3840
  mkdirSync7(stagedProjectDir, { recursive: true });
3740
- cpSync5(localPath, join33(stagedProjectDir, `${sid}.jsonl`), { force: true });
3741
- if (existsSync28(sessionDir)) {
3742
- 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 });
3743
3844
  }
3744
3845
  return true;
3745
3846
  }
@@ -3747,13 +3848,13 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3747
3848
  // src/commands.push.recovery.drop.ts
3748
3849
  init_config();
3749
3850
  import { rmSync as rmSync9 } from "node:fs";
3750
- import { join as join34 } from "node:path";
3851
+ import { join as join35 } from "node:path";
3751
3852
  function dropSessionFromStaged(sid, map) {
3752
3853
  const logicals = Object.keys(map.projects);
3753
3854
  if (logicals.length === 0) return false;
3754
3855
  for (const logical of logicals) {
3755
- const jsonl = join34(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
3756
- 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);
3757
3858
  rmSync9(jsonl, { force: true });
3758
3859
  rmSync9(dir, { recursive: true, force: true });
3759
3860
  }
@@ -3871,7 +3972,7 @@ function applyThenRescan(scanVerdict, repoHome) {
3871
3972
  return next;
3872
3973
  }
3873
3974
  function allowThenRescan(append, scanVerdict, repoHome) {
3874
- const ignPath = join35(repoHome, ".gitleaksignore");
3975
+ const ignPath = join36(repoHome, ".gitleaksignore");
3875
3976
  let before;
3876
3977
  try {
3877
3978
  before = readFileSync10(ignPath, "utf8");
@@ -3969,7 +4070,7 @@ function writeAnimatedDone(out, label, ms, useTTY) {
3969
4070
  `);
3970
4071
  }
3971
4072
  function resolveWorkerPath(deps = {}) {
3972
- const check = deps.existsSyncFn ?? existsSync29;
4073
+ const check = deps.existsSyncFn ?? existsSync30;
3973
4074
  const base = deps.baseUrl ?? import.meta.url;
3974
4075
  const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
3975
4076
  if (check(mjs)) return mjs;
@@ -4009,10 +4110,10 @@ function startSpinner(label, deps = {}) {
4009
4110
  const elapsed = now() - startMs;
4010
4111
  if (animate && !degraded && worker !== null) {
4011
4112
  worker.postMessage({ type: "pause" });
4012
- if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
4013
- else out.write("\r\x1B[K");
4014
4113
  worker.terminate();
4015
4114
  worker = null;
4115
+ if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
4116
+ else out.write("\r\x1B[K");
4016
4117
  } else if (success) {
4017
4118
  writePlainDone(out, dl, elapsed);
4018
4119
  }
@@ -4022,6 +4123,122 @@ function startSpinner(label, deps = {}) {
4022
4123
  stop: () => finalize(false)
4023
4124
  };
4024
4125
  }
4126
+ function withSpinner(label, fn, deps) {
4127
+ const sp = startSpinner(label, deps);
4128
+ try {
4129
+ const result = fn();
4130
+ sp.succeed();
4131
+ return result;
4132
+ } finally {
4133
+ sp.stop();
4134
+ }
4135
+ }
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
+ }
4025
4242
 
4026
4243
  // src/commands.pull.ts
4027
4244
  init_utils();
@@ -4030,14 +4247,7 @@ init_utils_json();
4030
4247
  function applyWetPull(ts, map) {
4031
4248
  applySharedLinks(ts, map);
4032
4249
  const { label } = regenerateSettings(ts);
4033
- const syncSp = startSpinner("Syncing sessions");
4034
- let remapResult;
4035
- try {
4036
- remapResult = remapPull(ts);
4037
- syncSp.succeed();
4038
- } finally {
4039
- syncSp.stop();
4040
- }
4250
+ const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
4041
4251
  const extrasResult = remapExtrasPull(ts);
4042
4252
  const summary = section("Summary");
4043
4253
  addItem(
@@ -4051,18 +4261,32 @@ function applyWetPull(ts, map) {
4051
4261
  summary
4052
4262
  ]);
4053
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
+ }
4054
4276
  function cmdPull(opts = {}) {
4055
4277
  const dryRun = opts.dryRun === true;
4056
- if (!existsSync30(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4057
- 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"))) {
4058
4281
  die("repo not initialized; run 'nomad init' to scaffold");
4059
4282
  }
4060
4283
  const handle = acquireLock("pull");
4061
4284
  if (handle === null) process.exit(0);
4062
4285
  try {
4063
4286
  const ts = freshBackupTs(BACKUP_BASE);
4287
+ handleWedge(REPO_HOME, forceRemote);
4064
4288
  if (!dryRun) {
4065
- const backupRoot = join36(BACKUP_BASE, ts);
4289
+ const backupRoot = join37(BACKUP_BASE, ts);
4066
4290
  try {
4067
4291
  mkdirSync8(backupRoot, { recursive: true });
4068
4292
  } catch (err) {
@@ -4073,13 +4297,12 @@ function cmdPull(opts = {}) {
4073
4297
  dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
4074
4298
  );
4075
4299
  gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
4076
- const mapPath = join36(REPO_HOME, "path-map.json");
4077
- const map = existsSync30(mapPath) ? readPathMap(mapPath) : { projects: {} };
4300
+ const mapPath = join37(REPO_HOME, "path-map.json");
4301
+ const map = existsSync31(mapPath) ? readPathMap(mapPath) : { projects: {} };
4078
4302
  divergenceCheckExtras(ts);
4079
4303
  if (dryRun) {
4080
- const previewResult = computePreview(ts, map);
4304
+ computePreview(ts, map, "pull");
4081
4305
  log("dry-run complete; no mutation");
4082
- emitSummary("pull", previewResult.unmapped);
4083
4306
  } else {
4084
4307
  applyWetPull(ts, map);
4085
4308
  }
@@ -4097,8 +4320,8 @@ function cmdPull(opts = {}) {
4097
4320
 
4098
4321
  // src/commands.push.ts
4099
4322
  init_config();
4100
- import { existsSync as existsSync32 } from "node:fs";
4101
- 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";
4102
4325
 
4103
4326
  // src/commands.push.allowlist.ts
4104
4327
  init_config();
@@ -4122,7 +4345,7 @@ function isNeverSync(path) {
4122
4345
  }
4123
4346
  return false;
4124
4347
  }
4125
- function parsePorcelainZ(statusPorcelain) {
4348
+ function parsePorcelainZ2(statusPorcelain) {
4126
4349
  const records = statusPorcelain.split("\0");
4127
4350
  const paths = [];
4128
4351
  for (let i = 0; i < records.length; i++) {
@@ -4152,7 +4375,7 @@ function enforceAllowList(statusPorcelain, map) {
4152
4375
  ];
4153
4376
  const neverSyncHits = [];
4154
4377
  const violations = [];
4155
- for (const path of parsePorcelainZ(statusPorcelain)) {
4378
+ for (const path of parsePorcelainZ2(statusPorcelain)) {
4156
4379
  if (isNeverSync(path)) {
4157
4380
  neverSyncHits.push(path);
4158
4381
  } else if (!isAllowed(path, allowed)) {
@@ -4178,9 +4401,9 @@ init_color();
4178
4401
  init_config();
4179
4402
  init_config_sharedDirs_guard();
4180
4403
  import { randomBytes as randomBytes2 } from "node:crypto";
4181
- 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";
4182
4405
  import { homedir as homedir5 } from "node:os";
4183
- import { join as join37 } from "node:path";
4406
+ import { join as join38 } from "node:path";
4184
4407
  init_push_leak_verdict();
4185
4408
  init_push_gitleaks();
4186
4409
  init_utils_fs();
@@ -4195,13 +4418,13 @@ function stageSessions(tmpRoot, map) {
4195
4418
  if (!p || p === "TBD") continue;
4196
4419
  reverse.set(encodePath(p), logical);
4197
4420
  }
4198
- const localProjects = join37(CLAUDE_HOME, "projects");
4199
- if (!existsSync31(localProjects)) return 0;
4421
+ const localProjects = join38(CLAUDE_HOME, "projects");
4422
+ if (!existsSync32(localProjects)) return 0;
4200
4423
  let staged = 0;
4201
4424
  for (const dir of readdirSync10(localProjects)) {
4202
4425
  const logical = reverse.get(dir);
4203
4426
  if (!logical) continue;
4204
- copyDirJsonlOnly(join37(localProjects, dir), join37(tmpRoot, "shared", "projects", logical));
4427
+ copyDirJsonlOnly(join38(localProjects, dir), join38(tmpRoot, "shared", "projects", logical));
4205
4428
  staged++;
4206
4429
  }
4207
4430
  return staged;
@@ -4217,9 +4440,9 @@ function stageExtras(tmpRoot, map) {
4217
4440
  if (!localRoot || localRoot === "TBD") continue;
4218
4441
  for (const dirname6 of dirnames) {
4219
4442
  if (!whitelist.includes(dirname6)) continue;
4220
- const src = join37(localRoot, dirname6);
4221
- if (!existsSync31(src)) continue;
4222
- 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);
4223
4446
  copyExtras(src, dst);
4224
4447
  staged++;
4225
4448
  }
@@ -4227,19 +4450,19 @@ function stageExtras(tmpRoot, map) {
4227
4450
  return staged;
4228
4451
  }
4229
4452
  function previewPushLeaks(map) {
4230
- const cacheDir = join37(homedir5(), ".cache", "claude-nomad");
4453
+ const cacheDir = join38(homedir5(), ".cache", "claude-nomad");
4231
4454
  mkdirSync9(cacheDir, { recursive: true });
4232
4455
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
4233
- const tmpRoot = join37(cacheDir, `push-preview-tree-${stamp}`);
4456
+ const tmpRoot = join38(cacheDir, `push-preview-tree-${stamp}`);
4234
4457
  try {
4235
4458
  const sessionCount = stageSessions(tmpRoot, map);
4236
4459
  const extrasCount = stageExtras(tmpRoot, map);
4237
4460
  if (sessionCount + extrasCount === 0) {
4238
4461
  return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
4239
4462
  }
4240
- const ignoreFile = join37(REPO_HOME, ".gitleaksignore");
4241
- if (existsSync31(ignoreFile)) {
4242
- copyFileSync(ignoreFile, join37(tmpRoot, ".gitleaksignore"));
4463
+ const ignoreFile = join38(REPO_HOME, ".gitleaksignore");
4464
+ if (existsSync32(ignoreFile)) {
4465
+ copyFileSync(ignoreFile, join38(tmpRoot, ".gitleaksignore"));
4243
4466
  }
4244
4467
  let findings;
4245
4468
  try {
@@ -4261,7 +4484,7 @@ init_utils();
4261
4484
  init_utils_fs();
4262
4485
  init_utils_json();
4263
4486
  function guardGitlinks() {
4264
- const gitlinks = findGitlinks(join38(REPO_HOME, "shared"));
4487
+ const gitlinks = findGitlinks(join39(REPO_HOME, "shared"));
4265
4488
  if (gitlinks.length === 0) return;
4266
4489
  for (const p of gitlinks) {
4267
4490
  const rel = relative5(REPO_HOME, p);
@@ -4272,34 +4495,15 @@ function guardGitlinks() {
4272
4495
  `gitlink trap: ${gitlinks.length} nested .git ${noun} in shared/; remove before retry`
4273
4496
  );
4274
4497
  }
4275
- function runScan() {
4276
- const sp = startSpinner("Scanning for secrets");
4277
- try {
4278
- const verdict = scanPushVerdict();
4279
- sp.succeed();
4280
- return verdict;
4281
- } finally {
4282
- sp.stop();
4283
- }
4284
- }
4285
- function runPush() {
4286
- const sp = startSpinner("Pushing");
4287
- try {
4288
- gitOrFatal(["push"], "git push", REPO_HOME);
4289
- sp.succeed();
4290
- } finally {
4291
- sp.stop();
4292
- }
4293
- }
4294
4498
  async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule) {
4295
4499
  gitOrFatal(["add", "-A"], "git add", REPO_HOME);
4296
- let verdict = runScan();
4500
+ let verdict = withSpinner("Scanning for secrets", scanPushVerdict);
4297
4501
  if (verdict.leak) {
4298
4502
  renderPushTree(st, verdict);
4299
4503
  verdict = await resolveLeakFindings(verdict, ts, map, { redactAll, allowAll, allowRule });
4300
4504
  }
4301
4505
  gitOrFatal(["commit", "-m", `chore: sync from ${HOST}`], "git commit", REPO_HOME);
4302
- runPush();
4506
+ withSpinner("Pushing", () => gitOrFatal(["push"], "git push", REPO_HOME));
4303
4507
  renderPushTree(st, verdict);
4304
4508
  }
4305
4509
  function runDryRunPreview(st, map) {
@@ -4332,22 +4536,16 @@ async function cmdPush(opts = {}) {
4332
4536
  const allowAll = opts.allowAll === true;
4333
4537
  const allowRule = opts.allowRule;
4334
4538
  guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
4335
- if (!existsSync32(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4539
+ if (!existsSync33(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4336
4540
  const handle = acquireLock("push");
4337
4541
  if (handle === null) process.exit(0);
4338
4542
  try {
4339
4543
  console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
4340
4544
  probeGitleaks();
4341
- const rebaseSp = startSpinner("Rebasing onto origin");
4342
- try {
4343
- rebaseBeforePush();
4344
- rebaseSp.succeed();
4345
- } finally {
4346
- rebaseSp.stop();
4347
- }
4545
+ withSpinner("Rebasing onto origin", rebaseBeforePush);
4348
4546
  const ts = freshBackupTs(BACKUP_BASE);
4349
- const remap = remapPush(ts, { dryRun });
4350
- const extras = remapExtrasPush(ts, { dryRun });
4547
+ const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun }));
4548
+ const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
4351
4549
  const st = { dryRun, remap, extras };
4352
4550
  guardGitlinks();
4353
4551
  const status = gitStatusPorcelainZ(REPO_HOME, { untrackedAll: true });
@@ -4356,8 +4554,8 @@ async function cmdPush(opts = {}) {
4356
4554
  renderNoScanTree(st);
4357
4555
  return;
4358
4556
  }
4359
- const mapPath = join38(REPO_HOME, "path-map.json");
4360
- if (!existsSync32(mapPath)) {
4557
+ const mapPath = join39(REPO_HOME, "path-map.json");
4558
+ if (!existsSync33(mapPath)) {
4361
4559
  if (dryRun) return runDryRunPreview(st, null);
4362
4560
  die("path-map.json missing, cannot enforce push allow-list");
4363
4561
  }
@@ -4378,9 +4576,9 @@ async function cmdPush(opts = {}) {
4378
4576
  }
4379
4577
 
4380
4578
  // src/commands.update.ts
4381
- import { execFileSync as execFileSync14 } from "node:child_process";
4579
+ import { execFileSync as execFileSync15 } from "node:child_process";
4382
4580
  init_utils();
4383
- function cmdUpdate(run = execFileSync14) {
4581
+ function cmdUpdate(run = execFileSync15) {
4384
4582
  try {
4385
4583
  run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
4386
4584
  } catch (err) {
@@ -4397,19 +4595,18 @@ init_config();
4397
4595
 
4398
4596
  // src/diff.ts
4399
4597
  init_config();
4400
- import { existsSync as existsSync33 } from "node:fs";
4401
- import { join as join39 } from "node:path";
4598
+ import { existsSync as existsSync34 } from "node:fs";
4599
+ import { join as join40 } from "node:path";
4402
4600
  init_utils();
4403
4601
  init_utils_fs();
4404
4602
  init_utils_json();
4405
4603
  function cmdDiff() {
4406
4604
  try {
4407
- if (!existsSync33(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4605
+ if (!existsSync34(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4408
4606
  const ts = freshBackupTs(BACKUP_BASE);
4409
- const mapPath = join39(REPO_HOME, "path-map.json");
4410
- const map = existsSync33(mapPath) ? readPathMap(mapPath) : { projects: {} };
4411
- const result = computePreview(ts, map);
4412
- emitSummary("diff", result.unmapped);
4607
+ const mapPath = join40(REPO_HOME, "path-map.json");
4608
+ const map = existsSync34(mapPath) ? readPathMap(mapPath) : { projects: {} };
4609
+ computePreview(ts, map, "diff");
4413
4610
  } catch (err) {
4414
4611
  if (err instanceof NomadFatal) {
4415
4612
  fail(err.message);
@@ -4422,19 +4619,19 @@ function cmdDiff() {
4422
4619
 
4423
4620
  // src/init.ts
4424
4621
  init_config();
4425
- import { existsSync as existsSync35, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
4426
- 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";
4427
4624
 
4428
4625
  // src/init.gh-onboard.ts
4429
4626
  init_config();
4430
- import { execFileSync as execFileSync15 } from "node:child_process";
4627
+ import { execFileSync as execFileSync16 } from "node:child_process";
4431
4628
  init_utils();
4432
4629
  var DEFAULT_REPO_NAME = "claude-nomad-config";
4433
4630
  function isValidRepoName(name) {
4434
4631
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
4435
4632
  }
4436
4633
  var GH_NETWORK_TIMEOUT_MS = 3e4;
4437
- function ensureOriginRepo(repoName, run = execFileSync15) {
4634
+ function ensureOriginRepo(repoName, run = execFileSync16) {
4438
4635
  if (!isValidRepoName(repoName)) {
4439
4636
  die(
4440
4637
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -4504,31 +4701,31 @@ init_config();
4504
4701
  init_utils();
4505
4702
  init_utils_fs();
4506
4703
  init_utils_json();
4507
- import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync34, rmSync as rmSync12, statSync as statSync9 } from "node:fs";
4508
- 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";
4509
4706
  function snapshotIntoShared(map) {
4510
4707
  for (const name of allSharedLinks(map)) {
4511
- const src = join40(CLAUDE_HOME, name);
4512
- if (!existsSync34(src)) continue;
4513
- 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);
4514
4711
  if (statSync9(src).isDirectory()) {
4515
- const gk = join40(dst, ".gitkeep");
4516
- if (existsSync34(gk)) rmSync12(gk);
4712
+ const gk = join41(dst, ".gitkeep");
4713
+ if (existsSync35(gk)) rmSync12(gk);
4517
4714
  cpSync6(src, dst, { recursive: true, force: false, errorOnExist: true });
4518
4715
  } else {
4519
4716
  copyFileSync2(src, dst);
4520
4717
  }
4521
4718
  log(`snapshotted shared/${name} from ${src}`);
4522
4719
  }
4523
- const userSettings = join40(CLAUDE_HOME, "settings.json");
4524
- if (existsSync34(userSettings)) {
4720
+ const userSettings = join41(CLAUDE_HOME, "settings.json");
4721
+ if (existsSync35(userSettings)) {
4525
4722
  let parsed;
4526
4723
  try {
4527
4724
  parsed = readJson(userSettings);
4528
4725
  } catch (err) {
4529
4726
  return die(`malformed ${userSettings}: ${err.message}`);
4530
4727
  }
4531
- const hostFile = join40(REPO_HOME, "hosts", `${HOST}.json`);
4728
+ const hostFile = join41(REPO_HOME, "hosts", `${HOST}.json`);
4532
4729
  writeJsonAtomic(hostFile, parsed);
4533
4730
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
4534
4731
  }
@@ -4541,14 +4738,14 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
4541
4738
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
4542
4739
  function preflightConflict(repoHome) {
4543
4740
  const candidates = [
4544
- join41(repoHome, "shared", "settings.base.json"),
4545
- join41(repoHome, "shared", "CLAUDE.md"),
4546
- join41(repoHome, "path-map.json"),
4547
- join41(repoHome, "hosts"),
4548
- 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")
4549
4746
  ];
4550
4747
  for (const c of candidates) {
4551
- if (existsSync35(c)) return c;
4748
+ if (existsSync36(c)) return c;
4552
4749
  }
4553
4750
  return null;
4554
4751
  }
@@ -4561,25 +4758,25 @@ function cmdInit(opts = {}) {
4561
4758
  die(`already initialized; refusing to clobber ${conflict}`);
4562
4759
  }
4563
4760
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
4564
- mkdirSync10(join41(REPO_HOME, "shared"), { recursive: true });
4565
- mkdirSync10(join41(REPO_HOME, "hosts"), { recursive: true });
4761
+ mkdirSync10(join42(REPO_HOME, "shared"), { recursive: true });
4762
+ mkdirSync10(join42(REPO_HOME, "hosts"), { recursive: true });
4566
4763
  for (const name of SHARED_KEEP_DIRS) {
4567
- mkdirSync10(join41(REPO_HOME, "shared", name), { recursive: true });
4764
+ mkdirSync10(join42(REPO_HOME, "shared", name), { recursive: true });
4568
4765
  }
4569
- const userClaudeMd = join41(CLAUDE_HOME, "CLAUDE.md");
4570
- if (!snapshot || !existsSync35(userClaudeMd)) {
4571
- 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);
4572
4769
  log("created shared/CLAUDE.md");
4573
4770
  }
4574
4771
  for (const name of SHARED_KEEP_DIRS) {
4575
- writeFileSync6(join41(REPO_HOME, "shared", name, ".gitkeep"), "");
4772
+ writeFileSync6(join42(REPO_HOME, "shared", name, ".gitkeep"), "");
4576
4773
  log(`created shared/${name}/.gitkeep`);
4577
4774
  }
4578
- writeFileSync6(join41(REPO_HOME, "hosts", ".gitkeep"), "");
4775
+ writeFileSync6(join42(REPO_HOME, "hosts", ".gitkeep"), "");
4579
4776
  log("created hosts/.gitkeep");
4580
- writeJsonAtomic(join41(REPO_HOME, "shared", "settings.base.json"), {});
4777
+ writeJsonAtomic(join42(REPO_HOME, "shared", "settings.base.json"), {});
4581
4778
  log("created shared/settings.base.json");
4582
- writeJsonAtomic(join41(REPO_HOME, "path-map.json"), { projects: {} });
4779
+ writeJsonAtomic(join42(REPO_HOME, "path-map.json"), { projects: {} });
4583
4780
  log("created path-map.json");
4584
4781
  if (snapshot) {
4585
4782
  snapshotIntoShared({ projects: {} });
@@ -4783,6 +4980,28 @@ function parseAllowArgs(argv) {
4783
4980
  return positionals;
4784
4981
  }
4785
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
+
4786
5005
  // src/nomad.dispatch.push.ts
4787
5006
  var REJECT2 = { ok: false, advance: 0 };
4788
5007
  function applyBool2(seen, set) {
@@ -4841,7 +5060,7 @@ function parsePushArgs(argv) {
4841
5060
  // package.json
4842
5061
  var package_default = {
4843
5062
  name: "claude-nomad",
4844
- version: "0.40.0",
5063
+ version: "0.42.0",
4845
5064
  type: "module",
4846
5065
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
4847
5066
  keywords: [
@@ -4854,7 +5073,7 @@ var package_default = {
4854
5073
  type: "git",
4855
5074
  url: "git+https://github.com/funkadelic/claude-nomad.git"
4856
5075
  },
4857
- homepage: "https://github.com/funkadelic/claude-nomad#readme",
5076
+ homepage: "https://funkadelic.github.io/claude-nomad/",
4858
5077
  bugs: {
4859
5078
  url: "https://github.com/funkadelic/claude-nomad/issues"
4860
5079
  },
@@ -4939,6 +5158,11 @@ var DEFAULT_HELP = [
4939
5158
  "Commands:",
4940
5159
  row(" pull", "Sync ~/.claude/ from the shared repo (settings, symlinks, sessions)."),
4941
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."),
4942
5166
  "",
4943
5167
  row(" push", "Rebase, run safety checks (gitleaks, gitlinks, allow-list), commit, push."),
4944
5168
  row(" --dry-run", "Run pre-checks (rebase, gitleaks probe, gitlink scan) and preview"),
@@ -5031,15 +5255,15 @@ var DEFAULT_HELP = [
5031
5255
  init_config();
5032
5256
  init_utils();
5033
5257
  init_utils_json();
5034
- import { existsSync as existsSync36, readFileSync as readFileSync11, readdirSync as readdirSync11 } from "node:fs";
5035
- 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";
5036
5260
  function resumeCmd(sessionId) {
5037
5261
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
5038
5262
  fail(`invalid session id: ${sessionId}`);
5039
5263
  process.exit(1);
5040
5264
  }
5041
- const projectsRoot = join42(CLAUDE_HOME, "projects");
5042
- if (!existsSync36(projectsRoot)) {
5265
+ const projectsRoot = join43(CLAUDE_HOME, "projects");
5266
+ if (!existsSync37(projectsRoot)) {
5043
5267
  fail(`${projectsRoot} does not exist`);
5044
5268
  process.exit(1);
5045
5269
  }
@@ -5053,8 +5277,8 @@ function resumeCmd(sessionId) {
5053
5277
  fail(`no cwd field found in ${jsonlPath}`);
5054
5278
  process.exit(1);
5055
5279
  }
5056
- const mapPath = join42(REPO_HOME, "path-map.json");
5057
- if (!existsSync36(mapPath)) {
5280
+ const mapPath = join43(REPO_HOME, "path-map.json");
5281
+ if (!existsSync37(mapPath)) {
5058
5282
  fail("path-map.json missing");
5059
5283
  process.exit(1);
5060
5284
  }
@@ -5077,8 +5301,8 @@ function resumeCmd(sessionId) {
5077
5301
  }
5078
5302
  function findTranscriptPath(projectsRoot, sessionId) {
5079
5303
  for (const dir of readdirSync11(projectsRoot)) {
5080
- const candidate = join42(projectsRoot, dir, `${sessionId}.jsonl`);
5081
- if (existsSync36(candidate)) return candidate;
5304
+ const candidate = join43(projectsRoot, dir, `${sessionId}.jsonl`);
5305
+ if (existsSync37(candidate)) return candidate;
5082
5306
  }
5083
5307
  return null;
5084
5308
  }
@@ -5150,15 +5374,12 @@ try {
5150
5374
  console.log(package_default.version);
5151
5375
  break;
5152
5376
  case "pull": {
5153
- const sub = process.argv[3];
5154
- if (sub === void 0) {
5155
- cmdPull();
5156
- } else if (sub === "--dry-run" && process.argv.length === 4) {
5157
- cmdPull({ dryRun: true });
5158
- } else {
5159
- 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]");
5160
5380
  process.exit(1);
5161
5381
  }
5382
+ cmdPull({ dryRun: pullArgs.dryRun, forceRemote: pullArgs.forceRemote });
5162
5383
  break;
5163
5384
  }
5164
5385
  case "push": {