claude-nomad 0.50.2 → 0.51.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
@@ -451,16 +451,41 @@ function readJson(path) {
451
451
  return data;
452
452
  }
453
453
  function readPathMap(mapPath) {
454
+ let parsed;
454
455
  try {
455
- return readJson(mapPath);
456
+ parsed = readJson(mapPath);
456
457
  } catch (err) {
457
458
  const verb = err instanceof SyntaxError ? "parse" : "read";
458
459
  throw new NomadFatal(`could not ${verb} path-map.json: ${err.message}`);
459
460
  }
461
+ const shapeError = validatePathMapShape(parsed);
462
+ if (shapeError !== null) throw new NomadFatal(shapeError);
463
+ return parsed;
464
+ }
465
+ function validatePathMapShape(raw) {
466
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
467
+ return "path-map.json invalid schema: top-level value must be an object";
468
+ }
469
+ const projects = raw.projects;
470
+ if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
471
+ return 'path-map.json invalid schema: "projects" must be an object';
472
+ }
473
+ for (const [name, hosts] of Object.entries(projects)) {
474
+ if (hosts === null || typeof hosts !== "object" || Array.isArray(hosts)) {
475
+ return `path-map.json invalid schema: project "${name}" hosts must be an object`;
476
+ }
477
+ for (const [host, value] of Object.entries(hosts)) {
478
+ if (typeof value !== "string") {
479
+ return `path-map.json invalid schema: project "${name}" host "${host}" path must be a string`;
480
+ }
481
+ }
482
+ }
483
+ return null;
460
484
  }
