claude-nomad 0.48.0 → 0.50.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
@@ -162,6 +162,13 @@ var init_color = __esm({
162
162
 
163
163
  // src/utils.ts
164
164
  import { execFileSync } from "node:child_process";
165
+ function gitCaptureRaw(args, cwd) {
166
+ return execFileSync("git", args, {
167
+ cwd,
168
+ stdio: ["ignore", "pipe", "pipe"],
169
+ maxBuffer: 64 * 1024 * 1024
170
+ }).toString();
171
+ }
165
172
  function gitOrFatal(args, context, cwd) {
166
173
  try {
167
174
  execFileSync("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
@@ -387,7 +394,7 @@ function allSharedLinks(map) {
387
394
  }
388
395
  return [...SHARED_LINKS, ...extras];
389
396
  }
390
- var SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, HOST, SHARED_LINKS, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, PUSH_ALLOWED_STATIC;
397
+ var SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, HOST, SHARED_LINKS, GSD_PREFIX, GSD_DROPPED_NAMES, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, PUSH_ALLOWED_STATIC;
391
398
  var init_config = __esm({
392
399
  "src/config.ts"() {
393
400
  "use strict";
@@ -399,15 +406,9 @@ var init_config = __esm({
399
406
  NPM_REGISTRY_LATEST_URL = "https://registry.npmjs.org/claude-nomad/latest";
400
407
  GITLEAKS_PINNED_VERSION = "8.30.1";
401
408
  HOST = (process.env.NOMAD_HOST || hostname()).toLowerCase();
402
- SHARED_LINKS = [
403
- "CLAUDE.md",
404
- "agents",
405
- "skills",
406
- "commands",
407
- "rules",
408
- "my-statusline.cjs",
409
- "hooks"
410
- ];
409
+ SHARED_LINKS = ["CLAUDE.md", "commands", "rules", "my-statusline.cjs"];
410
+ GSD_PREFIX = "gsd-";
411
+ GSD_DROPPED_NAMES = ["hooks", "agents"];
411
412
  SUPPORTED_EXTRAS = [".planning", "CLAUDE.md", ".claude"];
412
413
  ALWAYS_NEVER_SYNC = /* @__PURE__ */ new Set([
413
414
  ".claude.json",
@@ -420,12 +421,10 @@ var init_config = __esm({
420
421
  "shared/CLAUDE.md",
421
422
  "shared/my-statusline.cjs",
422
423
  "shared/settings.base.json",
423
- "shared/agents/",
424
424
  "shared/skills/",
425
425
  "shared/commands/",
426
426
  "shared/rules/",
427
427
  "shared/.gitignore",
428
- "shared/hooks/",
429
428
  "hosts/",
430
429
  "path-map.json",
431
430
  ".gitleaksignore",
@@ -571,6 +570,69 @@ var init_utils_fs = __esm({
571
570
  }
572
571
  });
573
572
 
573
+ // src/commands.pull.wedge.ts
574
+ import { execFileSync as execFileSync2 } from "node:child_process";
575
+ import { existsSync as existsSync10 } from "node:fs";
576
+ import { join as join11 } from "node:path";
577
+ function detectWedge(repo) {
578
+ const g = join11(repo, ".git");
579
+ if (existsSync10(join11(g, "rebase-merge")) || existsSync10(join11(g, "rebase-apply"))) return "rebase";
580
+ if (existsSync10(join11(g, "MERGE_HEAD"))) return "merge";
581
+ return null;
582
+ }
583
+ function unmergedIndexPresent(repo) {
584
+ let raw;
585
+ try {
586
+ raw = execFileSync2("git", ["diff", "--diff-filter=U", "--name-only", "-z"], {
587
+ cwd: repo,
588
+ stdio: ["ignore", "pipe", "pipe"],
589
+ maxBuffer: 64 * 1024 * 1024
590
+ }).toString();
591
+ } catch {
592
+ return false;
593
+ }
594
+ return raw.split("\0").some(Boolean);
595
+ }
596
+ function classifyWedge(repo) {
597
+ const mode = detectWedge(repo);
598
+ if (mode !== null) return mode;
599
+ return unmergedIndexPresent(repo) ? "unmerged-index" : null;
600
+ }
601
+ function unmergedIndexRunbookText(resumeCmd2) {
602
+ return `repo has an unmerged index with no active rebase or merge in progress (torn-down rebase left stage-2/3 entries behind).
603
+
604
+ Manual recovery:
605
+ 1. git reset --mixed HEAD (clears the stuck index; preserves working-tree files)
606
+ 2. git stash list (look for an orphaned autostash entry)
607
+ git stash pop (restore the autostash) or
608
+ git stash drop (discard it)
609
+ 3. ${resumeCmd2}
610
+
611
+ Auto-recover: run 'nomad pull --force-remote' to apply step 1 automatically
612
+ (see FAQ: "Every pull fails with unmerged files")`;
613
+ }
614
+ function wedgeMarkerRunbookText(state) {
615
+ return `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")`;
616
+ }
617
+ function orphanedAutostashPresent(repo) {
618
+ let raw;
619
+ try {
620
+ raw = execFileSync2("git", ["stash", "list"], {
621
+ cwd: repo,
622
+ stdio: ["ignore", "pipe", "pipe"],
623
+ maxBuffer: 64 * 1024 * 1024
624
+ }).toString();
625
+ } catch {
626
+ return false;
627
+ }
628
+ return raw.split("\n").some((line) => /:\s*autostash$/.test(line));
629
+ }
630
+ var init_commands_pull_wedge = __esm({
631
+ "src/commands.pull.wedge.ts"() {
632
+ "use strict";
633
+ }
634
+ });
635
+
574
636
  // src/push-gitleaks.config.ts
575
637
  import { existsSync as existsSync11, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
576
638
  import { tmpdir } from "node:os";
@@ -637,7 +699,7 @@ var init_push_gitleaks_config = __esm({
637
699
  });
638
700
 
639
701
  // src/push-checks.ts
640
- import { execFileSync as execFileSync2 } from "node:child_process";
702
+ import { execFileSync as execFileSync3 } from "node:child_process";
641
703
  import { readdirSync as readdirSync4, rmSync as rmSync4 } from "node:fs";
642
704
  import { homedir as homedir2, platform } from "node:os";
643
705
  import { join as join13 } from "node:path";
@@ -699,7 +761,7 @@ function probeGitleaks() {
699
761
  const args = ["version"];
700
762
  if (toml !== null) args.push("--config", toml);
701
763
  try {
702
- return execFileSync2("gitleaks", args, { stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
764
+ return execFileSync3("gitleaks", args, { stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
703
765
  } catch (err) {
704
766
  const e = err;
705
767
  if (e.code === "ENOENT") throw new NomadFatal(gitleaksInstallHint());
@@ -708,9 +770,18 @@ function probeGitleaks() {
708
770
  if (tempPath !== null) rmSync4(tempPath, { recursive: true, force: true });
709
771
  }
710
772
  }
773
+ function wedgePreflight(wedge) {
774
+ if (wedge === "unmerged-index") {
775
+ return unmergedIndexRunbookText("nomad push");
776
+ }
777
+ const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
778
+ return wedgeMarkerRunbookText(state);
779
+ }
711
780
  function rebaseBeforePush(repo) {
781
+ const wedge = classifyWedge(repo);
782
+ if (wedge !== null) throw new NomadFatal(wedgePreflight(wedge));
712
783
  try {
713
- execFileSync2("git", ["pull", "--rebase", "--autostash"], {
784
+ execFileSync3("git", ["pull", "--rebase", "--autostash"], {
714
785
  cwd: repo,
715
786
  stdio: ["ignore", "pipe", "pipe"]
716
787
  });
@@ -726,12 +797,13 @@ var init_push_checks = __esm({
726
797
  "src/push-checks.ts"() {
727
798
  "use strict";
728
799
  init_push_gitleaks_config();
800
+ init_commands_pull_wedge();
729
801
  init_utils();
730
802
  }
731
803
  });
732
804
 
733
805
  // src/push-gitleaks.scan.ts
734
- import { execFileSync as execFileSync5 } from "node:child_process";
806
+ import { execFileSync as execFileSync6 } from "node:child_process";
735
807
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync5 } from "node:fs";
736
808
  import { homedir as homedir3 } from "node:os";
737
809
  import { join as join17 } from "node:path";
@@ -761,9 +833,9 @@ function scanStagedTree(repoDir, forwardStreams = false) {
761
833
  if (toml !== null) args.push("--config", toml);
762
834
  const opts = { cwd: repoDir, stdio: ["ignore", "pipe", "pipe"] };
763
835
  try {
764
- execFileSync5("git", ["init", "-q"], opts);
765
- execFileSync5("git", ["add", "-A"], opts);
766
- execFileSync5("gitleaks", args, opts);
836
+ execFileSync6("git", ["init", "-q"], opts);
837
+ execFileSync6("git", ["add", "-A"], opts);
838
+ execFileSync6("gitleaks", args, opts);
767
839
  return [];
768
840
  } catch (err) {
769
841
  const e = err;
@@ -795,7 +867,7 @@ function scanFile(filePath, forwardStreams = false) {
795
867
  if (toml !== null) args.push("--config", toml);
796
868
  const opts = { stdio: ["ignore", "pipe", "pipe"] };
797
869
  try {
798
- execFileSync5("gitleaks", args, opts);
870
+ execFileSync6("gitleaks", args, opts);
799
871
  return [];
800
872
  } catch (err) {
801
873
  const e = err;
@@ -1618,6 +1690,23 @@ function reportSharedLinks(section2, map) {
1618
1690
  if (fail2) process.exitCode = 1;
1619
1691
  }
1620
1692
  }
1693
+ function reportDroppedNamesMigration(section2) {
1694
+ const claude = claudeHome();
1695
+ for (const name of GSD_DROPPED_NAMES) {
1696
+ const p = join8(claude, name);
1697
+ let stat;
1698
+ try {
1699
+ stat = lstatSync5(p);
1700
+ } catch {
1701
+ continue;
1702
+ }
1703
+ if (!stat.isSymbolicLink()) continue;
1704
+ addItem(
1705
+ section2,
1706
+ `${yellow(warnGlyph)} ${name}: gsd now owns this dir per-host (was a nomad symlink); run \`rm ~/.claude/${name}\` and let gsd reinstall a real dir`
1707
+ );
1708
+ }
1709
+ }
1621
1710
 
1622
1711
  // src/commands.doctor.checks.settings.ts
1623
1712
  init_color();
@@ -1788,26 +1877,15 @@ function reportNeverSync(section2) {
1788
1877
  // src/commands.doctor.checks.repository.ts
1789
1878
  init_color();
1790
1879
  init_config();
1791
- import { execFileSync as execFileSync3 } from "node:child_process";
1880
+ import { execFileSync as execFileSync4 } from "node:child_process";
1792
1881
  import { existsSync as existsSync12 } from "node:fs";
1793
1882
  import { join as join14, relative as relative2 } from "node:path";
1794
-
1795
- // src/commands.pull.wedge.ts
1796
- import { existsSync as existsSync10 } from "node:fs";
1797
- import { join as join11 } from "node:path";
1798
- function detectWedge(repo) {
1799
- const g = join11(repo, ".git");
1800
- if (existsSync10(join11(g, "rebase-merge")) || existsSync10(join11(g, "rebase-apply"))) return "rebase";
1801
- if (existsSync10(join11(g, "MERGE_HEAD"))) return "merge";
1802
- return null;
1803
- }
1804
-
1805
- // src/commands.doctor.checks.repository.ts
1883
+ init_commands_pull_wedge();
1806
1884
  init_push_checks();
1807
1885
  init_utils();
1808
1886
  function reportGitleaksProbe(section2) {
1809
1887
  try {
1810
- execFileSync3("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
1888
+ execFileSync4("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
1811
1889
  return true;
1812
1890
  } catch (err) {
1813
1891
  if (err.code === "ENOENT") {
@@ -1843,7 +1921,7 @@ function reportGitlinks(section2) {
1843
1921
  }
1844
1922
  function reportRemote(section2) {
1845
1923
  try {
1846
- const url = execFileSync3("git", ["remote", "get-url", "origin"], {
1924
+ const url = execFileSync4("git", ["remote", "get-url", "origin"], {
1847
1925
  cwd: repoHome(),
1848
1926
  stdio: ["ignore", "pipe", "pipe"]
1849
1927
  }).toString().trim();
@@ -1866,14 +1944,31 @@ function reportRebaseClean(section2) {
1866
1944
  }
1867
1945
  function reportRebaseState(section2) {
1868
1946
  try {
1869
- const wedge = detectWedge(repoHome());
1870
- if (wedge !== null) {
1947
+ const wedge = classifyWedge(repoHome());
1948
+ if (wedge === null) return;
1949
+ if (wedge === "unmerged-index") {
1950
+ addItem(
1951
+ section2,
1952
+ `${red(failGlyph)} repo has an unmerged index with no active rebase: run 'nomad pull --force-remote' to auto-recover`
1953
+ );
1954
+ } else {
1871
1955
  const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
1872
1956
  addItem(
1873
1957
  section2,
1874
1958
  `${red(failGlyph)} repo is ${state}: run 'nomad pull --force-remote' to auto-recover`
1875
1959
  );
1876
- process.exitCode = 1;
1960
+ }
1961
+ process.exitCode = 1;
1962
+ } catch {
1963
+ }
1964
+ }
1965
+ function reportOrphanedAutostash(sec) {
1966
+ try {
1967
+ if (orphanedAutostashPresent(repoHome())) {
1968
+ addItem(
1969
+ sec,
1970
+ `${yellow(warnGlyph)} repo has an orphaned autostash entry: run 'git stash pop' to restore or 'git stash drop' to discard`
1971
+ );
1877
1972
  }
1878
1973
  } catch {
1879
1974
  }
@@ -1920,7 +2015,7 @@ function reportBackupsCheck(section2, backupBase2 = backupBase()) {
1920
2015
  if (count > DOCTOR_BACKUP_COUNT_WARN || sizeMb > DOCTOR_BACKUP_SIZE_WARN_MB) {
1921
2016
  addItem(
1922
2017
  section2,
1923
- `${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups')`
2018
+ `${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups --keep <N>'; bare --backups only prunes dirs older than 14d)`
1924
2019
  );
1925
2020
  }
1926
2021
  }
@@ -1932,9 +2027,9 @@ import { join as join16 } from "node:path";
1932
2027
  init_config();
1933
2028
 
1934
2029
  // src/http-fetch.ts
1935
- import { execFileSync as execFileSync4 } from "node:child_process";
2030
+ import { execFileSync as execFileSync5 } from "node:child_process";
1936
2031
  var FETCH_TIMEOUT_MS = 3e3;
1937
- function fetchUrl(url, run = execFileSync4) {
2032
+ function fetchUrl(url, run = execFileSync5) {
1938
2033
  try {
1939
2034
  return run("curl", ["-fsSL", "-m", "3", url], {
1940
2035
  stdio: ["ignore", "pipe", "pipe"],
@@ -1995,7 +2090,7 @@ function reportCheckSchema(section2) {
1995
2090
  // src/commands.doctor.check-shared.ts
1996
2091
  init_color();
1997
2092
  import { randomBytes } from "node:crypto";
1998
- import { execFileSync as execFileSync6 } from "node:child_process";
2093
+ import { execFileSync as execFileSync7 } from "node:child_process";
1999
2094
  import { existsSync as existsSync16, mkdirSync as mkdirSync4, readdirSync as readdirSync7, rmSync as rmSync7 } from "node:fs";
2000
2095
  import { homedir as homedir4 } from "node:os";
2001
2096
  import { join as join20 } from "node:path";
@@ -2265,7 +2360,7 @@ function buildScanTree(tmpRoot) {
2265
2360
  }
2266
2361
  function probeGitleaksForScan() {
2267
2362
  try {
2268
- execFileSync6("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
2363
+ execFileSync7("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
2269
2364
  return "ok";
2270
2365
  } catch (err) {
2271
2366
  if (err.code === "ENOENT") return "missing";
@@ -3669,7 +3764,7 @@ function withSpinner(label, fn, deps) {
3669
3764
 
3670
3765
  // src/commands.doctor.gitleaks-version.ts
3671
3766
  init_color();
3672
- import { execFileSync as execFileSync7 } from "node:child_process";
3767
+ import { execFileSync as execFileSync8 } from "node:child_process";
3673
3768
  import { existsSync as existsSync26 } from "node:fs";
3674
3769
  import { join as join32 } from "node:path";
3675
3770
  init_config();
@@ -3692,7 +3787,7 @@ function readGitleaksVersion(run, tomlExists) {
3692
3787
  return null;
3693
3788
  }
3694
3789
  }
3695
- function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync26) {
3790
+ function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists = existsSync26) {
3696
3791
  const raw = readGitleaksVersion(run, tomlExists);
3697
3792
  if (raw === null) return;
3698
3793
  const local = majorMinorOf(raw);
@@ -3712,7 +3807,7 @@ function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists =
3712
3807
 
3713
3808
  // src/commands.doctor.checks.deps.ts
3714
3809
  init_color();
3715
- import { execFileSync as execFileSync8 } from "node:child_process";
3810
+ import { execFileSync as execFileSync9 } from "node:child_process";
3716
3811
  var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
3717
3812
  var PROBE_TIMEOUT_MS = 3e3;
3718
3813
  var FETCHER_BASE = "HTTP fetcher";
@@ -3749,7 +3844,7 @@ function reportFetcherRow(section2, run) {
3749
3844
  );
3750
3845
  }
3751
3846
  }
3752
- function reportOptionalDeps(section2, run = execFileSync8) {
3847
+ function reportOptionalDeps(section2, run = execFileSync9) {
3753
3848
  const gh = probeOptionalDep("gh", run);
3754
3849
  if (gh.status === "present") {
3755
3850
  addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
@@ -3764,11 +3859,11 @@ function reportOptionalDeps(section2, run = execFileSync8) {
3764
3859
 
3765
3860
  // src/commands.doctor.actions-drift.ts
3766
3861
  init_color();
3767
- import { execFileSync as execFileSync10 } from "node:child_process";
3862
+ import { execFileSync as execFileSync11 } from "node:child_process";
3768
3863
  init_config();
3769
3864
 
3770
3865
  // src/gh-actions.ts
3771
- import { execFileSync as execFileSync9 } from "node:child_process";
3866
+ import { execFileSync as execFileSync10 } from "node:child_process";
3772
3867
  var GH_TIMEOUT_MS = 5e3;
3773
3868
  function parseGitHubRemote(remoteUrl) {
3774
3869
  const normalized = remoteUrl.trim().replace(/\/$/, "");
@@ -3776,7 +3871,7 @@ function parseGitHubRemote(remoteUrl) {
3776
3871
  if (m === null) return null;
3777
3872
  return { owner: m[1], repo: m[2] };
3778
3873
  }
3779
- function ghAuthStatus(run = execFileSync9) {
3874
+ function ghAuthStatus(run = execFileSync10) {
3780
3875
  try {
3781
3876
  run("gh", ["auth", "status"], {
3782
3877
  stdio: ["ignore", "ignore", "ignore"],
@@ -3790,7 +3885,7 @@ function ghAuthStatus(run = execFileSync9) {
3790
3885
  return "gh-probe-error";
3791
3886
  }
3792
3887
  }
3793
- function isRepoPrivate(ref, run = execFileSync9) {
3888
+ function isRepoPrivate(ref, run = execFileSync10) {
3794
3889
  const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
3795
3890
  stdio: ["ignore", "pipe", "ignore"],
3796
3891
  timeout: GH_TIMEOUT_MS
@@ -3798,7 +3893,7 @@ function isRepoPrivate(ref, run = execFileSync9) {
3798
3893
  const parsed = JSON.parse(out);
3799
3894
  return parsed.isPrivate === true;
3800
3895
  }
3801
- function isActionsEnabled(ref, run = execFileSync9) {
3896
+ function isActionsEnabled(ref, run = execFileSync10) {
3802
3897
  const out = run(
3803
3898
  "gh",
3804
3899
  ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
@@ -3806,7 +3901,7 @@ function isActionsEnabled(ref, run = execFileSync9) {
3806
3901
  ).toString().trim();
3807
3902
  return out === "true";
3808
3903
  }
3809
- function disableActions(ref, run = execFileSync9) {
3904
+ function disableActions(ref, run = execFileSync10) {
3810
3905
  run(
3811
3906
  "gh",
3812
3907
  [
@@ -3820,7 +3915,7 @@ function disableActions(ref, run = execFileSync9) {
3820
3915
  { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
3821
3916
  );
3822
3917
  }
3823
- function readOriginRemote(cwd, run = execFileSync9) {
3918
+ function readOriginRemote(cwd, run = execFileSync10) {
3824
3919
  return run("git", ["remote", "get-url", "origin"], {
3825
3920
  cwd,
3826
3921
  stdio: ["ignore", "pipe", "ignore"]
@@ -3828,7 +3923,7 @@ function readOriginRemote(cwd, run = execFileSync9) {
3828
3923
  }
3829
3924
 
3830
3925
  // src/commands.doctor.actions-drift.ts
3831
- function reportActionsDrift(section2, run = execFileSync10) {
3926
+ function reportActionsDrift(section2, run = execFileSync11) {
3832
3927
  let remote;
3833
3928
  try {
3834
3929
  remote = readOriginRemote(repoHome(), run);
@@ -3896,6 +3991,7 @@ function gatherDoctorSections(opts) {
3896
3991
  const rawMap = existsSync27(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
3897
3992
  const map = rawMap ?? { projects: {} };
3898
3993
  reportSharedLinks(links, map);
3994
+ reportDroppedNamesMigration(links);
3899
3995
  const hooksScan = section("Hook targets");
3900
3996
  reportHooksTargetCheck(hooksScan);
3901
3997
  reportHookScopeCheck(hooksScan);
@@ -3915,6 +4011,7 @@ function gatherDoctorSections(opts) {
3915
4011
  reportRemote(repository);
3916
4012
  reportRebaseClean(repository);
3917
4013
  reportRebaseState(repository);
4014
+ reportOrphanedAutostash(repository);
3918
4015
  reportActionsDrift(repository);
3919
4016
  const nomadVersion = section("Nomad Version");
3920
4017
  reportVersionCheck(nomadVersion);
@@ -3958,15 +4055,15 @@ function cmdDoctor(opts = {}) {
3958
4055
 
3959
4056
  // src/commands.drop-session.ts
3960
4057
  init_config();
3961
- import { execFileSync as execFileSync12 } from "node:child_process";
4058
+ import { execFileSync as execFileSync13 } from "node:child_process";
3962
4059
  import { existsSync as existsSync29, readdirSync as readdirSync10, statSync as statSync8 } from "node:fs";
3963
4060
  import { join as join35, relative as relative4 } from "node:path";
3964
4061
 
3965
4062
  // src/commands.drop-session.git.ts
3966
- import { execFileSync as execFileSync11 } from "node:child_process";
4063
+ import { execFileSync as execFileSync12 } from "node:child_process";
3967
4064
  function expandStagedDir(dirRel, repo) {
3968
4065
  try {
3969
- const out = execFileSync11("git", ["ls-files", "-z", "--", dirRel], {
4066
+ const out = execFileSync12("git", ["ls-files", "-z", "--", dirRel], {
3970
4067
  cwd: repo,
3971
4068
  stdio: ["ignore", "pipe", "pipe"]
3972
4069
  });
@@ -3977,7 +4074,7 @@ function expandStagedDir(dirRel, repo) {
3977
4074
  }
3978
4075
  function isTrackedInHead(rel, repo) {
3979
4076
  try {
3980
- execFileSync11("git", ["cat-file", "-e", `HEAD:${rel}`], {
4077
+ execFileSync12("git", ["cat-file", "-e", `HEAD:${rel}`], {
3981
4078
  cwd: repo,
3982
4079
  stdio: ["ignore", "pipe", "pipe"]
3983
4080
  });
@@ -3988,7 +4085,7 @@ function isTrackedInHead(rel, repo) {
3988
4085
  }
3989
4086
  function isInIndex(rel, repo) {
3990
4087
  try {
3991
- const out = execFileSync11("git", ["ls-files", "--", rel], {
4088
+ const out = execFileSync12("git", ["ls-files", "--", rel], {
3992
4089
  cwd: repo,
3993
4090
  stdio: ["ignore", "pipe", "pipe"]
3994
4091
  });
@@ -4093,12 +4190,12 @@ function unstageOne(rel, repo) {
4093
4190
  }
4094
4191
  try {
4095
4192
  if (isTrackedInHead(rel, repo)) {
4096
- execFileSync12("git", ["restore", "--staged", "--worktree", "--", rel], {
4193
+ execFileSync13("git", ["restore", "--staged", "--worktree", "--", rel], {
4097
4194
  cwd: repo,
4098
4195
  stdio: ["ignore", "pipe", "pipe"]
4099
4196
  });
4100
4197
  } else {
4101
- execFileSync12("git", ["rm", "--cached", "-f", "--", rel], {
4198
+ execFileSync13("git", ["rm", "--cached", "-f", "--", rel], {
4102
4199
  cwd: repo,
4103
4200
  stdio: ["ignore", "pipe", "pipe"]
4104
4201
  });
@@ -4112,8 +4209,8 @@ function unstageOne(rel, repo) {
4112
4209
  }
4113
4210
 
4114
4211
  // src/commands.pull.ts
4115
- import { existsSync as existsSync35, mkdirSync as mkdirSync8 } from "node:fs";
4116
- import { join as join41 } from "node:path";
4212
+ import { existsSync as existsSync36, mkdirSync as mkdirSync9 } from "node:fs";
4213
+ import { join as join43 } from "node:path";
4117
4214
 
4118
4215
  // src/commands.push.sections.ts
4119
4216
  init_color();
@@ -4209,11 +4306,11 @@ init_config();
4209
4306
  // src/extras-sync.ts
4210
4307
  init_config();
4211
4308
  import { existsSync as existsSync32 } from "node:fs";
4212
- import { join as join38 } from "node:path";
4309
+ import { join as join39 } from "node:path";
4213
4310
 
4214
4311
  // src/extras-sync.diff.ts
4215
4312
  init_utils();
4216
- import { execFileSync as execFileSync13 } from "node:child_process";
4313
+ import { execFileSync as execFileSync14 } from "node:child_process";
4217
4314
  function labelDiffLine(line) {
4218
4315
  const tab = line.indexOf(" ");
4219
4316
  if (tab === -1) return line;
@@ -4228,7 +4325,7 @@ function parseDiffOutput(stdout) {
4228
4325
  }
4229
4326
  function listDivergingFiles(a, b) {
4230
4327
  try {
4231
- const stdout = execFileSync13("git", ["diff", "--no-index", "--name-status", a, b], {
4328
+ const stdout = execFileSync14("git", ["diff", "--no-index", "--name-status", a, b], {
4232
4329
  stdio: ["ignore", "pipe", "pipe"]
4233
4330
  }).toString();
4234
4331
  return parseDiffOutput(stdout);
@@ -4297,21 +4394,39 @@ function* eachExtrasTarget(v, counts) {
4297
4394
  counts.unmapped++;
4298
4395
  continue;
4299
4396
  }
4300
- for (const dirname7 of dirnames) {
4301
- if (!whitelist.includes(dirname7)) {
4397
+ for (const dirname8 of dirnames) {
4398
+ if (!whitelist.includes(dirname8)) {
4302
4399
  counts.skipped++;
4303
4400
  continue;
4304
4401
  }
4305
- yield { logical, localRoot, dirname: dirname7 };
4402
+ yield { logical, localRoot, dirname: dirname8 };
4306
4403
  }
4307
4404
  }
4308
4405
  }
4406
+ function copyExtrasOverlayFiltered(src, dst, blockSet) {
4407
+ try {
4408
+ cpSync6(src, dst, {
4409
+ recursive: true,
4410
+ force: true,
4411
+ verbatimSymlinks: true,
4412
+ filter: (srcEntry) => srcEntry === src || !blockSet.has(basename(srcEntry))
4413
+ });
4414
+ } catch (err) {
4415
+ const e = err;
4416
+ if (e.code === "EINVAL" || e.code === "ENOTEMPTY" || e.code === "ERR_FS_CP_NON_DIR_TO_DIR" || e.code === "ERR_FS_CP_DIR_TO_NON_DIR") {
4417
+ throw new NomadFatal(
4418
+ `copyExtrasOverlayFiltered: type collision copying ${JSON.stringify(src)} -> ${JSON.stringify(dst)} (${e.path ?? "unknown path"}): a file/directory type changed upstream; run nomad pull --force-remote to recover`
4419
+ );
4420
+ }
4421
+ throw err;
4422
+ }
4423
+ }
4309
4424
  function copyExtras(src, dst) {
4310
4425
  rmSync10(dst, { recursive: true, force: true });
4311
4426
  cpSync6(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
4312
4427
  }
4313
- function extrasDenySet(dirname7) {
4314
- return dirname7 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
4428
+ function extrasDenySet(dirname8) {
4429
+ return dirname8 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
4315
4430
  }
4316
4431
  function copyExtrasFiltered(src, dst, blockSet) {
4317
4432
  rmSync10(dst, { recursive: true, force: true });
@@ -4352,6 +4467,36 @@ function copyExtrasFilteredPreserving(src, dst, blockSet) {
4352
4467
  filter: (srcEntry) => srcEntry === src || !blockSet.has(basename(srcEntry))
4353
4468
  });
4354
4469
  }
4470
+ function prunePreservingBy(src, dst, isPreserved) {
4471
+ for (const name of readdirSync11(dst)) {
4472
+ if (isPreserved(name)) continue;
4473
+ const dstPath = join36(dst, name);
4474
+ const srcStat = lstatSync8(join36(src, name), { throwIfNoEntry: false });
4475
+ if (srcStat === void 0) {
4476
+ rmSync10(dstPath, { recursive: true, force: true });
4477
+ continue;
4478
+ }
4479
+ const dstStat = lstatSync8(dstPath);
4480
+ if (srcStat.isDirectory() && dstStat.isDirectory()) {
4481
+ prunePreservingBy(join36(src, name), dstPath, isPreserved);
4482
+ } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
4483
+ rmSync10(dstPath, { recursive: true, force: true });
4484
+ }
4485
+ }
4486
+ }
4487
+ function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
4488
+ const dstStat = lstatSync8(dst, { throwIfNoEntry: false });
4489
+ if (dstStat !== void 0) {
4490
+ if (dstStat.isDirectory()) prunePreservingBy(src, dst, isPreserved);
4491
+ else rmSync10(dst, { recursive: true, force: true });
4492
+ }
4493
+ cpSync6(src, dst, {
4494
+ recursive: true,
4495
+ force: true,
4496
+ verbatimSymlinks: true,
4497
+ filter: (srcEntry) => srcEntry === src || !isPreserved(basename(srcEntry))
4498
+ });
4499
+ }
4355
4500
 
4356
4501
  // src/extras-sync.ts
4357
4502
  init_utils();
@@ -4359,9 +4504,85 @@ init_utils_json();
4359
4504
 
4360
4505
  // src/extras-sync.remap.ts
4361
4506
  init_config();
4362
- import { existsSync as existsSync31, mkdirSync as mkdirSync7 } from "node:fs";
4363
- import { join as join37 } from "node:path";
4507
+ import { existsSync as existsSync31, mkdirSync as mkdirSync7, readdirSync as readdirSync12, realpathSync as realpathSync4, rmSync as rmSync11 } from "node:fs";
4508
+ import { dirname as dirname7, join as join38, sep as sep6 } from "node:path";
4509
+
4510
+ // src/extras-sync.planning-diff.ts
4511
+ import { join as join37, normalize as normalize2, sep as sep5 } from "node:path";
4512
+ init_utils();
4513
+ function processRecord(fields, i, changed, deleted) {
4514
+ const status = fields[i];
4515
+ const next = i + 1;
4516
+ if (status.startsWith("R")) {
4517
+ const oldPath = fields[next];
4518
+ const newPath = fields[next + 1];
4519
+ if (oldPath) deleted.push(oldPath);
4520
+ if (newPath) changed.push(newPath);
4521
+ return next + 2;
4522
+ }
4523
+ if (status.startsWith("C")) {
4524
+ const dstPath = fields[next + 1];
4525
+ if (dstPath) changed.push(dstPath);
4526
+ return next + 2;
4527
+ }
4528
+ const path = fields[next];
4529
+ if (path) {
4530
+ if (status === "D") {
4531
+ deleted.push(path);
4532
+ } else {
4533
+ changed.push(path);
4534
+ }
4535
+ }
4536
+ return next + 1;
4537
+ }
4538
+ function parsePlanningDiff(raw) {
4539
+ const changed = [];
4540
+ const deleted = [];
4541
+ if (raw === "") {
4542
+ return { changed, deleted };
4543
+ }
4544
+ const fields = raw.split("\0");
4545
+ let i = 0;
4546
+ while (i < fields.length) {
4547
+ const status = fields[i];
4548
+ if (!status) {
4549
+ i++;
4550
+ continue;
4551
+ }
4552
+ i = processRecord(fields, i, changed, deleted);
4553
+ }
4554
+ return { changed, deleted };
4555
+ }
4556
+ function planningDeleteTargets(opts) {
4557
+ const { raw, logical, localRoot } = opts;
4558
+ assertSafeLogical(logical);
4559
+ assertSafeLocalRoot(localRoot, logical);
4560
+ const { deleted } = parsePlanningDiff(raw);
4561
+ const logicalPrefix = "shared/extras/" + logical + "/";
4562
+ const prefix = logicalPrefix + ".planning/";
4563
+ const planningRoot = join37(localRoot, ".planning");
4564
+ const planningRootBoundary = planningRoot + sep5;
4565
+ const targets = [];
4566
+ for (const repoPath of deleted) {
4567
+ if (!repoPath.startsWith(prefix)) {
4568
+ continue;
4569
+ }
4570
+ const remainder = repoPath.slice(logicalPrefix.length);
4571
+ const candidate = join37(localRoot, remainder);
4572
+ const resolved = normalize2(candidate);
4573
+ if (!resolved.startsWith(planningRootBoundary)) {
4574
+ throw new NomadFatal(
4575
+ `planningDeleteTargets: resolved path ${JSON.stringify(resolved)} escapes localRoot/.planning for logical ${JSON.stringify(logical)} -- refusing delete`
4576
+ );
4577
+ }
4578
+ targets.push(resolved);
4579
+ }
4580
+ return targets;
4581
+ }
4582
+
4583
+ // src/extras-sync.remap.ts
4364
4584
  init_utils_fs();
4585
+ init_utils();
4365
4586
  function runExtrasOp(v, dryRun, paths, backup, copy) {
4366
4587
  const counts = { unmapped: 0, skipped: 0 };
4367
4588
  const done = [];
@@ -4380,56 +4601,139 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
4380
4601
  }
4381
4602
  return { ...counts, done, would };
4382
4603
  }
4604
+ function pruneEmptyAncestors(target, planningRoot) {
4605
+ let dir = dirname7(target);
4606
+ while (dir !== planningRoot && dir.startsWith(planningRoot + sep6)) {
4607
+ try {
4608
+ if (readdirSync12(dir).length > 0) break;
4609
+ rmSync11(dir, { recursive: true, force: true });
4610
+ } catch {
4611
+ break;
4612
+ }
4613
+ dir = dirname7(dir);
4614
+ }
4615
+ }
4616
+ function tryRealpath(dir) {
4617
+ try {
4618
+ return realpathSync4(dir);
4619
+ } catch {
4620
+ return void 0;
4621
+ }
4622
+ }
4623
+ function isInsidePlanningRoot(parentReal, rootReal) {
4624
+ return parentReal === rootReal || parentReal.startsWith(rootReal + sep6);
4625
+ }
4626
+ function deletePlanningTarget(target, planningRoot, repoCounterpart) {
4627
+ if (existsSync31(repoCounterpart)) return;
4628
+ const parentReal = tryRealpath(dirname7(target));
4629
+ if (parentReal === void 0) return;
4630
+ const rootReal = tryRealpath(planningRoot);
4631
+ if (rootReal === void 0) return;
4632
+ if (!isInsidePlanningRoot(parentReal, rootReal)) return;
4633
+ rmSync11(target, { recursive: true, force: true });
4634
+ pruneEmptyAncestors(target, planningRoot);
4635
+ }
4636
+ function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
4637
+ const repoExtras = join38(repo, "shared", "extras");
4638
+ for (const t of eachExtrasTarget(v, { unmapped: 0, skipped: 0 })) {
4639
+ if (t.dirname !== ".planning") continue;
4640
+ let raw;
4641
+ try {
4642
+ raw = gitCaptureRaw(
4643
+ [
4644
+ "diff",
4645
+ "--name-status",
4646
+ "-z",
4647
+ prePostHeads.pre,
4648
+ prePostHeads.post,
4649
+ "--",
4650
+ `shared/extras/${t.logical}/.planning/`
4651
+ ],
4652
+ repo
4653
+ );
4654
+ } catch (err) {
4655
+ const e = err;
4656
+ if (e.stderr) process.stderr.write(e.stderr);
4657
+ throw new NomadFatal(
4658
+ `git diff failed while propagating .planning deletes for ${t.logical}; run nomad pull --force-remote to recover`
4659
+ );
4660
+ }
4661
+ const targets = planningDeleteTargets({ raw, logical: t.logical, localRoot: t.localRoot });
4662
+ if (targets.length === 0) continue;
4663
+ backupExtrasWrite(join38(t.localRoot, t.dirname), ts, t.localRoot);
4664
+ const planningRoot = join38(t.localRoot, ".planning");
4665
+ for (const target of targets) {
4666
+ const relToLocal = target.slice(t.localRoot.length + sep6.length);
4667
+ deletePlanningTarget(target, planningRoot, join38(repoExtras, t.logical, relToLocal));
4668
+ }
4669
+ }
4670
+ }
4383
4671
  function remapExtrasPush(ts, opts = {}) {
4384
4672
  const dryRun = opts.dryRun === true;
4385
4673
  const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
4386
4674
  if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
4387
4675
  const repo = repoHome();
4388
- const repoExtras = join37(repo, "shared", "extras");
4676
+ const repoExtras = join38(repo, "shared", "extras");
4389
4677
  if (!dryRun) mkdirSync7(repoExtras, { recursive: true });
4390
4678
  const { unmapped, skipped, done, would } = runExtrasOp(
4391
4679
  v,
4392
4680
  dryRun,
4393
- ({ localRoot, logical, dirname: dirname7 }) => ({
4394
- src: join37(localRoot, dirname7),
4395
- dst: join37(repoExtras, logical, dirname7)
4681
+ ({ localRoot, logical, dirname: dirname8 }) => ({
4682
+ src: join38(localRoot, dirname8),
4683
+ dst: join38(repoExtras, logical, dirname8)
4396
4684
  }),
4397
4685
  (dst) => backupRepoWrite(dst, ts, repo),
4398
- // Push filters every extra by its per-name denylist: `.claude` gets the
4399
- // full NEVER_SYNC boundary, `.planning` keeps the narrow ALWAYS_NEVER_SYNC.
4400
- (src, dst, dirname7) => copyExtrasFiltered(src, dst, extrasDenySet(dirname7))
4686
+ // Push copy routing per extra type:
4687
+ // `.planning`: copyExtrasOverlayFiltered (no rmSync; deny-set filtered).
4688
+ // Repo-only files survive; local edits propagate (overlay overwrites).
4689
+ // The filter prevents ALWAYS_NEVER_SYNC files from landing in the repo
4690
+ // working tree before the allow-list gate fires, eliminating the
4691
+ // "residue wedges repeat push" regression (WR-02). The allow-list gate
4692
+ // (enforceAllowList / blockSetFor in commands.push.allowlist.ts)
4693
+ // remains the hard security boundary.
4694
+ // All others: copyExtrasFiltered with per-extra denylist.
4695
+ (src, dst, dirname8) => dirname8 === ".planning" ? copyExtrasOverlayFiltered(src, dst, extrasDenySet(dirname8)) : copyExtrasFiltered(src, dst, extrasDenySet(dirname8))
4401
4696
  );
4402
4697
  return { unmapped, skipped, pushed: done, wouldPush: would };
4403
4698
  }
4404
4699
  function remapExtrasPull(ts, opts = {}) {
4405
4700
  const dryRun = opts.dryRun === true;
4701
+ const { prePostHeads } = opts;
4406
4702
  const v = loadValidatedExtras({
4407
4703
  requireRepoExtras: true,
4408
4704
  missingMsg: "no path-map or repo extras dir; skipping extras remap"
4409
4705
  });
4410
4706
  if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
4411
- const repoExtras = join37(repoHome(), "shared", "extras");
4707
+ const repo = repoHome();
4412
4708
  const { unmapped, skipped, done, would } = runExtrasOp(
4413
4709
  v,
4414
4710
  dryRun,
4415
- ({ localRoot, logical, dirname: dirname7 }) => ({
4416
- src: join37(repoExtras, logical, dirname7),
4417
- dst: join37(localRoot, dirname7)
4711
+ ({ localRoot, logical, dirname: dirname8 }) => ({
4712
+ src: join38(repo, "shared", "extras", logical, dirname8),
4713
+ dst: join38(localRoot, dirname8)
4418
4714
  }),
4419
4715
  // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
4420
4716
  // localRoot so the backup tree mirrors the project layout.
4421
4717
  (dst, localRoot) => backupExtrasWrite(dst, ts, localRoot),
4422
- // Pull routes `.claude` through copyExtrasFilteredPreserving so host-local
4423
- // deny-set files already on disk (e.g. settings.local.json) are preserved
4424
- // instead of being wiped by a blanket rmSync. The same deny-set filter still
4425
- // strips blocked basenames from the src copy (defense-in-depth: a repo
4426
- // poisoned out-of-band cannot restore a blocked per-host file). Synced
4427
- // non-deny files that are absent from src are still mirror-pruned. This
4428
- // preservation is `.claude`-only; `.planning` and `CLAUDE.md` use the
4429
- // exact-mirror copyExtras (documented restore semantics; they rarely carry
4430
- // host-local files, so the exact mirror is the correct default).
4431
- (src, dst, dirname7) => dirname7 === ".claude" ? copyExtrasFilteredPreserving(src, dst, extrasDenySet(dirname7)) : copyExtras(src, dst)
4718
+ // Pull routing per extra type:
4719
+ // `.claude`: copyExtrasFilteredPreserving preserves host-local deny-set
4720
+ // files (e.g. settings.local.json) while mirror-pruning synced entries.
4721
+ // `.planning`: copyExtrasOverlayFiltered (no rmSync; deny-set filtered)
4722
+ // keeps local-only files; the delete pass below propagates upstream
4723
+ // removals via the git-diff D set. The filter is defense-in-depth
4724
+ // against a repo poisoned out-of-band.
4725
+ // All others: copyExtras (exact mirror; rarely carry host-local files).
4726
+ (src, dst, dirname8) => {
4727
+ if (dirname8 === ".claude")
4728
+ return copyExtrasFilteredPreserving(src, dst, extrasDenySet(dirname8));
4729
+ if (dirname8 === ".planning")
4730
+ return copyExtrasOverlayFiltered(src, dst, extrasDenySet(dirname8));
4731
+ return copyExtras(src, dst);
4732
+ }
4432
4733
  );
4734
+ if (!dryRun && prePostHeads !== void 0) {
4735
+ propagatePlanningDeletes(v, ts, prePostHeads, repo);
4736
+ }
4433
4737
  return { unmapped, skipped, pulled: done, wouldPull: would };
4434
4738
  }
4435
4739
 
@@ -4438,17 +4742,17 @@ function divergenceCheckExtras(ts) {
4438
4742
  const v = loadValidatedExtras({});
4439
4743
  if (v === null) return;
4440
4744
  const counts = { unmapped: 0, skipped: 0 };
4441
- const backupRoot = join38(backupBase(), ts, "extras");
4745
+ const backupRoot = join39(backupBase(), ts, "extras");
4442
4746
  const repo = repoHome();
4443
- for (const { logical, localRoot, dirname: dirname7 } of eachExtrasTarget(v, counts)) {
4444
- const local = join38(localRoot, dirname7);
4445
- const repoEntry = join38(repo, "shared", "extras", logical, dirname7);
4747
+ for (const { logical, localRoot, dirname: dirname8 } of eachExtrasTarget(v, counts)) {
4748
+ const local = join39(localRoot, dirname8);
4749
+ const repoEntry = join39(repo, "shared", "extras", logical, dirname8);
4446
4750
  if (!existsSync32(local) || !existsSync32(repoEntry)) continue;
4447
4751
  const diff = listDivergingFiles(local, repoEntry);
4448
4752
  if (diff.length === 0) continue;
4449
- const projectBackupRoot = join38(backupRoot, encodePath(localRoot));
4753
+ const projectBackupRoot = join39(backupRoot, encodePath(localRoot));
4450
4754
  warn(
4451
- `local ${dirname7} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
4755
+ `local ${dirname8} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will merge changes (.planning overlays, .claude/.CLAUDE.md mirror; backups at ${projectBackupRoot}/)`
4452
4756
  );
4453
4757
  for (const f of diff) warn(` ${f}`);
4454
4758
  }
@@ -4459,8 +4763,8 @@ init_config();
4459
4763
  init_utils();
4460
4764
  init_utils_fs();
4461
4765
  init_utils_json();
4462
- import { existsSync as existsSync33, lstatSync as lstatSync9, rmSync as rmSync11 } from "node:fs";
4463
- import { join as join39 } from "node:path";
4766
+ import { existsSync as existsSync33, lstatSync as lstatSync9, rmSync as rmSync12 } from "node:fs";
4767
+ import { join as join40 } from "node:path";
4464
4768
  function emitAutoMove(onPreview, linkPath, ts, name) {
4465
4769
  if (onPreview) {
4466
4770
  onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
@@ -4480,8 +4784,8 @@ function isAlreadySymlink(linkPath) {
4480
4784
  }
4481
4785
  function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
4482
4786
  for (const name of linkNames) {
4483
- const linkPath = join39(claude, name);
4484
- const target = join39(repo, "shared", name);
4787
+ const linkPath = join40(claude, name);
4788
+ const target = join40(repo, "shared", name);
4485
4789
  if (!existsSync33(linkPath)) continue;
4486
4790
  if (lstatSync9(linkPath).isSymbolicLink()) continue;
4487
4791
  if (!existsSync33(target)) continue;
@@ -4490,7 +4794,7 @@ function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
4490
4794
  continue;
4491
4795
  }
4492
4796
  backupBeforeWrite(linkPath, ts);
4493
- rmSync11(linkPath, { recursive: true, force: true });
4797
+ rmSync12(linkPath, { recursive: true, force: true });
4494
4798
  }
4495
4799
  }
4496
4800
  function applySharedLinks(ts, map, opts = {}) {
@@ -4500,9 +4804,9 @@ function applySharedLinks(ts, map, opts = {}) {
4500
4804
  const linkNames = allSharedLinks(map);
4501
4805
  runAutoMovePasses(linkNames, claude, repo, ts, dryRun, opts.onPreview);
4502
4806
  for (const name of linkNames) {
4503
- const target = join39(repo, "shared", name);
4807
+ const target = join40(repo, "shared", name);
4504
4808
  if (!existsSync33(target)) continue;
4505
- const linkPath = join39(claude, name);
4809
+ const linkPath = join40(claude, name);
4506
4810
  if (isAlreadySymlink(linkPath)) continue;
4507
4811
  if (dryRun) {
4508
4812
  emitCreate(opts.onPreview, linkPath, target);
@@ -4515,8 +4819,8 @@ function regenerateSettings(ts, opts = {}) {
4515
4819
  const dryRun = opts.dryRun === true;
4516
4820
  const repo = repoHome();
4517
4821
  const claude = claudeHome();
4518
- const basePath = join39(repo, "shared", "settings.base.json");
4519
- const hostPath = join39(repo, "hosts", `${HOST}.json`);
4822
+ const basePath = join40(repo, "shared", "settings.base.json");
4823
+ const hostPath = join40(repo, "hosts", `${HOST}.json`);
4520
4824
  if (!existsSync33(basePath)) {
4521
4825
  die("repo not initialized; run 'nomad init' to scaffold");
4522
4826
  }
@@ -4524,7 +4828,7 @@ function regenerateSettings(ts, opts = {}) {
4524
4828
  const hasOverrides = existsSync33(hostPath);
4525
4829
  const overrides = hasOverrides ? readJson(hostPath) : {};
4526
4830
  const merged = deepMerge(base, overrides);
4527
- const settingsPath = join39(claude, "settings.json");
4831
+ const settingsPath = join40(claude, "settings.json");
4528
4832
  if (!hasOverrides && existsSync33(settingsPath)) {
4529
4833
  try {
4530
4834
  const existing = readJson(settingsPath);
@@ -4549,10 +4853,53 @@ function regenerateSettings(ts, opts = {}) {
4549
4853
  return { label: overrideLabel };
4550
4854
  }
4551
4855
 
4856
+ // src/skills-sync.ts
4857
+ init_config();
4858
+ import { existsSync as existsSync34, lstatSync as lstatSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync13, rmSync as rmSync13 } from "node:fs";
4859
+ import { join as join41 } from "node:path";
4860
+ init_utils_fs();
4861
+ function isGsdOwned(name) {
4862
+ return name.startsWith(GSD_PREFIX);
4863
+ }
4864
+ function isSkillExcluded(name) {
4865
+ return isGsdOwned(name) || ALWAYS_NEVER_SYNC.has(name);
4866
+ }
4867
+ function copySkillsPush(src, dst) {
4868
+ const srcNames = readdirSync13(src, { encoding: "utf8" });
4869
+ const blockSet = /* @__PURE__ */ new Set([
4870
+ ...srcNames.filter((n) => isGsdOwned(n)),
4871
+ ...ALWAYS_NEVER_SYNC
4872
+ ]);
4873
+ copyExtrasFiltered(src, dst, blockSet);
4874
+ }
4875
+ function copySkillsPull(src, dst) {
4876
+ copyExtrasFilteredPreservingBy(src, dst, isSkillExcluded);
4877
+ }
4878
+ function syncSkillsPull(ts) {
4879
+ const sharedSkills = join41(repoHome(), "shared", "skills");
4880
+ if (!existsSync34(sharedSkills)) return;
4881
+ const localSkills = join41(claudeHome(), "skills");
4882
+ const dstStat = lstatSync10(localSkills, { throwIfNoEntry: false });
4883
+ if (dstStat?.isSymbolicLink() === true) {
4884
+ backupBeforeWrite(localSkills, ts);
4885
+ rmSync13(localSkills, { recursive: true, force: true });
4886
+ mkdirSync8(localSkills, { recursive: true });
4887
+ }
4888
+ copySkillsPull(sharedSkills, localSkills);
4889
+ }
4890
+ function syncSkillsPush() {
4891
+ const localSkills = join41(claudeHome(), "skills");
4892
+ const stat = lstatSync10(localSkills, { throwIfNoEntry: false });
4893
+ if (stat === void 0) return;
4894
+ if (stat.isSymbolicLink()) return;
4895
+ const sharedSkills = join41(repoHome(), "shared", "skills");
4896
+ copySkillsPush(localSkills, sharedSkills);
4897
+ }
4898
+
4552
4899
  // src/preview.ts
4553
4900
  init_config();
4554
- import { existsSync as existsSync34 } from "node:fs";
4555
- import { join as join40 } from "node:path";
4901
+ import { existsSync as existsSync35 } from "node:fs";
4902
+ import { join as join42 } from "node:path";
4556
4903
 
4557
4904
  // node_modules/diff/libesm/diff/base.js
4558
4905
  var Diff = class {
@@ -4838,7 +5185,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
4838
5185
  return lines.join("\n");
4839
5186
  }
4840
5187
  function readJsonOrNull(path) {
4841
- if (!existsSync34(path)) return null;
5188
+ if (!existsSync35(path)) return null;
4842
5189
  try {
4843
5190
  return readJson(path);
4844
5191
  } catch {
@@ -4852,12 +5199,12 @@ function previewSettings(basePath, hostPath, settingsPath) {
4852
5199
  }
4853
5200
  const notes = [];
4854
5201
  const hostOverrides = readJsonOrNull(hostPath);
4855
- if (hostOverrides === null && existsSync34(hostPath)) {
5202
+ if (hostOverrides === null && existsSync35(hostPath)) {
4856
5203
  notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
4857
5204
  }
4858
5205
  const merged = deepMerge(base, hostOverrides ?? {});
4859
5206
  const current = readJsonOrNull(settingsPath);
4860
- if (current === null && existsSync34(settingsPath)) {
5207
+ if (current === null && existsSync35(settingsPath)) {
4861
5208
  return { diff: "", notes: [...notes, "malformed; skipping diff"] };
4862
5209
  }
4863
5210
  const rawEqual = JSON.stringify(current ?? {}, null, 2) === JSON.stringify(merged, null, 2);
@@ -4897,9 +5244,9 @@ function computePreview(ts, map, verb = "pull") {
4897
5244
  onPreview: (e) => addItem(links, formatLinkRow(e))
4898
5245
  });
4899
5246
  const settingsResult = previewSettings(
4900
- join40(repo, "shared", "settings.base.json"),
4901
- join40(repo, "hosts", `${HOST}.json`),
4902
- join40(claude, "settings.json")
5247
+ join42(repo, "shared", "settings.base.json"),
5248
+ join42(repo, "hosts", `${HOST}.json`),
5249
+ join42(claude, "settings.json")
4903
5250
  );
4904
5251
  const settingsSection = buildSettingsSectionForPreview(settingsResult);
4905
5252
  const sessions = section("Sessions");
@@ -4913,13 +5260,21 @@ function computePreview(ts, map, verb = "pull") {
4913
5260
  return { unmapped: remapResult.unmapped, collisions: 0 };
4914
5261
  }
4915
5262
 
5263
+ // src/commands.pull.ts
5264
+ init_commands_pull_wedge();
5265
+
4916
5266
  // src/commands.pull.recovery.ts
4917
5267
  init_config();
4918
- import { execFileSync as execFileSync14 } from "node:child_process";
5268
+ init_commands_pull_wedge();
4919
5269
  init_utils();
4920
5270
  init_utils_fs();
5271
+ import { execFileSync as execFileSync15 } from "node:child_process";
4921
5272
  function gitCapture(args, cwd) {
4922
- return execFileSync14("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
5273
+ return execFileSync15("git", args, {
5274
+ cwd,
5275
+ stdio: ["ignore", "pipe", "pipe"],
5276
+ maxBuffer: 64 * 1024 * 1024
5277
+ }).toString().trim();
4923
5278
  }
4924
5279
  function isSyncedConfig(path) {
4925
5280
  return PUSH_ALLOWED_STATIC.some(
@@ -4990,6 +5345,20 @@ function freshStrandedBranch(repo) {
4990
5345
  while (exists(`${base}-${n}`)) n++;
4991
5346
  return `${base}-${n}`;
4992
5347
  }
5348
+ function recoverUnmergedIndex(repo) {
5349
+ gitOrFatal(["reset", "--mixed", "HEAD"], "git reset --mixed HEAD", repo);
5350
+ const dirty = gitCapture(["diff", "--name-only", "-z"], repo).split("\0").filter(Boolean);
5351
+ if (dirty.length > 0) {
5352
+ log(
5353
+ "index cleared, but these files still carry conflict content from the torn-down rebase; review and resolve before the next pull:\n" + dirty.map((p) => ` ${p}`).join("\n")
5354
+ );
5355
+ }
5356
+ if (orphanedAutostashPresent(repo)) {
5357
+ log(
5358
+ 'orphaned autostash preserved in the stash list; run "git stash pop" to restore or "git stash drop" to discard it, then re-run "nomad pull"'
5359
+ );
5360
+ }
5361
+ }
4993
5362
  function recoverForceRemote(mode, repo) {
4994
5363
  if (mode === "merge") {
4995
5364
  gitOrFatal(["merge", "--abort"], "git merge --abort", repo);
@@ -5023,11 +5392,26 @@ function recoverForceRemote(mode, repo) {
5023
5392
  init_utils();
5024
5393
  init_utils_fs();
5025
5394
  init_utils_json();
5026
- function applyWetPull(ts, map) {
5395
+ function captureHead(repo) {
5396
+ try {
5397
+ return gitCaptureRaw(["rev-parse", "HEAD"], repo).trim();
5398
+ } catch {
5399
+ return void 0;
5400
+ }
5401
+ }
5402
+ function capturePrePostHeads(repo, rebase) {
5403
+ const pre = captureHead(repo);
5404
+ rebase();
5405
+ const post = captureHead(repo);
5406
+ if (pre === void 0 || post === void 0) return void 0;
5407
+ return { pre, post };
5408
+ }
5409
+ function applyWetPull(ts, map, prePostHeads) {
5027
5410
  applySharedLinks(ts, map);
5028
5411
  const { label } = regenerateSettings(ts);
5412
+ syncSkillsPull(ts);
5029
5413
  const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
5030
- const extrasResult = remapExtrasPull(ts);
5414
+ const extrasResult = remapExtrasPull(ts, { prePostHeads });
5031
5415
  const summary = section("Summary");
5032
5416
  addItem(
5033
5417
  summary,
@@ -5041,24 +5425,30 @@ function applyWetPull(ts, map) {
5041
5425
  ]);
5042
5426
  }
5043
5427
  function handleWedge(repo, forceRemote) {
5044
- const wedge = detectWedge(repo);
5428
+ const wedge = classifyWedge(repo);
5045
5429
  if (wedge === null) return;
5430
+ if (wedge === "unmerged-index") {
5431
+ if (forceRemote) {
5432
+ recoverUnmergedIndex(repo);
5433
+ } else {
5434
+ die(unmergedIndexRunbookText("nomad pull"));
5435
+ }
5436
+ return;
5437
+ }
5046
5438
  if (forceRemote) {
5047
5439
  recoverForceRemote(wedge, repo);
5048
5440
  return;
5049
5441
  }
5050
5442
  const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
5051
- die(
5052
- `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")`
5053
- );
5443
+ die(wedgeMarkerRunbookText(state));
5054
5444
  }
5055
5445
  function cmdPull(opts = {}) {
5056
5446
  const dryRun = opts.dryRun === true;
5057
5447
  const forceRemote = opts.forceRemote === true;
5058
5448
  const repo = repoHome();
5059
5449
  const backup = backupBase();
5060
- if (!existsSync35(repo)) die(`repo not cloned at ${repo}`);
5061
- if (!existsSync35(join41(repo, "shared", "settings.base.json"))) {
5450
+ if (!existsSync36(repo)) die(`repo not cloned at ${repo}`);
5451
+ if (!existsSync36(join43(repo, "shared", "settings.base.json"))) {
5062
5452
  die("repo not initialized; run 'nomad init' to scaffold");
5063
5453
  }
5064
5454
  const handle = acquireLock("pull");
@@ -5067,9 +5457,9 @@ function cmdPull(opts = {}) {
5067
5457
  const ts = freshBackupTs(backup);
5068
5458
  handleWedge(repo, forceRemote);
5069
5459
  if (!dryRun) {
5070
- const backupRoot = join41(backup, ts);
5460
+ const backupRoot = join43(backup, ts);
5071
5461
  try {
5072
- mkdirSync8(backupRoot, { recursive: true });
5462
+ mkdirSync9(backupRoot, { recursive: true });
5073
5463
  } catch (err) {
5074
5464
  die(`could not create backup dir: ${err.message}`);
5075
5465
  }
@@ -5077,15 +5467,17 @@ function cmdPull(opts = {}) {
5077
5467
  log(
5078
5468
  dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
5079
5469
  );
5080
- gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
5081
- const mapPath = join41(repo, "path-map.json");
5082
- const map = existsSync35(mapPath) ? readPathMap(mapPath) : { projects: {} };
5470
+ const prePostHeads = capturePrePostHeads(repo, () => {
5471
+ gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
5472
+ });
5473
+ const mapPath = join43(repo, "path-map.json");
5474
+ const map = existsSync36(mapPath) ? readPathMap(mapPath) : { projects: {} };
5083
5475
  divergenceCheckExtras(ts);
5084
5476
  if (dryRun) {
5085
5477
  computePreview(ts, map, "pull");
5086
5478
  log("dry-run complete; no mutation");
5087
5479
  } else {
5088
- applyWetPull(ts, map);
5480
+ applyWetPull(ts, map, prePostHeads);
5089
5481
  }
5090
5482
  } catch (err) {
5091
5483
  if (err instanceof NomadFatal) {
@@ -5101,8 +5493,8 @@ function cmdPull(opts = {}) {
5101
5493
 
5102
5494
  // src/commands.push.ts
5103
5495
  init_config();
5104
- import { existsSync as existsSync37 } from "node:fs";
5105
- import { join as join43, relative as relative5 } from "node:path";
5496
+ import { existsSync as existsSync38 } from "node:fs";
5497
+ import { join as join45, relative as relative5 } from "node:path";
5106
5498
 
5107
5499
  // src/commands.push.allowlist.ts
5108
5500
  init_config();
@@ -5181,7 +5573,7 @@ function enforceAllowList(statusPorcelain, map) {
5181
5573
 
5182
5574
  // src/push-global-config.ts
5183
5575
  init_config();
5184
- import { execFileSync as execFileSync15 } from "node:child_process";
5576
+ import { execFileSync as execFileSync16 } from "node:child_process";
5185
5577
  var STATUS_LABELS = {
5186
5578
  A: "add",
5187
5579
  M: "modify",
@@ -5208,6 +5600,7 @@ function buildPrefixSets(hostname2) {
5208
5600
  }
5209
5601
  }
5210
5602
  exactPrefixes.add("shared/settings.base.json");
5603
+ dirPrefixes.push("shared/skills");
5211
5604
  exactPrefixes.add(`hosts/${hostname2}.json`);
5212
5605
  return { exactPrefixes, dirPrefixes };
5213
5606
  }
@@ -5220,7 +5613,7 @@ function isInScope(filePath, exactPrefixes, dirPrefixes) {
5220
5613
  }
5221
5614
  function collectGlobalConfigChanges(repoHome2, hostname2, opts) {
5222
5615
  const args = opts.staged ? ["diff", "--cached", "--name-status", "-z"] : ["diff", "HEAD", "--name-status", "-z"];
5223
- const raw = execFileSync15("git", args, {
5616
+ const raw = execFileSync16("git", args, {
5224
5617
  cwd: repoHome2,
5225
5618
  stdio: ["ignore", "pipe", "pipe"]
5226
5619
  }).toString();
@@ -5259,9 +5652,9 @@ init_color();
5259
5652
  init_config();
5260
5653
  init_config_sharedDirs_guard();
5261
5654
  import { randomBytes as randomBytes2 } from "node:crypto";
5262
- import { copyFileSync, existsSync as existsSync36, mkdirSync as mkdirSync9, readdirSync as readdirSync12, rmSync as rmSync12 } from "node:fs";
5655
+ import { copyFileSync, existsSync as existsSync37, mkdirSync as mkdirSync10, readdirSync as readdirSync14, rmSync as rmSync14 } from "node:fs";
5263
5656
  import { homedir as homedir5 } from "node:os";
5264
- import { join as join42 } from "node:path";
5657
+ import { join as join44 } from "node:path";
5265
5658
  init_push_leak_verdict();
5266
5659
  init_push_gitleaks();
5267
5660
  init_utils_fs();
@@ -5276,13 +5669,13 @@ function stageSessions(tmpRoot, map) {
5276
5669
  if (!p || p === "TBD") continue;
5277
5670
  reverse.set(encodePath(p), logical);
5278
5671
  }
5279
- const localProjects = join42(claudeHome(), "projects");
5280
- if (!existsSync36(localProjects)) return 0;
5672
+ const localProjects = join44(claudeHome(), "projects");
5673
+ if (!existsSync37(localProjects)) return 0;
5281
5674
  let staged = 0;
5282
- for (const dir of readdirSync12(localProjects)) {
5675
+ for (const dir of readdirSync14(localProjects)) {
5283
5676
  const logical = reverse.get(dir);
5284
5677
  if (!logical) continue;
5285
- copyDirJsonlOnly(join42(localProjects, dir), join42(tmpRoot, "shared", "projects", logical));
5678
+ copyDirJsonlOnly(join44(localProjects, dir), join44(tmpRoot, "shared", "projects", logical));
5286
5679
  staged++;
5287
5680
  }
5288
5681
  return staged;
@@ -5296,11 +5689,11 @@ function stageExtras(tmpRoot, map) {
5296
5689
  assertSafeLogical(logical);
5297
5690
  const localRoot = map.projects[logical]?.[HOST];
5298
5691
  if (!localRoot || localRoot === "TBD") continue;
5299
- for (const dirname7 of dirnames) {
5300
- if (!whitelist.includes(dirname7)) continue;
5301
- const src = join42(localRoot, dirname7);
5302
- if (!existsSync36(src)) continue;
5303
- const dst = join42(tmpRoot, "shared", "extras", logical, dirname7);
5692
+ for (const dirname8 of dirnames) {
5693
+ if (!whitelist.includes(dirname8)) continue;
5694
+ const src = join44(localRoot, dirname8);
5695
+ if (!existsSync37(src)) continue;
5696
+ const dst = join44(tmpRoot, "shared", "extras", logical, dirname8);
5304
5697
  copyExtras(src, dst);
5305
5698
  staged++;
5306
5699
  }
@@ -5308,19 +5701,19 @@ function stageExtras(tmpRoot, map) {
5308
5701
  return staged;
5309
5702
  }
5310
5703
  function previewPushLeaks(map) {
5311
- const cacheDir = join42(homedir5(), ".cache", "claude-nomad");
5312
- mkdirSync9(cacheDir, { recursive: true });
5704
+ const cacheDir = join44(homedir5(), ".cache", "claude-nomad");
5705
+ mkdirSync10(cacheDir, { recursive: true });
5313
5706
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
5314
- const tmpRoot = join42(cacheDir, `push-preview-tree-${stamp}`);
5707
+ const tmpRoot = join44(cacheDir, `push-preview-tree-${stamp}`);
5315
5708
  try {
5316
5709
  const sessionCount = stageSessions(tmpRoot, map);
5317
5710
  const extrasCount = stageExtras(tmpRoot, map);
5318
5711
  if (sessionCount + extrasCount === 0) {
5319
5712
  return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
5320
5713
  }
5321
- const ignoreFile = join42(repoHome(), ".gitleaksignore");
5322
- if (existsSync36(ignoreFile)) {
5323
- copyFileSync(ignoreFile, join42(tmpRoot, ".gitleaksignore"));
5714
+ const ignoreFile = join44(repoHome(), ".gitleaksignore");
5715
+ if (existsSync37(ignoreFile)) {
5716
+ copyFileSync(ignoreFile, join44(tmpRoot, ".gitleaksignore"));
5324
5717
  }
5325
5718
  let findings;
5326
5719
  try {
@@ -5333,7 +5726,7 @@ function previewPushLeaks(map) {
5333
5726
  }
5334
5727
  return verdictFromFindings(findings);
5335
5728
  } finally {
5336
- rmSync12(tmpRoot, { recursive: true, force: true });
5729
+ rmSync14(tmpRoot, { recursive: true, force: true });
5337
5730
  }
5338
5731
  }
5339
5732
 
@@ -5342,7 +5735,7 @@ init_utils();
5342
5735
  init_utils_fs();
5343
5736
  init_utils_json();
5344
5737
  function guardGitlinks(repo) {
5345
- const gitlinks = findGitlinks(join43(repo, "shared"));
5738
+ const gitlinks = findGitlinks(join45(repo, "shared"));
5346
5739
  if (gitlinks.length === 0) return;
5347
5740
  for (const p of gitlinks) {
5348
5741
  const rel = relative5(repo, p);
@@ -5398,7 +5791,7 @@ async function cmdPush(opts = {}) {
5398
5791
  guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
5399
5792
  const repo = repoHome();
5400
5793
  const backup = backupBase();
5401
- if (!existsSync37(repo)) die(`repo not cloned at ${repo}`);
5794
+ if (!existsSync38(repo)) die(`repo not cloned at ${repo}`);
5402
5795
  const handle = acquireLock("push");
5403
5796
  if (handle === null) process.exit(0);
5404
5797
  try {
@@ -5408,6 +5801,7 @@ async function cmdPush(opts = {}) {
5408
5801
  const ts = freshBackupTs(backup);
5409
5802
  const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun }));
5410
5803
  const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
5804
+ if (!dryRun) syncSkillsPush();
5411
5805
  const st = { dryRun, remap, extras, globalConfig: [] };
5412
5806
  guardGitlinks(repo);
5413
5807
  const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
@@ -5416,8 +5810,8 @@ async function cmdPush(opts = {}) {
5416
5810
  renderNoScanTree(st);
5417
5811
  return;
5418
5812
  }
5419
- const mapPath = join43(repo, "path-map.json");
5420
- if (!existsSync37(mapPath)) {
5813
+ const mapPath = join45(repo, "path-map.json");
5814
+ if (!existsSync38(mapPath)) {
5421
5815
  if (dryRun) return runDryRunPreview(st, null, repo);
5422
5816
  die("path-map.json missing, cannot enforce push allow-list");
5423
5817
  }
@@ -5438,16 +5832,16 @@ async function cmdPush(opts = {}) {
5438
5832
  }
5439
5833
 
5440
5834
  // src/commands.update.ts
5441
- import { execFileSync as execFileSync16 } from "node:child_process";
5835
+ import { execFileSync as execFileSync17 } from "node:child_process";
5442
5836
  init_utils();
5443
- function readInstalledVersion(run = execFileSync16) {
5837
+ function readInstalledVersion(run = execFileSync17) {
5444
5838
  try {
5445
5839
  return run("nomad", ["--version"], { encoding: "utf8" }).toString().trim() || null;
5446
5840
  } catch {
5447
5841
  return null;
5448
5842
  }
5449
5843
  }
5450
- function cmdUpdate(run = execFileSync16) {
5844
+ function cmdUpdate(run = execFileSync17) {
5451
5845
  console.log("Updating claude-nomad CLI via npm...");
5452
5846
  try {
5453
5847
  run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
@@ -5471,18 +5865,18 @@ init_config();
5471
5865
 
5472
5866
  // src/diff.ts
5473
5867
  init_config();
5474
- import { existsSync as existsSync38 } from "node:fs";
5475
- import { join as join44 } from "node:path";
5868
+ import { existsSync as existsSync39 } from "node:fs";
5869
+ import { join as join46 } from "node:path";
5476
5870
  init_utils();
5477
5871
  init_utils_fs();
5478
5872
  init_utils_json();
5479
5873
  function cmdDiff() {
5480
5874
  try {
5481
5875
  const repo = repoHome();
5482
- if (!existsSync38(repo)) die(`repo not cloned at ${repo}`);
5876
+ if (!existsSync39(repo)) die(`repo not cloned at ${repo}`);
5483
5877
  const ts = freshBackupTs(backupBase());
5484
- const mapPath = join44(repo, "path-map.json");
5485
- const map = existsSync38(mapPath) ? readPathMap(mapPath) : { projects: {} };
5878
+ const mapPath = join46(repo, "path-map.json");
5879
+ const map = existsSync39(mapPath) ? readPathMap(mapPath) : { projects: {} };
5486
5880
  computePreview(ts, map, "diff");
5487
5881
  } catch (err) {
5488
5882
  if (err instanceof NomadFatal) {
@@ -5496,19 +5890,19 @@ function cmdDiff() {
5496
5890
 
5497
5891
  // src/init.ts
5498
5892
  init_config();
5499
- import { existsSync as existsSync40, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
5500
- import { join as join46 } from "node:path";
5893
+ import { existsSync as existsSync41, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6 } from "node:fs";
5894
+ import { join as join48 } from "node:path";
5501
5895
 
5502
5896
  // src/init.gh-onboard.ts
5503
5897
  init_config();
5504
- import { execFileSync as execFileSync17 } from "node:child_process";
5898
+ import { execFileSync as execFileSync18 } from "node:child_process";
5505
5899
  init_utils();
5506
5900
  var DEFAULT_REPO_NAME = "claude-nomad-config";
5507
5901
  function isValidRepoName(name) {
5508
5902
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
5509
5903
  }
5510
5904
  var GH_NETWORK_TIMEOUT_MS = 3e4;
5511
- function ensureOriginRepo(repoName, run = execFileSync17) {
5905
+ function ensureOriginRepo(repoName, run = execFileSync18) {
5512
5906
  if (!isValidRepoName(repoName)) {
5513
5907
  die(
5514
5908
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -5579,33 +5973,33 @@ init_config();
5579
5973
  init_utils();
5580
5974
  init_utils_fs();
5581
5975
  init_utils_json();
5582
- import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync39, rmSync as rmSync13, statSync as statSync9 } from "node:fs";
5583
- import { join as join45 } from "node:path";
5976
+ import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync40, rmSync as rmSync15, statSync as statSync9 } from "node:fs";
5977
+ import { join as join47 } from "node:path";
5584
5978
  function snapshotIntoShared(map) {
5585
5979
  const repo = repoHome();
5586
5980
  const claude = claudeHome();
5587
5981
  for (const name of allSharedLinks(map)) {
5588
- const src = join45(claude, name);
5589
- if (!existsSync39(src)) continue;
5590
- const dst = join45(repo, "shared", name);
5982
+ const src = join47(claude, name);
5983
+ if (!existsSync40(src)) continue;
5984
+ const dst = join47(repo, "shared", name);
5591
5985
  if (statSync9(src).isDirectory()) {
5592
- const gk = join45(dst, ".gitkeep");
5593
- if (existsSync39(gk)) rmSync13(gk);
5986
+ const gk = join47(dst, ".gitkeep");
5987
+ if (existsSync40(gk)) rmSync15(gk);
5594
5988
  cpSync7(src, dst, { recursive: true, force: false, errorOnExist: true });
5595
5989
  } else {
5596
5990
  copyFileSync2(src, dst);
5597
5991
  }
5598
5992
  log(`snapshotted shared/${name} from ${src}`);
5599
5993
  }
5600
- const userSettings = join45(claude, "settings.json");
5601
- if (existsSync39(userSettings)) {
5994
+ const userSettings = join47(claude, "settings.json");
5995
+ if (existsSync40(userSettings)) {
5602
5996
  let parsed;
5603
5997
  try {
5604
5998
  parsed = readJson(userSettings);
5605
5999
  } catch (err) {
5606
6000
  return die(`malformed ${userSettings}: ${err.message}`);
5607
6001
  }
5608
- const hostFile = join45(repo, "hosts", `${HOST}.json`);
6002
+ const hostFile = join47(repo, "hosts", `${HOST}.json`);
5609
6003
  writeJsonAtomic(hostFile, parsed);
5610
6004
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
5611
6005
  }
@@ -5618,14 +6012,14 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
5618
6012
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
5619
6013
  function preflightConflict(repoHome2) {
5620
6014
  const candidates = [
5621
- join46(repoHome2, "shared", "settings.base.json"),
5622
- join46(repoHome2, "shared", "CLAUDE.md"),
5623
- join46(repoHome2, "path-map.json"),
5624
- join46(repoHome2, "hosts"),
5625
- join46(repoHome2, "shared")
6015
+ join48(repoHome2, "shared", "settings.base.json"),
6016
+ join48(repoHome2, "shared", "CLAUDE.md"),
6017
+ join48(repoHome2, "path-map.json"),
6018
+ join48(repoHome2, "hosts"),
6019
+ join48(repoHome2, "shared")
5626
6020
  ];
5627
6021
  for (const c of candidates) {
5628
- if (existsSync40(c)) return c;
6022
+ if (existsSync41(c)) return c;
5629
6023
  }
5630
6024
  return null;
5631
6025
  }
@@ -5634,31 +6028,31 @@ function cmdInit(opts = {}) {
5634
6028
  const keepActions = opts.keepActions === true;
5635
6029
  const repo = repoHome();
5636
6030
  const claude = claudeHome();
5637
- mkdirSync10(repo, { recursive: true });
6031
+ mkdirSync11(repo, { recursive: true });
5638
6032
  const conflict = preflightConflict(repo);
5639
6033
  if (conflict !== null) {
5640
6034
  die(`already initialized; refusing to clobber ${conflict}`);
5641
6035
  }
5642
6036
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
5643
- mkdirSync10(join46(repo, "shared"), { recursive: true });
5644
- mkdirSync10(join46(repo, "hosts"), { recursive: true });
6037
+ mkdirSync11(join48(repo, "shared"), { recursive: true });
6038
+ mkdirSync11(join48(repo, "hosts"), { recursive: true });
5645
6039
  for (const name of SHARED_KEEP_DIRS) {
5646
- mkdirSync10(join46(repo, "shared", name), { recursive: true });
6040
+ mkdirSync11(join48(repo, "shared", name), { recursive: true });
5647
6041
  }
5648
- const userClaudeMd = join46(claude, "CLAUDE.md");
5649
- if (!snapshot || !existsSync40(userClaudeMd)) {
5650
- writeFileSync6(join46(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
6042
+ const userClaudeMd = join48(claude, "CLAUDE.md");
6043
+ if (!snapshot || !existsSync41(userClaudeMd)) {
6044
+ writeFileSync6(join48(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
5651
6045
  item("created shared/CLAUDE.md");
5652
6046
  }
5653
6047
  for (const name of SHARED_KEEP_DIRS) {
5654
- writeFileSync6(join46(repo, "shared", name, ".gitkeep"), "");
6048
+ writeFileSync6(join48(repo, "shared", name, ".gitkeep"), "");
5655
6049
  item(`created shared/${name}/.gitkeep`);
5656
6050
  }
5657
- writeFileSync6(join46(repo, "hosts", ".gitkeep"), "");
6051
+ writeFileSync6(join48(repo, "hosts", ".gitkeep"), "");
5658
6052
  item("created hosts/.gitkeep");
5659
- writeJsonAtomic(join46(repo, "shared", "settings.base.json"), {});
6053
+ writeJsonAtomic(join48(repo, "shared", "settings.base.json"), {});
5660
6054
  item("created shared/settings.base.json");
5661
- writeJsonAtomic(join46(repo, "path-map.json"), { projects: {} });
6055
+ writeJsonAtomic(join48(repo, "path-map.json"), { projects: {} });
5662
6056
  item("created path-map.json");
5663
6057
  if (snapshot) {
5664
6058
  snapshotIntoShared({ projects: {} });
@@ -5959,7 +6353,7 @@ function parsePushArgs(argv) {
5959
6353
  // package.json
5960
6354
  var package_default = {
5961
6355
  name: "claude-nomad",
5962
- version: "0.48.0",
6356
+ version: "0.50.0",
5963
6357
  type: "module",
5964
6358
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
5965
6359
  keywords: [
@@ -6160,15 +6554,15 @@ var DEFAULT_HELP = [
6160
6554
  init_config();
6161
6555
  init_utils();
6162
6556
  init_utils_json();
6163
- import { existsSync as existsSync41, readFileSync as readFileSync14, readdirSync as readdirSync13 } from "node:fs";
6164
- import { join as join47 } from "node:path";
6557
+ import { existsSync as existsSync42, readFileSync as readFileSync14, readdirSync as readdirSync15 } from "node:fs";
6558
+ import { join as join49 } from "node:path";
6165
6559
  function resumeCmd(sessionId) {
6166
6560
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
6167
6561
  fail(`invalid session id: ${sessionId}`);
6168
6562
  process.exit(1);
6169
6563
  }
6170
- const projectsRoot = join47(claudeHome(), "projects");
6171
- if (!existsSync41(projectsRoot)) {
6564
+ const projectsRoot = join49(claudeHome(), "projects");
6565
+ if (!existsSync42(projectsRoot)) {
6172
6566
  fail(`${projectsRoot} does not exist`);
6173
6567
  process.exit(1);
6174
6568
  }
@@ -6182,8 +6576,8 @@ function resumeCmd(sessionId) {
6182
6576
  fail(`no cwd field found in ${jsonlPath}`);
6183
6577
  process.exit(1);
6184
6578
  }
6185
- const mapPath = join47(repoHome(), "path-map.json");
6186
- if (!existsSync41(mapPath)) {
6579
+ const mapPath = join49(repoHome(), "path-map.json");
6580
+ if (!existsSync42(mapPath)) {
6187
6581
  fail("path-map.json missing");
6188
6582
  process.exit(1);
6189
6583
  }
@@ -6205,9 +6599,9 @@ function resumeCmd(sessionId) {
6205
6599
  console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
6206
6600
  }
6207
6601
  function findTranscriptPath(projectsRoot, sessionId) {
6208
- for (const dir of readdirSync13(projectsRoot)) {
6209
- const candidate = join47(projectsRoot, dir, `${sessionId}.jsonl`);
6210
- if (existsSync41(candidate)) return candidate;
6602
+ for (const dir of readdirSync15(projectsRoot)) {
6603
+ const candidate = join49(projectsRoot, dir, `${sessionId}.jsonl`);
6604
+ if (existsSync42(candidate)) return candidate;
6211
6605
  }
6212
6606
  return null;
6213
6607
  }