claude-nomad 0.44.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/nomad.mjs CHANGED
@@ -170,7 +170,7 @@ function gitOrFatal(args, context, cwd) {
170
170
  throw new NomadFatal(`${context} failed`);
171
171
  }
172
172
  }
173
- var log, warn, fail, NomadFatal, die, gitStatusPorcelainZ;
173
+ var log, warn, fail, item, NomadFatal, die, gitStatusPorcelainZ;
174
174
  var init_utils = __esm({
175
175
  "src/utils.ts"() {
176
176
  "use strict";
@@ -182,6 +182,7 @@ var init_utils = __esm({
182
182
  fail = (msg) => {
183
183
  console.error(`${red(failGlyph)} ${msg}`);
184
184
  };
185
+ item = (msg) => console.log(dim(` ${msg}`));
185
186
  NomadFatal = class extends Error {
186
187
  constructor(message) {
187
188
  super(message);
@@ -360,6 +361,18 @@ var init_settings_keys = __esm({
360
361
  // src/config.ts
361
362
  import { homedir, hostname } from "node:os";
362
363
  import { join, resolve } from "node:path";
364
+ function home() {
365
+ return process.env.HOME || homedir();
366
+ }
367
+ function claudeHome() {
368
+ return resolve(home(), ".claude");
369
+ }
370
+ function repoHome() {
371
+ return process.env.NOMAD_REPO || resolve(home(), "claude-nomad");
372
+ }
373
+ function backupBase() {
374
+ return join(home(), ".cache", "claude-nomad", "backup");
375
+ }
363
376
  function allSharedLinks(map) {
364
377
  const extras = [];
365
378
  for (const entry of map.sharedDirs ?? []) {
@@ -373,7 +386,7 @@ function allSharedLinks(map) {
373
386
  }
374
387
  return [...SHARED_LINKS, ...extras];
375
388
  }
376
- var HOME, CLAUDE_HOME, BACKUP_BASE, REPO_HOME, SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, HOST, SHARED_LINKS, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, PUSH_ALLOWED_STATIC;
389
+ var SETTINGS_SCHEMA_URL, NPM_REGISTRY_LATEST_URL, GITLEAKS_PINNED_VERSION, HOST, SHARED_LINKS, SUPPORTED_EXTRAS, ALWAYS_NEVER_SYNC, PUSH_ALLOWED_STATIC;
377
390
  var init_config = __esm({
378
391
  "src/config.ts"() {
379
392
  "use strict";
@@ -381,10 +394,6 @@ var init_config = __esm({
381
394
  init_utils();
382
395
  init_config_never_sync();
383
396
  init_settings_keys();
384
- HOME = homedir();
385
- CLAUDE_HOME = resolve(HOME, ".claude");
386
- BACKUP_BASE = join(HOME, ".cache", "claude-nomad", "backup");
387
- REPO_HOME = process.env.NOMAD_REPO || resolve(HOME, "claude-nomad");
388
397
  SETTINGS_SCHEMA_URL = "https://json.schemastore.org/claude-code-settings.json";
389
398
  NPM_REGISTRY_LATEST_URL = "https://registry.npmjs.org/claude-nomad/latest";
390
399
  GITLEAKS_PINNED_VERSION = "8.30.1";
@@ -526,18 +535,19 @@ function ensureSymlink(linkPath, target) {
526
535
  }
527
536
  function backupBeforeWrite(absPath, ts) {
528
537
  if (!existsSync(absPath)) return;
529
- const rel = relative(CLAUDE_HOME, absPath);
538
+ const claude = claudeHome();
539
+ const rel = relative(claude, absPath);
530
540
  if (rel.startsWith("..") || rel === "") return;
531
- const backupRoot = join2(BACKUP_BASE, ts);
541
+ const backupRoot = join2(backupBase(), ts);
532
542
  const dst = join2(backupRoot, rel);
533
543
  mkdirSync(dirname(dst), { recursive: true });
534
544
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
535
545
  }
536
- function backupRepoWrite(absPath, ts, repoHome) {
546
+ function backupRepoWrite(absPath, ts, repoHome2) {
537
547
  if (!existsSync(absPath)) return;
538
- const rel = relative(repoHome, absPath);
548
+ const rel = relative(repoHome2, absPath);
539
549
  if (rel.startsWith("..") || rel === "") return;
540
- const backupRoot = join2(BACKUP_BASE, ts, "repo");
550
+ const backupRoot = join2(backupBase(), ts, "repo");
541
551
  const dst = join2(backupRoot, rel);
542
552
  mkdirSync(dirname(dst), { recursive: true });
543
553
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
@@ -546,7 +556,7 @@ function backupExtrasWrite(absPath, ts, projectRoot) {
546
556
  if (!existsSync(absPath)) return;
547
557
  const rel = relative(projectRoot, absPath);
548
558
  if (rel.startsWith("..") || rel === "") return;
549
- const backupRoot = join2(BACKUP_BASE, ts, "extras");
559
+ const backupRoot = join2(backupBase(), ts, "extras");
550
560
  const dst = join2(backupRoot, encodePath(projectRoot), rel);
551
561
  mkdirSync(dirname(dst), { recursive: true });
552
562
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
@@ -565,8 +575,8 @@ import { existsSync as existsSync11, mkdtempSync, readFileSync as readFileSync3,
565
575
  import { tmpdir } from "node:os";
566
576
  import { join as join12 } from "node:path";
567
577
  import { fileURLToPath } from "node:url";
568
- function resolveTomlPath() {
569
- const repoToml = join12(REPO_HOME, ".gitleaks.toml");
578
+ function resolveTomlPath(repo = repoHome()) {
579
+ const repoToml = join12(repo, ".gitleaks.toml");
570
580
  if (existsSync11(repoToml)) return repoToml;
571
581
  const bundled = fileURLToPath(new URL("../.gitleaks.toml", import.meta.url));
572
582
  return existsSync11(bundled) ? bundled : null;
@@ -582,9 +592,10 @@ ${overlayBody}`;
582
592
  return { configPath, tempPath };
583
593
  }
584
594
  function resolveTomlConfig() {
585
- const overlayPath = join12(REPO_HOME, ".gitleaks.overlay.toml");
586
- const repoToml = join12(REPO_HOME, ".gitleaks.toml");
587
- const bundled = resolveTomlPath();
595
+ const repo = repoHome();
596
+ const overlayPath = join12(repo, ".gitleaks.overlay.toml");
597
+ const repoToml = join12(repo, ".gitleaks.toml");
598
+ const bundled = resolveTomlPath(repo);
588
599
  if (!existsSync11(overlayPath)) {
589
600
  return { path: bundled, tempPath: null };
590
601
  }
@@ -696,24 +707,23 @@ function probeGitleaks() {
696
707
  if (tempPath !== null) rmSync4(tempPath, { recursive: true, force: true });
697
708
  }
698
709
  }
699
- function rebaseBeforePush() {
710
+ function rebaseBeforePush(repo) {
700
711
  try {
701
712
  execFileSync2("git", ["pull", "--rebase", "--autostash"], {
702
- cwd: REPO_HOME,
713
+ cwd: repo,
703
714
  stdio: ["ignore", "pipe", "pipe"]
704
715
  });
705
716
  } catch (err) {
706
717
  const e = err;
707
718
  if (e.stderr) process.stderr.write(e.stderr);
708
719
  throw new NomadFatal(
709
- 'rebase failed; if a conflict was reported, resolve it in ~/claude-nomad/ and run "git rebase --continue" (or "git rebase --abort" to give up). Re-run nomad push after resolution.'
720
+ `rebase failed; if a conflict was reported, resolve it in ${repo} and run "git rebase --continue" (or "git rebase --abort" to give up). Re-run nomad push after resolution.`
710
721
  );
711
722
  }
712
723
  }
713
724
  var init_push_checks = __esm({
714
725
  "src/push-checks.ts"() {
715
726
  "use strict";
716
- init_config();
717
727
  init_push_gitleaks_config();
718
728
  init_utils();
719
729
  }
@@ -937,10 +947,10 @@ function verdictScanError(text) {
937
947
  process.exitCode = 1;
938
948
  return { leak: false, verdictRow: failRow(text), recovery: null, findings: [] };
939
949
  }
940
- function scanPushVerdict() {
950
+ function scanPushVerdict(repo) {
941
951
  let findings;
942
952
  try {
943
- findings = scanStagedTree(REPO_HOME, true);
953
+ findings = scanStagedTree(repo, true);
944
954
  } catch (err) {
945
955
  if (err.code === "ENOENT") {
946
956
  return {
@@ -970,7 +980,6 @@ var init_push_leak_verdict = __esm({
970
980
  "src/push-leak-verdict.ts"() {
971
981
  "use strict";
972
982
  init_color();
973
- init_config();
974
983
  init_push_checks();
975
984
  init_push_gitleaks();
976
985
  noLeaksRow = () => `${green(okGlyph)} no leaks`;
@@ -995,8 +1004,8 @@ function lexists(p) {
995
1004
  return false;
996
1005
  }
997
1006
  }
998
- function readMapIfPresent(repoHome) {
999
- const mapPath = join3(repoHome, "path-map.json");
1007
+ function readMapIfPresent(repoHome2) {
1008
+ const mapPath = join3(repoHome2, "path-map.json");
1000
1009
  return existsSync2(mapPath) ? readPathMap(mapPath) : { projects: {} };
1001
1010
  }
1002
1011
  function isConfiguredTarget(name, map) {
@@ -1006,14 +1015,14 @@ function isValidAdoptName(name) {
1006
1015
  if (SHARED_LINKS.includes(name)) return true;
1007
1016
  return isValidSharedDir(name);
1008
1017
  }
1009
- function performAdoptMove(name, linkPath, sharedTarget) {
1010
- const ts = freshBackupTs(BACKUP_BASE);
1018
+ function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
1019
+ const ts = freshBackupTs(backup);
1011
1020
  backupBeforeWrite(linkPath, ts);
1012
1021
  cpSync2(linkPath, sharedTarget, { recursive: true, force: true, preserveTimestamps: true });
1013
1022
  rmSync(linkPath, { recursive: true, force: true });
1014
1023
  ensureSymlink(linkPath, sharedTarget);
1015
1024
  const rel = join3("shared", name);
1016
- gitOrFatal(["add", "--", rel], `git add shared/${name}`, REPO_HOME);
1025
+ gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
1017
1026
  log(`adopted ${name}; ${ADOPT_PUSH_HINT}`);
1018
1027
  }
1019
1028
  function cmdAdopt(name, opts = {}) {
@@ -1022,15 +1031,18 @@ function cmdAdopt(name, opts = {}) {
1022
1031
  fail(`invalid name: ${JSON.stringify(name)}`);
1023
1032
  process.exit(1);
1024
1033
  }
1025
- const map = readMapIfPresent(REPO_HOME);
1034
+ const repo = repoHome();
1035
+ const claude = claudeHome();
1036
+ const backup = backupBase();
1037
+ const map = readMapIfPresent(repo);
1026
1038
  if (!isConfiguredTarget(name, map)) {
1027
1039
  fail(
1028
1040
  `${name}: not a configured shared target. Add it to sharedDirs in path-map.json first, then re-run adopt.`
1029
1041
  );
1030
1042
  process.exit(1);
1031
1043
  }
1032
- const linkPath = join3(CLAUDE_HOME, name);
1033
- const sharedTarget = join3(REPO_HOME, "shared", name);
1044
+ const linkPath = join3(claude, name);
1045
+ const sharedTarget = join3(repo, "shared", name);
1034
1046
  if (!existsSync2(linkPath)) {
1035
1047
  log(`${name}: nothing to adopt (not present in ~/.claude/)`);
1036
1048
  return;
@@ -1044,14 +1056,14 @@ function cmdAdopt(name, opts = {}) {
1044
1056
  process.exit(1);
1045
1057
  }
1046
1058
  if (dryRun) {
1047
- const ts = freshBackupTs(BACKUP_BASE);
1059
+ const ts = freshBackupTs(backup);
1048
1060
  log(`would backup: ${linkPath} -> backup/${ts}/${name}`);
1049
1061
  log(`would move: ${linkPath} -> shared/${name}`);
1050
1062
  log(`would stage: shared/${name}`);
1051
1063
  return;
1052
1064
  }
1053
1065
  try {
1054
- performAdoptMove(name, linkPath, sharedTarget);
1066
+ performAdoptMove(name, linkPath, sharedTarget, repo, backup);
1055
1067
  } catch (err) {
1056
1068
  if (!(err instanceof NomadFatal)) throw err;
1057
1069
  fail(err.message);
@@ -1064,7 +1076,6 @@ init_config();
1064
1076
  import { existsSync as existsSync3 } from "node:fs";
1065
1077
 
1066
1078
  // src/commands.redact.core.ts
1067
- init_config();
1068
1079
  import { appendFileSync, readFileSync as readFileSync2 } from "node:fs";
1069
1080
  import { join as join4 } from "node:path";
1070
1081
  function collectMatchIntervals(content, findings) {
@@ -1121,10 +1132,10 @@ function isValidFingerprint(fingerprint) {
1121
1132
  function isAlreadyPresent(line, lines) {
1122
1133
  return lines.includes(line);
1123
1134
  }
1124
- function appendGitleaksIgnore(fingerprint) {
1135
+ function appendGitleaksIgnore(fingerprint, repo) {
1125
1136
  const sanitized = fingerprint.replace(/[\r\n]/g, "").trim();
1126
1137
  if (sanitized.length === 0) return;
1127
- const ignPath = join4(REPO_HOME, ".gitleaksignore");
1138
+ const ignPath = join4(repo, ".gitleaksignore");
1128
1139
  let raw;
1129
1140
  try {
1130
1141
  raw = readFileSync2(ignPath, "utf8");
@@ -1141,7 +1152,8 @@ function appendGitleaksIgnore(fingerprint) {
1141
1152
  // src/commands.allow.ts
1142
1153
  init_utils();
1143
1154
  function cmdAllow(fingerprints) {
1144
- if (!existsSync3(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
1155
+ const repo = repoHome();
1156
+ if (!existsSync3(repo)) die(`repo not cloned at ${repo}`);
1145
1157
  for (const fp of fingerprints) {
1146
1158
  if (!isValidFingerprint(fp)) {
1147
1159
  const shown = fp.replaceAll("\r", String.raw`\r`).replaceAll("\n", String.raw`\n`);
@@ -1150,9 +1162,10 @@ function cmdAllow(fingerprints) {
1150
1162
  }
1151
1163
  }
1152
1164
  for (const fp of fingerprints) {
1153
- appendGitleaksIgnore(fp);
1154
- log(`allowed: ${fp}`);
1165
+ appendGitleaksIgnore(fp, repo);
1166
+ item(`allowed: ${fp}`);
1155
1167
  }
1168
+ log(`allowed ${fingerprints.length} fingerprint(s)`);
1156
1169
  }
1157
1170
 
1158
1171
  // src/commands.clean.ts
@@ -1172,9 +1185,9 @@ function parseDuration(s) {
1172
1185
  if (!m) return null;
1173
1186
  return Number(m[1]) * UNIT_MS[m[2]];
1174
1187
  }
1175
- function listBackupDirs(backupBase) {
1176
- if (!existsSync4(backupBase)) return [];
1177
- return readdirSync(backupBase).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join5(backupBase, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
1188
+ function listBackupDirs(backupBase2) {
1189
+ if (!existsSync4(backupBase2)) return [];
1190
+ return readdirSync(backupBase2).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join5(backupBase2, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
1178
1191
  }
1179
1192
  function prunableByAge(dirs, olderThanMs, nowMs) {
1180
1193
  return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
@@ -1182,9 +1195,9 @@ function prunableByAge(dirs, olderThanMs, nowMs) {
1182
1195
  function prunableByCount(dirs, keep) {
1183
1196
  return dirs.slice(keep).map((d) => d.name);
1184
1197
  }
1185
- function safeDelete(backupBase, name) {
1198
+ function safeDelete(backupBase2, name) {
1186
1199
  if (!isTsDir(name)) return;
1187
- const full = join5(backupBase, name);
1200
+ const full = join5(backupBase2, name);
1188
1201
  const st = lstatSync3(full, { throwIfNoEntry: false });
1189
1202
  if (!st || st.isSymbolicLink() || !st.isDirectory()) return;
1190
1203
  rmSync2(full, { recursive: true, force: true });
@@ -1193,7 +1206,7 @@ function resolveTargets(dirs, olderThanMs, keep) {
1193
1206
  if (keep !== void 0) return prunableByCount(dirs, keep);
1194
1207
  return prunableByAge(dirs, olderThanMs, Date.now());
1195
1208
  }
1196
- function cmdClean(opts, backupBase = BACKUP_BASE) {
1209
+ function cmdClean(opts, backupBase2 = backupBase()) {
1197
1210
  const { dryRun, olderThan, keep } = opts;
1198
1211
  if (olderThan !== void 0 && keep !== void 0) {
1199
1212
  fail("--older-than and --keep are mutually exclusive");
@@ -1208,14 +1221,14 @@ function cmdClean(opts, backupBase = BACKUP_BASE) {
1208
1221
  }
1209
1222
  olderThanMs = parsed;
1210
1223
  }
1211
- const dirs = listBackupDirs(backupBase);
1224
+ const dirs = listBackupDirs(backupBase2);
1212
1225
  const targets = resolveTargets(dirs, olderThanMs, keep);
1213
1226
  if (dryRun) {
1214
- for (const name of targets) log(`would remove ${name}`);
1227
+ for (const name of targets) item(name);
1215
1228
  log(`dry-run: ${targets.length} backup(s) would be removed`);
1216
1229
  return;
1217
1230
  }
1218
- for (const name of targets) safeDelete(backupBase, name);
1231
+ for (const name of targets) safeDelete(backupBase2, name);
1219
1232
  log(`removed ${targets.length} backup(s)`);
1220
1233
  }
1221
1234
 
@@ -1225,15 +1238,16 @@ init_utils();
1225
1238
  init_utils_json();
1226
1239
  import { cpSync as cpSync3, existsSync as existsSync5, lstatSync as lstatSync4, realpathSync, renameSync as renameSync2, rmSync as rmSync3 } from "node:fs";
1227
1240
  import { join as join6, sep } from "node:path";
1228
- var EJECT_CHECKLIST = [
1229
- "Manual steps remaining to finish leaving claude-nomad on this host:",
1230
- ` 1. Uninstall the CLI: npm uninstall -g claude-nomad`,
1231
- ` 2. Remove NOMAD_HOST and NOMAD_REPO from your shell rc (~/.zshrc or ~/.bashrc)`,
1232
- ` 3. Optionally delete the local sync checkout: rm -rf ${REPO_HOME}`,
1233
- ` 4. Optionally delete the private sync repo on GitHub`,
1234
- ` 5. Optionally delete the backup cache: rm -rf ${BACKUP_BASE}`
1235
- ].join("\n");
1236
- var DEFAULT_ROOTS = { claudeHome: CLAUDE_HOME, repoHome: REPO_HOME };
1241
+ function ejectChecklist() {
1242
+ return [
1243
+ "Manual steps remaining to finish leaving claude-nomad on this host:",
1244
+ ` 1. Uninstall the CLI: npm uninstall -g claude-nomad`,
1245
+ ` 2. Remove NOMAD_HOST and NOMAD_REPO from your shell rc (~/.zshrc or ~/.bashrc)`,
1246
+ ` 3. Optionally delete the local sync checkout: rm -rf ${repoHome()}`,
1247
+ ` 4. Optionally delete the private sync repo on GitHub`,
1248
+ ` 5. Optionally delete the backup cache: rm -rf ${backupBase()}`
1249
+ ].join("\n");
1250
+ }
1237
1251
  function errMessage(err) {
1238
1252
  return err instanceof Error ? err.message : String(err);
1239
1253
  }
@@ -1245,8 +1259,8 @@ function lexists2(p) {
1245
1259
  return false;
1246
1260
  }
1247
1261
  }
1248
- function readMapIfPresent2(repoHome) {
1249
- const mapPath = join6(repoHome, "path-map.json");
1262
+ function readMapIfPresent2(repoHome2) {
1263
+ const mapPath = join6(repoHome2, "path-map.json");
1250
1264
  return existsSync5(mapPath) ? readPathMap(mapPath) : { projects: {} };
1251
1265
  }
1252
1266
  function classifyName(linkPath) {
@@ -1255,12 +1269,12 @@ function classifyName(linkPath) {
1255
1269
  if (!existsSync5(linkPath)) return "dangling";
1256
1270
  return "materialize";
1257
1271
  }
1258
- function resolveSharedRoot(repoHome) {
1272
+ function resolveSharedRoot(repoHome2) {
1259
1273
  try {
1260
- return realpathSync(join6(repoHome, "shared"));
1274
+ return realpathSync(join6(repoHome2, "shared"));
1261
1275
  } catch {
1262
1276
  return die(
1263
- `cannot resolve ${join6(repoHome, "shared")} (repo checkout incomplete). run \`nomad pull\` first, then re-run \`nomad eject\``
1277
+ `cannot resolve ${join6(repoHome2, "shared")} (repo checkout incomplete). run \`nomad pull\` first, then re-run \`nomad eject\``
1264
1278
  );
1265
1279
  }
1266
1280
  }
@@ -1270,7 +1284,7 @@ function isManagedTarget(target, sharedRoot) {
1270
1284
  function materializeOne(name, linkPath, sharedRoot) {
1271
1285
  const target = realpathSync(linkPath);
1272
1286
  if (!isManagedTarget(target, sharedRoot)) {
1273
- log(`skipped (not a nomad-managed target): ${name} -> ${target}`);
1287
+ item(`skipped (not a nomad-managed target): ${name} -> ${target}`);
1274
1288
  return false;
1275
1289
  }
1276
1290
  const tmp = `${linkPath}.eject.tmp.${process.pid}.${Date.now()}`;
@@ -1284,7 +1298,7 @@ function materializeOne(name, linkPath, sharedRoot) {
1284
1298
  });
1285
1299
  rmSync3(linkPath, { force: true });
1286
1300
  renameSync2(tmp, linkPath);
1287
- log(`ejected: ${name}`);
1301
+ item(`ejected: ${name}`);
1288
1302
  return true;
1289
1303
  } catch (err) {
1290
1304
  try {
@@ -1294,45 +1308,45 @@ function materializeOne(name, linkPath, sharedRoot) {
1294
1308
  throw err;
1295
1309
  }
1296
1310
  }
1297
- function previewDryRun(names, classifications, claudeHome, sharedRoot) {
1311
+ function previewDryRun(names, classifications, claudeHome2, sharedRoot) {
1298
1312
  for (const name of names) {
1299
1313
  const cls = classifications.get(name);
1300
- const linkPath = join6(claudeHome, name);
1314
+ const linkPath = join6(claudeHome2, name);
1301
1315
  if (cls === "absent") {
1302
- log(`skipped (absent): ${name}`);
1316
+ item(`skipped (absent): ${name}`);
1303
1317
  } else if (cls === "skip-real") {
1304
- log(`skipped (not a symlink): ${name}`);
1318
+ item(`skipped (not a symlink): ${name}`);
1305
1319
  } else {
1306
1320
  previewMaterialize(name, linkPath, sharedRoot);
1307
1321
  }
1308
1322
  }
1309
- log(EJECT_CHECKLIST);
1323
+ log(ejectChecklist());
1310
1324
  }
1311
1325
  function previewMaterialize(name, linkPath, sharedRoot) {
1312
1326
  let target;
1313
1327
  try {
1314
1328
  target = realpathSync(linkPath);
1315
1329
  } catch {
1316
- log(`would materialize: ${name} (target now unresolvable; re-run to re-classify)`);
1330
+ item(`would materialize: ${name} (target now unresolvable; re-run to re-classify)`);
1317
1331
  return;
1318
1332
  }
1319
1333
  if (!isManagedTarget(target, sharedRoot)) {
1320
- log(`skipped (not a nomad-managed target): ${name} -> ${target}`);
1334
+ item(`skipped (not a nomad-managed target): ${name} -> ${target}`);
1321
1335
  return;
1322
1336
  }
1323
- log(`would materialize: ${name} (copy ${target} -> ${linkPath})`);
1337
+ item(`would materialize: ${name} (copy ${target} -> ${linkPath})`);
1324
1338
  }
1325
- function runLiveEject(names, classifications, claudeHome, sharedRoot) {
1339
+ function runLiveEject(names, classifications, claudeHome2, sharedRoot) {
1326
1340
  const done = [];
1327
1341
  let skipped = 0;
1328
1342
  for (const name of names) {
1329
1343
  const cls = classifications.get(name);
1330
- const linkPath = join6(claudeHome, name);
1344
+ const linkPath = join6(claudeHome2, name);
1331
1345
  if (cls === "absent") {
1332
- log(`skipped (absent): ${name}`);
1346
+ item(`skipped (absent): ${name}`);
1333
1347
  skipped++;
1334
1348
  } else if (cls === "skip-real") {
1335
- log(`skipped (not a symlink): ${name}`);
1349
+ item(`skipped (not a symlink): ${name}`);
1336
1350
  skipped++;
1337
1351
  } else if (materializeOneOrDie(name, linkPath, sharedRoot, done)) {
1338
1352
  done.push(name);
@@ -1341,7 +1355,7 @@ function runLiveEject(names, classifications, claudeHome, sharedRoot) {
1341
1355
  }
1342
1356
  }
1343
1357
  log(`materialized ${done.length}, skipped ${skipped}`);
1344
- log(EJECT_CHECKLIST);
1358
+ log(ejectChecklist());
1345
1359
  }
1346
1360
  function materializeOneOrDie(name, linkPath, sharedRoot, done) {
1347
1361
  try {
@@ -1349,18 +1363,21 @@ function materializeOneOrDie(name, linkPath, sharedRoot, done) {
1349
1363
  } catch (err) {
1350
1364
  const msg = errMessage(err);
1351
1365
  return die(
1352
- `failed to materialize ${name}: ${msg}. already materialized: ${done.join(", ") || "(none)"}. the remaining names are still symlinks; do NOT delete ${REPO_HOME} yet, fix the cause and re-run \`nomad eject\` (it is idempotent on already-real names)`
1366
+ `failed to materialize ${name}: ${msg}. already materialized: ${done.join(", ") || "(none)"}. the remaining names are still symlinks; do NOT delete ${repoHome()} yet, fix the cause and re-run \`nomad eject\` (it is idempotent on already-real names)`
1353
1367
  );
1354
1368
  }
1355
1369
  }
1356
- function cmdEject(opts = {}, roots = DEFAULT_ROOTS) {
1370
+ function defaultEjectRoots() {
1371
+ return { claudeHome: claudeHome(), repoHome: repoHome() };
1372
+ }
1373
+ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
1357
1374
  const dryRun = opts.dryRun === true;
1358
- const { claudeHome, repoHome } = roots;
1359
- const map = readMapIfPresent2(repoHome);
1375
+ const { claudeHome: claudeHome2, repoHome: repoHome2 } = roots;
1376
+ const map = readMapIfPresent2(repoHome2);
1360
1377
  const names = allSharedLinks(map);
1361
1378
  const classifications = /* @__PURE__ */ new Map();
1362
1379
  for (const name of names) {
1363
- classifications.set(name, classifyName(join6(claudeHome, name)));
1380
+ classifications.set(name, classifyName(join6(claudeHome2, name)));
1364
1381
  }
1365
1382
  const dangling = names.filter((n) => classifications.get(n) === "dangling");
1366
1383
  if (dangling.length > 0) {
@@ -1369,13 +1386,13 @@ function cmdEject(opts = {}, roots = DEFAULT_ROOTS) {
1369
1386
  );
1370
1387
  process.exit(1);
1371
1388
  }
1372
- const sharedRoot = resolveSharedRoot(repoHome);
1389
+ const sharedRoot = resolveSharedRoot(repoHome2);
1373
1390
  if (dryRun) {
1374
- previewDryRun(names, classifications, claudeHome, sharedRoot);
1391
+ previewDryRun(names, classifications, claudeHome2, sharedRoot);
1375
1392
  return;
1376
1393
  }
1377
1394
  try {
1378
- runLiveEject(names, classifications, claudeHome, sharedRoot);
1395
+ runLiveEject(names, classifications, claudeHome2, sharedRoot);
1379
1396
  } catch (err) {
1380
1397
  fail(errMessage(err));
1381
1398
  process.exit(1);
@@ -1383,8 +1400,8 @@ function cmdEject(opts = {}, roots = DEFAULT_ROOTS) {
1383
1400
  }
1384
1401
 
1385
1402
  // src/commands.doctor.ts
1386
- import { existsSync as existsSync22 } from "node:fs";
1387
- import { join as join26 } from "node:path";
1403
+ import { existsSync as existsSync23 } from "node:fs";
1404
+ import { join as join27 } from "node:path";
1388
1405
 
1389
1406
  // src/commands.doctor.checks.repo.ts
1390
1407
  init_color();
@@ -1411,8 +1428,8 @@ function sectionFailed(s) {
1411
1428
  return s.items.some((line) => line.includes(failGlyph));
1412
1429
  }
1413
1430
  function renderRawItems(items) {
1414
- for (const item of items) {
1415
- console.log(item === "" ? "" : ` ${item}`);
1431
+ for (const item2 of items) {
1432
+ console.log(item2 === "" ? "" : ` ${item2}`);
1416
1433
  }
1417
1434
  }
1418
1435
  function renderSection(s) {
@@ -1424,18 +1441,18 @@ function renderSection(s) {
1424
1441
  const header = sectionFailed(s) ? `${red(FAIL_GLYPH_BARE)} ${s.header}` : s.header;
1425
1442
  console.log(header);
1426
1443
  const lastContent = s.items.reduce(
1427
- (acc, item, j) => item === "" || isChild(item) ? acc : j,
1444
+ (acc, item2, j) => item2 === "" || isChild(item2) ? acc : j,
1428
1445
  -1
1429
1446
  );
1430
1447
  for (let j = 0; j < s.items.length; j++) {
1431
- const item = s.items[j];
1432
- if (item === "") console.log("");
1433
- else if (isChild(item)) console.log(renderChildLine(s.items, j));
1434
- else console.log(`${j === lastContent ? " \u2514 " : " \u251C "}${item}`);
1448
+ const item2 = s.items[j];
1449
+ if (item2 === "") console.log("");
1450
+ else if (isChild(item2)) console.log(renderChildLine(s.items, j));
1451
+ else console.log(`${j === lastContent ? " \u2514 " : " \u251C "}${item2}`);
1435
1452
  }
1436
1453
  }
1437
- function isChild(item) {
1438
- return item.startsWith(" ");
1454
+ function isChild(item2) {
1455
+ return item2.startsWith(" ");
1439
1456
  }
1440
1457
  function renderChildLine(items, j) {
1441
1458
  const parentContinues = items.some((it, k) => k > j && it !== "" && !isChild(it));
@@ -1470,10 +1487,10 @@ init_config();
1470
1487
  init_utils_json();
1471
1488
  import { existsSync as existsSync6 } from "node:fs";
1472
1489
  import { join as join7 } from "node:path";
1473
- function classifyRepoState(repoHome, host) {
1474
- const basePath = join7(repoHome, "shared", "settings.base.json");
1475
- const mapPath = join7(repoHome, "path-map.json");
1476
- const hostPath = join7(repoHome, "hosts", `${host}.json`);
1490
+ function classifyRepoState(repoHome2, host) {
1491
+ const basePath = join7(repoHome2, "shared", "settings.base.json");
1492
+ const mapPath = join7(repoHome2, "path-map.json");
1493
+ const hostPath = join7(repoHome2, "hosts", `${host}.json`);
1477
1494
  const hasBase = existsSync6(basePath);
1478
1495
  const hasMap = existsSync6(mapPath);
1479
1496
  const hasHost = existsSync6(hostPath);
@@ -1490,10 +1507,10 @@ function classifyRepoState(repoHome, host) {
1490
1507
  if (hasBase && mapEntryCount > 0 && hasHost) return "populated";
1491
1508
  return "partial";
1492
1509
  }
1493
- function reasonForPartial(repoHome, host) {
1494
- const basePath = join7(repoHome, "shared", "settings.base.json");
1495
- const mapPath = join7(repoHome, "path-map.json");
1496
- const hostPath = join7(repoHome, "hosts", `${host}.json`);
1510
+ function reasonForPartial(repoHome2, host) {
1511
+ const basePath = join7(repoHome2, "shared", "settings.base.json");
1512
+ const mapPath = join7(repoHome2, "path-map.json");
1513
+ const hostPath = join7(repoHome2, "hosts", `${host}.json`);
1497
1514
  if (!existsSync6(basePath)) return "- shared/settings.base.json missing";
1498
1515
  if (!existsSync6(mapPath)) return "- path-map.json missing";
1499
1516
  let mapEntryCount;
@@ -1514,28 +1531,28 @@ function isOverrideActive() {
1514
1531
  }
1515
1532
  function reportHostAndPaths(section2) {
1516
1533
  const unsetHint = process.env.NOMAD_HOST ? "" : dim(" (env unset, using hostname)");
1534
+ const repo = repoHome();
1535
+ const claude = claudeHome();
1517
1536
  addItem(section2, `${dim(infoGlyph)} NOMAD_HOST: ${cyan(HOST)}${unsetHint}`);
1518
1537
  if (isOverrideActive()) {
1519
- addItem(section2, `${dim(infoGlyph)} NOMAD_REPO: ${blue(REPO_HOME)}`);
1538
+ addItem(section2, `${dim(infoGlyph)} NOMAD_REPO: ${blue(repo)}`);
1520
1539
  }
1540
+ addItem(section2, `${existsSync7(repo) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(repo)}`);
1521
1541
  addItem(
1522
1542
  section2,
1523
- `${existsSync7(REPO_HOME) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(REPO_HOME)}`
1524
- );
1525
- addItem(
1526
- section2,
1527
- `${existsSync7(CLAUDE_HOME) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(CLAUDE_HOME)}`
1543
+ `${existsSync7(claude) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(claude)}`
1528
1544
  );
1529
1545
  }
1530
1546
  function reportRepoState(section2) {
1531
- const state = classifyRepoState(REPO_HOME, HOST);
1547
+ const repo = repoHome();
1548
+ const state = classifyRepoState(repo, HOST);
1532
1549
  const overrideLabel = isOverrideActive() ? " (NOMAD_REPO)" : "";
1533
1550
  if (state === "populated") {
1534
1551
  addItem(section2, `${green(okGlyph)} repo state: populated${overrideLabel}`);
1535
1552
  } else if (state === "partial") {
1536
1553
  addItem(
1537
1554
  section2,
1538
- `${yellow(warnGlyph)} repo state: partial ${reasonForPartial(REPO_HOME, HOST)}${overrideLabel}`
1555
+ `${yellow(warnGlyph)} repo state: partial ${reasonForPartial(repo, HOST)}${overrideLabel}`
1539
1556
  );
1540
1557
  } else {
1541
1558
  addItem(
@@ -1546,7 +1563,7 @@ function reportRepoState(section2) {
1546
1563
  }
1547
1564
  }
1548
1565
  function repoHasSharedSource(name) {
1549
- return existsSync7(join8(REPO_HOME, "shared", name));
1566
+ return existsSync7(join8(repoHome(), "shared", name));
1550
1567
  }
1551
1568
  function classifySharedLink(name, p) {
1552
1569
  let stat;
@@ -1592,8 +1609,9 @@ function classifySymlinkTarget(name, p) {
1592
1609
  }
1593
1610
  }
1594
1611
  function reportSharedLinks(section2, map) {
1612
+ const claude = claudeHome();
1595
1613
  for (const name of allSharedLinks(map)) {
1596
- const p = join8(CLAUDE_HOME, name);
1614
+ const p = join8(claude, name);
1597
1615
  const { line, fail: fail2 } = classifySharedLink(name, p);
1598
1616
  addItem(section2, line);
1599
1617
  if (fail2) process.exitCode = 1;
@@ -1606,7 +1624,7 @@ init_config();
1606
1624
  import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
1607
1625
  import { join as join9 } from "node:path";
1608
1626
  function loadBaseSettings(section2) {
1609
- const basePath = join9(REPO_HOME, "shared", "settings.base.json");
1627
+ const basePath = join9(repoHome(), "shared", "settings.base.json");
1610
1628
  if (!existsSync8(basePath)) {
1611
1629
  addItem(section2, `${red(failGlyph)} shared/settings.base.json missing at ${blue(basePath)}`);
1612
1630
  process.exitCode = 1;
@@ -1615,7 +1633,7 @@ function loadBaseSettings(section2) {
1615
1633
  return readJsonSafe(basePath, basePath, section2);
1616
1634
  }
1617
1635
  function loadAndReportSettings(section2) {
1618
- const settingsPath = join9(CLAUDE_HOME, "settings.json");
1636
+ const settingsPath = join9(claudeHome(), "settings.json");
1619
1637
  if (!existsSync8(settingsPath)) return null;
1620
1638
  const settings = readJsonSafe(settingsPath, settingsPath, section2);
1621
1639
  if (settings === null) return null;
@@ -1631,7 +1649,8 @@ function loadAndReportSettings(section2) {
1631
1649
  return settings;
1632
1650
  }
1633
1651
  function reportHostOverrides(section2, base, settings) {
1634
- const hostFile = join9(REPO_HOME, "hosts", `${HOST}.json`);
1652
+ const repo = repoHome();
1653
+ const hostFile = join9(repo, "hosts", `${HOST}.json`);
1635
1654
  let drift = [];
1636
1655
  if (base !== null && settings !== null) {
1637
1656
  const baseKeys = new Set(Object.keys(base));
@@ -1646,7 +1665,7 @@ function reportHostOverrides(section2, base, settings) {
1646
1665
  section2,
1647
1666
  `${red(failGlyph)} no hosts/${HOST}.json AND settings.json has unbased keys ${JSON.stringify(drift)}`
1648
1667
  );
1649
- const hostsDir = join9(REPO_HOME, "hosts");
1668
+ const hostsDir = join9(repo, "hosts");
1650
1669
  if (existsSync8(hostsDir)) {
1651
1670
  const cands = readdirSync2(hostsDir).filter((f) => f.endsWith(".json"));
1652
1671
  if (cands.length > 0) addItem(section2, `${dim(infoGlyph)} candidates: ${cands.join(", ")}`);
@@ -1674,7 +1693,7 @@ function reportMappedProjects(section2, map) {
1674
1693
  }
1675
1694
  }
1676
1695
  function reportUnmappedProjects(section2, map) {
1677
- const localProjects = join10(CLAUDE_HOME, "projects");
1696
+ const localProjects = join10(claudeHome(), "projects");
1678
1697
  if (!existsSync9(localProjects)) return;
1679
1698
  let localDirs;
1680
1699
  try {
@@ -1715,7 +1734,7 @@ function reportPathCollisions(section2, map) {
1715
1734
  else addItem(section2, `${green(okGlyph)} path-encoding: no collisions`);
1716
1735
  }
1717
1736
  function reportPathMap(section2) {
1718
- const mapPath = join10(REPO_HOME, "path-map.json");
1737
+ const mapPath = join10(repoHome(), "path-map.json");
1719
1738
  if (!existsSync9(mapPath)) {
1720
1739
  addItem(section2, `${red(failGlyph)} path-map.json missing at ${blue(mapPath)}`);
1721
1740
  process.exitCode = 1;
@@ -1803,11 +1822,12 @@ function reportGitleaksProbe(section2) {
1803
1822
  }
1804
1823
  }
1805
1824
  function reportGitlinks(section2) {
1806
- const sharedDir = join14(REPO_HOME, "shared");
1825
+ const repo = repoHome();
1826
+ const sharedDir = join14(repo, "shared");
1807
1827
  if (existsSync12(sharedDir)) {
1808
1828
  const gitlinks = findGitlinks(sharedDir);
1809
1829
  for (const p of gitlinks) {
1810
- const rel = relative2(REPO_HOME, p);
1830
+ const rel = relative2(repo, p);
1811
1831
  addItem(
1812
1832
  section2,
1813
1833
  `${red(failGlyph)} gitlink: ${blue(rel)} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`
@@ -1823,7 +1843,7 @@ function reportGitlinks(section2) {
1823
1843
  function reportRemote(section2) {
1824
1844
  try {
1825
1845
  const url = execFileSync3("git", ["remote", "get-url", "origin"], {
1826
- cwd: REPO_HOME,
1846
+ cwd: repoHome(),
1827
1847
  stdio: ["ignore", "pipe", "pipe"]
1828
1848
  }).toString().trim();
1829
1849
  addItem(section2, `${dim(infoGlyph)} remote origin: ${cyan(url)}`);
@@ -1833,7 +1853,7 @@ function reportRemote(section2) {
1833
1853
  }
1834
1854
  function reportRebaseClean(section2) {
1835
1855
  try {
1836
- const status = gitStatusPorcelainZ(REPO_HOME);
1856
+ const status = gitStatusPorcelainZ(repoHome());
1837
1857
  if (status.length > 0) {
1838
1858
  addItem(
1839
1859
  section2,
@@ -1845,7 +1865,7 @@ function reportRebaseClean(section2) {
1845
1865
  }
1846
1866
  function reportRebaseState(section2) {
1847
1867
  try {
1848
- const wedge = detectWedge(REPO_HOME);
1868
+ const wedge = detectWedge(repoHome());
1849
1869
  if (wedge !== null) {
1850
1870
  const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
1851
1871
  addItem(
@@ -1886,16 +1906,16 @@ function dirSizeBytes(dir) {
1886
1906
  }
1887
1907
  return bytes;
1888
1908
  }
1889
- function totalSizeMb(backupBase, dirs) {
1909
+ function totalSizeMb(backupBase2, dirs) {
1890
1910
  let bytes = 0;
1891
- for (const name of dirs) bytes += dirSizeBytes(join15(backupBase, name));
1911
+ for (const name of dirs) bytes += dirSizeBytes(join15(backupBase2, name));
1892
1912
  return bytes / BYTES_PER_MB;
1893
1913
  }
1894
- function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
1895
- if (!existsSync13(backupBase)) return;
1896
- const dirs = safeReaddir(backupBase).filter((n) => TS_SHAPE2.test(n));
1914
+ function reportBackupsCheck(section2, backupBase2 = backupBase()) {
1915
+ if (!existsSync13(backupBase2)) return;
1916
+ const dirs = safeReaddir(backupBase2).filter((n) => TS_SHAPE2.test(n));
1897
1917
  const count = dirs.length;
1898
- const sizeMb = totalSizeMb(backupBase, dirs);
1918
+ const sizeMb = totalSizeMb(backupBase2, dirs);
1899
1919
  if (count > DOCTOR_BACKUP_COUNT_WARN || sizeMb > DOCTOR_BACKUP_SIZE_WARN_MB) {
1900
1920
  addItem(
1901
1921
  section2,
@@ -1944,7 +1964,7 @@ function fetchSchemaKeys() {
1944
1964
  }
1945
1965
  }
1946
1966
  function reportCheckSchema(section2) {
1947
- const settingsPath = join16(CLAUDE_HOME, "settings.json");
1967
+ const settingsPath = join16(claudeHome(), "settings.json");
1948
1968
  if (!existsSync14(settingsPath)) {
1949
1969
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
1950
1970
  return;
@@ -1986,7 +2006,7 @@ init_config();
1986
2006
  init_push_gitleaks();
1987
2007
  function scrubPath(logical, sid, logicalToEncoded) {
1988
2008
  const encoded = logicalToEncoded.get(logical) ?? logical;
1989
- return join18(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
2009
+ return join18(claudeHome(), "projects", encoded, `${sid}.jsonl`);
1990
2010
  }
1991
2011
  function reportSessionFindings(section2, bySession) {
1992
2012
  for (const [sid, counts] of bySession) {
@@ -2095,7 +2115,7 @@ function copyDirJsonlOnly(src, dst) {
2095
2115
  if (rel.split(sep2).length > 1) return true;
2096
2116
  if (statSync4(srcPath).isDirectory()) return true;
2097
2117
  if (srcPath.endsWith(".jsonl")) return true;
2098
- log(`skip ${rel}: extension not in allowlist`);
2118
+ item(`skip ${rel}: extension not in allowlist`);
2099
2119
  return false;
2100
2120
  }
2101
2121
  });
@@ -2109,15 +2129,17 @@ function remapPull(ts, opts = {}) {
2109
2129
  let unmapped = 0;
2110
2130
  const pulled = [];
2111
2131
  const wouldPull = [];
2112
- const mapPath = join19(REPO_HOME, "path-map.json");
2113
- const repoProjects = join19(REPO_HOME, "shared", "projects");
2132
+ const repo = repoHome();
2133
+ const claude = claudeHome();
2134
+ const mapPath = join19(repo, "path-map.json");
2135
+ const repoProjects = join19(repo, "shared", "projects");
2114
2136
  if (!existsSync15(mapPath) || !existsSync15(repoProjects)) {
2115
2137
  const text = "no path-map or repo projects dir; skipping session remap";
2116
2138
  emitPreview(opts.onPreview, { kind: "note", text }, text);
2117
2139
  return { unmapped: 0, pulled, wouldPull };
2118
2140
  }
2119
2141
  const map = readJson(mapPath);
2120
- const localProjects = join19(CLAUDE_HOME, "projects");
2142
+ const localProjects = join19(claude, "projects");
2121
2143
  if (!dryRun) mkdirSync3(localProjects, { recursive: true });
2122
2144
  for (const [logical, hosts] of Object.entries(map.projects)) {
2123
2145
  assertSafeLogical(logical);
@@ -2173,14 +2195,16 @@ function remapPush(ts, opts = {}) {
2173
2195
  let unmapped = 0;
2174
2196
  const pushed = [];
2175
2197
  const wouldPush = [];
2176
- const mapPath = join19(REPO_HOME, "path-map.json");
2198
+ const repo = repoHome();
2199
+ const claude = claudeHome();
2200
+ const mapPath = join19(repo, "path-map.json");
2177
2201
  if (!existsSync15(mapPath)) {
2178
2202
  log("no path-map.json; skipping session export");
2179
2203
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
2180
2204
  }
2181
2205
  const map = readJson(mapPath);
2182
- const localProjects = join19(CLAUDE_HOME, "projects");
2183
- const repoProjects = join19(REPO_HOME, "shared", "projects");
2206
+ const localProjects = join19(claude, "projects");
2207
+ const repoProjects = join19(repo, "shared", "projects");
2184
2208
  const reverse = buildReverseMap(map);
2185
2209
  if (!existsSync15(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
2186
2210
  if (!dryRun) mkdirSync3(repoProjects, { recursive: true });
@@ -2195,7 +2219,7 @@ function remapPush(ts, opts = {}) {
2195
2219
  wouldPush.push(logical);
2196
2220
  continue;
2197
2221
  }
2198
- backupRepoWrite(repoDst, ts, REPO_HOME);
2222
+ backupRepoWrite(repoDst, ts, repo);
2199
2223
  copyDirJsonlOnly(join19(localProjects, dir), repoDst);
2200
2224
  pushed.push(logical);
2201
2225
  }
@@ -2208,7 +2232,8 @@ init_utils_json();
2208
2232
  function buildScanTree(tmpRoot) {
2209
2233
  const logicalToEncoded = /* @__PURE__ */ new Map();
2210
2234
  let staged = 0;
2211
- const mapPath = join20(REPO_HOME, "path-map.json");
2235
+ const repo = repoHome();
2236
+ const mapPath = join20(repo, "path-map.json");
2212
2237
  if (!existsSync16(mapPath)) return { logicalToEncoded, staged, malformed: false };
2213
2238
  let map;
2214
2239
  try {
@@ -2226,7 +2251,7 @@ function buildScanTree(tmpRoot) {
2226
2251
  if (!p || p === "TBD") continue;
2227
2252
  reverse.set(encodePath(p), logical);
2228
2253
  }
2229
- const localProjects = join20(CLAUDE_HOME, "projects");
2254
+ const localProjects = join20(claudeHome(), "projects");
2230
2255
  if (!existsSync16(localProjects)) return { logicalToEncoded, staged, malformed: false };
2231
2256
  for (const dir of readdirSync7(localProjects)) {
2232
2257
  const logical = reverse.get(dir);
@@ -2353,7 +2378,7 @@ function safeRead(path) {
2353
2378
  }
2354
2379
  }
2355
2380
  function reportHookScopeCheck(section2) {
2356
- const hooksDir = join21(CLAUDE_HOME, "hooks");
2381
+ const hooksDir = join21(claudeHome(), "hooks");
2357
2382
  if (!existsSync17(hooksDir)) {
2358
2383
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
2359
2384
  return;
@@ -2385,13 +2410,14 @@ import { existsSync as existsSync18 } from "node:fs";
2385
2410
  import { join as join22 } from "node:path";
2386
2411
  init_config();
2387
2412
  function expandHome(token) {
2388
- return token.replace(/^\$\{HOME\}/, HOME).replace(/^\$HOME/, HOME).replace(/^~/, HOME);
2413
+ const h2 = home();
2414
+ return token.replace(/^\$\{HOME\}/, h2).replace(/^\$HOME/, h2).replace(/^~/, h2);
2389
2415
  }
2390
2416
  function stripShellPunctuation(token) {
2391
2417
  return token.replace(/^['"]+/, "").replace(/['"`;)|&>]+$/, "");
2392
2418
  }
2393
2419
  function* claudePathsIn(command) {
2394
- const claudePrefix = `${CLAUDE_HOME}/`;
2420
+ const claudePrefix = `${claudeHome()}/`;
2395
2421
  for (const segment of command.split(/&&|\|\||;|\|/)) {
2396
2422
  for (const raw of segment.trim().split(/\s+/).filter(Boolean)) {
2397
2423
  const expanded = expandHome(stripShellPunctuation(raw));
@@ -2433,7 +2459,7 @@ function checkEventGroups(section2, event, groups) {
2433
2459
  return anyFail;
2434
2460
  }
2435
2461
  function reportHooksTargetCheck(section2) {
2436
- const settingsPath = join22(CLAUDE_HOME, "settings.json");
2462
+ const settingsPath = join22(claudeHome(), "settings.json");
2437
2463
  if (!existsSync18(settingsPath)) {
2438
2464
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
2439
2465
  return;
@@ -2535,7 +2561,8 @@ function relativeRequireTargetsBroken(scriptPath) {
2535
2561
  // src/commands.doctor.checks.hooks.preserve-symlinks.ts
2536
2562
  init_config();
2537
2563
  function expandHome2(token) {
2538
- return token.replace(/^\$\{HOME\}/, HOME).replace(/^\$HOME/, HOME).replace(/^~/, HOME);
2564
+ const h2 = home();
2565
+ return token.replace(/^\$\{HOME\}/, h2).replace(/^\$HOME/, h2).replace(/^~/, h2);
2539
2566
  }
2540
2567
  function stripShellPunct(token) {
2541
2568
  const head = token.replace(/^['"]+/, "");
@@ -2553,7 +2580,7 @@ function commandTokens(command) {
2553
2580
  return tokens;
2554
2581
  }
2555
2582
  function readPathMapSafe() {
2556
- const mapPath = join24(REPO_HOME, "path-map.json");
2583
+ const mapPath = join24(repoHome(), "path-map.json");
2557
2584
  if (!existsSync20(mapPath)) return { projects: {} };
2558
2585
  try {
2559
2586
  return JSON.parse(readFileSync6(mapPath, "utf8"));
@@ -2563,7 +2590,7 @@ function readPathMapSafe() {
2563
2590
  }
2564
2591
  function resolvesUnderSymlinkedShared(scriptPath, sharedLinkNames) {
2565
2592
  for (const name of sharedLinkNames) {
2566
- const prefix = `${CLAUDE_HOME}/${name}/`;
2593
+ const prefix = `${claudeHome()}/${name}/`;
2567
2594
  if (scriptPath.startsWith(prefix)) return true;
2568
2595
  }
2569
2596
  return false;
@@ -2627,7 +2654,7 @@ function checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames)
2627
2654
  return anyWarn;
2628
2655
  }
2629
2656
  function reportPreserveSymlinksCheck(section2) {
2630
- const settingsPath = join24(CLAUDE_HOME, "settings.json");
2657
+ const settingsPath = join24(claudeHome(), "settings.json");
2631
2658
  if (!existsSync20(settingsPath)) {
2632
2659
  addItem(
2633
2660
  section2,
@@ -2659,17 +2686,150 @@ function reportPreserveSymlinksCheck(section2) {
2659
2686
  }
2660
2687
  }
2661
2688
 
2689
+ // src/commands.doctor.checks.settings-drift.ts
2690
+ init_color();
2691
+ import { existsSync as existsSync21, readFileSync as readFileSync7 } from "node:fs";
2692
+ import { join as join25 } from "node:path";
2693
+ init_config();
2694
+ init_utils_json();
2695
+ function arraysEqual(a, b) {
2696
+ if (a.length !== b.length) return false;
2697
+ for (let i = 0; i < a.length; i++) {
2698
+ if (!deepEqual(a[i], b[i])) return false;
2699
+ }
2700
+ return true;
2701
+ }
2702
+ function objectsEqual(a, b) {
2703
+ const aKeys = Object.keys(a);
2704
+ const bKeys = Object.keys(b);
2705
+ if (aKeys.length !== bKeys.length) return false;
2706
+ for (const k of aKeys) {
2707
+ if (!Object.hasOwn(b, k)) return false;
2708
+ if (!deepEqual(a[k], b[k])) return false;
2709
+ }
2710
+ return true;
2711
+ }
2712
+ function deepEqual(a, b) {
2713
+ if (a === b) return true;
2714
+ if (a === null || b === null) return false;
2715
+ if (Array.isArray(a) && Array.isArray(b)) return arraysEqual(a, b);
2716
+ if (Array.isArray(a) || Array.isArray(b)) return false;
2717
+ if (typeof a === "object" && typeof b === "object") {
2718
+ return objectsEqual(a, b);
2719
+ }
2720
+ return false;
2721
+ }
2722
+ function diffMergedSettings(merged, settings) {
2723
+ const missing = [];
2724
+ const changed = [];
2725
+ const extra = [];
2726
+ const settingsKeys = new Set(Object.keys(settings));
2727
+ for (const key of Object.keys(merged)) {
2728
+ if (!settingsKeys.has(key)) {
2729
+ missing.push(key);
2730
+ } else if (!deepEqual(merged[key], settings[key])) {
2731
+ changed.push(key);
2732
+ }
2733
+ }
2734
+ const mergedKeys = new Set(Object.keys(merged));
2735
+ for (const key of Object.keys(settings)) {
2736
+ if (!mergedKeys.has(key)) extra.push(key);
2737
+ }
2738
+ const collator = (a, b) => a.localeCompare(b, "en");
2739
+ return {
2740
+ missing: missing.toSorted(collator),
2741
+ changed: changed.toSorted(collator),
2742
+ extra: extra.toSorted(collator)
2743
+ };
2744
+ }
2745
+ function tryReadJson(filePath) {
2746
+ try {
2747
+ const raw = readFileSync7(filePath, "utf8");
2748
+ const parsed = JSON.parse(raw);
2749
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2750
+ return parsed;
2751
+ } catch {
2752
+ return null;
2753
+ }
2754
+ }
2755
+ function reportSettingsDriftCheck(section2) {
2756
+ const claude = claudeHome();
2757
+ const repo = repoHome();
2758
+ const host = HOST;
2759
+ const settingsPath = join25(claude, "settings.json");
2760
+ const basePath = join25(repo, "shared", "settings.base.json");
2761
+ const hostPath = join25(repo, "hosts", `${host}.json`);
2762
+ if (!existsSync21(settingsPath)) {
2763
+ addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping merge-drift check`);
2764
+ return;
2765
+ }
2766
+ if (!existsSync21(basePath)) {
2767
+ addItem(
2768
+ section2,
2769
+ `${dim(infoGlyph)} shared/settings.base.json missing; skipping merge-drift check`
2770
+ );
2771
+ return;
2772
+ }
2773
+ const base = tryReadJson(basePath);
2774
+ if (base === null) {
2775
+ addItem(
2776
+ section2,
2777
+ `${dim(infoGlyph)} shared/settings.base.json unparseable; skipping merge-drift check`
2778
+ );
2779
+ return;
2780
+ }
2781
+ const settings = tryReadJson(settingsPath);
2782
+ if (settings === null) {
2783
+ return;
2784
+ }
2785
+ const hostExists = existsSync21(hostPath);
2786
+ const hostObj = hostExists ? tryReadJson(hostPath) : null;
2787
+ if (hostExists && hostObj === null) {
2788
+ addItem(
2789
+ section2,
2790
+ `${yellow(warnGlyph)} hosts/${host}.json unparseable; 'nomad pull' will fail (fix the host file)`
2791
+ );
2792
+ return;
2793
+ }
2794
+ const merged = deepMerge(base, hostObj ?? {});
2795
+ const { missing, changed, extra } = diffMergedSettings(merged, settings);
2796
+ emitDriftRows(section2, missing, changed, extra, host, hostExists);
2797
+ }
2798
+ function emitDriftRows(section2, missing, changed, extra, host, hostFileExists) {
2799
+ if (missing.length > 0) {
2800
+ addItem(
2801
+ section2,
2802
+ `${yellow(warnGlyph)} settings.json drift: merged keys missing locally: ${missing.join(", ")} (external writer clobbered settings.json; run 'nomad pull')`
2803
+ );
2804
+ }
2805
+ if (changed.length > 0) {
2806
+ addItem(
2807
+ section2,
2808
+ `${yellow(warnGlyph)} settings.json drift: merged keys with changed values: ${changed.join(", ")} (run 'nomad pull')`
2809
+ );
2810
+ }
2811
+ if (extra.length > 0 && hostFileExists) {
2812
+ addItem(
2813
+ section2,
2814
+ `${dim(infoGlyph)} settings.json has ${extra.length} local-only key(s) not in base+host merge: ${extra.join(", ")} (promotion candidates for shared/settings.base.json or hosts/${host}.json)`
2815
+ );
2816
+ }
2817
+ if (missing.length === 0 && changed.length === 0 && extra.length === 0) {
2818
+ addItem(section2, `${green(okGlyph)} settings.json matches base+host merge`);
2819
+ }
2820
+ }
2821
+
2662
2822
  // src/commands.doctor.ts
2663
2823
  init_config();
2664
2824
 
2665
2825
  // src/commands.doctor.engine.ts
2666
2826
  init_color();
2667
- import { readFileSync as readFileSync8 } from "node:fs";
2827
+ import { readFileSync as readFileSync9 } from "node:fs";
2668
2828
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2669
2829
 
2670
2830
  // src/commands.doctor.version.ts
2671
2831
  init_color();
2672
- import { readFileSync as readFileSync7 } from "node:fs";
2832
+ import { readFileSync as readFileSync8 } from "node:fs";
2673
2833
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2674
2834
  init_config();
2675
2835
  var STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
@@ -2686,7 +2846,7 @@ function compareSemver(a, b) {
2686
2846
  function readLocalVersion() {
2687
2847
  try {
2688
2848
  const pkgPath = fileURLToPath2(new URL("../package.json", import.meta.url));
2689
- const parsed = JSON.parse(readFileSync7(pkgPath, "utf8"));
2849
+ const parsed = JSON.parse(readFileSync8(pkgPath, "utf8"));
2690
2850
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
2691
2851
  return parsed.version;
2692
2852
  }
@@ -2745,7 +2905,7 @@ function parseMinVersion(spec) {
2745
2905
  function readEnginesNode() {
2746
2906
  try {
2747
2907
  const pkgPath = fileURLToPath3(new URL("../package.json", import.meta.url));
2748
- const parsed = JSON.parse(readFileSync8(pkgPath, "utf8"));
2908
+ const parsed = JSON.parse(readFileSync9(pkgPath, "utf8"));
2749
2909
  const node = parsed.engines?.node;
2750
2910
  if (typeof node === "string" && node.length > 0) return node;
2751
2911
  return null;
@@ -2774,8 +2934,8 @@ function reportNodeEngineCheck(section2) {
2774
2934
  // src/commands.doctor.gitleaks-version.ts
2775
2935
  init_color();
2776
2936
  import { execFileSync as execFileSync7 } from "node:child_process";
2777
- import { existsSync as existsSync21 } from "node:fs";
2778
- import { join as join25 } from "node:path";
2937
+ import { existsSync as existsSync22 } from "node:fs";
2938
+ import { join as join26 } from "node:path";
2779
2939
  init_config();
2780
2940
  var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
2781
2941
  var GITLEAKS_TIMEOUT_MS = 5e3;
@@ -2784,7 +2944,7 @@ function majorMinorOf(value) {
2784
2944
  return m === null ? null : [m[1], m[2]];
2785
2945
  }
2786
2946
  function readGitleaksVersion(run, tomlExists) {
2787
- const tomlPath = join25(REPO_HOME, ".gitleaks.toml");
2947
+ const tomlPath = join26(repoHome(), ".gitleaks.toml");
2788
2948
  const args = ["version"];
2789
2949
  if (tomlExists(tomlPath)) args.push("--config", tomlPath);
2790
2950
  try {
@@ -2796,7 +2956,7 @@ function readGitleaksVersion(run, tomlExists) {
2796
2956
  return null;
2797
2957
  }
2798
2958
  }
2799
- function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync21) {
2959
+ function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync22) {
2800
2960
  const raw = readGitleaksVersion(run, tomlExists);
2801
2961
  if (raw === null) return;
2802
2962
  const local = majorMinorOf(raw);
@@ -2935,7 +3095,7 @@ function readOriginRemote(cwd, run = execFileSync9) {
2935
3095
  function reportActionsDrift(section2, run = execFileSync10) {
2936
3096
  let remote;
2937
3097
  try {
2938
- remote = readOriginRemote(REPO_HOME, run);
3098
+ remote = readOriginRemote(repoHome(), run);
2939
3099
  } catch {
2940
3100
  return;
2941
3101
  }
@@ -2965,15 +3125,15 @@ function reportActionsDrift(section2, run = execFileSync10) {
2965
3125
 
2966
3126
  // src/commands.doctor.verdict.ts
2967
3127
  init_color();
2968
- function isFailLine(item) {
2969
- return item.includes(failGlyph);
3128
+ function isFailLine(item2) {
3129
+ return item2.includes(failGlyph);
2970
3130
  }
2971
- function isWarnLine(item) {
2972
- return !isFailLine(item) && item.includes(warnGlyph);
3131
+ function isWarnLine(item2) {
3132
+ return !isFailLine(item2) && item2.includes(warnGlyph);
2973
3133
  }
2974
3134
  function buildVerdictSection(sections) {
2975
3135
  const summary = section("Summary");
2976
- const lines = sections.flatMap((s) => s.items).map((item) => item.replace(/^\t/, ""));
3136
+ const lines = sections.flatMap((s) => s.items).map((item2) => item2.replace(/^\t/, ""));
2977
3137
  const failures = lines.filter(isFailLine);
2978
3138
  const warnings = lines.filter(isWarnLine);
2979
3139
  for (const line of [...failures, ...warnings]) addItem(summary, line);
@@ -2996,8 +3156,8 @@ function cmdDoctor(opts = {}) {
2996
3156
  reportHostAndPaths(host);
2997
3157
  reportRepoState(host);
2998
3158
  const links = section("Shared links");
2999
- const mapPath = join26(REPO_HOME, "path-map.json");
3000
- const rawMap = existsSync22(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
3159
+ const mapPath = join27(repoHome(), "path-map.json");
3160
+ const rawMap = existsSync23(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
3001
3161
  const map = rawMap ?? { projects: {} };
3002
3162
  reportSharedLinks(links, map);
3003
3163
  const hooksScan = section("Hook targets");
@@ -3008,6 +3168,7 @@ function cmdDoctor(opts = {}) {
3008
3168
  const base = loadBaseSettings(settings);
3009
3169
  const parsedSettings = loadAndReportSettings(settings);
3010
3170
  reportHostOverrides(settings, base, parsedSettings);
3171
+ reportSettingsDriftCheck(settings);
3011
3172
  const pathMap = section("Path map");
3012
3173
  reportPathMap(pathMap);
3013
3174
  const neverSync = section("Never-sync");
@@ -3051,16 +3212,15 @@ function cmdDoctor(opts = {}) {
3051
3212
  // src/commands.drop-session.ts
3052
3213
  init_config();
3053
3214
  import { execFileSync as execFileSync12 } from "node:child_process";
3054
- import { existsSync as existsSync24, readdirSync as readdirSync9, statSync as statSync5 } from "node:fs";
3055
- import { join as join29, relative as relative4 } from "node:path";
3215
+ import { existsSync as existsSync25, readdirSync as readdirSync9, statSync as statSync5 } from "node:fs";
3216
+ import { join as join30, relative as relative4 } from "node:path";
3056
3217
 
3057
3218
  // src/commands.drop-session.git.ts
3058
- init_config();
3059
3219
  import { execFileSync as execFileSync11 } from "node:child_process";
3060
- function expandStagedDir(dirRel) {
3220
+ function expandStagedDir(dirRel, repo) {
3061
3221
  try {
3062
3222
  const out = execFileSync11("git", ["ls-files", "-z", "--", dirRel], {
3063
- cwd: REPO_HOME,
3223
+ cwd: repo,
3064
3224
  stdio: ["ignore", "pipe", "pipe"]
3065
3225
  });
3066
3226
  return out.toString().split("\0").filter((p) => p !== "");
@@ -3068,10 +3228,10 @@ function expandStagedDir(dirRel) {
3068
3228
  return [];
3069
3229
  }
3070
3230
  }
3071
- function isTrackedInHead(rel) {
3231
+ function isTrackedInHead(rel, repo) {
3072
3232
  try {
3073
3233
  execFileSync11("git", ["cat-file", "-e", `HEAD:${rel}`], {
3074
- cwd: REPO_HOME,
3234
+ cwd: repo,
3075
3235
  stdio: ["ignore", "pipe", "pipe"]
3076
3236
  });
3077
3237
  return true;
@@ -3079,10 +3239,10 @@ function isTrackedInHead(rel) {
3079
3239
  return false;
3080
3240
  }
3081
3241
  }
3082
- function isInIndex(rel) {
3242
+ function isInIndex(rel, repo) {
3083
3243
  try {
3084
3244
  const out = execFileSync11("git", ["ls-files", "--", rel], {
3085
- cwd: REPO_HOME,
3245
+ cwd: repo,
3086
3246
  stdio: ["ignore", "pipe", "pipe"]
3087
3247
  });
3088
3248
  return out.toString().trim() !== "";
@@ -3095,8 +3255,8 @@ function isInIndex(rel) {
3095
3255
  init_config();
3096
3256
  init_utils();
3097
3257
  init_utils_json();
3098
- import { existsSync as existsSync23 } from "node:fs";
3099
- import { join as join27 } from "node:path";
3258
+ import { existsSync as existsSync24 } from "node:fs";
3259
+ import { join as join28 } from "node:path";
3100
3260
  var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
3101
3261
  function reportScrubHint(id, matches) {
3102
3262
  const live = resolveLiveTranscript(id, matches);
@@ -3112,16 +3272,17 @@ function reportScrubHint(id, matches) {
3112
3272
  }
3113
3273
  function resolveLiveTranscript(id, matches) {
3114
3274
  try {
3115
- const mapPath = join27(REPO_HOME, "path-map.json");
3116
- if (!existsSync23(mapPath)) return null;
3275
+ const mapPath = join28(repoHome(), "path-map.json");
3276
+ if (!existsSync24(mapPath)) return null;
3117
3277
  const projects = readJson(mapPath).projects;
3278
+ const claude = claudeHome();
3118
3279
  for (const rel of matches) {
3119
3280
  const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
3120
3281
  if (logical === void 0) continue;
3121
3282
  const abs = projects[logical]?.[HOST];
3122
3283
  if (abs === void 0) continue;
3123
- const live = join27(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
3124
- if (existsSync23(live)) return live;
3284
+ const live = join28(claude, "projects", encodePath(abs), `${id}.jsonl`);
3285
+ if (existsSync24(live)) return live;
3125
3286
  }
3126
3287
  return null;
3127
3288
  } catch {
@@ -3135,13 +3296,16 @@ init_utils();
3135
3296
  // src/utils.lockfile.ts
3136
3297
  init_config();
3137
3298
  init_utils();
3138
- import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
3139
- import { dirname as dirname4, join as join28 } from "node:path";
3140
- var LOCK_PATH = join28(HOME, ".cache", "claude-nomad", "nomad.lock");
3299
+ import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync10, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
3300
+ import { dirname as dirname4, join as join29 } from "node:path";
3301
+ function lockFilePath() {
3302
+ return join29(home(), ".cache", "claude-nomad", "nomad.lock");
3303
+ }
3141
3304
  function acquireLock(verb) {
3142
- mkdirSync5(dirname4(LOCK_PATH), { recursive: true });
3305
+ const lp = lockFilePath();
3306
+ mkdirSync5(dirname4(lp), { recursive: true });
3143
3307
  try {
3144
- const fd = openSync3(LOCK_PATH, "wx");
3308
+ const fd = openSync3(lp, "wx");
3145
3309
  try {
3146
3310
  writeFileSync3(fd, String(process.pid));
3147
3311
  } catch (writeErr) {
@@ -3150,55 +3314,56 @@ function acquireLock(verb) {
3150
3314
  } catch {
3151
3315
  }
3152
3316
  try {
3153
- unlinkSync(LOCK_PATH);
3317
+ unlinkSync(lp);
3154
3318
  } catch {
3155
3319
  }
3156
3320
  throw writeErr;
3157
3321
  }
3158
- return { fd };
3322
+ return { fd, path: lp };
3159
3323
  } catch (err) {
3160
3324
  const code = err.code;
3161
3325
  if (code !== "EEXIST") throw err;
3162
- return checkStaleAndRetry(verb);
3326
+ return checkStaleAndRetry(verb, lp);
3163
3327
  }
3164
3328
  }
3165
3329
  function releaseLock(handle) {
3166
3330
  if (handle === null) return;
3331
+ const lp = handle.path;
3167
3332
  try {
3168
3333
  closeSync3(handle.fd);
3169
3334
  } catch {
3170
3335
  }
3171
3336
  try {
3172
- unlinkSync(LOCK_PATH);
3337
+ unlinkSync(lp);
3173
3338
  } catch (err) {
3174
3339
  if (err.code !== "ENOENT") throw err;
3175
3340
  }
3176
3341
  }
3177
- function unlinkIfSamePid(expectedPidStr) {
3342
+ function unlinkIfSamePid(expectedPidStr, lp) {
3178
3343
  let current;
3179
3344
  try {
3180
- current = readFileSync9(LOCK_PATH, "utf8").trim();
3345
+ current = readFileSync10(lp, "utf8").trim();
3181
3346
  } catch {
3182
3347
  return false;
3183
3348
  }
3184
3349
  if (current !== expectedPidStr) return false;
3185
3350
  try {
3186
- unlinkSync(LOCK_PATH);
3351
+ unlinkSync(lp);
3187
3352
  return true;
3188
3353
  } catch {
3189
3354
  return false;
3190
3355
  }
3191
3356
  }
3192
- function checkStaleAndRetry(verb) {
3357
+ function checkStaleAndRetry(verb, lp) {
3193
3358
  let pidStr;
3194
3359
  try {
3195
- pidStr = readFileSync9(LOCK_PATH, "utf8").trim();
3360
+ pidStr = readFileSync10(lp, "utf8").trim();
3196
3361
  } catch {
3197
3362
  pidStr = "";
3198
3363
  }
3199
3364
  const pid = Number.parseInt(pidStr, 10);
3200
3365
  if (!Number.isFinite(pid) || pid <= 0) {
3201
- if (unlinkIfSamePid(pidStr)) return retryOnce(verb);
3366
+ if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
3202
3367
  warn(`another nomad ${verb} running, skipping`);
3203
3368
  return null;
3204
3369
  }
@@ -3209,7 +3374,7 @@ function checkStaleAndRetry(verb) {
3209
3374
  } catch (err) {
3210
3375
  const code = err.code;
3211
3376
  if (code === "ESRCH") {
3212
- if (unlinkIfSamePid(pidStr)) return retryOnce(verb);
3377
+ if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
3213
3378
  warn(`another nomad ${verb} running, skipping`);
3214
3379
  return null;
3215
3380
  }
@@ -3217,9 +3382,9 @@ function checkStaleAndRetry(verb) {
3217
3382
  return null;
3218
3383
  }
3219
3384
  }
3220
- function retryOnce(verb) {
3385
+ function retryOnce(verb, lp) {
3221
3386
  try {
3222
- const fd = openSync3(LOCK_PATH, "wx");
3387
+ const fd = openSync3(lp, "wx");
3223
3388
  try {
3224
3389
  writeFileSync3(fd, String(process.pid));
3225
3390
  } catch {
@@ -3228,13 +3393,13 @@ function retryOnce(verb) {
3228
3393
  } catch {
3229
3394
  }
3230
3395
  try {
3231
- unlinkSync(LOCK_PATH);
3396
+ unlinkSync(lp);
3232
3397
  } catch {
3233
3398
  }
3234
3399
  warn(`another nomad ${verb} running, skipping`);
3235
3400
  return null;
3236
3401
  }
3237
- return { fd };
3402
+ return { fd, path: lp };
3238
3403
  } catch {
3239
3404
  warn(`another nomad ${verb} running, skipping`);
3240
3405
  return null;
@@ -3247,19 +3412,20 @@ function cmdDropSession(id) {
3247
3412
  fail(`invalid session id: ${id}`);
3248
3413
  process.exit(1);
3249
3414
  }
3250
- if (!existsSync24(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3415
+ const repo = repoHome();
3416
+ if (!existsSync25(repo)) die(`repo not cloned at ${repo}`);
3251
3417
  const handle = acquireLock("drop-session");
3252
3418
  if (handle === null) process.exit(0);
3253
3419
  try {
3254
- const repoProjects = join29(REPO_HOME, "shared", "projects");
3255
- if (!existsSync24(repoProjects)) {
3420
+ const repoProjects = join30(repo, "shared", "projects");
3421
+ if (!existsSync25(repoProjects)) {
3256
3422
  throw new NomadFatal(`no staged session matches ${id}`);
3257
3423
  }
3258
- const matches = collectMatches(repoProjects, id);
3424
+ const matches = collectMatches(repoProjects, id, repo);
3259
3425
  if (matches.length === 0) {
3260
3426
  throw new NomadFatal(`no staged session matches ${id}`);
3261
3427
  }
3262
- for (const rel of matches) unstageOne(rel);
3428
+ for (const rel of matches) unstageOne(rel, repo);
3263
3429
  reportScrubHint(id, matches);
3264
3430
  } catch (err) {
3265
3431
  if (!(err instanceof NomadFatal)) {
@@ -3271,37 +3437,37 @@ function cmdDropSession(id) {
3271
3437
  releaseLock(handle);
3272
3438
  }
3273
3439
  }
3274
- function collectMatches(repoProjects, id) {
3440
+ function collectMatches(repoProjects, id, repo) {
3275
3441
  const matches = [];
3276
3442
  for (const logical of readdirSync9(repoProjects)) {
3277
- const candidate = join29(repoProjects, logical, `${id}.jsonl`);
3278
- if (existsSync24(candidate)) {
3279
- matches.push(relative4(REPO_HOME, candidate));
3280
- }
3281
- const dir = join29(repoProjects, logical, id);
3282
- if (existsSync24(dir) && statSync5(dir).isDirectory()) {
3283
- const dirRel = relative4(REPO_HOME, dir);
3284
- const staged = expandStagedDir(dirRel);
3443
+ const candidate = join30(repoProjects, logical, `${id}.jsonl`);
3444
+ if (existsSync25(candidate)) {
3445
+ matches.push(relative4(repo, candidate));
3446
+ }
3447
+ const dir = join30(repoProjects, logical, id);
3448
+ if (existsSync25(dir) && statSync5(dir).isDirectory()) {
3449
+ const dirRel = relative4(repo, dir);
3450
+ const staged = expandStagedDir(dirRel, repo);
3285
3451
  if (staged.length > 0) matches.push(...staged);
3286
3452
  else matches.push(dirRel);
3287
3453
  }
3288
3454
  }
3289
3455
  return matches;
3290
3456
  }
3291
- function unstageOne(rel) {
3292
- if (!isInIndex(rel)) {
3293
- log(`dropped ${rel} (already absent from index)`);
3457
+ function unstageOne(rel, repo) {
3458
+ if (!isInIndex(rel, repo)) {
3459
+ item(`dropped ${rel} (already absent from index)`);
3294
3460
  return;
3295
3461
  }
3296
3462
  try {
3297
- if (isTrackedInHead(rel)) {
3463
+ if (isTrackedInHead(rel, repo)) {
3298
3464
  execFileSync12("git", ["restore", "--staged", "--worktree", "--", rel], {
3299
- cwd: REPO_HOME,
3465
+ cwd: repo,
3300
3466
  stdio: ["ignore", "pipe", "pipe"]
3301
3467
  });
3302
3468
  } else {
3303
3469
  execFileSync12("git", ["rm", "--cached", "-f", "--", rel], {
3304
- cwd: REPO_HOME,
3470
+ cwd: repo,
3305
3471
  stdio: ["ignore", "pipe", "pipe"]
3306
3472
  });
3307
3473
  }
@@ -3310,24 +3476,24 @@ function unstageOne(rel) {
3310
3476
  const detail = e.stderr?.toString().trim() ?? e.message;
3311
3477
  throw new NomadFatal(`git failed to unstage ${rel}: ${detail}`);
3312
3478
  }
3313
- log(`dropped ${rel}`);
3479
+ item(`dropped ${rel}`);
3314
3480
  }
3315
3481
 
3316
3482
  // src/commands.redact.ts
3317
3483
  init_config();
3318
- import { existsSync as existsSync26, statSync as statSync7 } from "node:fs";
3319
- import { dirname as dirname5, join as join31 } from "node:path";
3484
+ import { existsSync as existsSync27, statSync as statSync7 } from "node:fs";
3485
+ import { dirname as dirname5, join as join32 } from "node:path";
3320
3486
 
3321
3487
  // src/commands.redact.subtree.ts
3322
- import { existsSync as existsSync25, lstatSync as lstatSync7, readFileSync as readFileSync10, readdirSync as readdirSync10, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
3323
- import { join as join30 } from "node:path";
3488
+ import { existsSync as existsSync26, lstatSync as lstatSync7, readFileSync as readFileSync11, readdirSync as readdirSync10, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
3489
+ import { join as join31 } from "node:path";
3324
3490
  init_utils_fs();
3325
3491
  function collectFiles(dir, out) {
3326
- if (!existsSync25(dir)) return;
3492
+ if (!existsSync26(dir)) return;
3327
3493
  const st = lstatSync7(dir);
3328
3494
  if (!st.isDirectory()) return;
3329
3495
  for (const entry of readdirSync10(dir)) {
3330
- const abs = join30(dir, entry);
3496
+ const abs = join31(dir, entry);
3331
3497
  const lst = lstatSync7(abs);
3332
3498
  if (lst.isSymbolicLink()) continue;
3333
3499
  if (lst.isDirectory()) {
@@ -3364,7 +3530,7 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
3364
3530
  if (!dryRun && total > 0) {
3365
3531
  for (const { path: filePath, findings } of dirty) {
3366
3532
  backupBeforeWrite(filePath, ts);
3367
- writeFileSync4(filePath, applyRedactions(readFileSync10(filePath, "utf8"), findings), "utf8");
3533
+ writeFileSync4(filePath, applyRedactions(readFileSync11(filePath, "utf8"), findings), "utf8");
3368
3534
  }
3369
3535
  }
3370
3536
  return { total, dirty };
@@ -3377,14 +3543,15 @@ init_utils_json();
3377
3543
  init_utils();
3378
3544
  function resolveLiveTranscript2(id) {
3379
3545
  try {
3380
- const mapPath = join31(REPO_HOME, "path-map.json");
3381
- if (!existsSync26(mapPath)) return null;
3546
+ const mapPath = join32(repoHome(), "path-map.json");
3547
+ if (!existsSync27(mapPath)) return null;
3382
3548
  const projects = readJson(mapPath).projects;
3549
+ const claude = claudeHome();
3383
3550
  for (const hostMap of Object.values(projects)) {
3384
3551
  const abs = hostMap[HOST];
3385
3552
  if (abs === void 0) continue;
3386
- const live = join31(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
3387
- if (existsSync26(live)) return live;
3553
+ const live = join32(claude, "projects", encodePath(abs), `${id}.jsonl`);
3554
+ if (existsSync27(live)) return live;
3388
3555
  }
3389
3556
  return null;
3390
3557
  } catch {
@@ -3402,17 +3569,19 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
3402
3569
  fail(`invalid session id: ${id}`);
3403
3570
  process.exit(1);
3404
3571
  }
3405
- if (!existsSync26(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
3572
+ const repo = repoHome();
3573
+ const backup = backupBase();
3574
+ if (!existsSync27(repo)) die(`repo not cloned at ${repo}`);
3406
3575
  const handle = acquireLock("redact");
3407
3576
  if (handle === null) process.exit(0);
3408
3577
  try {
3409
3578
  const localPath = resolveLiveTranscript2(id);
3410
- if (localPath === null || !existsSync26(localPath)) {
3579
+ if (localPath === null || !existsSync27(localPath)) {
3411
3580
  fail(`could not resolve local transcript for session ${id} on this host`);
3412
3581
  process.exitCode = 1;
3413
3582
  return;
3414
3583
  }
3415
- const sessionDir = join31(dirname5(localPath), id);
3584
+ const sessionDir = join32(dirname5(localPath), id);
3416
3585
  const subtreeFiles = listSubtreeFiles(sessionDir);
3417
3586
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
3418
3587
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -3431,7 +3600,7 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
3431
3600
  process.exitCode = 1;
3432
3601
  return;
3433
3602
  }
3434
- const ts = freshBackupTs(BACKUP_BASE);
3603
+ const ts = freshBackupTs(backup);
3435
3604
  const { total: totalCount, dirty } = applySubtreeRedactions(
3436
3605
  localPath,
3437
3606
  mainFindings,
@@ -3465,8 +3634,8 @@ ${lines}`);
3465
3634
  }
3466
3635
 
3467
3636
  // src/commands.pull.ts
3468
- import { existsSync as existsSync34, mkdirSync as mkdirSync8 } from "node:fs";
3469
- import { join as join40 } from "node:path";
3637
+ import { existsSync as existsSync35, mkdirSync as mkdirSync8 } from "node:fs";
3638
+ import { join as join41 } from "node:path";
3470
3639
 
3471
3640
  // src/commands.push.sections.ts
3472
3641
  init_color();
@@ -3554,8 +3723,8 @@ init_config();
3554
3723
 
3555
3724
  // src/extras-sync.ts
3556
3725
  init_config();
3557
- import { existsSync as existsSync29 } from "node:fs";
3558
- import { join as join34 } from "node:path";
3726
+ import { existsSync as existsSync30 } from "node:fs";
3727
+ import { join as join35 } from "node:path";
3559
3728
 
3560
3729
  // src/extras-sync.diff.ts
3561
3730
  init_utils();
@@ -3594,8 +3763,8 @@ function listDivergingFiles(a, b) {
3594
3763
 
3595
3764
  // src/extras-sync.core.ts
3596
3765
  init_config();
3597
- import { cpSync as cpSync5, existsSync as existsSync27, rmSync as rmSync8 } from "node:fs";
3598
- import { join as join32 } from "node:path";
3766
+ import { cpSync as cpSync5, existsSync as existsSync28, rmSync as rmSync8 } from "node:fs";
3767
+ import { join as join33 } from "node:path";
3599
3768
 
3600
3769
  // src/extras-sync.guards.ts
3601
3770
  init_utils();
@@ -3618,9 +3787,10 @@ function assertSafeLocalRoot(localRoot, logical) {
3618
3787
  init_utils();
3619
3788
  init_utils_json();
3620
3789
  function loadValidatedExtras(opts) {
3621
- const mapPath = join32(REPO_HOME, "path-map.json");
3622
- const repoExtras = join32(REPO_HOME, "shared", "extras");
3623
- if (!existsSync27(mapPath) || opts.requireRepoExtras === true && !existsSync27(repoExtras)) {
3790
+ const repo = repoHome();
3791
+ const mapPath = join33(repo, "path-map.json");
3792
+ const repoExtras = join33(repo, "shared", "extras");
3793
+ if (!existsSync28(mapPath) || opts.requireRepoExtras === true && !existsSync28(repoExtras)) {
3624
3794
  if (opts.missingMsg !== void 0) log(opts.missingMsg);
3625
3795
  return null;
3626
3796
  }
@@ -3662,8 +3832,8 @@ init_utils_json();
3662
3832
 
3663
3833
  // src/extras-sync.remap.ts
3664
3834
  init_config();
3665
- import { existsSync as existsSync28, mkdirSync as mkdirSync6 } from "node:fs";
3666
- import { join as join33 } from "node:path";
3835
+ import { existsSync as existsSync29, mkdirSync as mkdirSync6 } from "node:fs";
3836
+ import { join as join34 } from "node:path";
3667
3837
  init_utils_fs();
3668
3838
  function runExtrasOp(v, dryRun, paths, backup) {
3669
3839
  const counts = { unmapped: 0, skipped: 0 };
@@ -3671,15 +3841,15 @@ function runExtrasOp(v, dryRun, paths, backup) {
3671
3841
  const would = [];
3672
3842
  for (const t of eachExtrasTarget(v, counts)) {
3673
3843
  const { src, dst } = paths(t);
3674
- if (!existsSync28(src)) continue;
3675
- const item = `${t.logical}/${t.dirname}`;
3844
+ if (!existsSync29(src)) continue;
3845
+ const item2 = `${t.logical}/${t.dirname}`;
3676
3846
  if (dryRun) {
3677
- would.push(item);
3847
+ would.push(item2);
3678
3848
  continue;
3679
3849
  }
3680
3850
  backup(dst, t.localRoot);
3681
3851
  copyExtras(src, dst);
3682
- done.push(item);
3852
+ done.push(item2);
3683
3853
  }
3684
3854
  return { ...counts, done, would };
3685
3855
  }
@@ -3687,16 +3857,17 @@ function remapExtrasPush(ts, opts = {}) {
3687
3857
  const dryRun = opts.dryRun === true;
3688
3858
  const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
3689
3859
  if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
3690
- const repoExtras = join33(REPO_HOME, "shared", "extras");
3860
+ const repo = repoHome();
3861
+ const repoExtras = join34(repo, "shared", "extras");
3691
3862
  if (!dryRun) mkdirSync6(repoExtras, { recursive: true });
3692
3863
  const { unmapped, skipped, done, would } = runExtrasOp(
3693
3864
  v,
3694
3865
  dryRun,
3695
3866
  ({ localRoot, logical, dirname: dirname7 }) => ({
3696
- src: join33(localRoot, dirname7),
3697
- dst: join33(repoExtras, logical, dirname7)
3867
+ src: join34(localRoot, dirname7),
3868
+ dst: join34(repoExtras, logical, dirname7)
3698
3869
  }),
3699
- (dst) => backupRepoWrite(dst, ts, REPO_HOME)
3870
+ (dst) => backupRepoWrite(dst, ts, repo)
3700
3871
  );
3701
3872
  return { unmapped, skipped, pushed: done, wouldPush: would };
3702
3873
  }
@@ -3707,13 +3878,13 @@ function remapExtrasPull(ts, opts = {}) {
3707
3878
  missingMsg: "no path-map or repo extras dir; skipping extras remap"
3708
3879
  });
3709
3880
  if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
3710
- const repoExtras = join33(REPO_HOME, "shared", "extras");
3881
+ const repoExtras = join34(repoHome(), "shared", "extras");
3711
3882
  const { unmapped, skipped, done, would } = runExtrasOp(
3712
3883
  v,
3713
3884
  dryRun,
3714
3885
  ({ localRoot, logical, dirname: dirname7 }) => ({
3715
- src: join33(repoExtras, logical, dirname7),
3716
- dst: join33(localRoot, dirname7)
3886
+ src: join34(repoExtras, logical, dirname7),
3887
+ dst: join34(localRoot, dirname7)
3717
3888
  }),
3718
3889
  // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
3719
3890
  // localRoot so the backup tree mirrors the project layout.
@@ -3727,14 +3898,15 @@ function divergenceCheckExtras(ts) {
3727
3898
  const v = loadValidatedExtras({});
3728
3899
  if (v === null) return;
3729
3900
  const counts = { unmapped: 0, skipped: 0 };
3730
- const backupRoot = join34(BACKUP_BASE, ts, "extras");
3901
+ const backupRoot = join35(backupBase(), ts, "extras");
3902
+ const repo = repoHome();
3731
3903
  for (const { logical, localRoot, dirname: dirname7 } of eachExtrasTarget(v, counts)) {
3732
- const local = join34(localRoot, dirname7);
3733
- const repo = join34(REPO_HOME, "shared", "extras", logical, dirname7);
3734
- if (!existsSync29(local) || !existsSync29(repo)) continue;
3735
- const diff = listDivergingFiles(local, repo);
3904
+ const local = join35(localRoot, dirname7);
3905
+ const repoEntry = join35(repo, "shared", "extras", logical, dirname7);
3906
+ if (!existsSync30(local) || !existsSync30(repoEntry)) continue;
3907
+ const diff = listDivergingFiles(local, repoEntry);
3736
3908
  if (diff.length === 0) continue;
3737
- const projectBackupRoot = join34(backupRoot, encodePath(localRoot));
3909
+ const projectBackupRoot = join35(backupRoot, encodePath(localRoot));
3738
3910
  warn(
3739
3911
  `local ${dirname7} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
3740
3912
  );
@@ -3747,8 +3919,8 @@ init_config();
3747
3919
  init_utils();
3748
3920
  init_utils_fs();
3749
3921
  init_utils_json();
3750
- import { existsSync as existsSync30, lstatSync as lstatSync8, rmSync as rmSync9 } from "node:fs";
3751
- import { join as join35 } from "node:path";
3922
+ import { existsSync as existsSync31, lstatSync as lstatSync8, rmSync as rmSync9 } from "node:fs";
3923
+ import { join as join36 } from "node:path";
3752
3924
  function emitAutoMove(onPreview, linkPath, ts, name) {
3753
3925
  if (onPreview) {
3754
3926
  onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
@@ -3765,13 +3937,15 @@ function emitCreate(onPreview, from, to) {
3765
3937
  }
3766
3938
  function applySharedLinks(ts, map, opts = {}) {
3767
3939
  const dryRun = opts.dryRun === true;
3940
+ const claude = claudeHome();
3941
+ const repo = repoHome();
3768
3942
  const linkNames = allSharedLinks(map);
3769
3943
  for (const name of linkNames) {
3770
- const linkPath = join35(CLAUDE_HOME, name);
3771
- const target = join35(REPO_HOME, "shared", name);
3772
- if (!existsSync30(linkPath)) continue;
3944
+ const linkPath = join36(claude, name);
3945
+ const target = join36(repo, "shared", name);
3946
+ if (!existsSync31(linkPath)) continue;
3773
3947
  if (lstatSync8(linkPath).isSymbolicLink()) continue;
3774
- if (!existsSync30(target)) continue;
3948
+ if (!existsSync31(target)) continue;
3775
3949
  if (dryRun) {
3776
3950
  emitAutoMove(opts.onPreview, linkPath, ts, name);
3777
3951
  continue;
@@ -3780,28 +3954,30 @@ function applySharedLinks(ts, map, opts = {}) {
3780
3954
  rmSync9(linkPath, { recursive: true, force: true });
3781
3955
  }
3782
3956
  for (const name of linkNames) {
3783
- const target = join35(REPO_HOME, "shared", name);
3784
- if (!existsSync30(target)) continue;
3957
+ const target = join36(repo, "shared", name);
3958
+ if (!existsSync31(target)) continue;
3785
3959
  if (dryRun) {
3786
- emitCreate(opts.onPreview, join35(CLAUDE_HOME, name), target);
3960
+ emitCreate(opts.onPreview, join36(claude, name), target);
3787
3961
  continue;
3788
3962
  }
3789
- ensureSymlink(join35(CLAUDE_HOME, name), target);
3963
+ ensureSymlink(join36(claude, name), target);
3790
3964
  }
3791
3965
  }
3792
3966
  function regenerateSettings(ts, opts = {}) {
3793
3967
  const dryRun = opts.dryRun === true;
3794
- const basePath = join35(REPO_HOME, "shared", "settings.base.json");
3795
- const hostPath = join35(REPO_HOME, "hosts", `${HOST}.json`);
3796
- if (!existsSync30(basePath)) {
3968
+ const repo = repoHome();
3969
+ const claude = claudeHome();
3970
+ const basePath = join36(repo, "shared", "settings.base.json");
3971
+ const hostPath = join36(repo, "hosts", `${HOST}.json`);
3972
+ if (!existsSync31(basePath)) {
3797
3973
  die("repo not initialized; run 'nomad init' to scaffold");
3798
3974
  }
3799
3975
  const base = readJson(basePath);
3800
- const hasOverrides = existsSync30(hostPath);
3976
+ const hasOverrides = existsSync31(hostPath);
3801
3977
  const overrides = hasOverrides ? readJson(hostPath) : {};
3802
3978
  const merged = deepMerge(base, overrides);
3803
- const settingsPath = join35(CLAUDE_HOME, "settings.json");
3804
- if (!hasOverrides && existsSync30(settingsPath)) {
3979
+ const settingsPath = join36(claude, "settings.json");
3980
+ if (!hasOverrides && existsSync31(settingsPath)) {
3805
3981
  try {
3806
3982
  const existing = readJson(settingsPath);
3807
3983
  const baseKeys = new Set(Object.keys(base));
@@ -3827,8 +4003,8 @@ function regenerateSettings(ts, opts = {}) {
3827
4003
 
3828
4004
  // src/preview.ts
3829
4005
  init_config();
3830
- import { existsSync as existsSync31 } from "node:fs";
3831
- import { join as join36 } from "node:path";
4006
+ import { existsSync as existsSync32 } from "node:fs";
4007
+ import { join as join37 } from "node:path";
3832
4008
 
3833
4009
  // node_modules/diff/libesm/diff/base.js
3834
4010
  var Diff = class {
@@ -4114,7 +4290,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
4114
4290
  return lines.join("\n");
4115
4291
  }
4116
4292
  function readJsonOrNull(path) {
4117
- if (!existsSync31(path)) return null;
4293
+ if (!existsSync32(path)) return null;
4118
4294
  try {
4119
4295
  return readJson(path);
4120
4296
  } catch {
@@ -4128,12 +4304,12 @@ function previewSettings(basePath, hostPath, settingsPath) {
4128
4304
  }
4129
4305
  const notes = [];
4130
4306
  const hostOverrides = readJsonOrNull(hostPath);
4131
- if (hostOverrides === null && existsSync31(hostPath)) {
4307
+ if (hostOverrides === null && existsSync32(hostPath)) {
4132
4308
  notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
4133
4309
  }
4134
4310
  const merged = deepMerge(base, hostOverrides ?? {});
4135
4311
  const current = readJsonOrNull(settingsPath);
4136
- if (current === null && existsSync31(settingsPath)) {
4312
+ if (current === null && existsSync32(settingsPath)) {
4137
4313
  return { diff: "", notes: [...notes, "malformed; skipping diff"] };
4138
4314
  }
4139
4315
  const rawEqual = JSON.stringify(current ?? {}, null, 2) === JSON.stringify(merged, null, 2);
@@ -4163,6 +4339,8 @@ function buildSettingsSectionForPreview(result) {
4163
4339
  return s;
4164
4340
  }
4165
4341
  function computePreview(ts, map, verb = "pull") {
4342
+ const repo = repoHome();
4343
+ const claude = claudeHome();
4166
4344
  console.log(`would pull on host=${HOST} (dry-run; no mutation)`);
4167
4345
  console.log("");
4168
4346
  const links = section("Symlinks");
@@ -4171,9 +4349,9 @@ function computePreview(ts, map, verb = "pull") {
4171
4349
  onPreview: (e) => addItem(links, formatLinkRow(e))
4172
4350
  });
4173
4351
  const settingsResult = previewSettings(
4174
- join36(REPO_HOME, "shared", "settings.base.json"),
4175
- join36(REPO_HOME, "hosts", `${HOST}.json`),
4176
- join36(CLAUDE_HOME, "settings.json")
4352
+ join37(repo, "shared", "settings.base.json"),
4353
+ join37(repo, "hosts", `${HOST}.json`),
4354
+ join37(claude, "settings.json")
4177
4355
  );
4178
4356
  const settingsSection = buildSettingsSectionForPreview(settingsResult);
4179
4357
  const sessions = section("Sessions");
@@ -4189,21 +4367,21 @@ function computePreview(ts, map, verb = "pull") {
4189
4367
 
4190
4368
  // src/spinner.ts
4191
4369
  init_color();
4192
- import { existsSync as existsSync33 } from "node:fs";
4370
+ import { existsSync as existsSync34 } from "node:fs";
4193
4371
  import { fileURLToPath as fileURLToPath4 } from "node:url";
4194
4372
  import { Worker } from "node:worker_threads";
4195
4373
 
4196
4374
  // src/commands.push.recovery.ts
4197
4375
  init_config();
4198
- import { readFileSync as readFileSync11, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
4199
- import { join as join39 } from "node:path";
4376
+ import { readFileSync as readFileSync12, rmSync as rmSync11, writeFileSync as writeFileSync5 } from "node:fs";
4377
+ import { join as join40 } from "node:path";
4200
4378
  import { createInterface } from "node:readline/promises";
4201
4379
 
4202
4380
  // src/commands.push.recovery.redact.ts
4203
4381
  init_config();
4204
4382
  init_config_sharedDirs_guard();
4205
- import { cpSync as cpSync6, existsSync as existsSync32, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
4206
- import { dirname as dirname6, join as join37, sep as sep3 } from "node:path";
4383
+ import { cpSync as cpSync6, existsSync as existsSync33, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
4384
+ import { dirname as dirname6, join as join38, sep as sep3 } from "node:path";
4207
4385
  init_push_gitleaks_scan();
4208
4386
  init_utils_json();
4209
4387
  init_utils();
@@ -4229,13 +4407,13 @@ function parseAction(raw) {
4229
4407
  }
4230
4408
 
4231
4409
  // src/commands.push.recovery.redact.ts
4232
- function resolveStagedDir(localPath, map) {
4410
+ function resolveStagedDir(localPath, map, claude, repo) {
4233
4411
  for (const [logical, hostMap] of Object.entries(map.projects)) {
4234
4412
  assertSafeLogical(logical);
4235
4413
  const abs = hostMap[HOST];
4236
4414
  if (abs === void 0) continue;
4237
- if (localPath.startsWith(join37(CLAUDE_HOME, "projects", encodePath(abs)) + sep3)) {
4238
- return join37(REPO_HOME, "shared", "projects", logical);
4415
+ if (localPath.startsWith(join38(claude, "projects", encodePath(abs)) + sep3)) {
4416
+ return join38(repo, "shared", "projects", logical);
4239
4417
  }
4240
4418
  }
4241
4419
  return null;
@@ -4245,6 +4423,8 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4245
4423
  log(msg);
4246
4424
  return false;
4247
4425
  };
4426
+ const claude = claudeHome();
4427
+ const repo = repoHome();
4248
4428
  const sid = sessionIdFromFinding(f);
4249
4429
  if (sid === null) {
4250
4430
  return refuse(
@@ -4257,7 +4437,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4257
4437
  `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
4258
4438
  );
4259
4439
  }
4260
- const sessionDir = join37(dirname6(localPath), sid);
4440
+ const sessionDir = join38(dirname6(localPath), sid);
4261
4441
  const subtreeFiles = listSubtreeFiles(sessionDir);
4262
4442
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
4263
4443
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -4266,7 +4446,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4266
4446
  End the session and choose Redact again, or choose Drop session (holds this session back from the push, local copy kept) or Skip.`
4267
4447
  );
4268
4448
  }
4269
- const stagedProjectDir = resolveStagedDir(localPath, map);
4449
+ const stagedProjectDir = resolveStagedDir(localPath, map, claude, repo);
4270
4450
  if (stagedProjectDir === null) {
4271
4451
  return refuse(
4272
4452
  `could not map the local transcript for session ${sid} to a staged copy; choose Drop session or Skip.`
@@ -4291,9 +4471,9 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4291
4471
  );
4292
4472
  }
4293
4473
  mkdirSync7(stagedProjectDir, { recursive: true });
4294
- cpSync6(localPath, join37(stagedProjectDir, `${sid}.jsonl`), { force: true });
4295
- if (existsSync32(sessionDir)) {
4296
- cpSync6(sessionDir, join37(stagedProjectDir, sid), { force: true, recursive: true });
4474
+ cpSync6(localPath, join38(stagedProjectDir, `${sid}.jsonl`), { force: true });
4475
+ if (existsSync33(sessionDir)) {
4476
+ cpSync6(sessionDir, join38(stagedProjectDir, sid), { force: true, recursive: true });
4297
4477
  }
4298
4478
  return true;
4299
4479
  }
@@ -4301,13 +4481,14 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
4301
4481
  // src/commands.push.recovery.drop.ts
4302
4482
  init_config();
4303
4483
  import { rmSync as rmSync10 } from "node:fs";
4304
- import { join as join38 } from "node:path";
4484
+ import { join as join39 } from "node:path";
4305
4485
  function dropSessionFromStaged(sid, map) {
4306
4486
  const logicals = Object.keys(map.projects);
4307
4487
  if (logicals.length === 0) return false;
4488
+ const repo = repoHome();
4308
4489
  for (const logical of logicals) {
4309
- const jsonl = join38(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
4310
- const dir = join38(REPO_HOME, "shared", "projects", logical, sid);
4490
+ const jsonl = join39(repo, "shared", "projects", logical, `${sid}.jsonl`);
4491
+ const dir = join39(repo, "shared", "projects", logical, sid);
4311
4492
  rmSync10(jsonl, { force: true });
4312
4493
  rmSync10(dir, { recursive: true, force: true });
4313
4494
  }
@@ -4317,19 +4498,19 @@ function dropSessionFromStaged(sid, map) {
4317
4498
  // src/commands.push.recovery.actions.ts
4318
4499
  init_push_gitleaks_scan();
4319
4500
  init_utils();
4320
- function applyAllow(f) {
4321
- appendGitleaksIgnore(f.Fingerprint);
4501
+ function applyAllow(f, repo) {
4502
+ appendGitleaksIgnore(f.Fingerprint, repo);
4322
4503
  }
4323
- function allowAllFindings(findings) {
4504
+ function allowAllFindings(findings, repo) {
4324
4505
  for (const f of findings) {
4325
- appendGitleaksIgnore(f.Fingerprint);
4506
+ appendGitleaksIgnore(f.Fingerprint, repo);
4326
4507
  }
4327
4508
  }
4328
- function allowFindingsByRule(findings, ruleId) {
4509
+ function allowFindingsByRule(findings, ruleId, repo) {
4329
4510
  let count = 0;
4330
4511
  for (const f of findings) {
4331
4512
  if (f.RuleID === ruleId) {
4332
- appendGitleaksIgnore(f.Fingerprint);
4513
+ appendGitleaksIgnore(f.Fingerprint, repo);
4333
4514
  count++;
4334
4515
  }
4335
4516
  }
@@ -4351,7 +4532,7 @@ function dispatchOne(f, ctx) {
4351
4532
  const sid = sessionIdFromFinding(f);
4352
4533
  if (sid !== null && ctx.droppedSids.has(sid)) return;
4353
4534
  if (action === "allow") {
4354
- applyAllow(f);
4535
+ applyAllow(f, ctx.repo);
4355
4536
  return;
4356
4537
  }
4357
4538
  if (sid === null) return;
@@ -4368,12 +4549,14 @@ function dispatchOne(f, ctx) {
4368
4549
  if (applyRedact(f, ctx.ts, ctx.map, ctx.nowMs, ctx.scan)) ctx.redactedSids.add(sid);
4369
4550
  }
4370
4551
  }
4371
- function dispatchActions(findings, actions, ts, map, nowMs, scan = scanFile, drop = dropSessionFromStaged) {
4552
+ function dispatchActions(findings, actions, opts) {
4553
+ const { ts, map, nowMs, repo, scan = scanFile, drop = dropSessionFromStaged } = opts;
4372
4554
  const ctx = {
4373
4555
  actions,
4374
4556
  ts,
4375
4557
  map,
4376
4558
  nowMs,
4559
+ repo,
4377
4560
  scan,
4378
4561
  drop,
4379
4562
  redactedSids: /* @__PURE__ */ new Set(),
@@ -4415,26 +4598,26 @@ function printRecoveryLegend(print = console.log) {
4415
4598
  print(" Skip - leave unresolved (the push aborts)");
4416
4599
  print("");
4417
4600
  }
4418
- function applyThenRescan(scanVerdict, repoHome) {
4419
- gitOrFatal(["add", "-A"], "git add", repoHome);
4420
- const next = scanVerdict();
4601
+ function applyThenRescan(scanVerdict, repoHome2) {
4602
+ gitOrFatal(["add", "-A"], "git add", repoHome2);
4603
+ const next = scanVerdict(repoHome2);
4421
4604
  if (next.leak) {
4422
4605
  const { bySession, other } = partitionFindings(next.findings);
4423
4606
  throw new NomadFatal(buildSessionAwareFatal(bySession, other));
4424
4607
  }
4425
4608
  return next;
4426
4609
  }
4427
- function allowThenRescan(append, scanVerdict, repoHome) {
4428
- const ignPath = join39(repoHome, ".gitleaksignore");
4610
+ function allowThenRescan(append, scanVerdict, repoHome2) {
4611
+ const ignPath = join40(repoHome2, ".gitleaksignore");
4429
4612
  let before;
4430
4613
  try {
4431
- before = readFileSync11(ignPath, "utf8");
4614
+ before = readFileSync12(ignPath, "utf8");
4432
4615
  } catch {
4433
4616
  before = null;
4434
4617
  }
4435
4618
  append();
4436
4619
  try {
4437
- return applyThenRescan(scanVerdict, repoHome);
4620
+ return applyThenRescan(scanVerdict, repoHome2);
4438
4621
  } catch (err) {
4439
4622
  if (before === null) rmSync11(ignPath, { force: true });
4440
4623
  else writeFileSync5(ignPath, before, "utf8");
@@ -4467,22 +4650,23 @@ async function resolveLeakFindings(verdict, ts, map, deps = {}) {
4467
4650
  printLegend = printRecoveryLegend
4468
4651
  } = deps;
4469
4652
  const scanVerdict = deps.scanVerdict ?? (await Promise.resolve().then(() => (init_push_leak_verdict(), push_leak_verdict_exports))).scanPushVerdict;
4653
+ const repo = repoHome();
4470
4654
  let current = verdict;
4471
4655
  if (redactAll) {
4472
4656
  redactAllFindings(current.findings, ts, map, nowMs, scan);
4473
- return applyThenRescan(scanVerdict, REPO_HOME);
4657
+ return applyThenRescan(scanVerdict, repo);
4474
4658
  }
4475
4659
  if (allowAll) {
4476
- return allowThenRescan(() => allowAllFindings(current.findings), scanVerdict, REPO_HOME);
4660
+ return allowThenRescan(() => allowAllFindings(current.findings, repo), scanVerdict, repo);
4477
4661
  }
4478
4662
  if (allowRule !== void 0) {
4479
4663
  return allowThenRescan(
4480
4664
  () => {
4481
- const matched = allowFindingsByRule(current.findings, allowRule);
4665
+ const matched = allowFindingsByRule(current.findings, allowRule, repo);
4482
4666
  if (matched === 0) log(`no findings matched rule ${allowRule}; re-scanning`);
4483
4667
  },
4484
4668
  scanVerdict,
4485
- REPO_HOME
4669
+ repo
4486
4670
  );
4487
4671
  }
4488
4672
  if (!isTTYCheck()) {
@@ -4497,9 +4681,9 @@ async function resolveLeakFindings(verdict, ts, map, deps = {}) {
4497
4681
  const { bySession, other } = partitionFindings(unresolved);
4498
4682
  throw new NomadFatal(buildSessionAwareFatal(bySession, other));
4499
4683
  }
4500
- dispatchActions(current.findings, actions, ts, map, nowMs, scan);
4501
- gitOrFatal(["add", "-A"], "git add", REPO_HOME);
4502
- current = scanVerdict();
4684
+ dispatchActions(current.findings, actions, { ts, map, nowMs, repo, scan });
4685
+ gitOrFatal(["add", "-A"], "git add", repo);
4686
+ current = scanVerdict(repo);
4503
4687
  }
4504
4688
  return current;
4505
4689
  }
@@ -4523,7 +4707,7 @@ function writeAnimatedDone(out, label, ms, useTTY) {
4523
4707
  `);
4524
4708
  }
4525
4709
  function resolveWorkerPath(deps = {}) {
4526
- const check = deps.existsSyncFn ?? existsSync33;
4710
+ const check = deps.existsSyncFn ?? existsSync34;
4527
4711
  const base = deps.baseUrl ?? import.meta.url;
4528
4712
  const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
4529
4713
  if (check(mjs)) return mjs;
@@ -4729,17 +4913,19 @@ function handleWedge(repo, forceRemote) {
4729
4913
  function cmdPull(opts = {}) {
4730
4914
  const dryRun = opts.dryRun === true;
4731
4915
  const forceRemote = opts.forceRemote === true;
4732
- if (!existsSync34(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
4733
- if (!existsSync34(join40(REPO_HOME, "shared", "settings.base.json"))) {
4916
+ const repo = repoHome();
4917
+ const backup = backupBase();
4918
+ if (!existsSync35(repo)) die(`repo not cloned at ${repo}`);
4919
+ if (!existsSync35(join41(repo, "shared", "settings.base.json"))) {
4734
4920
  die("repo not initialized; run 'nomad init' to scaffold");
4735
4921
  }
4736
4922
  const handle = acquireLock("pull");
4737
4923
  if (handle === null) process.exit(0);
4738
4924
  try {
4739
- const ts = freshBackupTs(BACKUP_BASE);
4740
- handleWedge(REPO_HOME, forceRemote);
4925
+ const ts = freshBackupTs(backup);
4926
+ handleWedge(repo, forceRemote);
4741
4927
  if (!dryRun) {
4742
- const backupRoot = join40(BACKUP_BASE, ts);
4928
+ const backupRoot = join41(backup, ts);
4743
4929
  try {
4744
4930
  mkdirSync8(backupRoot, { recursive: true });
4745
4931
  } catch (err) {
@@ -4749,9 +4935,9 @@ function cmdPull(opts = {}) {
4749
4935
  log(
4750
4936
  dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
4751
4937
  );
4752
- gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
4753
- const mapPath = join40(REPO_HOME, "path-map.json");
4754
- const map = existsSync34(mapPath) ? readPathMap(mapPath) : { projects: {} };
4938
+ gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
4939
+ const mapPath = join41(repo, "path-map.json");
4940
+ const map = existsSync35(mapPath) ? readPathMap(mapPath) : { projects: {} };
4755
4941
  divergenceCheckExtras(ts);
4756
4942
  if (dryRun) {
4757
4943
  computePreview(ts, map, "pull");
@@ -4773,8 +4959,8 @@ function cmdPull(opts = {}) {
4773
4959
 
4774
4960
  // src/commands.push.ts
4775
4961
  init_config();
4776
- import { existsSync as existsSync36 } from "node:fs";
4777
- import { join as join42, relative as relative5 } from "node:path";
4962
+ import { existsSync as existsSync37 } from "node:fs";
4963
+ import { join as join43, relative as relative5 } from "node:path";
4778
4964
 
4779
4965
  // src/commands.push.allowlist.ts
4780
4966
  init_config();
@@ -4854,9 +5040,9 @@ init_color();
4854
5040
  init_config();
4855
5041
  init_config_sharedDirs_guard();
4856
5042
  import { randomBytes as randomBytes2 } from "node:crypto";
4857
- import { copyFileSync, existsSync as existsSync35, mkdirSync as mkdirSync9, readdirSync as readdirSync11, rmSync as rmSync12 } from "node:fs";
5043
+ import { copyFileSync, existsSync as existsSync36, mkdirSync as mkdirSync9, readdirSync as readdirSync11, rmSync as rmSync12 } from "node:fs";
4858
5044
  import { homedir as homedir5 } from "node:os";
4859
- import { join as join41 } from "node:path";
5045
+ import { join as join42 } from "node:path";
4860
5046
  init_push_leak_verdict();
4861
5047
  init_push_gitleaks();
4862
5048
  init_utils_fs();
@@ -4871,13 +5057,13 @@ function stageSessions(tmpRoot, map) {
4871
5057
  if (!p || p === "TBD") continue;
4872
5058
  reverse.set(encodePath(p), logical);
4873
5059
  }
4874
- const localProjects = join41(CLAUDE_HOME, "projects");
4875
- if (!existsSync35(localProjects)) return 0;
5060
+ const localProjects = join42(claudeHome(), "projects");
5061
+ if (!existsSync36(localProjects)) return 0;
4876
5062
  let staged = 0;
4877
5063
  for (const dir of readdirSync11(localProjects)) {
4878
5064
  const logical = reverse.get(dir);
4879
5065
  if (!logical) continue;
4880
- copyDirJsonlOnly(join41(localProjects, dir), join41(tmpRoot, "shared", "projects", logical));
5066
+ copyDirJsonlOnly(join42(localProjects, dir), join42(tmpRoot, "shared", "projects", logical));
4881
5067
  staged++;
4882
5068
  }
4883
5069
  return staged;
@@ -4893,9 +5079,9 @@ function stageExtras(tmpRoot, map) {
4893
5079
  if (!localRoot || localRoot === "TBD") continue;
4894
5080
  for (const dirname7 of dirnames) {
4895
5081
  if (!whitelist.includes(dirname7)) continue;
4896
- const src = join41(localRoot, dirname7);
4897
- if (!existsSync35(src)) continue;
4898
- const dst = join41(tmpRoot, "shared", "extras", logical, dirname7);
5082
+ const src = join42(localRoot, dirname7);
5083
+ if (!existsSync36(src)) continue;
5084
+ const dst = join42(tmpRoot, "shared", "extras", logical, dirname7);
4899
5085
  copyExtras(src, dst);
4900
5086
  staged++;
4901
5087
  }
@@ -4903,19 +5089,19 @@ function stageExtras(tmpRoot, map) {
4903
5089
  return staged;
4904
5090
  }
4905
5091
  function previewPushLeaks(map) {
4906
- const cacheDir = join41(homedir5(), ".cache", "claude-nomad");
5092
+ const cacheDir = join42(homedir5(), ".cache", "claude-nomad");
4907
5093
  mkdirSync9(cacheDir, { recursive: true });
4908
5094
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
4909
- const tmpRoot = join41(cacheDir, `push-preview-tree-${stamp}`);
5095
+ const tmpRoot = join42(cacheDir, `push-preview-tree-${stamp}`);
4910
5096
  try {
4911
5097
  const sessionCount = stageSessions(tmpRoot, map);
4912
5098
  const extrasCount = stageExtras(tmpRoot, map);
4913
5099
  if (sessionCount + extrasCount === 0) {
4914
5100
  return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
4915
5101
  }
4916
- const ignoreFile = join41(REPO_HOME, ".gitleaksignore");
4917
- if (existsSync35(ignoreFile)) {
4918
- copyFileSync(ignoreFile, join41(tmpRoot, ".gitleaksignore"));
5102
+ const ignoreFile = join42(repoHome(), ".gitleaksignore");
5103
+ if (existsSync36(ignoreFile)) {
5104
+ copyFileSync(ignoreFile, join42(tmpRoot, ".gitleaksignore"));
4919
5105
  }
4920
5106
  let findings;
4921
5107
  try {
@@ -4936,11 +5122,11 @@ function previewPushLeaks(map) {
4936
5122
  init_utils();
4937
5123
  init_utils_fs();
4938
5124
  init_utils_json();
4939
- function guardGitlinks() {
4940
- const gitlinks = findGitlinks(join42(REPO_HOME, "shared"));
5125
+ function guardGitlinks(repo) {
5126
+ const gitlinks = findGitlinks(join43(repo, "shared"));
4941
5127
  if (gitlinks.length === 0) return;
4942
5128
  for (const p of gitlinks) {
4943
- const rel = relative5(REPO_HOME, p);
5129
+ const rel = relative5(repo, p);
4944
5130
  fail(`gitlink: ${rel} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`);
4945
5131
  }
4946
5132
  const noun = gitlinks.length === 1 ? "entry" : "entries";
@@ -4948,15 +5134,15 @@ function guardGitlinks() {
4948
5134
  `gitlink trap: ${gitlinks.length} nested .git ${noun} in shared/; remove before retry`
4949
5135
  );
4950
5136
  }
4951
- async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule) {
4952
- gitOrFatal(["add", "-A"], "git add", REPO_HOME);
4953
- let verdict = withSpinner("Scanning for secrets", scanPushVerdict);
5137
+ async function commitAndPush(st, ts, map, redactAll, allowAll, allowRule, repo) {
5138
+ gitOrFatal(["add", "-A"], "git add", repo);
5139
+ let verdict = withSpinner("Scanning for secrets", () => scanPushVerdict(repo));
4954
5140
  if (verdict.leak) {
4955
5141
  renderPushTree(st, verdict);
4956
5142
  verdict = await resolveLeakFindings(verdict, ts, map, { redactAll, allowAll, allowRule });
4957
5143
  }
4958
- gitOrFatal(["commit", "-m", `chore: sync from ${HOST}`], "git commit", REPO_HOME);
4959
- withSpinner("Pushing", () => gitOrFatal(["push"], "git push", REPO_HOME));
5144
+ gitOrFatal(["commit", "-m", `chore: sync from ${HOST}`], "git commit", repo);
5145
+ withSpinner("Pushing", () => gitOrFatal(["push"], "git push", repo));
4960
5146
  renderPushTree(st, verdict);
4961
5147
  }
4962
5148
  function runDryRunPreview(st, map) {
@@ -4989,33 +5175,35 @@ async function cmdPush(opts = {}) {
4989
5175
  const allowAll = opts.allowAll === true;
4990
5176
  const allowRule = opts.allowRule;
4991
5177
  guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
4992
- if (!existsSync36(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
5178
+ const repo = repoHome();
5179
+ const backup = backupBase();
5180
+ if (!existsSync37(repo)) die(`repo not cloned at ${repo}`);
4993
5181
  const handle = acquireLock("push");
4994
5182
  if (handle === null) process.exit(0);
4995
5183
  try {
4996
5184
  console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
4997
5185
  probeGitleaks();
4998
- withSpinner("Rebasing onto origin", rebaseBeforePush);
4999
- const ts = freshBackupTs(BACKUP_BASE);
5186
+ withSpinner("Rebasing onto origin", () => rebaseBeforePush(repo));
5187
+ const ts = freshBackupTs(backup);
5000
5188
  const remap = withSpinner("Syncing sessions", () => remapPush(ts, { dryRun }));
5001
5189
  const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
5002
5190
  const st = { dryRun, remap, extras };
5003
- guardGitlinks();
5004
- const status = gitStatusPorcelainZ(REPO_HOME, { untrackedAll: true });
5191
+ guardGitlinks(repo);
5192
+ const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
5005
5193
  if (!dryRun && !status) {
5006
5194
  log("nothing to commit");
5007
5195
  renderNoScanTree(st);
5008
5196
  return;
5009
5197
  }
5010
- const mapPath = join42(REPO_HOME, "path-map.json");
5011
- if (!existsSync36(mapPath)) {
5198
+ const mapPath = join43(repo, "path-map.json");
5199
+ if (!existsSync37(mapPath)) {
5012
5200
  if (dryRun) return runDryRunPreview(st, null);
5013
5201
  die("path-map.json missing, cannot enforce push allow-list");
5014
5202
  }
5015
5203
  const map = readPathMap(mapPath);
5016
5204
  if (status) enforceAllowList(status, map);
5017
5205
  if (dryRun) return runDryRunPreview(st, map);
5018
- await commitAndPush(st, ts, map, redactAll, allowAll, allowRule);
5206
+ await commitAndPush(st, ts, map, redactAll, allowAll, allowRule, repo);
5019
5207
  } catch (err) {
5020
5208
  if (err instanceof NomadFatal) {
5021
5209
  fail(err.message);
@@ -5048,17 +5236,18 @@ init_config();
5048
5236
 
5049
5237
  // src/diff.ts
5050
5238
  init_config();
5051
- import { existsSync as existsSync37 } from "node:fs";
5052
- import { join as join43 } from "node:path";
5239
+ import { existsSync as existsSync38 } from "node:fs";
5240
+ import { join as join44 } from "node:path";
5053
5241
  init_utils();
5054
5242
  init_utils_fs();
5055
5243
  init_utils_json();
5056
5244
  function cmdDiff() {
5057
5245
  try {
5058
- if (!existsSync37(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
5059
- const ts = freshBackupTs(BACKUP_BASE);
5060
- const mapPath = join43(REPO_HOME, "path-map.json");
5061
- const map = existsSync37(mapPath) ? readPathMap(mapPath) : { projects: {} };
5246
+ const repo = repoHome();
5247
+ if (!existsSync38(repo)) die(`repo not cloned at ${repo}`);
5248
+ const ts = freshBackupTs(backupBase());
5249
+ const mapPath = join44(repo, "path-map.json");
5250
+ const map = existsSync38(mapPath) ? readPathMap(mapPath) : { projects: {} };
5062
5251
  computePreview(ts, map, "diff");
5063
5252
  } catch (err) {
5064
5253
  if (err instanceof NomadFatal) {
@@ -5072,8 +5261,8 @@ function cmdDiff() {
5072
5261
 
5073
5262
  // src/init.ts
5074
5263
  init_config();
5075
- import { existsSync as existsSync39, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
5076
- import { join as join45 } from "node:path";
5264
+ import { existsSync as existsSync40, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
5265
+ import { join as join46 } from "node:path";
5077
5266
 
5078
5267
  // src/init.gh-onboard.ts
5079
5268
  init_config();
@@ -5090,8 +5279,9 @@ function ensureOriginRepo(repoName, run = execFileSync16) {
5090
5279
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
5091
5280
  );
5092
5281
  }
5282
+ const repo = repoHome();
5093
5283
  try {
5094
- readOriginRemote(REPO_HOME, run);
5284
+ readOriginRemote(repo, run);
5095
5285
  return;
5096
5286
  } catch {
5097
5287
  }
@@ -5106,7 +5296,7 @@ function ensureOriginRepo(repoName, run = execFileSync16) {
5106
5296
  die("gh CLI is not authenticated. Run `gh auth login` and retry.");
5107
5297
  }
5108
5298
  try {
5109
- run("git", ["init", "-b", "main"], { cwd: REPO_HOME, stdio: ["ignore", "ignore", "pipe"] });
5299
+ run("git", ["init", "-b", "main"], { cwd: repo, stdio: ["ignore", "ignore", "pipe"] });
5110
5300
  } catch (err) {
5111
5301
  const e = err;
5112
5302
  throw new NomadFatal(`git init failed: ${e.message}`);
@@ -5139,7 +5329,7 @@ function ensureOriginRepo(repoName, run = execFileSync16) {
5139
5329
  }
5140
5330
  try {
5141
5331
  run("git", ["remote", "add", "origin", `git@github.com:${owner}/${repoName}.git`], {
5142
- cwd: REPO_HOME,
5332
+ cwd: repo,
5143
5333
  stdio: ["ignore", "ignore", "pipe"]
5144
5334
  });
5145
5335
  } catch (err) {
@@ -5154,31 +5344,33 @@ init_config();
5154
5344
  init_utils();
5155
5345
  init_utils_fs();
5156
5346
  init_utils_json();
5157
- import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync38, rmSync as rmSync13, statSync as statSync9 } from "node:fs";
5158
- import { join as join44 } from "node:path";
5347
+ import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync39, rmSync as rmSync13, statSync as statSync9 } from "node:fs";
5348
+ import { join as join45 } from "node:path";
5159
5349
  function snapshotIntoShared(map) {
5350
+ const repo = repoHome();
5351
+ const claude = claudeHome();
5160
5352
  for (const name of allSharedLinks(map)) {
5161
- const src = join44(CLAUDE_HOME, name);
5162
- if (!existsSync38(src)) continue;
5163
- const dst = join44(REPO_HOME, "shared", name);
5353
+ const src = join45(claude, name);
5354
+ if (!existsSync39(src)) continue;
5355
+ const dst = join45(repo, "shared", name);
5164
5356
  if (statSync9(src).isDirectory()) {
5165
- const gk = join44(dst, ".gitkeep");
5166
- if (existsSync38(gk)) rmSync13(gk);
5357
+ const gk = join45(dst, ".gitkeep");
5358
+ if (existsSync39(gk)) rmSync13(gk);
5167
5359
  cpSync7(src, dst, { recursive: true, force: false, errorOnExist: true });
5168
5360
  } else {
5169
5361
  copyFileSync2(src, dst);
5170
5362
  }
5171
5363
  log(`snapshotted shared/${name} from ${src}`);
5172
5364
  }
5173
- const userSettings = join44(CLAUDE_HOME, "settings.json");
5174
- if (existsSync38(userSettings)) {
5365
+ const userSettings = join45(claude, "settings.json");
5366
+ if (existsSync39(userSettings)) {
5175
5367
  let parsed;
5176
5368
  try {
5177
5369
  parsed = readJson(userSettings);
5178
5370
  } catch (err) {
5179
5371
  return die(`malformed ${userSettings}: ${err.message}`);
5180
5372
  }
5181
- const hostFile = join44(REPO_HOME, "hosts", `${HOST}.json`);
5373
+ const hostFile = join45(repo, "hosts", `${HOST}.json`);
5182
5374
  writeJsonAtomic(hostFile, parsed);
5183
5375
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
5184
5376
  }
@@ -5189,62 +5381,64 @@ init_utils();
5189
5381
  init_utils_fs();
5190
5382
  var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.claude/CLAUDE.md by nomad pull -->\n";
5191
5383
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
5192
- function preflightConflict(repoHome) {
5384
+ function preflightConflict(repoHome2) {
5193
5385
  const candidates = [
5194
- join45(repoHome, "shared", "settings.base.json"),
5195
- join45(repoHome, "shared", "CLAUDE.md"),
5196
- join45(repoHome, "path-map.json"),
5197
- join45(repoHome, "hosts"),
5198
- join45(repoHome, "shared")
5386
+ join46(repoHome2, "shared", "settings.base.json"),
5387
+ join46(repoHome2, "shared", "CLAUDE.md"),
5388
+ join46(repoHome2, "path-map.json"),
5389
+ join46(repoHome2, "hosts"),
5390
+ join46(repoHome2, "shared")
5199
5391
  ];
5200
5392
  for (const c of candidates) {
5201
- if (existsSync39(c)) return c;
5393
+ if (existsSync40(c)) return c;
5202
5394
  }
5203
5395
  return null;
5204
5396
  }
5205
5397
  function cmdInit(opts = {}) {
5206
5398
  const snapshot = opts.snapshot === true;
5207
5399
  const keepActions = opts.keepActions === true;
5208
- mkdirSync10(REPO_HOME, { recursive: true });
5209
- const conflict = preflightConflict(REPO_HOME);
5400
+ const repo = repoHome();
5401
+ const claude = claudeHome();
5402
+ mkdirSync10(repo, { recursive: true });
5403
+ const conflict = preflightConflict(repo);
5210
5404
  if (conflict !== null) {
5211
5405
  die(`already initialized; refusing to clobber ${conflict}`);
5212
5406
  }
5213
5407
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
5214
- mkdirSync10(join45(REPO_HOME, "shared"), { recursive: true });
5215
- mkdirSync10(join45(REPO_HOME, "hosts"), { recursive: true });
5408
+ mkdirSync10(join46(repo, "shared"), { recursive: true });
5409
+ mkdirSync10(join46(repo, "hosts"), { recursive: true });
5216
5410
  for (const name of SHARED_KEEP_DIRS) {
5217
- mkdirSync10(join45(REPO_HOME, "shared", name), { recursive: true });
5411
+ mkdirSync10(join46(repo, "shared", name), { recursive: true });
5218
5412
  }
5219
- const userClaudeMd = join45(CLAUDE_HOME, "CLAUDE.md");
5220
- if (!snapshot || !existsSync39(userClaudeMd)) {
5221
- writeFileSync6(join45(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
5222
- log("created shared/CLAUDE.md");
5413
+ const userClaudeMd = join46(claude, "CLAUDE.md");
5414
+ if (!snapshot || !existsSync40(userClaudeMd)) {
5415
+ writeFileSync6(join46(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
5416
+ item("created shared/CLAUDE.md");
5223
5417
  }
5224
5418
  for (const name of SHARED_KEEP_DIRS) {
5225
- writeFileSync6(join45(REPO_HOME, "shared", name, ".gitkeep"), "");
5226
- log(`created shared/${name}/.gitkeep`);
5227
- }
5228
- writeFileSync6(join45(REPO_HOME, "hosts", ".gitkeep"), "");
5229
- log("created hosts/.gitkeep");
5230
- writeJsonAtomic(join45(REPO_HOME, "shared", "settings.base.json"), {});
5231
- log("created shared/settings.base.json");
5232
- writeJsonAtomic(join45(REPO_HOME, "path-map.json"), { projects: {} });
5233
- log("created path-map.json");
5419
+ writeFileSync6(join46(repo, "shared", name, ".gitkeep"), "");
5420
+ item(`created shared/${name}/.gitkeep`);
5421
+ }
5422
+ writeFileSync6(join46(repo, "hosts", ".gitkeep"), "");
5423
+ item("created hosts/.gitkeep");
5424
+ writeJsonAtomic(join46(repo, "shared", "settings.base.json"), {});
5425
+ item("created shared/settings.base.json");
5426
+ writeJsonAtomic(join46(repo, "path-map.json"), { projects: {} });
5427
+ item("created path-map.json");
5234
5428
  if (snapshot) {
5235
5429
  snapshotIntoShared({ projects: {} });
5236
5430
  log(`snapshot staged in shared/; review, then 'nomad push' to share with other hosts.`);
5237
5431
  log("~/.claude/ originals were NOT removed.");
5238
5432
  }
5239
5433
  if (!keepActions) {
5240
- maybeDisableRepoActions(REPO_HOME, opts.run);
5434
+ maybeDisableRepoActions(repo, opts.run);
5241
5435
  }
5242
5436
  log("init complete");
5243
5437
  }
5244
- function maybeDisableRepoActions(repoHome, run) {
5438
+ function maybeDisableRepoActions(repoHome2, run) {
5245
5439
  let remote;
5246
5440
  try {
5247
- remote = readOriginRemote(repoHome, run);
5441
+ remote = readOriginRemote(repoHome2, run);
5248
5442
  } catch {
5249
5443
  return;
5250
5444
  }
@@ -5530,7 +5724,7 @@ function parsePushArgs(argv) {
5530
5724
  // package.json
5531
5725
  var package_default = {
5532
5726
  name: "claude-nomad",
5533
- version: "0.44.0",
5727
+ version: "0.45.0",
5534
5728
  type: "module",
5535
5729
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
5536
5730
  keywords: [
@@ -5597,6 +5791,8 @@ var package_default = {
5597
5791
  "@commitlint/cli": "^21.0.1",
5598
5792
  "@commitlint/config-conventional": "^21.0.1",
5599
5793
  "@eslint/js": "^10.0.1",
5794
+ "@stryker-mutator/core": "9.6.1",
5795
+ "@stryker-mutator/vitest-runner": "9.6.1",
5600
5796
  "@types/node": "^22.0.0",
5601
5797
  "@vitest/coverage-v8": "^4.1.6",
5602
5798
  diff: "^9.0.0",
@@ -5730,15 +5926,15 @@ var DEFAULT_HELP = [
5730
5926
  init_config();
5731
5927
  init_utils();
5732
5928
  init_utils_json();
5733
- import { existsSync as existsSync40, readFileSync as readFileSync12, readdirSync as readdirSync12 } from "node:fs";
5734
- import { join as join46 } from "node:path";
5929
+ import { existsSync as existsSync41, readFileSync as readFileSync13, readdirSync as readdirSync12 } from "node:fs";
5930
+ import { join as join47 } from "node:path";
5735
5931
  function resumeCmd(sessionId) {
5736
5932
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
5737
5933
  fail(`invalid session id: ${sessionId}`);
5738
5934
  process.exit(1);
5739
5935
  }
5740
- const projectsRoot = join46(CLAUDE_HOME, "projects");
5741
- if (!existsSync40(projectsRoot)) {
5936
+ const projectsRoot = join47(claudeHome(), "projects");
5937
+ if (!existsSync41(projectsRoot)) {
5742
5938
  fail(`${projectsRoot} does not exist`);
5743
5939
  process.exit(1);
5744
5940
  }
@@ -5752,8 +5948,8 @@ function resumeCmd(sessionId) {
5752
5948
  fail(`no cwd field found in ${jsonlPath}`);
5753
5949
  process.exit(1);
5754
5950
  }
5755
- const mapPath = join46(REPO_HOME, "path-map.json");
5756
- if (!existsSync40(mapPath)) {
5951
+ const mapPath = join47(repoHome(), "path-map.json");
5952
+ if (!existsSync41(mapPath)) {
5757
5953
  fail("path-map.json missing");
5758
5954
  process.exit(1);
5759
5955
  }
@@ -5776,13 +5972,13 @@ function resumeCmd(sessionId) {
5776
5972
  }
5777
5973
  function findTranscriptPath(projectsRoot, sessionId) {
5778
5974
  for (const dir of readdirSync12(projectsRoot)) {
5779
- const candidate = join46(projectsRoot, dir, `${sessionId}.jsonl`);
5780
- if (existsSync40(candidate)) return candidate;
5975
+ const candidate = join47(projectsRoot, dir, `${sessionId}.jsonl`);
5976
+ if (existsSync41(candidate)) return candidate;
5781
5977
  }
5782
5978
  return null;
5783
5979
  }
5784
5980
  function extractRecordedCwd(jsonlPath) {
5785
- for (const line of readFileSync12(jsonlPath, "utf8").split("\n")) {
5981
+ for (const line of readFileSync13(jsonlPath, "utf8").split("\n")) {
5786
5982
  if (!line.trim()) continue;
5787
5983
  try {
5788
5984
  const obj = JSON.parse(line);
@@ -5832,7 +6028,8 @@ function shQuote(s) {
5832
6028
 
5833
6029
  // src/nomad.ts
5834
6030
  init_utils();
5835
- if (!HOME) {
6031
+ var h = home();
6032
+ if (!h) {
5836
6033
  fail(
5837
6034
  "could not determine home directory (HOME env unset and no uid mapping). Set HOME and retry."
5838
6035
  );