461
485
  function deepMerge(target, source) {
462
486
  const out = { ...target };
463
487
  for (const [key, value] of Object.entries(source)) {
488
+ if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
464
489
  const existing = out[key];
465
490
  const bothObjects = value !== null && typeof value === "object" && !Array.isArray(value) && existing !== null && typeof existing === "object" && !Array.isArray(existing);
466
491
  out[key] = bothObjects ? deepMerge(existing, value) : value;
@@ -502,7 +527,7 @@ import {
502
527
  symlinkSync,
503
528
  writeFileSync
504
529
  } from "node:fs";
505
- import { dirname, join as join2, relative } from "node:path";
530
+ import { dirname, join as join2, relative, sep } from "node:path";
506
531
  function writeJsonAtomic(path, data) {
507
532
  const mode = existsSync(path) ? statSync(path).mode & 511 : 384;
508
533
  const tmp = `${path}.tmp.${process.pid}`;
@@ -542,33 +567,22 @@ function ensureSymlink(linkPath, target) {
542
567
  symlinkSync(target, linkPath);
543
568
  log(`linked ${linkPath} -> ${target}`);
544
569
  }
545
- function backupBeforeWrite(absPath, ts) {
570
+ function backupUnder(absPath, anchor, destRoot) {
546
571
  if (!existsSync(absPath)) return;
547
- const claude = claudeHome();
548
- const rel = relative(claude, absPath);
549
- if (rel.startsWith("..") || rel === "") return;
550
- const backupRoot = join2(backupBase(), ts);
551
- const dst = join2(backupRoot, rel);
572
+ const rel = relative(anchor, absPath);
573
+ if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`)) return;
574
+ const dst = join2(destRoot, rel);
552
575
  mkdirSync(dirname(dst), { recursive: true });
553
576
  cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
554
577
  }
578
+ function backupBeforeWrite(absPath, ts) {
579
+ backupUnder(absPath, claudeHome(), join2(backupBase(), ts));
580
+ }
555
581
  function backupRepoWrite(absPath, ts, repoHome2) {
556
- if (!existsSync(absPath)) return;
557
- const rel = relative(repoHome2, absPath);
558
- if (rel.startsWith("..") || rel === "") return;
559
- const backupRoot = join2(backupBase(), ts, "repo");
560
- const dst = join2(backupRoot, rel);
561
- mkdirSync(dirname(dst), { recursive: true });
562
- cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
582
+ backupUnder(absPath, repoHome2, join2(backupBase(), ts, "repo"));
563
583
  }
564
584
  function backupExtrasWrite(absPath, ts, projectRoot) {
565
- if (!existsSync(absPath)) return;
566
- const rel = relative(projectRoot, absPath);
567
- if (rel.startsWith("..") || rel === "") return;
568
- const backupRoot = join2(backupBase(), ts, "extras");
569
- const dst = join2(backupRoot, encodePath(projectRoot), rel);
570
- mkdirSync(dirname(dst), { recursive: true });
571
- cpSync(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
585
+ backupUnder(absPath, projectRoot, join2(backupBase(), ts, "extras", encodePath(projectRoot)));
572
586
  }
573
587
  var init_utils_fs = __esm({
574
588
  "src/utils.fs.ts"() {
@@ -581,12 +595,12 @@ var init_utils_fs = __esm({
581
595
 
582
596
  // src/commands.pull.wedge.ts
583
597
  import { execFileSync as execFileSync2 } from "node:child_process";
584
- import { existsSync as existsSync10 } from "node:fs";
585
- import { join as join11 } from "node:path";
598
+ import { existsSync as existsSync12 } from "node:fs";
599
+ import { join as join14 } from "node:path";
586
600
  function detectWedge(repo) {
587
- const g = join11(repo, ".git");
588
- if (existsSync10(join11(g, "rebase-merge")) || existsSync10(join11(g, "rebase-apply"))) return "rebase";
589
- if (existsSync10(join11(g, "MERGE_HEAD"))) return "merge";
601
+ const g = join14(repo, ".git");
602
+ if (existsSync12(join14(g, "rebase-merge")) || existsSync12(join14(g, "rebase-apply"))) return "rebase";
603
+ if (existsSync12(join14(g, "MERGE_HEAD"))) return "merge";
590
604
  return null;
591
605
  }
592
606
  function unmergedIndexPresent(repo) {
@@ -643,32 +657,32 @@ var init_commands_pull_wedge = __esm({
643
657
  });
644
658
 
645
659
  // src/push-gitleaks.config.ts
646
- import { existsSync as existsSync11, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
660
+ import { existsSync as existsSync13, mkdtempSync, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
647
661
  import { tmpdir } from "node:os";
648
- import { join as join12 } from "node:path";
662
+ import { join as join15 } from "node:path";
649
663
  import { fileURLToPath } from "node:url";
650
664
  function resolveTomlPath(repo = repoHome()) {
651
- const repoToml = join12(repo, ".gitleaks.toml");
652
- if (existsSync11(repoToml)) return repoToml;
665
+ const repoToml = join15(repo, ".gitleaks.toml");
666
+ if (existsSync13(repoToml)) return repoToml;
653
667
  const bundled = fileURLToPath(new URL("../.gitleaks.toml", import.meta.url));
654
- return existsSync11(bundled) ? bundled : null;
668
+ return existsSync13(bundled) ? bundled : null;
655
669
  }
656
670
  function buildOverlayTempConfig(overlayBody, bundled) {
657
671
  const tempBody = `[extend]
658
672
  path = ${JSON.stringify(bundled)}
659
673
 
660
674
  ${overlayBody}`;
661
- const tempPath = mkdtempSync(join12(tmpdir(), "nomad-gitleaks-cfg-"));
662
- const configPath = join12(tempPath, "config.toml");
663
- writeFileSync2(configPath, tempBody, { mode: 384, flag: "wx" });
675
+ const tempPath = mkdtempSync(join15(tmpdir(), "nomad-gitleaks-cfg-"));
676
+ const configPath = join15(tempPath, "config.toml");
677
+ writeFileSync3(configPath, tempBody, { mode: 384, flag: "wx" });
664
678
  return { configPath, tempPath };
665
679
  }
666
680
  function resolveTomlConfig() {
667
681
  const repo = repoHome();
668
- const overlayPath = join12(repo, ".gitleaks.overlay.toml");
669
- const repoToml = join12(repo, ".gitleaks.toml");
682
+ const overlayPath = join15(repo, ".gitleaks.overlay.toml");
683
+ const repoToml = join15(repo, ".gitleaks.toml");
670
684
  const bundled = resolveTomlPath(repo);
671
- if (!existsSync11(overlayPath)) {
685
+ if (!existsSync13(overlayPath)) {
672
686
  return { path: bundled, tempPath: null };
673
687
  }
674
688
  if (bundled === repoToml) {
@@ -681,7 +695,7 @@ function resolveTomlConfig() {
681
695
  return { path: null, tempPath: null };
682
696
  }
683
697
  try {
684
- const overlayBody = readFileSync3(overlayPath, "utf8");
698
+ const overlayBody = readFileSync4(overlayPath, "utf8");
685
699
  if (OVERLAY_EXTEND_RE.test(overlayBody)) {
686
700
  throw new NomadFatal(
687
701
  ".gitleaks.overlay.toml must not contain an [extend] block; it is generated automatically. Remove the [extend] section and retry."
@@ -709,9 +723,9 @@ var init_push_gitleaks_config = __esm({
709
723
 
710
724
  // src/push-checks.ts
711
725
  import { execFileSync as execFileSync3 } from "node:child_process";
712
- import { readdirSync as readdirSync4, rmSync as rmSync4 } from "node:fs";
726
+ import { readdirSync as readdirSync4, rmSync as rmSync5 } from "node:fs";
713
727
  import { homedir as homedir2, platform } from "node:os";
714
- import { join as join13 } from "node:path";
728
+ import { join as join16 } from "node:path";
715
729
  function gitleaksInstallHint() {
716
730
  const head = "gitleaks not on PATH (required for nomad push). Install:";
717
731
  const plat = platform();
@@ -754,7 +768,7 @@ function findGitlinks(dir) {
754
768
  return;
755
769
  }
756
770
  for (const e of entries) {
757
- const p = join13(current, e.name);
771
+ const p = join16(current, e.name);
758
772
  if (e.name === ".git") {
759
773
  hits.push(p);
760
774
  continue;
@@ -776,7 +790,7 @@ function probeGitleaks() {
776
790
  if (e.code === "ENOENT") throw new NomadFatal(gitleaksInstallHint());
777
791
  throw new NomadFatal(`gitleaks --version failed: ${e.message}`);
778
792
  } finally {
779
- if (tempPath !== null) rmSync4(tempPath, { recursive: true, force: true });
793
+ if (tempPath !== null) rmSync5(tempPath, { recursive: true, force: true });
780
794
  }
781
795
  }
782
796
  function wedgePreflight(wedge) {
@@ -813,12 +827,12 @@ var init_push_checks = __esm({
813
827
 
814
828
  // src/push-gitleaks.scan.ts
815
829
  import { execFileSync as execFileSync6 } from "node:child_process";
816
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync5 } from "node:fs";
830
+ import { mkdirSync as mkdirSync3, readFileSync as readFileSync5, rmSync as rmSync6 } from "node:fs";
817
831
  import { homedir as homedir3 } from "node:os";
818
- import { join as join17 } from "node:path";
832
+ import { join as join20 } from "node:path";
819
833
  function readGitleaksReport(reportPath) {
820
834
  try {
821
- const raw = readFileSync4(reportPath, "utf8");
835
+ const raw = readFileSync5(reportPath, "utf8");
822
836
  const parsed = JSON.parse(raw);
823
837
  if (!Array.isArray(parsed)) return null;
824
838
  return parsed;
@@ -827,9 +841,9 @@ function readGitleaksReport(reportPath) {
827
841
  }
828
842
  }
829
843
  function scanStagedTree(repoDir, forwardStreams = false) {
830
- const cacheDir = join17(homedir3(), ".cache", "claude-nomad");
831
- mkdirSync2(cacheDir, { recursive: true });
832
- const reportPath = join17(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
844
+ const cacheDir = join20(homedir3(), ".cache", "claude-nomad");
845
+ mkdirSync3(cacheDir, { recursive: true });
846
+ const reportPath = join20(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
833
847
  const { path: toml, tempPath } = resolveTomlConfig();
834
848
  const args = [
835
849
  "protect",
@@ -856,14 +870,14 @@ function scanStagedTree(repoDir, forwardStreams = false) {
856
870
  }
857
871
  return report;
858
872
  } finally {
859
- if (tempPath !== null) rmSync5(tempPath, { recursive: true, force: true });
860
- rmSync5(reportPath, { force: true });
873
+ if (tempPath !== null) rmSync6(tempPath, { recursive: true, force: true });
874
+ rmSync6(reportPath, { force: true });
861
875
  }
862
876
  }
863
877
  function scanFile(filePath, forwardStreams = false) {
864
- const cacheDir = join17(homedir3(), ".cache", "claude-nomad");
865
- mkdirSync2(cacheDir, { recursive: true });
866
- const reportPath = join17(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
878
+ const cacheDir = join20(homedir3(), ".cache", "claude-nomad");
879
+ mkdirSync3(cacheDir, { recursive: true });
880
+ const reportPath = join20(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
867
881
  const { path: toml, tempPath } = resolveTomlConfig();
868
882
  const args = [
869
883
  "detect",
@@ -888,8 +902,8 @@ function scanFile(filePath, forwardStreams = false) {
888
902
  }
889
903
  return report;
890
904
  } finally {
891
- if (tempPath !== null) rmSync5(tempPath, { recursive: true, force: true });
892
- rmSync5(reportPath, { force: true });
905
+ if (tempPath !== null) rmSync6(tempPath, { recursive: true, force: true });
906
+ rmSync6(reportPath, { force: true });
893
907
  }
894
908
  }
895
909
  var init_push_gitleaks_scan = __esm({
@@ -1250,11 +1264,406 @@ function cmdAllow(fingerprints) {
1250
1264
  log(`allowed ${fingerprints.length} fingerprint(s)`);
1251
1265
  }
1252
1266
 
1267
+ // src/commands.capture-settings.ts
1268
+ init_config();
1269
+ import { existsSync as existsSync5 } from "node:fs";
1270
+ import { join as join7 } from "node:path";
1271
+ import { createInterface } from "node:readline/promises";
1272
+
1273
+ // src/commands.capture-settings.core.ts
1274
+ function arraysEqual(a, b) {
1275
+ if (a.length !== b.length) return false;
1276
+ for (let i = 0; i < a.length; i++) {
1277
+ if (!deepEqual(a[i], b[i])) return false;
1278
+ }
1279
+ return true;
1280
+ }
1281
+ function objectsEqual(a, b) {
1282
+ const aKeys = Object.keys(a);
1283
+ const bKeys = Object.keys(b);
1284
+ if (aKeys.length !== bKeys.length) return false;
1285
+ for (const k of aKeys) {
1286
+ if (!Object.hasOwn(b, k)) return false;
1287
+ if (!deepEqual(a[k], b[k])) return false;
1288
+ }
1289
+ return true;
1290
+ }
1291
+ function deepEqual(a, b) {
1292
+ if (a === b) return true;
1293
+ if (a === null || b === null) return false;
1294
+ if (Array.isArray(a) && Array.isArray(b)) return arraysEqual(a, b);
1295
+ if (Array.isArray(a) || Array.isArray(b)) return false;
1296
+ if (typeof a === "object" && typeof b === "object") {
1297
+ return objectsEqual(a, b);
1298
+ }
1299
+ return false;
1300
+ }
1301
+ function classifySettingsDrift(merged, settings) {
1302
+ const behind = [];
1303
+ const ahead = [];
1304
+ const changed = [];
1305
+ const settingsKeys = new Set(Object.keys(settings));
1306
+ for (const key of Object.keys(merged)) {
1307
+ if (!settingsKeys.has(key)) {
1308
+ behind.push(key);
1309
+ } else if (!deepEqual(merged[key], settings[key])) {
1310
+ changed.push(key);
1311
+ }
1312
+ }
1313
+ const mergedKeys = new Set(Object.keys(merged));
1314
+ for (const key of Object.keys(settings)) {
1315
+ if (!mergedKeys.has(key)) ahead.push(key);
1316
+ }
1317
+ const collator = (a, b) => a.localeCompare(b, "en");
1318
+ return {
1319
+ behind: behind.toSorted(collator),
1320
+ ahead: ahead.toSorted(collator),
1321
+ changed: changed.toSorted(collator)
1322
+ };
1323
+ }
1324
+ var CAPTURE_EXCLUDED_KEYS = /* @__PURE__ */ new Set([
1325
+ "apiKeyHelper",
1326
+ "awsAuthRefresh",
1327
+ "awsCredentialExport",
1328
+ "otelHeadersHelper",
1329
+ "env"
1330
+ ]);
1331
+ function partitionByCaptureExclusion(keys) {
1332
+ const promotable = [];
1333
+ const excluded = [];
1334
+ for (const key of keys) {
1335
+ if (CAPTURE_EXCLUDED_KEYS.has(key)) excluded.push(key);
1336
+ else promotable.push(key);
1337
+ }
1338
+ return { promotable, excluded };
1339
+ }
1340
+ var BIN_NODE_RE = /^(?:[A-Za-z]:)?[\\/](?:.*[\\/])?bin[\\/]node$/;
1341
+ function normalizeNodePathsDeep(value) {
1342
+ if (typeof value === "string") {
1343
+ return BIN_NODE_RE.test(value) ? "node" : value;
1344
+ }
1345
+ if (Array.isArray(value)) {
1346
+ return value.map(normalizeNodePathsDeep);
1347
+ }
1348
+ if (value !== null && typeof value === "object") {
1349
+ const out = {};
1350
+ for (const [k, v] of Object.entries(value)) {
1351
+ out[k] = normalizeNodePathsDeep(v);
1352
+ }
1353
+ return out;
1354
+ }
1355
+ return value;
1356
+ }
1357
+ function buildCaptureSubset(merged, settings, opts) {
1358
+ const { ahead } = classifySettingsDrift(merged, settings);
1359
+ const out = {};
1360
+ for (const key of ahead) {
1361
+ if (CAPTURE_EXCLUDED_KEYS.has(key)) continue;
1362
+ const raw = settings[key];
1363
+ out[key] = opts.normalizeNodePath ? normalizeNodePathsDeep(raw) : raw;
1364
+ }
1365
+ return out;
1366
+ }
1367
+
1368
+ // src/links.ts
1369
+ init_config();
1370
+ import { existsSync as existsSync4, lstatSync as lstatSync3, rmSync as rmSync2 } from "node:fs";
1371
+ import { join as join5 } from "node:path";
1372
+ init_utils();
1373
+ init_utils_fs();
1374
+ init_utils_json();
1375
+ function emitAutoMove(onPreview, linkPath, ts, name) {
1376
+ if (onPreview) {
1377
+ onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
1378
+ } else {
1379
+ log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
1380
+ }
1381
+ }
1382
+ function emitCreate(onPreview, from, to) {
1383
+ if (onPreview) {
1384
+ onPreview({ kind: "create", from, to });
1385
+ } else {
1386
+ log(`would create symlink: ${from} -> ${to}`);
1387
+ }
1388
+ }
1389
+ function isAlreadySymlink(linkPath) {
1390
+ return existsSync4(linkPath) && lstatSync3(linkPath).isSymbolicLink();
1391
+ }
1392
+ function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
1393
+ for (const name of linkNames) {
1394
+ const linkPath = join5(claude, name);
1395
+ const target = join5(repo, "shared", name);
1396
+ if (!existsSync4(linkPath)) continue;
1397
+ if (lstatSync3(linkPath).isSymbolicLink()) continue;
1398
+ if (!existsSync4(target)) continue;
1399
+ if (dryRun) {
1400
+ emitAutoMove(onPreview, linkPath, ts, name);
1401
+ continue;
1402
+ }
1403
+ backupBeforeWrite(linkPath, ts);
1404
+ rmSync2(linkPath, { recursive: true, force: true });
1405
+ }
1406
+ }
1407
+ function applySharedLinks(ts, map, opts = {}) {
1408
+ const dryRun = opts.dryRun === true;
1409
+ const claude = claudeHome();
1410
+ const repo = repoHome();
1411
+ const linkNames = allSharedLinks(map);
1412
+ runAutoMovePasses(linkNames, claude, repo, ts, dryRun, opts.onPreview);
1413
+ for (const name of linkNames) {
1414
+ const target = join5(repo, "shared", name);
1415
+ if (!existsSync4(target)) continue;
1416
+ const linkPath = join5(claude, name);
1417
+ if (isAlreadySymlink(linkPath)) continue;
1418
+ if (dryRun) {
1419
+ emitCreate(opts.onPreview, linkPath, target);
1420
+ continue;
1421
+ }
1422
+ ensureSymlink(linkPath, target);
1423
+ }
1424
+ }
1425
+ function regenerateSettings(ts, opts = {}) {
1426
+ const dryRun = opts.dryRun === true;
1427
+ const suppressDriftWarn = opts.suppressDriftWarn === true;
1428
+ const repo = repoHome();
1429
+ const claude = claudeHome();
1430
+ const basePath = join5(repo, "shared", "settings.base.json");
1431
+ const hostPath = join5(repo, "hosts", `${HOST}.json`);
1432
+ if (!existsSync4(basePath)) {
1433
+ die("repo not initialized; run 'nomad init' to scaffold");
1434
+ }
1435
+ const base = readJson(basePath);
1436
+ const hasOverrides = existsSync4(hostPath);
1437
+ const overrides = hasOverrides ? readJson(hostPath) : {};
1438
+ const merged = deepMerge(base, overrides);
1439
+ const settingsPath = join5(claude, "settings.json");
1440
+ if (!suppressDriftWarn && existsSync4(settingsPath)) {
1441
+ try {
1442
+ const existing = readJson(settingsPath);
1443
+ const drift = classifySettingsDrift(merged, existing);
1444
+ if (drift.behind.length > 0) {
1445
+ warn(
1446
+ `existing settings.json is missing merged keys ${JSON.stringify(drift.behind)}. Run 'nomad pull' to restore them.`
1447
+ );
1448
+ }
1449
+ const { promotable } = partitionByCaptureExclusion(drift.ahead);
1450
+ if (promotable.length > 0) {
1451
+ warn(
1452
+ `existing settings.json has local-only keys ${JSON.stringify(promotable)}. Run 'nomad capture-settings' to promote them into the repo before they are overwritten.`
1453
+ );
1454
+ }
1455
+ } catch {
1456
+ warn("existing settings.json is malformed; skipping drift-check and regenerating.");
1457
+ }
1458
+ }
1459
+ const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
1460
+ if (dryRun) {
1461
+ log(`would write settings.json (base + ${overrideLabel})`);
1462
+ return { label: overrideLabel };
1463
+ }
1464
+ backupBeforeWrite(settingsPath, ts);
1465
+ writeJsonAtomic(settingsPath, merged);
1466
+ return { label: overrideLabel };
1467
+ }
1468
+
1469
+ // src/commands.capture-settings.ts
1470
+ init_utils_fs();
1471
+ init_utils_json();
1472
+
1473
+ // src/utils.lockfile.ts
1474
+ init_config();
1475
+ init_utils();
1476
+ import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync3, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
1477
+ import { dirname as dirname2, join as join6 } from "node:path";
1478
+ function lockFilePath() {
1479
+ return join6(home(), ".cache", "claude-nomad", "nomad.lock");
1480
+ }
1481
+ function acquireLock(verb) {
1482
+ const lp = lockFilePath();
1483
+ mkdirSync2(dirname2(lp), { recursive: true });
1484
+ try {
1485
+ const fd = openSync2(lp, "wx");
1486
+ try {
1487
+ writeFileSync2(fd, String(process.pid));
1488
+ } catch (writeErr) {
1489
+ try {
1490
+ closeSync2(fd);
1491
+ } catch {
1492
+ }
1493
+ try {
1494
+ unlinkSync(lp);
1495
+ } catch {
1496
+ }
1497
+ throw writeErr;
1498
+ }
1499
+ return { fd, path: lp };
1500
+ } catch (err) {
1501
+ const code = err.code;
1502
+ if (code !== "EEXIST") throw err;
1503
+ return checkStaleAndRetry(verb, lp);
1504
+ }
1505
+ }
1506
+ function releaseLock(handle) {
1507
+ if (handle === null) return;
1508
+ const lp = handle.path;
1509
+ try {
1510
+ closeSync2(handle.fd);
1511
+ } catch {
1512
+ }
1513
+ try {
1514
+ unlinkSync(lp);
1515
+ } catch (err) {
1516
+ if (err.code !== "ENOENT") throw err;
1517
+ }
1518
+ }
1519
+ function unlinkIfSamePid(expectedPidStr, lp) {
1520
+ let current;
1521
+ try {
1522
+ current = readFileSync3(lp, "utf8").trim();
1523
+ } catch {
1524
+ return false;
1525
+ }
1526
+ if (current !== expectedPidStr) return false;
1527
+ try {
1528
+ unlinkSync(lp);
1529
+ return true;
1530
+ } catch {
1531
+ return false;
1532
+ }
1533
+ }
1534
+ function checkStaleAndRetry(verb, lp) {
1535
+ let pidStr;
1536
+ try {
1537
+ pidStr = readFileSync3(lp, "utf8").trim();
1538
+ } catch {
1539
+ pidStr = "";
1540
+ }
1541
+ const pid = Number.parseInt(pidStr, 10);
1542
+ if (!Number.isFinite(pid) || pid <= 0) {
1543
+ if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
1544
+ warn(`another nomad ${verb} running, skipping`);
1545
+ return null;
1546
+ }
1547
+ try {
1548
+ process.kill(pid, 0);
1549
+ warn(`another nomad ${verb} running, skipping`);
1550
+ return null;
1551
+ } catch (err) {
1552
+ const code = err.code;
1553
+ if (code === "ESRCH") {
1554
+ if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
1555
+ warn(`another nomad ${verb} running, skipping`);
1556
+ return null;
1557
+ }
1558
+ warn(`another nomad ${verb} running, skipping`);
1559
+ return null;
1560
+ }
1561
+ }
1562
+ function retryOnce(verb, lp) {
1563
+ try {
1564
+ const fd = openSync2(lp, "wx");
1565
+ try {
1566
+ writeFileSync2(fd, String(process.pid));
1567
+ } catch {
1568
+ try {
1569
+ closeSync2(fd);
1570
+ } catch {
1571
+ }
1572
+ try {
1573
+ unlinkSync(lp);
1574
+ } catch {
1575
+ }
1576
+ warn(`another nomad ${verb} running, skipping`);
1577
+ return null;
1578
+ }
1579
+ return { fd, path: lp };
1580
+ } catch {
1581
+ warn(`another nomad ${verb} running, skipping`);
1582
+ return null;
1583
+ }
1584
+ }
1585
+
1586
+ // src/commands.capture-settings.ts
1587
+ init_utils();
1588
+ async function confirmCapture(destLabel, keys) {
1589
+ if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
1590
+ warn(
1591
+ `refusing to write ${destLabel} without confirmation in a non-interactive shell; re-run with --yes (or --dry-run to preview)`
1592
+ );
1593
+ return false;
1594
+ }
1595
+ log(`About to promote ${keys.length} key(s) into ${destLabel}: ${keys.join(", ")}`);
1596
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
1597
+ try {
1598
+ const answer = await rl.question("Proceed? [y/N] ");
1599
+ return /^y(es)?$/i.test(answer.trim());
1600
+ } finally {
1601
+ rl.close();
1602
+ }
1603
+ }
1604
+ function resolveCaptureDestination(repo, useHost) {
1605
+ const destPath = useHost ? join7(repo, "hosts", `${HOST}.json`) : join7(repo, "shared", "settings.base.json");
1606
+ const existing = existsSync5(destPath) ? readJson(destPath) : {};
1607
+ return { destPath, existing };
1608
+ }
1609
+ async function cmdCaptureSettings(opts) {
1610
+ const { host: useHost, dryRun } = opts;
1611
+ const repo = repoHome();
1612
+ if (!existsSync5(repo)) die(`repo not cloned at ${repo}`);
1613
+ const handle = acquireLock("capture-settings");
1614
+ if (handle === null) process.exit(0);
1615
+ try {
1616
+ const claude = claudeHome();
1617
+ const basePath = join7(repo, "shared", "settings.base.json");
1618
+ if (!existsSync5(basePath)) {
1619
+ die("repo not initialized; run 'nomad init' to scaffold");
1620
+ }
1621
+ const settingsPath = join7(claude, "settings.json");
1622
+ if (!existsSync5(settingsPath)) {
1623
+ log("no ~/.claude/settings.json found; nothing to capture");
1624
+ return;
1625
+ }
1626
+ const base = readJson(basePath);
1627
+ const hostPath = join7(repo, "hosts", `${HOST}.json`);
1628
+ const overrides = existsSync5(hostPath) ? readJson(hostPath) : {};
1629
+ const merged = deepMerge(base, overrides);
1630
+ const settings = readJson(settingsPath);
1631
+ const subset = buildCaptureSubset(merged, settings, { normalizeNodePath: !useHost });
1632
+ if (Object.keys(subset).length === 0) {
1633
+ log("nothing to capture: no local-only keys found");
1634
+ return;
1635
+ }
1636
+ const { destPath, existing } = resolveCaptureDestination(repo, useHost);
1637
+ const newContent = deepMerge(existing, subset);
1638
+ const dest = useHost ? `hosts/${HOST}.json` : "shared/settings.base.json";
1639
+ const keys = Object.keys(subset).sort((a, b) => a.localeCompare(b, "en"));
1640
+ if (dryRun) {
1641
+ log(`dry-run: would write ${dest} with keys: ${keys.join(", ")}`);
1642
+ return;
1643
+ }
1644
+ if (opts.yes !== true) {
1645
+ const confirm = opts.confirm ?? confirmCapture;
1646
+ const proceed = await confirm(dest, keys);
1647
+ if (!proceed) {
1648
+ log("capture aborted; nothing written");
1649
+ return;
1650
+ }
1651
+ }
1652
+ const ts = freshBackupTs(backupBase());
1653
+ backupRepoWrite(destPath, ts, repo);
1654
+ writeJsonAtomic(destPath, newContent);
1655
+ regenerateSettings(ts, { suppressDriftWarn: true });
1656
+ log(`captured ${keys.length} key(s) into ${dest} (backup: ${ts})`);
1657
+ } finally {
1658
+ releaseLock(handle);
1659
+ }
1660
+ }
1661
+
1253
1662
  // src/commands.clean.ts
1254
1663
  init_config();
1255
1664
  init_utils();
1256
- import { existsSync as existsSync4, lstatSync as lstatSync3, readdirSync, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
1257
- import { join as join5 } from "node:path";
1665
+ import { existsSync as existsSync6, lstatSync as lstatSync4, readdirSync, rmSync as rmSync3, statSync as statSync2 } from "node:fs";
1666
+ import { join as join8 } from "node:path";
1258
1667
  var TS_SHAPE = /^\d{8}-\d{6}(-\d+)?$/;
1259
1668
  var DURATION_RE = /^(\d+)([dhm])$/;
1260
1669
  var UNIT_MS = { d: 864e5, h: 36e5, m: 6e4 };
@@ -1268,8 +1677,8 @@ function parseDuration(s) {
1268
1677
  return Number(m[1]) * UNIT_MS[m[2]];
1269
1678
  }
1270
1679
  function listBackupDirs(backupBase2) {
1271
- if (!existsSync4(backupBase2)) return [];
1272
- return readdirSync(backupBase2).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join5(backupBase2, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
1680
+ if (!existsSync6(backupBase2)) return [];
1681
+ return readdirSync(backupBase2).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join8(backupBase2, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
1273
1682
  }
1274
1683
  function prunableByAge(dirs, olderThanMs, nowMs) {
1275
1684
  return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
@@ -1279,10 +1688,10 @@ function prunableByCount(dirs, keep) {
1279
1688
  }
1280
1689
  function safeDelete(backupBase2, name) {
1281
1690
  if (!isTsDir(name)) return;
1282
- const full = join5(backupBase2, name);
1283
- const st = lstatSync3(full, { throwIfNoEntry: false });
1691
+ const full = join8(backupBase2, name);
1692
+ const st = lstatSync4(full, { throwIfNoEntry: false });
1284
1693
  if (!st || st.isSymbolicLink() || !st.isDirectory()) return;
1285
- rmSync2(full, { recursive: true, force: true });
1694
+ rmSync3(full, { recursive: true, force: true });
1286
1695
  }
1287
1696
  function resolveTargets(dirs, olderThanMs, keep) {
1288
1697
  if (keep !== void 0) return prunableByCount(dirs, keep);
@@ -1318,8 +1727,8 @@ function cmdClean(opts, backupBase2 = backupBase()) {
1318
1727
  init_config();
1319
1728
  init_utils();
1320
1729
  init_utils_json();
1321
- import { cpSync as cpSync3, existsSync as existsSync5, lstatSync as lstatSync4, realpathSync, renameSync as renameSync2, rmSync as rmSync3 } from "node:fs";
1322
- import { join as join6, sep } from "node:path";
1730
+ import { cpSync as cpSync3, existsSync as existsSync7, lstatSync as lstatSync5, realpathSync, renameSync as renameSync2, rmSync as rmSync4 } from "node:fs";
1731
+ import { join as join9, sep as sep2 } from "node:path";
1323
1732
  function ejectChecklist() {
1324
1733
  return [
1325
1734
  "Manual steps remaining to finish leaving claude-nomad on this host:",
@@ -1335,33 +1744,33 @@ function errMessage(err) {
1335
1744
  }
1336
1745
  function lexists2(p) {
1337
1746
  try {
1338
- lstatSync4(p);
1747
+ lstatSync5(p);
1339
1748
  return true;
1340
1749
  } catch {
1341
1750
  return false;
1342
1751
  }
1343
1752
  }
1344
1753
  function readMapIfPresent2(repoHome2) {
1345
- const mapPath = join6(repoHome2, "path-map.json");
1346
- return existsSync5(mapPath) ? readPathMap(mapPath) : { projects: {} };
1754
+ const mapPath = join9(repoHome2, "path-map.json");
1755
+ return existsSync7(mapPath) ? readPathMap(mapPath) : { projects: {} };
1347
1756
  }
1348
1757
  function classifyName(linkPath) {
1349
1758
  if (!lexists2(linkPath)) return "absent";
1350
- if (!lstatSync4(linkPath).isSymbolicLink()) return "skip-real";
1351
- if (!existsSync5(linkPath)) return "dangling";
1759
+ if (!lstatSync5(linkPath).isSymbolicLink()) return "skip-real";
1760
+ if (!existsSync7(linkPath)) return "dangling";
1352
1761
  return "materialize";
1353
1762
  }
1354
1763
  function resolveSharedRoot(repoHome2) {
1355
1764
  try {
1356
- return realpathSync(join6(repoHome2, "shared"));
1765
+ return realpathSync(join9(repoHome2, "shared"));
1357
1766
  } catch {
1358
1767
  return die(
1359
- `cannot resolve ${join6(repoHome2, "shared")} (repo checkout incomplete). run \`nomad pull\` first, then re-run \`nomad eject\``
1768
+ `cannot resolve ${join9(repoHome2, "shared")} (repo checkout incomplete). run \`nomad pull\` first, then re-run \`nomad eject\``
1360
1769
  );
1361
1770
  }
1362
1771
  }
1363
1772
  function isManagedTarget(target, sharedRoot) {
1364
- return target.startsWith(sharedRoot + sep);
1773
+ return target.startsWith(sharedRoot + sep2);
1365
1774
  }
1366
1775
  function materializeOne(name, linkPath, sharedRoot) {
1367
1776
  const target = realpathSync(linkPath);
@@ -1371,20 +1780,20 @@ function materializeOne(name, linkPath, sharedRoot) {
1371
1780
  }
1372
1781
  const tmp = `${linkPath}.eject.tmp.${process.pid}.${Date.now()}`;
1373
1782
  try {
1374
- rmSync3(tmp, { recursive: true, force: true });
1783
+ rmSync4(tmp, { recursive: true, force: true });
1375
1784
  cpSync3(target, tmp, {
1376
1785
  recursive: true,
1377
1786
  force: true,
1378
1787
  dereference: true,
1379
1788
  preserveTimestamps: true
1380
1789
  });
1381
- rmSync3(linkPath, { force: true });
1790
+ rmSync4(linkPath, { force: true });
1382
1791
  renameSync2(tmp, linkPath);
1383
1792
  item(`ejected: ${name}`);
1384
1793
  return true;
1385
1794
  } catch (err) {
1386
1795
  try {
1387
- rmSync3(tmp, { recursive: true, force: true });
1796
+ rmSync4(tmp, { recursive: true, force: true });
1388
1797
  } catch {
1389
1798
  }
1390
1799
  throw err;
@@ -1393,7 +1802,7 @@ function materializeOne(name, linkPath, sharedRoot) {
1393
1802
  function previewDryRun(names, classifications, claudeHome2, sharedRoot) {
1394
1803
  for (const name of names) {
1395
1804
  const cls = classifications.get(name);
1396
- const linkPath = join6(claudeHome2, name);
1805
+ const linkPath = join9(claudeHome2, name);
1397
1806
  if (cls === "absent") {
1398
1807
  item(`skipped (absent): ${name}`);
1399
1808
  } else if (cls === "skip-real") {
@@ -1423,7 +1832,7 @@ function runLiveEject(names, classifications, claudeHome2, sharedRoot) {
1423
1832
  let skipped = 0;
1424
1833
  for (const name of names) {
1425
1834
  const cls = classifications.get(name);
1426
- const linkPath = join6(claudeHome2, name);
1835
+ const linkPath = join9(claudeHome2, name);
1427
1836
  if (cls === "absent") {
1428
1837
  item(`skipped (absent): ${name}`);
1429
1838
  skipped++;
@@ -1459,7 +1868,7 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
1459
1868
  const names = allSharedLinks(map);
1460
1869
  const classifications = /* @__PURE__ */ new Map();
1461
1870
  for (const name of names) {
1462
- classifications.set(name, classifyName(join6(claudeHome2, name)));
1871
+ classifications.set(name, classifyName(join9(claudeHome2, name)));
1463
1872
  }
1464
1873
  const dangling = names.filter((n) => classifications.get(n) === "dangling");
1465
1874
  if (dangling.length > 0) {
@@ -1482,14 +1891,14 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
1482
1891
  }
1483
1892
 
1484
1893
  // src/commands.doctor.ts
1485
- import { existsSync as existsSync27 } from "node:fs";
1486
- import { join as join33 } from "node:path";
1894
+ import { existsSync as existsSync29 } from "node:fs";
1895
+ import { join as join35 } from "node:path";
1487
1896
 
1488
1897
  // src/commands.doctor.checks.repo.ts
1489
1898
  init_color();
1490
1899
  init_config();
1491
- import { existsSync as existsSync7, lstatSync as lstatSync5, statSync as statSync3 } from "node:fs";
1492
- import { join as join8 } from "node:path";
1900
+ import { existsSync as existsSync9, lstatSync as lstatSync6, statSync as statSync3 } from "node:fs";
1901
+ import { join as join11 } from "node:path";
1493
1902
 
1494
1903
  // src/commands.doctor.format.ts
1495
1904
  init_color();
@@ -1567,15 +1976,15 @@ function readJsonSafe(path, label, section2) {
1567
1976
  // src/init.classify.ts
1568
1977
  init_config();
1569
1978
  init_utils_json();
1570
- import { existsSync as existsSync6 } from "node:fs";
1571
- import { join as join7 } from "node:path";
1979
+ import { existsSync as existsSync8 } from "node:fs";
1980
+ import { join as join10 } from "node:path";
1572
1981
  function classifyRepoState(repoHome2, host) {
1573
- const basePath = join7(repoHome2, "shared", "settings.base.json");
1574
- const mapPath = join7(repoHome2, "path-map.json");
1575
- const hostPath = join7(repoHome2, "hosts", `${host}.json`);
1576
- const hasBase = existsSync6(basePath);
1577
- const hasMap = existsSync6(mapPath);
1578
- const hasHost = existsSync6(hostPath);
1982
+ const basePath = join10(repoHome2, "shared", "settings.base.json");
1983
+ const mapPath = join10(repoHome2, "path-map.json");
1984
+ const hostPath = join10(repoHome2, "hosts", `${host}.json`);
1985
+ const hasBase = existsSync8(basePath);
1986
+ const hasMap = existsSync8(mapPath);
1987
+ const hasHost = existsSync8(hostPath);
1579
1988
  let mapEntryCount = 0;
1580
1989
  if (hasMap) {
1581
1990
  try {
@@ -1590,11 +1999,11 @@ function classifyRepoState(repoHome2, host) {
1590
1999
  return "partial";
1591
2000
  }
1592
2001
  function reasonForPartial(repoHome2, host) {
1593
- const basePath = join7(repoHome2, "shared", "settings.base.json");
1594
- const mapPath = join7(repoHome2, "path-map.json");
1595
- const hostPath = join7(repoHome2, "hosts", `${host}.json`);
1596
- if (!existsSync6(basePath)) return "- shared/settings.base.json missing";
1597
- if (!existsSync6(mapPath)) return "- path-map.json missing";
2002
+ const basePath = join10(repoHome2, "shared", "settings.base.json");
2003
+ const mapPath = join10(repoHome2, "path-map.json");
2004
+ const hostPath = join10(repoHome2, "hosts", `${host}.json`);
2005
+ if (!existsSync8(basePath)) return "- shared/settings.base.json missing";
2006
+ if (!existsSync8(mapPath)) return "- path-map.json missing";
1598
2007
  let mapEntryCount;
1599
2008
  try {
1600
2009
  const map = readJson(mapPath);
@@ -1603,7 +2012,7 @@ function reasonForPartial(repoHome2, host) {
1603
2012
  mapEntryCount = 0;
1604
2013
  }
1605
2014
  if (mapEntryCount === 0) return "- path-map.json.projects has no entries";
1606
- if (!existsSync6(hostPath)) return `- hosts/${host}.json missing`;
2015
+ if (!existsSync8(hostPath)) return `- hosts/${host}.json missing`;
1607
2016
  return "- partial state (unknown gap)";
1608
2017
  }
1609
2018
 
@@ -1619,10 +2028,10 @@ function reportHostAndPaths(section2) {
1619
2028
  if (isOverrideActive()) {
1620
2029
  addItem(section2, `${dim(infoGlyph)} NOMAD_REPO: ${blue(repo)}`);
1621
2030
  }
1622
- addItem(section2, `${existsSync7(repo) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(repo)}`);
2031
+ addItem(section2, `${existsSync9(repo) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(repo)}`);
1623
2032
  addItem(
1624
2033
  section2,
1625
- `${existsSync7(claude) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(claude)}`
2034
+ `${existsSync9(claude) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(claude)}`
1626
2035
  );
1627
2036
  }
1628
2037
  function reportRepoState(section2) {
@@ -1645,12 +2054,12 @@ function reportRepoState(section2) {
1645
2054
  }
1646
2055
  }
1647
2056
  function repoHasSharedSource(name) {
1648
- return existsSync7(join8(repoHome(), "shared", name));
2057
+ return existsSync9(join11(repoHome(), "shared", name));
1649
2058
  }
1650
2059
  function classifySharedLink(name, p) {
1651
2060
  let stat;
1652
2061
  try {
1653
- stat = lstatSync5(p);
2062
+ stat = lstatSync6(p);
1654
2063
  } catch (err) {
1655
2064
  const code = err.code;
1656
2065
  if (code === "ENOENT") {
@@ -1693,7 +2102,7 @@ function classifySymlinkTarget(name, p) {
1693
2102
  function reportSharedLinks(section2, map) {
1694
2103
  const claude = claudeHome();
1695
2104
  for (const name of allSharedLinks(map)) {
1696
- const p = join8(claude, name);
2105
+ const p = join11(claude, name);
1697
2106
  const { line, fail: fail2 } = classifySharedLink(name, p);
1698
2107
  addItem(section2, line);
1699
2108
  if (fail2) process.exitCode = 1;
@@ -1702,10 +2111,10 @@ function reportSharedLinks(section2, map) {
1702
2111
  function reportDroppedNamesMigration(section2) {
1703
2112
  const claude = claudeHome();
1704
2113
  for (const name of GSD_DROPPED_NAMES) {
1705
- const p = join8(claude, name);
2114
+ const p = join11(claude, name);
1706
2115
  let stat;
1707
2116
  try {
1708
- stat = lstatSync5(p);
2117
+ stat = lstatSync6(p);
1709
2118
  } catch {
1710
2119
  continue;
1711
2120
  }
@@ -1720,11 +2129,11 @@ function reportDroppedNamesMigration(section2) {
1720
2129
  // src/commands.doctor.checks.settings.ts
1721
2130
  init_color();
1722
2131
  init_config();
1723
- import { existsSync as existsSync8, readdirSync as readdirSync2 } from "node:fs";
1724
- import { join as join9 } from "node:path";
2132
+ import { existsSync as existsSync10, readdirSync as readdirSync2 } from "node:fs";
2133
+ import { join as join12 } from "node:path";
1725
2134
  function loadBaseSettings(section2) {
1726
- const basePath = join9(repoHome(), "shared", "settings.base.json");
1727
- if (!existsSync8(basePath)) {
2135
+ const basePath = join12(repoHome(), "shared", "settings.base.json");
2136
+ if (!existsSync10(basePath)) {
1728
2137
  addItem(section2, `${red(failGlyph)} shared/settings.base.json missing at ${blue(basePath)}`);
1729
2138
  process.exitCode = 1;
1730
2139
  return null;
@@ -1732,8 +2141,8 @@ function loadBaseSettings(section2) {
1732
2141
  return readJsonSafe(basePath, basePath, section2);
1733
2142
  }
1734
2143
  function loadAndReportSettings(section2) {
1735
- const settingsPath = join9(claudeHome(), "settings.json");
1736
- if (!existsSync8(settingsPath)) return null;
2144
+ const settingsPath = join12(claudeHome(), "settings.json");
2145
+ if (!existsSync10(settingsPath)) return null;
1737
2146
  const settings = readJsonSafe(settingsPath, settingsPath, section2);
1738
2147
  if (settings === null) return null;
1739
2148
  const unknownKeys = Object.keys(settings).filter((k) => !KNOWN_SETTINGS_KEYS.has(k));
@@ -1749,13 +2158,13 @@ function loadAndReportSettings(section2) {
1749
2158
  }
1750
2159
  function reportHostOverrides(section2, base, settings) {
1751
2160
  const repo = repoHome();
1752
- const hostFile = join9(repo, "hosts", `${HOST}.json`);
2161
+ const hostFile = join12(repo, "hosts", `${HOST}.json`);
1753
2162
  let drift = [];
1754
2163
  if (base !== null && settings !== null) {
1755
2164
  const baseKeys = new Set(Object.keys(base));
1756
2165
  drift = Object.keys(settings).filter((k) => !baseKeys.has(k));
1757
2166
  }
1758
- if (existsSync8(hostFile)) {
2167
+ if (existsSync10(hostFile)) {
1759
2168
  if (readJsonSafe(hostFile, hostFile, section2) !== null) {
1760
2169
  addItem(section2, `${green(okGlyph)} host overrides: ${blue(hostFile)}`);
1761
2170
  }
@@ -1764,8 +2173,8 @@ function reportHostOverrides(section2, base, settings) {
1764
2173
  section2,
1765
2174
  `${red(failGlyph)} no hosts/${HOST}.json AND settings.json has unbased keys ${JSON.stringify(drift)}`
1766
2175
  );
1767
- const hostsDir = join9(repo, "hosts");
1768
- if (existsSync8(hostsDir)) {
2176
+ const hostsDir = join12(repo, "hosts");
2177
+ if (existsSync10(hostsDir)) {
1769
2178
  const cands = readdirSync2(hostsDir).filter((f) => f.endsWith(".json"));
1770
2179
  if (cands.length > 0) addItem(section2, `${dim(infoGlyph)} candidates: ${cands.join(", ")}`);
1771
2180
  }
@@ -1781,8 +2190,8 @@ function reportHostOverrides(section2, base, settings) {
1781
2190
  // src/commands.doctor.checks.pathmap.ts
1782
2191
  init_color();
1783
2192
  init_config();
1784
- import { existsSync as existsSync9, readdirSync as readdirSync3 } from "node:fs";
1785
- import { join as join10 } from "node:path";
2193
+ import { existsSync as existsSync11, readdirSync as readdirSync3 } from "node:fs";
2194
+ import { join as join13 } from "node:path";
1786
2195
  init_utils_json();
1787
2196
  function reportMappedProjects(section2, map) {
1788
2197
  const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
@@ -1792,8 +2201,8 @@ function reportMappedProjects(section2, map) {
1792
2201
  }
1793
2202
  }
1794
2203
  function reportUnmappedProjects(section2, map) {
1795
- const localProjects = join10(claudeHome(), "projects");
1796
- if (!existsSync9(localProjects)) return;
2204
+ const localProjects = join13(claudeHome(), "projects");
2205
+ if (!existsSync11(localProjects)) return;
1797
2206
  let localDirs;
1798
2207
  try {
1799
2208
  localDirs = readdirSync3(localProjects);
@@ -1833,43 +2242,20 @@ function reportPathCollisions(section2, map) {
1833
2242
  else addItem(section2, `${green(okGlyph)} path-encoding: no collisions`);
1834
2243
  }
1835
2244
  function reportPathMap(section2) {
1836
- const mapPath = join10(repoHome(), "path-map.json");
1837
- if (!existsSync9(mapPath)) {
2245
+ const mapPath = join13(repoHome(), "path-map.json");
2246
+ if (!existsSync11(mapPath)) {
1838
2247
  addItem(section2, `${red(failGlyph)} path-map.json missing at ${blue(mapPath)}`);
1839
2248
  process.exitCode = 1;
1840
2249
  return;
1841
2250
  }
1842
2251
  const map = readJsonSafe(mapPath, mapPath, section2);
1843
2252
  if (map === null) return;
1844
- const projects = map.projects;
1845
- if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
1846
- addItem(
1847
- section2,
1848
- `${red(failGlyph)} path-map.json invalid schema: "projects" must be an object`
1849
- );
2253
+ const shapeError = validatePathMapShape(map);
2254
+ if (shapeError !== null) {
2255
+ addItem(section2, `${red(failGlyph)} ${shapeError}`);
1850
2256
  process.exitCode = 1;
1851
2257
  return;
1852
2258
  }
1853
- for (const [name, hosts] of Object.entries(projects)) {
1854
- if (hosts === null || typeof hosts !== "object" || Array.isArray(hosts)) {
1855
- addItem(
1856
- section2,
1857
- `${red(failGlyph)} path-map.json invalid schema: project "${name}" hosts must be an object`
1858
- );
1859
- process.exitCode = 1;
1860
- return;
1861
- }
1862
- for (const [hostName, mappedPath] of Object.entries(hosts)) {
1863
- if (typeof mappedPath !== "string") {
1864
- addItem(
1865
- section2,
1866
- `${red(failGlyph)} path-map.json invalid schema: project "${name}" host "${hostName}" path must be a string`
1867
- );
1868
- process.exitCode = 1;
1869
- return;
1870
- }
1871
- }
1872
- }
1873
2259
  reportMappedProjects(section2, map);
1874
2260
  reportUnmappedProjects(section2, map);
1875
2261
  reportPathCollisions(section2, map);
@@ -1883,12 +2269,12 @@ function reportNeverSync(section2) {
1883
2269
  );
1884
2270
  }
1885
2271
 
1886
- // src/commands.doctor.checks.repository.ts
2272
+ // src/commands.doctor.checks.git-state.ts
1887
2273
  init_color();
1888
2274
  init_config();
1889
2275
  import { execFileSync as execFileSync4 } from "node:child_process";
1890
- import { existsSync as existsSync12 } from "node:fs";
1891
- import { join as join14, relative as relative2 } from "node:path";
2276
+ import { existsSync as existsSync14 } from "node:fs";
2277
+ import { join as join17, relative as relative2 } from "node:path";
1892
2278
  init_commands_pull_wedge();
1893
2279
  init_push_checks();
1894
2280
  init_utils();
@@ -1911,8 +2297,8 @@ function reportGitleaksProbe(section2) {
1911
2297
  }
1912
2298
  function reportGitlinks(section2) {
1913
2299
  const repo = repoHome();
1914
- const sharedDir = join14(repo, "shared");
1915
- if (existsSync12(sharedDir)) {
2300
+ const sharedDir = join17(repo, "shared");
2301
+ if (existsSync14(sharedDir)) {
1916
2302
  const gitlinks = findGitlinks(sharedDir);
1917
2303
  for (const p of gitlinks) {
1918
2304
  const rel = relative2(repo, p);
@@ -1985,8 +2371,8 @@ function reportOrphanedAutostash(sec) {
1985
2371
 
1986
2372
  // src/commands.doctor.checks.backups.ts
1987
2373
  init_color();
1988
- import { existsSync as existsSync13, lstatSync as lstatSync6, readdirSync as readdirSync5 } from "node:fs";
1989
- import { join as join15 } from "node:path";
2374
+ import { existsSync as existsSync15, lstatSync as lstatSync7, readdirSync as readdirSync5 } from "node:fs";
2375
+ import { join as join18 } from "node:path";
1990
2376
  init_config();
1991
2377
  var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
1992
2378
  function safeReaddir(dir) {
@@ -2002,8 +2388,8 @@ var BYTES_PER_MB = 1024 * 1024;
2002
2388
  function dirSizeBytes(dir) {
2003
2389
  let bytes = 0;
2004
2390
  for (const entry of safeReaddir(dir)) {
2005
- const full = join15(dir, entry);
2006
- const st = lstatSync6(full, { throwIfNoEntry: false });
2391
+ const full = join18(dir, entry);
2392
+ const st = lstatSync7(full, { throwIfNoEntry: false });
2007
2393
  if (!st) continue;
2008
2394
  if (st.isSymbolicLink()) continue;
2009
2395
  if (st.isDirectory()) bytes += dirSizeBytes(full);
@@ -2013,11 +2399,11 @@ function dirSizeBytes(dir) {
2013
2399
  }
2014
2400
  function totalSizeMb(backupBase2, dirs) {
2015
2401
  let bytes = 0;
2016
- for (const name of dirs) bytes += dirSizeBytes(join15(backupBase2, name));
2402
+ for (const name of dirs) bytes += dirSizeBytes(join18(backupBase2, name));
2017
2403
  return bytes / BYTES_PER_MB;
2018
2404
  }
2019
2405
  function reportBackupsCheck(section2, backupBase2 = backupBase()) {
2020
- if (!existsSync13(backupBase2)) return;
2406
+ if (!existsSync15(backupBase2)) return;
2021
2407
  const dirs = safeReaddir(backupBase2).filter((n) => TS_SHAPE2.test(n));
2022
2408
  const count = dirs.length;
2023
2409
  const sizeMb = totalSizeMb(backupBase2, dirs);
@@ -2031,8 +2417,8 @@ function reportBackupsCheck(section2, backupBase2 = backupBase()) {
2031
2417
 
2032
2418
  // src/commands.doctor.check-schema.ts
2033
2419
  init_color();
2034
- import { existsSync as existsSync14 } from "node:fs";
2035
- import { join as join16 } from "node:path";
2420
+ import { existsSync as existsSync16 } from "node:fs";
2421
+ import { join as join19 } from "node:path";
2036
2422
  init_config();
2037
2423
 
2038
2424
  // src/http-fetch.ts
@@ -2069,8 +2455,8 @@ function fetchSchemaKeys() {
2069
2455
  }
2070
2456
  }
2071
2457
  function reportCheckSchema(section2) {
2072
- const settingsPath = join16(claudeHome(), "settings.json");
2073
- if (!existsSync14(settingsPath)) {
2458
+ const settingsPath = join19(claudeHome(), "settings.json");
2459
+ if (!existsSync16(settingsPath)) {
2074
2460
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
2075
2461
  return;
2076
2462
  }
@@ -2100,18 +2486,18 @@ function reportCheckSchema(section2) {
2100
2486
  init_color();
2101
2487
  import { randomBytes } from "node:crypto";
2102
2488
  import { execFileSync as execFileSync7 } from "node:child_process";
2103
- import { existsSync as existsSync16, mkdirSync as mkdirSync4, readdirSync as readdirSync7, rmSync as rmSync7 } from "node:fs";
2489
+ import { existsSync as existsSync18, mkdirSync as mkdirSync5, readdirSync as readdirSync7, rmSync as rmSync8 } from "node:fs";
2104
2490
  import { homedir as homedir4 } from "node:os";
2105
- import { join as join20 } from "node:path";
2491
+ import { join as join23 } from "node:path";
2106
2492
 
2107
2493
  // src/commands.doctor.check-shared.scan.ts
2108
2494
  init_color();
2109
- import { join as join18 } from "node:path";
2495
+ import { join as join21 } from "node:path";
2110
2496
  init_config();
2111
2497
  init_push_gitleaks();
2112
2498
  function scrubPath(logical, sid, logicalToEncoded) {
2113
2499
  const encoded = logicalToEncoded.get(logical) ?? logical;
2114
- return join18(claudeHome(), "projects", encoded, `${sid}.jsonl`);
2500
+ return join21(claudeHome(), "projects", encoded, `${sid}.jsonl`);
2115
2501
  }
2116
2502
  function reportSessionFindings(section2, bySession) {
2117
2503
  for (const [sid, counts] of bySession) {
@@ -2203,21 +2589,27 @@ init_config();
2203
2589
  init_utils();
2204
2590
  init_utils_fs();
2205
2591
  init_utils_json();
2206
- import { cpSync as cpSync4, existsSync as existsSync15, mkdirSync as mkdirSync3, readdirSync as readdirSync6, rmSync as rmSync6, statSync as statSync4 } from "node:fs";
2207
- import { join as join19, relative as relative3, sep as sep2 } from "node:path";
2592
+ import { cpSync as cpSync4, existsSync as existsSync17, mkdirSync as mkdirSync4, readdirSync as readdirSync6, renameSync as renameSync3, rmSync as rmSync7, statSync as statSync4 } from "node:fs";
2593
+ import { join as join22, relative as relative3, sep as sep3 } from "node:path";
2594
+ var TMP_SUFFIX = ".nomad-tmp";
2595
+ function atomicMirror(src, dst, options) {
2596
+ const tmp = `${dst}${TMP_SUFFIX}`;
2597
+ rmSync7(tmp, { recursive: true, force: true });
2598
+ cpSync4(src, tmp, options);
2599
+ rmSync7(dst, { recursive: true, force: true });
2600
+ renameSync3(tmp, dst);
2601
+ }
2208
2602
  function copyDir(src, dst) {
2209
- rmSync6(dst, { recursive: true, force: true });
2210
- cpSync4(src, dst, { recursive: true, force: true });
2603
+ atomicMirror(src, dst, { recursive: true, force: true });
2211
2604
  }
2212
2605
  function copyDirJsonlOnly(src, dst) {
2213
- rmSync6(dst, { recursive: true, force: true });
2214
- cpSync4(src, dst, {
2606
+ atomicMirror(src, dst, {
2215
2607
  recursive: true,
2216
2608
  force: true,
2217
2609
  filter: (srcPath) => {
2218
2610
  const rel = relative3(src, srcPath);
2219
2611
  if (rel === "") return true;
2220
- if (rel.split(sep2).length > 1) return true;
2612
+ if (rel.split(sep3).length > 1) return true;
2221
2613
  if (statSync4(srcPath).isDirectory()) return true;
2222
2614
  if (srcPath.endsWith(".jsonl")) return true;
2223
2615
  item(`skip ${rel}: extension not in allowlist`);
@@ -2236,16 +2628,16 @@ function remapPull(ts, opts = {}) {
2236
2628
  const wouldPull = [];
2237
2629
  const repo = repoHome();
2238
2630
  const claude = claudeHome();
2239
- const mapPath = join19(repo, "path-map.json");
2240
- const repoProjects = join19(repo, "shared", "projects");
2241
- if (!existsSync15(mapPath) || !existsSync15(repoProjects)) {
2631
+ const mapPath = join22(repo, "path-map.json");
2632
+ const repoProjects = join22(repo, "shared", "projects");
2633
+ if (!existsSync17(mapPath) || !existsSync17(repoProjects)) {
2242
2634
  const text = "no path-map or repo projects dir; skipping session remap";
2243
2635
  emitPreview(opts.onPreview, { kind: "note", text }, text);
2244
2636
  return { unmapped: 0, pulled, wouldPull };
2245
2637
  }
2246
- const map = readJson(mapPath);
2247
- const localProjects = join19(claude, "projects");
2248
- if (!dryRun) mkdirSync3(localProjects, { recursive: true });
2638
+ const map = readPathMap(mapPath);
2639
+ const localProjects = join22(claude, "projects");
2640
+ if (!dryRun) mkdirSync4(localProjects, { recursive: true });
2249
2641
  for (const [logical, hosts] of Object.entries(map.projects)) {
2250
2642
  assertSafeLogical(logical);
2251
2643
  const localPath = hosts[HOST];
@@ -2253,9 +2645,9 @@ function remapPull(ts, opts = {}) {
2253
2645
  unmapped++;
2254
2646
  continue;
2255
2647
  }
2256
- const src = join19(repoProjects, logical);
2257
- if (!existsSync15(src)) continue;
2258
- const dst = join19(localProjects, encodePath(localPath));
2648
+ const src = join22(repoProjects, logical);
2649
+ if (!existsSync17(src)) continue;
2650
+ const dst = join22(localProjects, encodePath(localPath));
2259
2651
  if (dryRun) {
2260
2652
  wouldPull.push(logical);
2261
2653
  emitPreview(
@@ -2302,30 +2694,31 @@ function remapPush(ts, opts = {}) {
2302
2694
  const wouldPush = [];
2303
2695
  const repo = repoHome();
2304
2696
  const claude = claudeHome();
2305
- const mapPath = join19(repo, "path-map.json");
2306
- if (!existsSync15(mapPath)) {
2697
+ const mapPath = join22(repo, "path-map.json");
2698
+ if (!existsSync17(mapPath)) {
2307
2699
  log("no path-map.json; skipping session export");
2308
2700
  return { unmapped: 0, collisions: 0, pushed, wouldPush };
2309
2701
  }
2310
- const map = readJson(mapPath);
2311
- const localProjects = join19(claude, "projects");
2312
- const repoProjects = join19(repo, "shared", "projects");
2702
+ const map = readPathMap(mapPath);
2703
+ const localProjects = join22(claude, "projects");
2704
+ const repoProjects = join22(repo, "shared", "projects");
2313
2705
  const reverse = buildReverseMap(map);
2314
- if (!existsSync15(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
2315
- if (!dryRun) mkdirSync3(repoProjects, { recursive: true });
2706
+ if (!existsSync17(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
2707
+ if (!dryRun) mkdirSync4(repoProjects, { recursive: true });
2316
2708
  for (const dir of readdirSync6(localProjects)) {
2709
+ if (dir.endsWith(TMP_SUFFIX)) continue;
2317
2710
  const logical = reverse.get(dir);
2318
2711
  if (!logical) {
2319
2712
  unmapped++;
2320
2713
  continue;
2321
2714
  }
2322
- const repoDst = join19(repoProjects, logical);
2715
+ const repoDst = join22(repoProjects, logical);
2323
2716
  if (dryRun) {
2324
2717
  wouldPush.push(logical);
2325
2718
  continue;
2326
2719
  }
2327
2720
  backupRepoWrite(repoDst, ts, repo);
2328
- copyDirJsonlOnly(join19(localProjects, dir), repoDst);
2721
+ copyDirJsonlOnly(join22(localProjects, dir), repoDst);
2329
2722
  pushed.push(logical);
2330
2723
  }
2331
2724
  return { unmapped, collisions: 0, pushed, wouldPush };
@@ -2338,8 +2731,8 @@ function buildScanTree(tmpRoot) {
2338
2731
  const logicalToEncoded = /* @__PURE__ */ new Map();
2339
2732
  let staged = 0;
2340
2733
  const repo = repoHome();
2341
- const mapPath = join20(repo, "path-map.json");
2342
- if (!existsSync16(mapPath)) return { logicalToEncoded, staged, malformed: false };
2734
+ const mapPath = join23(repo, "path-map.json");
2735
+ if (!existsSync18(mapPath)) return { logicalToEncoded, staged, malformed: false };
2343
2736
  let map;
2344
2737
  try {
2345
2738
  map = readJson(mapPath);
@@ -2356,12 +2749,12 @@ function buildScanTree(tmpRoot) {
2356
2749
  if (!p || p === "TBD") continue;
2357
2750
  reverse.set(encodePath(p), logical);
2358
2751
  }
2359
- const localProjects = join20(claudeHome(), "projects");
2360
- if (!existsSync16(localProjects)) return { logicalToEncoded, staged, malformed: false };
2752
+ const localProjects = join23(claudeHome(), "projects");
2753
+ if (!existsSync18(localProjects)) return { logicalToEncoded, staged, malformed: false };
2361
2754
  for (const dir of readdirSync7(localProjects)) {
2362
2755
  const logical = reverse.get(dir);
2363
2756
  if (!logical) continue;
2364
- copyDirJsonlOnly(join20(localProjects, dir), join20(tmpRoot, "shared", "projects", logical));
2757
+ copyDirJsonlOnly(join23(localProjects, dir), join23(tmpRoot, "shared", "projects", logical));
2365
2758
  logicalToEncoded.set(logical, dir);
2366
2759
  staged++;
2367
2760
  }
@@ -2392,11 +2785,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
2392
2785
  }
2393
2786
  function reportCheckShared(section2, gitleaksReady) {
2394
2787
  if (!ensureGitleaksReady(section2, gitleaksReady)) return;
2395
- const cacheDir = join20(homedir4(), ".cache", "claude-nomad");
2396
- mkdirSync4(cacheDir, { recursive: true });
2788
+ const cacheDir = join23(homedir4(), ".cache", "claude-nomad");
2789
+ mkdirSync5(cacheDir, { recursive: true });
2397
2790
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes(4).toString("hex")}`;
2398
- const reportPath = join20(cacheDir, `check-shared-${stamp}.json`);
2399
- const tmpRoot = join20(cacheDir, `check-shared-tree-${stamp}`);
2791
+ const reportPath = join23(cacheDir, `check-shared-${stamp}.json`);
2792
+ const tmpRoot = join23(cacheDir, `check-shared-tree-${stamp}`);
2400
2793
  try {
2401
2794
  const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
2402
2795
  if (malformed) {
@@ -2410,19 +2803,19 @@ function reportCheckShared(section2, gitleaksReady) {
2410
2803
  }
2411
2804
  scanAndReport(section2, tmpRoot, staged, logicalToEncoded);
2412
2805
  } finally {
2413
- rmSync7(reportPath, { force: true });
2414
- rmSync7(tmpRoot, { recursive: true, force: true });
2806
+ rmSync8(reportPath, { force: true });
2807
+ rmSync8(tmpRoot, { recursive: true, force: true });
2415
2808
  }
2416
2809
  }
2417
2810
 
2418
2811
  // src/commands.doctor.checks.hooks.scope.ts
2419
2812
  init_color();
2420
- import { existsSync as existsSync17, readFileSync as readFileSync5, readdirSync as readdirSync8, realpathSync as realpathSync2 } from "node:fs";
2421
- import { dirname as dirname2, extname, join as join21 } from "node:path";
2813
+ import { existsSync as existsSync19, readFileSync as readFileSync6, readdirSync as readdirSync8, realpathSync as realpathSync2 } from "node:fs";
2814
+ import { dirname as dirname3, extname, join as join24 } from "node:path";
2422
2815
  init_config();
2423
2816
  function typeFromPackageJson(pkgPath) {
2424
2817
  try {
2425
- const parsed = JSON.parse(readFileSync5(pkgPath, "utf8"));
2818
+ const parsed = JSON.parse(readFileSync6(pkgPath, "utf8"));
2426
2819
  return parsed.type === "module" ? "esm" : "cjs";
2427
2820
  } catch {
2428
2821
  return "cjs";
@@ -2438,11 +2831,11 @@ function effectiveType(hookPath) {
2438
2831
  } catch {
2439
2832
  return null;
2440
2833
  }
2441
- let dir = dirname2(real);
2834
+ let dir = dirname3(real);
2442
2835
  for (; ; ) {
2443
- const pkg = join21(dir, "package.json");
2444
- if (existsSync17(pkg)) return typeFromPackageJson(pkg);
2445
- const parent = dirname2(dir);
2836
+ const pkg = join24(dir, "package.json");
2837
+ if (existsSync19(pkg)) return typeFromPackageJson(pkg);
2838
+ const parent = dirname3(dir);
2446
2839
  if (parent === dir) return "cjs";
2447
2840
  dir = parent;
2448
2841
  }
@@ -2477,21 +2870,21 @@ function safeReaddir2(dir) {
2477
2870
  }
2478
2871
  function safeRead(path) {
2479
2872
  try {
2480
- return readFileSync5(path, "utf8");
2873
+ return readFileSync6(path, "utf8");
2481
2874
  } catch {
2482
2875
  return null;
2483
2876
  }
2484
2877
  }
2485
2878
  function reportHookScopeCheck(section2) {
2486
- const hooksDir = join21(claudeHome(), "hooks");
2487
- if (!existsSync17(hooksDir)) {
2879
+ const hooksDir = join24(claudeHome(), "hooks");
2880
+ if (!existsSync19(hooksDir)) {
2488
2881
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
2489
2882
  return;
2490
2883
  }
2491
2884
  let anyWarn = false;
2492
2885
  for (const name of safeReaddir2(hooksDir)) {
2493
2886
  if (extname(name) !== ".js") continue;
2494
- const abs = join21(hooksDir, name);
2887
+ const abs = join24(hooksDir, name);
2495
2888
  const eff = effectiveType(abs);
2496
2889
  if (eff === null) continue;
2497
2890
  const src = safeRead(abs);
@@ -2511,8 +2904,8 @@ function reportHookScopeCheck(section2) {
2511
2904
 
2512
2905
  // src/commands.doctor.checks.hooks.ts
2513
2906
  init_color();
2514
- import { existsSync as existsSync18 } from "node:fs";
2515
- import { join as join22 } from "node:path";
2907
+ import { existsSync as existsSync20 } from "node:fs";
2908
+ import { join as join25 } from "node:path";
2516
2909
  init_config();
2517
2910
  function expandHome(token) {
2518
2911
  const h2 = home();
@@ -2551,7 +2944,7 @@ function checkEventGroups(section2, event, groups) {
2551
2944
  for (const group of groups) {
2552
2945
  for (const cmd of commandsFromGroup(group)) {
2553
2946
  for (const resolved of claudePathsIn(cmd)) {
2554
- if (existsSync18(resolved)) continue;
2947
+ if (existsSync20(resolved)) continue;
2555
2948
  addItem(
2556
2949
  section2,
2557
2950
  `${red(failGlyph)} hooks/${event}: command target missing: ${resolved} (run nomad pull)`
@@ -2564,8 +2957,8 @@ function checkEventGroups(section2, event, groups) {
2564
2957
  return anyFail;
2565
2958
  }
2566
2959
  function reportHooksTargetCheck(section2) {
2567
- const settingsPath = join22(claudeHome(), "settings.json");
2568
- if (!existsSync18(settingsPath)) {
2960
+ const settingsPath = join25(claudeHome(), "settings.json");
2961
+ if (!existsSync20(settingsPath)) {
2569
2962
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
2570
2963
  return;
2571
2964
  }
@@ -2588,12 +2981,12 @@ function reportHooksTargetCheck(section2) {
2588
2981
 
2589
2982
  // src/commands.doctor.checks.hooks.preserve-symlinks.ts
2590
2983
  init_color();
2591
- import { existsSync as existsSync20, readFileSync as readFileSync6 } from "node:fs";
2592
- import { join as join24 } from "node:path";
2984
+ import { existsSync as existsSync22, readFileSync as readFileSync7 } from "node:fs";
2985
+ import { join as join27 } from "node:path";
2593
2986
 
2594
2987
  // src/commands.doctor.checks.hooks.preserve-symlinks.probe.ts
2595
- import { closeSync as closeSync2, existsSync as existsSync19, openSync as openSync2, readSync, realpathSync as realpathSync3 } from "node:fs";
2596
- import { dirname as dirname3, join as join23, resolve as resolve2 } from "node:path";
2988
+ import { closeSync as closeSync3, existsSync as existsSync21, openSync as openSync3, readSync, realpathSync as realpathSync3 } from "node:fs";
2989
+ import { dirname as dirname4, join as join26, resolve as resolve2 } from "node:path";
2597
2990
  function suppressedRanges(src) {
2598
2991
  const ranges = [];
2599
2992
  const re = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'[^']*'|"[^"]*"|`[^`]*`/g;
@@ -2625,12 +3018,12 @@ function topRelativeSpecifiers(src) {
2625
3018
  }
2626
3019
  function specifierIsMissing(specifier, baseDir) {
2627
3020
  const base = resolve2(baseDir, specifier);
2628
- if (existsSync19(base)) return false;
3021
+ if (existsSync21(base)) return false;
2629
3022
  for (const ext of [".js", ".cjs", ".mjs"]) {
2630
- if (existsSync19(base + ext)) return false;
3023
+ if (existsSync21(base + ext)) return false;
2631
3024
  }
2632
3025
  for (const idx of ["index.js", "index.cjs", "index.mjs"]) {
2633
- if (existsSync19(join23(base, idx))) return false;
3026
+ if (existsSync21(join26(base, idx))) return false;
2634
3027
  }
2635
3028
  return true;
2636
3029
  }
@@ -2643,20 +3036,20 @@ function relativeRequireTargetsBroken(scriptPath) {
2643
3036
  }
2644
3037
  let raw;
2645
3038
  try {
2646
- const fd = openSync2(realPath, "r");
3039
+ const fd = openSync3(realPath, "r");
2647
3040
  try {
2648
3041
  const buf = Buffer.alloc(65536);
2649
3042
  const bytesRead = readSync(fd, buf, 0, 65536, 0);
2650
3043
  raw = buf.toString("utf8", 0, bytesRead);
2651
3044
  } finally {
2652
- closeSync2(fd);
3045
+ closeSync3(fd);
2653
3046
  }
2654
3047
  } catch {
2655
3048
  return false;
2656
3049
  }
2657
3050
  const specifiers = topRelativeSpecifiers(raw);
2658
3051
  if (specifiers.length === 0) return false;
2659
- const baseDir = dirname3(realPath);
3052
+ const baseDir = dirname4(realPath);
2660
3053
  for (const spec of specifiers) {
2661
3054
  if (specifierIsMissing(spec, baseDir)) return true;
2662
3055
  }
@@ -2685,10 +3078,10 @@ function commandTokens(command) {
2685
3078
  return tokens;
2686
3079
  }
2687
3080
  function readPathMapSafe() {
2688
- const mapPath = join24(repoHome(), "path-map.json");
2689
- if (!existsSync20(mapPath)) return { projects: {} };
3081
+ const mapPath = join27(repoHome(), "path-map.json");
3082
+ if (!existsSync22(mapPath)) return { projects: {} };
2690
3083
  try {
2691
- return JSON.parse(readFileSync6(mapPath, "utf8"));
3084
+ return JSON.parse(readFileSync7(mapPath, "utf8"));
2692
3085
  } catch {
2693
3086
  return { projects: {} };
2694
3087
  }
@@ -2759,8 +3152,8 @@ function checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames)
2759
3152
  return anyWarn;
2760
3153
  }
2761
3154
  function reportPreserveSymlinksCheck(section2) {
2762
- const settingsPath = join24(claudeHome(), "settings.json");
2763
- if (!existsSync20(settingsPath)) {
3155
+ const settingsPath = join27(claudeHome(), "settings.json");
3156
+ if (!existsSync22(settingsPath)) {
2764
3157
  addItem(
2765
3158
  section2,
2766
3159
  `${dim(infoGlyph)} no ~/.claude/settings.json; skipping preserve-symlinks-main check`
@@ -2769,7 +3162,7 @@ function reportPreserveSymlinksCheck(section2) {
2769
3162
  }
2770
3163
  let settings;
2771
3164
  try {
2772
- settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
3165
+ settings = JSON.parse(readFileSync7(settingsPath, "utf8"));
2773
3166
  } catch {
2774
3167
  return;
2775
3168
  }
@@ -2781,75 +3174,29 @@ function reportPreserveSymlinksCheck(section2) {
2781
3174
  }
2782
3175
  const map = readPathMapSafe();
2783
3176
  const sharedLinkNames = allSharedLinks(map);
2784
- let anyWarn = false;
2785
- for (const [event, groups] of Object.entries(hooks)) {
2786
- if (!Array.isArray(groups)) continue;
2787
- if (checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames)) anyWarn = true;
2788
- }
2789
- if (!anyWarn) {
2790
- addItem(section2, `${green(okGlyph)} hooks: preserve-symlinks-main not needed`);
2791
- }
2792
- }
2793
-
2794
- // src/commands.doctor.checks.settings-drift.ts
2795
- init_color();
2796
- import { existsSync as existsSync21, readFileSync as readFileSync7 } from "node:fs";
2797
- import { join as join25 } from "node:path";
2798
- init_config();
2799
- init_utils_json();
2800
- function arraysEqual(a, b) {
2801
- if (a.length !== b.length) return false;
2802
- for (let i = 0; i < a.length; i++) {
2803
- if (!deepEqual(a[i], b[i])) return false;
2804
- }
2805
- return true;
2806
- }
2807
- function objectsEqual(a, b) {
2808
- const aKeys = Object.keys(a);
2809
- const bKeys = Object.keys(b);
2810
- if (aKeys.length !== bKeys.length) return false;
2811
- for (const k of aKeys) {
2812
- if (!Object.hasOwn(b, k)) return false;
2813
- if (!deepEqual(a[k], b[k])) return false;
3177
+ let anyWarn = false;
3178
+ for (const [event, groups] of Object.entries(hooks)) {
3179
+ if (!Array.isArray(groups)) continue;
3180
+ if (checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames)) anyWarn = true;
2814
3181
  }
2815
- return true;
2816
- }
2817
- function deepEqual(a, b) {
2818
- if (a === b) return true;
2819
- if (a === null || b === null) return false;
2820
- if (Array.isArray(a) && Array.isArray(b)) return arraysEqual(a, b);
2821
- if (Array.isArray(a) || Array.isArray(b)) return false;
2822
- if (typeof a === "object" && typeof b === "object") {
2823
- return objectsEqual(a, b);
3182
+ if (!anyWarn) {
3183
+ addItem(section2, `${green(okGlyph)} hooks: preserve-symlinks-main not needed`);
2824
3184
  }
2825
- return false;
2826
3185
  }
3186
+
3187
+ // src/commands.doctor.checks.settings-drift.ts
3188
+ init_color();
3189
+ import { existsSync as existsSync23, readFileSync as readFileSync8 } from "node:fs";
3190
+ import { join as join28 } from "node:path";
3191
+ init_config();
3192
+ init_utils_json();
2827
3193
  function diffMergedSettings(merged, settings) {
2828
- const missing = [];
2829
- const changed = [];
2830
- const extra = [];
2831
- const settingsKeys = new Set(Object.keys(settings));
2832
- for (const key of Object.keys(merged)) {
2833
- if (!settingsKeys.has(key)) {
2834
- missing.push(key);
2835
- } else if (!deepEqual(merged[key], settings[key])) {
2836
- changed.push(key);
2837
- }
2838
- }
2839
- const mergedKeys = new Set(Object.keys(merged));
2840
- for (const key of Object.keys(settings)) {
2841
- if (!mergedKeys.has(key)) extra.push(key);
2842
- }
2843
- const collator = (a, b) => a.localeCompare(b, "en");
2844
- return {
2845
- missing: missing.toSorted(collator),
2846
- changed: changed.toSorted(collator),
2847
- extra: extra.toSorted(collator)
2848
- };
3194
+ const { behind, changed, ahead } = classifySettingsDrift(merged, settings);
3195
+ return { missing: behind, changed, extra: ahead };
2849
3196
  }
2850
3197
  function tryReadJson(filePath) {
2851
3198
  try {
2852
- const raw = readFileSync7(filePath, "utf8");
3199
+ const raw = readFileSync8(filePath, "utf8");
2853
3200
  const parsed = JSON.parse(raw);
2854
3201
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
2855
3202
  return parsed;
@@ -2861,14 +3208,14 @@ function reportSettingsDriftCheck(section2) {
2861
3208
  const claude = claudeHome();
2862
3209
  const repo = repoHome();
2863
3210
  const host = HOST;
2864
- const settingsPath = join25(claude, "settings.json");
2865
- const basePath = join25(repo, "shared", "settings.base.json");
2866
- const hostPath = join25(repo, "hosts", `${host}.json`);
2867
- if (!existsSync21(settingsPath)) {
3211
+ const settingsPath = join28(claude, "settings.json");
3212
+ const basePath = join28(repo, "shared", "settings.base.json");
3213
+ const hostPath = join28(repo, "hosts", `${host}.json`);
3214
+ if (!existsSync23(settingsPath)) {
2868
3215
  addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping merge-drift check`);
2869
3216
  return;
2870
3217
  }
2871
- if (!existsSync21(basePath)) {
3218
+ if (!existsSync23(basePath)) {
2872
3219
  addItem(
2873
3220
  section2,
2874
3221
  `${dim(infoGlyph)} shared/settings.base.json missing; skipping merge-drift check`
@@ -2887,7 +3234,7 @@ function reportSettingsDriftCheck(section2) {
2887
3234
  if (settings === null) {
2888
3235
  return;
2889
3236
  }
2890
- const hostExists = existsSync21(hostPath);
3237
+ const hostExists = existsSync23(hostPath);
2891
3238
  const hostObj = hostExists ? tryReadJson(hostPath) : null;
2892
3239
  if (hostExists && hostObj === null) {
2893
3240
  addItem(
@@ -2898,9 +3245,10 @@ function reportSettingsDriftCheck(section2) {
2898
3245
  }
2899
3246
  const merged = deepMerge(base, hostObj ?? {});
2900
3247
  const { missing, changed, extra } = diffMergedSettings(merged, settings);
2901
- emitDriftRows(section2, missing, changed, extra, host, hostExists);
3248
+ const { promotable, excluded } = partitionByCaptureExclusion(extra);
3249
+ emitDriftRows(section2, missing, changed, promotable, excluded, hostExists);
2902
3250
  }
2903
- function emitDriftRows(section2, missing, changed, extra, host, hostFileExists) {
3251
+ function emitDriftRows(section2, missing, changed, promotable, excluded, hostFileExists) {
2904
3252
  if (missing.length > 0) {
2905
3253
  addItem(
2906
3254
  section2,
@@ -2913,13 +3261,19 @@ function emitDriftRows(section2, missing, changed, extra, host, hostFileExists)
2913
3261
  `${yellow(warnGlyph)} settings.json drift: merged keys with changed values: ${changed.join(", ")} (run 'nomad pull')`
2914
3262
  );
2915
3263
  }
2916
- if (extra.length > 0 && hostFileExists) {
3264
+ if (promotable.length > 0 && hostFileExists) {
3265
+ addItem(
3266
+ section2,
3267
+ `${dim(infoGlyph)} settings.json has ${promotable.length} local-only key(s) not in base+host merge: ${promotable.join(", ")} (run 'nomad capture-settings' to promote them into the repo)`
3268
+ );
3269
+ }
3270
+ if (excluded.length > 0 && hostFileExists) {
2917
3271
  addItem(
2918
3272
  section2,
2919
- `${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)`
3273
+ `${dim(infoGlyph)} settings.json has ${excluded.length} local-only key(s) outside the sync set (credential or host-local; nomad does not promote these)`
2920
3274
  );
2921
3275
  }
2922
- if (missing.length === 0 && changed.length === 0 && extra.length === 0) {
3276
+ if (missing.length === 0 && changed.length === 0 && promotable.length === 0 && excluded.length === 0) {
2923
3277
  addItem(section2, `${green(okGlyph)} settings.json matches base+host merge`);
2924
3278
  }
2925
3279
  }
@@ -2929,12 +3283,12 @@ init_config();
2929
3283
 
2930
3284
  // src/commands.doctor.engine.ts
2931
3285
  init_color();
2932
- import { readFileSync as readFileSync9 } from "node:fs";
3286
+ import { readFileSync as readFileSync10 } from "node:fs";
2933
3287
  import { fileURLToPath as fileURLToPath3 } from "node:url";
2934
3288
 
2935
3289
  // src/commands.doctor.version.ts
2936
3290
  init_color();
2937
- import { readFileSync as readFileSync8 } from "node:fs";
3291
+ import { readFileSync as readFileSync9 } from "node:fs";
2938
3292
  import { fileURLToPath as fileURLToPath2 } from "node:url";
2939
3293
  init_config();
2940
3294
  var STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
@@ -2951,7 +3305,7 @@ function compareSemver(a, b) {
2951
3305
  function readLocalVersion() {
2952
3306
  try {
2953
3307
  const pkgPath = fileURLToPath2(new URL("../package.json", import.meta.url));
2954
- const parsed = JSON.parse(readFileSync8(pkgPath, "utf8"));
3308
+ const parsed = JSON.parse(readFileSync9(pkgPath, "utf8"));
2955
3309
  if (typeof parsed.version === "string" && parsed.version.length > 0) {
2956
3310
  return parsed.version;
2957
3311
  }
@@ -3010,7 +3364,7 @@ function parseMinVersion(spec) {
3010
3364
  function readEnginesNode() {
3011
3365
  try {
3012
3366
  const pkgPath = fileURLToPath3(new URL("../package.json", import.meta.url));
3013
- const parsed = JSON.parse(readFileSync9(pkgPath, "utf8"));
3367
+ const parsed = JSON.parse(readFileSync10(pkgPath, "utf8"));
3014
3368
  const node = parsed.engines?.node;
3015
3369
  if (typeof node === "string" && node.length > 0) return node;
3016
3370
  return null;
@@ -3038,43 +3392,44 @@ function reportNodeEngineCheck(section2) {
3038
3392
 
3039
3393
  // src/spinner.ts
3040
3394
  init_color();
3041
- import { existsSync as existsSync25 } from "node:fs";
3395
+ import { existsSync as existsSync27 } from "node:fs";
3042
3396
  import { fileURLToPath as fileURLToPath4 } from "node:url";
3043
3397
  import { Worker } from "node:worker_threads";
3044
3398
 
3045
3399
  // src/commands.push.recovery.ts
3046
3400
  init_config();
3047
- import { readFileSync as readFileSync13, rmSync as rmSync9, writeFileSync as writeFileSync5 } from "node:fs";
3048
- import { join as join31 } from "node:path";
3049
- import { createInterface } from "node:readline/promises";
3401
+ import { readFileSync as readFileSync13, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
3402
+ import { join as join33 } from "node:path";
3403
+ import { createInterface as createInterface2 } from "node:readline/promises";
3050
3404
 
3051
3405
  // src/commands.push.recovery.actions.ts
3052
3406
  init_config();
3053
3407
  import { readFileSync as readFileSync12 } from "node:fs";
3054
- import { isAbsolute, resolve as resolve3, sep as sep4 } from "node:path";
3408
+ import { isAbsolute, resolve as resolve3, sep as sep5 } from "node:path";
3055
3409
 
3056
3410
  // src/commands.push.recovery.redact.ts
3057
3411
  init_config();
3058
3412
  init_config_sharedDirs_guard();
3059
- import { cpSync as cpSync5, existsSync as existsSync24, mkdirSync as mkdirSync6, statSync as statSync7 } from "node:fs";
3060
- import { dirname as dirname6, join as join29, sep as sep3 } from "node:path";
3413
+ import { cpSync as cpSync5, existsSync as existsSync26, mkdirSync as mkdirSync6, statSync as statSync7 } from "node:fs";
3414
+ import { dirname as dirname6, join as join31, sep as sep4 } from "node:path";
3061
3415
 
3062
3416
  // src/commands.redact.ts
3063
3417
  init_config();
3064
- import { existsSync as existsSync23, statSync as statSync6 } from "node:fs";
3065
- import { dirname as dirname5, join as join28 } from "node:path";
3418
+ import { existsSync as existsSync25, statSync as statSync6 } from "node:fs";
3419
+ import { dirname as dirname5, join as join30 } from "node:path";
3066
3420
 
3067
3421
  // src/commands.redact.subtree.ts
3068
- import { existsSync as existsSync22, lstatSync as lstatSync7, readFileSync as readFileSync10, readdirSync as readdirSync9, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
3069
- import { join as join26 } from "node:path";
3422
+ import { existsSync as existsSync24, lstatSync as lstatSync8, readFileSync as readFileSync11, readdirSync as readdirSync9, statSync as statSync5, writeFileSync as writeFileSync4 } from "node:fs";
3423
+ import { join as join29 } from "node:path";
3070
3424
  init_utils_fs();
3425
+ init_utils();
3071
3426
  function collectFiles(dir, out) {
3072
- if (!existsSync22(dir)) return;
3073
- const st = lstatSync7(dir);
3427
+ if (!existsSync24(dir)) return;
3428
+ const st = lstatSync8(dir);
3074
3429
  if (!st.isDirectory()) return;
3075
3430
  for (const entry of readdirSync9(dir)) {
3076
- const abs = join26(dir, entry);
3077
- const lst = lstatSync7(abs);
3431
+ const abs = join29(dir, entry);
3432
+ const lst = lstatSync8(abs);
3078
3433
  if (lst.isSymbolicLink()) continue;
3079
3434
  if (lst.isDirectory()) {
3080
3435
  collectFiles(abs, out);
@@ -3110,143 +3465,83 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
3110
3465
  if (!dryRun && total > 0) {
3111
3466
  for (const { path: filePath, findings } of dirty) {
3112
3467
  backupBeforeWrite(filePath, ts);
3113
- writeFileSync3(filePath, applyRedactions(readFileSync10(filePath, "utf8"), findings), "utf8");
3468
+ const before = readFileSync11(filePath, "utf8");
3469
+ const after = applyRedactions(before, findings);
3470
+ if (after === before) {
3471
+ log(
3472
+ `warning: no redaction applied to ${filePath}: finding match values were not located in the file. Inspect it manually; the push re-scan still blocks a real leak.`
3473
+ );
3474
+ }
3475
+ writeFileSync4(filePath, after, "utf8");
3114
3476
  }
3115
3477
  }
3116
3478
  return { total, dirty };
3117
3479
  }
3118
3480
 
3119
- // src/commands.redact.ts
3120
- init_push_gitleaks_scan();
3121
- init_utils_fs();
3122
- init_utils_json();
3123
- init_utils();
3124
-
3125
- // src/utils.lockfile.ts
3126
- init_config();
3481
+ // src/commands.pushed-history.ts
3127
3482
  init_utils();
3128
- import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync11, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
3129
- import { dirname as dirname4, join as join27 } from "node:path";
3130
- function lockFilePath() {
3131
- return join27(home(), ".cache", "claude-nomad", "nomad.lock");
3132
- }
3133
- function acquireLock(verb) {
3134
- const lp = lockFilePath();
3135
- mkdirSync5(dirname4(lp), { recursive: true });
3136
- try {
3137
- const fd = openSync3(lp, "wx");
3138
- try {
3139
- writeFileSync4(fd, String(process.pid));
3140
- } catch (writeErr) {
3141
- try {
3142
- closeSync3(fd);
3143
- } catch {
3144
- }
3145
- try {
3146
- unlinkSync(lp);
3147
- } catch {
3148
- }
3149
- throw writeErr;
3150
- }
3151
- return { fd, path: lp };
3152
- } catch (err) {
3153
- const code = err.code;
3154
- if (code !== "EEXIST") throw err;
3155
- return checkStaleAndRetry(verb, lp);
3156
- }
3157
- }
3158
- function releaseLock(handle) {
3159
- if (handle === null) return;
3160
- const lp = handle.path;
3483
+ import { execFileSync as execFileSync8 } from "node:child_process";
3484
+ function pushedRef(repo) {
3161
3485
  try {
3162
- closeSync3(handle.fd);
3486
+ const ref = execFileSync8("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
3487
+ cwd: repo,
3488
+ stdio: ["ignore", "pipe", "ignore"]
3489
+ }).toString().trim();
3490
+ return ref.length > 0 ? ref : null;
3163
3491
  } catch {
3164
- }
3165
- try {
3166
- unlinkSync(lp);
3167
- } catch (err) {
3168
- if (err.code !== "ENOENT") throw err;
3492
+ return null;
3169
3493
  }
3170
3494
  }
3171
- function unlinkIfSamePid(expectedPidStr, lp) {
3172
- let current;
3173
- try {
3174
- current = readFileSync11(lp, "utf8").trim();
3175
- } catch {
3176
- return false;
3177
- }
3178
- if (current !== expectedPidStr) return false;
3495
+ function sessionInPushedHistory(id, repo) {
3496
+ const ref = pushedRef(repo);
3497
+ if (ref === null) return false;
3179
3498
  try {
3180
- unlinkSync(lp);
3181
- return true;
3499
+ const out = execFileSync8(
3500
+ "git",
3501
+ [
3502
+ "log",
3503
+ ref,
3504
+ "--oneline",
3505
+ "-1",
3506
+ "--",
3507
+ `shared/projects/*/${id}.jsonl`,
3508
+ `shared/projects/*/${id}/*`
3509
+ ],
3510
+ { cwd: repo, stdio: ["ignore", "pipe", "ignore"] }
3511
+ ).toString().trim();
3512
+ return out.length > 0;
3182
3513
  } catch {
3183
3514
  return false;
3184
3515
  }
3185
3516
  }
3186
- function checkStaleAndRetry(verb, lp) {
3187
- let pidStr;
3188
- try {
3189
- pidStr = readFileSync11(lp, "utf8").trim();
3190
- } catch {
3191
- pidStr = "";
3192
- }
3193
- const pid = Number.parseInt(pidStr, 10);
3194
- if (!Number.isFinite(pid) || pid <= 0) {
3195
- if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
3196
- warn(`another nomad ${verb} running, skipping`);
3197
- return null;
3198
- }
3199
- try {
3200
- process.kill(pid, 0);
3201
- warn(`another nomad ${verb} running, skipping`);
3202
- return null;
3203
- } catch (err) {
3204
- const code = err.code;
3205
- if (code === "ESRCH") {
3206
- if (unlinkIfSamePid(pidStr, lp)) return retryOnce(verb, lp);
3207
- warn(`another nomad ${verb} running, skipping`);
3208
- return null;
3209
- }
3210
- warn(`another nomad ${verb} running, skipping`);
3211
- return null;
3212
- }
3213
- }
3214
- function retryOnce(verb, lp) {
3215
- try {
3216
- const fd = openSync3(lp, "wx");
3217
- try {
3218
- writeFileSync4(fd, String(process.pid));
3219
- } catch {
3220
- try {
3221
- closeSync3(fd);
3222
- } catch {
3223
- }
3224
- try {
3225
- unlinkSync(lp);
3226
- } catch {
3227
- }
3228
- warn(`another nomad ${verb} running, skipping`);
3229
- return null;
3230
- }
3231
- return { fd, path: lp };
3232
- } catch {
3233
- warn(`another nomad ${verb} running, skipping`);
3234
- return null;
3235
- }
3517
+ function warnIfSessionPushed(id, repo) {
3518
+ if (!sessionInPushedHistory(id, repo)) return;
3519
+ log(
3520
+ `warning: session ${id} is already in pushed history (origin).
3521
+ This command only changes your local copy and the next push; it does NOT
3522
+ remove the secret from commits already on the remote.
3523
+ To fully remediate a real secret: rotate the credential, then rewrite
3524
+ history (e.g. with git filter-repo) and force-push, coordinating with
3525
+ anyone else who has cloned the repo.`
3526
+ );
3236
3527
  }
3237
3528
 
3238
3529
  // src/commands.redact.ts
3530
+ init_push_gitleaks_scan();
3531
+ init_utils_fs();
3532
+ init_utils_json();
3533
+ init_utils();
3239
3534
  function resolveLiveTranscript(id) {
3240
3535
  try {
3241
- const mapPath = join28(repoHome(), "path-map.json");
3242
- if (!existsSync23(mapPath)) return null;
3536
+ const mapPath = join30(repoHome(), "path-map.json");
3537
+ if (!existsSync25(mapPath)) return null;
3243
3538
  const projects = readJson(mapPath).projects;
3244
3539
  const claude = claudeHome();
3245
3540
  for (const hostMap of Object.values(projects)) {
3246
3541
  const abs = hostMap[HOST];
3247
3542
  if (abs === void 0) continue;
3248
- const live = join28(claude, "projects", encodePath(abs), `${id}.jsonl`);
3249
- if (existsSync23(live)) return live;
3543
+ const live = join30(claude, "projects", encodePath(abs), `${id}.jsonl`);
3544
+ if (existsSync25(live)) return live;
3250
3545
  }
3251
3546
  return null;
3252
3547
  } catch {
@@ -3266,17 +3561,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
3266
3561
  }
3267
3562
  const repo = repoHome();
3268
3563
  const backup = backupBase();
3269
- if (!existsSync23(repo)) die(`repo not cloned at ${repo}`);
3564
+ if (!existsSync25(repo)) die(`repo not cloned at ${repo}`);
3270
3565
  const handle = acquireLock("redact");
3271
3566
  if (handle === null) process.exit(0);
3272
3567
  try {
3273
3568
  const localPath = resolveLiveTranscript(id);
3274
- if (localPath === null || !existsSync23(localPath)) {
3569
+ if (localPath === null || !existsSync25(localPath)) {
3275
3570
  fail(`could not resolve local transcript for session ${id} on this host`);
3276
3571
  process.exitCode = 1;
3277
3572
  return;
3278
3573
  }
3279
- const sessionDir = join28(dirname5(localPath), id);
3574
+ const sessionDir = join30(dirname5(localPath), id);
3280
3575
  const subtreeFiles = listSubtreeFiles(sessionDir);
3281
3576
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync6(p).mtimeMs);
3282
3577
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -3317,6 +3612,7 @@ ${lines}`);
3317
3612
  return;
3318
3613
  }
3319
3614
  log(`redacted ${totalCount} finding(s) in ${localPath} (backup: ${ts})`);
3615
+ warnIfSessionPushed(id, repo);
3320
3616
  } catch (err) {
3321
3617
  if (!(err instanceof NomadFatal)) {
3322
3618
  throw err;
@@ -3391,12 +3687,28 @@ function resolveStagedDir(localPath, map, claude, repo) {
3391
3687
  assertSafeLogical(logical);
3392
3688
  const abs = hostMap[HOST];
3393
3689
  if (abs === void 0) continue;
3394
- if (localPath.startsWith(join29(claude, "projects", encodePath(abs)) + sep3)) {
3395
- return join29(repo, "shared", "projects", logical);
3690
+ if (localPath.startsWith(join31(claude, "projects", encodePath(abs)) + sep4)) {
3691
+ return join31(repo, "shared", "projects", logical);
3396
3692
  }
3397
3693
  }
3398
3694
  return null;
3399
3695
  }
3696
+ function preflightRedactable(f, map, nowMs) {
3697
+ const sid = sessionIdFromFinding(f);
3698
+ if (sid === null) return "a finding has no resolvable session id (not a session transcript)";
3699
+ const localPath = resolveLiveTranscript(sid);
3700
+ if (localPath === null) return `session ${sid}: local transcript not found`;
3701
+ const sessionDir = join31(dirname6(localPath), sid);
3702
+ const subtreeFiles = listSubtreeFiles(sessionDir);
3703
+ const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
3704
+ if (isRecentlyModified(subtreeMtime, nowMs())) {
3705
+ return `session ${sid}: looks active (modified within the last 5 minutes)`;
3706
+ }
3707
+ if (resolveStagedDir(localPath, map, claudeHome(), repoHome()) === null) {
3708
+ return `session ${sid}: not mapped to a staged copy`;
3709
+ }
3710
+ return null;
3711
+ }
3400
3712
  function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3401
3713
  const refuse = (msg) => {
3402
3714
  log(msg);
@@ -3416,7 +3728,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3416
3728
  `could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
3417
3729
  );
3418
3730
  }
3419
- const sessionDir = join29(dirname6(localPath), sid);
3731
+ const sessionDir = join31(dirname6(localPath), sid);
3420
3732
  const subtreeFiles = listSubtreeFiles(sessionDir);
3421
3733
  const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
3422
3734
  if (isRecentlyModified(subtreeMtime, nowMs())) {
@@ -3450,26 +3762,26 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
3450
3762
  );
3451
3763
  }
3452
3764
  mkdirSync6(stagedProjectDir, { recursive: true });
3453
- cpSync5(localPath, join29(stagedProjectDir, `${sid}.jsonl`), { force: true });
3454
- if (existsSync24(sessionDir)) {
3455
- cpSync5(sessionDir, join29(stagedProjectDir, sid), { force: true, recursive: true });
3765
+ cpSync5(localPath, join31(stagedProjectDir, `${sid}.jsonl`), { force: true });
3766
+ if (existsSync26(sessionDir)) {
3767
+ cpSync5(sessionDir, join31(stagedProjectDir, sid), { force: true, recursive: true });
3456
3768
  }
3457
3769
  return true;
3458
3770
  }
3459
3771
 
3460
3772
  // src/commands.push.recovery.drop.ts
3461
3773
  init_config();
3462
- import { rmSync as rmSync8 } from "node:fs";
3463
- import { join as join30 } from "node:path";
3774
+ import { rmSync as rmSync9 } from "node:fs";
3775
+ import { join as join32 } from "node:path";
3464
3776
  function dropSessionFromStaged(sid, map) {
3465
3777
  const logicals = Object.keys(map.projects);
3466
3778
  if (logicals.length === 0) return false;
3467
3779
  const repo = repoHome();
3468
3780
  for (const logical of logicals) {
3469
- const jsonl = join30(repo, "shared", "projects", logical, `${sid}.jsonl`);
3470
- const dir = join30(repo, "shared", "projects", logical, sid);
3471
- rmSync8(jsonl, { force: true });
3472
- rmSync8(dir, { recursive: true, force: true });
3781
+ const jsonl = join32(repo, "shared", "projects", logical, `${sid}.jsonl`);
3782
+ const dir = join32(repo, "shared", "projects", logical, sid);
3783
+ rmSync9(jsonl, { force: true });
3784
+ rmSync9(dir, { recursive: true, force: true });
3473
3785
  }
3474
3786
  return true;
3475
3787
  }
@@ -3500,7 +3812,7 @@ function makeDefaultReadLine(repo) {
3500
3812
  try {
3501
3813
  const repoRoot = resolve3(repo);
3502
3814
  const target = resolve3(repoRoot, file);
3503
- if (isAbsolute(file) || target !== repoRoot && !target.startsWith(repoRoot + sep4)) {
3815
+ if (isAbsolute(file) || target !== repoRoot && !target.startsWith(repoRoot + sep5)) {
3504
3816
  return null;
3505
3817
  }
3506
3818
  const content = readFileSync12(target, "utf8");
@@ -3567,6 +3879,22 @@ function dispatchActions(findings, actions, opts) {
3567
3879
  }
3568
3880
  }
3569
3881
  function redactAllFindings(findings, ts, map, nowMs, scan = scanFile) {
3882
+ const refusals = [];
3883
+ const preflighted = /* @__PURE__ */ new Set();
3884
+ for (const f of findings) {
3885
+ const dedupeKey = sessionIdFromFinding(f) ?? findingKey(f);
3886
+ if (preflighted.has(dedupeKey)) continue;
3887
+ preflighted.add(dedupeKey);
3888
+ const reason = preflightRedactable(f, map, nowMs);
3889
+ if (reason !== null) refusals.push(reason);
3890
+ }
3891
+ if (refusals.length > 0) {
3892
+ throw new NomadFatal(
3893
+ `--redact-all cannot redact every finding, so no changes were made:
3894
+ ` + refusals.map((r) => ` - ${r}`).join("\n") + `
3895
+ Re-run without --redact-all to triage these interactively (Drop session / Skip), or end any active session and retry.`
3896
+ );
3897
+ }
3570
3898
  const redactedSids = /* @__PURE__ */ new Set();
3571
3899
  for (const f of findings) {
3572
3900
  const sid = sessionIdFromFinding(f);
@@ -3608,7 +3936,7 @@ function applyThenRescan(scanVerdict, repoHome2) {
3608
3936
  return next;
3609
3937
  }
3610
3938
  function allowThenRescan(append, scanVerdict, repoHome2) {
3611
- const ignPath = join31(repoHome2, ".gitleaksignore");
3939
+ const ignPath = join33(repoHome2, ".gitleaksignore");
3612
3940
  let before;
3613
3941
  try {
3614
3942
  before = readFileSync13(ignPath, "utf8");
@@ -3619,14 +3947,14 @@ function allowThenRescan(append, scanVerdict, repoHome2) {
3619
3947
  try {
3620
3948
  return applyThenRescan(scanVerdict, repoHome2);
3621
3949
  } catch (err) {
3622
- if (before === null) rmSync9(ignPath, { force: true });
3950
+ if (before === null) rmSync10(ignPath, { force: true });
3623
3951
  else writeFileSync5(ignPath, before, "utf8");
3624
3952
  throw err;
3625
3953
  }
3626
3954
  }
3627
3955
  function makeRealPrompt() {
3628
3956
  return async (prompt) => {
3629
- const rl = createInterface({
3957
+ const rl = createInterface2({
3630
3958
  input: process.stdin,
3631
3959
  output: process.stdout,
3632
3960
  terminal: true
@@ -3707,7 +4035,7 @@ function writeAnimatedDone(out, label, ms, useTTY) {
3707
4035
  `);
3708
4036
  }
3709
4037
  function resolveWorkerPath(deps = {}) {
3710
- const check = deps.existsSyncFn ?? existsSync25;
4038
+ const check = deps.existsSyncFn ?? existsSync27;
3711
4039
  const base = deps.baseUrl ?? import.meta.url;
3712
4040
  const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
3713
4041
  if (check(mjs)) return mjs;
@@ -3773,9 +4101,9 @@ function withSpinner(label, fn, deps) {
3773
4101
 
3774
4102
  // src/commands.doctor.gitleaks-version.ts
3775
4103
  init_color();
3776
- import { execFileSync as execFileSync8 } from "node:child_process";
3777
- import { existsSync as existsSync26 } from "node:fs";
3778
- import { join as join32 } from "node:path";
4104
+ import { execFileSync as execFileSync9 } from "node:child_process";
4105
+ import { existsSync as existsSync28 } from "node:fs";
4106
+ import { join as join34 } from "node:path";
3779
4107
  init_config();
3780
4108
  var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
3781
4109
  var GITLEAKS_TIMEOUT_MS = 5e3;
@@ -3784,7 +4112,7 @@ function majorMinorOf(value) {
3784
4112
  return m === null ? null : [m[1], m[2]];
3785
4113
  }
3786
4114
  function readGitleaksVersion(run, tomlExists) {
3787
- const tomlPath = join32(repoHome(), ".gitleaks.toml");
4115
+ const tomlPath = join34(repoHome(), ".gitleaks.toml");
3788
4116
  const args = ["version"];
3789
4117
  if (tomlExists(tomlPath)) args.push("--config", tomlPath);
3790
4118
  try {
@@ -3796,7 +4124,7 @@ function readGitleaksVersion(run, tomlExists) {
3796
4124
  return null;
3797
4125
  }
3798
4126
  }
3799
- function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists = existsSync26) {
4127
+ function reportGitleaksVersionCheck(section2, run = execFileSync9, tomlExists = existsSync28) {
3800
4128
  const raw = readGitleaksVersion(run, tomlExists);
3801
4129
  if (raw === null) return;
3802
4130
  const local = majorMinorOf(raw);
@@ -3816,7 +4144,7 @@ function reportGitleaksVersionCheck(section2, run = execFileSync8, tomlExists =
3816
4144
 
3817
4145
  // src/commands.doctor.checks.deps.ts
3818
4146
  init_color();
3819
- import { execFileSync as execFileSync9 } from "node:child_process";
4147
+ import { execFileSync as execFileSync10 } from "node:child_process";
3820
4148
  var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
3821
4149
  var PROBE_TIMEOUT_MS = 3e3;
3822
4150
  var FETCHER_BASE = "HTTP fetcher";
@@ -3853,7 +4181,7 @@ function reportFetcherRow(section2, run) {
3853
4181
  );
3854
4182
  }
3855
4183
  }
3856
- function reportOptionalDeps(section2, run = execFileSync9) {
4184
+ function reportOptionalDeps(section2, run = execFileSync10) {
3857
4185
  const gh = probeOptionalDep("gh", run);
3858
4186
  if (gh.status === "present") {
3859
4187
  addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
@@ -3868,11 +4196,11 @@ function reportOptionalDeps(section2, run = execFileSync9) {
3868
4196
 
3869
4197
  // src/commands.doctor.actions-drift.ts
3870
4198
  init_color();
3871
- import { execFileSync as execFileSync11 } from "node:child_process";
4199
+ import { execFileSync as execFileSync12 } from "node:child_process";
3872
4200
  init_config();
3873
4201
 
3874
4202
  // src/gh-actions.ts
3875
- import { execFileSync as execFileSync10 } from "node:child_process";
4203
+ import { execFileSync as execFileSync11 } from "node:child_process";
3876
4204
  var GH_TIMEOUT_MS = 5e3;
3877
4205
  function parseGitHubRemote(remoteUrl) {
3878
4206
  const normalized = remoteUrl.trim().replace(/\/$/, "");
@@ -3880,7 +4208,7 @@ function parseGitHubRemote(remoteUrl) {
3880
4208
  if (m === null) return null;
3881
4209
  return { owner: m[1], repo: m[2] };
3882
4210
  }
3883
- function ghAuthStatus(run = execFileSync10) {
4211
+ function ghAuthStatus(run = execFileSync11) {
3884
4212
  try {
3885
4213
  run("gh", ["auth", "status"], {
3886
4214
  stdio: ["ignore", "ignore", "ignore"],
@@ -3894,7 +4222,7 @@ function ghAuthStatus(run = execFileSync10) {
3894
4222
  return "gh-probe-error";
3895
4223
  }
3896
4224
  }
3897
- function isRepoPrivate(ref, run = execFileSync10) {
4225
+ function isRepoPrivate(ref, run = execFileSync11) {
3898
4226
  const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
3899
4227
  stdio: ["ignore", "pipe", "ignore"],
3900
4228
  timeout: GH_TIMEOUT_MS
@@ -3902,7 +4230,7 @@ function isRepoPrivate(ref, run = execFileSync10) {
3902
4230
  const parsed = JSON.parse(out);
3903
4231
  return parsed.isPrivate === true;
3904
4232
  }
3905
- function isActionsEnabled(ref, run = execFileSync10) {
4233
+ function isActionsEnabled(ref, run = execFileSync11) {
3906
4234
  const out = run(
3907
4235
  "gh",
3908
4236
  ["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
@@ -3910,7 +4238,7 @@ function isActionsEnabled(ref, run = execFileSync10) {
3910
4238
  ).toString().trim();
3911
4239
  return out === "true";
3912
4240
  }
3913
- function disableActions(ref, run = execFileSync10) {
4241
+ function disableActions(ref, run = execFileSync11) {
3914
4242
  run(
3915
4243
  "gh",
3916
4244
  [
@@ -3924,7 +4252,7 @@ function disableActions(ref, run = execFileSync10) {
3924
4252
  { stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
3925
4253
  );
3926
4254
  }
3927
- function readOriginRemote(cwd, run = execFileSync10) {
4255
+ function readOriginRemote(cwd, run = execFileSync11) {
3928
4256
  return run("git", ["remote", "get-url", "origin"], {
3929
4257
  cwd,
3930
4258
  stdio: ["ignore", "pipe", "ignore"]
@@ -3932,7 +4260,7 @@ function readOriginRemote(cwd, run = execFileSync10) {
3932
4260
  }
3933
4261
 
3934
4262
  // src/commands.doctor.actions-drift.ts
3935
- function reportActionsDrift(section2, run = execFileSync11) {
4263
+ function reportActionsDrift(section2, run = execFileSync12) {
3936
4264
  let remote;
3937
4265
  try {
3938
4266
  remote = readOriginRemote(repoHome(), run);
@@ -3993,8 +4321,8 @@ function gatherDoctorSections(opts) {
3993
4321
  reportHostAndPaths(host);
3994
4322
  reportRepoState(host);
3995
4323
  const links = section("Shared links");
3996
- const mapPath = join33(repoHome(), "path-map.json");
3997
- const rawMap = existsSync27(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
4324
+ const mapPath = join35(repoHome(), "path-map.json");
4325
+ const rawMap = existsSync29(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
3998
4326
  const map = rawMap ?? { projects: {} };
3999
4327
  reportSharedLinks(links, map);
4000
4328
  reportDroppedNamesMigration(links);
@@ -4061,15 +4389,15 @@ function cmdDoctor(opts = {}) {
4061
4389
 
4062
4390
  // src/commands.drop-session.ts
4063
4391
  init_config();
4064
- import { execFileSync as execFileSync13 } from "node:child_process";
4065
- import { existsSync as existsSync29, readdirSync as readdirSync10, statSync as statSync8 } from "node:fs";
4066
- import { join as join35, relative as relative4 } from "node:path";
4392
+ import { execFileSync as execFileSync14 } from "node:child_process";
4393
+ import { existsSync as existsSync31, readdirSync as readdirSync10, statSync as statSync8 } from "node:fs";
4394
+ import { join as join37, relative as relative4 } from "node:path";
4067
4395
 
4068
4396
  // src/commands.drop-session.git.ts
4069
- import { execFileSync as execFileSync12 } from "node:child_process";
4397
+ import { execFileSync as execFileSync13 } from "node:child_process";
4070
4398
  function expandStagedDir(dirRel, repo) {
4071
4399
  try {
4072
- const out = execFileSync12("git", ["ls-files", "-z", "--", dirRel], {
4400
+ const out = execFileSync13("git", ["ls-files", "-z", "--", dirRel], {
4073
4401
  cwd: repo,
4074
4402
  stdio: ["ignore", "pipe", "pipe"]
4075
4403
  });
@@ -4080,7 +4408,7 @@ function expandStagedDir(dirRel, repo) {
4080
4408
  }
4081
4409
  function isTrackedInHead(rel, repo) {
4082
4410
  try {
4083
- execFileSync12("git", ["cat-file", "-e", `HEAD:${rel}`], {
4411
+ execFileSync13("git", ["cat-file", "-e", `HEAD:${rel}`], {
4084
4412
  cwd: repo,
4085
4413
  stdio: ["ignore", "pipe", "pipe"]
4086
4414
  });
@@ -4091,7 +4419,7 @@ function isTrackedInHead(rel, repo) {
4091
4419
  }
4092
4420
  function isInIndex(rel, repo) {
4093
4421
  try {
4094
- const out = execFileSync12("git", ["ls-files", "--", rel], {
4422
+ const out = execFileSync13("git", ["ls-files", "--", rel], {
4095
4423
  cwd: repo,
4096
4424
  stdio: ["ignore", "pipe", "pipe"]
4097
4425
  });
@@ -4105,8 +4433,8 @@ function isInIndex(rel, repo) {
4105
4433
  init_config();
4106
4434
  init_utils();
4107
4435
  init_utils_json();
4108
- import { existsSync as existsSync28 } from "node:fs";
4109
- import { join as join34 } from "node:path";
4436
+ import { existsSync as existsSync30 } from "node:fs";
4437
+ import { join as join36 } from "node:path";
4110
4438
  var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
4111
4439
  function reportScrubHint(id, matches) {
4112
4440
  const live = resolveLiveTranscript2(id, matches);
@@ -4122,8 +4450,8 @@ function reportScrubHint(id, matches) {
4122
4450
  }
4123
4451
  function resolveLiveTranscript2(id, matches) {
4124
4452
  try {
4125
- const mapPath = join34(repoHome(), "path-map.json");
4126
- if (!existsSync28(mapPath)) return null;
4453
+ const mapPath = join36(repoHome(), "path-map.json");
4454
+ if (!existsSync30(mapPath)) return null;
4127
4455
  const projects = readJson(mapPath).projects;
4128
4456
  const claude = claudeHome();
4129
4457
  for (const rel of matches) {
@@ -4131,8 +4459,8 @@ function resolveLiveTranscript2(id, matches) {
4131
4459
  if (logical === void 0) continue;
4132
4460
  const abs = projects[logical]?.[HOST];
4133
4461
  if (abs === void 0) continue;
4134
- const live = join34(claude, "projects", encodePath(abs), `${id}.jsonl`);
4135
- if (existsSync28(live)) return live;
4462
+ const live = join36(claude, "projects", encodePath(abs), `${id}.jsonl`);
4463
+ if (existsSync30(live)) return live;
4136
4464
  }
4137
4465
  return null;
4138
4466
  } catch {
@@ -4148,12 +4476,12 @@ function cmdDropSession(id) {
4148
4476
  process.exit(1);
4149
4477
  }
4150
4478
  const repo = repoHome();
4151
- if (!existsSync29(repo)) die(`repo not cloned at ${repo}`);
4479
+ if (!existsSync31(repo)) die(`repo not cloned at ${repo}`);
4152
4480
  const handle = acquireLock("drop-session");
4153
4481
  if (handle === null) process.exit(0);
4154
4482
  try {
4155
- const repoProjects = join35(repo, "shared", "projects");
4156
- if (!existsSync29(repoProjects)) {
4483
+ const repoProjects = join37(repo, "shared", "projects");
4484
+ if (!existsSync31(repoProjects)) {
4157
4485
  throw new NomadFatal(`no staged session matches ${id}`);
4158
4486
  }
4159
4487
  const matches = collectMatches(repoProjects, id, repo);
@@ -4162,6 +4490,7 @@ function cmdDropSession(id) {
4162
4490
  }
4163
4491
  for (const rel of matches) unstageOne(rel, repo);
4164
4492
  reportScrubHint(id, matches);
4493
+ warnIfSessionPushed(id, repo);
4165
4494
  } catch (err) {
4166
4495
  if (!(err instanceof NomadFatal)) {
4167
4496
  throw err;
@@ -4175,12 +4504,12 @@ function cmdDropSession(id) {
4175
4504
  function collectMatches(repoProjects, id, repo) {
4176
4505
  const matches = [];
4177
4506
  for (const logical of readdirSync10(repoProjects)) {
4178
- const candidate = join35(repoProjects, logical, `${id}.jsonl`);
4179
- if (existsSync29(candidate)) {
4507
+ const candidate = join37(repoProjects, logical, `${id}.jsonl`);
4508
+ if (existsSync31(candidate)) {
4180
4509
  matches.push(relative4(repo, candidate));
4181
4510
  }
4182
- const dir = join35(repoProjects, logical, id);
4183
- if (existsSync29(dir) && statSync8(dir).isDirectory()) {
4511
+ const dir = join37(repoProjects, logical, id);
4512
+ if (existsSync31(dir) && statSync8(dir).isDirectory()) {
4184
4513
  const dirRel = relative4(repo, dir);
4185
4514
  const staged = expandStagedDir(dirRel, repo);
4186
4515
  if (staged.length > 0) matches.push(...staged);
@@ -4196,12 +4525,12 @@ function unstageOne(rel, repo) {
4196
4525
  }
4197
4526
  try {
4198
4527
  if (isTrackedInHead(rel, repo)) {
4199
- execFileSync13("git", ["restore", "--staged", "--worktree", "--", rel], {
4528
+ execFileSync14("git", ["restore", "--staged", "--worktree", "--", rel], {
4200
4529
  cwd: repo,
4201
4530
  stdio: ["ignore", "pipe", "pipe"]
4202
4531
  });
4203
4532
  } else {
4204
- execFileSync13("git", ["rm", "--cached", "-f", "--", rel], {
4533
+ execFileSync14("git", ["rm", "--cached", "-f", "--", rel], {
4205
4534
  cwd: repo,
4206
4535
  stdio: ["ignore", "pipe", "pipe"]
4207
4536
  });
@@ -4215,8 +4544,8 @@ function unstageOne(rel, repo) {
4215
4544
  }
4216
4545
 
4217
4546
  // src/commands.pull.ts
4218
- import { existsSync as existsSync36, mkdirSync as mkdirSync9 } from "node:fs";
4219
- import { join as join43 } from "node:path";
4547
+ import { existsSync as existsSync37, mkdirSync as mkdirSync9 } from "node:fs";
4548
+ import { join as join44 } from "node:path";
4220
4549
 
4221
4550
  // src/commands.push.sections.ts
4222
4551
  init_color();
@@ -4311,12 +4640,12 @@ init_config();
4311
4640
 
4312
4641
  // src/extras-sync.ts
4313
4642
  init_config();
4314
- import { existsSync as existsSync32 } from "node:fs";
4315
- import { join as join39 } from "node:path";
4643
+ import { existsSync as existsSync34 } from "node:fs";
4644
+ import { join as join41 } from "node:path";
4316
4645
 
4317
4646
  // src/extras-sync.diff.ts
4318
4647
  init_utils();
4319
- import { execFileSync as execFileSync14 } from "node:child_process";
4648
+ import { execFileSync as execFileSync15 } from "node:child_process";
4320
4649
  function labelDiffLine(line) {
4321
4650
  const tab = line.indexOf(" ");
4322
4651
  if (tab === -1) return line;
@@ -4331,7 +4660,7 @@ function parseDiffOutput(stdout) {
4331
4660
  }
4332
4661
  function listDivergingFiles(a, b) {
4333
4662
  try {
4334
- const stdout = execFileSync14("git", ["diff", "--no-index", "--name-status", a, b], {
4663
+ const stdout = execFileSync15("git", ["diff", "--no-index", "--name-status", a, b], {
4335
4664
  stdio: ["ignore", "pipe", "pipe"]
4336
4665
  }).toString();
4337
4666
  return parseDiffOutput(stdout);
@@ -4351,8 +4680,8 @@ function listDivergingFiles(a, b) {
4351
4680
 
4352
4681
  // src/extras-sync.core.ts
4353
4682
  init_config();
4354
- import { cpSync as cpSync6, existsSync as existsSync30, lstatSync as lstatSync8, readdirSync as readdirSync11, rmSync as rmSync10 } from "node:fs";
4355
- import { basename, join as join36 } from "node:path";
4683
+ import { cpSync as cpSync6, existsSync as existsSync32, lstatSync as lstatSync9, readdirSync as readdirSync11, rmSync as rmSync11 } from "node:fs";
4684
+ import { basename, join as join38 } from "node:path";
4356
4685
 
4357
4686
  // src/extras-sync.guards.ts
4358
4687
  init_utils();
@@ -4376,9 +4705,9 @@ init_utils();
4376
4705
  init_utils_json();
4377
4706
  function loadValidatedExtras(opts) {
4378
4707
  const repo = repoHome();
4379
- const mapPath = join36(repo, "path-map.json");
4380
- const repoExtras = join36(repo, "shared", "extras");
4381
- if (!existsSync30(mapPath) || opts.requireRepoExtras === true && !existsSync30(repoExtras)) {
4708
+ const mapPath = join38(repo, "path-map.json");
4709
+ const repoExtras = join38(repo, "shared", "extras");
4710
+ if (!existsSync32(mapPath) || opts.requireRepoExtras === true && !existsSync32(repoExtras)) {
4382
4711
  if (opts.missingMsg !== void 0) log(opts.missingMsg);
4383
4712
  return null;
4384
4713
  }
@@ -4428,14 +4757,14 @@ function copyExtrasOverlayFiltered(src, dst, blockSet) {
4428
4757
  }
4429
4758
  }
4430
4759
  function copyExtras(src, dst) {
4431
- rmSync10(dst, { recursive: true, force: true });
4760
+ rmSync11(dst, { recursive: true, force: true });
4432
4761
  cpSync6(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
4433
4762
  }
4434
4763
  function extrasDenySet(dirname8) {
4435
4764
  return dirname8 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
4436
4765
  }
4437
4766
  function copyExtrasFiltered(src, dst, blockSet) {
4438
- rmSync10(dst, { recursive: true, force: true });
4767
+ rmSync11(dst, { recursive: true, force: true });
4439
4768
  cpSync6(src, dst, {
4440
4769
  recursive: true,
4441
4770
  force: true,
@@ -4446,25 +4775,25 @@ function copyExtrasFiltered(src, dst, blockSet) {
4446
4775
  function prunePreservingDenied(src, dst, blockSet) {
4447
4776
  for (const name of readdirSync11(dst)) {
4448
4777
  if (blockSet.has(name)) continue;
4449
- const dstPath = join36(dst, name);
4450
- const srcStat = lstatSync8(join36(src, name), { throwIfNoEntry: false });
4778
+ const dstPath = join38(dst, name);
4779
+ const srcStat = lstatSync9(join38(src, name), { throwIfNoEntry: false });
4451
4780
  if (srcStat === void 0) {
4452
- rmSync10(dstPath, { recursive: true, force: true });
4781
+ rmSync11(dstPath, { recursive: true, force: true });
4453
4782
  continue;
4454
4783
  }
4455
- const dstStat = lstatSync8(dstPath);
4784
+ const dstStat = lstatSync9(dstPath);
4456
4785
  if (srcStat.isDirectory() && dstStat.isDirectory()) {
4457
- prunePreservingDenied(join36(src, name), dstPath, blockSet);
4786
+ prunePreservingDenied(join38(src, name), dstPath, blockSet);
4458
4787
  } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
4459
- rmSync10(dstPath, { recursive: true, force: true });
4788
+ rmSync11(dstPath, { recursive: true, force: true });
4460
4789
  }
4461
4790
  }
4462
4791
  }
4463
4792
  function copyExtrasFilteredPreserving(src, dst, blockSet) {
4464
- const dstStat = lstatSync8(dst, { throwIfNoEntry: false });
4793
+ const dstStat = lstatSync9(dst, { throwIfNoEntry: false });
4465
4794
  if (dstStat !== void 0) {
4466
4795
  if (dstStat.isDirectory()) prunePreservingDenied(src, dst, blockSet);
4467
- else rmSync10(dst, { recursive: true, force: true });
4796
+ else rmSync11(dst, { recursive: true, force: true });
4468
4797
  }
4469
4798
  cpSync6(src, dst, {
4470
4799
  recursive: true,
@@ -4476,25 +4805,25 @@ function copyExtrasFilteredPreserving(src, dst, blockSet) {
4476
4805
  function prunePreservingBy(src, dst, isPreserved) {
4477
4806
  for (const name of readdirSync11(dst)) {
4478
4807
  if (isPreserved(name)) continue;
4479
- const dstPath = join36(dst, name);
4480
- const srcStat = lstatSync8(join36(src, name), { throwIfNoEntry: false });
4808
+ const dstPath = join38(dst, name);
4809
+ const srcStat = lstatSync9(join38(src, name), { throwIfNoEntry: false });
4481
4810
  if (srcStat === void 0) {
4482
- rmSync10(dstPath, { recursive: true, force: true });
4811
+ rmSync11(dstPath, { recursive: true, force: true });
4483
4812
  continue;
4484
4813
  }
4485
- const dstStat = lstatSync8(dstPath);
4814
+ const dstStat = lstatSync9(dstPath);
4486
4815
  if (srcStat.isDirectory() && dstStat.isDirectory()) {
4487
- prunePreservingBy(join36(src, name), dstPath, isPreserved);
4816
+ prunePreservingBy(join38(src, name), dstPath, isPreserved);
4488
4817
  } else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
4489
- rmSync10(dstPath, { recursive: true, force: true });
4818
+ rmSync11(dstPath, { recursive: true, force: true });
4490
4819
  }
4491
4820
  }
4492
4821
  }
4493
4822
  function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
4494
- const dstStat = lstatSync8(dst, { throwIfNoEntry: false });
4823
+ const dstStat = lstatSync9(dst, { throwIfNoEntry: false });
4495
4824
  if (dstStat !== void 0) {
4496
4825
  if (dstStat.isDirectory()) prunePreservingBy(src, dst, isPreserved);
4497
- else rmSync10(dst, { recursive: true, force: true });
4826
+ else rmSync11(dst, { recursive: true, force: true });
4498
4827
  }
4499
4828
  cpSync6(src, dst, {
4500
4829
  recursive: true,
@@ -4510,11 +4839,11 @@ init_utils_json();
4510
4839
 
4511
4840
  // src/extras-sync.remap.ts
4512
4841
  init_config();
4513
- import { existsSync as existsSync31, mkdirSync as mkdirSync7, readdirSync as readdirSync12, realpathSync as realpathSync4, rmSync as rmSync11 } from "node:fs";
4514
- import { dirname as dirname7, join as join38, sep as sep6 } from "node:path";
4842
+ import { existsSync as existsSync33, mkdirSync as mkdirSync7, readdirSync as readdirSync12, realpathSync as realpathSync4, rmSync as rmSync12 } from "node:fs";
4843
+ import { dirname as dirname7, join as join40, sep as sep7 } from "node:path";
4515
4844
 
4516
4845
  // src/extras-sync.planning-diff.ts
4517
- import { join as join37, normalize as normalize2, sep as sep5 } from "node:path";
4846
+ import { join as join39, normalize as normalize2, sep as sep6 } from "node:path";
4518
4847
  init_utils();
4519
4848
  function processRecord(fields, i, changed, deleted) {
4520
4849
  const status = fields[i];
@@ -4566,15 +4895,15 @@ function planningDeleteTargets(opts) {
4566
4895
  const { deleted } = parsePlanningDiff(raw);
4567
4896
  const logicalPrefix = "shared/extras/" + logical + "/";
4568
4897
  const prefix = logicalPrefix + ".planning/";
4569
- const planningRoot = join37(localRoot, ".planning");
4570
- const planningRootBoundary = planningRoot + sep5;
4898
+ const planningRoot = join39(localRoot, ".planning");
4899
+ const planningRootBoundary = planningRoot + sep6;
4571
4900
  const targets = [];
4572
4901
  for (const repoPath of deleted) {
4573
4902
  if (!repoPath.startsWith(prefix)) {
4574
4903
  continue;
4575
4904
  }
4576
4905
  const remainder = repoPath.slice(logicalPrefix.length);
4577
- const candidate = join37(localRoot, remainder);
4906
+ const candidate = join39(localRoot, remainder);
4578
4907
  const resolved = normalize2(candidate);
4579
4908
  if (!resolved.startsWith(planningRootBoundary)) {
4580
4909
  throw new NomadFatal(
@@ -4595,7 +4924,7 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
4595
4924
  const would = [];
4596
4925
  for (const t of eachExtrasTarget(v, counts)) {
4597
4926
  const { src, dst } = paths(t);
4598
- if (!existsSync31(src)) continue;
4927
+ if (!existsSync33(src)) continue;
4599
4928
  const item2 = `${t.logical}/${t.dirname}`;
4600
4929
  if (dryRun) {
4601
4930
  would.push(item2);
@@ -4609,10 +4938,10 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
4609
4938
  }
4610
4939
  function pruneEmptyAncestors(target, planningRoot) {
4611
4940
  let dir = dirname7(target);
4612
- while (dir !== planningRoot && dir.startsWith(planningRoot + sep6)) {
4941
+ while (dir !== planningRoot && dir.startsWith(planningRoot + sep7)) {
4613
4942
  try {
4614
4943
  if (readdirSync12(dir).length > 0) break;
4615
- rmSync11(dir, { recursive: true, force: true });
4944
+ rmSync12(dir, { recursive: true, force: true });
4616
4945
  } catch {
4617
4946
  break;
4618
4947
  }
@@ -4627,20 +4956,20 @@ function tryRealpath(dir) {
4627
4956
  }
4628
4957
  }
4629
4958
  function isInsidePlanningRoot(parentReal, rootReal) {
4630
- return parentReal === rootReal || parentReal.startsWith(rootReal + sep6);
4959
+ return parentReal === rootReal || parentReal.startsWith(rootReal + sep7);
4631
4960
  }
4632
4961
  function deletePlanningTarget(target, planningRoot, repoCounterpart) {
4633
- if (existsSync31(repoCounterpart)) return;
4962
+ if (existsSync33(repoCounterpart)) return;
4634
4963
  const parentReal = tryRealpath(dirname7(target));
4635
4964
  if (parentReal === void 0) return;
4636
4965
  const rootReal = tryRealpath(planningRoot);
4637
4966
  if (rootReal === void 0) return;
4638
4967
  if (!isInsidePlanningRoot(parentReal, rootReal)) return;
4639
- rmSync11(target, { recursive: true, force: true });
4968
+ rmSync12(target, { recursive: true, force: true });
4640
4969
  pruneEmptyAncestors(target, planningRoot);
4641
4970
  }
4642
4971
  function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
4643
- const repoExtras = join38(repo, "shared", "extras");
4972
+ const repoExtras = join40(repo, "shared", "extras");
4644
4973
  for (const t of eachExtrasTarget(v, { unmapped: 0, skipped: 0 })) {
4645
4974
  if (t.dirname !== ".planning") continue;
4646
4975
  let raw;
@@ -4666,11 +4995,11 @@ function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
4666
4995
  }
4667
4996
  const targets = planningDeleteTargets({ raw, logical: t.logical, localRoot: t.localRoot });
4668
4997
  if (targets.length === 0) continue;
4669
- backupExtrasWrite(join38(t.localRoot, t.dirname), ts, t.localRoot);
4670
- const planningRoot = join38(t.localRoot, ".planning");
4998
+ backupExtrasWrite(join40(t.localRoot, t.dirname), ts, t.localRoot);
4999
+ const planningRoot = join40(t.localRoot, ".planning");
4671
5000
  for (const target of targets) {
4672
- const relToLocal = target.slice(t.localRoot.length + sep6.length);
4673
- deletePlanningTarget(target, planningRoot, join38(repoExtras, t.logical, relToLocal));
5001
+ const relToLocal = target.slice(t.localRoot.length + sep7.length);
5002
+ deletePlanningTarget(target, planningRoot, join40(repoExtras, t.logical, relToLocal));
4674
5003
  }
4675
5004
  }
4676
5005
  }
@@ -4679,14 +5008,14 @@ function remapExtrasPush(ts, opts = {}) {
4679
5008
  const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
4680
5009
  if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
4681
5010
  const repo = repoHome();
4682
- const repoExtras = join38(repo, "shared", "extras");
5011
+ const repoExtras = join40(repo, "shared", "extras");
4683
5012
  if (!dryRun) mkdirSync7(repoExtras, { recursive: true });
4684
5013
  const { unmapped, skipped, done, would } = runExtrasOp(
4685
5014
  v,
4686
5015
  dryRun,
4687
5016
  ({ localRoot, logical, dirname: dirname8 }) => ({
4688
- src: join38(localRoot, dirname8),
4689
- dst: join38(repoExtras, logical, dirname8)
5017
+ src: join40(localRoot, dirname8),
5018
+ dst: join40(repoExtras, logical, dirname8)
4690
5019
  }),
4691
5020
  (dst) => backupRepoWrite(dst, ts, repo),
4692
5021
  // Push copy routing per extra type:
@@ -4715,8 +5044,8 @@ function remapExtrasPull(ts, opts = {}) {
4715
5044
  v,
4716
5045
  dryRun,
4717
5046
  ({ localRoot, logical, dirname: dirname8 }) => ({
4718
- src: join38(repo, "shared", "extras", logical, dirname8),
4719
- dst: join38(localRoot, dirname8)
5047
+ src: join40(repo, "shared", "extras", logical, dirname8),
5048
+ dst: join40(localRoot, dirname8)
4720
5049
  }),
4721
5050
  // Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
4722
5051
  // localRoot so the backup tree mirrors the project layout.
@@ -4740,129 +5069,34 @@ function remapExtrasPull(ts, opts = {}) {
4740
5069
  if (!dryRun && prePostHeads !== void 0) {
4741
5070
  propagatePlanningDeletes(v, ts, prePostHeads, repo);
4742
5071
  }
4743
- return { unmapped, skipped, pulled: done, wouldPull: would };
4744
- }
4745
-
4746
- // src/extras-sync.ts
4747
- function divergenceCheckExtras(ts) {
4748
- const v = loadValidatedExtras({});
4749
- if (v === null) return;
4750
- const counts = { unmapped: 0, skipped: 0 };
4751
- const backupRoot = join39(backupBase(), ts, "extras");
4752
- const repo = repoHome();
4753
- for (const { logical, localRoot, dirname: dirname8 } of eachExtrasTarget(v, counts)) {
4754
- const local = join39(localRoot, dirname8);
4755
- const repoEntry = join39(repo, "shared", "extras", logical, dirname8);
4756
- if (!existsSync32(local) || !existsSync32(repoEntry)) continue;
4757
- const diff = listDivergingFiles(local, repoEntry);
4758
- if (diff.length === 0) continue;
4759
- const projectBackupRoot = join39(backupRoot, encodePath(localRoot));
4760
- warn(
4761
- `local ${dirname8} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will merge changes (.planning overlays, .claude/.CLAUDE.md mirror; backups at ${projectBackupRoot}/)`
4762
- );
4763
- for (const f of diff) warn(` ${f}`);
4764
- }
4765
- }
4766
-
4767
- // src/links.ts
4768
- init_config();
4769
- init_utils();
4770
- init_utils_fs();
4771
- init_utils_json();
4772
- import { existsSync as existsSync33, lstatSync as lstatSync9, rmSync as rmSync12 } from "node:fs";
4773
- import { join as join40 } from "node:path";
4774
- function emitAutoMove(onPreview, linkPath, ts, name) {
4775
- if (onPreview) {
4776
- onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
4777
- } else {
4778
- log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
4779
- }
4780
- }
4781
- function emitCreate(onPreview, from, to) {
4782
- if (onPreview) {
4783
- onPreview({ kind: "create", from, to });
4784
- } else {
4785
- log(`would create symlink: ${from} -> ${to}`);
4786
- }
4787
- }
4788
- function isAlreadySymlink(linkPath) {
4789
- return existsSync33(linkPath) && lstatSync9(linkPath).isSymbolicLink();
4790
- }
4791
- function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
4792
- for (const name of linkNames) {
4793
- const linkPath = join40(claude, name);
4794
- const target = join40(repo, "shared", name);
4795
- if (!existsSync33(linkPath)) continue;
4796
- if (lstatSync9(linkPath).isSymbolicLink()) continue;
4797
- if (!existsSync33(target)) continue;
4798
- if (dryRun) {
4799
- emitAutoMove(onPreview, linkPath, ts, name);
4800
- continue;
4801
- }
4802
- backupBeforeWrite(linkPath, ts);
4803
- rmSync12(linkPath, { recursive: true, force: true });
4804
- }
4805
- }
4806
- function applySharedLinks(ts, map, opts = {}) {
4807
- const dryRun = opts.dryRun === true;
4808
- const claude = claudeHome();
4809
- const repo = repoHome();
4810
- const linkNames = allSharedLinks(map);
4811
- runAutoMovePasses(linkNames, claude, repo, ts, dryRun, opts.onPreview);
4812
- for (const name of linkNames) {
4813
- const target = join40(repo, "shared", name);
4814
- if (!existsSync33(target)) continue;
4815
- const linkPath = join40(claude, name);
4816
- if (isAlreadySymlink(linkPath)) continue;
4817
- if (dryRun) {
4818
- emitCreate(opts.onPreview, linkPath, target);
4819
- continue;
4820
- }
4821
- ensureSymlink(linkPath, target);
4822
- }
5072
+ return { unmapped, skipped, pulled: done, wouldPull: would };
4823
5073
  }
4824
- function regenerateSettings(ts, opts = {}) {
4825
- const dryRun = opts.dryRun === true;
5074
+
5075
+ // src/extras-sync.ts
5076
+ function divergenceCheckExtras(ts) {
5077
+ const v = loadValidatedExtras({});
5078
+ if (v === null) return;
5079
+ const counts = { unmapped: 0, skipped: 0 };
5080
+ const backupRoot = join41(backupBase(), ts, "extras");
4826
5081
  const repo = repoHome();
4827
- const claude = claudeHome();
4828
- const basePath = join40(repo, "shared", "settings.base.json");
4829
- const hostPath = join40(repo, "hosts", `${HOST}.json`);
4830
- if (!existsSync33(basePath)) {
4831
- die("repo not initialized; run 'nomad init' to scaffold");
4832
- }
4833
- const base = readJson(basePath);
4834
- const hasOverrides = existsSync33(hostPath);
4835
- const overrides = hasOverrides ? readJson(hostPath) : {};
4836
- const merged = deepMerge(base, overrides);
4837
- const settingsPath = join40(claude, "settings.json");
4838
- if (!hasOverrides && existsSync33(settingsPath)) {
4839
- try {
4840
- const existing = readJson(settingsPath);
4841
- const baseKeys = new Set(Object.keys(base));
4842
- const drift = Object.keys(existing).filter((k) => !baseKeys.has(k));
4843
- if (drift.length > 0) {
4844
- warn(
4845
- `no hosts/${HOST}.json found; existing settings has unbased keys ${JSON.stringify(drift)}. Set NOMAD_HOST to match a hosts/*.json or rerun 'nomad doctor' for candidates.`
4846
- );
4847
- }
4848
- } catch {
4849
- warn("existing settings.json is malformed; skipping drift-check and regenerating.");
4850
- }
4851
- }
4852
- const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
4853
- if (dryRun) {
4854
- log(`would write settings.json (base + ${overrideLabel})`);
4855
- return { label: overrideLabel };
5082
+ for (const { logical, localRoot, dirname: dirname8 } of eachExtrasTarget(v, counts)) {
5083
+ const local = join41(localRoot, dirname8);
5084
+ const repoEntry = join41(repo, "shared", "extras", logical, dirname8);
5085
+ if (!existsSync34(local) || !existsSync34(repoEntry)) continue;
5086
+ const diff = listDivergingFiles(local, repoEntry);
5087
+ if (diff.length === 0) continue;
5088
+ const projectBackupRoot = join41(backupRoot, encodePath(localRoot));
5089
+ warn(
5090
+ `local ${dirname8} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will merge changes (.planning overlays, .claude/.CLAUDE.md mirror; backups at ${projectBackupRoot}/)`
5091
+ );
5092
+ for (const f of diff) warn(` ${f}`);
4856
5093
  }
4857
- backupBeforeWrite(settingsPath, ts);
4858
- writeJsonAtomic(settingsPath, merged);
4859
- return { label: overrideLabel };
4860
5094
  }
4861
5095
 
4862
5096
  // src/skills-sync.ts
4863
5097
  init_config();
4864
- import { existsSync as existsSync34, lstatSync as lstatSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync13, rmSync as rmSync13 } from "node:fs";
4865
- import { join as join41 } from "node:path";
5098
+ import { existsSync as existsSync35, lstatSync as lstatSync10, mkdirSync as mkdirSync8, readdirSync as readdirSync13, rmSync as rmSync13 } from "node:fs";
5099
+ import { join as join42 } from "node:path";
4866
5100
  init_utils_fs();
4867
5101
  function isGsdOwned(name) {
4868
5102
  return name.startsWith(GSD_PREFIX);
@@ -4882,9 +5116,9 @@ function copySkillsPull(src, dst) {
4882
5116
  copyExtrasFilteredPreservingBy(src, dst, isSkillExcluded);
4883
5117
  }
4884
5118
  function syncSkillsPull(ts) {
4885
- const sharedSkills = join41(repoHome(), "shared", "skills");
4886
- if (!existsSync34(sharedSkills)) return;
4887
- const localSkills = join41(claudeHome(), "skills");
5119
+ const sharedSkills = join42(repoHome(), "shared", "skills");
5120
+ if (!existsSync35(sharedSkills)) return;
5121
+ const localSkills = join42(claudeHome(), "skills");
4888
5122
  const dstStat = lstatSync10(localSkills, { throwIfNoEntry: false });
4889
5123
  if (dstStat?.isSymbolicLink() === true) {
4890
5124
  backupBeforeWrite(localSkills, ts);
@@ -4894,18 +5128,18 @@ function syncSkillsPull(ts) {
4894
5128
  copySkillsPull(sharedSkills, localSkills);
4895
5129
  }
4896
5130
  function syncSkillsPush() {
4897
- const localSkills = join41(claudeHome(), "skills");
5131
+ const localSkills = join42(claudeHome(), "skills");
4898
5132
  const stat = lstatSync10(localSkills, { throwIfNoEntry: false });
4899
5133
  if (stat === void 0) return;
4900
5134
  if (stat.isSymbolicLink()) return;
4901
- const sharedSkills = join41(repoHome(), "shared", "skills");
5135
+ const sharedSkills = join42(repoHome(), "shared", "skills");
4902
5136
  copySkillsPush(localSkills, sharedSkills);
4903
5137
  }
4904
5138
 
4905
5139
  // src/preview.ts
4906
5140
  init_config();
4907
- import { existsSync as existsSync35 } from "node:fs";
4908
- import { join as join42 } from "node:path";
5141
+ import { existsSync as existsSync36 } from "node:fs";
5142
+ import { join as join43 } from "node:path";
4909
5143
 
4910
5144
  // node_modules/diff/libesm/diff/base.js
4911
5145
  var Diff = class {
@@ -5191,7 +5425,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
5191
5425
  return lines.join("\n");
5192
5426
  }
5193
5427
  function readJsonOrNull(path) {
5194
- if (!existsSync35(path)) return null;
5428
+ if (!existsSync36(path)) return null;
5195
5429
  try {
5196
5430
  return readJson(path);
5197
5431
  } catch {
@@ -5205,12 +5439,12 @@ function previewSettings(basePath, hostPath, settingsPath) {
5205
5439
  }
5206
5440
  const notes = [];
5207
5441
  const hostOverrides = readJsonOrNull(hostPath);
5208
- if (hostOverrides === null && existsSync35(hostPath)) {
5442
+ if (hostOverrides === null && existsSync36(hostPath)) {
5209
5443
  notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
5210
5444
  }
5211
5445
  const merged = deepMerge(base, hostOverrides ?? {});
5212
5446
  const current = readJsonOrNull(settingsPath);
5213
- if (current === null && existsSync35(settingsPath)) {
5447
+ if (current === null && existsSync36(settingsPath)) {
5214
5448
  return { diff: "", notes: [...notes, "malformed; skipping diff"] };
5215
5449
  }
5216
5450
  const rawEqual = JSON.stringify(current ?? {}, null, 2) === JSON.stringify(merged, null, 2);
@@ -5250,9 +5484,9 @@ function computePreview(ts, map, verb = "pull") {
5250
5484
  onPreview: (e) => addItem(links, formatLinkRow(e))
5251
5485
  });
5252
5486
  const settingsResult = previewSettings(
5253
- join42(repo, "shared", "settings.base.json"),
5254
- join42(repo, "hosts", `${HOST}.json`),
5255
- join42(claude, "settings.json")
5487
+ join43(repo, "shared", "settings.base.json"),
5488
+ join43(repo, "hosts", `${HOST}.json`),
5489
+ join43(claude, "settings.json")
5256
5490
  );
5257
5491
  const settingsSection = buildSettingsSectionForPreview(settingsResult);
5258
5492
  const sessions = section("Sessions");
@@ -5274,9 +5508,9 @@ init_config();
5274
5508
  init_commands_pull_wedge();
5275
5509
  init_utils();
5276
5510
  init_utils_fs();
5277
- import { execFileSync as execFileSync15 } from "node:child_process";
5511
+ import { execFileSync as execFileSync16 } from "node:child_process";
5278
5512
  function gitCapture(args, cwd) {
5279
- return execFileSync15("git", args, {
5513
+ return execFileSync16("git", args, {
5280
5514
  cwd,
5281
5515
  stdio: ["ignore", "pipe", "pipe"],
5282
5516
  maxBuffer: 64 * 1024 * 1024
@@ -5453,8 +5687,8 @@ function cmdPull(opts = {}) {
5453
5687
  const forceRemote = opts.forceRemote === true;
5454
5688
  const repo = repoHome();
5455
5689
  const backup = backupBase();
5456
- if (!existsSync36(repo)) die(`repo not cloned at ${repo}`);
5457
- if (!existsSync36(join43(repo, "shared", "settings.base.json"))) {
5690
+ if (!existsSync37(repo)) die(`repo not cloned at ${repo}`);
5691
+ if (!existsSync37(join44(repo, "shared", "settings.base.json"))) {
5458
5692
  die("repo not initialized; run 'nomad init' to scaffold");
5459
5693
  }
5460
5694
  const handle = acquireLock("pull");
@@ -5463,7 +5697,7 @@ function cmdPull(opts = {}) {
5463
5697
  const ts = freshBackupTs(backup);
5464
5698
  handleWedge(repo, forceRemote);
5465
5699
  if (!dryRun) {
5466
- const backupRoot = join43(backup, ts);
5700
+ const backupRoot = join44(backup, ts);
5467
5701
  try {
5468
5702
  mkdirSync9(backupRoot, { recursive: true });
5469
5703
  } catch (err) {
@@ -5476,8 +5710,8 @@ function cmdPull(opts = {}) {
5476
5710
  const prePostHeads = capturePrePostHeads(repo, () => {
5477
5711
  gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
5478
5712
  });
5479
- const mapPath = join43(repo, "path-map.json");
5480
- const map = existsSync36(mapPath) ? readPathMap(mapPath) : { projects: {} };
5713
+ const mapPath = join44(repo, "path-map.json");
5714
+ const map = existsSync37(mapPath) ? readPathMap(mapPath) : { projects: {} };
5481
5715
  divergenceCheckExtras(ts);
5482
5716
  if (dryRun) {
5483
5717
  computePreview(ts, map, "pull");
@@ -5499,8 +5733,8 @@ function cmdPull(opts = {}) {
5499
5733
 
5500
5734
  // src/commands.push.ts
5501
5735
  init_config();
5502
- import { existsSync as existsSync38 } from "node:fs";
5503
- import { join as join45, relative as relative5 } from "node:path";
5736
+ import { existsSync as existsSync39 } from "node:fs";
5737
+ import { join as join46, relative as relative5 } from "node:path";
5504
5738
 
5505
5739
  // src/commands.push.allowlist.ts
5506
5740
  init_config();
@@ -5593,7 +5827,7 @@ function enforceAllowList(statusPorcelain, map) {
5593
5827
 
5594
5828
  // src/push-global-config.ts
5595
5829
  init_config();
5596
- import { execFileSync as execFileSync16 } from "node:child_process";
5830
+ import { execFileSync as execFileSync17 } from "node:child_process";
5597
5831
  var STATUS_LABELS = {
5598
5832
  A: "add",
5599
5833
  M: "modify",
@@ -5633,7 +5867,7 @@ function isInScope(filePath, exactPrefixes, dirPrefixes) {
5633
5867
  }
5634
5868
  function collectGlobalConfigChanges(repoHome2, hostname2, opts) {
5635
5869
  const args = opts.staged ? ["diff", "--cached", "--name-status", "-z"] : ["diff", "HEAD", "--name-status", "-z"];
5636
- const raw = execFileSync16("git", args, {
5870
+ const raw = execFileSync17("git", args, {
5637
5871
  cwd: repoHome2,
5638
5872
  stdio: ["ignore", "pipe", "pipe"]
5639
5873
  }).toString();
@@ -5672,9 +5906,9 @@ init_color();
5672
5906
  init_config();
5673
5907
  init_config_sharedDirs_guard();
5674
5908
  import { randomBytes as randomBytes2 } from "node:crypto";
5675
- import { copyFileSync, existsSync as existsSync37, mkdirSync as mkdirSync10, readdirSync as readdirSync14, rmSync as rmSync14 } from "node:fs";
5909
+ import { copyFileSync, existsSync as existsSync38, mkdirSync as mkdirSync10, readdirSync as readdirSync14, rmSync as rmSync14 } from "node:fs";
5676
5910
  import { homedir as homedir5 } from "node:os";
5677
- import { join as join44 } from "node:path";
5911
+ import { join as join45 } from "node:path";
5678
5912
  init_push_leak_verdict();
5679
5913
  init_push_gitleaks();
5680
5914
  init_utils_fs();
@@ -5689,13 +5923,13 @@ function stageSessions(tmpRoot, map) {
5689
5923
  if (!p || p === "TBD") continue;
5690
5924
  reverse.set(encodePath(p), logical);
5691
5925
  }
5692
- const localProjects = join44(claudeHome(), "projects");
5693
- if (!existsSync37(localProjects)) return 0;
5926
+ const localProjects = join45(claudeHome(), "projects");
5927
+ if (!existsSync38(localProjects)) return 0;
5694
5928
  let staged = 0;
5695
5929
  for (const dir of readdirSync14(localProjects)) {
5696
5930
  const logical = reverse.get(dir);
5697
5931
  if (!logical) continue;
5698
- copyDirJsonlOnly(join44(localProjects, dir), join44(tmpRoot, "shared", "projects", logical));
5932
+ copyDirJsonlOnly(join45(localProjects, dir), join45(tmpRoot, "shared", "projects", logical));
5699
5933
  staged++;
5700
5934
  }
5701
5935
  return staged;
@@ -5711,9 +5945,9 @@ function stageExtras(tmpRoot, map) {
5711
5945
  if (!localRoot || localRoot === "TBD") continue;
5712
5946
  for (const dirname8 of dirnames) {
5713
5947
  if (!whitelist.includes(dirname8)) continue;
5714
- const src = join44(localRoot, dirname8);
5715
- if (!existsSync37(src)) continue;
5716
- const dst = join44(tmpRoot, "shared", "extras", logical, dirname8);
5948
+ const src = join45(localRoot, dirname8);
5949
+ if (!existsSync38(src)) continue;
5950
+ const dst = join45(tmpRoot, "shared", "extras", logical, dirname8);
5717
5951
  copyExtras(src, dst);
5718
5952
  staged++;
5719
5953
  }
@@ -5721,19 +5955,19 @@ function stageExtras(tmpRoot, map) {
5721
5955
  return staged;
5722
5956
  }
5723
5957
  function previewPushLeaks(map) {
5724
- const cacheDir = join44(homedir5(), ".cache", "claude-nomad");
5958
+ const cacheDir = join45(homedir5(), ".cache", "claude-nomad");
5725
5959
  mkdirSync10(cacheDir, { recursive: true });
5726
5960
  const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
5727
- const tmpRoot = join44(cacheDir, `push-preview-tree-${stamp}`);
5961
+ const tmpRoot = join45(cacheDir, `push-preview-tree-${stamp}`);
5728
5962
  try {
5729
5963
  const sessionCount = stageSessions(tmpRoot, map);
5730
5964
  const extrasCount = stageExtras(tmpRoot, map);
5731
5965
  if (sessionCount + extrasCount === 0) {
5732
5966
  return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
5733
5967
  }
5734
- const ignoreFile = join44(repoHome(), ".gitleaksignore");
5735
- if (existsSync37(ignoreFile)) {
5736
- copyFileSync(ignoreFile, join44(tmpRoot, ".gitleaksignore"));
5968
+ const ignoreFile = join45(repoHome(), ".gitleaksignore");
5969
+ if (existsSync38(ignoreFile)) {
5970
+ copyFileSync(ignoreFile, join45(tmpRoot, ".gitleaksignore"));
5737
5971
  }
5738
5972
  let findings;
5739
5973
  try {
@@ -5754,8 +5988,29 @@ function previewPushLeaks(map) {
5754
5988
  init_utils();
5755
5989
  init_utils_fs();
5756
5990
  init_utils_json();
5991
+ function reportSettingsAheadDrift(repo) {
5992
+ const basePath = join46(repo, "shared", "settings.base.json");
5993
+ if (!existsSync39(basePath)) return;
5994
+ const settingsPath = join46(claudeHome(), "settings.json");
5995
+ if (!existsSync39(settingsPath)) return;
5996
+ try {
5997
+ const base = readJson(basePath);
5998
+ const hostPath = join46(repo, "hosts", `${HOST}.json`);
5999
+ const overrides = existsSync39(hostPath) ? readJson(hostPath) : {};
6000
+ const merged = deepMerge(base, overrides);
6001
+ const settings = readJson(settingsPath);
6002
+ const { ahead } = classifySettingsDrift(merged, settings);
6003
+ const { promotable } = partitionByCaptureExclusion(ahead);
6004
+ if (promotable.length === 0) return;
6005
+ const keys = JSON.stringify(promotable);
6006
+ warn(
6007
+ `settings.json has local-only keys ${keys} not in the repo. Run 'nomad capture-settings' to promote them (or 'nomad capture-settings --host' for host-specific values).`
6008
+ );
6009
+ } catch {
6010
+ }
6011
+ }
5757
6012
  function guardGitlinks(repo) {
5758
- const gitlinks = findGitlinks(join45(repo, "shared"));
6013
+ const gitlinks = findGitlinks(join46(repo, "shared"));
5759
6014
  if (gitlinks.length === 0) return;
5760
6015
  for (const p of gitlinks) {
5761
6016
  const rel = relative5(repo, p);
@@ -5821,11 +6076,12 @@ async function cmdPush(opts = {}) {
5821
6076
  guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
5822
6077
  const repo = repoHome();
5823
6078
  const backup = backupBase();
5824
- if (!existsSync38(repo)) die(`repo not cloned at ${repo}`);
6079
+ if (!existsSync39(repo)) die(`repo not cloned at ${repo}`);
5825
6080
  const handle = acquireLock("push");
5826
6081
  if (handle === null) process.exit(0);
5827
6082
  try {
5828
6083
  console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
6084
+ reportSettingsAheadDrift(repo);
5829
6085
  probeGitleaks();
5830
6086
  withSpinner("Rebasing onto origin", () => rebaseBeforePush(repo));
5831
6087
  const ts = freshBackupTs(backup);
@@ -5840,8 +6096,8 @@ async function cmdPush(opts = {}) {
5840
6096
  renderNoScanTree(st);
5841
6097
  return;
5842
6098
  }
5843
- const mapPath = join45(repo, "path-map.json");
5844
- if (!existsSync38(mapPath)) {
6099
+ const mapPath = join46(repo, "path-map.json");
6100
+ if (!existsSync39(mapPath)) {
5845
6101
  if (dryRun) return runDryRunPreview(st, null, repo);
5846
6102
  die("path-map.json missing, cannot enforce push allow-list");
5847
6103
  }
@@ -5862,16 +6118,16 @@ async function cmdPush(opts = {}) {
5862
6118
  }
5863
6119
 
5864
6120
  // src/commands.update.ts
5865
- import { execFileSync as execFileSync17 } from "node:child_process";
6121
+ import { execFileSync as execFileSync18 } from "node:child_process";
5866
6122
  init_utils();
5867
- function readInstalledVersion(run = execFileSync17) {
6123
+ function readInstalledVersion(run = execFileSync18) {
5868
6124
  try {
5869
6125
  return run("nomad", ["--version"], { encoding: "utf8" }).toString().trim() || null;
5870
6126
  } catch {
5871
6127
  return null;
5872
6128
  }
5873
6129
  }
5874
- function cmdUpdate(run = execFileSync17) {
6130
+ function cmdUpdate(run = execFileSync18) {
5875
6131
  console.log("Updating claude-nomad CLI via npm...");
5876
6132
  try {
5877
6133
  run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
@@ -5895,18 +6151,18 @@ init_config();
5895
6151
 
5896
6152
  // src/diff.ts
5897
6153
  init_config();
5898
- import { existsSync as existsSync39 } from "node:fs";
5899
- import { join as join46 } from "node:path";
6154
+ import { existsSync as existsSync40 } from "node:fs";
6155
+ import { join as join47 } from "node:path";
5900
6156
  init_utils();
5901
6157
  init_utils_fs();
5902
6158
  init_utils_json();
5903
6159
  function cmdDiff() {
5904
6160
  try {
5905
6161
  const repo = repoHome();
5906
- if (!existsSync39(repo)) die(`repo not cloned at ${repo}`);
6162
+ if (!existsSync40(repo)) die(`repo not cloned at ${repo}`);
5907
6163
  const ts = freshBackupTs(backupBase());
5908
- const mapPath = join46(repo, "path-map.json");
5909
- const map = existsSync39(mapPath) ? readPathMap(mapPath) : { projects: {} };
6164
+ const mapPath = join47(repo, "path-map.json");
6165
+ const map = existsSync40(mapPath) ? readPathMap(mapPath) : { projects: {} };
5910
6166
  computePreview(ts, map, "diff");
5911
6167
  } catch (err) {
5912
6168
  if (err instanceof NomadFatal) {
@@ -5920,19 +6176,19 @@ function cmdDiff() {
5920
6176
 
5921
6177
  // src/init.ts
5922
6178
  init_config();
5923
- import { existsSync as existsSync41, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6 } from "node:fs";
5924
- import { join as join48 } from "node:path";
6179
+ import { existsSync as existsSync42, mkdirSync as mkdirSync11, writeFileSync as writeFileSync6 } from "node:fs";
6180
+ import { join as join49 } from "node:path";
5925
6181
 
5926
6182
  // src/init.gh-onboard.ts
5927
6183
  init_config();
5928
- import { execFileSync as execFileSync18 } from "node:child_process";
6184
+ import { execFileSync as execFileSync19 } from "node:child_process";
5929
6185
  init_utils();
5930
6186
  var DEFAULT_REPO_NAME = "claude-nomad-config";
5931
6187
  function isValidRepoName(name) {
5932
6188
  return /^[A-Za-z0-9._-]{1,100}$/.test(name);
5933
6189
  }
5934
6190
  var GH_NETWORK_TIMEOUT_MS = 3e4;
5935
- function ensureOriginRepo(repoName, run = execFileSync18) {
6191
+ function ensureOriginRepo(repoName, run = execFileSync19) {
5936
6192
  if (!isValidRepoName(repoName)) {
5937
6193
  die(
5938
6194
  `invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
@@ -6003,33 +6259,33 @@ init_config();
6003
6259
  init_utils();
6004
6260
  init_utils_fs();
6005
6261
  init_utils_json();
6006
- import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync40, rmSync as rmSync15, statSync as statSync9 } from "node:fs";
6007
- import { join as join47 } from "node:path";
6262
+ import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync41, rmSync as rmSync15, statSync as statSync9 } from "node:fs";
6263
+ import { join as join48 } from "node:path";
6008
6264
  function snapshotIntoShared(map) {
6009
6265
  const repo = repoHome();
6010
6266
  const claude = claudeHome();
6011
6267
  for (const name of allSharedLinks(map)) {
6012
- const src = join47(claude, name);
6013
- if (!existsSync40(src)) continue;
6014
- const dst = join47(repo, "shared", name);
6268
+ const src = join48(claude, name);
6269
+ if (!existsSync41(src)) continue;
6270
+ const dst = join48(repo, "shared", name);
6015
6271
  if (statSync9(src).isDirectory()) {
6016
- const gk = join47(dst, ".gitkeep");
6017
- if (existsSync40(gk)) rmSync15(gk);
6272
+ const gk = join48(dst, ".gitkeep");
6273
+ if (existsSync41(gk)) rmSync15(gk);
6018
6274
  cpSync7(src, dst, { recursive: true, force: false, errorOnExist: true });
6019
6275
  } else {
6020
6276
  copyFileSync2(src, dst);
6021
6277
  }
6022
6278
  log(`snapshotted shared/${name} from ${src}`);
6023
6279
  }
6024
- const userSettings = join47(claude, "settings.json");
6025
- if (existsSync40(userSettings)) {
6280
+ const userSettings = join48(claude, "settings.json");
6281
+ if (existsSync41(userSettings)) {
6026
6282
  let parsed;
6027
6283
  try {
6028
6284
  parsed = readJson(userSettings);
6029
6285
  } catch (err) {
6030
6286
  return die(`malformed ${userSettings}: ${err.message}`);
6031
6287
  }
6032
- const hostFile = join47(repo, "hosts", `${HOST}.json`);
6288
+ const hostFile = join48(repo, "hosts", `${HOST}.json`);
6033
6289
  writeJsonAtomic(hostFile, parsed);
6034
6290
  log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
6035
6291
  }
@@ -6042,14 +6298,14 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
6042
6298
  var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
6043
6299
  function preflightConflict(repoHome2) {
6044
6300
  const candidates = [
6045
- join48(repoHome2, "shared", "settings.base.json"),
6046
- join48(repoHome2, "shared", "CLAUDE.md"),
6047
- join48(repoHome2, "path-map.json"),
6048
- join48(repoHome2, "hosts"),
6049
- join48(repoHome2, "shared")
6301
+ join49(repoHome2, "shared", "settings.base.json"),
6302
+ join49(repoHome2, "shared", "CLAUDE.md"),
6303
+ join49(repoHome2, "path-map.json"),
6304
+ join49(repoHome2, "hosts"),
6305
+ join49(repoHome2, "shared")
6050
6306
  ];
6051
6307
  for (const c of candidates) {
6052
- if (existsSync41(c)) return c;
6308
+ if (existsSync42(c)) return c;
6053
6309
  }
6054
6310
  return null;
6055
6311
  }
@@ -6064,25 +6320,25 @@ function cmdInit(opts = {}) {
6064
6320
  die(`already initialized; refusing to clobber ${conflict}`);
6065
6321
  }
6066
6322
  ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
6067
- mkdirSync11(join48(repo, "shared"), { recursive: true });
6068
- mkdirSync11(join48(repo, "hosts"), { recursive: true });
6323
+ mkdirSync11(join49(repo, "shared"), { recursive: true });
6324
+ mkdirSync11(join49(repo, "hosts"), { recursive: true });
6069
6325
  for (const name of SHARED_KEEP_DIRS) {
6070
- mkdirSync11(join48(repo, "shared", name), { recursive: true });
6326
+ mkdirSync11(join49(repo, "shared", name), { recursive: true });
6071
6327
  }
6072
- const userClaudeMd = join48(claude, "CLAUDE.md");
6073
- if (!snapshot || !existsSync41(userClaudeMd)) {
6074
- writeFileSync6(join48(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
6328
+ const userClaudeMd = join49(claude, "CLAUDE.md");
6329
+ if (!snapshot || !existsSync42(userClaudeMd)) {
6330
+ writeFileSync6(join49(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
6075
6331
  item("created shared/CLAUDE.md");
6076
6332
  }
6077
6333
  for (const name of SHARED_KEEP_DIRS) {
6078
- writeFileSync6(join48(repo, "shared", name, ".gitkeep"), "");
6334
+ writeFileSync6(join49(repo, "shared", name, ".gitkeep"), "");
6079
6335
  item(`created shared/${name}/.gitkeep`);
6080
6336
  }
6081
- writeFileSync6(join48(repo, "hosts", ".gitkeep"), "");
6337
+ writeFileSync6(join49(repo, "hosts", ".gitkeep"), "");
6082
6338
  item("created hosts/.gitkeep");
6083
- writeJsonAtomic(join48(repo, "shared", "settings.base.json"), {});
6339
+ writeJsonAtomic(join49(repo, "shared", "settings.base.json"), {});
6084
6340
  item("created shared/settings.base.json");
6085
- writeJsonAtomic(join48(repo, "path-map.json"), { projects: {} });
6341
+ writeJsonAtomic(join49(repo, "path-map.json"), { projects: {} });
6086
6342
  item("created path-map.json");
6087
6343
  if (snapshot) {
6088
6344
  snapshotIntoShared({ projects: {} });
@@ -6145,86 +6401,20 @@ function maybeDisableRepoActions(repoHome2, run) {
6145
6401
  }
6146
6402
  }
6147
6403
 
6148
- // src/nomad.dispatch.ts
6404
+ // src/nomad.dispatch.helpers.ts
6405
+ var REJECT = { ok: false, advance: 0 };
6406
+ function applyBool(seen, set) {
6407
+ if (seen) return REJECT;
6408
+ set();
6409
+ return { ok: true, advance: 1 };
6410
+ }
6149
6411
  function extractFlagValue(argv, i) {
6150
6412
  const val = argv[i + 1];
6151
6413
  if (val === void 0 || val.startsWith("--")) return null;
6152
6414
  return val;
6153
6415
  }
6154
- function applyInitToken(argv, i, st) {
6155
- const token = argv[i];
6156
- if (token === "--snapshot") {
6157
- if (st.sawSnapshot) return { ok: false, advance: 0 };
6158
- st.sawSnapshot = true;
6159
- st.snapshot = true;
6160
- return { ok: true, advance: 1 };
6161
- }
6162
- if (token === "--keep-actions") {
6163
- if (st.sawKeepActions) return { ok: false, advance: 0 };
6164
- st.sawKeepActions = true;
6165
- st.keepActions = true;
6166
- return { ok: true, advance: 1 };
6167
- }
6168
- if (token === "--repo") {
6169
- if (st.sawRepo) return { ok: false, advance: 0 };
6170
- st.sawRepo = true;
6171
- const val = extractFlagValue(argv, i);
6172
- if (val === null) return { ok: false, advance: 0 };
6173
- st.repoName = val;
6174
- return { ok: true, advance: 2 };
6175
- }
6176
- return { ok: false, advance: 0 };
6177
- }
6178
- function parseInitArgs(argv) {
6179
- const st = {
6180
- snapshot: false,
6181
- keepActions: false,
6182
- repoName: void 0,
6183
- sawSnapshot: false,
6184
- sawKeepActions: false,
6185
- sawRepo: false
6186
- };
6187
- let i = 3;
6188
- while (i < argv.length) {
6189
- const { ok: ok2, advance } = applyInitToken(argv, i, st);
6190
- if (!ok2) return null;
6191
- i += advance;
6192
- }
6193
- return { snapshot: st.snapshot, keepActions: st.keepActions, repoName: st.repoName };
6194
- }
6195
- function parseRedactArgs(argv) {
6196
- const id = argv[3];
6197
- if (typeof id !== "string" || !/^\w[\w-]{0,127}$/.test(id)) {
6198
- return null;
6199
- }
6200
- let rule;
6201
- let dryRun = false;
6202
- let sawRule = false;
6203
- let sawDryRun = false;
6204
- let i = 4;
6205
- while (i < argv.length) {
6206
- const token = argv[i];
6207
- if (token === "--dry-run") {
6208
- if (sawDryRun) return null;
6209
- sawDryRun = true;
6210
- dryRun = true;
6211
- i++;
6212
- } else if (token === "--rule") {
6213
- if (sawRule) return null;
6214
- sawRule = true;
6215
- const val = argv[i + 1];
6216
- if (val === void 0 || val.startsWith("--")) return null;
6217
- rule = val;
6218
- i += 2;
6219
- } else {
6220
- return null;
6221
- }
6222
- }
6223
- return { id, rule, dryRun };
6224
- }
6225
6416
 
6226
6417
  // src/nomad.dispatch.clean.ts
6227
- var REJECT = { ok: false, advance: 0 };
6228
6418
  function applyOlderThan(argv, i, st) {
6229
6419
  if (st.olderThan !== void 0) return REJECT;
6230
6420
  const val = extractFlagValue(argv, i);
@@ -6239,11 +6429,6 @@ function applyKeep(argv, i, st) {
6239
6429
  st.keep = Number(val);
6240
6430
  return { ok: true, advance: 2 };
6241
6431
  }
6242
- function applyBool(seen, set) {
6243
- if (seen) return REJECT;
6244
- set();
6245
- return { ok: true, advance: 1 };
6246
- }
6247
6432
  function applyCleanToken(argv, i, st) {
6248
6433
  switch (argv[i]) {
6249
6434
  case "--backups":
@@ -6276,6 +6461,31 @@ function parseCleanArgs(argv) {
6276
6461
  return { dryRun: st.dryRun, olderThan: st.olderThan, keep: st.keep };
6277
6462
  }
6278
6463
 
6464
+ // src/nomad.dispatch.capture-settings.ts
6465
+ function parseCaptureSettingsArgs(argv) {
6466
+ let host = false;
6467
+ let dryRun = false;
6468
+ let yes = false;
6469
+ let i = 3;
6470
+ while (i < argv.length) {
6471
+ const token = argv[i];
6472
+ if (token === "--host") {
6473
+ if (host) return null;
6474
+ host = true;
6475
+ } else if (token === "--dry-run") {
6476
+ if (dryRun) return null;
6477
+ dryRun = true;
6478
+ } else if (token === "--yes" || token === "-y") {
6479
+ if (yes) return null;
6480
+ yes = true;
6481
+ } else {
6482
+ return null;
6483
+ }
6484
+ i++;
6485
+ }
6486
+ return { host, dryRun, yes };
6487
+ }
6488
+
6279
6489
  // src/nomad.dispatch.eject.ts
6280
6490
  function parseEjectArgs(argv) {
6281
6491
  let dryRun = false;
@@ -6293,6 +6503,79 @@ function parseEjectArgs(argv) {
6293
6503
  return { dryRun };
6294
6504
  }
6295
6505
 
6506
+ // src/nomad.dispatch.ts
6507
+ function applyInitToken(argv, i, st) {
6508
+ const token = argv[i];
6509
+ if (token === "--snapshot") {
6510
+ if (st.sawSnapshot) return REJECT;
6511
+ st.sawSnapshot = true;
6512
+ st.snapshot = true;
6513
+ return { ok: true, advance: 1 };
6514
+ }
6515
+ if (token === "--keep-actions") {
6516
+ if (st.sawKeepActions) return REJECT;
6517
+ st.sawKeepActions = true;
6518
+ st.keepActions = true;
6519
+ return { ok: true, advance: 1 };
6520
+ }
6521
+ if (token === "--repo") {
6522
+ if (st.sawRepo) return REJECT;
6523
+ st.sawRepo = true;
6524
+ const val = extractFlagValue(argv, i);
6525
+ if (val === null) return REJECT;
6526
+ st.repoName = val;
6527
+ return { ok: true, advance: 2 };
6528
+ }
6529
+ return REJECT;
6530
+ }
6531
+ function parseInitArgs(argv) {
6532
+ const st = {
6533
+ snapshot: false,
6534
+ keepActions: false,
6535
+ repoName: void 0,
6536
+ sawSnapshot: false,
6537
+ sawKeepActions: false,
6538
+ sawRepo: false
6539
+ };
6540
+ let i = 3;
6541
+ while (i < argv.length) {
6542
+ const { ok: ok2, advance } = applyInitToken(argv, i, st);
6543
+ if (!ok2) return null;
6544
+ i += advance;
6545
+ }
6546
+ return { snapshot: st.snapshot, keepActions: st.keepActions, repoName: st.repoName };
6547
+ }
6548
+ function parseRedactArgs(argv) {
6549
+ const id = argv[3];
6550
+ if (typeof id !== "string" || !/^\w[\w-]{0,127}$/.test(id)) {
6551
+ return null;
6552
+ }
6553
+ let rule;
6554
+ let dryRun = false;
6555
+ let sawRule = false;
6556
+ let sawDryRun = false;
6557
+ let i = 4;
6558
+ while (i < argv.length) {
6559
+ const token = argv[i];
6560
+ if (token === "--dry-run") {
6561
+ if (sawDryRun) return null;
6562
+ sawDryRun = true;
6563
+ dryRun = true;
6564
+ i++;
6565
+ } else if (token === "--rule") {
6566
+ if (sawRule) return null;
6567
+ sawRule = true;
6568
+ const val = extractFlagValue(argv, i);
6569
+ if (val === null) return null;
6570
+ rule = val;
6571
+ i += 2;
6572
+ } else {
6573
+ return null;
6574
+ }
6575
+ }
6576
+ return { id, rule, dryRun };
6577
+ }
6578
+
6296
6579
  // src/nomad.dispatch.allow.ts
6297
6580
  function parseAllowArgs(argv) {
6298
6581
  const positionals = argv.slice(3);
@@ -6326,32 +6609,26 @@ function parsePullArgs(argv) {
6326
6609
  }
6327
6610
 
6328
6611
  // src/nomad.dispatch.push.ts
6329
- var REJECT2 = { ok: false, advance: 0 };
6330
- function applyBool2(seen, set) {
6331
- if (seen) return REJECT2;
6332
- set();
6333
- return { ok: true, advance: 1 };
6334
- }
6335
6612
  var RULE_ID_RE = /^\w[\w-]*$/;
6336
6613
  function applyAllow2(argv, i, st) {
6337
- if (st.allowRule !== void 0) return REJECT2;
6614
+ if (st.allowRule !== void 0) return REJECT;
6338
6615
  const val = extractFlagValue(argv, i);
6339
- if (val === null || !RULE_ID_RE.test(val)) return REJECT2;
6616
+ if (val === null || !RULE_ID_RE.test(val)) return REJECT;
6340
6617
  st.allowRule = val;
6341
6618
  return { ok: true, advance: 2 };
6342
6619
  }
6343
6620
  function applyPushToken(argv, i, st) {
6344
6621
  switch (argv[i]) {
6345
6622
  case "--dry-run":
6346
- return applyBool2(st.dryRun, () => st.dryRun = true);
6623
+ return applyBool(st.dryRun, () => st.dryRun = true);
6347
6624
  case "--redact-all":
6348
- return applyBool2(st.redactAll, () => st.redactAll = true);
6625
+ return applyBool(st.redactAll, () => st.redactAll = true);
6349
6626
  case "--allow-all":
6350
- return applyBool2(st.allowAll, () => st.allowAll = true);
6627
+ return applyBool(st.allowAll, () => st.allowAll = true);
6351
6628
  case "--allow":
6352
6629
  return applyAllow2(argv, i, st);
6353
6630
  default:
6354
- return REJECT2;
6631
+ return REJECT;
6355
6632
  }
6356
6633
  }
6357
6634
  function parsePushArgs(argv) {
@@ -6383,7 +6660,7 @@ function parsePushArgs(argv) {
6383
6660
  // package.json
6384
6661
  var package_default = {
6385
6662
  name: "claude-nomad",
6386
- version: "0.50.2",
6663
+ version: "0.51.0",
6387
6664
  type: "module",
6388
6665
  description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
6389
6666
  keywords: [
@@ -6548,6 +6825,21 @@ var DEFAULT_HELP = [
6548
6825
  cont("manual-remainder checklist (uninstall CLI, drop env vars, optional deletes)."),
6549
6826
  row(" --dry-run", "List what would be materialized without writing anything."),
6550
6827
  "",
6828
+ row(
6829
+ " capture-settings",
6830
+ "Promote local-only settings.json keys into the shared repo so they survive"
6831
+ ),
6832
+ cont("the next pull. Backs up the destination, writes atomically, then regenerates"),
6833
+ cont("settings.json so local matches. Idempotent when no local-only keys remain."),
6834
+ cont("Prompts for confirmation before writing (shows the destination and keys)."),
6835
+ row(" --host", "Write into hosts/<HOST>.json (host-specific values) instead of"),
6836
+ cont("shared/settings.base.json (default; normalizes absolute node launcher paths)."),
6837
+ row(
6838
+ " --dry-run",
6839
+ "Show the destination and keys that would be written without changing anything."
6840
+ ),
6841
+ row(" --yes, -y", "Skip the confirmation prompt (required in a non-interactive shell)."),
6842
+ "",
6551
6843
  row(
6552
6844
  " redact <session-id>",
6553
6845
  "Rewrite the secret span in the local source transcript for a session,"
@@ -6584,15 +6876,15 @@ var DEFAULT_HELP = [
6584
6876
  init_config();
6585
6877
  init_utils();
6586
6878
  init_utils_json();
6587
- import { existsSync as existsSync42, readFileSync as readFileSync14, readdirSync as readdirSync15 } from "node:fs";
6588
- import { join as join49 } from "node:path";
6879
+ import { existsSync as existsSync43, readFileSync as readFileSync14, readdirSync as readdirSync15 } from "node:fs";
6880
+ import { join as join50 } from "node:path";
6589
6881
  function resumeCmd(sessionId) {
6590
6882
  if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
6591
6883
  fail(`invalid session id: ${sessionId}`);
6592
6884
  process.exit(1);
6593
6885
  }
6594
- const projectsRoot = join49(claudeHome(), "projects");
6595
- if (!existsSync42(projectsRoot)) {
6886
+ const projectsRoot = join50(claudeHome(), "projects");
6887
+ if (!existsSync43(projectsRoot)) {
6596
6888
  fail(`${projectsRoot} does not exist`);
6597
6889
  process.exit(1);
6598
6890
  }
@@ -6606,13 +6898,13 @@ function resumeCmd(sessionId) {
6606
6898
  fail(`no cwd field found in ${jsonlPath}`);
6607
6899
  process.exit(1);
6608
6900
  }
6609
- const mapPath = join49(repoHome(), "path-map.json");
6610
- if (!existsSync42(mapPath)) {
6901
+ const mapPath = join50(repoHome(), "path-map.json");
6902
+ if (!existsSync43(mapPath)) {
6611
6903
  fail("path-map.json missing");
6612
6904
  process.exit(1);
6613
6905
  }
6614
6906
  const map = readJson(mapPath);
6615
- const schemaError = validatePathMap(map);
6907
+ const schemaError = validatePathMapShape(map);
6616
6908
  if (schemaError !== null) {
6617
6909
  fail(schemaError);
6618
6910
  process.exit(1);
@@ -6630,8 +6922,8 @@ function resumeCmd(sessionId) {
6630
6922
  }
6631
6923
  function findTranscriptPath(projectsRoot, sessionId) {
6632
6924
  for (const dir of readdirSync15(projectsRoot)) {
6633
- const candidate = join49(projectsRoot, dir, `${sessionId}.jsonl`);
6634
- if (existsSync42(candidate)) return candidate;
6925
+ const candidate = join50(projectsRoot, dir, `${sessionId}.jsonl`);
6926
+ if (existsSync43(candidate)) return candidate;
6635
6927
  }
6636
6928
  return null;
6637
6929
  }
@@ -6647,26 +6939,6 @@ function extractRecordedCwd(jsonlPath) {
6647
6939
  }
6648
6940
  return null;
6649
6941
  }
6650
- function validatePathMap(raw) {
6651
- if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
6652
- return "path-map.json invalid schema: top-level value must be an object";
6653
- }
6654
- const projects = raw.projects;
6655
- if (projects === null || typeof projects !== "object" || Array.isArray(projects)) {
6656
- return 'path-map.json invalid schema: "projects" must be an object';
6657
- }
6658
- for (const [name, hosts] of Object.entries(projects)) {
6659
- if (hosts === null || typeof hosts !== "object" || Array.isArray(hosts)) {
6660
- return `path-map.json invalid schema: project "${name}" hosts must be an object`;
6661
- }
6662
- for (const [host, value] of Object.entries(hosts)) {
6663
- if (typeof value !== "string") {
6664
- return `path-map.json invalid schema: project "${name}" host "${host}" path must be a string`;
6665
- }
6666
- }
6667
- }
6668
- return null;
6669
- }
6670
6942
  function lookupLocalPath(map, recordedCwd) {
6671
6943
  for (const [logical, hosts] of Object.entries(map.projects)) {
6672
6944
  const isUnderMappedPath = Object.values(hosts).some(
@@ -6775,6 +7047,19 @@ try {
6775
7047
  cmdEject({ dryRun: ejectArgs.dryRun });
6776
7048
  break;
6777
7049
  }
7050
+ case "capture-settings": {
7051
+ const captureArgs = parseCaptureSettingsArgs(process.argv);
7052
+ if (captureArgs === null) {
7053
+ console.error("usage: nomad capture-settings [--host] [--dry-run] [--yes]");
7054
+ process.exit(1);
7055
+ }
7056
+ await cmdCaptureSettings({
7057
+ host: captureArgs.host,
7058
+ dryRun: captureArgs.dryRun,
7059
+ yes: captureArgs.yes
7060
+ });
7061
+ break;
7062
+ }
6778
7063
  case "doctor":
6779
7064
  if (process.argv[3] === void 0) {
6780
7065
  cmdDoctor();