claude-nomad 0.34.1 → 0.36.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
@@ -327,7 +327,7 @@ var init_settings_keys = __esm({
327
327
 
328
328
  // src/config.ts
329
329
  import { homedir, hostname } from "node:os";
330
- import { resolve } from "node:path";
330
+ import { join, resolve } from "node:path";
331
331
  function allSharedLinks(map) {
332
332
  const extras = [];
333
333
  for (const entry of map.sharedDirs ?? []) {
@@ -341,7 +341,7 @@ function allSharedLinks(map) {
341
341
  }
342
342
  return [...SHARED_LINKS, ...extras];
343
343
  }
344
- var HOME, CLAUDE_HOME, REPO_HOME, SETTINGS_SCHEMA_URL, GITLEAKS_PINNED_VERSION, HOST, SHARED_LINKS, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, NEVER_SYNC, PUSH_ALLOWED_STATIC;
344
+ var HOME, CLAUDE_HOME, BACKUP_BASE, REPO_HOME, SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, HOST, SHARED_LINKS, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, NEVER_SYNC, PUSH_ALLOWED_STATIC;
345
345
  var init_config = __esm({
346
346
  "src/config.ts"() {
347
347
  "use strict";
@@ -350,8 +350,10 @@ var init_config = __esm({
350
350
  init_settings_keys();
351
351
  HOME = homedir();
352
352
  CLAUDE_HOME = resolve(HOME, ".claude");
353
+ BACKUP_BASE = join(HOME, ".cache", "claude-nomad", "backup");
353
354
  REPO_HOME = process.env.NOMAD_REPO || resolve(HOME, "claude-nomad");
354
355
  SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
356
+ NPM_REGISTRY_LATEST_URL = "https://registry.npmjs.org/claude-nomad/latest";
355
357
  GITLEAKS_PINNED_VERSION = "8.30.1";
356
358
  HOST = (process.env.NOMAD_HOST || hostname()).toLowerCase();
357
359
  SHARED_LINKS = [
@@ -463,7 +465,7 @@ import {
463
465
  symlinkSync,
464
466
  writeFileSync
465
467
  } from "node:fs";
466
- import { dirname, join, relative } from "node:path";
468
+ import { dirname, join as join2, relative } from "node:path";
467
469
  function writeJsonAtomic(path, data) {
468
470
  const mode = existsSync(path) ? statSync(path).mode & 511 : 384;
469
471
  const tmp = `${path}.tmp.${process.pid}`;
@@ -489,9 +491,9 @@ function nowTimestamp() {
489
491
  }
490
492
  function freshBackupTs(backupRoot) {
491
493
  const base = nowTimestamp();
492
- if (!existsSync(join(backupRoot, base))) return base;
494
+ if (!existsSync(join2(backupRoot, base))) return base;
493
495
  let n = 1;
494
- while (existsSync(join(backupRoot, `${base}-${n}`))) n++;
496
+ while (existsSync(join2(backupRoot, `${base}-${n}`))) n++;
495
497
  return `${base}-${n}`;
496
498
  }
497
499
  function ensureSymlink(linkPath, target) {
@@ -507,8 +509,8 @@ function backupBeforeWrite(absPath, ts) {
507
509
  if (!existsSync(absPath)) return;
508
510
  const rel = relative(CLAUDE_HOME, absPath);
509
511
  if (rel.startsWith("..") || rel === "") return;
510
- const backupRoot = join(HOME, ".cache", "claude-nomad", "backup", ts);
511
- const dst = join(backupRoot, rel);
512
+ const backupRoot = join2(BACKUP_BASE, ts);
513
+ const dst = join2(backupRoot, rel);
512
514
  mkdirSync(dirname(dst), { recursive: true });
513
515
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
514
516
  }
@@ -516,8 +518,8 @@ function backupRepoWrite(absPath, ts, repoHome) {
516
518
  if (!existsSync(absPath)) return;
517
519
  const rel = relative(repoHome, absPath);
518
520
  if (rel.startsWith("..") || rel === "") return;
519
- const backupRoot = join(HOME, ".cache", "claude-nomad", "backup", ts, "repo");
520
- const dst = join(backupRoot, rel);
521
+ const backupRoot = join2(BACKUP_BASE, ts, "repo");
522
+ const dst = join2(backupRoot, rel);
521
523
  mkdirSync(dirname(dst), { recursive: true });
522
524
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
523
525
  }
@@ -525,8 +527,8 @@ function backupExtrasWrite(absPath, ts, projectRoot) {
525
527
  if (!existsSync(absPath)) return;
526
528
  const rel = relative(projectRoot, absPath);
527
529
  if (rel.startsWith("..") || rel === "") return;
528
- const backupRoot = join(HOME, ".cache", "claude-nomad", "backup", ts, "extras");
529
- const dst = join(backupRoot, encodePath(projectRoot), rel);
530
+ const backupRoot = join2(BACKUP_BASE, ts, "extras");
531
+ const dst = join2(backupRoot, encodePath(projectRoot), rel);
530
532
  mkdirSync(dirname(dst), { recursive: true });
531
533
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
532
534
  }
@@ -541,15 +543,15 @@ var init_utils_fs = __esm({
541
543
 
542
544
  // src/push-gitleaks.scan.ts
543
545
  import { execFileSync as execFileSync2 } from "node:child_process";
544
- import { existsSync as existsSync7, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
546
+ import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync3 } from "node:fs";
545
547
  import { homedir as homedir2 } from "node:os";
546
- import { join as join7 } from "node:path";
548
+ import { join as join9 } from "node:path";
547
549
  import { fileURLToPath } from "node:url";
548
550
  function resolveTomlPath() {
549
- const repoToml = join7(REPO_HOME, ".gitleaks.toml");
550
- if (existsSync7(repoToml)) return repoToml;
551
+ const repoToml = join9(REPO_HOME, ".gitleaks.toml");
552
+ if (existsSync8(repoToml)) return repoToml;
551
553
  const bundled = fileURLToPath(new URL("../.gitleaks.toml", import.meta.url));
552
- return existsSync7(bundled) ? bundled : null;
554
+ return existsSync8(bundled) ? bundled : null;
553
555
  }
554
556
  function readGitleaksReport(reportPath) {
555
557
  try {
@@ -562,9 +564,9 @@ function readGitleaksReport(reportPath) {
562
564
  }
563
565
  }
564
566
  function scanStagedTree(repoDir, forwardStreams = false) {
565
- const cacheDir = join7(homedir2(), ".cache", "claude-nomad");
567
+ const cacheDir = join9(homedir2(), ".cache", "claude-nomad");
566
568
  mkdirSync2(cacheDir, { recursive: true });
567
- const reportPath = join7(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
569
+ const reportPath = join9(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
568
570
  const { path: toml, tempPath } = resolveTomlConfig();
569
571
  const args = [
570
572
  "protect",
@@ -591,14 +593,14 @@ function scanStagedTree(repoDir, forwardStreams = false) {
591
593
  }
592
594
  return report;
593
595
  } finally {
594
- if (tempPath !== null) rmSync2(tempPath, { recursive: true, force: true });
595
- rmSync2(reportPath, { force: true });
596
+ if (tempPath !== null) rmSync3(tempPath, { recursive: true, force: true });
597
+ rmSync3(reportPath, { force: true });
596
598
  }
597
599
  }
598
600
  function scanFile(filePath, forwardStreams = false) {
599
- const cacheDir = join7(homedir2(), ".cache", "claude-nomad");
601
+ const cacheDir = join9(homedir2(), ".cache", "claude-nomad");
600
602
  mkdirSync2(cacheDir, { recursive: true });
601
- const reportPath = join7(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
603
+ const reportPath = join9(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
602
604
  const { path: toml, tempPath } = resolveTomlConfig();
603
605
  const args = [
604
606
  "detect",
@@ -623,8 +625,8 @@ function scanFile(filePath, forwardStreams = false) {
623
625
  }
624
626
  return report;
625
627
  } finally {
626
- if (tempPath !== null) rmSync2(tempPath, { recursive: true, force: true });
627
- rmSync2(reportPath, { force: true });
628
+ if (tempPath !== null) rmSync3(tempPath, { recursive: true, force: true });
629
+ rmSync3(reportPath, { force: true });
628
630
  }
629
631
  }
630
632
  var init_push_gitleaks_scan = __esm({
@@ -637,24 +639,24 @@ var init_push_gitleaks_scan = __esm({
637
639
  });
638
640
 
639
641
  // src/push-gitleaks.config.ts
640
- import { existsSync as existsSync8, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
642
+ import { existsSync as existsSync9, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
641
643
  import { tmpdir } from "node:os";
642
- import { join as join8 } from "node:path";
644
+ import { join as join10 } from "node:path";
643
645
  function buildOverlayTempConfig(overlayBody, bundled) {
644
646
  const tempBody = `[extend]
645
647
  path = ${JSON.stringify(bundled)}
646
648
 
647
649
  ${overlayBody}`;
648
- const tempPath = mkdtempSync(join8(tmpdir(), "nomad-gitleaks-cfg-"));
649
- const configPath = join8(tempPath, "config.toml");
650
+ const tempPath = mkdtempSync(join10(tmpdir(), "nomad-gitleaks-cfg-"));
651
+ const configPath = join10(tempPath, "config.toml");
650
652
  writeFileSync2(configPath, tempBody, { mode: 384, flag: "wx" });
651
653
  return { configPath, tempPath };
652
654
  }
653
655
  function resolveTomlConfig() {
654
- const overlayPath = join8(REPO_HOME, ".gitleaks.overlay.toml");
655
- const repoToml = join8(REPO_HOME, ".gitleaks.toml");
656
+ const overlayPath = join10(REPO_HOME, ".gitleaks.overlay.toml");
657
+ const repoToml = join10(REPO_HOME, ".gitleaks.toml");
656
658
  const bundled = resolveTomlPath();
657
- if (!existsSync8(overlayPath)) {
659
+ if (!existsSync9(overlayPath)) {
658
660
  return { path: bundled, tempPath: null };
659
661
  }
660
662
  if (bundled === repoToml) {
@@ -696,9 +698,9 @@ var init_push_gitleaks_config = __esm({
696
698
 
697
699
  // src/push-checks.ts
698
700
  import { execFileSync as execFileSync3 } from "node:child_process";
699
- import { readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
701
+ import { readdirSync as readdirSync3, rmSync as rmSync4 } from "node:fs";
700
702
  import { homedir as homedir3, platform } from "node:os";
701
- import { join as join9 } from "node:path";
703
+ import { join as join11 } from "node:path";
702
704
  function gitleaksInstallHint() {
703
705
  const head = "gitleaks not on PATH (required for nomad push). Install:";
704
706
  const plat = platform();
@@ -736,12 +738,12 @@ function findGitlinks(dir) {
736
738
  function walk(current) {
737
739
  let entries;
738
740
  try {
739
- entries = readdirSync2(current, { withFileTypes: true });
741
+ entries = readdirSync3(current, { withFileTypes: true });
740
742
  } catch {
741
743
  return;
742
744
  }
743
745
  for (const e of entries) {
744
- const p = join9(current, e.name);
746
+ const p = join11(current, e.name);
745
747
  if (e.name === ".git") {
746
748
  hits.push(p);
747
749
  continue;
@@ -763,7 +765,7 @@ function probeGitleaks() {
763
765
  if (e.code === "ENOENT") throw new NomadFatal(gitleaksInstallHint());
764
766
  throw new NomadFatal(`gitleaks --version failed: ${e.message}`);
765
767
  } finally {
766
- if (tempPath !== null) rmSync3(tempPath, { recursive: true, force: true });
768
+ if (tempPath !== null) rmSync4(tempPath, { recursive: true, force: true });
767
769
  }
768
770
  }
769
771
  function rebaseBeforePush() {
@@ -949,7 +951,7 @@ init_utils();
949
951
  init_utils_fs();
950
952
  init_utils_json();
951
953
  import { cpSync as cpSync2, existsSync as existsSync2, lstatSync as lstatSync2, rmSync } from "node:fs";
952
- import { join as join2 } from "node:path";
954
+ import { join as join3 } from "node:path";
953
955
  var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
954
956
  function lexists(p) {
955
957
  try {
@@ -960,7 +962,7 @@ function lexists(p) {
960
962
  }
961
963
  }
962
964
  function readMapIfPresent(repoHome) {
963
- const mapPath = join2(repoHome, "path-map.json");
965
+ const mapPath = join3(repoHome, "path-map.json");
964
966
  return existsSync2(mapPath) ? readPathMap(mapPath) : { projects: {} };
965
967
  }
966
968
  function isConfiguredTarget(name, map) {
@@ -971,13 +973,12 @@ function isValidAdoptName(name) {
971
973
  return isValidSharedDir(name);
972
974
  }
973
975
  function performAdoptMove(name, linkPath, sharedTarget) {
974
- const backupBase = join2(HOME, ".cache", "claude-nomad", "backup");
975
- const ts = freshBackupTs(backupBase);
976
+ const ts = freshBackupTs(BACKUP_BASE);
976
977
  backupBeforeWrite(linkPath, ts);
977
978
  cpSync2(linkPath, sharedTarget, { recursive: true, force: true, preserveTimestamps: true });
978
979
  rmSync(linkPath, { recursive: true, force: true });
979
980
  ensureSymlink(linkPath, sharedTarget);
980
- const rel = join2("shared", name);
981
+ const rel = join3("shared", name);
981
982
  gitOrFatal(["add", "--", rel], `git add shared/${name}`, REPO_HOME);
982
983
  log(`adopted ${name}; ${ADOPT_PUSH_HINT}`);
983
984
  }
@@ -994,8 +995,8 @@ function cmdAdopt(name, opts = {}) {
994
995
  );
995
996
  process.exit(1);
996
997
  }
997
- const linkPath = join2(CLAUDE_HOME, name);
998
- const sharedTarget = join2(REPO_HOME, "shared", name);
998
+ const linkPath = join3(CLAUDE_HOME, name);
999
+ const sharedTarget = join3(REPO_HOME, "shared", name);
999
1000
  if (!existsSync2(linkPath)) {
1000
1001
  log(`${name}: nothing to adopt (not present in ~/.claude/)`);
1001
1002
  return;
@@ -1009,8 +1010,7 @@ function cmdAdopt(name, opts = {}) {
1009
1010
  process.exit(1);
1010
1011
  }
1011
1012
  if (dryRun) {
1012
- const backupBase = join2(HOME, ".cache", "claude-nomad", "backup");
1013
- const ts = freshBackupTs(backupBase);
1013
+ const ts = freshBackupTs(BACKUP_BASE);
1014
1014
  log(`would backup: ${linkPath} -> backup/${ts}/${name}`);
1015
1015
  log(`would move: ${linkPath} -> shared/${name}`);
1016
1016
  log(`would stage: shared/${name}`);
@@ -1025,15 +1025,79 @@ function cmdAdopt(name, opts = {}) {
1025
1025
  }
1026
1026
  }
1027
1027
 
1028
+ // src/commands.clean.ts
1029
+ init_config();
1030
+ init_utils();
1031
+ import { existsSync as existsSync3, lstatSync as lstatSync3, readdirSync, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
1032
+ import { join as join4 } from "node:path";
1033
+ var TS_SHAPE = /^\d{8}-\d{6}(-\d+)?$/;
1034
+ var DURATION_RE = /^(\d+)([dhm])$/;
1035
+ var UNIT_MS = { d: 864e5, h: 36e5, m: 6e4 };
1036
+ var CLEAN_DEFAULT_OLDER_THAN_MS = 14 * 24 * 60 * 60 * 1e3;
1037
+ function isTsDir(name) {
1038
+ return TS_SHAPE.test(name);
1039
+ }
1040
+ function parseDuration(s) {
1041
+ const m = DURATION_RE.exec(s);
1042
+ if (!m) return null;
1043
+ return Number(m[1]) * UNIT_MS[m[2]];
1044
+ }
1045
+ function listBackupDirs(backupBase) {
1046
+ if (!existsSync3(backupBase)) return [];
1047
+ return readdirSync(backupBase).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join4(backupBase, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
1048
+ }
1049
+ function prunableByAge(dirs, olderThanMs, nowMs) {
1050
+ return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
1051
+ }
1052
+ function prunableByCount(dirs, keep) {
1053
+ return dirs.slice(keep).map((d) => d.name);
1054
+ }
1055
+ function safeDelete(backupBase, name) {
1056
+ if (!isTsDir(name)) return;
1057
+ const full = join4(backupBase, name);
1058
+ const st = lstatSync3(full, { throwIfNoEntry: false });
1059
+ if (!st || st.isSymbolicLink() || !st.isDirectory()) return;
1060
+ rmSync2(full, { recursive: true, force: true });
1061
+ }
1062
+ function resolveTargets(dirs, olderThanMs, keep) {
1063
+ if (keep !== void 0) return prunableByCount(dirs, keep);
1064
+ return prunableByAge(dirs, olderThanMs, Date.now());
1065
+ }
1066
+ function cmdClean(opts, backupBase = BACKUP_BASE) {
1067
+ const { dryRun, olderThan, keep } = opts;
1068
+ if (olderThan !== void 0 && keep !== void 0) {
1069
+ fail("--older-than and --keep are mutually exclusive");
1070
+ process.exit(1);
1071
+ }
1072
+ let olderThanMs = CLEAN_DEFAULT_OLDER_THAN_MS;
1073
+ if (olderThan !== void 0) {
1074
+ const parsed = parseDuration(olderThan);
1075
+ if (parsed === null) {
1076
+ fail(`invalid --older-than duration: ${olderThan} (expected e.g. 14d, 24h, 30m)`);
1077
+ process.exit(1);
1078
+ }
1079
+ olderThanMs = parsed;
1080
+ }
1081
+ const dirs = listBackupDirs(backupBase);
1082
+ const targets = resolveTargets(dirs, olderThanMs, keep);
1083
+ if (dryRun) {
1084
+ for (const name of targets) log(`would remove ${name}`);
1085
+ log(`dry-run: ${targets.length} backup(s) would be removed`);
1086
+ return;
1087
+ }
1088
+ for (const name of targets) safeDelete(backupBase, name);
1089
+ log(`removed ${targets.length} backup(s)`);
1090
+ }
1091
+
1028
1092
  // src/commands.doctor.ts
1029
- import { existsSync as existsSync16 } from "node:fs";
1030
- import { join as join18 } from "node:path";
1093
+ import { existsSync as existsSync18 } from "node:fs";
1094
+ import { join as join21 } from "node:path";
1031
1095
 
1032
1096
  // src/commands.doctor.checks.repo.ts
1033
1097
  init_color();
1034
1098
  init_config();
1035
- import { existsSync as existsSync4, lstatSync as lstatSync3, statSync as statSync2 } from "node:fs";
1036
- import { join as join4 } from "node:path";
1099
+ import { existsSync as existsSync5, lstatSync as lstatSync4, statSync as statSync3 } from "node:fs";
1100
+ import { join as join6 } from "node:path";
1037
1101
 
1038
1102
  // src/commands.doctor.format.ts
1039
1103
  init_color();
@@ -1053,7 +1117,7 @@ function sectionFailed(s) {
1053
1117
  function renderSection(s) {
1054
1118
  const header = sectionFailed(s) ? `${red(FAIL_GLYPH_BARE)} ${s.header}` : s.header;
1055
1119
  console.log(header);
1056
- const lastContent = s.items.reduce((acc, item, j) => item !== "" ? j : acc, -1);
1120
+ const lastContent = s.items.reduce((acc, item, j) => item === "" ? acc : j, -1);
1057
1121
  for (let j = 0; j < s.items.length; j++) {
1058
1122
  if (s.items[j] === "") console.log("");
1059
1123
  else console.log(`${j === lastContent ? " \u2514 " : " \u251C "}${s.items[j]}`);
@@ -1083,15 +1147,15 @@ function readJsonSafe(path, label, section2) {
1083
1147
  // src/init.classify.ts
1084
1148
  init_config();
1085
1149
  init_utils_json();
1086
- import { existsSync as existsSync3 } from "node:fs";
1087
- import { join as join3 } from "node:path";
1150
+ import { existsSync as existsSync4 } from "node:fs";
1151
+ import { join as join5 } from "node:path";
1088
1152
  function classifyRepoState(repoHome, host) {
1089
- const basePath = join3(repoHome, "shared", "settings.base.json");
1090
- const mapPath = join3(repoHome, "path-map.json");
1091
- const hostPath = join3(repoHome, "hosts", `${host}.json`);
1092
- const hasBase = existsSync3(basePath);
1093
- const hasMap = existsSync3(mapPath);
1094
- const hasHost = existsSync3(hostPath);
1153
+ const basePath = join5(repoHome, "shared", "settings.base.json");
1154
+ const mapPath = join5(repoHome, "path-map.json");
1155
+ const hostPath = join5(repoHome, "hosts", `${host}.json`);
1156
+ const hasBase = existsSync4(basePath);
1157
+ const hasMap = existsSync4(mapPath);
1158
+ const hasHost = existsSync4(hostPath);
1095
1159
  let mapEntryCount = 0;
1096
1160
  if (hasMap) {
1097
1161
  try {
@@ -1106,11 +1170,11 @@ function classifyRepoState(repoHome, host) {
1106
1170
  return "partial";
1107
1171
  }
1108
1172
  function reasonForPartial(repoHome, host) {
1109
- const basePath = join3(repoHome, "shared", "settings.base.json");
1110
- const mapPath = join3(repoHome, "path-map.json");
1111
- const hostPath = join3(repoHome, "hosts", `${host}.json`);
1112
- if (!existsSync3(basePath)) return "- shared/settings.base.json missing";
1113
- if (!existsSync3(mapPath)) return "- path-map.json missing";
1173
+ const basePath = join5(repoHome, "shared", "settings.base.json");
1174
+ const mapPath = join5(repoHome, "path-map.json");
1175
+ const hostPath = join5(repoHome, "hosts", `${host}.json`);
1176
+ if (!existsSync4(basePath)) return "- shared/settings.base.json missing";
1177
+ if (!existsSync4(mapPath)) return "- path-map.json missing";
1114
1178
  let mapEntryCount;
1115
1179
  try {
1116
1180
  const map = readJson(mapPath);
@@ -1119,7 +1183,7 @@ function reasonForPartial(repoHome, host) {
1119
1183
  mapEntryCount = 0;
1120
1184
  }
1121
1185
  if (mapEntryCount === 0) return "- path-map.json.projects has no entries";
1122
- if (!existsSync3(hostPath)) return `- hosts/${host}.json missing`;
1186
+ if (!existsSync4(hostPath)) return `- hosts/${host}.json missing`;
1123
1187
  return "- partial state (unknown gap)";
1124
1188
  }
1125
1189
 
@@ -1131,11 +1195,11 @@ function reportHostAndPaths(section2) {
1131
1195
  addItem(section2, `${dim(infoGlyph)} host: ${cyan(HOST)}`);
1132
1196
  addItem(
1133
1197
  section2,
1134
- `${existsSync4(REPO_HOME) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(REPO_HOME)}`
1198
+ `${existsSync5(REPO_HOME) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(REPO_HOME)}`
1135
1199
  );
1136
1200
  addItem(
1137
1201
  section2,
1138
- `${existsSync4(CLAUDE_HOME) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(CLAUDE_HOME)}`
1202
+ `${existsSync5(CLAUDE_HOME) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(CLAUDE_HOME)}`
1139
1203
  );
1140
1204
  }
1141
1205
  function reportRepoState(section2) {
@@ -1157,12 +1221,12 @@ function reportRepoState(section2) {
1157
1221
  }
1158
1222
  }
1159
1223
  function repoHasSharedSource(name) {
1160
- return existsSync4(join4(REPO_HOME, "shared", name));
1224
+ return existsSync5(join6(REPO_HOME, "shared", name));
1161
1225
  }
1162
1226
  function classifySharedLink(name, p) {
1163
1227
  let stat;
1164
1228
  try {
1165
- stat = lstatSync3(p);
1229
+ stat = lstatSync4(p);
1166
1230
  } catch (err) {
1167
1231
  const code = err.code;
1168
1232
  if (code === "ENOENT") {
@@ -1183,7 +1247,7 @@ function classifySharedLink(name, p) {
1183
1247
  }
1184
1248
  function classifySymlinkTarget(name, p) {
1185
1249
  try {
1186
- statSync2(p);
1250
+ statSync3(p);
1187
1251
  return { line: `${green(okGlyph)} ${name}: symlink`, fail: false };
1188
1252
  } catch (err) {
1189
1253
  const code = err.code;
@@ -1204,7 +1268,7 @@ function classifySymlinkTarget(name, p) {
1204
1268
  }
1205
1269
  function reportSharedLinks(section2, map) {
1206
1270
  for (const name of allSharedLinks(map)) {
1207
- const p = join4(CLAUDE_HOME, name);
1271
+ const p = join6(CLAUDE_HOME, name);
1208
1272
  const { line, fail: fail2 } = classifySharedLink(name, p);
1209
1273
  addItem(section2, line);
1210
1274
  if (fail2) process.exitCode = 1;
@@ -1214,11 +1278,11 @@ function reportSharedLinks(section2, map) {
1214
1278
  // src/commands.doctor.checks.settings.ts
1215
1279
  init_color();
1216
1280
  init_config();
1217
- import { existsSync as existsSync5, readdirSync } from "node:fs";
1218
- import { join as join5 } from "node:path";
1281
+ import { existsSync as existsSync6, readdirSync as readdirSync2 } from "node:fs";
1282
+ import { join as join7 } from "node:path";
1219
1283
  function loadBaseSettings(section2) {
1220
- const basePath = join5(REPO_HOME, "shared", "settings.base.json");
1221
- if (!existsSync5(basePath)) {
1284
+ const basePath = join7(REPO_HOME, "shared", "settings.base.json");
1285
+ if (!existsSync6(basePath)) {
1222
1286
  addItem(section2, `${red(failGlyph)} shared/settings.base.json missing at ${blue(basePath)}`);
1223
1287
  process.exitCode = 1;
1224
1288
  return null;
@@ -1226,8 +1290,8 @@ function loadBaseSettings(section2) {
1226
1290
  return readJsonSafe(basePath, basePath, section2);
1227
1291
  }
1228
1292
  function loadAndReportSettings(section2) {
1229
- const settingsPath = join5(CLAUDE_HOME, "settings.json");
1230
- if (!existsSync5(settingsPath)) return null;
1293
+ const settingsPath = join7(CLAUDE_HOME, "settings.json");
1294
+ if (!existsSync6(settingsPath)) return null;
1231
1295
  const settings = readJsonSafe(settingsPath, settingsPath, section2);
1232
1296
  if (settings === null) return null;
1233
1297
  const unknownKeys = Object.keys(settings).filter((k) => !KNOWN_SETTINGS_KEYS.has(k));
@@ -1242,13 +1306,13 @@ function loadAndReportSettings(section2) {
1242
1306
  return settings;
1243
1307
  }
1244
1308
  function reportHostOverrides(section2, base, settings) {
1245
- const hostFile = join5(REPO_HOME, "hosts", `${HOST}.json`);
1309
+ const hostFile = join7(REPO_HOME, "hosts", `${HOST}.json`);
1246
1310
  let drift = [];
1247
1311
  if (base !== null && settings !== null) {
1248
1312
  const baseKeys = new Set(Object.keys(base));
1249
1313
  drift = Object.keys(settings).filter((k) => !baseKeys.has(k));
1250
1314
  }
1251
- if (existsSync5(hostFile)) {
1315
+ if (existsSync6(hostFile)) {
1252
1316
  if (readJsonSafe(hostFile, hostFile, section2) !== null) {
1253
1317
  addItem(section2, `${green(okGlyph)} host overrides: ${blue(hostFile)}`);
1254
1318
  }
@@ -1257,9 +1321,9 @@ function reportHostOverrides(section2, base, settings) {
1257
1321
  section2,
1258
1322
  `${red(failGlyph)} no hosts/${HOST}.json AND settings.json has unbased keys ${JSON.stringify(drift)}`
1259
1323
  );
1260
- const hostsDir = join5(REPO_HOME, "hosts");
1261
- if (existsSync5(hostsDir)) {
1262
- const cands = readdirSync(hostsDir).filter((f) => f.endsWith(".json"));
1324
+ const hostsDir = join7(REPO_HOME, "hosts");
1325
+ if (existsSync6(hostsDir)) {
1326
+ const cands = readdirSync2(hostsDir).filter((f) => f.endsWith(".json"));
1263
1327
  if (cands.length > 0) addItem(section2, `${dim(infoGlyph)} candidates: ${cands.join(", ")}`);
1264
1328
  }
1265
1329
  process.exitCode = 1;
@@ -1274,8 +1338,8 @@ function reportHostOverrides(section2, base, settings) {
1274
1338
  // src/commands.doctor.checks.pathmap.ts
1275
1339
  init_color();
1276
1340
  init_config();
1277
- import { existsSync as existsSync6 } from "node:fs";
1278
- import { join as join6 } from "node:path";
1341
+ import { existsSync as existsSync7 } from "node:fs";
1342
+ import { join as join8 } from "node:path";
1279
1343
  init_utils_json();
1280
1344
  function reportMappedProjects(section2, map) {
1281
1345
  const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
@@ -1310,8 +1374,8 @@ function reportPathCollisions(section2, map) {
1310
1374
  else addItem(section2, `${green(okGlyph)} path-encoding: no collisions`);
1311
1375
  }
1312
1376
  function reportPathMap(section2) {
1313
- const mapPath = join6(REPO_HOME, "path-map.json");
1314
- if (!existsSync6(mapPath)) {
1377
+ const mapPath = join8(REPO_HOME, "path-map.json");
1378
+ if (!existsSync7(mapPath)) {
1315
1379
  addItem(section2, `${red(failGlyph)} path-map.json missing at ${blue(mapPath)}`);
1316
1380
  process.exitCode = 1;
1317
1381
  return;
@@ -1358,8 +1422,8 @@ function reportNeverSync(section2) {
1358
1422
  init_color();
1359
1423
  init_config();
1360
1424
  import { execFileSync as execFileSync4 } from "node:child_process";
1361
- import { existsSync as existsSync9 } from "node:fs";
1362
- import { join as join10, relative as relative2 } from "node:path";
1425
+ import { existsSync as existsSync10 } from "node:fs";
1426
+ import { join as join12, relative as relative2 } from "node:path";
1363
1427
  init_push_checks();
1364
1428
  init_utils();
1365
1429
  function reportGitleaksProbe(section2) {
@@ -1378,8 +1442,8 @@ function reportGitleaksProbe(section2) {
1378
1442
  }
1379
1443
  }
1380
1444
  function reportGitlinks(section2) {
1381
- const sharedDir = join10(REPO_HOME, "shared");
1382
- if (existsSync9(sharedDir)) {
1445
+ const sharedDir = join12(REPO_HOME, "shared");
1446
+ if (existsSync10(sharedDir)) {
1383
1447
  const gitlinks = findGitlinks(sharedDir);
1384
1448
  for (const p of gitlinks) {
1385
1449
  const rel = relative2(REPO_HOME, p);
@@ -1419,17 +1483,84 @@ function reportRebaseClean(section2) {
1419
1483
  }
1420
1484
  }
1421
1485
 
1486
+ // src/commands.doctor.checks.backups.ts
1487
+ init_color();
1488
+ import { existsSync as existsSync11, lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
1489
+ import { join as join13 } from "node:path";
1490
+ init_config();
1491
+ var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
1492
+ function safeReaddir(dir) {
1493
+ try {
1494
+ return readdirSync4(dir);
1495
+ } catch {
1496
+ return [];
1497
+ }
1498
+ }
1499
+ var DOCTOR_BACKUP_COUNT_WARN = 20;
1500
+ var DOCTOR_BACKUP_SIZE_WARN_MB = 200;
1501
+ var BYTES_PER_MB = 1024 * 1024;
1502
+ function dirSizeBytes(dir) {
1503
+ let bytes = 0;
1504
+ for (const entry of safeReaddir(dir)) {
1505
+ const full = join13(dir, entry);
1506
+ const st = lstatSync5(full, { throwIfNoEntry: false });
1507
+ if (!st) continue;
1508
+ if (st.isSymbolicLink()) continue;
1509
+ if (st.isDirectory()) bytes += dirSizeBytes(full);
1510
+ else bytes += st.size;
1511
+ }
1512
+ return bytes;
1513
+ }
1514
+ function totalSizeMb(backupBase, dirs) {
1515
+ let bytes = 0;
1516
+ for (const name of dirs) bytes += dirSizeBytes(join13(backupBase, name));
1517
+ return bytes / BYTES_PER_MB;
1518
+ }
1519
+ function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
1520
+ if (!existsSync11(backupBase)) return;
1521
+ const dirs = safeReaddir(backupBase).filter((n) => TS_SHAPE2.test(n));
1522
+ const count = dirs.length;
1523
+ const sizeMb = totalSizeMb(backupBase, dirs);
1524
+ if (count > DOCTOR_BACKUP_COUNT_WARN || sizeMb > DOCTOR_BACKUP_SIZE_WARN_MB) {
1525
+ addItem(
1526
+ section2,
1527
+ `${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups')`
1528
+ );
1529
+ }
1530
+ }
1531
+
1422
1532
  // src/commands.doctor.check-schema.ts
1423
1533
  init_color();
1424
- import { execFileSync as execFileSync5 } from "node:child_process";
1425
- import { existsSync as existsSync10 } from "node:fs";
1426
- import { join as join11 } from "node:path";
1534
+ import { existsSync as existsSync12 } from "node:fs";
1535
+ import { join as join14 } from "node:path";
1427
1536
  init_config();
1428
- function fetchSchemaKeys() {
1537
+
1538
+ // src/http-fetch.ts
1539
+ import { execFileSync as execFileSync5 } from "node:child_process";
1540
+ var FETCH_TIMEOUT_MS = 3e3;
1541
+ function fetchUrl(url, run = execFileSync5) {
1429
1542
  try {
1430
- const raw = execFileSync5("curl", ["-fsSL", "-m", "3", SETTINGS_SCHEMA_URL], {
1431
- stdio: ["ignore", "pipe", "pipe"]
1543
+ return run("curl", ["-fsSL", "-m", "3", url], {
1544
+ stdio: ["ignore", "pipe", "pipe"],
1545
+ timeout: FETCH_TIMEOUT_MS
1432
1546
  }).toString();
1547
+ } catch {
1548
+ try {
1549
+ return run("wget", ["-qO-", "--timeout=3", "--tries=1", url], {
1550
+ stdio: ["ignore", "pipe", "pipe"],
1551
+ timeout: FETCH_TIMEOUT_MS
1552
+ }).toString();
1553
+ } catch {
1554
+ return null;
1555
+ }
1556
+ }
1557
+ }
1558
+
1559
+ // src/commands.doctor.check-schema.ts
1560
+ function fetchSchemaKeys() {
1561
+ try {
1562
+ const raw = fetchUrl(SETTINGS_SCHEMA_URL);
1563
+ if (raw === null) return null;
1433
1564
  const parsed = JSON.parse(raw);
1434
1565
  if (typeof parsed.properties !== "object" || parsed.properties === null) return null;
1435
1566
  return Object.keys(parsed.properties);
@@ -1438,8 +1569,8 @@ function fetchSchemaKeys() {
1438
1569
  }
1439
1570
  }
1440
1571
  function reportCheckSchema(section2) {
1441
- const settingsPath = join11(CLAUDE_HOME, "settings.json");
1442
- if (!existsSync10(settingsPath)) {
1572
+ const settingsPath = join14(CLAUDE_HOME, "settings.json");
1573
+ if (!existsSync12(settingsPath)) {
1443
1574
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
1444
1575
  return;
1445
1576
  }
@@ -1449,7 +1580,7 @@ function reportCheckSchema(section2) {
1449
1580
  if (liveKeys === null) {
1450
1581
  addItem(
1451
1582
  section2,
1452
- `${yellow(warnGlyph)} schema check skipped (offline, curl missing, or schema unreachable)`
1583
+ `${yellow(warnGlyph)} schema check skipped (offline, curl or wget missing, or schema unreachable)`
1453
1584
  );
1454
1585
  return;
1455
1586
  }
@@ -1469,18 +1600,18 @@ function reportCheckSchema(section2) {
1469
1600
  init_color();
1470
1601
  import { randomBytes } from "node:crypto";
1471
1602
  import { execFileSync as execFileSync6 } from "node:child_process";
1472
- import { existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync4, rmSync as rmSync5 } from "node:fs";
1603
+ import { existsSync as existsSync14, mkdirSync as mkdirSync4, readdirSync as readdirSync6, rmSync as rmSync6 } from "node:fs";
1473
1604
  import { homedir as homedir4 } from "node:os";
1474
- import { join as join14 } from "node:path";
1605
+ import { join as join17 } from "node:path";
1475
1606
 
1476
1607
  // src/commands.doctor.check-shared.scan.ts
1477
1608
  init_color();
1478
- import { join as join12 } from "node:path";
1609
+ import { join as join15 } from "node:path";
1479
1610
  init_config();
1480
1611
  init_push_gitleaks();
1481
1612
  function scrubPath(logical, sid, logicalToEncoded) {
1482
1613
  const encoded = logicalToEncoded.get(logical) ?? logical;
1483
- return join12(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
1614
+ return join15(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
1484
1615
  }
1485
1616
  function reportSessionFindings(section2, bySession) {
1486
1617
  for (const [sid, counts] of bySession) {
@@ -1572,14 +1703,14 @@ init_config();
1572
1703
  init_utils();
1573
1704
  init_utils_fs();
1574
1705
  init_utils_json();
1575
- import { cpSync as cpSync3, existsSync as existsSync11, mkdirSync as mkdirSync3, readdirSync as readdirSync3, rmSync as rmSync4, statSync as statSync3 } from "node:fs";
1576
- import { join as join13, relative as relative3, sep } from "node:path";
1706
+ import { cpSync as cpSync3, existsSync as existsSync13, mkdirSync as mkdirSync3, readdirSync as readdirSync5, rmSync as rmSync5, statSync as statSync4 } from "node:fs";
1707
+ import { join as join16, relative as relative3, sep } from "node:path";
1577
1708
  function copyDir(src, dst) {
1578
- rmSync4(dst, { recursive: true, force: true });
1709
+ rmSync5(dst, { recursive: true, force: true });
1579
1710
  cpSync3(src, dst, { recursive: true, force: true });
1580
1711
  }
1581
1712
  function copyDirJsonlOnly(src, dst) {
1582
- rmSync4(dst, { recursive: true, force: true });
1713
+ rmSync5(dst, { recursive: true, force: true });
1583
1714
  cpSync3(src, dst, {
1584
1715
  recursive: true,
1585
1716
  force: true,
@@ -1587,7 +1718,7 @@ function copyDirJsonlOnly(src, dst) {
1587
1718
  const rel = relative3(src, srcPath);
1588
1719
  if (rel === "") return true;
1589
1720
  if (rel.split(sep).length > 1) return true;
1590
- if (statSync3(srcPath).isDirectory()) return true;
1721
+ if (statSync4(srcPath).isDirectory()) return true;
1591
1722
  if (srcPath.endsWith(".jsonl")) return true;
1592
1723
  log(`skip ${rel}: extension not in allowlist`);
1593
1724
  return false;
@@ -1599,14 +1730,14 @@ function remapPull(ts, opts = {}) {
1599
1730
  let unmapped = 0;
1600
1731
  const pulled = [];
1601
1732
  const wouldPull = [];
1602
- const mapPath = join13(REPO_HOME, "path-map.json");
1603
- const repoProjects = join13(REPO_HOME, "shared", "projects");
1604
- if (!existsSync11(mapPath) || !existsSync11(repoProjects)) {
1733
+ const mapPath = join16(REPO_HOME, "path-map.json");
1734
+ const repoProjects = join16(REPO_HOME, "shared", "projects");
1735
+ if (!existsSync13(mapPath) || !existsSync13(repoProjects)) {
1605
1736
  log("no path-map or repo projects dir; skipping session remap");
1606
1737
  return { unmapped: 0, pulled, wouldPull };
1607
1738
  }
1608
1739
  const map = readJson(mapPath);
1609
- const localProjects = join13(CLAUDE_HOME, "projects");
1740
+ const localProjects = join16(CLAUDE_HOME, "projects");
1610
1741
  if (!dryRun) mkdirSync3(localProjects, { recursive: true });
1611
1742
  for (const [logical, hosts] of Object.entries(map.projects)) {
1612
1743
  assertSafeLogical(logical);
@@ -1615,9 +1746,9 @@ function remapPull(ts, opts = {}) {
1615
1746
  unmapped++;
1616
1747
  continue;
1617
1748
  }
1618
- const src = join13(repoProjects, logical);
1619
- if (!existsSync11(src)) continue;
1620
- const dst = join13(localProjects, encodePath(localPath));
1749
+ const src = join16(repoProjects, logical);
1750
+ if (!existsSync13(src)) continue;
1751
+ const dst = join16(localProjects, encodePath(localPath));
1621
1752
  if (dryRun) {
1622
1753
  wouldPull.push(logical);
1623
1754
  log(`would overwrite: ${dst} (from ${src})`);
@@ -1658,30 +1789,30 @@ function remapPush(ts, opts = {}) {
1658
1789
  let unmapped = 0;
1659
1790
  const pushed = [];
1660
1791
  const wouldPush = [];
1661
- const mapPath = join13(REPO_HOME, "path-map.json");
1662
- if (!existsSync11(mapPath)) {
1792
+ const mapPath = join16(REPO_HOME, "path-map.json");
1793
+ if (!existsSync13(mapPath)) {
1663
1794
  log("no path-map.json; skipping session export");
1664
1795
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
1665
1796
  }
1666
1797
  const map = readJson(mapPath);
1667
- const localProjects = join13(CLAUDE_HOME, "projects");
1668
- const repoProjects = join13(REPO_HOME, "shared", "projects");
1798
+ const localProjects = join16(CLAUDE_HOME, "projects");
1799
+ const repoProjects = join16(REPO_HOME, "shared", "projects");
1669
1800
  const reverse = buildReverseMap(map);
1670
- if (!existsSync11(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
1801
+ if (!existsSync13(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
1671
1802
  if (!dryRun) mkdirSync3(repoProjects, { recursive: true });
1672
- for (const dir of readdirSync3(localProjects)) {
1803
+ for (const dir of readdirSync5(localProjects)) {
1673
1804
  const logical = reverse.get(dir);
1674
1805
  if (!logical) {
1675
1806
  unmapped++;
1676
1807
  continue;
1677
1808
  }
1678
- const repoDst = join13(repoProjects, logical);
1809
+ const repoDst = join16(repoProjects, logical);
1679
1810
  if (dryRun) {
1680
1811
  wouldPush.push(logical);
1681
1812
  continue;
1682
1813
  }
1683
1814
  backupRepoWrite(repoDst, ts, REPO_HOME);
1684
- copyDirJsonlOnly(join13(localProjects, dir), repoDst);
1815
+ copyDirJsonlOnly(join16(localProjects, dir), repoDst);
1685
1816
  pushed.push(logical);
1686
1817
  }
1687
1818
  return { unmapped, collisions: 0, pushed, wouldPush };
@@ -1693,8 +1824,8 @@ init_utils_json();
1693
1824
  function buildScanTree(tmpRoot) {
1694
1825
  const logicalToEncoded = /* @__PURE__ */ new Map();
1695
1826
  let staged = 0;
1696
- const mapPath = join14(REPO_HOME, "path-map.json");
1697
- if (!existsSync12(mapPath)) return { logicalToEncoded, staged, malformed: false };
1827
+ const mapPath = join17(REPO_HOME, "path-map.json");
1828
+ if (!existsSync14(mapPath)) return { logicalToEncoded, staged, malformed: false };
1698
1829
  let map;
1699
1830
  try {
1700
1831
  map = readJson(mapPath);
@@ -1711,12 +1842,12 @@ function buildScanTree(tmpRoot) {
1711
1842
  if (!p || p === "TBD") continue;
1712
1843
  reverse.set(encodePath(p), logical);
1713
1844
  }
1714
- const localProjects = join14(CLAUDE_HOME, "projects");
1715
- if (!existsSync12(localProjects)) return { logicalToEncoded, staged, malformed: false };
1716
- for (const dir of readdirSync4(localProjects)) {
1845
+ const localProjects = join17(CLAUDE_HOME, "projects");
1846
+ if (!existsSync14(localProjects)) return { logicalToEncoded, staged, malformed: false };
1847
+ for (const dir of readdirSync6(localProjects)) {
1717
1848
  const logical = reverse.get(dir);
1718
1849
  if (!logical) continue;
1719
- copyDirJsonlOnly(join14(localProjects, dir), join14(tmpRoot, "shared", "projects", logical));
1850
+ copyDirJsonlOnly(join17(localProjects, dir), join17(tmpRoot, "shared", "projects", logical));
1720
1851
  logicalToEncoded.set(logical, dir);
1721
1852
  staged++;
1722
1853
  }
@@ -1747,11 +1878,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
1747
1878
  }
1748
1879
  function reportCheckShared(section2, gitleaksReady) {
1749
1880
  if (!ensureGitleaksReady(section2, gitleaksReady)) return;
1750
- const cacheDir = join14(homedir4(), ".cache", "claude-nomad");
1881
+ const cacheDir = join17(homedir4(), ".cache", "claude-nomad");
1751
1882
  mkdirSync4(cacheDir, { recursive: true });
1752
1883
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes(4).toString("hex")}`;
1753
- const reportPath = join14(cacheDir, `check-shared-${stamp}.json`);
1754
- const tmpRoot = join14(cacheDir, `check-shared-tree-${stamp}`);
1884
+ const reportPath = join17(cacheDir, `check-shared-${stamp}.json`);
1885
+ const tmpRoot = join17(cacheDir, `check-shared-tree-${stamp}`);
1755
1886
  try {
1756
1887
  const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
1757
1888
  if (malformed) {
@@ -1765,15 +1896,109 @@ function reportCheckShared(section2, gitleaksReady) {
1765
1896
  }
1766
1897
  scanAndReport(section2, tmpRoot, staged, logicalToEncoded);
1767
1898
  } finally {
1768
- rmSync5(reportPath, { force: true });
1769
- rmSync5(tmpRoot, { recursive: true, force: true });
1899
+ rmSync6(reportPath, { force: true });
1900
+ rmSync6(tmpRoot, { recursive: true, force: true });
1901
+ }
1902
+ }
1903
+
1904
+ // src/commands.doctor.checks.hooks.scope.ts
1905
+ init_color();
1906
+ import { existsSync as existsSync15, readFileSync as readFileSync4, readdirSync as readdirSync7, realpathSync } from "node:fs";
1907
+ import { dirname as dirname2, extname, join as join18 } from "node:path";
1908
+ init_config();
1909
+ function typeFromPackageJson(pkgPath) {
1910
+ try {
1911
+ const parsed = JSON.parse(readFileSync4(pkgPath, "utf8"));
1912
+ return parsed.type === "module" ? "esm" : "cjs";
1913
+ } catch {
1914
+ return "cjs";
1915
+ }
1916
+ }
1917
+ function effectiveType(hookPath) {
1918
+ const ext = extname(hookPath);
1919
+ if (ext === ".mjs") return "esm";
1920
+ if (ext === ".cjs") return "cjs";
1921
+ let real;
1922
+ try {
1923
+ real = realpathSync(hookPath);
1924
+ } catch {
1925
+ return null;
1926
+ }
1927
+ let dir = dirname2(real);
1928
+ for (; ; ) {
1929
+ const pkg = join18(dir, "package.json");
1930
+ if (existsSync15(pkg)) return typeFromPackageJson(pkg);
1931
+ const parent = dirname2(dir);
1932
+ if (parent === dir) return "cjs";
1933
+ dir = parent;
1934
+ }
1935
+ }
1936
+ function stripCommentsAndStrings(src) {
1937
+ return src.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|'[^']*'|"[^"]*"|`[^`]*`/g, (match) => {
1938
+ const open = match[0];
1939
+ if (open === "'") return "''";
1940
+ if (open === '"') return '""';
1941
+ if (open === "`") return "``";
1942
+ return " ";
1943
+ });
1944
+ }
1945
+ function classifySource(src) {
1946
+ const code = stripCommentsAndStrings(src);
1947
+ const cjs = /\brequire\s*\(/.test(code) || /\bmodule\.exports\b/.test(code) || /\bexports\.\w/.test(code);
1948
+ const esm = /^[^\S\r\n]*import\s/m.test(code) || /^[^\S\r\n]*export\s/m.test(code) || /\bimport\.meta\b/.test(code);
1949
+ if (cjs && !esm) return "cjs";
1950
+ if (esm && !cjs) return "esm";
1951
+ if (cjs && esm) return "cjs";
1952
+ return "unknown";
1953
+ }
1954
+ function remedy(family) {
1955
+ return family === "cjs" ? 'rename to .cjs, or add { "type": "commonjs" } to the hooks dir' : "rename to .mjs (a synced hooks/ dir treats .js as CommonJS)";
1956
+ }
1957
+ function safeReaddir2(dir) {
1958
+ try {
1959
+ return readdirSync7(dir);
1960
+ } catch {
1961
+ return [];
1962
+ }
1963
+ }
1964
+ function safeRead(path) {
1965
+ try {
1966
+ return readFileSync4(path, "utf8");
1967
+ } catch {
1968
+ return null;
1969
+ }
1970
+ }
1971
+ function reportHookScopeCheck(section2) {
1972
+ const hooksDir = join18(CLAUDE_HOME, "hooks");
1973
+ if (!existsSync15(hooksDir)) {
1974
+ addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
1975
+ return;
1976
+ }
1977
+ let anyWarn = false;
1978
+ for (const name of safeReaddir2(hooksDir)) {
1979
+ if (extname(name) !== ".js") continue;
1980
+ const abs = join18(hooksDir, name);
1981
+ const eff = effectiveType(abs);
1982
+ if (eff === null) continue;
1983
+ const src = safeRead(abs);
1984
+ if (src === null) continue;
1985
+ const fam = classifySource(src);
1986
+ if (fam === "unknown" || fam === eff) continue;
1987
+ addItem(
1988
+ section2,
1989
+ `${yellow(warnGlyph)} hooks/${name}: ${fam} source loads as ${eff} (${remedy(fam)})`
1990
+ );
1991
+ anyWarn = true;
1992
+ }
1993
+ if (!anyWarn) {
1994
+ addItem(section2, `${green(okGlyph)} hooks: module type consistent`);
1770
1995
  }
1771
1996
  }
1772
1997
 
1773
1998
  // src/commands.doctor.checks.hooks.ts
1774
1999
  init_color();
1775
- import { existsSync as existsSync13 } from "node:fs";
1776
- import { join as join15 } from "node:path";
2000
+ import { existsSync as existsSync16 } from "node:fs";
2001
+ import { join as join19 } from "node:path";
1777
2002
  init_config();
1778
2003
  function expandHome(token) {
1779
2004
  return token.replace(/^\$\{HOME\}/, HOME).replace(/^\$HOME/, HOME).replace(/^~/, HOME);
@@ -1811,7 +2036,7 @@ function checkEventGroups(section2, event, groups) {
1811
2036
  for (const group of groups) {
1812
2037
  for (const cmd of commandsFromGroup(group)) {
1813
2038
  for (const resolved of claudePathsIn(cmd)) {
1814
- if (existsSync13(resolved)) continue;
2039
+ if (existsSync16(resolved)) continue;
1815
2040
  addItem(section2, `${red(failGlyph)} hooks/${event}: command target missing: ${resolved}`);
1816
2041
  process.exitCode = 1;
1817
2042
  anyFail = true;
@@ -1821,8 +2046,8 @@ function checkEventGroups(section2, event, groups) {
1821
2046
  return anyFail;
1822
2047
  }
1823
2048
  function reportHooksTargetCheck(section2) {
1824
- const settingsPath = join15(CLAUDE_HOME, "settings.json");
1825
- if (!existsSync13(settingsPath)) {
2049
+ const settingsPath = join19(CLAUDE_HOME, "settings.json");
2050
+ if (!existsSync16(settingsPath)) {
1826
2051
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
1827
2052
  return;
1828
2053
  }
@@ -1848,18 +2073,14 @@ init_config();
1848
2073
 
1849
2074
  // src/commands.doctor.engine.ts
1850
2075
  init_color();
1851
- import { readFileSync as readFileSync5 } from "node:fs";
2076
+ import { readFileSync as readFileSync6 } from "node:fs";
1852
2077
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1853
2078
 
1854
2079
  // src/commands.doctor.version.ts
1855
2080
  init_color();
1856
- import { execFileSync as execFileSync7 } from "node:child_process";
1857
- import { existsSync as existsSync14, mkdirSync as mkdirSync5, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
1858
- import { dirname as dirname2, join as join16 } from "node:path";
2081
+ import { readFileSync as readFileSync5 } from "node:fs";
1859
2082
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1860
2083
  init_config();
1861
- var CACHE_PATH = join16(HOME, ".cache", "claude-nomad", "version-check.json");
1862
- var CACHE_TTL_MS = 60 * 60 * 1e3;
1863
2084
  var STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
1864
2085
  var STRICT_SEMVER_PREFIX = /^(\d+\.\d+\.\d+)(?:[-+]|$)/;
1865
2086
  function compareSemver(a, b) {
@@ -1874,7 +2095,7 @@ function compareSemver(a, b) {
1874
2095
  function readLocalVersion() {
1875
2096
  try {
1876
2097
  const pkgPath = fileURLToPath2(new URL("../package.json", import.meta.url));
1877
- const parsed = JSON.parse(readFileSync4(pkgPath, "utf8"));
2098
+ const parsed = JSON.parse(readFileSync5(pkgPath, "utf8"));
1878
2099
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
1879
2100
  return parsed.version;
1880
2101
  }
@@ -1883,34 +2104,10 @@ function readLocalVersion() {
1883
2104
  return null;
1884
2105
  }
1885
2106
  }
1886
- function loadCache() {
1887
- try {
1888
- if (!existsSync14(CACHE_PATH)) return null;
1889
- const parsed = JSON.parse(readFileSync4(CACHE_PATH, "utf8"));
1890
- if (typeof parsed.checked_at !== "number" || !Number.isFinite(parsed.checked_at)) {
1891
- return null;
1892
- }
1893
- if (typeof parsed.latest !== "string" || !STRICT_SEMVER.test(parsed.latest)) {
1894
- return null;
1895
- }
1896
- return { checked_at: parsed.checked_at, latest: parsed.latest };
1897
- } catch {
1898
- return null;
1899
- }
1900
- }
1901
- function saveCache(latest) {
1902
- try {
1903
- mkdirSync5(dirname2(CACHE_PATH), { recursive: true });
1904
- writeFileSync3(CACHE_PATH, JSON.stringify({ checked_at: Date.now(), latest }));
1905
- } catch {
1906
- }
1907
- }
1908
2107
  function fetchLatestVersion() {
1909
2108
  try {
1910
- const url = "https://registry.npmjs.org/claude-nomad/latest";
1911
- const raw = execFileSync7("curl", ["-fsSL", "-m", "3", url], {
1912
- stdio: ["ignore", "pipe", "pipe"]
1913
- }).toString();
2109
+ const raw = fetchUrl(NPM_REGISTRY_LATEST_URL);
2110
+ if (raw === null) return null;
1914
2111
  const parsed = JSON.parse(raw);
1915
2112
  if (typeof parsed.version !== "string") return null;
1916
2113
  if (!STRICT_SEMVER.test(parsed.version)) return null;
@@ -1924,16 +2121,8 @@ function reportVersionCheck(section2) {
1924
2121
  if (local === null) return;
1925
2122
  const localPure = STRICT_SEMVER_PREFIX.exec(local)?.[1] ?? null;
1926
2123
  if (localPure === null) return;
1927
- let latest = null;
1928
- const cached = loadCache();
1929
- if (cached !== null && Date.now() - cached.checked_at < CACHE_TTL_MS) {
1930
- latest = cached.latest;
1931
- }
1932
- if (latest === null) {
1933
- latest = fetchLatestVersion();
1934
- if (latest === null) return;
1935
- saveCache(latest);
1936
- }
2124
+ const latest = fetchLatestVersion();
2125
+ if (latest === null) return;
1937
2126
  const cmp = compareSemver(localPure, latest);
1938
2127
  if (cmp === 0) {
1939
2128
  addItem(section2, `${green(okGlyph)} claude-nomad: ${local} (latest)`);
@@ -1959,7 +2148,7 @@ function parseMinVersion(spec) {
1959
2148
  function readEnginesNode() {
1960
2149
  try {
1961
2150
  const pkgPath = fileURLToPath3(new URL("../package.json", import.meta.url));
1962
- const parsed = JSON.parse(readFileSync5(pkgPath, "utf8"));
2151
+ const parsed = JSON.parse(readFileSync6(pkgPath, "utf8"));
1963
2152
  const node = parsed.engines?.node;
1964
2153
  if (typeof node === "string" && node.length > 0) return node;
1965
2154
  return null;
@@ -1987,9 +2176,9 @@ function reportNodeEngineCheck(section2) {
1987
2176
 
1988
2177
  // src/commands.doctor.gitleaks-version.ts
1989
2178
  init_color();
1990
- import { execFileSync as execFileSync8 } from "node:child_process";
1991
- import { existsSync as existsSync15 } from "node:fs";
1992
- import { join as join17 } from "node:path";
2179
+ import { execFileSync as execFileSync7 } from "node:child_process";
2180
+ import { existsSync as existsSync17 } from "node:fs";
2181
+ import { join as join20 } from "node:path";
1993
2182
  init_config();
1994
2183
  var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
1995
2184
  var GITLEAKS_TIMEOUT_MS = 5e3;
@@ -1998,7 +2187,7 @@ function majorMinorOf(value) {
1998
2187
  return m === null ? null : [m[1], m[2]];
1999
2188
  }
2000
2189
  function readGitleaksVersion(run, tomlExists) {
2001
- const tomlPath = join17(REPO_HOME, ".gitleaks.toml");
2190
+ const tomlPath = join20(REPO_HOME, ".gitleaks.toml");
2002
2191
  const args = ["version"];
2003
2192
  if (tomlExists(tomlPath)) args.push("--config", tomlPath);
2004
2193
  try {
@@ -2010,7 +2199,7 @@ function readGitleaksVersion(run, tomlExists) {
2010
2199
  return null;
2011
2200
  }
2012
2201
  }
2013
- function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists = existsSync15) {
2202
+ function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync17) {
2014
2203
  const raw = readGitleaksVersion(run, tomlExists);
2015
2204
  if (raw === null) return;
2016
2205
  const local = majorMinorOf(raw);
@@ -2030,15 +2219,20 @@ function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists =
2030
2219
 
2031
2220
  // src/commands.doctor.checks.deps.ts
2032
2221
  init_color();
2033
- import { execFileSync as execFileSync9 } from "node:child_process";
2222
+ import { execFileSync as execFileSync8 } from "node:child_process";
2034
2223
  var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
2224
+ var PROBE_TIMEOUT_MS = 3e3;
2225
+ var FETCHER_LABEL = "HTTP fetcher (curl or wget)";
2035
2226
  function parseFirstVersion(line) {
2036
2227
  const m = VERSION_TOKEN.exec(line);
2037
2228
  return m ? m[1] : null;
2038
2229
  }
2039
2230
  function probeOptionalDep(bin, run) {
2040
2231
  try {
2041
- const firstLine = run(bin, ["--version"], { stdio: ["ignore", "pipe", "pipe"] }).toString().split("\n")[0].trim();
2232
+ const firstLine = run(bin, ["--version"], {
2233
+ stdio: ["ignore", "pipe", "pipe"],
2234
+ timeout: PROBE_TIMEOUT_MS
2235
+ }).toString().split("\n")[0].trim();
2042
2236
  const version = parseFirstVersion(firstLine);
2043
2237
  return { status: "present", version };
2044
2238
  } catch (err) {
@@ -2048,34 +2242,40 @@ function probeOptionalDep(bin, run) {
2048
2242
  return { status: "present", version: null };
2049
2243
  }
2050
2244
  }
2051
- function reportOptionalDeps(section2, run = execFileSync9) {
2052
- const gh = probeOptionalDep("gh", run);
2053
- if (gh.status === "present") {
2054
- addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
2245
+ function reportFetcherRow(section2, run) {
2246
+ const curl = probeOptionalDep("curl", run);
2247
+ const wget = probeOptionalDep("wget", run);
2248
+ if (curl.status === "present") {
2249
+ addItem(section2, `${green(okGlyph)} ${FETCHER_LABEL}: ${curl.version ?? "present"}`);
2250
+ } else if (wget.status === "present") {
2251
+ addItem(section2, `${green(okGlyph)} ${FETCHER_LABEL}: ${wget.version ?? "present"}`);
2055
2252
  } else {
2056
2253
  addItem(
2057
2254
  section2,
2058
- `${yellow(warnGlyph)} gh: not installed (optional; needed for nomad init Actions auto-disable + mirror-Actions drift check)`
2255
+ `${yellow(warnGlyph)} ${FETCHER_LABEL}: not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
2059
2256
  );
2060
2257
  }
2061
- const curl = probeOptionalDep("curl", run);
2062
- if (curl.status === "present") {
2063
- addItem(section2, `${green(okGlyph)} curl: ${curl.version ?? "present"}`);
2258
+ }
2259
+ function reportOptionalDeps(section2, run = execFileSync8) {
2260
+ const gh = probeOptionalDep("gh", run);
2261
+ if (gh.status === "present") {
2262
+ addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
2064
2263
  } else {
2065
2264
  addItem(
2066
2265
  section2,
2067
- `${yellow(warnGlyph)} curl: not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
2266
+ `${yellow(warnGlyph)} gh: not installed (optional; needed for nomad init Actions auto-disable + mirror-Actions drift check)`
2068
2267
  );
2069
2268
  }
2269
+ reportFetcherRow(section2, run);
2070
2270
  }
2071
2271
 
2072
2272
  // src/commands.doctor.mirror-actions.ts
2073
2273
  init_color();
2074
- import { execFileSync as execFileSync11 } from "node:child_process";
2274
+ import { execFileSync as execFileSync10 } from "node:child_process";
2075
2275
  init_config();
2076
2276
 
2077
2277
  // src/gh-actions.ts
2078
- import { execFileSync as execFileSync10 } from "node:child_process";
2278
+ import { execFileSync as execFileSync9 } from "node:child_process";
2079
2279
  var GH_TIMEOUT_MS = 5e3;
2080
2280
  function parseGitHubRemote(remoteUrl) {
2081
2281
  const normalized = remoteUrl.trim().replace(/\/$/, "");
@@ -2083,7 +2283,7 @@ function parseGitHubRemote(remoteUrl) {
2083
2283
  if (m === null) return null;
2084
2284
  return { owner: m[1], repo: m[2] };
2085
2285
  }
2086
- function ghAuthStatus(run = execFileSync10) {
2286
+ function ghAuthStatus(run = execFileSync9) {
2087
2287
  try {
2088
2288
  run("gh", ["auth", "status"], {
2089
2289
  stdio: ["ignore", "ignore", "ignore"],
@@ -2097,7 +2297,7 @@ function ghAuthStatus(run = execFileSync10) {
2097
2297
  return "gh-probe-error";
2098
2298
  }
2099
2299
  }
2100
- function isRepoPrivate(ref, run = execFileSync10) {
2300
+ function isRepoPrivate(ref, run = execFileSync9) {
2101
2301
  const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
2102
2302
  stdio: ["ignore", "pipe", "ignore"],
2103
2303
  timeout: GH_TIMEOUT_MS
@@ -2105,7 +2305,7 @@ function isRepoPrivate(ref, run = execFileSync10) {
2105
2305
  const parsed = JSON.parse(out);
2106
2306
  return parsed.isPrivate === true;
2107
2307
  }
2108
- function isActionsEnabled(ref, run = execFileSync10) {
2308
+ function isActionsEnabled(ref, run = execFileSync9) {
2109
2309
  const out = run(
2110
2310
  "gh",
2111
2311
  ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
@@ -2113,7 +2313,7 @@ function isActionsEnabled(ref, run = execFileSync10) {
2113
2313
  ).toString().trim();
2114
2314
  return out === "true";
2115
2315
  }
2116
- function disableActions(ref, run = execFileSync10) {
2316
+ function disableActions(ref, run = execFileSync9) {
2117
2317
  run(
2118
2318
  "gh",
2119
2319
  [
@@ -2127,7 +2327,7 @@ function disableActions(ref, run = execFileSync10) {
2127
2327
  { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
2128
2328
  );
2129
2329
  }
2130
- function readOriginRemote(cwd, run = execFileSync10) {
2330
+ function readOriginRemote(cwd, run = execFileSync9) {
2131
2331
  return run("git", ["remote", "get-url", "origin"], {
2132
2332
  cwd,
2133
2333
  stdio: ["ignore", "pipe", "ignore"]
@@ -2135,7 +2335,7 @@ function readOriginRemote(cwd, run = execFileSync10) {
2135
2335
  }
2136
2336
 
2137
2337
  // src/commands.doctor.mirror-actions.ts
2138
- function reportMirrorActions(section2, run = execFileSync11) {
2338
+ function reportMirrorActions(section2, run = execFileSync10) {
2139
2339
  let remote;
2140
2340
  try {
2141
2341
  remote = readOriginRemote(REPO_HOME, run);
@@ -2172,12 +2372,13 @@ function cmdDoctor(opts = {}) {
2172
2372
  reportHostAndPaths(host);
2173
2373
  reportRepoState(host);
2174
2374
  const links = section("Shared links");
2175
- const mapPath = join18(REPO_HOME, "path-map.json");
2176
- const rawMap = existsSync16(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
2375
+ const mapPath = join21(REPO_HOME, "path-map.json");
2376
+ const rawMap = existsSync18(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
2177
2377
  const map = rawMap ?? { projects: {} };
2178
2378
  reportSharedLinks(links, map);
2179
2379
  const hooksScan = section("Hook targets");
2180
2380
  reportHooksTargetCheck(hooksScan);
2381
+ reportHookScopeCheck(hooksScan);
2181
2382
  const settings = section("Settings");
2182
2383
  const base = loadBaseSettings(settings);
2183
2384
  const parsedSettings = loadAndReportSettings(settings);
@@ -2197,6 +2398,7 @@ function cmdDoctor(opts = {}) {
2197
2398
  reportNodeEngineCheck(version);
2198
2399
  reportGitleaksVersionCheck(version);
2199
2400
  reportOptionalDeps(version);
2401
+ reportBackupsCheck(version);
2200
2402
  const sharedScan = section("Shared scan");
2201
2403
  if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
2202
2404
  const schemaScan = section("Schema scan");
@@ -2217,16 +2419,16 @@ function cmdDoctor(opts = {}) {
2217
2419
 
2218
2420
  // src/commands.drop-session.ts
2219
2421
  init_config();
2220
- import { execFileSync as execFileSync13 } from "node:child_process";
2221
- import { existsSync as existsSync18, readdirSync as readdirSync5, statSync as statSync4 } from "node:fs";
2222
- import { join as join21, relative as relative4 } from "node:path";
2422
+ import { execFileSync as execFileSync12 } from "node:child_process";
2423
+ import { existsSync as existsSync20, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
2424
+ import { join as join24, relative as relative4 } from "node:path";
2223
2425
 
2224
2426
  // src/commands.drop-session.git.ts
2225
2427
  init_config();
2226
- import { execFileSync as execFileSync12 } from "node:child_process";
2428
+ import { execFileSync as execFileSync11 } from "node:child_process";
2227
2429
  function expandStagedDir(dirRel) {
2228
2430
  try {
2229
- const out = execFileSync12("git", ["ls-files", "-z", "--", dirRel], {
2431
+ const out = execFileSync11("git", ["ls-files", "-z", "--", dirRel], {
2230
2432
  cwd: REPO_HOME,
2231
2433
  stdio: ["ignore", "pipe", "pipe"]
2232
2434
  });
@@ -2237,7 +2439,7 @@ function expandStagedDir(dirRel) {
2237
2439
  }
2238
2440
  function isTrackedInHead(rel) {
2239
2441
  try {
2240
- execFileSync12("git", ["cat-file", "-e", `HEAD:${rel}`], {
2442
+ execFileSync11("git", ["cat-file", "-e", `HEAD:${rel}`], {
2241
2443
  cwd: REPO_HOME,
2242
2444
  stdio: ["ignore", "pipe", "pipe"]
2243
2445
  });
@@ -2248,7 +2450,7 @@ function isTrackedInHead(rel) {
2248
2450
  }
2249
2451
  function isInIndex(rel) {
2250
2452
  try {
2251
- const out = execFileSync12("git", ["ls-files", "--", rel], {
2453
+ const out = execFileSync11("git", ["ls-files", "--", rel], {
2252
2454
  cwd: REPO_HOME,
2253
2455
  stdio: ["ignore", "pipe", "pipe"]
2254
2456
  });
@@ -2262,8 +2464,8 @@ function isInIndex(rel) {
2262
2464
  init_config();
2263
2465
  init_utils();
2264
2466
  init_utils_json();
2265
- import { existsSync as existsSync17 } from "node:fs";
2266
- import { join as join19 } from "node:path";
2467
+ import { existsSync as existsSync19 } from "node:fs";
2468
+ import { join as join22 } from "node:path";
2267
2469
  var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
2268
2470
  function reportScrubHint(id, matches) {
2269
2471
  const live = resolveLiveTranscript(id, matches);
@@ -2279,16 +2481,16 @@ function reportScrubHint(id, matches) {
2279
2481
  }
2280
2482
  function resolveLiveTranscript(id, matches) {
2281
2483
  try {
2282
- const mapPath = join19(REPO_HOME, "path-map.json");
2283
- if (!existsSync17(mapPath)) return null;
2484
+ const mapPath = join22(REPO_HOME, "path-map.json");
2485
+ if (!existsSync19(mapPath)) return null;
2284
2486
  const projects = readJson(mapPath).projects;
2285
2487
  for (const rel of matches) {
2286
2488
  const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
2287
2489
  if (logical === void 0) continue;
2288
2490
  const abs = projects[logical]?.[HOST];
2289
2491
  if (abs === void 0) continue;
2290
- const live = join19(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2291
- if (existsSync17(live)) return live;
2492
+ const live = join22(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2493
+ if (existsSync19(live)) return live;
2292
2494
  }
2293
2495
  return null;
2294
2496
  } catch {
@@ -2302,15 +2504,15 @@ init_utils();
2302
2504
  // src/utils.lockfile.ts
2303
2505
  init_config();
2304
2506
  init_utils();
2305
- import { closeSync as closeSync2, mkdirSync as mkdirSync6, openSync as openSync2, readFileSync as readFileSync6, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
2306
- import { dirname as dirname3, join as join20 } from "node:path";
2307
- var LOCK_PATH = join20(HOME, ".cache", "claude-nomad", "nomad.lock");
2507
+ import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
2508
+ import { dirname as dirname3, join as join23 } from "node:path";
2509
+ var LOCK_PATH = join23(HOME, ".cache", "claude-nomad", "nomad.lock");
2308
2510
  function acquireLock(verb) {
2309
- mkdirSync6(dirname3(LOCK_PATH), { recursive: true });
2511
+ mkdirSync5(dirname3(LOCK_PATH), { recursive: true });
2310
2512
  try {
2311
2513
  const fd = openSync2(LOCK_PATH, "wx");
2312
2514
  try {
2313
- writeFileSync4(fd, String(process.pid));
2515
+ writeFileSync3(fd, String(process.pid));
2314
2516
  } catch (writeErr) {
2315
2517
  try {
2316
2518
  closeSync2(fd);
@@ -2344,7 +2546,7 @@ function releaseLock(handle) {
2344
2546
  function unlinkIfSamePid(expectedPidStr) {
2345
2547
  let current;
2346
2548
  try {
2347
- current = readFileSync6(LOCK_PATH, "utf8").trim();
2549
+ current = readFileSync7(LOCK_PATH, "utf8").trim();
2348
2550
  } catch {
2349
2551
  return false;
2350
2552
  }
@@ -2359,7 +2561,7 @@ function unlinkIfSamePid(expectedPidStr) {
2359
2561
  function checkStaleAndRetry(verb) {
2360
2562
  let pidStr;
2361
2563
  try {
2362
- pidStr = readFileSync6(LOCK_PATH, "utf8").trim();
2564
+ pidStr = readFileSync7(LOCK_PATH, "utf8").trim();
2363
2565
  } catch {
2364
2566
  pidStr = "";
2365
2567
  }
@@ -2388,7 +2590,7 @@ function retryOnce(verb) {
2388
2590
  try {
2389
2591
  const fd = openSync2(LOCK_PATH, "wx");
2390
2592
  try {
2391
- writeFileSync4(fd, String(process.pid));
2593
+ writeFileSync3(fd, String(process.pid));
2392
2594
  } catch {
2393
2595
  try {
2394
2596
  closeSync2(fd);
@@ -2414,12 +2616,12 @@ function cmdDropSession(id) {
2414
2616
  fail(`invalid session id: ${id}`);
2415
2617
  process.exit(1);
2416
2618
  }
2417
- if (!existsSync18(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2619
+ if (!existsSync20(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2418
2620
  const handle = acquireLock("drop-session");
2419
2621
  if (handle === null) process.exit(0);
2420
2622
  try {
2421
- const repoProjects = join21(REPO_HOME, "shared", "projects");
2422
- if (!existsSync18(repoProjects)) {
2623
+ const repoProjects = join24(REPO_HOME, "shared", "projects");
2624
+ if (!existsSync20(repoProjects)) {
2423
2625
  throw new NomadFatal(`no staged session matches ${id}`);
2424
2626
  }
2425
2627
  const matches = collectMatches(repoProjects, id);
@@ -2440,13 +2642,13 @@ function cmdDropSession(id) {
2440
2642
  }
2441
2643
  function collectMatches(repoProjects, id) {
2442
2644
  const matches = [];
2443
- for (const logical of readdirSync5(repoProjects)) {
2444
- const candidate = join21(repoProjects, logical, `${id}.jsonl`);
2445
- if (existsSync18(candidate)) {
2645
+ for (const logical of readdirSync8(repoProjects)) {
2646
+ const candidate = join24(repoProjects, logical, `${id}.jsonl`);
2647
+ if (existsSync20(candidate)) {
2446
2648
  matches.push(relative4(REPO_HOME, candidate));
2447
2649
  }
2448
- const dir = join21(repoProjects, logical, id);
2449
- if (existsSync18(dir) && statSync4(dir).isDirectory()) {
2650
+ const dir = join24(repoProjects, logical, id);
2651
+ if (existsSync20(dir) && statSync5(dir).isDirectory()) {
2450
2652
  const dirRel = relative4(REPO_HOME, dir);
2451
2653
  const staged = expandStagedDir(dirRel);
2452
2654
  if (staged.length > 0) matches.push(...staged);
@@ -2462,12 +2664,12 @@ function unstageOne(rel) {
2462
2664
  }
2463
2665
  try {
2464
2666
  if (isTrackedInHead(rel)) {
2465
- execFileSync13("git", ["restore", "--staged", "--worktree", "--", rel], {
2667
+ execFileSync12("git", ["restore", "--staged", "--worktree", "--", rel], {
2466
2668
  cwd: REPO_HOME,
2467
2669
  stdio: ["ignore", "pipe", "pipe"]
2468
2670
  });
2469
2671
  } else {
2470
- execFileSync13("git", ["rm", "--cached", "-f", "--", rel], {
2672
+ execFileSync12("git", ["rm", "--cached", "-f", "--", rel], {
2471
2673
  cwd: REPO_HOME,
2472
2674
  stdio: ["ignore", "pipe", "pipe"]
2473
2675
  });
@@ -2482,13 +2684,13 @@ function unstageOne(rel) {
2482
2684
 
2483
2685
  // src/commands.redact.ts
2484
2686
  init_config();
2485
- import { existsSync as existsSync19, readFileSync as readFileSync7, statSync as statSync5, writeFileSync as writeFileSync5 } from "node:fs";
2486
- import { join as join23 } from "node:path";
2687
+ import { existsSync as existsSync21, readFileSync as readFileSync8, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
2688
+ import { join as join26 } from "node:path";
2487
2689
 
2488
2690
  // src/commands.redact.core.ts
2489
2691
  init_config();
2490
2692
  import { appendFileSync } from "node:fs";
2491
- import { join as join22 } from "node:path";
2693
+ import { join as join25 } from "node:path";
2492
2694
  function collectMatchIntervals(content, findings) {
2493
2695
  const intervals = [];
2494
2696
  for (const f of findings) {
@@ -2538,7 +2740,7 @@ function isRecentlyModified(mtimeMs, nowMs, thresholdMs = 5 * 60 * 1e3) {
2538
2740
  return nowMs - mtimeMs < thresholdMs;
2539
2741
  }
2540
2742
  function appendGitleaksIgnore(fingerprint) {
2541
- appendFileSync(join22(REPO_HOME, ".gitleaksignore"), formatFingerprint(fingerprint), "utf8");
2743
+ appendFileSync(join25(REPO_HOME, ".gitleaksignore"), formatFingerprint(fingerprint), "utf8");
2542
2744
  }
2543
2745
 
2544
2746
  // src/commands.redact.ts
@@ -2548,14 +2750,14 @@ init_utils_json();
2548
2750
  init_utils();
2549
2751
  function resolveLiveTranscript2(id) {
2550
2752
  try {
2551
- const mapPath = join23(REPO_HOME, "path-map.json");
2552
- if (!existsSync19(mapPath)) return null;
2753
+ const mapPath = join26(REPO_HOME, "path-map.json");
2754
+ if (!existsSync21(mapPath)) return null;
2553
2755
  const projects = readJson(mapPath).projects;
2554
2756
  for (const hostMap of Object.values(projects)) {
2555
2757
  const abs = hostMap[HOST];
2556
2758
  if (abs === void 0) continue;
2557
- const live = join23(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2558
- if (existsSync19(live)) return live;
2759
+ const live = join26(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2760
+ if (existsSync21(live)) return live;
2559
2761
  }
2560
2762
  return null;
2561
2763
  } catch {
@@ -2573,17 +2775,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
2573
2775
  fail(`invalid session id: ${id}`);
2574
2776
  process.exit(1);
2575
2777
  }
2576
- if (!existsSync19(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2778
+ if (!existsSync21(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2577
2779
  const handle = acquireLock("redact");
2578
2780
  if (handle === null) process.exit(0);
2579
2781
  try {
2580
2782
  const localPath = resolveLiveTranscript2(id);
2581
- if (localPath === null || !existsSync19(localPath)) {
2783
+ if (localPath === null || !existsSync21(localPath)) {
2582
2784
  fail(`could not resolve local transcript for session ${id} on this host`);
2583
2785
  process.exitCode = 1;
2584
2786
  return;
2585
2787
  }
2586
- const mtimeMs = statSync5(localPath).mtimeMs;
2788
+ const mtimeMs = statSync6(localPath).mtimeMs;
2587
2789
  if (isRecentlyModified(mtimeMs, nowMs())) {
2588
2790
  log(
2589
2791
  `session ${id} was modified recently and may be active.
@@ -2612,12 +2814,11 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
2612
2814
  );
2613
2815
  return;
2614
2816
  }
2615
- const backupBase = join23(HOME, ".cache", "claude-nomad", "backup");
2616
- const ts = freshBackupTs(backupBase);
2817
+ const ts = freshBackupTs(BACKUP_BASE);
2617
2818
  backupBeforeWrite(localPath, ts);
2618
- const original = readFileSync7(localPath, "utf8");
2819
+ const original = readFileSync8(localPath, "utf8");
2619
2820
  const redacted = applyRedactions(original, findings);
2620
- writeFileSync5(localPath, redacted, "utf8");
2821
+ writeFileSync4(localPath, redacted, "utf8");
2621
2822
  log(`redacted ${findings.length} finding(s) in ${localPath} (backup: ${ts})`);
2622
2823
  } catch (err) {
2623
2824
  if (!(err instanceof NomadFatal)) {
@@ -2631,8 +2832,8 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
2631
2832
  }
2632
2833
 
2633
2834
  // src/commands.pull.ts
2634
- import { existsSync as existsSync25, mkdirSync as mkdirSync8 } from "node:fs";
2635
- import { join as join29 } from "node:path";
2835
+ import { existsSync as existsSync27, mkdirSync as mkdirSync7 } from "node:fs";
2836
+ import { join as join32 } from "node:path";
2636
2837
 
2637
2838
  // src/commands.push.sections.ts
2638
2839
  init_color();
@@ -2728,15 +2929,15 @@ init_config();
2728
2929
 
2729
2930
  // src/extras-sync.ts
2730
2931
  init_config();
2731
- import { existsSync as existsSync22 } from "node:fs";
2732
- import { join as join26 } from "node:path";
2932
+ import { existsSync as existsSync24 } from "node:fs";
2933
+ import { join as join29 } from "node:path";
2733
2934
 
2734
2935
  // src/extras-sync.diff.ts
2735
2936
  init_utils();
2736
- import { execFileSync as execFileSync14 } from "node:child_process";
2937
+ import { execFileSync as execFileSync13 } from "node:child_process";
2737
2938
  function listDivergingFiles(a, b) {
2738
2939
  try {
2739
- const stdout = execFileSync14("git", ["diff", "--no-index", "--name-only", a, b], {
2940
+ const stdout = execFileSync13("git", ["diff", "--no-index", "--name-only", a, b], {
2740
2941
  stdio: ["ignore", "pipe", "pipe"]
2741
2942
  }).toString();
2742
2943
  return stdout.split("\n").filter((line) => line.length > 0);
@@ -2756,8 +2957,8 @@ function listDivergingFiles(a, b) {
2756
2957
 
2757
2958
  // src/extras-sync.core.ts
2758
2959
  init_config();
2759
- import { cpSync as cpSync4, existsSync as existsSync20, rmSync as rmSync6 } from "node:fs";
2760
- import { join as join24 } from "node:path";
2960
+ import { cpSync as cpSync4, existsSync as existsSync22, rmSync as rmSync7 } from "node:fs";
2961
+ import { join as join27 } from "node:path";
2761
2962
 
2762
2963
  // src/extras-sync.guards.ts
2763
2964
  init_utils();
@@ -2780,9 +2981,9 @@ function assertSafeLocalRoot(localRoot, logical) {
2780
2981
  init_utils();
2781
2982
  init_utils_json();
2782
2983
  function loadValidatedExtras(opts) {
2783
- const mapPath = join24(REPO_HOME, "path-map.json");
2784
- const repoExtras = join24(REPO_HOME, "shared", "extras");
2785
- if (!existsSync20(mapPath) || opts.requireRepoExtras === true && !existsSync20(repoExtras)) {
2984
+ const mapPath = join27(REPO_HOME, "path-map.json");
2985
+ const repoExtras = join27(REPO_HOME, "shared", "extras");
2986
+ if (!existsSync22(mapPath) || opts.requireRepoExtras === true && !existsSync22(repoExtras)) {
2786
2987
  if (opts.missingMsg !== void 0) log(opts.missingMsg);
2787
2988
  return null;
2788
2989
  }
@@ -2814,7 +3015,7 @@ function* eachExtrasTarget(v, counts) {
2814
3015
  }
2815
3016
  }
2816
3017
  function copyExtras(src, dst) {
2817
- rmSync6(dst, { recursive: true, force: true });
3018
+ rmSync7(dst, { recursive: true, force: true });
2818
3019
  cpSync4(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
2819
3020
  }
2820
3021
 
@@ -2824,8 +3025,8 @@ init_utils_json();
2824
3025
 
2825
3026
  // src/extras-sync.remap.ts
2826
3027
  init_config();
2827
- import { existsSync as existsSync21, mkdirSync as mkdirSync7 } from "node:fs";
2828
- import { join as join25 } from "node:path";
3028
+ import { existsSync as existsSync23, mkdirSync as mkdirSync6 } from "node:fs";
3029
+ import { join as join28 } from "node:path";
2829
3030
  init_utils_fs();
2830
3031
  function runExtrasOp(v, dryRun, paths, backup) {
2831
3032
  const counts = { unmapped: 0, skipped: 0 };
@@ -2833,7 +3034,7 @@ function runExtrasOp(v, dryRun, paths, backup) {
2833
3034
  const would = [];
2834
3035
  for (const t of eachExtrasTarget(v, counts)) {
2835
3036
  const { src, dst } = paths(t);
2836
- if (!existsSync21(src)) continue;
3037
+ if (!existsSync23(src)) continue;
2837
3038
  const item = `${t.logical}/${t.dirname}`;
2838
3039
  if (dryRun) {
2839
3040
  would.push(item);
@@ -2849,14 +3050,14 @@ function remapExtrasPush(ts, opts = {}) {
2849
3050
  const dryRun = opts.dryRun === true;
2850
3051
  const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
2851
3052
  if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
2852
- const repoExtras = join25(REPO_HOME, "shared", "extras");
2853
- if (!dryRun) mkdirSync7(repoExtras, { recursive: true });
3053
+ const repoExtras = join28(REPO_HOME, "shared", "extras");
3054
+ if (!dryRun) mkdirSync6(repoExtras, { recursive: true });
2854
3055
  const { unmapped, skipped, done, would } = runExtrasOp(
2855
3056
  v,
2856
3057
  dryRun,
2857
3058
  ({ localRoot, logical, dirname: dirname4 }) => ({
2858
- src: join25(localRoot, dirname4),
2859
- dst: join25(repoExtras, logical, dirname4)
3059
+ src: join28(localRoot, dirname4),
3060
+ dst: join28(repoExtras, logical, dirname4)
2860
3061
  }),
2861
3062
  (dst) => backupRepoWrite(dst, ts, REPO_HOME)
2862
3063
  );
@@ -2869,13 +3070,13 @@ function remapExtrasPull(ts, opts = {}) {
2869
3070
  missingMsg: "no path-map or repo extras dir; skipping extras remap"
2870
3071
  });
2871
3072
  if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
2872
- const repoExtras = join25(REPO_HOME, "shared", "extras");
3073
+ const repoExtras = join28(REPO_HOME, "shared", "extras");
2873
3074
  const { unmapped, skipped, done, would } = runExtrasOp(
2874
3075
  v,
2875
3076
  dryRun,
2876
3077
  ({ localRoot, logical, dirname: dirname4 }) => ({
2877
- src: join25(repoExtras, logical, dirname4),
2878
- dst: join25(localRoot, dirname4)
3078
+ src: join28(repoExtras, logical, dirname4),
3079
+ dst: join28(localRoot, dirname4)
2879
3080
  }),
2880
3081
  // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
2881
3082
  // localRoot so the backup tree mirrors the project layout.
@@ -2889,14 +3090,14 @@ function divergenceCheckExtras(ts) {
2889
3090
  const v = loadValidatedExtras({});
2890
3091
  if (v === null) return;
2891
3092
  const counts = { unmapped: 0, skipped: 0 };
2892
- const backupRoot = join26(HOME, ".cache", "claude-nomad", "backup", ts, "extras");
3093
+ const backupRoot = join29(BACKUP_BASE, ts, "extras");
2893
3094
  for (const { logical, localRoot, dirname: dirname4 } of eachExtrasTarget(v, counts)) {
2894
- const local = join26(localRoot, dirname4);
2895
- const repo = join26(REPO_HOME, "shared", "extras", logical, dirname4);
2896
- if (!existsSync22(local) || !existsSync22(repo)) continue;
3095
+ const local = join29(localRoot, dirname4);
3096
+ const repo = join29(REPO_HOME, "shared", "extras", logical, dirname4);
3097
+ if (!existsSync24(local) || !existsSync24(repo)) continue;
2897
3098
  const diff = listDivergingFiles(local, repo);
2898
3099
  if (diff.length === 0) continue;
2899
- const projectBackupRoot = join26(backupRoot, encodePath(localRoot));
3100
+ const projectBackupRoot = join29(backupRoot, encodePath(localRoot));
2900
3101
  warn(
2901
3102
  `local ${dirname4} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
2902
3103
  );
@@ -2909,47 +3110,47 @@ init_config();
2909
3110
  init_utils();
2910
3111
  init_utils_fs();
2911
3112
  init_utils_json();
2912
- import { existsSync as existsSync23, lstatSync as lstatSync4, rmSync as rmSync7 } from "node:fs";
2913
- import { join as join27 } from "node:path";
3113
+ import { existsSync as existsSync25, lstatSync as lstatSync6, rmSync as rmSync8 } from "node:fs";
3114
+ import { join as join30 } from "node:path";
2914
3115
  function applySharedLinks(ts, map, opts = {}) {
2915
3116
  const dryRun = opts.dryRun === true;
2916
3117
  const linkNames = allSharedLinks(map);
2917
3118
  for (const name of linkNames) {
2918
- const linkPath = join27(CLAUDE_HOME, name);
2919
- const target = join27(REPO_HOME, "shared", name);
2920
- if (!existsSync23(linkPath)) continue;
2921
- if (lstatSync4(linkPath).isSymbolicLink()) continue;
2922
- if (!existsSync23(target)) continue;
3119
+ const linkPath = join30(CLAUDE_HOME, name);
3120
+ const target = join30(REPO_HOME, "shared", name);
3121
+ if (!existsSync25(linkPath)) continue;
3122
+ if (lstatSync6(linkPath).isSymbolicLink()) continue;
3123
+ if (!existsSync25(target)) continue;
2923
3124
  if (dryRun) {
2924
3125
  log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
2925
3126
  continue;
2926
3127
  }
2927
3128
  backupBeforeWrite(linkPath, ts);
2928
- rmSync7(linkPath, { recursive: true, force: true });
3129
+ rmSync8(linkPath, { recursive: true, force: true });
2929
3130
  }
2930
3131
  for (const name of linkNames) {
2931
- const target = join27(REPO_HOME, "shared", name);
2932
- if (!existsSync23(target)) continue;
3132
+ const target = join30(REPO_HOME, "shared", name);
3133
+ if (!existsSync25(target)) continue;
2933
3134
  if (dryRun) {
2934
- log(`would create symlink: ${join27(CLAUDE_HOME, name)} -> ${target}`);
3135
+ log(`would create symlink: ${join30(CLAUDE_HOME, name)} -> ${target}`);
2935
3136
  continue;
2936
3137
  }
2937
- ensureSymlink(join27(CLAUDE_HOME, name), target);
3138
+ ensureSymlink(join30(CLAUDE_HOME, name), target);
2938
3139
  }
2939
3140
  }
2940
3141
  function regenerateSettings(ts, opts = {}) {
2941
3142
  const dryRun = opts.dryRun === true;
2942
- const basePath = join27(REPO_HOME, "shared", "settings.base.json");
2943
- const hostPath = join27(REPO_HOME, "hosts", `${HOST}.json`);
2944
- if (!existsSync23(basePath)) {
3143
+ const basePath = join30(REPO_HOME, "shared", "settings.base.json");
3144
+ const hostPath = join30(REPO_HOME, "hosts", `${HOST}.json`);
3145
+ if (!existsSync25(basePath)) {
2945
3146
  die("repo not initialized; run 'nomad init' to scaffold");
2946
3147
  }
2947
3148
  const base = readJson(basePath);
2948
- const hasOverrides = existsSync23(hostPath);
3149
+ const hasOverrides = existsSync25(hostPath);
2949
3150
  const overrides = hasOverrides ? readJson(hostPath) : {};
2950
3151
  const merged = deepMerge(base, overrides);
2951
- const settingsPath = join27(CLAUDE_HOME, "settings.json");
2952
- if (!hasOverrides && existsSync23(settingsPath)) {
3152
+ const settingsPath = join30(CLAUDE_HOME, "settings.json");
3153
+ if (!hasOverrides && existsSync25(settingsPath)) {
2953
3154
  try {
2954
3155
  const existing = readJson(settingsPath);
2955
3156
  const baseKeys = new Set(Object.keys(base));
@@ -2975,8 +3176,8 @@ function regenerateSettings(ts, opts = {}) {
2975
3176
 
2976
3177
  // src/preview.ts
2977
3178
  init_config();
2978
- import { existsSync as existsSync24 } from "node:fs";
2979
- import { join as join28 } from "node:path";
3179
+ import { existsSync as existsSync26 } from "node:fs";
3180
+ import { join as join31 } from "node:path";
2980
3181
 
2981
3182
  // node_modules/diff/libesm/diff/base.js
2982
3183
  var Diff = class {
@@ -3262,7 +3463,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
3262
3463
  return lines.join("\n");
3263
3464
  }
3264
3465
  function readJsonOrNull(path) {
3265
- if (!existsSync24(path)) return null;
3466
+ if (!existsSync26(path)) return null;
3266
3467
  try {
3267
3468
  return readJson(path);
3268
3469
  } catch {
@@ -3276,12 +3477,12 @@ function previewSettings(basePath, hostPath, settingsPath) {
3276
3477
  return;
3277
3478
  }
3278
3479
  const hostOverrides = readJsonOrNull(hostPath);
3279
- if (hostOverrides === null && existsSync24(hostPath)) {
3480
+ if (hostOverrides === null && existsSync26(hostPath)) {
3280
3481
  log(`settings.json: malformed hosts/${HOST}.json; ignoring overrides`);
3281
3482
  }
3282
3483
  const merged = deepMerge(base, hostOverrides ?? {});
3283
3484
  const current = readJsonOrNull(settingsPath);
3284
- if (current === null && existsSync24(settingsPath)) {
3485
+ if (current === null && existsSync26(settingsPath)) {
3285
3486
  log("settings.json: malformed; skipping diff");
3286
3487
  return;
3287
3488
  }
@@ -3300,9 +3501,9 @@ function computePreview(ts, map) {
3300
3501
  log(`would pull on host=${HOST} (dry-run; no mutation)`);
3301
3502
  applySharedLinks(ts, map, { dryRun: true });
3302
3503
  previewSettings(
3303
- join28(REPO_HOME, "shared", "settings.base.json"),
3304
- join28(REPO_HOME, "hosts", `${HOST}.json`),
3305
- join28(CLAUDE_HOME, "settings.json")
3504
+ join31(REPO_HOME, "shared", "settings.base.json"),
3505
+ join31(REPO_HOME, "hosts", `${HOST}.json`),
3506
+ join31(CLAUDE_HOME, "settings.json")
3306
3507
  );
3307
3508
  const remapResult = remapPull(ts, { dryRun: true });
3308
3509
  return { unmapped: remapResult.unmapped, collisions: 0 };
@@ -3331,19 +3532,18 @@ function applyWetPull(ts, map) {
3331
3532
  }
3332
3533
  function cmdPull(opts = {}) {
3333
3534
  const dryRun = opts.dryRun === true;
3334
- if (!existsSync25(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3335
- if (!existsSync25(join29(REPO_HOME, "shared", "settings.base.json"))) {
3535
+ if (!existsSync27(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3536
+ if (!existsSync27(join32(REPO_HOME, "shared", "settings.base.json"))) {
3336
3537
  die("repo not initialized; run 'nomad init' to scaffold");
3337
3538
  }
3338
3539
  const handle = acquireLock("pull");
3339
3540
  if (handle === null) process.exit(0);
3340
3541
  try {
3341
- const backupBase = join29(HOME, ".cache", "claude-nomad", "backup");
3342
- const ts = freshBackupTs(backupBase);
3542
+ const ts = freshBackupTs(BACKUP_BASE);
3343
3543
  if (!dryRun) {
3344
- const backupRoot = join29(backupBase, ts);
3544
+ const backupRoot = join32(BACKUP_BASE, ts);
3345
3545
  try {
3346
- mkdirSync8(backupRoot, { recursive: true });
3546
+ mkdirSync7(backupRoot, { recursive: true });
3347
3547
  } catch (err) {
3348
3548
  die(`could not create backup dir: ${err.message}`);
3349
3549
  }
@@ -3352,8 +3552,8 @@ function cmdPull(opts = {}) {
3352
3552
  dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
3353
3553
  );
3354
3554
  gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
3355
- const mapPath = join29(REPO_HOME, "path-map.json");
3356
- const map = existsSync25(mapPath) ? readPathMap(mapPath) : { projects: {} };
3555
+ const mapPath = join32(REPO_HOME, "path-map.json");
3556
+ const map = existsSync27(mapPath) ? readPathMap(mapPath) : { projects: {} };
3357
3557
  divergenceCheckExtras(ts);
3358
3558
  if (dryRun) {
3359
3559
  const previewResult = computePreview(ts, map);
@@ -3376,8 +3576,8 @@ function cmdPull(opts = {}) {
3376
3576
 
3377
3577
  // src/commands.push.ts
3378
3578
  init_config();
3379
- import { existsSync as existsSync27 } from "node:fs";
3380
- import { join as join33, relative as relative5 } from "node:path";
3579
+ import { existsSync as existsSync29 } from "node:fs";
3580
+ import { join as join36, relative as relative5 } from "node:path";
3381
3581
 
3382
3582
  // src/commands.push.allowlist.ts
3383
3583
  init_config();
@@ -3454,8 +3654,8 @@ import { createInterface } from "node:readline/promises";
3454
3654
 
3455
3655
  // src/commands.push.recovery.redact.ts
3456
3656
  init_config();
3457
- import { cpSync as cpSync5, readFileSync as readFileSync8, statSync as statSync6, writeFileSync as writeFileSync6 } from "node:fs";
3458
- import { join as join30, sep as sep2 } from "node:path";
3657
+ import { cpSync as cpSync5, readFileSync as readFileSync9, statSync as statSync7, writeFileSync as writeFileSync5 } from "node:fs";
3658
+ import { join as join33, sep as sep2 } from "node:path";
3459
3659
  init_push_gitleaks_scan();
3460
3660
  init_utils_fs();
3461
3661
  init_utils_json();
@@ -3499,7 +3699,7 @@ function applyRedact(f, allFindings, ts, map, nowMs, scan = scanFile) {
3499
3699
  `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
3500
3700
  );
3501
3701
  }
3502
- if (isRecentlyModified(statSync6(localPath).mtimeMs, nowMs())) {
3702
+ if (isRecentlyModified(statSync7(localPath).mtimeMs, nowMs())) {
3503
3703
  return refuse(
3504
3704
  `session ${sid} looks active (modified within the last 5 minutes); refusing to redact, no changes made.
3505
3705
  End the session and choose Redact again, or choose Drop session (holds this session back from the push, local copy kept) or Skip.`
@@ -3515,13 +3715,13 @@ function applyRedact(f, allFindings, ts, map, nowMs, scan = scanFile) {
3515
3715
  );
3516
3716
  }
3517
3717
  backupBeforeWrite(localPath, ts);
3518
- writeFileSync6(localPath, applyRedactions(readFileSync8(localPath, "utf8"), realFindings), "utf8");
3718
+ writeFileSync5(localPath, applyRedactions(readFileSync9(localPath, "utf8"), realFindings), "utf8");
3519
3719
  let copied = false;
3520
3720
  for (const [logical, hostMap] of Object.entries(map.projects)) {
3521
3721
  const abs = hostMap[HOST];
3522
3722
  if (abs === void 0) continue;
3523
- if (localPath.startsWith(join30(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
3524
- cpSync5(localPath, join30(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`), {
3723
+ if (localPath.startsWith(join33(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
3724
+ cpSync5(localPath, join33(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`), {
3525
3725
  force: true
3526
3726
  });
3527
3727
  copied = true;
@@ -3538,16 +3738,16 @@ function applyRedact(f, allFindings, ts, map, nowMs, scan = scanFile) {
3538
3738
 
3539
3739
  // src/commands.push.recovery.drop.ts
3540
3740
  init_config();
3541
- import { rmSync as rmSync8 } from "node:fs";
3542
- import { join as join31 } from "node:path";
3741
+ import { rmSync as rmSync9 } from "node:fs";
3742
+ import { join as join34 } from "node:path";
3543
3743
  function dropSessionFromStaged(sid, map) {
3544
3744
  const logicals = Object.keys(map.projects);
3545
3745
  if (logicals.length === 0) return false;
3546
3746
  for (const logical of logicals) {
3547
- const jsonl = join31(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
3548
- const dir = join31(REPO_HOME, "shared", "projects", logical, sid);
3549
- rmSync8(jsonl, { force: true });
3550
- rmSync8(dir, { recursive: true, force: true });
3747
+ const jsonl = join34(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
3748
+ const dir = join34(REPO_HOME, "shared", "projects", logical, sid);
3749
+ rmSync9(jsonl, { force: true });
3750
+ rmSync9(dir, { recursive: true, force: true });
3551
3751
  }
3552
3752
  return true;
3553
3753
  }
@@ -3702,9 +3902,9 @@ init_push_checks();
3702
3902
  init_color();
3703
3903
  init_config();
3704
3904
  import { randomBytes as randomBytes2 } from "node:crypto";
3705
- import { existsSync as existsSync26, mkdirSync as mkdirSync9, readdirSync as readdirSync6, rmSync as rmSync9 } from "node:fs";
3905
+ import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync9, rmSync as rmSync10 } from "node:fs";
3706
3906
  import { homedir as homedir5 } from "node:os";
3707
- import { join as join32 } from "node:path";
3907
+ import { join as join35 } from "node:path";
3708
3908
  init_push_leak_verdict();
3709
3909
  init_push_gitleaks();
3710
3910
  init_utils_fs();
@@ -3719,13 +3919,13 @@ function stageSessions(tmpRoot, map) {
3719
3919
  if (!p || p === "TBD") continue;
3720
3920
  reverse.set(encodePath(p), logical);
3721
3921
  }
3722
- const localProjects = join32(CLAUDE_HOME, "projects");
3723
- if (!existsSync26(localProjects)) return 0;
3922
+ const localProjects = join35(CLAUDE_HOME, "projects");
3923
+ if (!existsSync28(localProjects)) return 0;
3724
3924
  let staged = 0;
3725
- for (const dir of readdirSync6(localProjects)) {
3925
+ for (const dir of readdirSync9(localProjects)) {
3726
3926
  const logical = reverse.get(dir);
3727
3927
  if (!logical) continue;
3728
- copyDirJsonlOnly(join32(localProjects, dir), join32(tmpRoot, "shared", "projects", logical));
3928
+ copyDirJsonlOnly(join35(localProjects, dir), join35(tmpRoot, "shared", "projects", logical));
3729
3929
  staged++;
3730
3930
  }
3731
3931
  return staged;
@@ -3741,9 +3941,9 @@ function stageExtras(tmpRoot, map) {
3741
3941
  if (!localRoot || localRoot === "TBD") continue;
3742
3942
  for (const dirname4 of dirnames) {
3743
3943
  if (!whitelist.includes(dirname4)) continue;
3744
- const src = join32(localRoot, dirname4);
3745
- if (!existsSync26(src)) continue;
3746
- const dst = join32(tmpRoot, "shared", "extras", logical, dirname4);
3944
+ const src = join35(localRoot, dirname4);
3945
+ if (!existsSync28(src)) continue;
3946
+ const dst = join35(tmpRoot, "shared", "extras", logical, dirname4);
3747
3947
  copyExtras(src, dst);
3748
3948
  staged++;
3749
3949
  }
@@ -3751,10 +3951,10 @@ function stageExtras(tmpRoot, map) {
3751
3951
  return staged;
3752
3952
  }
3753
3953
  function previewPushLeaks(map) {
3754
- const cacheDir = join32(homedir5(), ".cache", "claude-nomad");
3755
- mkdirSync9(cacheDir, { recursive: true });
3954
+ const cacheDir = join35(homedir5(), ".cache", "claude-nomad");
3955
+ mkdirSync8(cacheDir, { recursive: true });
3756
3956
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
3757
- const tmpRoot = join32(cacheDir, `push-preview-tree-${stamp}`);
3957
+ const tmpRoot = join35(cacheDir, `push-preview-tree-${stamp}`);
3758
3958
  try {
3759
3959
  const sessionCount = stageSessions(tmpRoot, map);
3760
3960
  const extrasCount = stageExtras(tmpRoot, map);
@@ -3772,7 +3972,7 @@ function previewPushLeaks(map) {
3772
3972
  }
3773
3973
  return verdictFromFindings(findings);
3774
3974
  } finally {
3775
- rmSync9(tmpRoot, { recursive: true, force: true });
3975
+ rmSync10(tmpRoot, { recursive: true, force: true });
3776
3976
  }
3777
3977
  }
3778
3978
 
@@ -3781,7 +3981,7 @@ init_utils();
3781
3981
  init_utils_fs();
3782
3982
  init_utils_json();
3783
3983
  function guardGitlinks() {
3784
- const gitlinks = findGitlinks(join33(REPO_HOME, "shared"));
3984
+ const gitlinks = findGitlinks(join36(REPO_HOME, "shared"));
3785
3985
  if (gitlinks.length === 0) return;
3786
3986
  for (const p of gitlinks) {
3787
3987
  const rel = relative5(REPO_HOME, p);
@@ -3815,15 +4015,14 @@ function runDryRunPreview(st, map) {
3815
4015
  async function cmdPush(opts = {}) {
3816
4016
  const dryRun = opts.dryRun === true;
3817
4017
  const redactAll = opts.redactAll === true;
3818
- if (!existsSync27(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4018
+ if (!existsSync29(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3819
4019
  const handle = acquireLock("push");
3820
4020
  if (handle === null) process.exit(0);
3821
4021
  try {
3822
4022
  console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
3823
4023
  probeGitleaks();
3824
4024
  rebaseBeforePush();
3825
- const backupBase = join33(HOME, ".cache", "claude-nomad", "backup");
3826
- const ts = freshBackupTs(backupBase);
4025
+ const ts = freshBackupTs(BACKUP_BASE);
3827
4026
  const remap = remapPush(ts, { dryRun });
3828
4027
  const extras = remapExtrasPush(ts, { dryRun });
3829
4028
  const st = { dryRun, remap, extras };
@@ -3834,8 +4033,8 @@ async function cmdPush(opts = {}) {
3834
4033
  renderNoScanTree(st);
3835
4034
  return;
3836
4035
  }
3837
- const mapPath = join33(REPO_HOME, "path-map.json");
3838
- if (!existsSync27(mapPath)) {
4036
+ const mapPath = join36(REPO_HOME, "path-map.json");
4037
+ if (!existsSync29(mapPath)) {
3839
4038
  if (dryRun) return runDryRunPreview(st, null);
3840
4039
  die("path-map.json missing, cannot enforce push allow-list");
3841
4040
  }
@@ -3856,9 +4055,9 @@ async function cmdPush(opts = {}) {
3856
4055
  }
3857
4056
 
3858
4057
  // src/commands.update.ts
3859
- import { execFileSync as execFileSync15 } from "node:child_process";
4058
+ import { execFileSync as execFileSync14 } from "node:child_process";
3860
4059
  init_utils();
3861
- function cmdUpdate(run = execFileSync15) {
4060
+ function cmdUpdate(run = execFileSync14) {
3862
4061
  try {
3863
4062
  run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
3864
4063
  } catch (err) {
@@ -3875,18 +4074,17 @@ init_config();
3875
4074
 
3876
4075
  // src/diff.ts
3877
4076
  init_config();
3878
- import { existsSync as existsSync28 } from "node:fs";
3879
- import { join as join34 } from "node:path";
4077
+ import { existsSync as existsSync30 } from "node:fs";
4078
+ import { join as join37 } from "node:path";
3880
4079
  init_utils();
3881
4080
  init_utils_fs();
3882
4081
  init_utils_json();
3883
4082
  function cmdDiff() {
3884
4083
  try {
3885
- if (!existsSync28(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3886
- const backupBase = join34(HOME, ".cache", "claude-nomad", "backup");
3887
- const ts = freshBackupTs(backupBase);
3888
- const mapPath = join34(REPO_HOME, "path-map.json");
3889
- const map = existsSync28(mapPath) ? readPathMap(mapPath) : { projects: {} };
4084
+ if (!existsSync30(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4085
+ const ts = freshBackupTs(BACKUP_BASE);
4086
+ const mapPath = join37(REPO_HOME, "path-map.json");
4087
+ const map = existsSync30(mapPath) ? readPathMap(mapPath) : { projects: {} };
3890
4088
  const result = computePreview(ts, map);
3891
4089
  emitSummary("diff", result.unmapped);
3892
4090
  } catch (err) {
@@ -3901,19 +4099,19 @@ function cmdDiff() {
3901
4099
 
3902
4100
  // src/init.ts
3903
4101
  init_config();
3904
- import { existsSync as existsSync30, mkdirSync as mkdirSync10, writeFileSync as writeFileSync7 } from "node:fs";
3905
- import { join as join36 } from "node:path";
4102
+ import { existsSync as existsSync32, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "node:fs";
4103
+ import { join as join39 } from "node:path";
3906
4104
 
3907
4105
  // src/init.gh-onboard.ts
3908
4106
  init_config();
3909
- import { execFileSync as execFileSync16 } from "node:child_process";
4107
+ import { execFileSync as execFileSync15 } from "node:child_process";
3910
4108
  init_utils();
3911
4109
  var DEFAULT_REPO_NAME = "claude-nomad-config";
3912
4110
  function isValidRepoName(name) {
3913
4111
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
3914
4112
  }
3915
4113
  var GH_NETWORK_TIMEOUT_MS = 3e4;
3916
- function ensureOriginRepo(repoName, run = execFileSync16) {
4114
+ function ensureOriginRepo(repoName, run = execFileSync15) {
3917
4115
  if (!isValidRepoName(repoName)) {
3918
4116
  die(
3919
4117
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -3983,31 +4181,31 @@ init_config();
3983
4181
  init_utils();
3984
4182
  init_utils_fs();
3985
4183
  init_utils_json();
3986
- import { copyFileSync, cpSync as cpSync6, existsSync as existsSync29, rmSync as rmSync10, statSync as statSync7 } from "node:fs";
3987
- import { join as join35 } from "node:path";
4184
+ import { copyFileSync, cpSync as cpSync6, existsSync as existsSync31, rmSync as rmSync11, statSync as statSync8 } from "node:fs";
4185
+ import { join as join38 } from "node:path";
3988
4186
  function snapshotIntoShared(map) {
3989
4187
  for (const name of allSharedLinks(map)) {
3990
- const src = join35(CLAUDE_HOME, name);
3991
- if (!existsSync29(src)) continue;
3992
- const dst = join35(REPO_HOME, "shared", name);
3993
- if (statSync7(src).isDirectory()) {
3994
- const gk = join35(dst, ".gitkeep");
3995
- if (existsSync29(gk)) rmSync10(gk);
4188
+ const src = join38(CLAUDE_HOME, name);
4189
+ if (!existsSync31(src)) continue;
4190
+ const dst = join38(REPO_HOME, "shared", name);
4191
+ if (statSync8(src).isDirectory()) {
4192
+ const gk = join38(dst, ".gitkeep");
4193
+ if (existsSync31(gk)) rmSync11(gk);
3996
4194
  cpSync6(src, dst, { recursive: true, force: false, errorOnExist: true });
3997
4195
  } else {
3998
4196
  copyFileSync(src, dst);
3999
4197
  }
4000
4198
  log(`snapshotted shared/${name} from ${src}`);
4001
4199
  }
4002
- const userSettings = join35(CLAUDE_HOME, "settings.json");
4003
- if (existsSync29(userSettings)) {
4200
+ const userSettings = join38(CLAUDE_HOME, "settings.json");
4201
+ if (existsSync31(userSettings)) {
4004
4202
  let parsed;
4005
4203
  try {
4006
4204
  parsed = readJson(userSettings);
4007
4205
  } catch (err) {
4008
4206
  return die(`malformed ${userSettings}: ${err.message}`);
4009
4207
  }
4010
- const hostFile = join35(REPO_HOME, "hosts", `${HOST}.json`);
4208
+ const hostFile = join38(REPO_HOME, "hosts", `${HOST}.json`);
4011
4209
  writeJsonAtomic(hostFile, parsed);
4012
4210
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
4013
4211
  }
@@ -4020,45 +4218,45 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
4020
4218
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
4021
4219
  function preflightConflict(repoHome) {
4022
4220
  const candidates = [
4023
- join36(repoHome, "shared", "settings.base.json"),
4024
- join36(repoHome, "shared", "CLAUDE.md"),
4025
- join36(repoHome, "path-map.json"),
4026
- join36(repoHome, "hosts"),
4027
- join36(repoHome, "shared")
4221
+ join39(repoHome, "shared", "settings.base.json"),
4222
+ join39(repoHome, "shared", "CLAUDE.md"),
4223
+ join39(repoHome, "path-map.json"),
4224
+ join39(repoHome, "hosts"),
4225
+ join39(repoHome, "shared")
4028
4226
  ];
4029
4227
  for (const c of candidates) {
4030
- if (existsSync30(c)) return c;
4228
+ if (existsSync32(c)) return c;
4031
4229
  }
4032
4230
  return null;
4033
4231
  }
4034
4232
  function cmdInit(opts = {}) {
4035
4233
  const snapshot = opts.snapshot === true;
4036
4234
  const keepActions = opts.keepActions === true;
4037
- mkdirSync10(REPO_HOME, { recursive: true });
4235
+ mkdirSync9(REPO_HOME, { recursive: true });
4038
4236
  const conflict = preflightConflict(REPO_HOME);
4039
4237
  if (conflict !== null) {
4040
4238
  die(`already initialized; refusing to clobber ${conflict}`);
4041
4239
  }
4042
4240
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
4043
- mkdirSync10(join36(REPO_HOME, "shared"), { recursive: true });
4044
- mkdirSync10(join36(REPO_HOME, "hosts"), { recursive: true });
4241
+ mkdirSync9(join39(REPO_HOME, "shared"), { recursive: true });
4242
+ mkdirSync9(join39(REPO_HOME, "hosts"), { recursive: true });
4045
4243
  for (const name of SHARED_KEEP_DIRS) {
4046
- mkdirSync10(join36(REPO_HOME, "shared", name), { recursive: true });
4244
+ mkdirSync9(join39(REPO_HOME, "shared", name), { recursive: true });
4047
4245
  }
4048
- const userClaudeMd = join36(CLAUDE_HOME, "CLAUDE.md");
4049
- if (!snapshot || !existsSync30(userClaudeMd)) {
4050
- writeFileSync7(join36(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
4246
+ const userClaudeMd = join39(CLAUDE_HOME, "CLAUDE.md");
4247
+ if (!snapshot || !existsSync32(userClaudeMd)) {
4248
+ writeFileSync6(join39(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
4051
4249
  log("created shared/CLAUDE.md");
4052
4250
  }
4053
4251
  for (const name of SHARED_KEEP_DIRS) {
4054
- writeFileSync7(join36(REPO_HOME, "shared", name, ".gitkeep"), "");
4252
+ writeFileSync6(join39(REPO_HOME, "shared", name, ".gitkeep"), "");
4055
4253
  log(`created shared/${name}/.gitkeep`);
4056
4254
  }
4057
- writeFileSync7(join36(REPO_HOME, "hosts", ".gitkeep"), "");
4255
+ writeFileSync6(join39(REPO_HOME, "hosts", ".gitkeep"), "");
4058
4256
  log("created hosts/.gitkeep");
4059
- writeJsonAtomic(join36(REPO_HOME, "shared", "settings.base.json"), {});
4257
+ writeJsonAtomic(join39(REPO_HOME, "shared", "settings.base.json"), {});
4060
4258
  log("created shared/settings.base.json");
4061
- writeJsonAtomic(join36(REPO_HOME, "path-map.json"), { projects: {} });
4259
+ writeJsonAtomic(join39(REPO_HOME, "path-map.json"), { projects: {} });
4062
4260
  log("created path-map.json");
4063
4261
  if (snapshot) {
4064
4262
  snapshotIntoShared({ projects: {} });
@@ -4210,10 +4408,63 @@ function parseRedactArgs(argv) {
4210
4408
  return { id, rule, dryRun };
4211
4409
  }
4212
4410
 
4411
+ // src/nomad.dispatch.clean.ts
4412
+ var REJECT = { ok: false, advance: 0 };
4413
+ function applyOlderThan(argv, i, st) {
4414
+ if (st.olderThan !== void 0) return REJECT;
4415
+ const val = extractFlagValue(argv, i);
4416
+ if (val === null) return REJECT;
4417
+ st.olderThan = val;
4418
+ return { ok: true, advance: 2 };
4419
+ }
4420
+ function applyKeep(argv, i, st) {
4421
+ if (st.keep !== void 0) return REJECT;
4422
+ const val = extractFlagValue(argv, i);
4423
+ if (val === null || !/^\d+$/.test(val)) return REJECT;
4424
+ st.keep = Number(val);
4425
+ return { ok: true, advance: 2 };
4426
+ }
4427
+ function applyBool(seen, set) {
4428
+ if (seen) return REJECT;
4429
+ set();
4430
+ return { ok: true, advance: 1 };
4431
+ }
4432
+ function applyCleanToken(argv, i, st) {
4433
+ switch (argv[i]) {
4434
+ case "--backups":
4435
+ return applyBool(st.backups, () => st.backups = true);
4436
+ case "--dry-run":
4437
+ return applyBool(st.dryRun, () => st.dryRun = true);
4438
+ case "--older-than":
4439
+ return applyOlderThan(argv, i, st);
4440
+ case "--keep":
4441
+ return applyKeep(argv, i, st);
4442
+ default:
4443
+ return REJECT;
4444
+ }
4445
+ }
4446
+ function parseCleanArgs(argv) {
4447
+ const st = {
4448
+ backups: false,
4449
+ dryRun: false,
4450
+ olderThan: void 0,
4451
+ keep: void 0
4452
+ };
4453
+ let i = 3;
4454
+ while (i < argv.length) {
4455
+ const { ok: ok2, advance } = applyCleanToken(argv, i, st);
4456
+ if (!ok2) return null;
4457
+ i += advance;
4458
+ }
4459
+ if (!st.backups) return null;
4460
+ if (st.olderThan !== void 0 && st.keep !== void 0) return null;
4461
+ return { dryRun: st.dryRun, olderThan: st.olderThan, keep: st.keep };
4462
+ }
4463
+
4213
4464
  // package.json
4214
4465
  var package_default = {
4215
4466
  name: "claude-nomad",
4216
- version: "0.34.1",
4467
+ version: "0.36.0",
4217
4468
  type: "module",
4218
4469
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
4219
4470
  keywords: [
@@ -4363,6 +4614,14 @@ var DEFAULT_HELP = [
4363
4614
  "",
4364
4615
  row(" update", "Update the claude-nomad CLI to the latest npm release."),
4365
4616
  "",
4617
+ row(" clean", "Prune old backup snapshots under ~/.cache/claude-nomad/backup/."),
4618
+ row(" --backups", "Required: confirm backup pruning is the intended target."),
4619
+ row(" --dry-run", "List the snapshots that would be removed without deleting."),
4620
+ row(" --older-than <dur>", "Delete snapshots older than a duration (e.g. 14d, 24h, 30m)."),
4621
+ cont("Default when no retention flag is given: 14d."),
4622
+ row(" --keep <N>", "Keep the N most-recent snapshots, delete the rest. Mutually"),
4623
+ cont("exclusive with --older-than."),
4624
+ "",
4366
4625
  row(" --version", "Print the installed CLI version as bare semver to stdout; exits 0."),
4367
4626
  "",
4368
4627
  "Run `nomad doctor` to validate your setup. Edit shared/ or hosts/<HOST>.json",
@@ -4374,15 +4633,15 @@ var DEFAULT_HELP = [
4374
4633
  init_config();
4375
4634
  init_utils();
4376
4635
  init_utils_json();
4377
- import { existsSync as existsSync31, readFileSync as readFileSync9, readdirSync as readdirSync7 } from "node:fs";
4378
- import { join as join37 } from "node:path";
4636
+ import { existsSync as existsSync33, readFileSync as readFileSync10, readdirSync as readdirSync10 } from "node:fs";
4637
+ import { join as join40 } from "node:path";
4379
4638
  function resumeCmd(sessionId) {
4380
4639
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
4381
4640
  fail(`invalid session id: ${sessionId}`);
4382
4641
  process.exit(1);
4383
4642
  }
4384
- const projectsRoot = join37(CLAUDE_HOME, "projects");
4385
- if (!existsSync31(projectsRoot)) {
4643
+ const projectsRoot = join40(CLAUDE_HOME, "projects");
4644
+ if (!existsSync33(projectsRoot)) {
4386
4645
  fail(`${projectsRoot} does not exist`);
4387
4646
  process.exit(1);
4388
4647
  }
@@ -4396,8 +4655,8 @@ function resumeCmd(sessionId) {
4396
4655
  fail(`no cwd field found in ${jsonlPath}`);
4397
4656
  process.exit(1);
4398
4657
  }
4399
- const mapPath = join37(REPO_HOME, "path-map.json");
4400
- if (!existsSync31(mapPath)) {
4658
+ const mapPath = join40(REPO_HOME, "path-map.json");
4659
+ if (!existsSync33(mapPath)) {
4401
4660
  fail("path-map.json missing");
4402
4661
  process.exit(1);
4403
4662
  }
@@ -4419,14 +4678,14 @@ function resumeCmd(sessionId) {
4419
4678
  console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
4420
4679
  }
4421
4680
  function findTranscriptPath(projectsRoot, sessionId) {
4422
- for (const dir of readdirSync7(projectsRoot)) {
4423
- const candidate = join37(projectsRoot, dir, `${sessionId}.jsonl`);
4424
- if (existsSync31(candidate)) return candidate;
4681
+ for (const dir of readdirSync10(projectsRoot)) {
4682
+ const candidate = join40(projectsRoot, dir, `${sessionId}.jsonl`);
4683
+ if (existsSync33(candidate)) return candidate;
4425
4684
  }
4426
4685
  return null;
4427
4686
  }
4428
4687
  function extractRecordedCwd(jsonlPath) {
4429
- for (const line of readFileSync9(jsonlPath, "utf8").split("\n")) {
4688
+ for (const line of readFileSync10(jsonlPath, "utf8").split("\n")) {
4430
4689
  if (!line.trim()) continue;
4431
4690
  try {
4432
4691
  const obj = JSON.parse(line);
@@ -4590,6 +4849,15 @@ try {
4590
4849
  cmdRedact(redactArgs);
4591
4850
  break;
4592
4851
  }
4852
+ case "clean": {
4853
+ const cleanArgs = parseCleanArgs(process.argv);
4854
+ if (cleanArgs === null) {
4855
+ console.error("usage: nomad clean --backups [--dry-run] [--older-than <dur> | --keep <N>]");
4856
+ process.exit(1);
4857
+ }
4858
+ cmdClean(cleanArgs);
4859
+ break;
4860
+ }
4593
4861
  default:
4594
4862
  console.error(DEFAULT_HELP);
4595
4863
  process.exit(1);