claude-nomad 0.34.1 → 0.35.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, 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,6 +350,7 @@ 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";
355
356
  GITLEAKS_PINNED_VERSION = "8.30.1";
@@ -463,7 +464,7 @@ import {
463
464
  symlinkSync,
464
465
  writeFileSync
465
466
  } from "node:fs";
466
- import { dirname, join, relative } from "node:path";
467
+ import { dirname, join as join2, relative } from "node:path";
467
468
  function writeJsonAtomic(path, data) {
468
469
  const mode = existsSync(path) ? statSync(path).mode & 511 : 384;
469
470
  const tmp = `${path}.tmp.${process.pid}`;
@@ -489,9 +490,9 @@ function nowTimestamp() {
489
490
  }
490
491
  function freshBackupTs(backupRoot) {
491
492
  const base = nowTimestamp();
492
- if (!existsSync(join(backupRoot, base))) return base;
493
+ if (!existsSync(join2(backupRoot, base))) return base;
493
494
  let n = 1;
494
- while (existsSync(join(backupRoot, `${base}-${n}`))) n++;
495
+ while (existsSync(join2(backupRoot, `${base}-${n}`))) n++;
495
496
  return `${base}-${n}`;
496
497
  }
497
498
  function ensureSymlink(linkPath, target) {
@@ -507,8 +508,8 @@ function backupBeforeWrite(absPath, ts) {
507
508
  if (!existsSync(absPath)) return;
508
509
  const rel = relative(CLAUDE_HOME, absPath);
509
510
  if (rel.startsWith("..") || rel === "") return;
510
- const backupRoot = join(HOME, ".cache", "claude-nomad", "backup", ts);
511
- const dst = join(backupRoot, rel);
511
+ const backupRoot = join2(HOME, ".cache", "claude-nomad", "backup", ts);
512
+ const dst = join2(backupRoot, rel);
512
513
  mkdirSync(dirname(dst), { recursive: true });
513
514
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
514
515
  }
@@ -516,8 +517,8 @@ function backupRepoWrite(absPath, ts, repoHome) {
516
517
  if (!existsSync(absPath)) return;
517
518
  const rel = relative(repoHome, absPath);
518
519
  if (rel.startsWith("..") || rel === "") return;
519
- const backupRoot = join(HOME, ".cache", "claude-nomad", "backup", ts, "repo");
520
- const dst = join(backupRoot, rel);
520
+ const backupRoot = join2(HOME, ".cache", "claude-nomad", "backup", ts, "repo");
521
+ const dst = join2(backupRoot, rel);
521
522
  mkdirSync(dirname(dst), { recursive: true });
522
523
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
523
524
  }
@@ -525,8 +526,8 @@ function backupExtrasWrite(absPath, ts, projectRoot) {
525
526
  if (!existsSync(absPath)) return;
526
527
  const rel = relative(projectRoot, absPath);
527
528
  if (rel.startsWith("..") || rel === "") return;
528
- const backupRoot = join(HOME, ".cache", "claude-nomad", "backup", ts, "extras");
529
- const dst = join(backupRoot, encodePath(projectRoot), rel);
529
+ const backupRoot = join2(HOME, ".cache", "claude-nomad", "backup", ts, "extras");
530
+ const dst = join2(backupRoot, encodePath(projectRoot), rel);
530
531
  mkdirSync(dirname(dst), { recursive: true });
531
532
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
532
533
  }
@@ -541,15 +542,15 @@ var init_utils_fs = __esm({
541
542
 
542
543
  // src/push-gitleaks.scan.ts
543
544
  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";
545
+ import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync3 } from "node:fs";
545
546
  import { homedir as homedir2 } from "node:os";
546
- import { join as join7 } from "node:path";
547
+ import { join as join9 } from "node:path";
547
548
  import { fileURLToPath } from "node:url";
548
549
  function resolveTomlPath() {
549
- const repoToml = join7(REPO_HOME, ".gitleaks.toml");
550
- if (existsSync7(repoToml)) return repoToml;
550
+ const repoToml = join9(REPO_HOME, ".gitleaks.toml");
551
+ if (existsSync8(repoToml)) return repoToml;
551
552
  const bundled = fileURLToPath(new URL("../.gitleaks.toml", import.meta.url));
552
- return existsSync7(bundled) ? bundled : null;
553
+ return existsSync8(bundled) ? bundled : null;
553
554
  }
554
555
  function readGitleaksReport(reportPath) {
555
556
  try {
@@ -562,9 +563,9 @@ function readGitleaksReport(reportPath) {
562
563
  }
563
564
  }
564
565
  function scanStagedTree(repoDir, forwardStreams = false) {
565
- const cacheDir = join7(homedir2(), ".cache", "claude-nomad");
566
+ const cacheDir = join9(homedir2(), ".cache", "claude-nomad");
566
567
  mkdirSync2(cacheDir, { recursive: true });
567
- const reportPath = join7(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
568
+ const reportPath = join9(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
568
569
  const { path: toml, tempPath } = resolveTomlConfig();
569
570
  const args = [
570
571
  "protect",
@@ -591,14 +592,14 @@ function scanStagedTree(repoDir, forwardStreams = false) {
591
592
  }
592
593
  return report;
593
594
  } finally {
594
- if (tempPath !== null) rmSync2(tempPath, { recursive: true, force: true });
595
- rmSync2(reportPath, { force: true });
595
+ if (tempPath !== null) rmSync3(tempPath, { recursive: true, force: true });
596
+ rmSync3(reportPath, { force: true });
596
597
  }
597
598
  }
598
599
  function scanFile(filePath, forwardStreams = false) {
599
- const cacheDir = join7(homedir2(), ".cache", "claude-nomad");
600
+ const cacheDir = join9(homedir2(), ".cache", "claude-nomad");
600
601
  mkdirSync2(cacheDir, { recursive: true });
601
- const reportPath = join7(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
602
+ const reportPath = join9(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
602
603
  const { path: toml, tempPath } = resolveTomlConfig();
603
604
  const args = [
604
605
  "detect",
@@ -623,8 +624,8 @@ function scanFile(filePath, forwardStreams = false) {
623
624
  }
624
625
  return report;
625
626
  } finally {
626
- if (tempPath !== null) rmSync2(tempPath, { recursive: true, force: true });
627
- rmSync2(reportPath, { force: true });
627
+ if (tempPath !== null) rmSync3(tempPath, { recursive: true, force: true });
628
+ rmSync3(reportPath, { force: true });
628
629
  }
629
630
  }
630
631
  var init_push_gitleaks_scan = __esm({
@@ -637,24 +638,24 @@ var init_push_gitleaks_scan = __esm({
637
638
  });
638
639
 
639
640
  // src/push-gitleaks.config.ts
640
- import { existsSync as existsSync8, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
641
+ import { existsSync as existsSync9, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
641
642
  import { tmpdir } from "node:os";
642
- import { join as join8 } from "node:path";
643
+ import { join as join10 } from "node:path";
643
644
  function buildOverlayTempConfig(overlayBody, bundled) {
644
645
  const tempBody = `[extend]
645
646
  path = ${JSON.stringify(bundled)}
646
647
 
647
648
  ${overlayBody}`;
648
- const tempPath = mkdtempSync(join8(tmpdir(), "nomad-gitleaks-cfg-"));
649
- const configPath = join8(tempPath, "config.toml");
649
+ const tempPath = mkdtempSync(join10(tmpdir(), "nomad-gitleaks-cfg-"));
650
+ const configPath = join10(tempPath, "config.toml");
650
651
  writeFileSync2(configPath, tempBody, { mode: 384, flag: "wx" });
651
652
  return { configPath, tempPath };
652
653
  }
653
654
  function resolveTomlConfig() {
654
- const overlayPath = join8(REPO_HOME, ".gitleaks.overlay.toml");
655
- const repoToml = join8(REPO_HOME, ".gitleaks.toml");
655
+ const overlayPath = join10(REPO_HOME, ".gitleaks.overlay.toml");
656
+ const repoToml = join10(REPO_HOME, ".gitleaks.toml");
656
657
  const bundled = resolveTomlPath();
657
- if (!existsSync8(overlayPath)) {
658
+ if (!existsSync9(overlayPath)) {
658
659
  return { path: bundled, tempPath: null };
659
660
  }
660
661
  if (bundled === repoToml) {
@@ -696,9 +697,9 @@ var init_push_gitleaks_config = __esm({
696
697
 
697
698
  // src/push-checks.ts
698
699
  import { execFileSync as execFileSync3 } from "node:child_process";
699
- import { readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
700
+ import { readdirSync as readdirSync3, rmSync as rmSync4 } from "node:fs";
700
701
  import { homedir as homedir3, platform } from "node:os";
701
- import { join as join9 } from "node:path";
702
+ import { join as join11 } from "node:path";
702
703
  function gitleaksInstallHint() {
703
704
  const head = "gitleaks not on PATH (required for nomad push). Install:";
704
705
  const plat = platform();
@@ -736,12 +737,12 @@ function findGitlinks(dir) {
736
737
  function walk(current) {
737
738
  let entries;
738
739
  try {
739
- entries = readdirSync2(current, { withFileTypes: true });
740
+ entries = readdirSync3(current, { withFileTypes: true });
740
741
  } catch {
741
742
  return;
742
743
  }
743
744
  for (const e of entries) {
744
- const p = join9(current, e.name);
745
+ const p = join11(current, e.name);
745
746
  if (e.name === ".git") {
746
747
  hits.push(p);
747
748
  continue;
@@ -763,7 +764,7 @@ function probeGitleaks() {
763
764
  if (e.code === "ENOENT") throw new NomadFatal(gitleaksInstallHint());
764
765
  throw new NomadFatal(`gitleaks --version failed: ${e.message}`);
765
766
  } finally {
766
- if (tempPath !== null) rmSync3(tempPath, { recursive: true, force: true });
767
+ if (tempPath !== null) rmSync4(tempPath, { recursive: true, force: true });
767
768
  }
768
769
  }
769
770
  function rebaseBeforePush() {
@@ -949,7 +950,7 @@ init_utils();
949
950
  init_utils_fs();
950
951
  init_utils_json();
951
952
  import { cpSync as cpSync2, existsSync as existsSync2, lstatSync as lstatSync2, rmSync } from "node:fs";
952
- import { join as join2 } from "node:path";
953
+ import { join as join3 } from "node:path";
953
954
  var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
954
955
  function lexists(p) {
955
956
  try {
@@ -960,7 +961,7 @@ function lexists(p) {
960
961
  }
961
962
  }
962
963
  function readMapIfPresent(repoHome) {
963
- const mapPath = join2(repoHome, "path-map.json");
964
+ const mapPath = join3(repoHome, "path-map.json");
964
965
  return existsSync2(mapPath) ? readPathMap(mapPath) : { projects: {} };
965
966
  }
966
967
  function isConfiguredTarget(name, map) {
@@ -971,13 +972,13 @@ function isValidAdoptName(name) {
971
972
  return isValidSharedDir(name);
972
973
  }
973
974
  function performAdoptMove(name, linkPath, sharedTarget) {
974
- const backupBase = join2(HOME, ".cache", "claude-nomad", "backup");
975
+ const backupBase = join3(HOME, ".cache", "claude-nomad", "backup");
975
976
  const ts = freshBackupTs(backupBase);
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,7 +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 backupBase = join3(HOME, ".cache", "claude-nomad", "backup");
1013
1014
  const ts = freshBackupTs(backupBase);
1014
1015
  log(`would backup: ${linkPath} -> backup/${ts}/${name}`);
1015
1016
  log(`would move: ${linkPath} -> shared/${name}`);
@@ -1025,15 +1026,79 @@ function cmdAdopt(name, opts = {}) {
1025
1026
  }
1026
1027
  }
1027
1028
 
1029
+ // src/commands.clean.ts
1030
+ init_config();
1031
+ init_utils();
1032
+ import { existsSync as existsSync3, lstatSync as lstatSync3, readdirSync, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
1033
+ import { join as join4 } from "node:path";
1034
+ var TS_SHAPE = /^\d{8}-\d{6}(-\d+)?$/;
1035
+ var DURATION_RE = /^(\d+)([dhm])$/;
1036
+ var UNIT_MS = { d: 864e5, h: 36e5, m: 6e4 };
1037
+ var CLEAN_DEFAULT_OLDER_THAN_MS = 14 * 24 * 60 * 60 * 1e3;
1038
+ function isTsDir(name) {
1039
+ return TS_SHAPE.test(name);
1040
+ }
1041
+ function parseDuration(s) {
1042
+ const m = DURATION_RE.exec(s);
1043
+ if (!m) return null;
1044
+ return Number(m[1]) * UNIT_MS[m[2]];
1045
+ }
1046
+ function listBackupDirs(backupBase) {
1047
+ if (!existsSync3(backupBase)) return [];
1048
+ return readdirSync(backupBase).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join4(backupBase, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
1049
+ }
1050
+ function prunableByAge(dirs, olderThanMs, nowMs) {
1051
+ return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
1052
+ }
1053
+ function prunableByCount(dirs, keep) {
1054
+ return dirs.slice(keep).map((d) => d.name);
1055
+ }
1056
+ function safeDelete(backupBase, name) {
1057
+ if (!isTsDir(name)) return;
1058
+ const full = join4(backupBase, name);
1059
+ const st = lstatSync3(full, { throwIfNoEntry: false });
1060
+ if (!st || st.isSymbolicLink() || !st.isDirectory()) return;
1061
+ rmSync2(full, { recursive: true, force: true });
1062
+ }
1063
+ function resolveTargets(dirs, olderThanMs, keep) {
1064
+ if (keep !== void 0) return prunableByCount(dirs, keep);
1065
+ return prunableByAge(dirs, olderThanMs, Date.now());
1066
+ }
1067
+ function cmdClean(opts, backupBase = BACKUP_BASE) {
1068
+ const { dryRun, olderThan, keep } = opts;
1069
+ if (olderThan !== void 0 && keep !== void 0) {
1070
+ fail("--older-than and --keep are mutually exclusive");
1071
+ process.exit(1);
1072
+ }
1073
+ let olderThanMs = CLEAN_DEFAULT_OLDER_THAN_MS;
1074
+ if (olderThan !== void 0) {
1075
+ const parsed = parseDuration(olderThan);
1076
+ if (parsed === null) {
1077
+ fail(`invalid --older-than duration: ${olderThan} (expected e.g. 14d, 24h, 30m)`);
1078
+ process.exit(1);
1079
+ }
1080
+ olderThanMs = parsed;
1081
+ }
1082
+ const dirs = listBackupDirs(backupBase);
1083
+ const targets = resolveTargets(dirs, olderThanMs, keep);
1084
+ if (dryRun) {
1085
+ for (const name of targets) log(`would remove ${name}`);
1086
+ log(`dry-run: ${targets.length} backup(s) would be removed`);
1087
+ return;
1088
+ }
1089
+ for (const name of targets) safeDelete(backupBase, name);
1090
+ log(`removed ${targets.length} backup(s)`);
1091
+ }
1092
+
1028
1093
  // src/commands.doctor.ts
1029
- import { existsSync as existsSync16 } from "node:fs";
1030
- import { join as join18 } from "node:path";
1094
+ import { existsSync as existsSync18 } from "node:fs";
1095
+ import { join as join21 } from "node:path";
1031
1096
 
1032
1097
  // src/commands.doctor.checks.repo.ts
1033
1098
  init_color();
1034
1099
  init_config();
1035
- import { existsSync as existsSync4, lstatSync as lstatSync3, statSync as statSync2 } from "node:fs";
1036
- import { join as join4 } from "node:path";
1100
+ import { existsSync as existsSync5, lstatSync as lstatSync4, statSync as statSync3 } from "node:fs";
1101
+ import { join as join6 } from "node:path";
1037
1102
 
1038
1103
  // src/commands.doctor.format.ts
1039
1104
  init_color();
@@ -1083,15 +1148,15 @@ function readJsonSafe(path, label, section2) {
1083
1148
  // src/init.classify.ts
1084
1149
  init_config();
1085
1150
  init_utils_json();
1086
- import { existsSync as existsSync3 } from "node:fs";
1087
- import { join as join3 } from "node:path";
1151
+ import { existsSync as existsSync4 } from "node:fs";
1152
+ import { join as join5 } from "node:path";
1088
1153
  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);
1154
+ const basePath = join5(repoHome, "shared", "settings.base.json");
1155
+ const mapPath = join5(repoHome, "path-map.json");
1156
+ const hostPath = join5(repoHome, "hosts", `${host}.json`);
1157
+ const hasBase = existsSync4(basePath);
1158
+ const hasMap = existsSync4(mapPath);
1159
+ const hasHost = existsSync4(hostPath);
1095
1160
  let mapEntryCount = 0;
1096
1161
  if (hasMap) {
1097
1162
  try {
@@ -1106,11 +1171,11 @@ function classifyRepoState(repoHome, host) {
1106
1171
  return "partial";
1107
1172
  }
1108
1173
  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";
1174
+ const basePath = join5(repoHome, "shared", "settings.base.json");
1175
+ const mapPath = join5(repoHome, "path-map.json");
1176
+ const hostPath = join5(repoHome, "hosts", `${host}.json`);
1177
+ if (!existsSync4(basePath)) return "- shared/settings.base.json missing";
1178
+ if (!existsSync4(mapPath)) return "- path-map.json missing";
1114
1179
  let mapEntryCount;
1115
1180
  try {
1116
1181
  const map = readJson(mapPath);
@@ -1119,7 +1184,7 @@ function reasonForPartial(repoHome, host) {
1119
1184
  mapEntryCount = 0;
1120
1185
  }
1121
1186
  if (mapEntryCount === 0) return "- path-map.json.projects has no entries";
1122
- if (!existsSync3(hostPath)) return `- hosts/${host}.json missing`;
1187
+ if (!existsSync4(hostPath)) return `- hosts/${host}.json missing`;
1123
1188
  return "- partial state (unknown gap)";
1124
1189
  }
1125
1190
 
@@ -1131,11 +1196,11 @@ function reportHostAndPaths(section2) {
1131
1196
  addItem(section2, `${dim(infoGlyph)} host: ${cyan(HOST)}`);
1132
1197
  addItem(
1133
1198
  section2,
1134
- `${existsSync4(REPO_HOME) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(REPO_HOME)}`
1199
+ `${existsSync5(REPO_HOME) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(REPO_HOME)}`
1135
1200
  );
1136
1201
  addItem(
1137
1202
  section2,
1138
- `${existsSync4(CLAUDE_HOME) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(CLAUDE_HOME)}`
1203
+ `${existsSync5(CLAUDE_HOME) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(CLAUDE_HOME)}`
1139
1204
  );
1140
1205
  }
1141
1206
  function reportRepoState(section2) {
@@ -1157,12 +1222,12 @@ function reportRepoState(section2) {
1157
1222
  }
1158
1223
  }
1159
1224
  function repoHasSharedSource(name) {
1160
- return existsSync4(join4(REPO_HOME, "shared", name));
1225
+ return existsSync5(join6(REPO_HOME, "shared", name));
1161
1226
  }
1162
1227
  function classifySharedLink(name, p) {
1163
1228
  let stat;
1164
1229
  try {
1165
- stat = lstatSync3(p);
1230
+ stat = lstatSync4(p);
1166
1231
  } catch (err) {
1167
1232
  const code = err.code;
1168
1233
  if (code === "ENOENT") {
@@ -1183,7 +1248,7 @@ function classifySharedLink(name, p) {
1183
1248
  }
1184
1249
  function classifySymlinkTarget(name, p) {
1185
1250
  try {
1186
- statSync2(p);
1251
+ statSync3(p);
1187
1252
  return { line: `${green(okGlyph)} ${name}: symlink`, fail: false };
1188
1253
  } catch (err) {
1189
1254
  const code = err.code;
@@ -1204,7 +1269,7 @@ function classifySymlinkTarget(name, p) {
1204
1269
  }
1205
1270
  function reportSharedLinks(section2, map) {
1206
1271
  for (const name of allSharedLinks(map)) {
1207
- const p = join4(CLAUDE_HOME, name);
1272
+ const p = join6(CLAUDE_HOME, name);
1208
1273
  const { line, fail: fail2 } = classifySharedLink(name, p);
1209
1274
  addItem(section2, line);
1210
1275
  if (fail2) process.exitCode = 1;
@@ -1214,11 +1279,11 @@ function reportSharedLinks(section2, map) {
1214
1279
  // src/commands.doctor.checks.settings.ts
1215
1280
  init_color();
1216
1281
  init_config();
1217
- import { existsSync as existsSync5, readdirSync } from "node:fs";
1218
- import { join as join5 } from "node:path";
1282
+ import { existsSync as existsSync6, readdirSync as readdirSync2 } from "node:fs";
1283
+ import { join as join7 } from "node:path";
1219
1284
  function loadBaseSettings(section2) {
1220
- const basePath = join5(REPO_HOME, "shared", "settings.base.json");
1221
- if (!existsSync5(basePath)) {
1285
+ const basePath = join7(REPO_HOME, "shared", "settings.base.json");
1286
+ if (!existsSync6(basePath)) {
1222
1287
  addItem(section2, `${red(failGlyph)} shared/settings.base.json missing at ${blue(basePath)}`);
1223
1288
  process.exitCode = 1;
1224
1289
  return null;
@@ -1226,8 +1291,8 @@ function loadBaseSettings(section2) {
1226
1291
  return readJsonSafe(basePath, basePath, section2);
1227
1292
  }
1228
1293
  function loadAndReportSettings(section2) {
1229
- const settingsPath = join5(CLAUDE_HOME, "settings.json");
1230
- if (!existsSync5(settingsPath)) return null;
1294
+ const settingsPath = join7(CLAUDE_HOME, "settings.json");
1295
+ if (!existsSync6(settingsPath)) return null;
1231
1296
  const settings = readJsonSafe(settingsPath, settingsPath, section2);
1232
1297
  if (settings === null) return null;
1233
1298
  const unknownKeys = Object.keys(settings).filter((k) => !KNOWN_SETTINGS_KEYS.has(k));
@@ -1242,13 +1307,13 @@ function loadAndReportSettings(section2) {
1242
1307
  return settings;
1243
1308
  }
1244
1309
  function reportHostOverrides(section2, base, settings) {
1245
- const hostFile = join5(REPO_HOME, "hosts", `${HOST}.json`);
1310
+ const hostFile = join7(REPO_HOME, "hosts", `${HOST}.json`);
1246
1311
  let drift = [];
1247
1312
  if (base !== null && settings !== null) {
1248
1313
  const baseKeys = new Set(Object.keys(base));
1249
1314
  drift = Object.keys(settings).filter((k) => !baseKeys.has(k));
1250
1315
  }
1251
- if (existsSync5(hostFile)) {
1316
+ if (existsSync6(hostFile)) {
1252
1317
  if (readJsonSafe(hostFile, hostFile, section2) !== null) {
1253
1318
  addItem(section2, `${green(okGlyph)} host overrides: ${blue(hostFile)}`);
1254
1319
  }
@@ -1257,9 +1322,9 @@ function reportHostOverrides(section2, base, settings) {
1257
1322
  section2,
1258
1323
  `${red(failGlyph)} no hosts/${HOST}.json AND settings.json has unbased keys ${JSON.stringify(drift)}`
1259
1324
  );
1260
- const hostsDir = join5(REPO_HOME, "hosts");
1261
- if (existsSync5(hostsDir)) {
1262
- const cands = readdirSync(hostsDir).filter((f) => f.endsWith(".json"));
1325
+ const hostsDir = join7(REPO_HOME, "hosts");
1326
+ if (existsSync6(hostsDir)) {
1327
+ const cands = readdirSync2(hostsDir).filter((f) => f.endsWith(".json"));
1263
1328
  if (cands.length > 0) addItem(section2, `${dim(infoGlyph)} candidates: ${cands.join(", ")}`);
1264
1329
  }
1265
1330
  process.exitCode = 1;
@@ -1274,8 +1339,8 @@ function reportHostOverrides(section2, base, settings) {
1274
1339
  // src/commands.doctor.checks.pathmap.ts
1275
1340
  init_color();
1276
1341
  init_config();
1277
- import { existsSync as existsSync6 } from "node:fs";
1278
- import { join as join6 } from "node:path";
1342
+ import { existsSync as existsSync7 } from "node:fs";
1343
+ import { join as join8 } from "node:path";
1279
1344
  init_utils_json();
1280
1345
  function reportMappedProjects(section2, map) {
1281
1346
  const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
@@ -1310,8 +1375,8 @@ function reportPathCollisions(section2, map) {
1310
1375
  else addItem(section2, `${green(okGlyph)} path-encoding: no collisions`);
1311
1376
  }
1312
1377
  function reportPathMap(section2) {
1313
- const mapPath = join6(REPO_HOME, "path-map.json");
1314
- if (!existsSync6(mapPath)) {
1378
+ const mapPath = join8(REPO_HOME, "path-map.json");
1379
+ if (!existsSync7(mapPath)) {
1315
1380
  addItem(section2, `${red(failGlyph)} path-map.json missing at ${blue(mapPath)}`);
1316
1381
  process.exitCode = 1;
1317
1382
  return;
@@ -1358,8 +1423,8 @@ function reportNeverSync(section2) {
1358
1423
  init_color();
1359
1424
  init_config();
1360
1425
  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";
1426
+ import { existsSync as existsSync10 } from "node:fs";
1427
+ import { join as join12, relative as relative2 } from "node:path";
1363
1428
  init_push_checks();
1364
1429
  init_utils();
1365
1430
  function reportGitleaksProbe(section2) {
@@ -1378,8 +1443,8 @@ function reportGitleaksProbe(section2) {
1378
1443
  }
1379
1444
  }
1380
1445
  function reportGitlinks(section2) {
1381
- const sharedDir = join10(REPO_HOME, "shared");
1382
- if (existsSync9(sharedDir)) {
1446
+ const sharedDir = join12(REPO_HOME, "shared");
1447
+ if (existsSync10(sharedDir)) {
1383
1448
  const gitlinks = findGitlinks(sharedDir);
1384
1449
  for (const p of gitlinks) {
1385
1450
  const rel = relative2(REPO_HOME, p);
@@ -1419,11 +1484,57 @@ function reportRebaseClean(section2) {
1419
1484
  }
1420
1485
  }
1421
1486
 
1487
+ // src/commands.doctor.checks.backups.ts
1488
+ init_color();
1489
+ import { existsSync as existsSync11, lstatSync as lstatSync5, readdirSync as readdirSync4 } from "node:fs";
1490
+ import { join as join13 } from "node:path";
1491
+ init_config();
1492
+ var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
1493
+ function safeReaddir(dir) {
1494
+ try {
1495
+ return readdirSync4(dir);
1496
+ } catch {
1497
+ return [];
1498
+ }
1499
+ }
1500
+ var DOCTOR_BACKUP_COUNT_WARN = 20;
1501
+ var DOCTOR_BACKUP_SIZE_WARN_MB = 200;
1502
+ var BYTES_PER_MB = 1024 * 1024;
1503
+ function dirSizeBytes(dir) {
1504
+ let bytes = 0;
1505
+ for (const entry of safeReaddir(dir)) {
1506
+ const full = join13(dir, entry);
1507
+ const st = lstatSync5(full, { throwIfNoEntry: false });
1508
+ if (!st) continue;
1509
+ if (st.isSymbolicLink()) continue;
1510
+ if (st.isDirectory()) bytes += dirSizeBytes(full);
1511
+ else bytes += st.size;
1512
+ }
1513
+ return bytes;
1514
+ }
1515
+ function totalSizeMb(backupBase, dirs) {
1516
+ let bytes = 0;
1517
+ for (const name of dirs) bytes += dirSizeBytes(join13(backupBase, name));
1518
+ return bytes / BYTES_PER_MB;
1519
+ }
1520
+ function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
1521
+ if (!existsSync11(backupBase)) return;
1522
+ const dirs = safeReaddir(backupBase).filter((n) => TS_SHAPE2.test(n));
1523
+ const count = dirs.length;
1524
+ const sizeMb = totalSizeMb(backupBase, dirs);
1525
+ if (count > DOCTOR_BACKUP_COUNT_WARN || sizeMb > DOCTOR_BACKUP_SIZE_WARN_MB) {
1526
+ addItem(
1527
+ section2,
1528
+ `${yellow(warnGlyph)} backups: ${count} dirs / ${sizeMb.toFixed(1)} MB (run 'nomad clean --backups')`
1529
+ );
1530
+ }
1531
+ }
1532
+
1422
1533
  // src/commands.doctor.check-schema.ts
1423
1534
  init_color();
1424
1535
  import { execFileSync as execFileSync5 } from "node:child_process";
1425
- import { existsSync as existsSync10 } from "node:fs";
1426
- import { join as join11 } from "node:path";
1536
+ import { existsSync as existsSync12 } from "node:fs";
1537
+ import { join as join14 } from "node:path";
1427
1538
  init_config();
1428
1539
  function fetchSchemaKeys() {
1429
1540
  try {
@@ -1438,8 +1549,8 @@ function fetchSchemaKeys() {
1438
1549
  }
1439
1550
  }
1440
1551
  function reportCheckSchema(section2) {
1441
- const settingsPath = join11(CLAUDE_HOME, "settings.json");
1442
- if (!existsSync10(settingsPath)) {
1552
+ const settingsPath = join14(CLAUDE_HOME, "settings.json");
1553
+ if (!existsSync12(settingsPath)) {
1443
1554
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
1444
1555
  return;
1445
1556
  }
@@ -1469,18 +1580,18 @@ function reportCheckSchema(section2) {
1469
1580
  init_color();
1470
1581
  import { randomBytes } from "node:crypto";
1471
1582
  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";
1583
+ import { existsSync as existsSync14, mkdirSync as mkdirSync4, readdirSync as readdirSync6, rmSync as rmSync6 } from "node:fs";
1473
1584
  import { homedir as homedir4 } from "node:os";
1474
- import { join as join14 } from "node:path";
1585
+ import { join as join17 } from "node:path";
1475
1586
 
1476
1587
  // src/commands.doctor.check-shared.scan.ts
1477
1588
  init_color();
1478
- import { join as join12 } from "node:path";
1589
+ import { join as join15 } from "node:path";
1479
1590
  init_config();
1480
1591
  init_push_gitleaks();
1481
1592
  function scrubPath(logical, sid, logicalToEncoded) {
1482
1593
  const encoded = logicalToEncoded.get(logical) ?? logical;
1483
- return join12(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
1594
+ return join15(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
1484
1595
  }
1485
1596
  function reportSessionFindings(section2, bySession) {
1486
1597
  for (const [sid, counts] of bySession) {
@@ -1572,14 +1683,14 @@ init_config();
1572
1683
  init_utils();
1573
1684
  init_utils_fs();
1574
1685
  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";
1686
+ import { cpSync as cpSync3, existsSync as existsSync13, mkdirSync as mkdirSync3, readdirSync as readdirSync5, rmSync as rmSync5, statSync as statSync4 } from "node:fs";
1687
+ import { join as join16, relative as relative3, sep } from "node:path";
1577
1688
  function copyDir(src, dst) {
1578
- rmSync4(dst, { recursive: true, force: true });
1689
+ rmSync5(dst, { recursive: true, force: true });
1579
1690
  cpSync3(src, dst, { recursive: true, force: true });
1580
1691
  }
1581
1692
  function copyDirJsonlOnly(src, dst) {
1582
- rmSync4(dst, { recursive: true, force: true });
1693
+ rmSync5(dst, { recursive: true, force: true });
1583
1694
  cpSync3(src, dst, {
1584
1695
  recursive: true,
1585
1696
  force: true,
@@ -1587,7 +1698,7 @@ function copyDirJsonlOnly(src, dst) {
1587
1698
  const rel = relative3(src, srcPath);
1588
1699
  if (rel === "") return true;
1589
1700
  if (rel.split(sep).length > 1) return true;
1590
- if (statSync3(srcPath).isDirectory()) return true;
1701
+ if (statSync4(srcPath).isDirectory()) return true;
1591
1702
  if (srcPath.endsWith(".jsonl")) return true;
1592
1703
  log(`skip ${rel}: extension not in allowlist`);
1593
1704
  return false;
@@ -1599,14 +1710,14 @@ function remapPull(ts, opts = {}) {
1599
1710
  let unmapped = 0;
1600
1711
  const pulled = [];
1601
1712
  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)) {
1713
+ const mapPath = join16(REPO_HOME, "path-map.json");
1714
+ const repoProjects = join16(REPO_HOME, "shared", "projects");
1715
+ if (!existsSync13(mapPath) || !existsSync13(repoProjects)) {
1605
1716
  log("no path-map or repo projects dir; skipping session remap");
1606
1717
  return { unmapped: 0, pulled, wouldPull };
1607
1718
  }
1608
1719
  const map = readJson(mapPath);
1609
- const localProjects = join13(CLAUDE_HOME, "projects");
1720
+ const localProjects = join16(CLAUDE_HOME, "projects");
1610
1721
  if (!dryRun) mkdirSync3(localProjects, { recursive: true });
1611
1722
  for (const [logical, hosts] of Object.entries(map.projects)) {
1612
1723
  assertSafeLogical(logical);
@@ -1615,9 +1726,9 @@ function remapPull(ts, opts = {}) {
1615
1726
  unmapped++;
1616
1727
  continue;
1617
1728
  }
1618
- const src = join13(repoProjects, logical);
1619
- if (!existsSync11(src)) continue;
1620
- const dst = join13(localProjects, encodePath(localPath));
1729
+ const src = join16(repoProjects, logical);
1730
+ if (!existsSync13(src)) continue;
1731
+ const dst = join16(localProjects, encodePath(localPath));
1621
1732
  if (dryRun) {
1622
1733
  wouldPull.push(logical);
1623
1734
  log(`would overwrite: ${dst} (from ${src})`);
@@ -1658,30 +1769,30 @@ function remapPush(ts, opts = {}) {
1658
1769
  let unmapped = 0;
1659
1770
  const pushed = [];
1660
1771
  const wouldPush = [];
1661
- const mapPath = join13(REPO_HOME, "path-map.json");
1662
- if (!existsSync11(mapPath)) {
1772
+ const mapPath = join16(REPO_HOME, "path-map.json");
1773
+ if (!existsSync13(mapPath)) {
1663
1774
  log("no path-map.json; skipping session export");
1664
1775
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
1665
1776
  }
1666
1777
  const map = readJson(mapPath);
1667
- const localProjects = join13(CLAUDE_HOME, "projects");
1668
- const repoProjects = join13(REPO_HOME, "shared", "projects");
1778
+ const localProjects = join16(CLAUDE_HOME, "projects");
1779
+ const repoProjects = join16(REPO_HOME, "shared", "projects");
1669
1780
  const reverse = buildReverseMap(map);
1670
- if (!existsSync11(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
1781
+ if (!existsSync13(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
1671
1782
  if (!dryRun) mkdirSync3(repoProjects, { recursive: true });
1672
- for (const dir of readdirSync3(localProjects)) {
1783
+ for (const dir of readdirSync5(localProjects)) {
1673
1784
  const logical = reverse.get(dir);
1674
1785
  if (!logical) {
1675
1786
  unmapped++;
1676
1787
  continue;
1677
1788
  }
1678
- const repoDst = join13(repoProjects, logical);
1789
+ const repoDst = join16(repoProjects, logical);
1679
1790
  if (dryRun) {
1680
1791
  wouldPush.push(logical);
1681
1792
  continue;
1682
1793
  }
1683
1794
  backupRepoWrite(repoDst, ts, REPO_HOME);
1684
- copyDirJsonlOnly(join13(localProjects, dir), repoDst);
1795
+ copyDirJsonlOnly(join16(localProjects, dir), repoDst);
1685
1796
  pushed.push(logical);
1686
1797
  }
1687
1798
  return { unmapped, collisions: 0, pushed, wouldPush };
@@ -1693,8 +1804,8 @@ init_utils_json();
1693
1804
  function buildScanTree(tmpRoot) {
1694
1805
  const logicalToEncoded = /* @__PURE__ */ new Map();
1695
1806
  let staged = 0;
1696
- const mapPath = join14(REPO_HOME, "path-map.json");
1697
- if (!existsSync12(mapPath)) return { logicalToEncoded, staged, malformed: false };
1807
+ const mapPath = join17(REPO_HOME, "path-map.json");
1808
+ if (!existsSync14(mapPath)) return { logicalToEncoded, staged, malformed: false };
1698
1809
  let map;
1699
1810
  try {
1700
1811
  map = readJson(mapPath);
@@ -1711,12 +1822,12 @@ function buildScanTree(tmpRoot) {
1711
1822
  if (!p || p === "TBD") continue;
1712
1823
  reverse.set(encodePath(p), logical);
1713
1824
  }
1714
- const localProjects = join14(CLAUDE_HOME, "projects");
1715
- if (!existsSync12(localProjects)) return { logicalToEncoded, staged, malformed: false };
1716
- for (const dir of readdirSync4(localProjects)) {
1825
+ const localProjects = join17(CLAUDE_HOME, "projects");
1826
+ if (!existsSync14(localProjects)) return { logicalToEncoded, staged, malformed: false };
1827
+ for (const dir of readdirSync6(localProjects)) {
1717
1828
  const logical = reverse.get(dir);
1718
1829
  if (!logical) continue;
1719
- copyDirJsonlOnly(join14(localProjects, dir), join14(tmpRoot, "shared", "projects", logical));
1830
+ copyDirJsonlOnly(join17(localProjects, dir), join17(tmpRoot, "shared", "projects", logical));
1720
1831
  logicalToEncoded.set(logical, dir);
1721
1832
  staged++;
1722
1833
  }
@@ -1747,11 +1858,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
1747
1858
  }
1748
1859
  function reportCheckShared(section2, gitleaksReady) {
1749
1860
  if (!ensureGitleaksReady(section2, gitleaksReady)) return;
1750
- const cacheDir = join14(homedir4(), ".cache", "claude-nomad");
1861
+ const cacheDir = join17(homedir4(), ".cache", "claude-nomad");
1751
1862
  mkdirSync4(cacheDir, { recursive: true });
1752
1863
  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}`);
1864
+ const reportPath = join17(cacheDir, `check-shared-${stamp}.json`);
1865
+ const tmpRoot = join17(cacheDir, `check-shared-tree-${stamp}`);
1755
1866
  try {
1756
1867
  const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
1757
1868
  if (malformed) {
@@ -1765,15 +1876,109 @@ function reportCheckShared(section2, gitleaksReady) {
1765
1876
  }
1766
1877
  scanAndReport(section2, tmpRoot, staged, logicalToEncoded);
1767
1878
  } finally {
1768
- rmSync5(reportPath, { force: true });
1769
- rmSync5(tmpRoot, { recursive: true, force: true });
1879
+ rmSync6(reportPath, { force: true });
1880
+ rmSync6(tmpRoot, { recursive: true, force: true });
1881
+ }
1882
+ }
1883
+
1884
+ // src/commands.doctor.checks.hooks.scope.ts
1885
+ init_color();
1886
+ import { existsSync as existsSync15, readFileSync as readFileSync4, readdirSync as readdirSync7, realpathSync } from "node:fs";
1887
+ import { dirname as dirname2, extname, join as join18 } from "node:path";
1888
+ init_config();
1889
+ function typeFromPackageJson(pkgPath) {
1890
+ try {
1891
+ const parsed = JSON.parse(readFileSync4(pkgPath, "utf8"));
1892
+ return parsed.type === "module" ? "esm" : "cjs";
1893
+ } catch {
1894
+ return "cjs";
1895
+ }
1896
+ }
1897
+ function effectiveType(hookPath) {
1898
+ const ext = extname(hookPath);
1899
+ if (ext === ".mjs") return "esm";
1900
+ if (ext === ".cjs") return "cjs";
1901
+ let real;
1902
+ try {
1903
+ real = realpathSync(hookPath);
1904
+ } catch {
1905
+ return null;
1906
+ }
1907
+ let dir = dirname2(real);
1908
+ for (; ; ) {
1909
+ const pkg = join18(dir, "package.json");
1910
+ if (existsSync15(pkg)) return typeFromPackageJson(pkg);
1911
+ const parent = dirname2(dir);
1912
+ if (parent === dir) return "cjs";
1913
+ dir = parent;
1914
+ }
1915
+ }
1916
+ function stripCommentsAndStrings(src) {
1917
+ return src.replace(/\/\*[\s\S]*?\*\/|\/\/[^\n]*|'[^']*'|"[^"]*"|`[^`]*`/g, (match) => {
1918
+ const open = match[0];
1919
+ if (open === "'") return "''";
1920
+ if (open === '"') return '""';
1921
+ if (open === "`") return "``";
1922
+ return " ";
1923
+ });
1924
+ }
1925
+ function classifySource(src) {
1926
+ const code = stripCommentsAndStrings(src);
1927
+ const cjs = /\brequire\s*\(/.test(code) || /\bmodule\.exports\b/.test(code) || /\bexports\.\w/.test(code);
1928
+ const esm = /^[^\S\r\n]*import\s/m.test(code) || /^[^\S\r\n]*export\s/m.test(code) || /\bimport\.meta\b/.test(code);
1929
+ if (cjs && !esm) return "cjs";
1930
+ if (esm && !cjs) return "esm";
1931
+ if (cjs && esm) return "cjs";
1932
+ return "unknown";
1933
+ }
1934
+ function remedy(family) {
1935
+ 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)";
1936
+ }
1937
+ function safeReaddir2(dir) {
1938
+ try {
1939
+ return readdirSync7(dir);
1940
+ } catch {
1941
+ return [];
1942
+ }
1943
+ }
1944
+ function safeRead(path) {
1945
+ try {
1946
+ return readFileSync4(path, "utf8");
1947
+ } catch {
1948
+ return null;
1949
+ }
1950
+ }
1951
+ function reportHookScopeCheck(section2) {
1952
+ const hooksDir = join18(CLAUDE_HOME, "hooks");
1953
+ if (!existsSync15(hooksDir)) {
1954
+ addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
1955
+ return;
1956
+ }
1957
+ let anyWarn = false;
1958
+ for (const name of safeReaddir2(hooksDir)) {
1959
+ if (extname(name) !== ".js") continue;
1960
+ const abs = join18(hooksDir, name);
1961
+ const eff = effectiveType(abs);
1962
+ if (eff === null) continue;
1963
+ const src = safeRead(abs);
1964
+ if (src === null) continue;
1965
+ const fam = classifySource(src);
1966
+ if (fam === "unknown" || fam === eff) continue;
1967
+ addItem(
1968
+ section2,
1969
+ `${yellow(warnGlyph)} hooks/${name}: ${fam} source loads as ${eff} (${remedy(fam)})`
1970
+ );
1971
+ anyWarn = true;
1972
+ }
1973
+ if (!anyWarn) {
1974
+ addItem(section2, `${green(okGlyph)} hooks: module type consistent`);
1770
1975
  }
1771
1976
  }
1772
1977
 
1773
1978
  // src/commands.doctor.checks.hooks.ts
1774
1979
  init_color();
1775
- import { existsSync as existsSync13 } from "node:fs";
1776
- import { join as join15 } from "node:path";
1980
+ import { existsSync as existsSync16 } from "node:fs";
1981
+ import { join as join19 } from "node:path";
1777
1982
  init_config();
1778
1983
  function expandHome(token) {
1779
1984
  return token.replace(/^\$\{HOME\}/, HOME).replace(/^\$HOME/, HOME).replace(/^~/, HOME);
@@ -1811,7 +2016,7 @@ function checkEventGroups(section2, event, groups) {
1811
2016
  for (const group of groups) {
1812
2017
  for (const cmd of commandsFromGroup(group)) {
1813
2018
  for (const resolved of claudePathsIn(cmd)) {
1814
- if (existsSync13(resolved)) continue;
2019
+ if (existsSync16(resolved)) continue;
1815
2020
  addItem(section2, `${red(failGlyph)} hooks/${event}: command target missing: ${resolved}`);
1816
2021
  process.exitCode = 1;
1817
2022
  anyFail = true;
@@ -1821,8 +2026,8 @@ function checkEventGroups(section2, event, groups) {
1821
2026
  return anyFail;
1822
2027
  }
1823
2028
  function reportHooksTargetCheck(section2) {
1824
- const settingsPath = join15(CLAUDE_HOME, "settings.json");
1825
- if (!existsSync13(settingsPath)) {
2029
+ const settingsPath = join19(CLAUDE_HOME, "settings.json");
2030
+ if (!existsSync16(settingsPath)) {
1826
2031
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
1827
2032
  return;
1828
2033
  }
@@ -1848,18 +2053,14 @@ init_config();
1848
2053
 
1849
2054
  // src/commands.doctor.engine.ts
1850
2055
  init_color();
1851
- import { readFileSync as readFileSync5 } from "node:fs";
2056
+ import { readFileSync as readFileSync6 } from "node:fs";
1852
2057
  import { fileURLToPath as fileURLToPath3 } from "node:url";
1853
2058
 
1854
2059
  // src/commands.doctor.version.ts
1855
2060
  init_color();
1856
2061
  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";
2062
+ import { readFileSync as readFileSync5 } from "node:fs";
1859
2063
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1860
- init_config();
1861
- var CACHE_PATH = join16(HOME, ".cache", "claude-nomad", "version-check.json");
1862
- var CACHE_TTL_MS = 60 * 60 * 1e3;
1863
2064
  var STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
1864
2065
  var STRICT_SEMVER_PREFIX = /^(\d+\.\d+\.\d+)(?:[-+]|$)/;
1865
2066
  function compareSemver(a, b) {
@@ -1874,7 +2075,7 @@ function compareSemver(a, b) {
1874
2075
  function readLocalVersion() {
1875
2076
  try {
1876
2077
  const pkgPath = fileURLToPath2(new URL("../package.json", import.meta.url));
1877
- const parsed = JSON.parse(readFileSync4(pkgPath, "utf8"));
2078
+ const parsed = JSON.parse(readFileSync5(pkgPath, "utf8"));
1878
2079
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
1879
2080
  return parsed.version;
1880
2081
  }
@@ -1883,28 +2084,6 @@ function readLocalVersion() {
1883
2084
  return null;
1884
2085
  }
1885
2086
  }
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
2087
  function fetchLatestVersion() {
1909
2088
  try {
1910
2089
  const url = "https://registry.npmjs.org/claude-nomad/latest";
@@ -1924,16 +2103,8 @@ function reportVersionCheck(section2) {
1924
2103
  if (local === null) return;
1925
2104
  const localPure = STRICT_SEMVER_PREFIX.exec(local)?.[1] ?? null;
1926
2105
  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
- }
2106
+ const latest = fetchLatestVersion();
2107
+ if (latest === null) return;
1937
2108
  const cmp = compareSemver(localPure, latest);
1938
2109
  if (cmp === 0) {
1939
2110
  addItem(section2, `${green(okGlyph)} claude-nomad: ${local} (latest)`);
@@ -1959,7 +2130,7 @@ function parseMinVersion(spec) {
1959
2130
  function readEnginesNode() {
1960
2131
  try {
1961
2132
  const pkgPath = fileURLToPath3(new URL("../package.json", import.meta.url));
1962
- const parsed = JSON.parse(readFileSync5(pkgPath, "utf8"));
2133
+ const parsed = JSON.parse(readFileSync6(pkgPath, "utf8"));
1963
2134
  const node = parsed.engines?.node;
1964
2135
  if (typeof node === "string" && node.length > 0) return node;
1965
2136
  return null;
@@ -1988,8 +2159,8 @@ function reportNodeEngineCheck(section2) {
1988
2159
  // src/commands.doctor.gitleaks-version.ts
1989
2160
  init_color();
1990
2161
  import { execFileSync as execFileSync8 } from "node:child_process";
1991
- import { existsSync as existsSync15 } from "node:fs";
1992
- import { join as join17 } from "node:path";
2162
+ import { existsSync as existsSync17 } from "node:fs";
2163
+ import { join as join20 } from "node:path";
1993
2164
  init_config();
1994
2165
  var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
1995
2166
  var GITLEAKS_TIMEOUT_MS = 5e3;
@@ -1998,7 +2169,7 @@ function majorMinorOf(value) {
1998
2169
  return m === null ? null : [m[1], m[2]];
1999
2170
  }
2000
2171
  function readGitleaksVersion(run, tomlExists) {
2001
- const tomlPath = join17(REPO_HOME, ".gitleaks.toml");
2172
+ const tomlPath = join20(REPO_HOME, ".gitleaks.toml");
2002
2173
  const args = ["version"];
2003
2174
  if (tomlExists(tomlPath)) args.push("--config", tomlPath);
2004
2175
  try {
@@ -2010,7 +2181,7 @@ function readGitleaksVersion(run, tomlExists) {
2010
2181
  return null;
2011
2182
  }
2012
2183
  }
2013
- function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists = existsSync15) {
2184
+ function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists = existsSync17) {
2014
2185
  const raw = readGitleaksVersion(run, tomlExists);
2015
2186
  if (raw === null) return;
2016
2187
  const local = majorMinorOf(raw);
@@ -2172,12 +2343,13 @@ function cmdDoctor(opts = {}) {
2172
2343
  reportHostAndPaths(host);
2173
2344
  reportRepoState(host);
2174
2345
  const links = section("Shared links");
2175
- const mapPath = join18(REPO_HOME, "path-map.json");
2176
- const rawMap = existsSync16(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
2346
+ const mapPath = join21(REPO_HOME, "path-map.json");
2347
+ const rawMap = existsSync18(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
2177
2348
  const map = rawMap ?? { projects: {} };
2178
2349
  reportSharedLinks(links, map);
2179
2350
  const hooksScan = section("Hook targets");
2180
2351
  reportHooksTargetCheck(hooksScan);
2352
+ reportHookScopeCheck(hooksScan);
2181
2353
  const settings = section("Settings");
2182
2354
  const base = loadBaseSettings(settings);
2183
2355
  const parsedSettings = loadAndReportSettings(settings);
@@ -2197,6 +2369,7 @@ function cmdDoctor(opts = {}) {
2197
2369
  reportNodeEngineCheck(version);
2198
2370
  reportGitleaksVersionCheck(version);
2199
2371
  reportOptionalDeps(version);
2372
+ reportBackupsCheck(version);
2200
2373
  const sharedScan = section("Shared scan");
2201
2374
  if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
2202
2375
  const schemaScan = section("Schema scan");
@@ -2218,8 +2391,8 @@ function cmdDoctor(opts = {}) {
2218
2391
  // src/commands.drop-session.ts
2219
2392
  init_config();
2220
2393
  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";
2394
+ import { existsSync as existsSync20, readdirSync as readdirSync8, statSync as statSync5 } from "node:fs";
2395
+ import { join as join24, relative as relative4 } from "node:path";
2223
2396
 
2224
2397
  // src/commands.drop-session.git.ts
2225
2398
  init_config();
@@ -2262,8 +2435,8 @@ function isInIndex(rel) {
2262
2435
  init_config();
2263
2436
  init_utils();
2264
2437
  init_utils_json();
2265
- import { existsSync as existsSync17 } from "node:fs";
2266
- import { join as join19 } from "node:path";
2438
+ import { existsSync as existsSync19 } from "node:fs";
2439
+ import { join as join22 } from "node:path";
2267
2440
  var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
2268
2441
  function reportScrubHint(id, matches) {
2269
2442
  const live = resolveLiveTranscript(id, matches);
@@ -2279,16 +2452,16 @@ function reportScrubHint(id, matches) {
2279
2452
  }
2280
2453
  function resolveLiveTranscript(id, matches) {
2281
2454
  try {
2282
- const mapPath = join19(REPO_HOME, "path-map.json");
2283
- if (!existsSync17(mapPath)) return null;
2455
+ const mapPath = join22(REPO_HOME, "path-map.json");
2456
+ if (!existsSync19(mapPath)) return null;
2284
2457
  const projects = readJson(mapPath).projects;
2285
2458
  for (const rel of matches) {
2286
2459
  const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
2287
2460
  if (logical === void 0) continue;
2288
2461
  const abs = projects[logical]?.[HOST];
2289
2462
  if (abs === void 0) continue;
2290
- const live = join19(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2291
- if (existsSync17(live)) return live;
2463
+ const live = join22(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2464
+ if (existsSync19(live)) return live;
2292
2465
  }
2293
2466
  return null;
2294
2467
  } catch {
@@ -2302,15 +2475,15 @@ init_utils();
2302
2475
  // src/utils.lockfile.ts
2303
2476
  init_config();
2304
2477
  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");
2478
+ import { closeSync as closeSync2, mkdirSync as mkdirSync5, openSync as openSync2, readFileSync as readFileSync7, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
2479
+ import { dirname as dirname3, join as join23 } from "node:path";
2480
+ var LOCK_PATH = join23(HOME, ".cache", "claude-nomad", "nomad.lock");
2308
2481
  function acquireLock(verb) {
2309
- mkdirSync6(dirname3(LOCK_PATH), { recursive: true });
2482
+ mkdirSync5(dirname3(LOCK_PATH), { recursive: true });
2310
2483
  try {
2311
2484
  const fd = openSync2(LOCK_PATH, "wx");
2312
2485
  try {
2313
- writeFileSync4(fd, String(process.pid));
2486
+ writeFileSync3(fd, String(process.pid));
2314
2487
  } catch (writeErr) {
2315
2488
  try {
2316
2489
  closeSync2(fd);
@@ -2344,7 +2517,7 @@ function releaseLock(handle) {
2344
2517
  function unlinkIfSamePid(expectedPidStr) {
2345
2518
  let current;
2346
2519
  try {
2347
- current = readFileSync6(LOCK_PATH, "utf8").trim();
2520
+ current = readFileSync7(LOCK_PATH, "utf8").trim();
2348
2521
  } catch {
2349
2522
  return false;
2350
2523
  }
@@ -2359,7 +2532,7 @@ function unlinkIfSamePid(expectedPidStr) {
2359
2532
  function checkStaleAndRetry(verb) {
2360
2533
  let pidStr;
2361
2534
  try {
2362
- pidStr = readFileSync6(LOCK_PATH, "utf8").trim();
2535
+ pidStr = readFileSync7(LOCK_PATH, "utf8").trim();
2363
2536
  } catch {
2364
2537
  pidStr = "";
2365
2538
  }
@@ -2388,7 +2561,7 @@ function retryOnce(verb) {
2388
2561
  try {
2389
2562
  const fd = openSync2(LOCK_PATH, "wx");
2390
2563
  try {
2391
- writeFileSync4(fd, String(process.pid));
2564
+ writeFileSync3(fd, String(process.pid));
2392
2565
  } catch {
2393
2566
  try {
2394
2567
  closeSync2(fd);
@@ -2414,12 +2587,12 @@ function cmdDropSession(id) {
2414
2587
  fail(`invalid session id: ${id}`);
2415
2588
  process.exit(1);
2416
2589
  }
2417
- if (!existsSync18(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2590
+ if (!existsSync20(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2418
2591
  const handle = acquireLock("drop-session");
2419
2592
  if (handle === null) process.exit(0);
2420
2593
  try {
2421
- const repoProjects = join21(REPO_HOME, "shared", "projects");
2422
- if (!existsSync18(repoProjects)) {
2594
+ const repoProjects = join24(REPO_HOME, "shared", "projects");
2595
+ if (!existsSync20(repoProjects)) {
2423
2596
  throw new NomadFatal(`no staged session matches ${id}`);
2424
2597
  }
2425
2598
  const matches = collectMatches(repoProjects, id);
@@ -2440,13 +2613,13 @@ function cmdDropSession(id) {
2440
2613
  }
2441
2614
  function collectMatches(repoProjects, id) {
2442
2615
  const matches = [];
2443
- for (const logical of readdirSync5(repoProjects)) {
2444
- const candidate = join21(repoProjects, logical, `${id}.jsonl`);
2445
- if (existsSync18(candidate)) {
2616
+ for (const logical of readdirSync8(repoProjects)) {
2617
+ const candidate = join24(repoProjects, logical, `${id}.jsonl`);
2618
+ if (existsSync20(candidate)) {
2446
2619
  matches.push(relative4(REPO_HOME, candidate));
2447
2620
  }
2448
- const dir = join21(repoProjects, logical, id);
2449
- if (existsSync18(dir) && statSync4(dir).isDirectory()) {
2621
+ const dir = join24(repoProjects, logical, id);
2622
+ if (existsSync20(dir) && statSync5(dir).isDirectory()) {
2450
2623
  const dirRel = relative4(REPO_HOME, dir);
2451
2624
  const staged = expandStagedDir(dirRel);
2452
2625
  if (staged.length > 0) matches.push(...staged);
@@ -2482,13 +2655,13 @@ function unstageOne(rel) {
2482
2655
 
2483
2656
  // src/commands.redact.ts
2484
2657
  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";
2658
+ import { existsSync as existsSync21, readFileSync as readFileSync8, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
2659
+ import { join as join26 } from "node:path";
2487
2660
 
2488
2661
  // src/commands.redact.core.ts
2489
2662
  init_config();
2490
2663
  import { appendFileSync } from "node:fs";
2491
- import { join as join22 } from "node:path";
2664
+ import { join as join25 } from "node:path";
2492
2665
  function collectMatchIntervals(content, findings) {
2493
2666
  const intervals = [];
2494
2667
  for (const f of findings) {
@@ -2538,7 +2711,7 @@ function isRecentlyModified(mtimeMs, nowMs, thresholdMs = 5 * 60 * 1e3) {
2538
2711
  return nowMs - mtimeMs < thresholdMs;
2539
2712
  }
2540
2713
  function appendGitleaksIgnore(fingerprint) {
2541
- appendFileSync(join22(REPO_HOME, ".gitleaksignore"), formatFingerprint(fingerprint), "utf8");
2714
+ appendFileSync(join25(REPO_HOME, ".gitleaksignore"), formatFingerprint(fingerprint), "utf8");
2542
2715
  }
2543
2716
 
2544
2717
  // src/commands.redact.ts
@@ -2548,14 +2721,14 @@ init_utils_json();
2548
2721
  init_utils();
2549
2722
  function resolveLiveTranscript2(id) {
2550
2723
  try {
2551
- const mapPath = join23(REPO_HOME, "path-map.json");
2552
- if (!existsSync19(mapPath)) return null;
2724
+ const mapPath = join26(REPO_HOME, "path-map.json");
2725
+ if (!existsSync21(mapPath)) return null;
2553
2726
  const projects = readJson(mapPath).projects;
2554
2727
  for (const hostMap of Object.values(projects)) {
2555
2728
  const abs = hostMap[HOST];
2556
2729
  if (abs === void 0) continue;
2557
- const live = join23(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2558
- if (existsSync19(live)) return live;
2730
+ const live = join26(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
2731
+ if (existsSync21(live)) return live;
2559
2732
  }
2560
2733
  return null;
2561
2734
  } catch {
@@ -2573,17 +2746,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
2573
2746
  fail(`invalid session id: ${id}`);
2574
2747
  process.exit(1);
2575
2748
  }
2576
- if (!existsSync19(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2749
+ if (!existsSync21(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
2577
2750
  const handle = acquireLock("redact");
2578
2751
  if (handle === null) process.exit(0);
2579
2752
  try {
2580
2753
  const localPath = resolveLiveTranscript2(id);
2581
- if (localPath === null || !existsSync19(localPath)) {
2754
+ if (localPath === null || !existsSync21(localPath)) {
2582
2755
  fail(`could not resolve local transcript for session ${id} on this host`);
2583
2756
  process.exitCode = 1;
2584
2757
  return;
2585
2758
  }
2586
- const mtimeMs = statSync5(localPath).mtimeMs;
2759
+ const mtimeMs = statSync6(localPath).mtimeMs;
2587
2760
  if (isRecentlyModified(mtimeMs, nowMs())) {
2588
2761
  log(
2589
2762
  `session ${id} was modified recently and may be active.
@@ -2612,12 +2785,12 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
2612
2785
  );
2613
2786
  return;
2614
2787
  }
2615
- const backupBase = join23(HOME, ".cache", "claude-nomad", "backup");
2788
+ const backupBase = join26(HOME, ".cache", "claude-nomad", "backup");
2616
2789
  const ts = freshBackupTs(backupBase);
2617
2790
  backupBeforeWrite(localPath, ts);
2618
- const original = readFileSync7(localPath, "utf8");
2791
+ const original = readFileSync8(localPath, "utf8");
2619
2792
  const redacted = applyRedactions(original, findings);
2620
- writeFileSync5(localPath, redacted, "utf8");
2793
+ writeFileSync4(localPath, redacted, "utf8");
2621
2794
  log(`redacted ${findings.length} finding(s) in ${localPath} (backup: ${ts})`);
2622
2795
  } catch (err) {
2623
2796
  if (!(err instanceof NomadFatal)) {
@@ -2631,8 +2804,8 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
2631
2804
  }
2632
2805
 
2633
2806
  // src/commands.pull.ts
2634
- import { existsSync as existsSync25, mkdirSync as mkdirSync8 } from "node:fs";
2635
- import { join as join29 } from "node:path";
2807
+ import { existsSync as existsSync27, mkdirSync as mkdirSync7 } from "node:fs";
2808
+ import { join as join32 } from "node:path";
2636
2809
 
2637
2810
  // src/commands.push.sections.ts
2638
2811
  init_color();
@@ -2728,8 +2901,8 @@ init_config();
2728
2901
 
2729
2902
  // src/extras-sync.ts
2730
2903
  init_config();
2731
- import { existsSync as existsSync22 } from "node:fs";
2732
- import { join as join26 } from "node:path";
2904
+ import { existsSync as existsSync24 } from "node:fs";
2905
+ import { join as join29 } from "node:path";
2733
2906
 
2734
2907
  // src/extras-sync.diff.ts
2735
2908
  init_utils();
@@ -2756,8 +2929,8 @@ function listDivergingFiles(a, b) {
2756
2929
 
2757
2930
  // src/extras-sync.core.ts
2758
2931
  init_config();
2759
- import { cpSync as cpSync4, existsSync as existsSync20, rmSync as rmSync6 } from "node:fs";
2760
- import { join as join24 } from "node:path";
2932
+ import { cpSync as cpSync4, existsSync as existsSync22, rmSync as rmSync7 } from "node:fs";
2933
+ import { join as join27 } from "node:path";
2761
2934
 
2762
2935
  // src/extras-sync.guards.ts
2763
2936
  init_utils();
@@ -2780,9 +2953,9 @@ function assertSafeLocalRoot(localRoot, logical) {
2780
2953
  init_utils();
2781
2954
  init_utils_json();
2782
2955
  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)) {
2956
+ const mapPath = join27(REPO_HOME, "path-map.json");
2957
+ const repoExtras = join27(REPO_HOME, "shared", "extras");
2958
+ if (!existsSync22(mapPath) || opts.requireRepoExtras === true && !existsSync22(repoExtras)) {
2786
2959
  if (opts.missingMsg !== void 0) log(opts.missingMsg);
2787
2960
  return null;
2788
2961
  }
@@ -2814,7 +2987,7 @@ function* eachExtrasTarget(v, counts) {
2814
2987
  }
2815
2988
  }
2816
2989
  function copyExtras(src, dst) {
2817
- rmSync6(dst, { recursive: true, force: true });
2990
+ rmSync7(dst, { recursive: true, force: true });
2818
2991
  cpSync4(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
2819
2992
  }
2820
2993
 
@@ -2824,8 +2997,8 @@ init_utils_json();
2824
2997
 
2825
2998
  // src/extras-sync.remap.ts
2826
2999
  init_config();
2827
- import { existsSync as existsSync21, mkdirSync as mkdirSync7 } from "node:fs";
2828
- import { join as join25 } from "node:path";
3000
+ import { existsSync as existsSync23, mkdirSync as mkdirSync6 } from "node:fs";
3001
+ import { join as join28 } from "node:path";
2829
3002
  init_utils_fs();
2830
3003
  function runExtrasOp(v, dryRun, paths, backup) {
2831
3004
  const counts = { unmapped: 0, skipped: 0 };
@@ -2833,7 +3006,7 @@ function runExtrasOp(v, dryRun, paths, backup) {
2833
3006
  const would = [];
2834
3007
  for (const t of eachExtrasTarget(v, counts)) {
2835
3008
  const { src, dst } = paths(t);
2836
- if (!existsSync21(src)) continue;
3009
+ if (!existsSync23(src)) continue;
2837
3010
  const item = `${t.logical}/${t.dirname}`;
2838
3011
  if (dryRun) {
2839
3012
  would.push(item);
@@ -2849,14 +3022,14 @@ function remapExtrasPush(ts, opts = {}) {
2849
3022
  const dryRun = opts.dryRun === true;
2850
3023
  const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
2851
3024
  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 });
3025
+ const repoExtras = join28(REPO_HOME, "shared", "extras");
3026
+ if (!dryRun) mkdirSync6(repoExtras, { recursive: true });
2854
3027
  const { unmapped, skipped, done, would } = runExtrasOp(
2855
3028
  v,
2856
3029
  dryRun,
2857
3030
  ({ localRoot, logical, dirname: dirname4 }) => ({
2858
- src: join25(localRoot, dirname4),
2859
- dst: join25(repoExtras, logical, dirname4)
3031
+ src: join28(localRoot, dirname4),
3032
+ dst: join28(repoExtras, logical, dirname4)
2860
3033
  }),
2861
3034
  (dst) => backupRepoWrite(dst, ts, REPO_HOME)
2862
3035
  );
@@ -2869,13 +3042,13 @@ function remapExtrasPull(ts, opts = {}) {
2869
3042
  missingMsg: "no path-map or repo extras dir; skipping extras remap"
2870
3043
  });
2871
3044
  if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
2872
- const repoExtras = join25(REPO_HOME, "shared", "extras");
3045
+ const repoExtras = join28(REPO_HOME, "shared", "extras");
2873
3046
  const { unmapped, skipped, done, would } = runExtrasOp(
2874
3047
  v,
2875
3048
  dryRun,
2876
3049
  ({ localRoot, logical, dirname: dirname4 }) => ({
2877
- src: join25(repoExtras, logical, dirname4),
2878
- dst: join25(localRoot, dirname4)
3050
+ src: join28(repoExtras, logical, dirname4),
3051
+ dst: join28(localRoot, dirname4)
2879
3052
  }),
2880
3053
  // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
2881
3054
  // localRoot so the backup tree mirrors the project layout.
@@ -2889,14 +3062,14 @@ function divergenceCheckExtras(ts) {
2889
3062
  const v = loadValidatedExtras({});
2890
3063
  if (v === null) return;
2891
3064
  const counts = { unmapped: 0, skipped: 0 };
2892
- const backupRoot = join26(HOME, ".cache", "claude-nomad", "backup", ts, "extras");
3065
+ const backupRoot = join29(HOME, ".cache", "claude-nomad", "backup", ts, "extras");
2893
3066
  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;
3067
+ const local = join29(localRoot, dirname4);
3068
+ const repo = join29(REPO_HOME, "shared", "extras", logical, dirname4);
3069
+ if (!existsSync24(local) || !existsSync24(repo)) continue;
2897
3070
  const diff = listDivergingFiles(local, repo);
2898
3071
  if (diff.length === 0) continue;
2899
- const projectBackupRoot = join26(backupRoot, encodePath(localRoot));
3072
+ const projectBackupRoot = join29(backupRoot, encodePath(localRoot));
2900
3073
  warn(
2901
3074
  `local ${dirname4} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
2902
3075
  );
@@ -2909,47 +3082,47 @@ init_config();
2909
3082
  init_utils();
2910
3083
  init_utils_fs();
2911
3084
  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";
3085
+ import { existsSync as existsSync25, lstatSync as lstatSync6, rmSync as rmSync8 } from "node:fs";
3086
+ import { join as join30 } from "node:path";
2914
3087
  function applySharedLinks(ts, map, opts = {}) {
2915
3088
  const dryRun = opts.dryRun === true;
2916
3089
  const linkNames = allSharedLinks(map);
2917
3090
  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;
3091
+ const linkPath = join30(CLAUDE_HOME, name);
3092
+ const target = join30(REPO_HOME, "shared", name);
3093
+ if (!existsSync25(linkPath)) continue;
3094
+ if (lstatSync6(linkPath).isSymbolicLink()) continue;
3095
+ if (!existsSync25(target)) continue;
2923
3096
  if (dryRun) {
2924
3097
  log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
2925
3098
  continue;
2926
3099
  }
2927
3100
  backupBeforeWrite(linkPath, ts);
2928
- rmSync7(linkPath, { recursive: true, force: true });
3101
+ rmSync8(linkPath, { recursive: true, force: true });
2929
3102
  }
2930
3103
  for (const name of linkNames) {
2931
- const target = join27(REPO_HOME, "shared", name);
2932
- if (!existsSync23(target)) continue;
3104
+ const target = join30(REPO_HOME, "shared", name);
3105
+ if (!existsSync25(target)) continue;
2933
3106
  if (dryRun) {
2934
- log(`would create symlink: ${join27(CLAUDE_HOME, name)} -> ${target}`);
3107
+ log(`would create symlink: ${join30(CLAUDE_HOME, name)} -> ${target}`);
2935
3108
  continue;
2936
3109
  }
2937
- ensureSymlink(join27(CLAUDE_HOME, name), target);
3110
+ ensureSymlink(join30(CLAUDE_HOME, name), target);
2938
3111
  }
2939
3112
  }
2940
3113
  function regenerateSettings(ts, opts = {}) {
2941
3114
  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)) {
3115
+ const basePath = join30(REPO_HOME, "shared", "settings.base.json");
3116
+ const hostPath = join30(REPO_HOME, "hosts", `${HOST}.json`);
3117
+ if (!existsSync25(basePath)) {
2945
3118
  die("repo not initialized; run 'nomad init' to scaffold");
2946
3119
  }
2947
3120
  const base = readJson(basePath);
2948
- const hasOverrides = existsSync23(hostPath);
3121
+ const hasOverrides = existsSync25(hostPath);
2949
3122
  const overrides = hasOverrides ? readJson(hostPath) : {};
2950
3123
  const merged = deepMerge(base, overrides);
2951
- const settingsPath = join27(CLAUDE_HOME, "settings.json");
2952
- if (!hasOverrides && existsSync23(settingsPath)) {
3124
+ const settingsPath = join30(CLAUDE_HOME, "settings.json");
3125
+ if (!hasOverrides && existsSync25(settingsPath)) {
2953
3126
  try {
2954
3127
  const existing = readJson(settingsPath);
2955
3128
  const baseKeys = new Set(Object.keys(base));
@@ -2975,8 +3148,8 @@ function regenerateSettings(ts, opts = {}) {
2975
3148
 
2976
3149
  // src/preview.ts
2977
3150
  init_config();
2978
- import { existsSync as existsSync24 } from "node:fs";
2979
- import { join as join28 } from "node:path";
3151
+ import { existsSync as existsSync26 } from "node:fs";
3152
+ import { join as join31 } from "node:path";
2980
3153
 
2981
3154
  // node_modules/diff/libesm/diff/base.js
2982
3155
  var Diff = class {
@@ -3262,7 +3435,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
3262
3435
  return lines.join("\n");
3263
3436
  }
3264
3437
  function readJsonOrNull(path) {
3265
- if (!existsSync24(path)) return null;
3438
+ if (!existsSync26(path)) return null;
3266
3439
  try {
3267
3440
  return readJson(path);
3268
3441
  } catch {
@@ -3276,12 +3449,12 @@ function previewSettings(basePath, hostPath, settingsPath) {
3276
3449
  return;
3277
3450
  }
3278
3451
  const hostOverrides = readJsonOrNull(hostPath);
3279
- if (hostOverrides === null && existsSync24(hostPath)) {
3452
+ if (hostOverrides === null && existsSync26(hostPath)) {
3280
3453
  log(`settings.json: malformed hosts/${HOST}.json; ignoring overrides`);
3281
3454
  }
3282
3455
  const merged = deepMerge(base, hostOverrides ?? {});
3283
3456
  const current = readJsonOrNull(settingsPath);
3284
- if (current === null && existsSync24(settingsPath)) {
3457
+ if (current === null && existsSync26(settingsPath)) {
3285
3458
  log("settings.json: malformed; skipping diff");
3286
3459
  return;
3287
3460
  }
@@ -3300,9 +3473,9 @@ function computePreview(ts, map) {
3300
3473
  log(`would pull on host=${HOST} (dry-run; no mutation)`);
3301
3474
  applySharedLinks(ts, map, { dryRun: true });
3302
3475
  previewSettings(
3303
- join28(REPO_HOME, "shared", "settings.base.json"),
3304
- join28(REPO_HOME, "hosts", `${HOST}.json`),
3305
- join28(CLAUDE_HOME, "settings.json")
3476
+ join31(REPO_HOME, "shared", "settings.base.json"),
3477
+ join31(REPO_HOME, "hosts", `${HOST}.json`),
3478
+ join31(CLAUDE_HOME, "settings.json")
3306
3479
  );
3307
3480
  const remapResult = remapPull(ts, { dryRun: true });
3308
3481
  return { unmapped: remapResult.unmapped, collisions: 0 };
@@ -3331,19 +3504,18 @@ function applyWetPull(ts, map) {
3331
3504
  }
3332
3505
  function cmdPull(opts = {}) {
3333
3506
  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"))) {
3507
+ if (!existsSync27(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3508
+ if (!existsSync27(join32(REPO_HOME, "shared", "settings.base.json"))) {
3336
3509
  die("repo not initialized; run 'nomad init' to scaffold");
3337
3510
  }
3338
3511
  const handle = acquireLock("pull");
3339
3512
  if (handle === null) process.exit(0);
3340
3513
  try {
3341
- const backupBase = join29(HOME, ".cache", "claude-nomad", "backup");
3342
- const ts = freshBackupTs(backupBase);
3514
+ const ts = freshBackupTs(BACKUP_BASE);
3343
3515
  if (!dryRun) {
3344
- const backupRoot = join29(backupBase, ts);
3516
+ const backupRoot = join32(BACKUP_BASE, ts);
3345
3517
  try {
3346
- mkdirSync8(backupRoot, { recursive: true });
3518
+ mkdirSync7(backupRoot, { recursive: true });
3347
3519
  } catch (err) {
3348
3520
  die(`could not create backup dir: ${err.message}`);
3349
3521
  }
@@ -3352,8 +3524,8 @@ function cmdPull(opts = {}) {
3352
3524
  dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
3353
3525
  );
3354
3526
  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: {} };
3527
+ const mapPath = join32(REPO_HOME, "path-map.json");
3528
+ const map = existsSync27(mapPath) ? readPathMap(mapPath) : { projects: {} };
3357
3529
  divergenceCheckExtras(ts);
3358
3530
  if (dryRun) {
3359
3531
  const previewResult = computePreview(ts, map);
@@ -3376,8 +3548,8 @@ function cmdPull(opts = {}) {
3376
3548
 
3377
3549
  // src/commands.push.ts
3378
3550
  init_config();
3379
- import { existsSync as existsSync27 } from "node:fs";
3380
- import { join as join33, relative as relative5 } from "node:path";
3551
+ import { existsSync as existsSync29 } from "node:fs";
3552
+ import { join as join36, relative as relative5 } from "node:path";
3381
3553
 
3382
3554
  // src/commands.push.allowlist.ts
3383
3555
  init_config();
@@ -3454,8 +3626,8 @@ import { createInterface } from "node:readline/promises";
3454
3626
 
3455
3627
  // src/commands.push.recovery.redact.ts
3456
3628
  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";
3629
+ import { cpSync as cpSync5, readFileSync as readFileSync9, statSync as statSync7, writeFileSync as writeFileSync5 } from "node:fs";
3630
+ import { join as join33, sep as sep2 } from "node:path";
3459
3631
  init_push_gitleaks_scan();
3460
3632
  init_utils_fs();
3461
3633
  init_utils_json();
@@ -3499,7 +3671,7 @@ function applyRedact(f, allFindings, ts, map, nowMs, scan = scanFile) {
3499
3671
  `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
3500
3672
  );
3501
3673
  }
3502
- if (isRecentlyModified(statSync6(localPath).mtimeMs, nowMs())) {
3674
+ if (isRecentlyModified(statSync7(localPath).mtimeMs, nowMs())) {
3503
3675
  return refuse(
3504
3676
  `session ${sid} looks active (modified within the last 5 minutes); refusing to redact, no changes made.
3505
3677
  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 +3687,13 @@ function applyRedact(f, allFindings, ts, map, nowMs, scan = scanFile) {
3515
3687
  );
3516
3688
  }
3517
3689
  backupBeforeWrite(localPath, ts);
3518
- writeFileSync6(localPath, applyRedactions(readFileSync8(localPath, "utf8"), realFindings), "utf8");
3690
+ writeFileSync5(localPath, applyRedactions(readFileSync9(localPath, "utf8"), realFindings), "utf8");
3519
3691
  let copied = false;
3520
3692
  for (const [logical, hostMap] of Object.entries(map.projects)) {
3521
3693
  const abs = hostMap[HOST];
3522
3694
  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`), {
3695
+ if (localPath.startsWith(join33(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
3696
+ cpSync5(localPath, join33(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`), {
3525
3697
  force: true
3526
3698
  });
3527
3699
  copied = true;
@@ -3538,16 +3710,16 @@ function applyRedact(f, allFindings, ts, map, nowMs, scan = scanFile) {
3538
3710
 
3539
3711
  // src/commands.push.recovery.drop.ts
3540
3712
  init_config();
3541
- import { rmSync as rmSync8 } from "node:fs";
3542
- import { join as join31 } from "node:path";
3713
+ import { rmSync as rmSync9 } from "node:fs";
3714
+ import { join as join34 } from "node:path";
3543
3715
  function dropSessionFromStaged(sid, map) {
3544
3716
  const logicals = Object.keys(map.projects);
3545
3717
  if (logicals.length === 0) return false;
3546
3718
  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 });
3719
+ const jsonl = join34(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
3720
+ const dir = join34(REPO_HOME, "shared", "projects", logical, sid);
3721
+ rmSync9(jsonl, { force: true });
3722
+ rmSync9(dir, { recursive: true, force: true });
3551
3723
  }
3552
3724
  return true;
3553
3725
  }
@@ -3702,9 +3874,9 @@ init_push_checks();
3702
3874
  init_color();
3703
3875
  init_config();
3704
3876
  import { randomBytes as randomBytes2 } from "node:crypto";
3705
- import { existsSync as existsSync26, mkdirSync as mkdirSync9, readdirSync as readdirSync6, rmSync as rmSync9 } from "node:fs";
3877
+ import { existsSync as existsSync28, mkdirSync as mkdirSync8, readdirSync as readdirSync9, rmSync as rmSync10 } from "node:fs";
3706
3878
  import { homedir as homedir5 } from "node:os";
3707
- import { join as join32 } from "node:path";
3879
+ import { join as join35 } from "node:path";
3708
3880
  init_push_leak_verdict();
3709
3881
  init_push_gitleaks();
3710
3882
  init_utils_fs();
@@ -3719,13 +3891,13 @@ function stageSessions(tmpRoot, map) {
3719
3891
  if (!p || p === "TBD") continue;
3720
3892
  reverse.set(encodePath(p), logical);
3721
3893
  }
3722
- const localProjects = join32(CLAUDE_HOME, "projects");
3723
- if (!existsSync26(localProjects)) return 0;
3894
+ const localProjects = join35(CLAUDE_HOME, "projects");
3895
+ if (!existsSync28(localProjects)) return 0;
3724
3896
  let staged = 0;
3725
- for (const dir of readdirSync6(localProjects)) {
3897
+ for (const dir of readdirSync9(localProjects)) {
3726
3898
  const logical = reverse.get(dir);
3727
3899
  if (!logical) continue;
3728
- copyDirJsonlOnly(join32(localProjects, dir), join32(tmpRoot, "shared", "projects", logical));
3900
+ copyDirJsonlOnly(join35(localProjects, dir), join35(tmpRoot, "shared", "projects", logical));
3729
3901
  staged++;
3730
3902
  }
3731
3903
  return staged;
@@ -3741,9 +3913,9 @@ function stageExtras(tmpRoot, map) {
3741
3913
  if (!localRoot || localRoot === "TBD") continue;
3742
3914
  for (const dirname4 of dirnames) {
3743
3915
  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);
3916
+ const src = join35(localRoot, dirname4);
3917
+ if (!existsSync28(src)) continue;
3918
+ const dst = join35(tmpRoot, "shared", "extras", logical, dirname4);
3747
3919
  copyExtras(src, dst);
3748
3920
  staged++;
3749
3921
  }
@@ -3751,10 +3923,10 @@ function stageExtras(tmpRoot, map) {
3751
3923
  return staged;
3752
3924
  }
3753
3925
  function previewPushLeaks(map) {
3754
- const cacheDir = join32(homedir5(), ".cache", "claude-nomad");
3755
- mkdirSync9(cacheDir, { recursive: true });
3926
+ const cacheDir = join35(homedir5(), ".cache", "claude-nomad");
3927
+ mkdirSync8(cacheDir, { recursive: true });
3756
3928
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
3757
- const tmpRoot = join32(cacheDir, `push-preview-tree-${stamp}`);
3929
+ const tmpRoot = join35(cacheDir, `push-preview-tree-${stamp}`);
3758
3930
  try {
3759
3931
  const sessionCount = stageSessions(tmpRoot, map);
3760
3932
  const extrasCount = stageExtras(tmpRoot, map);
@@ -3772,7 +3944,7 @@ function previewPushLeaks(map) {
3772
3944
  }
3773
3945
  return verdictFromFindings(findings);
3774
3946
  } finally {
3775
- rmSync9(tmpRoot, { recursive: true, force: true });
3947
+ rmSync10(tmpRoot, { recursive: true, force: true });
3776
3948
  }
3777
3949
  }
3778
3950
 
@@ -3781,7 +3953,7 @@ init_utils();
3781
3953
  init_utils_fs();
3782
3954
  init_utils_json();
3783
3955
  function guardGitlinks() {
3784
- const gitlinks = findGitlinks(join33(REPO_HOME, "shared"));
3956
+ const gitlinks = findGitlinks(join36(REPO_HOME, "shared"));
3785
3957
  if (gitlinks.length === 0) return;
3786
3958
  for (const p of gitlinks) {
3787
3959
  const rel = relative5(REPO_HOME, p);
@@ -3815,15 +3987,14 @@ function runDryRunPreview(st, map) {
3815
3987
  async function cmdPush(opts = {}) {
3816
3988
  const dryRun = opts.dryRun === true;
3817
3989
  const redactAll = opts.redactAll === true;
3818
- if (!existsSync27(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3990
+ if (!existsSync29(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3819
3991
  const handle = acquireLock("push");
3820
3992
  if (handle === null) process.exit(0);
3821
3993
  try {
3822
3994
  console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
3823
3995
  probeGitleaks();
3824
3996
  rebaseBeforePush();
3825
- const backupBase = join33(HOME, ".cache", "claude-nomad", "backup");
3826
- const ts = freshBackupTs(backupBase);
3997
+ const ts = freshBackupTs(BACKUP_BASE);
3827
3998
  const remap = remapPush(ts, { dryRun });
3828
3999
  const extras = remapExtrasPush(ts, { dryRun });
3829
4000
  const st = { dryRun, remap, extras };
@@ -3834,8 +4005,8 @@ async function cmdPush(opts = {}) {
3834
4005
  renderNoScanTree(st);
3835
4006
  return;
3836
4007
  }
3837
- const mapPath = join33(REPO_HOME, "path-map.json");
3838
- if (!existsSync27(mapPath)) {
4008
+ const mapPath = join36(REPO_HOME, "path-map.json");
4009
+ if (!existsSync29(mapPath)) {
3839
4010
  if (dryRun) return runDryRunPreview(st, null);
3840
4011
  die("path-map.json missing, cannot enforce push allow-list");
3841
4012
  }
@@ -3875,18 +4046,17 @@ init_config();
3875
4046
 
3876
4047
  // src/diff.ts
3877
4048
  init_config();
3878
- import { existsSync as existsSync28 } from "node:fs";
3879
- import { join as join34 } from "node:path";
4049
+ import { existsSync as existsSync30 } from "node:fs";
4050
+ import { join as join37 } from "node:path";
3880
4051
  init_utils();
3881
4052
  init_utils_fs();
3882
4053
  init_utils_json();
3883
4054
  function cmdDiff() {
3884
4055
  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: {} };
4056
+ if (!existsSync30(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4057
+ const ts = freshBackupTs(BACKUP_BASE);
4058
+ const mapPath = join37(REPO_HOME, "path-map.json");
4059
+ const map = existsSync30(mapPath) ? readPathMap(mapPath) : { projects: {} };
3890
4060
  const result = computePreview(ts, map);
3891
4061
  emitSummary("diff", result.unmapped);
3892
4062
  } catch (err) {
@@ -3901,8 +4071,8 @@ function cmdDiff() {
3901
4071
 
3902
4072
  // src/init.ts
3903
4073
  init_config();
3904
- import { existsSync as existsSync30, mkdirSync as mkdirSync10, writeFileSync as writeFileSync7 } from "node:fs";
3905
- import { join as join36 } from "node:path";
4074
+ import { existsSync as existsSync32, mkdirSync as mkdirSync9, writeFileSync as writeFileSync6 } from "node:fs";
4075
+ import { join as join39 } from "node:path";
3906
4076
 
3907
4077
  // src/init.gh-onboard.ts
3908
4078
  init_config();
@@ -3983,31 +4153,31 @@ init_config();
3983
4153
  init_utils();
3984
4154
  init_utils_fs();
3985
4155
  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";
4156
+ import { copyFileSync, cpSync as cpSync6, existsSync as existsSync31, rmSync as rmSync11, statSync as statSync8 } from "node:fs";
4157
+ import { join as join38 } from "node:path";
3988
4158
  function snapshotIntoShared(map) {
3989
4159
  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);
4160
+ const src = join38(CLAUDE_HOME, name);
4161
+ if (!existsSync31(src)) continue;
4162
+ const dst = join38(REPO_HOME, "shared", name);
4163
+ if (statSync8(src).isDirectory()) {
4164
+ const gk = join38(dst, ".gitkeep");
4165
+ if (existsSync31(gk)) rmSync11(gk);
3996
4166
  cpSync6(src, dst, { recursive: true, force: false, errorOnExist: true });
3997
4167
  } else {
3998
4168
  copyFileSync(src, dst);
3999
4169
  }
4000
4170
  log(`snapshotted shared/${name} from ${src}`);
4001
4171
  }
4002
- const userSettings = join35(CLAUDE_HOME, "settings.json");
4003
- if (existsSync29(userSettings)) {
4172
+ const userSettings = join38(CLAUDE_HOME, "settings.json");
4173
+ if (existsSync31(userSettings)) {
4004
4174
  let parsed;
4005
4175
  try {
4006
4176
  parsed = readJson(userSettings);
4007
4177
  } catch (err) {
4008
4178
  return die(`malformed ${userSettings}: ${err.message}`);
4009
4179
  }
4010
- const hostFile = join35(REPO_HOME, "hosts", `${HOST}.json`);
4180
+ const hostFile = join38(REPO_HOME, "hosts", `${HOST}.json`);
4011
4181
  writeJsonAtomic(hostFile, parsed);
4012
4182
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
4013
4183
  }
@@ -4020,45 +4190,45 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
4020
4190
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
4021
4191
  function preflightConflict(repoHome) {
4022
4192
  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")
4193
+ join39(repoHome, "shared", "settings.base.json"),
4194
+ join39(repoHome, "shared", "CLAUDE.md"),
4195
+ join39(repoHome, "path-map.json"),
4196
+ join39(repoHome, "hosts"),
4197
+ join39(repoHome, "shared")
4028
4198
  ];
4029
4199
  for (const c of candidates) {
4030
- if (existsSync30(c)) return c;
4200
+ if (existsSync32(c)) return c;
4031
4201
  }
4032
4202
  return null;
4033
4203
  }
4034
4204
  function cmdInit(opts = {}) {
4035
4205
  const snapshot = opts.snapshot === true;
4036
4206
  const keepActions = opts.keepActions === true;
4037
- mkdirSync10(REPO_HOME, { recursive: true });
4207
+ mkdirSync9(REPO_HOME, { recursive: true });
4038
4208
  const conflict = preflightConflict(REPO_HOME);
4039
4209
  if (conflict !== null) {
4040
4210
  die(`already initialized; refusing to clobber ${conflict}`);
4041
4211
  }
4042
4212
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
4043
- mkdirSync10(join36(REPO_HOME, "shared"), { recursive: true });
4044
- mkdirSync10(join36(REPO_HOME, "hosts"), { recursive: true });
4213
+ mkdirSync9(join39(REPO_HOME, "shared"), { recursive: true });
4214
+ mkdirSync9(join39(REPO_HOME, "hosts"), { recursive: true });
4045
4215
  for (const name of SHARED_KEEP_DIRS) {
4046
- mkdirSync10(join36(REPO_HOME, "shared", name), { recursive: true });
4216
+ mkdirSync9(join39(REPO_HOME, "shared", name), { recursive: true });
4047
4217
  }
4048
- const userClaudeMd = join36(CLAUDE_HOME, "CLAUDE.md");
4049
- if (!snapshot || !existsSync30(userClaudeMd)) {
4050
- writeFileSync7(join36(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
4218
+ const userClaudeMd = join39(CLAUDE_HOME, "CLAUDE.md");
4219
+ if (!snapshot || !existsSync32(userClaudeMd)) {
4220
+ writeFileSync6(join39(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
4051
4221
  log("created shared/CLAUDE.md");
4052
4222
  }
4053
4223
  for (const name of SHARED_KEEP_DIRS) {
4054
- writeFileSync7(join36(REPO_HOME, "shared", name, ".gitkeep"), "");
4224
+ writeFileSync6(join39(REPO_HOME, "shared", name, ".gitkeep"), "");
4055
4225
  log(`created shared/${name}/.gitkeep`);
4056
4226
  }
4057
- writeFileSync7(join36(REPO_HOME, "hosts", ".gitkeep"), "");
4227
+ writeFileSync6(join39(REPO_HOME, "hosts", ".gitkeep"), "");
4058
4228
  log("created hosts/.gitkeep");
4059
- writeJsonAtomic(join36(REPO_HOME, "shared", "settings.base.json"), {});
4229
+ writeJsonAtomic(join39(REPO_HOME, "shared", "settings.base.json"), {});
4060
4230
  log("created shared/settings.base.json");
4061
- writeJsonAtomic(join36(REPO_HOME, "path-map.json"), { projects: {} });
4231
+ writeJsonAtomic(join39(REPO_HOME, "path-map.json"), { projects: {} });
4062
4232
  log("created path-map.json");
4063
4233
  if (snapshot) {
4064
4234
  snapshotIntoShared({ projects: {} });
@@ -4210,10 +4380,63 @@ function parseRedactArgs(argv) {
4210
4380
  return { id, rule, dryRun };
4211
4381
  }
4212
4382
 
4383
+ // src/nomad.dispatch.clean.ts
4384
+ var REJECT = { ok: false, advance: 0 };
4385
+ function applyOlderThan(argv, i, st) {
4386
+ if (st.olderThan !== void 0) return REJECT;
4387
+ const val = extractFlagValue(argv, i);
4388
+ if (val === null) return REJECT;
4389
+ st.olderThan = val;
4390
+ return { ok: true, advance: 2 };
4391
+ }
4392
+ function applyKeep(argv, i, st) {
4393
+ if (st.keep !== void 0) return REJECT;
4394
+ const val = extractFlagValue(argv, i);
4395
+ if (val === null || !/^\d+$/.test(val)) return REJECT;
4396
+ st.keep = Number(val);
4397
+ return { ok: true, advance: 2 };
4398
+ }
4399
+ function applyBool(seen, set) {
4400
+ if (seen) return REJECT;
4401
+ set();
4402
+ return { ok: true, advance: 1 };
4403
+ }
4404
+ function applyCleanToken(argv, i, st) {
4405
+ switch (argv[i]) {
4406
+ case "--backups":
4407
+ return applyBool(st.backups, () => st.backups = true);
4408
+ case "--dry-run":
4409
+ return applyBool(st.dryRun, () => st.dryRun = true);
4410
+ case "--older-than":
4411
+ return applyOlderThan(argv, i, st);
4412
+ case "--keep":
4413
+ return applyKeep(argv, i, st);
4414
+ default:
4415
+ return REJECT;
4416
+ }
4417
+ }
4418
+ function parseCleanArgs(argv) {
4419
+ const st = {
4420
+ backups: false,
4421
+ dryRun: false,
4422
+ olderThan: void 0,
4423
+ keep: void 0
4424
+ };
4425
+ let i = 3;
4426
+ while (i < argv.length) {
4427
+ const { ok: ok2, advance } = applyCleanToken(argv, i, st);
4428
+ if (!ok2) return null;
4429
+ i += advance;
4430
+ }
4431
+ if (!st.backups) return null;
4432
+ if (st.olderThan !== void 0 && st.keep !== void 0) return null;
4433
+ return { dryRun: st.dryRun, olderThan: st.olderThan, keep: st.keep };
4434
+ }
4435
+
4213
4436
  // package.json
4214
4437
  var package_default = {
4215
4438
  name: "claude-nomad",
4216
- version: "0.34.1",
4439
+ version: "0.35.0",
4217
4440
  type: "module",
4218
4441
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
4219
4442
  keywords: [
@@ -4363,6 +4586,14 @@ var DEFAULT_HELP = [
4363
4586
  "",
4364
4587
  row(" update", "Update the claude-nomad CLI to the latest npm release."),
4365
4588
  "",
4589
+ row(" clean", "Prune old backup snapshots under ~/.cache/claude-nomad/backup/."),
4590
+ row(" --backups", "Required: confirm backup pruning is the intended target."),
4591
+ row(" --dry-run", "List the snapshots that would be removed without deleting."),
4592
+ row(" --older-than <dur>", "Delete snapshots older than a duration (e.g. 14d, 24h, 30m)."),
4593
+ cont("Default when no retention flag is given: 14d."),
4594
+ row(" --keep <N>", "Keep the N most-recent snapshots, delete the rest. Mutually"),
4595
+ cont("exclusive with --older-than."),
4596
+ "",
4366
4597
  row(" --version", "Print the installed CLI version as bare semver to stdout; exits 0."),
4367
4598
  "",
4368
4599
  "Run `nomad doctor` to validate your setup. Edit shared/ or hosts/<HOST>.json",
@@ -4374,15 +4605,15 @@ var DEFAULT_HELP = [
4374
4605
  init_config();
4375
4606
  init_utils();
4376
4607
  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";
4608
+ import { existsSync as existsSync33, readFileSync as readFileSync10, readdirSync as readdirSync10 } from "node:fs";
4609
+ import { join as join40 } from "node:path";
4379
4610
  function resumeCmd(sessionId) {
4380
4611
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
4381
4612
  fail(`invalid session id: ${sessionId}`);
4382
4613
  process.exit(1);
4383
4614
  }
4384
- const projectsRoot = join37(CLAUDE_HOME, "projects");
4385
- if (!existsSync31(projectsRoot)) {
4615
+ const projectsRoot = join40(CLAUDE_HOME, "projects");
4616
+ if (!existsSync33(projectsRoot)) {
4386
4617
  fail(`${projectsRoot} does not exist`);
4387
4618
  process.exit(1);
4388
4619
  }
@@ -4396,8 +4627,8 @@ function resumeCmd(sessionId) {
4396
4627
  fail(`no cwd field found in ${jsonlPath}`);
4397
4628
  process.exit(1);
4398
4629
  }
4399
- const mapPath = join37(REPO_HOME, "path-map.json");
4400
- if (!existsSync31(mapPath)) {
4630
+ const mapPath = join40(REPO_HOME, "path-map.json");
4631
+ if (!existsSync33(mapPath)) {
4401
4632
  fail("path-map.json missing");
4402
4633
  process.exit(1);
4403
4634
  }
@@ -4419,14 +4650,14 @@ function resumeCmd(sessionId) {
4419
4650
  console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
4420
4651
  }
4421
4652
  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;
4653
+ for (const dir of readdirSync10(projectsRoot)) {
4654
+ const candidate = join40(projectsRoot, dir, `${sessionId}.jsonl`);
4655
+ if (existsSync33(candidate)) return candidate;
4425
4656
  }
4426
4657
  return null;
4427
4658
  }
4428
4659
  function extractRecordedCwd(jsonlPath) {
4429
- for (const line of readFileSync9(jsonlPath, "utf8").split("\n")) {
4660
+ for (const line of readFileSync10(jsonlPath, "utf8").split("\n")) {
4430
4661
  if (!line.trim()) continue;
4431
4662
  try {
4432
4663
  const obj = JSON.parse(line);
@@ -4590,6 +4821,15 @@ try {
4590
4821
  cmdRedact(redactArgs);
4591
4822
  break;
4592
4823
  }
4824
+ case "clean": {
4825
+ const cleanArgs = parseCleanArgs(process.argv);
4826
+ if (cleanArgs === null) {
4827
+ console.error("usage: nomad clean --backups [--dry-run] [--older-than <dur> | --keep <N>]");
4828
+ process.exit(1);
4829
+ }
4830
+ cmdClean(cleanArgs);
4831
+ break;
4832
+ }
4593
4833
  default:
4594
4834
  console.error(DEFAULT_HELP);
4595
4835
  process.exit(1);