claude-nomad 0.41.0 → 0.43.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/CHANGELOG.md +32 -0
- package/README.md +32 -0
- package/dist/nomad.mjs +798 -311
- package/package.json +1 -1
package/dist/nomad.mjs
CHANGED
|
@@ -449,6 +449,17 @@ function deepMerge(target, source) {
|
|
|
449
449
|
}
|
|
450
450
|
return out;
|
|
451
451
|
}
|
|
452
|
+
function sortKeysDeep(value) {
|
|
453
|
+
if (Array.isArray(value)) return value.map(sortKeysDeep);
|
|
454
|
+
if (value !== null && typeof value === "object") {
|
|
455
|
+
const out = {};
|
|
456
|
+
for (const key of Object.keys(value).sort((a, b) => a.localeCompare(b, "en"))) {
|
|
457
|
+
out[key] = sortKeysDeep(value[key]);
|
|
458
|
+
}
|
|
459
|
+
return out;
|
|
460
|
+
}
|
|
461
|
+
return value;
|
|
462
|
+
}
|
|
452
463
|
var encodePath;
|
|
453
464
|
var init_utils_json = __esm({
|
|
454
465
|
"src/utils.json.ts"() {
|
|
@@ -550,31 +561,31 @@ var init_utils_fs = __esm({
|
|
|
550
561
|
});
|
|
551
562
|
|
|
552
563
|
// src/push-gitleaks.config.ts
|
|
553
|
-
import { existsSync as
|
|
564
|
+
import { existsSync as existsSync10, mkdtempSync, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
554
565
|
import { tmpdir } from "node:os";
|
|
555
|
-
import { join as
|
|
566
|
+
import { join as join11 } from "node:path";
|
|
556
567
|
import { fileURLToPath } from "node:url";
|
|
557
568
|
function resolveTomlPath() {
|
|
558
|
-
const repoToml =
|
|
559
|
-
if (
|
|
569
|
+
const repoToml = join11(REPO_HOME, ".gitleaks.toml");
|
|
570
|
+
if (existsSync10(repoToml)) return repoToml;
|
|
560
571
|
const bundled = fileURLToPath(new URL("../.gitleaks.toml", import.meta.url));
|
|
561
|
-
return
|
|
572
|
+
return existsSync10(bundled) ? bundled : null;
|
|
562
573
|
}
|
|
563
574
|
function buildOverlayTempConfig(overlayBody, bundled) {
|
|
564
575
|
const tempBody = `[extend]
|
|
565
576
|
path = ${JSON.stringify(bundled)}
|
|
566
577
|
|
|
567
578
|
${overlayBody}`;
|
|
568
|
-
const tempPath = mkdtempSync(
|
|
569
|
-
const configPath =
|
|
579
|
+
const tempPath = mkdtempSync(join11(tmpdir(), "nomad-gitleaks-cfg-"));
|
|
580
|
+
const configPath = join11(tempPath, "config.toml");
|
|
570
581
|
writeFileSync2(configPath, tempBody, { mode: 384, flag: "wx" });
|
|
571
582
|
return { configPath, tempPath };
|
|
572
583
|
}
|
|
573
584
|
function resolveTomlConfig() {
|
|
574
|
-
const overlayPath =
|
|
575
|
-
const repoToml =
|
|
585
|
+
const overlayPath = join11(REPO_HOME, ".gitleaks.overlay.toml");
|
|
586
|
+
const repoToml = join11(REPO_HOME, ".gitleaks.toml");
|
|
576
587
|
const bundled = resolveTomlPath();
|
|
577
|
-
if (!
|
|
588
|
+
if (!existsSync10(overlayPath)) {
|
|
578
589
|
return { path: bundled, tempPath: null };
|
|
579
590
|
}
|
|
580
591
|
if (bundled === repoToml) {
|
|
@@ -615,9 +626,9 @@ var init_push_gitleaks_config = __esm({
|
|
|
615
626
|
|
|
616
627
|
// src/push-checks.ts
|
|
617
628
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
618
|
-
import { readdirSync as
|
|
629
|
+
import { readdirSync as readdirSync4, rmSync as rmSync3 } from "node:fs";
|
|
619
630
|
import { homedir as homedir2, platform } from "node:os";
|
|
620
|
-
import { join as
|
|
631
|
+
import { join as join12 } from "node:path";
|
|
621
632
|
function gitleaksInstallHint() {
|
|
622
633
|
const head = "gitleaks not on PATH (required for nomad push). Install:";
|
|
623
634
|
const plat = platform();
|
|
@@ -655,12 +666,12 @@ function findGitlinks(dir) {
|
|
|
655
666
|
function walk(current) {
|
|
656
667
|
let entries;
|
|
657
668
|
try {
|
|
658
|
-
entries =
|
|
669
|
+
entries = readdirSync4(current, { withFileTypes: true });
|
|
659
670
|
} catch {
|
|
660
671
|
return;
|
|
661
672
|
}
|
|
662
673
|
for (const e of entries) {
|
|
663
|
-
const p =
|
|
674
|
+
const p = join12(current, e.name);
|
|
664
675
|
if (e.name === ".git") {
|
|
665
676
|
hits.push(p);
|
|
666
677
|
continue;
|
|
@@ -712,7 +723,7 @@ var init_push_checks = __esm({
|
|
|
712
723
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
713
724
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync as rmSync4 } from "node:fs";
|
|
714
725
|
import { homedir as homedir3 } from "node:os";
|
|
715
|
-
import { join as
|
|
726
|
+
import { join as join16 } from "node:path";
|
|
716
727
|
function readGitleaksReport(reportPath) {
|
|
717
728
|
try {
|
|
718
729
|
const raw = readFileSync4(reportPath, "utf8");
|
|
@@ -724,9 +735,9 @@ function readGitleaksReport(reportPath) {
|
|
|
724
735
|
}
|
|
725
736
|
}
|
|
726
737
|
function scanStagedTree(repoDir, forwardStreams = false) {
|
|
727
|
-
const cacheDir =
|
|
738
|
+
const cacheDir = join16(homedir3(), ".cache", "claude-nomad");
|
|
728
739
|
mkdirSync2(cacheDir, { recursive: true });
|
|
729
|
-
const reportPath =
|
|
740
|
+
const reportPath = join16(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
|
|
730
741
|
const { path: toml, tempPath } = resolveTomlConfig();
|
|
731
742
|
const args = [
|
|
732
743
|
"protect",
|
|
@@ -758,9 +769,9 @@ function scanStagedTree(repoDir, forwardStreams = false) {
|
|
|
758
769
|
}
|
|
759
770
|
}
|
|
760
771
|
function scanFile(filePath, forwardStreams = false) {
|
|
761
|
-
const cacheDir =
|
|
772
|
+
const cacheDir = join16(homedir3(), ".cache", "claude-nomad");
|
|
762
773
|
mkdirSync2(cacheDir, { recursive: true });
|
|
763
|
-
const reportPath =
|
|
774
|
+
const reportPath = join16(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
|
|
764
775
|
const { path: toml, tempPath } = resolveTomlConfig();
|
|
765
776
|
const args = [
|
|
766
777
|
"detect",
|
|
@@ -1209,8 +1220,8 @@ function cmdClean(opts, backupBase = BACKUP_BASE) {
|
|
|
1209
1220
|
}
|
|
1210
1221
|
|
|
1211
1222
|
// src/commands.doctor.ts
|
|
1212
|
-
import { existsSync as
|
|
1213
|
-
import { join as
|
|
1223
|
+
import { existsSync as existsSync21 } from "node:fs";
|
|
1224
|
+
import { join as join25 } from "node:path";
|
|
1214
1225
|
|
|
1215
1226
|
// src/commands.doctor.checks.repo.ts
|
|
1216
1227
|
init_color();
|
|
@@ -1230,6 +1241,9 @@ function section(header, raw = false) {
|
|
|
1230
1241
|
function addItem(s, text) {
|
|
1231
1242
|
s.items.push(text);
|
|
1232
1243
|
}
|
|
1244
|
+
function addChildItem(s, text) {
|
|
1245
|
+
s.items.push(` ${text}`);
|
|
1246
|
+
}
|
|
1233
1247
|
function sectionFailed(s) {
|
|
1234
1248
|
return s.items.some((line) => line.includes(failGlyph));
|
|
1235
1249
|
}
|
|
@@ -1246,12 +1260,27 @@ function renderSection(s) {
|
|
|
1246
1260
|
}
|
|
1247
1261
|
const header = sectionFailed(s) ? `${red(FAIL_GLYPH_BARE)} ${s.header}` : s.header;
|
|
1248
1262
|
console.log(header);
|
|
1249
|
-
const lastContent = s.items.reduce(
|
|
1263
|
+
const lastContent = s.items.reduce(
|
|
1264
|
+
(acc, item, j) => item === "" || isChild(item) ? acc : j,
|
|
1265
|
+
-1
|
|
1266
|
+
);
|
|
1250
1267
|
for (let j = 0; j < s.items.length; j++) {
|
|
1251
|
-
|
|
1252
|
-
|
|
1268
|
+
const item = s.items[j];
|
|
1269
|
+
if (item === "") console.log("");
|
|
1270
|
+
else if (isChild(item)) console.log(renderChildLine(s.items, j));
|
|
1271
|
+
else console.log(`${j === lastContent ? " \u2514 " : " \u251C "}${item}`);
|
|
1253
1272
|
}
|
|
1254
1273
|
}
|
|
1274
|
+
function isChild(item) {
|
|
1275
|
+
return item.startsWith(" ");
|
|
1276
|
+
}
|
|
1277
|
+
function renderChildLine(items, j) {
|
|
1278
|
+
const parentContinues = items.some((it, k) => k > j && it !== "" && !isChild(it));
|
|
1279
|
+
const gutter = parentContinues ? " \u2502 " : " ";
|
|
1280
|
+
const next = items[j + 1];
|
|
1281
|
+
const elbow = next === void 0 || !isChild(next) ? "\u2514 " : "\u251C ";
|
|
1282
|
+
return `${gutter} ${elbow}${items[j].slice(1)}`;
|
|
1283
|
+
}
|
|
1255
1284
|
function renderTree(sections) {
|
|
1256
1285
|
const visible = sections.filter((s) => s.items.length > 0);
|
|
1257
1286
|
for (let i = 0; i < visible.length; i++) {
|
|
@@ -1321,7 +1350,11 @@ function isOverrideActive() {
|
|
|
1321
1350
|
return Boolean(process.env.NOMAD_REPO);
|
|
1322
1351
|
}
|
|
1323
1352
|
function reportHostAndPaths(section2) {
|
|
1324
|
-
|
|
1353
|
+
const unsetHint = process.env.NOMAD_HOST ? "" : dim(" (env unset, using hostname)");
|
|
1354
|
+
addItem(section2, `${dim(infoGlyph)} NOMAD_HOST: ${cyan(HOST)}${unsetHint}`);
|
|
1355
|
+
if (isOverrideActive()) {
|
|
1356
|
+
addItem(section2, `${dim(infoGlyph)} NOMAD_REPO: ${blue(REPO_HOME)}`);
|
|
1357
|
+
}
|
|
1325
1358
|
addItem(
|
|
1326
1359
|
section2,
|
|
1327
1360
|
`${existsSync6(REPO_HOME) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(REPO_HOME)}`
|
|
@@ -1427,7 +1460,7 @@ function loadAndReportSettings(section2) {
|
|
|
1427
1460
|
if (unknownKeys.length > 0) {
|
|
1428
1461
|
addItem(
|
|
1429
1462
|
section2,
|
|
1430
|
-
`${yellow(warnGlyph)} settings.json has unknown keys (schema drift?): ${unknownKeys.join(", ")}`
|
|
1463
|
+
`${yellow(warnGlyph)} settings.json has unknown keys (schema drift?): ${unknownKeys.join(", ")} (verify: nomad doctor --check-schema)`
|
|
1431
1464
|
);
|
|
1432
1465
|
} else {
|
|
1433
1466
|
addItem(section2, `${green(okGlyph)} settings.json schema: known keys only`);
|
|
@@ -1467,17 +1500,33 @@ function reportHostOverrides(section2, base, settings) {
|
|
|
1467
1500
|
// src/commands.doctor.checks.pathmap.ts
|
|
1468
1501
|
init_color();
|
|
1469
1502
|
init_config();
|
|
1470
|
-
import { existsSync as existsSync8 } from "node:fs";
|
|
1503
|
+
import { existsSync as existsSync8, readdirSync as readdirSync3 } from "node:fs";
|
|
1471
1504
|
import { join as join9 } from "node:path";
|
|
1472
1505
|
init_utils_json();
|
|
1473
1506
|
function reportMappedProjects(section2, map) {
|
|
1474
1507
|
const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
|
|
1475
|
-
addItem(
|
|
1476
|
-
section2,
|
|
1477
|
-
`${dim(infoGlyph)} mapped projects for ${cyan(HOST)}: ${dim(String(mapped.length))}`
|
|
1478
|
-
);
|
|
1508
|
+
addItem(section2, `Mapped projects for ${cyan(HOST)}: ${dim(String(mapped.length))}`);
|
|
1479
1509
|
for (const [name, hosts] of mapped) {
|
|
1480
|
-
|
|
1510
|
+
addChildItem(section2, `${name} -> ${blue(hosts[HOST])}`);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
function reportUnmappedProjects(section2, map) {
|
|
1514
|
+
const localProjects = join9(CLAUDE_HOME, "projects");
|
|
1515
|
+
if (!existsSync8(localProjects)) return;
|
|
1516
|
+
let localDirs;
|
|
1517
|
+
try {
|
|
1518
|
+
localDirs = readdirSync3(localProjects);
|
|
1519
|
+
} catch {
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
const mappedEncodings = new Set(
|
|
1523
|
+
Object.values(map.projects).map((hosts) => hosts[HOST]).filter(Boolean).map((abspath) => encodePath(abspath))
|
|
1524
|
+
);
|
|
1525
|
+
const unmapped = localDirs.filter((dir) => !mappedEncodings.has(dir));
|
|
1526
|
+
if (unmapped.length === 0) return;
|
|
1527
|
+
addItem(section2, `Unmapped local projects (not synced): ${dim(String(unmapped.length))}`);
|
|
1528
|
+
for (const dir of unmapped) {
|
|
1529
|
+
addChildItem(section2, dim(dir));
|
|
1481
1530
|
}
|
|
1482
1531
|
}
|
|
1483
1532
|
function reportPathCollisions(section2, map) {
|
|
@@ -1541,28 +1590,48 @@ function reportPathMap(section2) {
|
|
|
1541
1590
|
}
|
|
1542
1591
|
}
|
|
1543
1592
|
reportMappedProjects(section2, map);
|
|
1593
|
+
reportUnmappedProjects(section2, map);
|
|
1544
1594
|
reportPathCollisions(section2, map);
|
|
1545
1595
|
}
|
|
1546
1596
|
function reportNeverSync(section2) {
|
|
1547
|
-
addItem(
|
|
1597
|
+
addItem(
|
|
1598
|
+
section2,
|
|
1599
|
+
`${dim(infoGlyph)} never-sync items: ${NEVER_SYNC.size} protected ${dim(
|
|
1600
|
+
"(https://funkadelic.github.io/claude-nomad/how-it-works/)"
|
|
1601
|
+
)}`
|
|
1602
|
+
);
|
|
1548
1603
|
}
|
|
1549
1604
|
|
|
1550
1605
|
// src/commands.doctor.checks.repository.ts
|
|
1551
1606
|
init_color();
|
|
1552
1607
|
init_config();
|
|
1553
1608
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
1554
|
-
import { existsSync as
|
|
1555
|
-
import { join as
|
|
1609
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
1610
|
+
import { join as join13, relative as relative2 } from "node:path";
|
|
1611
|
+
|
|
1612
|
+
// src/commands.pull.wedge.ts
|
|
1613
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
1614
|
+
import { join as join10 } from "node:path";
|
|
1615
|
+
function detectWedge(repo) {
|
|
1616
|
+
const g = join10(repo, ".git");
|
|
1617
|
+
if (existsSync9(join10(g, "rebase-merge")) || existsSync9(join10(g, "rebase-apply"))) return "rebase";
|
|
1618
|
+
if (existsSync9(join10(g, "MERGE_HEAD"))) return "merge";
|
|
1619
|
+
return null;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
// src/commands.doctor.checks.repository.ts
|
|
1556
1623
|
init_push_checks();
|
|
1557
1624
|
init_utils();
|
|
1558
1625
|
function reportGitleaksProbe(section2) {
|
|
1559
1626
|
try {
|
|
1560
|
-
|
|
1561
|
-
addItem(section2, `${green(okGlyph)} gitleaks: ${dim(v)}`);
|
|
1627
|
+
execFileSync3("gitleaks", ["version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
1562
1628
|
return true;
|
|
1563
1629
|
} catch (err) {
|
|
1564
1630
|
if (err.code === "ENOENT") {
|
|
1565
|
-
addItem(
|
|
1631
|
+
addItem(
|
|
1632
|
+
section2,
|
|
1633
|
+
`${yellow(warnGlyph)} gitleaks: not on PATH (required for nomad push; install: https://github.com/gitleaks/gitleaks)`
|
|
1634
|
+
);
|
|
1566
1635
|
} else {
|
|
1567
1636
|
addItem(section2, `${red(failGlyph)} gitleaks: probe failed: ${err.message}`);
|
|
1568
1637
|
process.exitCode = 1;
|
|
@@ -1571,8 +1640,8 @@ function reportGitleaksProbe(section2) {
|
|
|
1571
1640
|
}
|
|
1572
1641
|
}
|
|
1573
1642
|
function reportGitlinks(section2) {
|
|
1574
|
-
const sharedDir =
|
|
1575
|
-
if (
|
|
1643
|
+
const sharedDir = join13(REPO_HOME, "shared");
|
|
1644
|
+
if (existsSync11(sharedDir)) {
|
|
1576
1645
|
const gitlinks = findGitlinks(sharedDir);
|
|
1577
1646
|
for (const p of gitlinks) {
|
|
1578
1647
|
const rel = relative2(REPO_HOME, p);
|
|
@@ -1611,16 +1680,30 @@ function reportRebaseClean(section2) {
|
|
|
1611
1680
|
} catch {
|
|
1612
1681
|
}
|
|
1613
1682
|
}
|
|
1683
|
+
function reportRebaseState(section2) {
|
|
1684
|
+
try {
|
|
1685
|
+
const wedge = detectWedge(REPO_HOME);
|
|
1686
|
+
if (wedge !== null) {
|
|
1687
|
+
const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
|
|
1688
|
+
addItem(
|
|
1689
|
+
section2,
|
|
1690
|
+
`${red(failGlyph)} repo is ${state}: run 'nomad pull --force-remote' to auto-recover`
|
|
1691
|
+
);
|
|
1692
|
+
process.exitCode = 1;
|
|
1693
|
+
}
|
|
1694
|
+
} catch {
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1614
1697
|
|
|
1615
1698
|
// src/commands.doctor.checks.backups.ts
|
|
1616
1699
|
init_color();
|
|
1617
|
-
import { existsSync as
|
|
1618
|
-
import { join as
|
|
1700
|
+
import { existsSync as existsSync12, lstatSync as lstatSync5, readdirSync as readdirSync5 } from "node:fs";
|
|
1701
|
+
import { join as join14 } from "node:path";
|
|
1619
1702
|
init_config();
|
|
1620
1703
|
var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
|
|
1621
1704
|
function safeReaddir(dir) {
|
|
1622
1705
|
try {
|
|
1623
|
-
return
|
|
1706
|
+
return readdirSync5(dir);
|
|
1624
1707
|
} catch {
|
|
1625
1708
|
return [];
|
|
1626
1709
|
}
|
|
@@ -1631,7 +1714,7 @@ var BYTES_PER_MB = 1024 * 1024;
|
|
|
1631
1714
|
function dirSizeBytes(dir) {
|
|
1632
1715
|
let bytes = 0;
|
|
1633
1716
|
for (const entry of safeReaddir(dir)) {
|
|
1634
|
-
const full =
|
|
1717
|
+
const full = join14(dir, entry);
|
|
1635
1718
|
const st = lstatSync5(full, { throwIfNoEntry: false });
|
|
1636
1719
|
if (!st) continue;
|
|
1637
1720
|
if (st.isSymbolicLink()) continue;
|
|
@@ -1642,11 +1725,11 @@ function dirSizeBytes(dir) {
|
|
|
1642
1725
|
}
|
|
1643
1726
|
function totalSizeMb(backupBase, dirs) {
|
|
1644
1727
|
let bytes = 0;
|
|
1645
|
-
for (const name of dirs) bytes += dirSizeBytes(
|
|
1728
|
+
for (const name of dirs) bytes += dirSizeBytes(join14(backupBase, name));
|
|
1646
1729
|
return bytes / BYTES_PER_MB;
|
|
1647
1730
|
}
|
|
1648
1731
|
function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
|
|
1649
|
-
if (!
|
|
1732
|
+
if (!existsSync12(backupBase)) return;
|
|
1650
1733
|
const dirs = safeReaddir(backupBase).filter((n) => TS_SHAPE2.test(n));
|
|
1651
1734
|
const count = dirs.length;
|
|
1652
1735
|
const sizeMb = totalSizeMb(backupBase, dirs);
|
|
@@ -1660,8 +1743,8 @@ function reportBackupsCheck(section2, backupBase = BACKUP_BASE) {
|
|
|
1660
1743
|
|
|
1661
1744
|
// src/commands.doctor.check-schema.ts
|
|
1662
1745
|
init_color();
|
|
1663
|
-
import { existsSync as
|
|
1664
|
-
import { join as
|
|
1746
|
+
import { existsSync as existsSync13 } from "node:fs";
|
|
1747
|
+
import { join as join15 } from "node:path";
|
|
1665
1748
|
init_config();
|
|
1666
1749
|
|
|
1667
1750
|
// src/http-fetch.ts
|
|
@@ -1698,8 +1781,8 @@ function fetchSchemaKeys() {
|
|
|
1698
1781
|
}
|
|
1699
1782
|
}
|
|
1700
1783
|
function reportCheckSchema(section2) {
|
|
1701
|
-
const settingsPath =
|
|
1702
|
-
if (!
|
|
1784
|
+
const settingsPath = join15(CLAUDE_HOME, "settings.json");
|
|
1785
|
+
if (!existsSync13(settingsPath)) {
|
|
1703
1786
|
addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
|
|
1704
1787
|
return;
|
|
1705
1788
|
}
|
|
@@ -1729,18 +1812,18 @@ function reportCheckSchema(section2) {
|
|
|
1729
1812
|
init_color();
|
|
1730
1813
|
import { randomBytes } from "node:crypto";
|
|
1731
1814
|
import { execFileSync as execFileSync6 } from "node:child_process";
|
|
1732
|
-
import { existsSync as
|
|
1815
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync4, readdirSync as readdirSync7, rmSync as rmSync6 } from "node:fs";
|
|
1733
1816
|
import { homedir as homedir4 } from "node:os";
|
|
1734
|
-
import { join as
|
|
1817
|
+
import { join as join19 } from "node:path";
|
|
1735
1818
|
|
|
1736
1819
|
// src/commands.doctor.check-shared.scan.ts
|
|
1737
1820
|
init_color();
|
|
1738
|
-
import { join as
|
|
1821
|
+
import { join as join17 } from "node:path";
|
|
1739
1822
|
init_config();
|
|
1740
1823
|
init_push_gitleaks();
|
|
1741
1824
|
function scrubPath(logical, sid, logicalToEncoded) {
|
|
1742
1825
|
const encoded = logicalToEncoded.get(logical) ?? logical;
|
|
1743
|
-
return
|
|
1826
|
+
return join17(CLAUDE_HOME, "projects", encoded, `${sid}.jsonl`);
|
|
1744
1827
|
}
|
|
1745
1828
|
function reportSessionFindings(section2, bySession) {
|
|
1746
1829
|
for (const [sid, counts] of bySession) {
|
|
@@ -1832,8 +1915,8 @@ init_config();
|
|
|
1832
1915
|
init_utils();
|
|
1833
1916
|
init_utils_fs();
|
|
1834
1917
|
init_utils_json();
|
|
1835
|
-
import { cpSync as cpSync3, existsSync as
|
|
1836
|
-
import { join as
|
|
1918
|
+
import { cpSync as cpSync3, existsSync as existsSync14, mkdirSync as mkdirSync3, readdirSync as readdirSync6, rmSync as rmSync5, statSync as statSync4 } from "node:fs";
|
|
1919
|
+
import { join as join18, relative as relative3, sep } from "node:path";
|
|
1837
1920
|
function copyDir(src, dst) {
|
|
1838
1921
|
rmSync5(dst, { recursive: true, force: true });
|
|
1839
1922
|
cpSync3(src, dst, { recursive: true, force: true });
|
|
@@ -1863,15 +1946,15 @@ function remapPull(ts, opts = {}) {
|
|
|
1863
1946
|
let unmapped = 0;
|
|
1864
1947
|
const pulled = [];
|
|
1865
1948
|
const wouldPull = [];
|
|
1866
|
-
const mapPath =
|
|
1867
|
-
const repoProjects =
|
|
1868
|
-
if (!
|
|
1949
|
+
const mapPath = join18(REPO_HOME, "path-map.json");
|
|
1950
|
+
const repoProjects = join18(REPO_HOME, "shared", "projects");
|
|
1951
|
+
if (!existsSync14(mapPath) || !existsSync14(repoProjects)) {
|
|
1869
1952
|
const text = "no path-map or repo projects dir; skipping session remap";
|
|
1870
1953
|
emitPreview(opts.onPreview, { kind: "note", text }, text);
|
|
1871
1954
|
return { unmapped: 0, pulled, wouldPull };
|
|
1872
1955
|
}
|
|
1873
1956
|
const map = readJson(mapPath);
|
|
1874
|
-
const localProjects =
|
|
1957
|
+
const localProjects = join18(CLAUDE_HOME, "projects");
|
|
1875
1958
|
if (!dryRun) mkdirSync3(localProjects, { recursive: true });
|
|
1876
1959
|
for (const [logical, hosts] of Object.entries(map.projects)) {
|
|
1877
1960
|
assertSafeLogical(logical);
|
|
@@ -1880,9 +1963,9 @@ function remapPull(ts, opts = {}) {
|
|
|
1880
1963
|
unmapped++;
|
|
1881
1964
|
continue;
|
|
1882
1965
|
}
|
|
1883
|
-
const src =
|
|
1884
|
-
if (!
|
|
1885
|
-
const dst =
|
|
1966
|
+
const src = join18(repoProjects, logical);
|
|
1967
|
+
if (!existsSync14(src)) continue;
|
|
1968
|
+
const dst = join18(localProjects, encodePath(localPath));
|
|
1886
1969
|
if (dryRun) {
|
|
1887
1970
|
wouldPull.push(logical);
|
|
1888
1971
|
emitPreview(
|
|
@@ -1927,30 +2010,30 @@ function remapPush(ts, opts = {}) {
|
|
|
1927
2010
|
let unmapped = 0;
|
|
1928
2011
|
const pushed = [];
|
|
1929
2012
|
const wouldPush = [];
|
|
1930
|
-
const mapPath =
|
|
1931
|
-
if (!
|
|
2013
|
+
const mapPath = join18(REPO_HOME, "path-map.json");
|
|
2014
|
+
if (!existsSync14(mapPath)) {
|
|
1932
2015
|
log("no path-map.json; skipping session export");
|
|
1933
2016
|
return { unmapped: 0, collisions: 0, pushed, wouldPush };
|
|
1934
2017
|
}
|
|
1935
2018
|
const map = readJson(mapPath);
|
|
1936
|
-
const localProjects =
|
|
1937
|
-
const repoProjects =
|
|
2019
|
+
const localProjects = join18(CLAUDE_HOME, "projects");
|
|
2020
|
+
const repoProjects = join18(REPO_HOME, "shared", "projects");
|
|
1938
2021
|
const reverse = buildReverseMap(map);
|
|
1939
|
-
if (!
|
|
2022
|
+
if (!existsSync14(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
|
|
1940
2023
|
if (!dryRun) mkdirSync3(repoProjects, { recursive: true });
|
|
1941
|
-
for (const dir of
|
|
2024
|
+
for (const dir of readdirSync6(localProjects)) {
|
|
1942
2025
|
const logical = reverse.get(dir);
|
|
1943
2026
|
if (!logical) {
|
|
1944
2027
|
unmapped++;
|
|
1945
2028
|
continue;
|
|
1946
2029
|
}
|
|
1947
|
-
const repoDst =
|
|
2030
|
+
const repoDst = join18(repoProjects, logical);
|
|
1948
2031
|
if (dryRun) {
|
|
1949
2032
|
wouldPush.push(logical);
|
|
1950
2033
|
continue;
|
|
1951
2034
|
}
|
|
1952
2035
|
backupRepoWrite(repoDst, ts, REPO_HOME);
|
|
1953
|
-
copyDirJsonlOnly(
|
|
2036
|
+
copyDirJsonlOnly(join18(localProjects, dir), repoDst);
|
|
1954
2037
|
pushed.push(logical);
|
|
1955
2038
|
}
|
|
1956
2039
|
return { unmapped, collisions: 0, pushed, wouldPush };
|
|
@@ -1962,8 +2045,8 @@ init_utils_json();
|
|
|
1962
2045
|
function buildScanTree(tmpRoot) {
|
|
1963
2046
|
const logicalToEncoded = /* @__PURE__ */ new Map();
|
|
1964
2047
|
let staged = 0;
|
|
1965
|
-
const mapPath =
|
|
1966
|
-
if (!
|
|
2048
|
+
const mapPath = join19(REPO_HOME, "path-map.json");
|
|
2049
|
+
if (!existsSync15(mapPath)) return { logicalToEncoded, staged, malformed: false };
|
|
1967
2050
|
let map;
|
|
1968
2051
|
try {
|
|
1969
2052
|
map = readJson(mapPath);
|
|
@@ -1980,12 +2063,12 @@ function buildScanTree(tmpRoot) {
|
|
|
1980
2063
|
if (!p || p === "TBD") continue;
|
|
1981
2064
|
reverse.set(encodePath(p), logical);
|
|
1982
2065
|
}
|
|
1983
|
-
const localProjects =
|
|
1984
|
-
if (!
|
|
1985
|
-
for (const dir of
|
|
2066
|
+
const localProjects = join19(CLAUDE_HOME, "projects");
|
|
2067
|
+
if (!existsSync15(localProjects)) return { logicalToEncoded, staged, malformed: false };
|
|
2068
|
+
for (const dir of readdirSync7(localProjects)) {
|
|
1986
2069
|
const logical = reverse.get(dir);
|
|
1987
2070
|
if (!logical) continue;
|
|
1988
|
-
copyDirJsonlOnly(
|
|
2071
|
+
copyDirJsonlOnly(join19(localProjects, dir), join19(tmpRoot, "shared", "projects", logical));
|
|
1989
2072
|
logicalToEncoded.set(logical, dir);
|
|
1990
2073
|
staged++;
|
|
1991
2074
|
}
|
|
@@ -2016,11 +2099,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
|
|
|
2016
2099
|
}
|
|
2017
2100
|
function reportCheckShared(section2, gitleaksReady) {
|
|
2018
2101
|
if (!ensureGitleaksReady(section2, gitleaksReady)) return;
|
|
2019
|
-
const cacheDir =
|
|
2102
|
+
const cacheDir = join19(homedir4(), ".cache", "claude-nomad");
|
|
2020
2103
|
mkdirSync4(cacheDir, { recursive: true });
|
|
2021
2104
|
const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
2022
|
-
const reportPath =
|
|
2023
|
-
const tmpRoot =
|
|
2105
|
+
const reportPath = join19(cacheDir, `check-shared-${stamp}.json`);
|
|
2106
|
+
const tmpRoot = join19(cacheDir, `check-shared-tree-${stamp}`);
|
|
2024
2107
|
try {
|
|
2025
2108
|
const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
|
|
2026
2109
|
if (malformed) {
|
|
@@ -2041,8 +2124,8 @@ function reportCheckShared(section2, gitleaksReady) {
|
|
|
2041
2124
|
|
|
2042
2125
|
// src/commands.doctor.checks.hooks.scope.ts
|
|
2043
2126
|
init_color();
|
|
2044
|
-
import { existsSync as
|
|
2045
|
-
import { dirname as dirname2, extname, join as
|
|
2127
|
+
import { existsSync as existsSync16, readFileSync as readFileSync5, readdirSync as readdirSync8, realpathSync } from "node:fs";
|
|
2128
|
+
import { dirname as dirname2, extname, join as join20 } from "node:path";
|
|
2046
2129
|
init_config();
|
|
2047
2130
|
function typeFromPackageJson(pkgPath) {
|
|
2048
2131
|
try {
|
|
@@ -2064,8 +2147,8 @@ function effectiveType(hookPath) {
|
|
|
2064
2147
|
}
|
|
2065
2148
|
let dir = dirname2(real);
|
|
2066
2149
|
for (; ; ) {
|
|
2067
|
-
const pkg =
|
|
2068
|
-
if (
|
|
2150
|
+
const pkg = join20(dir, "package.json");
|
|
2151
|
+
if (existsSync16(pkg)) return typeFromPackageJson(pkg);
|
|
2069
2152
|
const parent = dirname2(dir);
|
|
2070
2153
|
if (parent === dir) return "cjs";
|
|
2071
2154
|
dir = parent;
|
|
@@ -2094,7 +2177,7 @@ function remedy(family) {
|
|
|
2094
2177
|
}
|
|
2095
2178
|
function safeReaddir2(dir) {
|
|
2096
2179
|
try {
|
|
2097
|
-
return
|
|
2180
|
+
return readdirSync8(dir);
|
|
2098
2181
|
} catch {
|
|
2099
2182
|
return [];
|
|
2100
2183
|
}
|
|
@@ -2107,15 +2190,15 @@ function safeRead(path) {
|
|
|
2107
2190
|
}
|
|
2108
2191
|
}
|
|
2109
2192
|
function reportHookScopeCheck(section2) {
|
|
2110
|
-
const hooksDir =
|
|
2111
|
-
if (!
|
|
2193
|
+
const hooksDir = join20(CLAUDE_HOME, "hooks");
|
|
2194
|
+
if (!existsSync16(hooksDir)) {
|
|
2112
2195
|
addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
|
|
2113
2196
|
return;
|
|
2114
2197
|
}
|
|
2115
2198
|
let anyWarn = false;
|
|
2116
2199
|
for (const name of safeReaddir2(hooksDir)) {
|
|
2117
2200
|
if (extname(name) !== ".js") continue;
|
|
2118
|
-
const abs =
|
|
2201
|
+
const abs = join20(hooksDir, name);
|
|
2119
2202
|
const eff = effectiveType(abs);
|
|
2120
2203
|
if (eff === null) continue;
|
|
2121
2204
|
const src = safeRead(abs);
|
|
@@ -2135,8 +2218,8 @@ function reportHookScopeCheck(section2) {
|
|
|
2135
2218
|
|
|
2136
2219
|
// src/commands.doctor.checks.hooks.ts
|
|
2137
2220
|
init_color();
|
|
2138
|
-
import { existsSync as
|
|
2139
|
-
import { join as
|
|
2221
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
2222
|
+
import { join as join21 } from "node:path";
|
|
2140
2223
|
init_config();
|
|
2141
2224
|
function expandHome(token) {
|
|
2142
2225
|
return token.replace(/^\$\{HOME\}/, HOME).replace(/^\$HOME/, HOME).replace(/^~/, HOME);
|
|
@@ -2174,8 +2257,11 @@ function checkEventGroups(section2, event, groups) {
|
|
|
2174
2257
|
for (const group of groups) {
|
|
2175
2258
|
for (const cmd of commandsFromGroup(group)) {
|
|
2176
2259
|
for (const resolved of claudePathsIn(cmd)) {
|
|
2177
|
-
if (
|
|
2178
|
-
addItem(
|
|
2260
|
+
if (existsSync17(resolved)) continue;
|
|
2261
|
+
addItem(
|
|
2262
|
+
section2,
|
|
2263
|
+
`${red(failGlyph)} hooks/${event}: command target missing: ${resolved} (run nomad pull)`
|
|
2264
|
+
);
|
|
2179
2265
|
process.exitCode = 1;
|
|
2180
2266
|
anyFail = true;
|
|
2181
2267
|
}
|
|
@@ -2184,8 +2270,8 @@ function checkEventGroups(section2, event, groups) {
|
|
|
2184
2270
|
return anyFail;
|
|
2185
2271
|
}
|
|
2186
2272
|
function reportHooksTargetCheck(section2) {
|
|
2187
|
-
const settingsPath =
|
|
2188
|
-
if (!
|
|
2273
|
+
const settingsPath = join21(CLAUDE_HOME, "settings.json");
|
|
2274
|
+
if (!existsSync17(settingsPath)) {
|
|
2189
2275
|
addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
|
|
2190
2276
|
return;
|
|
2191
2277
|
}
|
|
@@ -2206,17 +2292,221 @@ function reportHooksTargetCheck(section2) {
|
|
|
2206
2292
|
}
|
|
2207
2293
|
}
|
|
2208
2294
|
|
|
2295
|
+
// src/commands.doctor.checks.hooks.preserve-symlinks.ts
|
|
2296
|
+
init_color();
|
|
2297
|
+
import { existsSync as existsSync19, readFileSync as readFileSync6 } from "node:fs";
|
|
2298
|
+
import { join as join23 } from "node:path";
|
|
2299
|
+
|
|
2300
|
+
// src/commands.doctor.checks.hooks.preserve-symlinks.probe.ts
|
|
2301
|
+
import { closeSync as closeSync2, existsSync as existsSync18, openSync as openSync2, readSync, realpathSync as realpathSync2 } from "node:fs";
|
|
2302
|
+
import { dirname as dirname3, join as join22, resolve as resolve2 } from "node:path";
|
|
2303
|
+
function suppressedRanges(src) {
|
|
2304
|
+
const ranges = [];
|
|
2305
|
+
const re = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'[^']*'|"[^"]*"|`[^`]*`/g;
|
|
2306
|
+
let m;
|
|
2307
|
+
while ((m = re.exec(src)) !== null) {
|
|
2308
|
+
ranges.push([m.index, m.index + m[0].length]);
|
|
2309
|
+
}
|
|
2310
|
+
return ranges;
|
|
2311
|
+
}
|
|
2312
|
+
function inSuppressedRange(pos, ranges) {
|
|
2313
|
+
for (const [start, end] of ranges) {
|
|
2314
|
+
if (pos >= start && pos < end) return true;
|
|
2315
|
+
}
|
|
2316
|
+
return false;
|
|
2317
|
+
}
|
|
2318
|
+
function topRelativeSpecifiers(src) {
|
|
2319
|
+
const ranges = suppressedRanges(src);
|
|
2320
|
+
const specifiers = [];
|
|
2321
|
+
const reqRe = /\brequire\s*\(\s*(['"])(\.\.?\/[^'"]*)\1\s*\)/g;
|
|
2322
|
+
let m;
|
|
2323
|
+
while ((m = reqRe.exec(src)) !== null) {
|
|
2324
|
+
if (!inSuppressedRange(m.index, ranges)) specifiers.push(m[2]);
|
|
2325
|
+
}
|
|
2326
|
+
const impRe = /\bfrom\s+(['"])(\.\.?\/[^'"]*)\1/g;
|
|
2327
|
+
while ((m = impRe.exec(src)) !== null) {
|
|
2328
|
+
if (!inSuppressedRange(m.index, ranges)) specifiers.push(m[2]);
|
|
2329
|
+
}
|
|
2330
|
+
return specifiers;
|
|
2331
|
+
}
|
|
2332
|
+
function specifierIsMissing(specifier, baseDir) {
|
|
2333
|
+
const base = resolve2(baseDir, specifier);
|
|
2334
|
+
if (existsSync18(base)) return false;
|
|
2335
|
+
for (const ext of [".js", ".cjs", ".mjs"]) {
|
|
2336
|
+
if (existsSync18(base + ext)) return false;
|
|
2337
|
+
}
|
|
2338
|
+
for (const idx of ["index.js", "index.cjs", "index.mjs"]) {
|
|
2339
|
+
if (existsSync18(join22(base, idx))) return false;
|
|
2340
|
+
}
|
|
2341
|
+
return true;
|
|
2342
|
+
}
|
|
2343
|
+
function relativeRequireTargetsBroken(scriptPath) {
|
|
2344
|
+
let realPath;
|
|
2345
|
+
try {
|
|
2346
|
+
realPath = realpathSync2(scriptPath);
|
|
2347
|
+
} catch {
|
|
2348
|
+
return false;
|
|
2349
|
+
}
|
|
2350
|
+
let raw;
|
|
2351
|
+
try {
|
|
2352
|
+
const fd = openSync2(realPath, "r");
|
|
2353
|
+
try {
|
|
2354
|
+
const buf = Buffer.alloc(65536);
|
|
2355
|
+
const bytesRead = readSync(fd, buf, 0, 65536, 0);
|
|
2356
|
+
raw = buf.toString("utf8", 0, bytesRead);
|
|
2357
|
+
} finally {
|
|
2358
|
+
closeSync2(fd);
|
|
2359
|
+
}
|
|
2360
|
+
} catch {
|
|
2361
|
+
return false;
|
|
2362
|
+
}
|
|
2363
|
+
const specifiers = topRelativeSpecifiers(raw);
|
|
2364
|
+
if (specifiers.length === 0) return false;
|
|
2365
|
+
const baseDir = dirname3(realPath);
|
|
2366
|
+
for (const spec of specifiers) {
|
|
2367
|
+
if (specifierIsMissing(spec, baseDir)) return true;
|
|
2368
|
+
}
|
|
2369
|
+
return false;
|
|
2370
|
+
}
|
|
2371
|
+
|
|
2372
|
+
// src/commands.doctor.checks.hooks.preserve-symlinks.ts
|
|
2373
|
+
init_config();
|
|
2374
|
+
function expandHome2(token) {
|
|
2375
|
+
return token.replace(/^\$\{HOME\}/, HOME).replace(/^\$HOME/, HOME).replace(/^~/, HOME);
|
|
2376
|
+
}
|
|
2377
|
+
function stripShellPunct(token) {
|
|
2378
|
+
const head = token.replace(/^['"]+/, "");
|
|
2379
|
+
let end = head.length;
|
|
2380
|
+
while (end > 0 && "'\"`;)|&>".includes(head[end - 1])) end--;
|
|
2381
|
+
return head.slice(0, end);
|
|
2382
|
+
}
|
|
2383
|
+
function commandTokens(command) {
|
|
2384
|
+
const tokens = [];
|
|
2385
|
+
for (const seg of command.split(/&&|\|\||;|\|/)) {
|
|
2386
|
+
for (const raw of seg.trim().split(/\s+/).filter(Boolean)) {
|
|
2387
|
+
tokens.push(expandHome2(stripShellPunct(raw)));
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return tokens;
|
|
2391
|
+
}
|
|
2392
|
+
function readPathMapSafe() {
|
|
2393
|
+
const mapPath = join23(REPO_HOME, "path-map.json");
|
|
2394
|
+
if (!existsSync19(mapPath)) return { projects: {} };
|
|
2395
|
+
try {
|
|
2396
|
+
return JSON.parse(readFileSync6(mapPath, "utf8"));
|
|
2397
|
+
} catch {
|
|
2398
|
+
return { projects: {} };
|
|
2399
|
+
}
|
|
2400
|
+
}
|
|
2401
|
+
function resolvesUnderSymlinkedShared(scriptPath, sharedLinkNames) {
|
|
2402
|
+
for (const name of sharedLinkNames) {
|
|
2403
|
+
const prefix = `${CLAUDE_HOME}/${name}/`;
|
|
2404
|
+
if (scriptPath.startsWith(prefix)) return true;
|
|
2405
|
+
}
|
|
2406
|
+
return false;
|
|
2407
|
+
}
|
|
2408
|
+
function nodeScriptArg(tokens, nodeIdx) {
|
|
2409
|
+
for (let i = nodeIdx + 1; i < tokens.length; i++) {
|
|
2410
|
+
const t = tokens[i];
|
|
2411
|
+
if (t.startsWith("-")) continue;
|
|
2412
|
+
if (t.endsWith(".js") || t.endsWith(".cjs")) return t;
|
|
2413
|
+
break;
|
|
2414
|
+
}
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
function hasPreserveSymlinksMain(tokens, nodeIdx) {
|
|
2418
|
+
for (let i = nodeIdx + 1; i < tokens.length; i++) {
|
|
2419
|
+
if (tokens[i] === "--preserve-symlinks-main") return true;
|
|
2420
|
+
if (!tokens[i].startsWith("-")) break;
|
|
2421
|
+
}
|
|
2422
|
+
return false;
|
|
2423
|
+
}
|
|
2424
|
+
function* commandsFromFlatEntries(entries) {
|
|
2425
|
+
for (const entry of entries) {
|
|
2426
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
2427
|
+
const e = entry;
|
|
2428
|
+
if (e.type === "command" && typeof e.command === "string") yield e.command;
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
function* commandsFromOneGroup(group) {
|
|
2432
|
+
if (typeof group !== "object" || group === null) return;
|
|
2433
|
+
const g = group;
|
|
2434
|
+
if (Array.isArray(g.hooks)) {
|
|
2435
|
+
yield* commandsFromFlatEntries(g.hooks);
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
if (g.type === "command" && typeof g.command === "string") yield g.command;
|
|
2439
|
+
}
|
|
2440
|
+
function flaggedScript(command, sharedLinkNames) {
|
|
2441
|
+
const tokens = commandTokens(command);
|
|
2442
|
+
const nodeIdx = tokens.indexOf("node");
|
|
2443
|
+
if (nodeIdx < 0) return null;
|
|
2444
|
+
if (hasPreserveSymlinksMain(tokens, nodeIdx)) return null;
|
|
2445
|
+
const script = nodeScriptArg(tokens, nodeIdx);
|
|
2446
|
+
if (script === null) return null;
|
|
2447
|
+
if (!resolvesUnderSymlinkedShared(script, sharedLinkNames)) return null;
|
|
2448
|
+
if (!relativeRequireTargetsBroken(script)) return null;
|
|
2449
|
+
return script;
|
|
2450
|
+
}
|
|
2451
|
+
function checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames) {
|
|
2452
|
+
let anyWarn = false;
|
|
2453
|
+
for (const group of groups) {
|
|
2454
|
+
for (const cmd of commandsFromOneGroup(group)) {
|
|
2455
|
+
const script = flaggedScript(cmd, sharedLinkNames);
|
|
2456
|
+
if (script === null) continue;
|
|
2457
|
+
addItem(
|
|
2458
|
+
section2,
|
|
2459
|
+
`${yellow(warnGlyph)} hooks/${event}: node ${script} needs --preserve-symlinks-main (add it to the hook command in shared/settings.base.json)`
|
|
2460
|
+
);
|
|
2461
|
+
anyWarn = true;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
return anyWarn;
|
|
2465
|
+
}
|
|
2466
|
+
function reportPreserveSymlinksCheck(section2) {
|
|
2467
|
+
const settingsPath = join23(CLAUDE_HOME, "settings.json");
|
|
2468
|
+
if (!existsSync19(settingsPath)) {
|
|
2469
|
+
addItem(
|
|
2470
|
+
section2,
|
|
2471
|
+
`${dim(infoGlyph)} no ~/.claude/settings.json; skipping preserve-symlinks-main check`
|
|
2472
|
+
);
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
let settings;
|
|
2476
|
+
try {
|
|
2477
|
+
settings = JSON.parse(readFileSync6(settingsPath, "utf8"));
|
|
2478
|
+
} catch {
|
|
2479
|
+
return;
|
|
2480
|
+
}
|
|
2481
|
+
if (settings === null) return;
|
|
2482
|
+
const hooks = settings.hooks;
|
|
2483
|
+
if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
|
|
2484
|
+
addItem(section2, `${green(okGlyph)} hooks: preserve-symlinks-main not needed`);
|
|
2485
|
+
return;
|
|
2486
|
+
}
|
|
2487
|
+
const map = readPathMapSafe();
|
|
2488
|
+
const sharedLinkNames = allSharedLinks(map);
|
|
2489
|
+
let anyWarn = false;
|
|
2490
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
2491
|
+
if (!Array.isArray(groups)) continue;
|
|
2492
|
+
if (checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames)) anyWarn = true;
|
|
2493
|
+
}
|
|
2494
|
+
if (!anyWarn) {
|
|
2495
|
+
addItem(section2, `${green(okGlyph)} hooks: preserve-symlinks-main not needed`);
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2209
2499
|
// src/commands.doctor.ts
|
|
2210
2500
|
init_config();
|
|
2211
2501
|
|
|
2212
2502
|
// src/commands.doctor.engine.ts
|
|
2213
2503
|
init_color();
|
|
2214
|
-
import { readFileSync as
|
|
2504
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
2215
2505
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2216
2506
|
|
|
2217
2507
|
// src/commands.doctor.version.ts
|
|
2218
2508
|
init_color();
|
|
2219
|
-
import { readFileSync as
|
|
2509
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
2220
2510
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2221
2511
|
init_config();
|
|
2222
2512
|
var STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
|
|
@@ -2233,7 +2523,7 @@ function compareSemver(a, b) {
|
|
|
2233
2523
|
function readLocalVersion() {
|
|
2234
2524
|
try {
|
|
2235
2525
|
const pkgPath = fileURLToPath2(new URL("../package.json", import.meta.url));
|
|
2236
|
-
const parsed = JSON.parse(
|
|
2526
|
+
const parsed = JSON.parse(readFileSync7(pkgPath, "utf8"));
|
|
2237
2527
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
2238
2528
|
return parsed.version;
|
|
2239
2529
|
}
|
|
@@ -2260,7 +2550,13 @@ function reportVersionCheck(section2) {
|
|
|
2260
2550
|
const localPure = STRICT_SEMVER_PREFIX.exec(local)?.[1] ?? null;
|
|
2261
2551
|
if (localPure === null) return;
|
|
2262
2552
|
const latest = fetchLatestVersion();
|
|
2263
|
-
if (latest === null)
|
|
2553
|
+
if (latest === null) {
|
|
2554
|
+
addItem(
|
|
2555
|
+
section2,
|
|
2556
|
+
`${dim(infoGlyph)} claude-nomad: ${local} (version check skipped: could not determine latest version)`
|
|
2557
|
+
);
|
|
2558
|
+
return;
|
|
2559
|
+
}
|
|
2264
2560
|
const cmp = compareSemver(localPure, latest);
|
|
2265
2561
|
if (cmp === 0) {
|
|
2266
2562
|
addItem(section2, `${green(okGlyph)} claude-nomad: ${local} (latest)`);
|
|
@@ -2286,7 +2582,7 @@ function parseMinVersion(spec) {
|
|
|
2286
2582
|
function readEnginesNode() {
|
|
2287
2583
|
try {
|
|
2288
2584
|
const pkgPath = fileURLToPath3(new URL("../package.json", import.meta.url));
|
|
2289
|
-
const parsed = JSON.parse(
|
|
2585
|
+
const parsed = JSON.parse(readFileSync8(pkgPath, "utf8"));
|
|
2290
2586
|
const node = parsed.engines?.node;
|
|
2291
2587
|
if (typeof node === "string" && node.length > 0) return node;
|
|
2292
2588
|
return null;
|
|
@@ -2315,8 +2611,8 @@ function reportNodeEngineCheck(section2) {
|
|
|
2315
2611
|
// src/commands.doctor.gitleaks-version.ts
|
|
2316
2612
|
init_color();
|
|
2317
2613
|
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
2318
|
-
import { existsSync as
|
|
2319
|
-
import { join as
|
|
2614
|
+
import { existsSync as existsSync20 } from "node:fs";
|
|
2615
|
+
import { join as join24 } from "node:path";
|
|
2320
2616
|
init_config();
|
|
2321
2617
|
var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
|
|
2322
2618
|
var GITLEAKS_TIMEOUT_MS = 5e3;
|
|
@@ -2325,7 +2621,7 @@ function majorMinorOf(value) {
|
|
|
2325
2621
|
return m === null ? null : [m[1], m[2]];
|
|
2326
2622
|
}
|
|
2327
2623
|
function readGitleaksVersion(run, tomlExists) {
|
|
2328
|
-
const tomlPath =
|
|
2624
|
+
const tomlPath = join24(REPO_HOME, ".gitleaks.toml");
|
|
2329
2625
|
const args = ["version"];
|
|
2330
2626
|
if (tomlExists(tomlPath)) args.push("--config", tomlPath);
|
|
2331
2627
|
try {
|
|
@@ -2337,7 +2633,7 @@ function readGitleaksVersion(run, tomlExists) {
|
|
|
2337
2633
|
return null;
|
|
2338
2634
|
}
|
|
2339
2635
|
}
|
|
2340
|
-
function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists =
|
|
2636
|
+
function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync20) {
|
|
2341
2637
|
const raw = readGitleaksVersion(run, tomlExists);
|
|
2342
2638
|
if (raw === null) return;
|
|
2343
2639
|
const local = majorMinorOf(raw);
|
|
@@ -2504,19 +2800,47 @@ function reportActionsDrift(section2, run = execFileSync10) {
|
|
|
2504
2800
|
);
|
|
2505
2801
|
}
|
|
2506
2802
|
|
|
2803
|
+
// src/commands.doctor.verdict.ts
|
|
2804
|
+
init_color();
|
|
2805
|
+
function isFailLine(item) {
|
|
2806
|
+
return item.includes(failGlyph);
|
|
2807
|
+
}
|
|
2808
|
+
function isWarnLine(item) {
|
|
2809
|
+
return !isFailLine(item) && item.includes(warnGlyph);
|
|
2810
|
+
}
|
|
2811
|
+
function buildVerdictSection(sections) {
|
|
2812
|
+
const summary = section("Summary");
|
|
2813
|
+
const lines = sections.flatMap((s) => s.items).map((item) => item.replace(/^\t/, ""));
|
|
2814
|
+
const failures = lines.filter(isFailLine);
|
|
2815
|
+
const warnings = lines.filter(isWarnLine);
|
|
2816
|
+
for (const line of [...failures, ...warnings]) addItem(summary, line);
|
|
2817
|
+
if (failures.length > 0) {
|
|
2818
|
+
addItem(
|
|
2819
|
+
summary,
|
|
2820
|
+
`${red(failGlyph)} ${failures.length} failure(s), ${warnings.length} warning(s)`
|
|
2821
|
+
);
|
|
2822
|
+
} else if (warnings.length > 0) {
|
|
2823
|
+
addItem(summary, `${yellow(warnGlyph)} ${warnings.length} warning(s)`);
|
|
2824
|
+
} else {
|
|
2825
|
+
addItem(summary, `${green(okGlyph)} healthy`);
|
|
2826
|
+
}
|
|
2827
|
+
return summary;
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2507
2830
|
// src/commands.doctor.ts
|
|
2508
2831
|
function cmdDoctor(opts = {}) {
|
|
2509
|
-
const host = section("
|
|
2832
|
+
const host = section("Environment");
|
|
2510
2833
|
reportHostAndPaths(host);
|
|
2511
2834
|
reportRepoState(host);
|
|
2512
2835
|
const links = section("Shared links");
|
|
2513
|
-
const mapPath =
|
|
2514
|
-
const rawMap =
|
|
2836
|
+
const mapPath = join25(REPO_HOME, "path-map.json");
|
|
2837
|
+
const rawMap = existsSync21(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
|
|
2515
2838
|
const map = rawMap ?? { projects: {} };
|
|
2516
2839
|
reportSharedLinks(links, map);
|
|
2517
2840
|
const hooksScan = section("Hook targets");
|
|
2518
2841
|
reportHooksTargetCheck(hooksScan);
|
|
2519
2842
|
reportHookScopeCheck(hooksScan);
|
|
2843
|
+
reportPreserveSymlinksCheck(hooksScan);
|
|
2520
2844
|
const settings = section("Settings");
|
|
2521
2845
|
const base = loadBaseSettings(settings);
|
|
2522
2846
|
const parsedSettings = loadAndReportSettings(settings);
|
|
@@ -2530,10 +2854,12 @@ function cmdDoctor(opts = {}) {
|
|
|
2530
2854
|
reportGitlinks(repository);
|
|
2531
2855
|
reportRemote(repository);
|
|
2532
2856
|
reportRebaseClean(repository);
|
|
2857
|
+
reportRebaseState(repository);
|
|
2533
2858
|
reportActionsDrift(repository);
|
|
2534
2859
|
const nomadVersion = section("Nomad Version");
|
|
2535
2860
|
reportVersionCheck(nomadVersion);
|
|
2536
|
-
|
|
2861
|
+
const housekeeping = section("Housekeeping");
|
|
2862
|
+
reportBackupsCheck(housekeeping);
|
|
2537
2863
|
const depVersions = section("Dependency Versions");
|
|
2538
2864
|
reportNodeEngineCheck(depVersions);
|
|
2539
2865
|
reportGitleaksVersionCheck(depVersions);
|
|
@@ -2542,7 +2868,7 @@ function cmdDoctor(opts = {}) {
|
|
|
2542
2868
|
if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
|
|
2543
2869
|
const schemaScan = section("Schema scan");
|
|
2544
2870
|
if (opts.checkSchema === true) reportCheckSchema(schemaScan);
|
|
2545
|
-
|
|
2871
|
+
const body = [
|
|
2546
2872
|
nomadVersion,
|
|
2547
2873
|
depVersions,
|
|
2548
2874
|
host,
|
|
@@ -2552,16 +2878,18 @@ function cmdDoctor(opts = {}) {
|
|
|
2552
2878
|
pathMap,
|
|
2553
2879
|
neverSync,
|
|
2554
2880
|
repository,
|
|
2881
|
+
housekeeping,
|
|
2555
2882
|
sharedScan,
|
|
2556
2883
|
schemaScan
|
|
2557
|
-
]
|
|
2884
|
+
];
|
|
2885
|
+
renderDoctor([...body, buildVerdictSection(body)]);
|
|
2558
2886
|
}
|
|
2559
2887
|
|
|
2560
2888
|
// src/commands.drop-session.ts
|
|
2561
2889
|
init_config();
|
|
2562
2890
|
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
2563
|
-
import { existsSync as
|
|
2564
|
-
import { join as
|
|
2891
|
+
import { existsSync as existsSync23, readdirSync as readdirSync9, statSync as statSync5 } from "node:fs";
|
|
2892
|
+
import { join as join28, relative as relative4 } from "node:path";
|
|
2565
2893
|
|
|
2566
2894
|
// src/commands.drop-session.git.ts
|
|
2567
2895
|
init_config();
|
|
@@ -2604,8 +2932,8 @@ function isInIndex(rel) {
|
|
|
2604
2932
|
init_config();
|
|
2605
2933
|
init_utils();
|
|
2606
2934
|
init_utils_json();
|
|
2607
|
-
import { existsSync as
|
|
2608
|
-
import { join as
|
|
2935
|
+
import { existsSync as existsSync22 } from "node:fs";
|
|
2936
|
+
import { join as join26 } from "node:path";
|
|
2609
2937
|
var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
|
|
2610
2938
|
function reportScrubHint(id, matches) {
|
|
2611
2939
|
const live = resolveLiveTranscript(id, matches);
|
|
@@ -2621,16 +2949,16 @@ function reportScrubHint(id, matches) {
|
|
|
2621
2949
|
}
|
|
2622
2950
|
function resolveLiveTranscript(id, matches) {
|
|
2623
2951
|
try {
|
|
2624
|
-
const mapPath =
|
|
2625
|
-
if (!
|
|
2952
|
+
const mapPath = join26(REPO_HOME, "path-map.json");
|
|
2953
|
+
if (!existsSync22(mapPath)) return null;
|
|
2626
2954
|
const projects = readJson(mapPath).projects;
|
|
2627
2955
|
for (const rel of matches) {
|
|
2628
2956
|
const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
|
|
2629
2957
|
if (logical === void 0) continue;
|
|
2630
2958
|
const abs = projects[logical]?.[HOST];
|
|
2631
2959
|
if (abs === void 0) continue;
|
|
2632
|
-
const live =
|
|
2633
|
-
if (
|
|
2960
|
+
const live = join26(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
|
|
2961
|
+
if (existsSync22(live)) return live;
|
|
2634
2962
|
}
|
|
2635
2963
|
return null;
|
|
2636
2964
|
} catch {
|
|
@@ -2644,18 +2972,18 @@ init_utils();
|
|
|
2644
2972
|
// src/utils.lockfile.ts
|
|
2645
2973
|
init_config();
|
|
2646
2974
|
init_utils();
|
|
2647
|
-
import { closeSync as
|
|
2648
|
-
import { dirname as
|
|
2649
|
-
var LOCK_PATH =
|
|
2975
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2976
|
+
import { dirname as dirname4, join as join27 } from "node:path";
|
|
2977
|
+
var LOCK_PATH = join27(HOME, ".cache", "claude-nomad", "nomad.lock");
|
|
2650
2978
|
function acquireLock(verb) {
|
|
2651
|
-
mkdirSync5(
|
|
2979
|
+
mkdirSync5(dirname4(LOCK_PATH), { recursive: true });
|
|
2652
2980
|
try {
|
|
2653
|
-
const fd =
|
|
2981
|
+
const fd = openSync3(LOCK_PATH, "wx");
|
|
2654
2982
|
try {
|
|
2655
2983
|
writeFileSync3(fd, String(process.pid));
|
|
2656
2984
|
} catch (writeErr) {
|
|
2657
2985
|
try {
|
|
2658
|
-
|
|
2986
|
+
closeSync3(fd);
|
|
2659
2987
|
} catch {
|
|
2660
2988
|
}
|
|
2661
2989
|
try {
|
|
@@ -2674,7 +3002,7 @@ function acquireLock(verb) {
|
|
|
2674
3002
|
function releaseLock(handle) {
|
|
2675
3003
|
if (handle === null) return;
|
|
2676
3004
|
try {
|
|
2677
|
-
|
|
3005
|
+
closeSync3(handle.fd);
|
|
2678
3006
|
} catch {
|
|
2679
3007
|
}
|
|
2680
3008
|
try {
|
|
@@ -2686,7 +3014,7 @@ function releaseLock(handle) {
|
|
|
2686
3014
|
function unlinkIfSamePid(expectedPidStr) {
|
|
2687
3015
|
let current;
|
|
2688
3016
|
try {
|
|
2689
|
-
current =
|
|
3017
|
+
current = readFileSync9(LOCK_PATH, "utf8").trim();
|
|
2690
3018
|
} catch {
|
|
2691
3019
|
return false;
|
|
2692
3020
|
}
|
|
@@ -2701,7 +3029,7 @@ function unlinkIfSamePid(expectedPidStr) {
|
|
|
2701
3029
|
function checkStaleAndRetry(verb) {
|
|
2702
3030
|
let pidStr;
|
|
2703
3031
|
try {
|
|
2704
|
-
pidStr =
|
|
3032
|
+
pidStr = readFileSync9(LOCK_PATH, "utf8").trim();
|
|
2705
3033
|
} catch {
|
|
2706
3034
|
pidStr = "";
|
|
2707
3035
|
}
|
|
@@ -2728,12 +3056,12 @@ function checkStaleAndRetry(verb) {
|
|
|
2728
3056
|
}
|
|
2729
3057
|
function retryOnce(verb) {
|
|
2730
3058
|
try {
|
|
2731
|
-
const fd =
|
|
3059
|
+
const fd = openSync3(LOCK_PATH, "wx");
|
|
2732
3060
|
try {
|
|
2733
3061
|
writeFileSync3(fd, String(process.pid));
|
|
2734
3062
|
} catch {
|
|
2735
3063
|
try {
|
|
2736
|
-
|
|
3064
|
+
closeSync3(fd);
|
|
2737
3065
|
} catch {
|
|
2738
3066
|
}
|
|
2739
3067
|
try {
|
|
@@ -2756,12 +3084,12 @@ function cmdDropSession(id) {
|
|
|
2756
3084
|
fail(`invalid session id: ${id}`);
|
|
2757
3085
|
process.exit(1);
|
|
2758
3086
|
}
|
|
2759
|
-
if (!
|
|
3087
|
+
if (!existsSync23(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
2760
3088
|
const handle = acquireLock("drop-session");
|
|
2761
3089
|
if (handle === null) process.exit(0);
|
|
2762
3090
|
try {
|
|
2763
|
-
const repoProjects =
|
|
2764
|
-
if (!
|
|
3091
|
+
const repoProjects = join28(REPO_HOME, "shared", "projects");
|
|
3092
|
+
if (!existsSync23(repoProjects)) {
|
|
2765
3093
|
throw new NomadFatal(`no staged session matches ${id}`);
|
|
2766
3094
|
}
|
|
2767
3095
|
const matches = collectMatches(repoProjects, id);
|
|
@@ -2782,13 +3110,13 @@ function cmdDropSession(id) {
|
|
|
2782
3110
|
}
|
|
2783
3111
|
function collectMatches(repoProjects, id) {
|
|
2784
3112
|
const matches = [];
|
|
2785
|
-
for (const logical of
|
|
2786
|
-
const candidate =
|
|
2787
|
-
if (
|
|
3113
|
+
for (const logical of readdirSync9(repoProjects)) {
|
|
3114
|
+
const candidate = join28(repoProjects, logical, `${id}.jsonl`);
|
|
3115
|
+
if (existsSync23(candidate)) {
|
|
2788
3116
|
matches.push(relative4(REPO_HOME, candidate));
|
|
2789
3117
|
}
|
|
2790
|
-
const dir =
|
|
2791
|
-
if (
|
|
3118
|
+
const dir = join28(repoProjects, logical, id);
|
|
3119
|
+
if (existsSync23(dir) && statSync5(dir).isDirectory()) {
|
|
2792
3120
|
const dirRel = relative4(REPO_HOME, dir);
|
|
2793
3121
|
const staged = expandStagedDir(dirRel);
|
|
2794
3122
|
if (staged.length > 0) matches.push(...staged);
|
|
@@ -2824,19 +3152,19 @@ function unstageOne(rel) {
|
|
|
2824
3152
|
|
|
2825
3153
|
// src/commands.redact.ts
|
|
2826
3154
|
init_config();
|
|
2827
|
-
import { existsSync as
|
|
2828
|
-
import { dirname as
|
|
3155
|
+
import { existsSync as existsSync25, statSync as statSync7 } from "node:fs";
|
|
3156
|
+
import { dirname as dirname5, join as join30 } from "node:path";
|
|
2829
3157
|
|
|
2830
3158
|
// src/commands.redact.subtree.ts
|
|
2831
|
-
import { existsSync as
|
|
2832
|
-
import { join as
|
|
3159
|
+
import { existsSync as existsSync24, lstatSync as lstatSync6, readFileSync as readFileSync10, readdirSync as readdirSync10, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3160
|
+
import { join as join29 } from "node:path";
|
|
2833
3161
|
init_utils_fs();
|
|
2834
3162
|
function collectFiles(dir, out) {
|
|
2835
|
-
if (!
|
|
3163
|
+
if (!existsSync24(dir)) return;
|
|
2836
3164
|
const st = lstatSync6(dir);
|
|
2837
3165
|
if (!st.isDirectory()) return;
|
|
2838
|
-
for (const entry of
|
|
2839
|
-
const abs =
|
|
3166
|
+
for (const entry of readdirSync10(dir)) {
|
|
3167
|
+
const abs = join29(dir, entry);
|
|
2840
3168
|
const lst = lstatSync6(abs);
|
|
2841
3169
|
if (lst.isSymbolicLink()) continue;
|
|
2842
3170
|
if (lst.isDirectory()) {
|
|
@@ -2873,7 +3201,7 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
|
|
|
2873
3201
|
if (!dryRun && total > 0) {
|
|
2874
3202
|
for (const { path: filePath, findings } of dirty) {
|
|
2875
3203
|
backupBeforeWrite(filePath, ts);
|
|
2876
|
-
writeFileSync4(filePath, applyRedactions(
|
|
3204
|
+
writeFileSync4(filePath, applyRedactions(readFileSync10(filePath, "utf8"), findings), "utf8");
|
|
2877
3205
|
}
|
|
2878
3206
|
}
|
|
2879
3207
|
return { total, dirty };
|
|
@@ -2886,14 +3214,14 @@ init_utils_json();
|
|
|
2886
3214
|
init_utils();
|
|
2887
3215
|
function resolveLiveTranscript2(id) {
|
|
2888
3216
|
try {
|
|
2889
|
-
const mapPath =
|
|
2890
|
-
if (!
|
|
3217
|
+
const mapPath = join30(REPO_HOME, "path-map.json");
|
|
3218
|
+
if (!existsSync25(mapPath)) return null;
|
|
2891
3219
|
const projects = readJson(mapPath).projects;
|
|
2892
3220
|
for (const hostMap of Object.values(projects)) {
|
|
2893
3221
|
const abs = hostMap[HOST];
|
|
2894
3222
|
if (abs === void 0) continue;
|
|
2895
|
-
const live =
|
|
2896
|
-
if (
|
|
3223
|
+
const live = join30(CLAUDE_HOME, "projects", encodePath(abs), `${id}.jsonl`);
|
|
3224
|
+
if (existsSync25(live)) return live;
|
|
2897
3225
|
}
|
|
2898
3226
|
return null;
|
|
2899
3227
|
} catch {
|
|
@@ -2911,17 +3239,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
|
|
|
2911
3239
|
fail(`invalid session id: ${id}`);
|
|
2912
3240
|
process.exit(1);
|
|
2913
3241
|
}
|
|
2914
|
-
if (!
|
|
3242
|
+
if (!existsSync25(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
2915
3243
|
const handle = acquireLock("redact");
|
|
2916
3244
|
if (handle === null) process.exit(0);
|
|
2917
3245
|
try {
|
|
2918
3246
|
const localPath = resolveLiveTranscript2(id);
|
|
2919
|
-
if (localPath === null || !
|
|
3247
|
+
if (localPath === null || !existsSync25(localPath)) {
|
|
2920
3248
|
fail(`could not resolve local transcript for session ${id} on this host`);
|
|
2921
3249
|
process.exitCode = 1;
|
|
2922
3250
|
return;
|
|
2923
3251
|
}
|
|
2924
|
-
const sessionDir =
|
|
3252
|
+
const sessionDir = join30(dirname5(localPath), id);
|
|
2925
3253
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
2926
3254
|
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
|
|
2927
3255
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
@@ -2974,8 +3302,8 @@ ${lines}`);
|
|
|
2974
3302
|
}
|
|
2975
3303
|
|
|
2976
3304
|
// src/commands.pull.ts
|
|
2977
|
-
import { existsSync as
|
|
2978
|
-
import { join as
|
|
3305
|
+
import { existsSync as existsSync33, mkdirSync as mkdirSync8 } from "node:fs";
|
|
3306
|
+
import { join as join39 } from "node:path";
|
|
2979
3307
|
|
|
2980
3308
|
// src/commands.push.sections.ts
|
|
2981
3309
|
init_color();
|
|
@@ -3063,22 +3391,34 @@ init_config();
|
|
|
3063
3391
|
|
|
3064
3392
|
// src/extras-sync.ts
|
|
3065
3393
|
init_config();
|
|
3066
|
-
import { existsSync as
|
|
3067
|
-
import { join as
|
|
3394
|
+
import { existsSync as existsSync28 } from "node:fs";
|
|
3395
|
+
import { join as join33 } from "node:path";
|
|
3068
3396
|
|
|
3069
3397
|
// src/extras-sync.diff.ts
|
|
3070
3398
|
init_utils();
|
|
3071
3399
|
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
3400
|
+
function labelDiffLine(line) {
|
|
3401
|
+
const tab = line.indexOf(" ");
|
|
3402
|
+
if (tab === -1) return line;
|
|
3403
|
+
const status = line.slice(0, tab);
|
|
3404
|
+
const path = line.slice(tab + 1);
|
|
3405
|
+
if (status === "D") return `${path} (local only)`;
|
|
3406
|
+
if (status === "A") return `${path} (repo only)`;
|
|
3407
|
+
return path;
|
|
3408
|
+
}
|
|
3409
|
+
function parseDiffOutput(stdout) {
|
|
3410
|
+
return stdout.split("\n").filter((line) => line.length > 0).map(labelDiffLine);
|
|
3411
|
+
}
|
|
3072
3412
|
function listDivergingFiles(a, b) {
|
|
3073
3413
|
try {
|
|
3074
|
-
const stdout = execFileSync13("git", ["diff", "--no-index", "--name-
|
|
3414
|
+
const stdout = execFileSync13("git", ["diff", "--no-index", "--name-status", a, b], {
|
|
3075
3415
|
stdio: ["ignore", "pipe", "pipe"]
|
|
3076
3416
|
}).toString();
|
|
3077
|
-
return stdout
|
|
3417
|
+
return parseDiffOutput(stdout);
|
|
3078
3418
|
} catch (err) {
|
|
3079
3419
|
const e = err;
|
|
3080
3420
|
if (e.status === 1 && e.stdout !== void 0) {
|
|
3081
|
-
return e.stdout.toString()
|
|
3421
|
+
return parseDiffOutput(e.stdout.toString());
|
|
3082
3422
|
}
|
|
3083
3423
|
if (e.code === "ENOENT") {
|
|
3084
3424
|
warn(`git not on PATH; divergence check skipped for ${a}`);
|
|
@@ -3091,8 +3431,8 @@ function listDivergingFiles(a, b) {
|
|
|
3091
3431
|
|
|
3092
3432
|
// src/extras-sync.core.ts
|
|
3093
3433
|
init_config();
|
|
3094
|
-
import { cpSync as cpSync4, existsSync as
|
|
3095
|
-
import { join as
|
|
3434
|
+
import { cpSync as cpSync4, existsSync as existsSync26, rmSync as rmSync7 } from "node:fs";
|
|
3435
|
+
import { join as join31 } from "node:path";
|
|
3096
3436
|
|
|
3097
3437
|
// src/extras-sync.guards.ts
|
|
3098
3438
|
init_utils();
|
|
@@ -3115,9 +3455,9 @@ function assertSafeLocalRoot(localRoot, logical) {
|
|
|
3115
3455
|
init_utils();
|
|
3116
3456
|
init_utils_json();
|
|
3117
3457
|
function loadValidatedExtras(opts) {
|
|
3118
|
-
const mapPath =
|
|
3119
|
-
const repoExtras =
|
|
3120
|
-
if (!
|
|
3458
|
+
const mapPath = join31(REPO_HOME, "path-map.json");
|
|
3459
|
+
const repoExtras = join31(REPO_HOME, "shared", "extras");
|
|
3460
|
+
if (!existsSync26(mapPath) || opts.requireRepoExtras === true && !existsSync26(repoExtras)) {
|
|
3121
3461
|
if (opts.missingMsg !== void 0) log(opts.missingMsg);
|
|
3122
3462
|
return null;
|
|
3123
3463
|
}
|
|
@@ -3139,12 +3479,12 @@ function* eachExtrasTarget(v, counts) {
|
|
|
3139
3479
|
counts.unmapped++;
|
|
3140
3480
|
continue;
|
|
3141
3481
|
}
|
|
3142
|
-
for (const
|
|
3143
|
-
if (!whitelist.includes(
|
|
3482
|
+
for (const dirname7 of dirnames) {
|
|
3483
|
+
if (!whitelist.includes(dirname7)) {
|
|
3144
3484
|
counts.skipped++;
|
|
3145
3485
|
continue;
|
|
3146
3486
|
}
|
|
3147
|
-
yield { logical, localRoot, dirname:
|
|
3487
|
+
yield { logical, localRoot, dirname: dirname7 };
|
|
3148
3488
|
}
|
|
3149
3489
|
}
|
|
3150
3490
|
}
|
|
@@ -3159,8 +3499,8 @@ init_utils_json();
|
|
|
3159
3499
|
|
|
3160
3500
|
// src/extras-sync.remap.ts
|
|
3161
3501
|
init_config();
|
|
3162
|
-
import { existsSync as
|
|
3163
|
-
import { join as
|
|
3502
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync6 } from "node:fs";
|
|
3503
|
+
import { join as join32 } from "node:path";
|
|
3164
3504
|
init_utils_fs();
|
|
3165
3505
|
function runExtrasOp(v, dryRun, paths, backup) {
|
|
3166
3506
|
const counts = { unmapped: 0, skipped: 0 };
|
|
@@ -3168,7 +3508,7 @@ function runExtrasOp(v, dryRun, paths, backup) {
|
|
|
3168
3508
|
const would = [];
|
|
3169
3509
|
for (const t of eachExtrasTarget(v, counts)) {
|
|
3170
3510
|
const { src, dst } = paths(t);
|
|
3171
|
-
if (!
|
|
3511
|
+
if (!existsSync27(src)) continue;
|
|
3172
3512
|
const item = `${t.logical}/${t.dirname}`;
|
|
3173
3513
|
if (dryRun) {
|
|
3174
3514
|
would.push(item);
|
|
@@ -3184,14 +3524,14 @@ function remapExtrasPush(ts, opts = {}) {
|
|
|
3184
3524
|
const dryRun = opts.dryRun === true;
|
|
3185
3525
|
const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
|
|
3186
3526
|
if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
|
|
3187
|
-
const repoExtras =
|
|
3527
|
+
const repoExtras = join32(REPO_HOME, "shared", "extras");
|
|
3188
3528
|
if (!dryRun) mkdirSync6(repoExtras, { recursive: true });
|
|
3189
3529
|
const { unmapped, skipped, done, would } = runExtrasOp(
|
|
3190
3530
|
v,
|
|
3191
3531
|
dryRun,
|
|
3192
|
-
({ localRoot, logical, dirname:
|
|
3193
|
-
src:
|
|
3194
|
-
dst:
|
|
3532
|
+
({ localRoot, logical, dirname: dirname7 }) => ({
|
|
3533
|
+
src: join32(localRoot, dirname7),
|
|
3534
|
+
dst: join32(repoExtras, logical, dirname7)
|
|
3195
3535
|
}),
|
|
3196
3536
|
(dst) => backupRepoWrite(dst, ts, REPO_HOME)
|
|
3197
3537
|
);
|
|
@@ -3204,13 +3544,13 @@ function remapExtrasPull(ts, opts = {}) {
|
|
|
3204
3544
|
missingMsg: "no path-map or repo extras dir; skipping extras remap"
|
|
3205
3545
|
});
|
|
3206
3546
|
if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
|
|
3207
|
-
const repoExtras =
|
|
3547
|
+
const repoExtras = join32(REPO_HOME, "shared", "extras");
|
|
3208
3548
|
const { unmapped, skipped, done, would } = runExtrasOp(
|
|
3209
3549
|
v,
|
|
3210
3550
|
dryRun,
|
|
3211
|
-
({ localRoot, logical, dirname:
|
|
3212
|
-
src:
|
|
3213
|
-
dst:
|
|
3551
|
+
({ localRoot, logical, dirname: dirname7 }) => ({
|
|
3552
|
+
src: join32(repoExtras, logical, dirname7),
|
|
3553
|
+
dst: join32(localRoot, dirname7)
|
|
3214
3554
|
}),
|
|
3215
3555
|
// Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
|
|
3216
3556
|
// localRoot so the backup tree mirrors the project layout.
|
|
@@ -3224,16 +3564,16 @@ function divergenceCheckExtras(ts) {
|
|
|
3224
3564
|
const v = loadValidatedExtras({});
|
|
3225
3565
|
if (v === null) return;
|
|
3226
3566
|
const counts = { unmapped: 0, skipped: 0 };
|
|
3227
|
-
const backupRoot =
|
|
3228
|
-
for (const { logical, localRoot, dirname:
|
|
3229
|
-
const local =
|
|
3230
|
-
const repo =
|
|
3231
|
-
if (!
|
|
3567
|
+
const backupRoot = join33(BACKUP_BASE, ts, "extras");
|
|
3568
|
+
for (const { logical, localRoot, dirname: dirname7 } of eachExtrasTarget(v, counts)) {
|
|
3569
|
+
const local = join33(localRoot, dirname7);
|
|
3570
|
+
const repo = join33(REPO_HOME, "shared", "extras", logical, dirname7);
|
|
3571
|
+
if (!existsSync28(local) || !existsSync28(repo)) continue;
|
|
3232
3572
|
const diff = listDivergingFiles(local, repo);
|
|
3233
3573
|
if (diff.length === 0) continue;
|
|
3234
|
-
const projectBackupRoot =
|
|
3574
|
+
const projectBackupRoot = join33(backupRoot, encodePath(localRoot));
|
|
3235
3575
|
warn(
|
|
3236
|
-
`local ${
|
|
3576
|
+
`local ${dirname7} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
|
|
3237
3577
|
);
|
|
3238
3578
|
for (const f of diff) warn(` ${f}`);
|
|
3239
3579
|
}
|
|
@@ -3244,8 +3584,8 @@ init_config();
|
|
|
3244
3584
|
init_utils();
|
|
3245
3585
|
init_utils_fs();
|
|
3246
3586
|
init_utils_json();
|
|
3247
|
-
import { existsSync as
|
|
3248
|
-
import { join as
|
|
3587
|
+
import { existsSync as existsSync29, lstatSync as lstatSync7, rmSync as rmSync8 } from "node:fs";
|
|
3588
|
+
import { join as join34 } from "node:path";
|
|
3249
3589
|
function emitAutoMove(onPreview, linkPath, ts, name) {
|
|
3250
3590
|
if (onPreview) {
|
|
3251
3591
|
onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
|
|
@@ -3264,11 +3604,11 @@ function applySharedLinks(ts, map, opts = {}) {
|
|
|
3264
3604
|
const dryRun = opts.dryRun === true;
|
|
3265
3605
|
const linkNames = allSharedLinks(map);
|
|
3266
3606
|
for (const name of linkNames) {
|
|
3267
|
-
const linkPath =
|
|
3268
|
-
const target =
|
|
3269
|
-
if (!
|
|
3607
|
+
const linkPath = join34(CLAUDE_HOME, name);
|
|
3608
|
+
const target = join34(REPO_HOME, "shared", name);
|
|
3609
|
+
if (!existsSync29(linkPath)) continue;
|
|
3270
3610
|
if (lstatSync7(linkPath).isSymbolicLink()) continue;
|
|
3271
|
-
if (!
|
|
3611
|
+
if (!existsSync29(target)) continue;
|
|
3272
3612
|
if (dryRun) {
|
|
3273
3613
|
emitAutoMove(opts.onPreview, linkPath, ts, name);
|
|
3274
3614
|
continue;
|
|
@@ -3277,28 +3617,28 @@ function applySharedLinks(ts, map, opts = {}) {
|
|
|
3277
3617
|
rmSync8(linkPath, { recursive: true, force: true });
|
|
3278
3618
|
}
|
|
3279
3619
|
for (const name of linkNames) {
|
|
3280
|
-
const target =
|
|
3281
|
-
if (!
|
|
3620
|
+
const target = join34(REPO_HOME, "shared", name);
|
|
3621
|
+
if (!existsSync29(target)) continue;
|
|
3282
3622
|
if (dryRun) {
|
|
3283
|
-
emitCreate(opts.onPreview,
|
|
3623
|
+
emitCreate(opts.onPreview, join34(CLAUDE_HOME, name), target);
|
|
3284
3624
|
continue;
|
|
3285
3625
|
}
|
|
3286
|
-
ensureSymlink(
|
|
3626
|
+
ensureSymlink(join34(CLAUDE_HOME, name), target);
|
|
3287
3627
|
}
|
|
3288
3628
|
}
|
|
3289
3629
|
function regenerateSettings(ts, opts = {}) {
|
|
3290
3630
|
const dryRun = opts.dryRun === true;
|
|
3291
|
-
const basePath =
|
|
3292
|
-
const hostPath =
|
|
3293
|
-
if (!
|
|
3631
|
+
const basePath = join34(REPO_HOME, "shared", "settings.base.json");
|
|
3632
|
+
const hostPath = join34(REPO_HOME, "hosts", `${HOST}.json`);
|
|
3633
|
+
if (!existsSync29(basePath)) {
|
|
3294
3634
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
3295
3635
|
}
|
|
3296
3636
|
const base = readJson(basePath);
|
|
3297
|
-
const hasOverrides =
|
|
3637
|
+
const hasOverrides = existsSync29(hostPath);
|
|
3298
3638
|
const overrides = hasOverrides ? readJson(hostPath) : {};
|
|
3299
3639
|
const merged = deepMerge(base, overrides);
|
|
3300
|
-
const settingsPath =
|
|
3301
|
-
if (!hasOverrides &&
|
|
3640
|
+
const settingsPath = join34(CLAUDE_HOME, "settings.json");
|
|
3641
|
+
if (!hasOverrides && existsSync29(settingsPath)) {
|
|
3302
3642
|
try {
|
|
3303
3643
|
const existing = readJson(settingsPath);
|
|
3304
3644
|
const baseKeys = new Set(Object.keys(base));
|
|
@@ -3324,8 +3664,8 @@ function regenerateSettings(ts, opts = {}) {
|
|
|
3324
3664
|
|
|
3325
3665
|
// src/preview.ts
|
|
3326
3666
|
init_config();
|
|
3327
|
-
import { existsSync as
|
|
3328
|
-
import { join as
|
|
3667
|
+
import { existsSync as existsSync30 } from "node:fs";
|
|
3668
|
+
import { join as join35 } from "node:path";
|
|
3329
3669
|
|
|
3330
3670
|
// node_modules/diff/libesm/diff/base.js
|
|
3331
3671
|
var Diff = class {
|
|
@@ -3600,6 +3940,7 @@ function diffLinesToUnified(oldStr, newStr) {
|
|
|
3600
3940
|
|
|
3601
3941
|
// src/preview.ts
|
|
3602
3942
|
init_utils_json();
|
|
3943
|
+
var CANONICAL_ORDER_NOTE = "settings.json will be rewritten in canonical key order; no value changes";
|
|
3603
3944
|
function diffJsonStrings(currentJsonText, newJsonText) {
|
|
3604
3945
|
if (currentJsonText === newJsonText) return "";
|
|
3605
3946
|
const lines = [
|
|
@@ -3610,7 +3951,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
|
|
|
3610
3951
|
return lines.join("\n");
|
|
3611
3952
|
}
|
|
3612
3953
|
function readJsonOrNull(path) {
|
|
3613
|
-
if (!
|
|
3954
|
+
if (!existsSync30(path)) return null;
|
|
3614
3955
|
try {
|
|
3615
3956
|
return readJson(path);
|
|
3616
3957
|
} catch {
|
|
@@ -3624,18 +3965,20 @@ function previewSettings(basePath, hostPath, settingsPath) {
|
|
|
3624
3965
|
}
|
|
3625
3966
|
const notes = [];
|
|
3626
3967
|
const hostOverrides = readJsonOrNull(hostPath);
|
|
3627
|
-
if (hostOverrides === null &&
|
|
3968
|
+
if (hostOverrides === null && existsSync30(hostPath)) {
|
|
3628
3969
|
notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
|
|
3629
3970
|
}
|
|
3630
3971
|
const merged = deepMerge(base, hostOverrides ?? {});
|
|
3631
3972
|
const current = readJsonOrNull(settingsPath);
|
|
3632
|
-
if (current === null &&
|
|
3973
|
+
if (current === null && existsSync30(settingsPath)) {
|
|
3633
3974
|
return { diff: "", notes: [...notes, "malformed; skipping diff"] };
|
|
3634
3975
|
}
|
|
3976
|
+
const rawEqual = JSON.stringify(current ?? {}, null, 2) === JSON.stringify(merged, null, 2);
|
|
3635
3977
|
const diff = diffJsonStrings(
|
|
3636
|
-
JSON.stringify(current ?? {}, null, 2),
|
|
3637
|
-
JSON.stringify(merged, null, 2)
|
|
3978
|
+
JSON.stringify(sortKeysDeep(current ?? {}), null, 2),
|
|
3979
|
+
JSON.stringify(sortKeysDeep(merged), null, 2)
|
|
3638
3980
|
);
|
|
3981
|
+
if (diff === "" && !rawEqual) notes.push(CANONICAL_ORDER_NOTE);
|
|
3639
3982
|
return { diff, notes };
|
|
3640
3983
|
}
|
|
3641
3984
|
function formatLinkRow(e) {
|
|
@@ -3665,9 +4008,9 @@ function computePreview(ts, map, verb = "pull") {
|
|
|
3665
4008
|
onPreview: (e) => addItem(links, formatLinkRow(e))
|
|
3666
4009
|
});
|
|
3667
4010
|
const settingsResult = previewSettings(
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
4011
|
+
join35(REPO_HOME, "shared", "settings.base.json"),
|
|
4012
|
+
join35(REPO_HOME, "hosts", `${HOST}.json`),
|
|
4013
|
+
join35(CLAUDE_HOME, "settings.json")
|
|
3671
4014
|
);
|
|
3672
4015
|
const settingsSection = buildSettingsSectionForPreview(settingsResult);
|
|
3673
4016
|
const sessions = section("Sessions");
|
|
@@ -3683,21 +4026,21 @@ function computePreview(ts, map, verb = "pull") {
|
|
|
3683
4026
|
|
|
3684
4027
|
// src/spinner.ts
|
|
3685
4028
|
init_color();
|
|
3686
|
-
import { existsSync as
|
|
4029
|
+
import { existsSync as existsSync32 } from "node:fs";
|
|
3687
4030
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
3688
4031
|
import { Worker } from "node:worker_threads";
|
|
3689
4032
|
|
|
3690
4033
|
// src/commands.push.recovery.ts
|
|
3691
4034
|
init_config();
|
|
3692
|
-
import { readFileSync as
|
|
3693
|
-
import { join as
|
|
4035
|
+
import { readFileSync as readFileSync11, rmSync as rmSync10, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4036
|
+
import { join as join38 } from "node:path";
|
|
3694
4037
|
import { createInterface } from "node:readline/promises";
|
|
3695
4038
|
|
|
3696
4039
|
// src/commands.push.recovery.redact.ts
|
|
3697
4040
|
init_config();
|
|
3698
4041
|
init_config_sharedDirs_guard();
|
|
3699
|
-
import { cpSync as cpSync5, existsSync as
|
|
3700
|
-
import { dirname as
|
|
4042
|
+
import { cpSync as cpSync5, existsSync as existsSync31, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
|
|
4043
|
+
import { dirname as dirname6, join as join36, sep as sep2 } from "node:path";
|
|
3701
4044
|
init_push_gitleaks_scan();
|
|
3702
4045
|
init_utils_json();
|
|
3703
4046
|
init_utils();
|
|
@@ -3728,8 +4071,8 @@ function resolveStagedDir(localPath, map) {
|
|
|
3728
4071
|
assertSafeLogical(logical);
|
|
3729
4072
|
const abs = hostMap[HOST];
|
|
3730
4073
|
if (abs === void 0) continue;
|
|
3731
|
-
if (localPath.startsWith(
|
|
3732
|
-
return
|
|
4074
|
+
if (localPath.startsWith(join36(CLAUDE_HOME, "projects", encodePath(abs)) + sep2)) {
|
|
4075
|
+
return join36(REPO_HOME, "shared", "projects", logical);
|
|
3733
4076
|
}
|
|
3734
4077
|
}
|
|
3735
4078
|
return null;
|
|
@@ -3751,7 +4094,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3751
4094
|
`could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
3752
4095
|
);
|
|
3753
4096
|
}
|
|
3754
|
-
const sessionDir =
|
|
4097
|
+
const sessionDir = join36(dirname6(localPath), sid);
|
|
3755
4098
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
3756
4099
|
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
|
|
3757
4100
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
@@ -3785,9 +4128,9 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3785
4128
|
);
|
|
3786
4129
|
}
|
|
3787
4130
|
mkdirSync7(stagedProjectDir, { recursive: true });
|
|
3788
|
-
cpSync5(localPath,
|
|
3789
|
-
if (
|
|
3790
|
-
cpSync5(sessionDir,
|
|
4131
|
+
cpSync5(localPath, join36(stagedProjectDir, `${sid}.jsonl`), { force: true });
|
|
4132
|
+
if (existsSync31(sessionDir)) {
|
|
4133
|
+
cpSync5(sessionDir, join36(stagedProjectDir, sid), { force: true, recursive: true });
|
|
3791
4134
|
}
|
|
3792
4135
|
return true;
|
|
3793
4136
|
}
|
|
@@ -3795,13 +4138,13 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
3795
4138
|
// src/commands.push.recovery.drop.ts
|
|
3796
4139
|
init_config();
|
|
3797
4140
|
import { rmSync as rmSync9 } from "node:fs";
|
|
3798
|
-
import { join as
|
|
4141
|
+
import { join as join37 } from "node:path";
|
|
3799
4142
|
function dropSessionFromStaged(sid, map) {
|
|
3800
4143
|
const logicals = Object.keys(map.projects);
|
|
3801
4144
|
if (logicals.length === 0) return false;
|
|
3802
4145
|
for (const logical of logicals) {
|
|
3803
|
-
const jsonl =
|
|
3804
|
-
const dir =
|
|
4146
|
+
const jsonl = join37(REPO_HOME, "shared", "projects", logical, `${sid}.jsonl`);
|
|
4147
|
+
const dir = join37(REPO_HOME, "shared", "projects", logical, sid);
|
|
3805
4148
|
rmSync9(jsonl, { force: true });
|
|
3806
4149
|
rmSync9(dir, { recursive: true, force: true });
|
|
3807
4150
|
}
|
|
@@ -3919,10 +4262,10 @@ function applyThenRescan(scanVerdict, repoHome) {
|
|
|
3919
4262
|
return next;
|
|
3920
4263
|
}
|
|
3921
4264
|
function allowThenRescan(append, scanVerdict, repoHome) {
|
|
3922
|
-
const ignPath =
|
|
4265
|
+
const ignPath = join38(repoHome, ".gitleaksignore");
|
|
3923
4266
|
let before;
|
|
3924
4267
|
try {
|
|
3925
|
-
before =
|
|
4268
|
+
before = readFileSync11(ignPath, "utf8");
|
|
3926
4269
|
} catch {
|
|
3927
4270
|
before = null;
|
|
3928
4271
|
}
|
|
@@ -4017,7 +4360,7 @@ function writeAnimatedDone(out, label, ms, useTTY) {
|
|
|
4017
4360
|
`);
|
|
4018
4361
|
}
|
|
4019
4362
|
function resolveWorkerPath(deps = {}) {
|
|
4020
|
-
const check = deps.existsSyncFn ??
|
|
4363
|
+
const check = deps.existsSyncFn ?? existsSync32;
|
|
4021
4364
|
const base = deps.baseUrl ?? import.meta.url;
|
|
4022
4365
|
const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
|
|
4023
4366
|
if (check(mjs)) return mjs;
|
|
@@ -4081,6 +4424,112 @@ function withSpinner(label, fn, deps) {
|
|
|
4081
4424
|
}
|
|
4082
4425
|
}
|
|
4083
4426
|
|
|
4427
|
+
// src/commands.pull.recovery.ts
|
|
4428
|
+
init_config();
|
|
4429
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
4430
|
+
init_utils();
|
|
4431
|
+
init_utils_fs();
|
|
4432
|
+
function gitCapture(args, cwd) {
|
|
4433
|
+
return execFileSync14("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
|
|
4434
|
+
}
|
|
4435
|
+
function isSyncedConfig(path) {
|
|
4436
|
+
return PUSH_ALLOWED_STATIC.some(
|
|
4437
|
+
(entry) => entry.endsWith("/") ? path.startsWith(entry) : path === entry
|
|
4438
|
+
);
|
|
4439
|
+
}
|
|
4440
|
+
function classifyTouched(touched) {
|
|
4441
|
+
const synced = [];
|
|
4442
|
+
const toolSource = [];
|
|
4443
|
+
for (const p of touched) {
|
|
4444
|
+
if (isSyncedConfig(p)) {
|
|
4445
|
+
synced.push(p);
|
|
4446
|
+
} else {
|
|
4447
|
+
toolSource.push(p);
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
return { synced, toolSource };
|
|
4451
|
+
}
|
|
4452
|
+
function parsePorcelainZ(raw) {
|
|
4453
|
+
const tracked = [];
|
|
4454
|
+
const untracked = [];
|
|
4455
|
+
if (!raw) return { tracked, untracked };
|
|
4456
|
+
const records = raw.split("\0");
|
|
4457
|
+
for (let i = 0; i < records.length; i++) {
|
|
4458
|
+
const record = records[i];
|
|
4459
|
+
if (record.length < 3) continue;
|
|
4460
|
+
const xy = record.slice(0, 2);
|
|
4461
|
+
const filePath = record.slice(3);
|
|
4462
|
+
if (xy === "??") {
|
|
4463
|
+
untracked.push(filePath);
|
|
4464
|
+
continue;
|
|
4465
|
+
}
|
|
4466
|
+
tracked.push(filePath);
|
|
4467
|
+
if (xy.startsWith("R") || xy.startsWith("C")) {
|
|
4468
|
+
const src = records[i + 1];
|
|
4469
|
+
if (src) {
|
|
4470
|
+
tracked.push(src);
|
|
4471
|
+
i++;
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
return { tracked, untracked };
|
|
4476
|
+
}
|
|
4477
|
+
function parseDirtyPaths(repo) {
|
|
4478
|
+
return parsePorcelainZ(gitStatusPorcelainZ(repo));
|
|
4479
|
+
}
|
|
4480
|
+
function buildRecoverySummary(branchName, strandedLog, untracked) {
|
|
4481
|
+
const strandedLines = strandedLog.split("\n").filter(Boolean).map((l) => ` ${l}`).join("\n");
|
|
4482
|
+
const parts = [`parked stranded commits on ${branchName}`];
|
|
4483
|
+
if (strandedLines) parts.push(`stranded:
|
|
4484
|
+
${strandedLines}`);
|
|
4485
|
+
if (untracked.length > 0) parts.push(`untracked files preserved: ${untracked.join(", ")}`);
|
|
4486
|
+
parts.push("continuing with normal pull");
|
|
4487
|
+
return parts.join("; ");
|
|
4488
|
+
}
|
|
4489
|
+
function freshStrandedBranch(repo) {
|
|
4490
|
+
const base = `nomad/stranded-${nowTimestamp()}`;
|
|
4491
|
+
const exists = (name) => {
|
|
4492
|
+
try {
|
|
4493
|
+
gitCapture(["rev-parse", "--verify", "--quiet", `refs/heads/${name}`], repo);
|
|
4494
|
+
return true;
|
|
4495
|
+
} catch {
|
|
4496
|
+
return false;
|
|
4497
|
+
}
|
|
4498
|
+
};
|
|
4499
|
+
if (!exists(base)) return base;
|
|
4500
|
+
let n = 1;
|
|
4501
|
+
while (exists(`${base}-${n}`)) n++;
|
|
4502
|
+
return `${base}-${n}`;
|
|
4503
|
+
}
|
|
4504
|
+
function recoverForceRemote(mode, repo) {
|
|
4505
|
+
if (mode === "merge") {
|
|
4506
|
+
gitOrFatal(["merge", "--abort"], "git merge --abort", repo);
|
|
4507
|
+
} else {
|
|
4508
|
+
gitOrFatal(["rebase", "--abort"], "git rebase --abort", repo);
|
|
4509
|
+
}
|
|
4510
|
+
gitOrFatal(["fetch", "origin", "main"], "git fetch origin main", repo);
|
|
4511
|
+
try {
|
|
4512
|
+
gitCapture(["rev-parse", "--verify", "origin/main"], repo);
|
|
4513
|
+
} catch {
|
|
4514
|
+
die("origin/main not found after fetch; check your remote configuration");
|
|
4515
|
+
}
|
|
4516
|
+
const committedRaw = gitCapture(["diff", "--name-only", "-z", "origin/main", "HEAD"], repo);
|
|
4517
|
+
const committedTouched = committedRaw.split("\0").filter(Boolean);
|
|
4518
|
+
const { tracked: dirtyTracked, untracked } = parseDirtyPaths(repo);
|
|
4519
|
+
const allTouched = [...committedTouched, ...dirtyTracked];
|
|
4520
|
+
const { synced } = classifyTouched(allTouched);
|
|
4521
|
+
if (synced.length > 0) {
|
|
4522
|
+
die(
|
|
4523
|
+
"force-remote refused: stranded or dirty tracked changes touch synced config.\nAt-risk paths:\n" + synced.map((p) => ` ${p}`).join("\n") + "\nCopy or cherry-pick those changes out before retrying."
|
|
4524
|
+
);
|
|
4525
|
+
}
|
|
4526
|
+
const branchName = freshStrandedBranch(repo);
|
|
4527
|
+
gitOrFatal(["branch", branchName, "HEAD"], "park stranded commits", repo);
|
|
4528
|
+
gitOrFatal(["reset", "--hard", "origin/main"], "reset to origin/main", repo);
|
|
4529
|
+
const strandedLog = gitCapture(["log", "--oneline", `origin/main..${branchName}`], repo);
|
|
4530
|
+
log(buildRecoverySummary(branchName, strandedLog, untracked));
|
|
4531
|
+
}
|
|
4532
|
+
|
|
4084
4533
|
// src/commands.pull.ts
|
|
4085
4534
|
init_utils();
|
|
4086
4535
|
init_utils_fs();
|
|
@@ -4102,18 +4551,32 @@ function applyWetPull(ts, map) {
|
|
|
4102
4551
|
summary
|
|
4103
4552
|
]);
|
|
4104
4553
|
}
|
|
4554
|
+
function handleWedge(repo, forceRemote) {
|
|
4555
|
+
const wedge = detectWedge(repo);
|
|
4556
|
+
if (wedge === null) return;
|
|
4557
|
+
if (forceRemote) {
|
|
4558
|
+
recoverForceRemote(wedge, repo);
|
|
4559
|
+
return;
|
|
4560
|
+
}
|
|
4561
|
+
const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
|
|
4562
|
+
die(
|
|
4563
|
+
`repo is ${state} from a previous failed pull; run 'nomad pull --force-remote' to auto-recover, or resolve manually (see FAQ: "Every pull fails with unmerged files")`
|
|
4564
|
+
);
|
|
4565
|
+
}
|
|
4105
4566
|
function cmdPull(opts = {}) {
|
|
4106
4567
|
const dryRun = opts.dryRun === true;
|
|
4107
|
-
|
|
4108
|
-
if (!
|
|
4568
|
+
const forceRemote = opts.forceRemote === true;
|
|
4569
|
+
if (!existsSync33(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4570
|
+
if (!existsSync33(join39(REPO_HOME, "shared", "settings.base.json"))) {
|
|
4109
4571
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
4110
4572
|
}
|
|
4111
4573
|
const handle = acquireLock("pull");
|
|
4112
4574
|
if (handle === null) process.exit(0);
|
|
4113
4575
|
try {
|
|
4114
4576
|
const ts = freshBackupTs(BACKUP_BASE);
|
|
4577
|
+
handleWedge(REPO_HOME, forceRemote);
|
|
4115
4578
|
if (!dryRun) {
|
|
4116
|
-
const backupRoot =
|
|
4579
|
+
const backupRoot = join39(BACKUP_BASE, ts);
|
|
4117
4580
|
try {
|
|
4118
4581
|
mkdirSync8(backupRoot, { recursive: true });
|
|
4119
4582
|
} catch (err) {
|
|
@@ -4124,8 +4587,8 @@ function cmdPull(opts = {}) {
|
|
|
4124
4587
|
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
4125
4588
|
);
|
|
4126
4589
|
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", REPO_HOME);
|
|
4127
|
-
const mapPath =
|
|
4128
|
-
const map =
|
|
4590
|
+
const mapPath = join39(REPO_HOME, "path-map.json");
|
|
4591
|
+
const map = existsSync33(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
4129
4592
|
divergenceCheckExtras(ts);
|
|
4130
4593
|
if (dryRun) {
|
|
4131
4594
|
computePreview(ts, map, "pull");
|
|
@@ -4147,8 +4610,8 @@ function cmdPull(opts = {}) {
|
|
|
4147
4610
|
|
|
4148
4611
|
// src/commands.push.ts
|
|
4149
4612
|
init_config();
|
|
4150
|
-
import { existsSync as
|
|
4151
|
-
import { join as
|
|
4613
|
+
import { existsSync as existsSync35 } from "node:fs";
|
|
4614
|
+
import { join as join41, relative as relative5 } from "node:path";
|
|
4152
4615
|
|
|
4153
4616
|
// src/commands.push.allowlist.ts
|
|
4154
4617
|
init_config();
|
|
@@ -4172,7 +4635,7 @@ function isNeverSync(path) {
|
|
|
4172
4635
|
}
|
|
4173
4636
|
return false;
|
|
4174
4637
|
}
|
|
4175
|
-
function
|
|
4638
|
+
function parsePorcelainZ2(statusPorcelain) {
|
|
4176
4639
|
const records = statusPorcelain.split("\0");
|
|
4177
4640
|
const paths = [];
|
|
4178
4641
|
for (let i = 0; i < records.length; i++) {
|
|
@@ -4202,7 +4665,7 @@ function enforceAllowList(statusPorcelain, map) {
|
|
|
4202
4665
|
];
|
|
4203
4666
|
const neverSyncHits = [];
|
|
4204
4667
|
const violations = [];
|
|
4205
|
-
for (const path of
|
|
4668
|
+
for (const path of parsePorcelainZ2(statusPorcelain)) {
|
|
4206
4669
|
if (isNeverSync(path)) {
|
|
4207
4670
|
neverSyncHits.push(path);
|
|
4208
4671
|
} else if (!isAllowed(path, allowed)) {
|
|
@@ -4228,9 +4691,9 @@ init_color();
|
|
|
4228
4691
|
init_config();
|
|
4229
4692
|
init_config_sharedDirs_guard();
|
|
4230
4693
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
4231
|
-
import { copyFileSync, existsSync as
|
|
4694
|
+
import { copyFileSync, existsSync as existsSync34, mkdirSync as mkdirSync9, readdirSync as readdirSync11, rmSync as rmSync11 } from "node:fs";
|
|
4232
4695
|
import { homedir as homedir5 } from "node:os";
|
|
4233
|
-
import { join as
|
|
4696
|
+
import { join as join40 } from "node:path";
|
|
4234
4697
|
init_push_leak_verdict();
|
|
4235
4698
|
init_push_gitleaks();
|
|
4236
4699
|
init_utils_fs();
|
|
@@ -4245,13 +4708,13 @@ function stageSessions(tmpRoot, map) {
|
|
|
4245
4708
|
if (!p || p === "TBD") continue;
|
|
4246
4709
|
reverse.set(encodePath(p), logical);
|
|
4247
4710
|
}
|
|
4248
|
-
const localProjects =
|
|
4249
|
-
if (!
|
|
4711
|
+
const localProjects = join40(CLAUDE_HOME, "projects");
|
|
4712
|
+
if (!existsSync34(localProjects)) return 0;
|
|
4250
4713
|
let staged = 0;
|
|
4251
|
-
for (const dir of
|
|
4714
|
+
for (const dir of readdirSync11(localProjects)) {
|
|
4252
4715
|
const logical = reverse.get(dir);
|
|
4253
4716
|
if (!logical) continue;
|
|
4254
|
-
copyDirJsonlOnly(
|
|
4717
|
+
copyDirJsonlOnly(join40(localProjects, dir), join40(tmpRoot, "shared", "projects", logical));
|
|
4255
4718
|
staged++;
|
|
4256
4719
|
}
|
|
4257
4720
|
return staged;
|
|
@@ -4265,11 +4728,11 @@ function stageExtras(tmpRoot, map) {
|
|
|
4265
4728
|
assertSafeLogical(logical);
|
|
4266
4729
|
const localRoot = map.projects[logical]?.[HOST];
|
|
4267
4730
|
if (!localRoot || localRoot === "TBD") continue;
|
|
4268
|
-
for (const
|
|
4269
|
-
if (!whitelist.includes(
|
|
4270
|
-
const src =
|
|
4271
|
-
if (!
|
|
4272
|
-
const dst =
|
|
4731
|
+
for (const dirname7 of dirnames) {
|
|
4732
|
+
if (!whitelist.includes(dirname7)) continue;
|
|
4733
|
+
const src = join40(localRoot, dirname7);
|
|
4734
|
+
if (!existsSync34(src)) continue;
|
|
4735
|
+
const dst = join40(tmpRoot, "shared", "extras", logical, dirname7);
|
|
4273
4736
|
copyExtras(src, dst);
|
|
4274
4737
|
staged++;
|
|
4275
4738
|
}
|
|
@@ -4277,19 +4740,19 @@ function stageExtras(tmpRoot, map) {
|
|
|
4277
4740
|
return staged;
|
|
4278
4741
|
}
|
|
4279
4742
|
function previewPushLeaks(map) {
|
|
4280
|
-
const cacheDir =
|
|
4743
|
+
const cacheDir = join40(homedir5(), ".cache", "claude-nomad");
|
|
4281
4744
|
mkdirSync9(cacheDir, { recursive: true });
|
|
4282
4745
|
const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
|
|
4283
|
-
const tmpRoot =
|
|
4746
|
+
const tmpRoot = join40(cacheDir, `push-preview-tree-${stamp}`);
|
|
4284
4747
|
try {
|
|
4285
4748
|
const sessionCount = stageSessions(tmpRoot, map);
|
|
4286
4749
|
const extrasCount = stageExtras(tmpRoot, map);
|
|
4287
4750
|
if (sessionCount + extrasCount === 0) {
|
|
4288
4751
|
return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
|
|
4289
4752
|
}
|
|
4290
|
-
const ignoreFile =
|
|
4291
|
-
if (
|
|
4292
|
-
copyFileSync(ignoreFile,
|
|
4753
|
+
const ignoreFile = join40(REPO_HOME, ".gitleaksignore");
|
|
4754
|
+
if (existsSync34(ignoreFile)) {
|
|
4755
|
+
copyFileSync(ignoreFile, join40(tmpRoot, ".gitleaksignore"));
|
|
4293
4756
|
}
|
|
4294
4757
|
let findings;
|
|
4295
4758
|
try {
|
|
@@ -4311,7 +4774,7 @@ init_utils();
|
|
|
4311
4774
|
init_utils_fs();
|
|
4312
4775
|
init_utils_json();
|
|
4313
4776
|
function guardGitlinks() {
|
|
4314
|
-
const gitlinks = findGitlinks(
|
|
4777
|
+
const gitlinks = findGitlinks(join41(REPO_HOME, "shared"));
|
|
4315
4778
|
if (gitlinks.length === 0) return;
|
|
4316
4779
|
for (const p of gitlinks) {
|
|
4317
4780
|
const rel = relative5(REPO_HOME, p);
|
|
@@ -4363,7 +4826,7 @@ async function cmdPush(opts = {}) {
|
|
|
4363
4826
|
const allowAll = opts.allowAll === true;
|
|
4364
4827
|
const allowRule = opts.allowRule;
|
|
4365
4828
|
guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
|
|
4366
|
-
if (!
|
|
4829
|
+
if (!existsSync35(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4367
4830
|
const handle = acquireLock("push");
|
|
4368
4831
|
if (handle === null) process.exit(0);
|
|
4369
4832
|
try {
|
|
@@ -4381,8 +4844,8 @@ async function cmdPush(opts = {}) {
|
|
|
4381
4844
|
renderNoScanTree(st);
|
|
4382
4845
|
return;
|
|
4383
4846
|
}
|
|
4384
|
-
const mapPath =
|
|
4385
|
-
if (!
|
|
4847
|
+
const mapPath = join41(REPO_HOME, "path-map.json");
|
|
4848
|
+
if (!existsSync35(mapPath)) {
|
|
4386
4849
|
if (dryRun) return runDryRunPreview(st, null);
|
|
4387
4850
|
die("path-map.json missing, cannot enforce push allow-list");
|
|
4388
4851
|
}
|
|
@@ -4403,9 +4866,9 @@ async function cmdPush(opts = {}) {
|
|
|
4403
4866
|
}
|
|
4404
4867
|
|
|
4405
4868
|
// src/commands.update.ts
|
|
4406
|
-
import { execFileSync as
|
|
4869
|
+
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
4407
4870
|
init_utils();
|
|
4408
|
-
function cmdUpdate(run =
|
|
4871
|
+
function cmdUpdate(run = execFileSync15) {
|
|
4409
4872
|
try {
|
|
4410
4873
|
run("npm", ["update", "-g", "claude-nomad"], { stdio: "inherit" });
|
|
4411
4874
|
} catch (err) {
|
|
@@ -4422,17 +4885,17 @@ init_config();
|
|
|
4422
4885
|
|
|
4423
4886
|
// src/diff.ts
|
|
4424
4887
|
init_config();
|
|
4425
|
-
import { existsSync as
|
|
4426
|
-
import { join as
|
|
4888
|
+
import { existsSync as existsSync36 } from "node:fs";
|
|
4889
|
+
import { join as join42 } from "node:path";
|
|
4427
4890
|
init_utils();
|
|
4428
4891
|
init_utils_fs();
|
|
4429
4892
|
init_utils_json();
|
|
4430
4893
|
function cmdDiff() {
|
|
4431
4894
|
try {
|
|
4432
|
-
if (!
|
|
4895
|
+
if (!existsSync36(REPO_HOME)) die(`repo not cloned at ${REPO_HOME}`);
|
|
4433
4896
|
const ts = freshBackupTs(BACKUP_BASE);
|
|
4434
|
-
const mapPath =
|
|
4435
|
-
const map =
|
|
4897
|
+
const mapPath = join42(REPO_HOME, "path-map.json");
|
|
4898
|
+
const map = existsSync36(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
4436
4899
|
computePreview(ts, map, "diff");
|
|
4437
4900
|
} catch (err) {
|
|
4438
4901
|
if (err instanceof NomadFatal) {
|
|
@@ -4446,19 +4909,19 @@ function cmdDiff() {
|
|
|
4446
4909
|
|
|
4447
4910
|
// src/init.ts
|
|
4448
4911
|
init_config();
|
|
4449
|
-
import { existsSync as
|
|
4450
|
-
import { join as
|
|
4912
|
+
import { existsSync as existsSync38, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4913
|
+
import { join as join44 } from "node:path";
|
|
4451
4914
|
|
|
4452
4915
|
// src/init.gh-onboard.ts
|
|
4453
4916
|
init_config();
|
|
4454
|
-
import { execFileSync as
|
|
4917
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
4455
4918
|
init_utils();
|
|
4456
4919
|
var DEFAULT_REPO_NAME = "claude-nomad-config";
|
|
4457
4920
|
function isValidRepoName(name) {
|
|
4458
4921
|
return /^[A-Za-z0-9._-]{1,100}$/.test(name);
|
|
4459
4922
|
}
|
|
4460
4923
|
var GH_NETWORK_TIMEOUT_MS = 3e4;
|
|
4461
|
-
function ensureOriginRepo(repoName, run =
|
|
4924
|
+
function ensureOriginRepo(repoName, run = execFileSync16) {
|
|
4462
4925
|
if (!isValidRepoName(repoName)) {
|
|
4463
4926
|
die(
|
|
4464
4927
|
`invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
|
|
@@ -4528,31 +4991,31 @@ init_config();
|
|
|
4528
4991
|
init_utils();
|
|
4529
4992
|
init_utils_fs();
|
|
4530
4993
|
init_utils_json();
|
|
4531
|
-
import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as
|
|
4532
|
-
import { join as
|
|
4994
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync6, existsSync as existsSync37, rmSync as rmSync12, statSync as statSync9 } from "node:fs";
|
|
4995
|
+
import { join as join43 } from "node:path";
|
|
4533
4996
|
function snapshotIntoShared(map) {
|
|
4534
4997
|
for (const name of allSharedLinks(map)) {
|
|
4535
|
-
const src =
|
|
4536
|
-
if (!
|
|
4537
|
-
const dst =
|
|
4998
|
+
const src = join43(CLAUDE_HOME, name);
|
|
4999
|
+
if (!existsSync37(src)) continue;
|
|
5000
|
+
const dst = join43(REPO_HOME, "shared", name);
|
|
4538
5001
|
if (statSync9(src).isDirectory()) {
|
|
4539
|
-
const gk =
|
|
4540
|
-
if (
|
|
5002
|
+
const gk = join43(dst, ".gitkeep");
|
|
5003
|
+
if (existsSync37(gk)) rmSync12(gk);
|
|
4541
5004
|
cpSync6(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
4542
5005
|
} else {
|
|
4543
5006
|
copyFileSync2(src, dst);
|
|
4544
5007
|
}
|
|
4545
5008
|
log(`snapshotted shared/${name} from ${src}`);
|
|
4546
5009
|
}
|
|
4547
|
-
const userSettings =
|
|
4548
|
-
if (
|
|
5010
|
+
const userSettings = join43(CLAUDE_HOME, "settings.json");
|
|
5011
|
+
if (existsSync37(userSettings)) {
|
|
4549
5012
|
let parsed;
|
|
4550
5013
|
try {
|
|
4551
5014
|
parsed = readJson(userSettings);
|
|
4552
5015
|
} catch (err) {
|
|
4553
5016
|
return die(`malformed ${userSettings}: ${err.message}`);
|
|
4554
5017
|
}
|
|
4555
|
-
const hostFile =
|
|
5018
|
+
const hostFile = join43(REPO_HOME, "hosts", `${HOST}.json`);
|
|
4556
5019
|
writeJsonAtomic(hostFile, parsed);
|
|
4557
5020
|
log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
|
|
4558
5021
|
}
|
|
@@ -4565,14 +5028,14 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
|
|
|
4565
5028
|
var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
|
|
4566
5029
|
function preflightConflict(repoHome) {
|
|
4567
5030
|
const candidates = [
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
5031
|
+
join44(repoHome, "shared", "settings.base.json"),
|
|
5032
|
+
join44(repoHome, "shared", "CLAUDE.md"),
|
|
5033
|
+
join44(repoHome, "path-map.json"),
|
|
5034
|
+
join44(repoHome, "hosts"),
|
|
5035
|
+
join44(repoHome, "shared")
|
|
4573
5036
|
];
|
|
4574
5037
|
for (const c of candidates) {
|
|
4575
|
-
if (
|
|
5038
|
+
if (existsSync38(c)) return c;
|
|
4576
5039
|
}
|
|
4577
5040
|
return null;
|
|
4578
5041
|
}
|
|
@@ -4585,25 +5048,25 @@ function cmdInit(opts = {}) {
|
|
|
4585
5048
|
die(`already initialized; refusing to clobber ${conflict}`);
|
|
4586
5049
|
}
|
|
4587
5050
|
ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
|
|
4588
|
-
mkdirSync10(
|
|
4589
|
-
mkdirSync10(
|
|
5051
|
+
mkdirSync10(join44(REPO_HOME, "shared"), { recursive: true });
|
|
5052
|
+
mkdirSync10(join44(REPO_HOME, "hosts"), { recursive: true });
|
|
4590
5053
|
for (const name of SHARED_KEEP_DIRS) {
|
|
4591
|
-
mkdirSync10(
|
|
5054
|
+
mkdirSync10(join44(REPO_HOME, "shared", name), { recursive: true });
|
|
4592
5055
|
}
|
|
4593
|
-
const userClaudeMd =
|
|
4594
|
-
if (!snapshot || !
|
|
4595
|
-
writeFileSync6(
|
|
5056
|
+
const userClaudeMd = join44(CLAUDE_HOME, "CLAUDE.md");
|
|
5057
|
+
if (!snapshot || !existsSync38(userClaudeMd)) {
|
|
5058
|
+
writeFileSync6(join44(REPO_HOME, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
|
|
4596
5059
|
log("created shared/CLAUDE.md");
|
|
4597
5060
|
}
|
|
4598
5061
|
for (const name of SHARED_KEEP_DIRS) {
|
|
4599
|
-
writeFileSync6(
|
|
5062
|
+
writeFileSync6(join44(REPO_HOME, "shared", name, ".gitkeep"), "");
|
|
4600
5063
|
log(`created shared/${name}/.gitkeep`);
|
|
4601
5064
|
}
|
|
4602
|
-
writeFileSync6(
|
|
5065
|
+
writeFileSync6(join44(REPO_HOME, "hosts", ".gitkeep"), "");
|
|
4603
5066
|
log("created hosts/.gitkeep");
|
|
4604
|
-
writeJsonAtomic(
|
|
5067
|
+
writeJsonAtomic(join44(REPO_HOME, "shared", "settings.base.json"), {});
|
|
4605
5068
|
log("created shared/settings.base.json");
|
|
4606
|
-
writeJsonAtomic(
|
|
5069
|
+
writeJsonAtomic(join44(REPO_HOME, "path-map.json"), { projects: {} });
|
|
4607
5070
|
log("created path-map.json");
|
|
4608
5071
|
if (snapshot) {
|
|
4609
5072
|
snapshotIntoShared({ projects: {} });
|
|
@@ -4807,6 +5270,28 @@ function parseAllowArgs(argv) {
|
|
|
4807
5270
|
return positionals;
|
|
4808
5271
|
}
|
|
4809
5272
|
|
|
5273
|
+
// src/nomad.dispatch.pull.ts
|
|
5274
|
+
function parsePullArgs(argv) {
|
|
5275
|
+
let dryRun = false;
|
|
5276
|
+
let forceRemote = false;
|
|
5277
|
+
let i = 3;
|
|
5278
|
+
while (i < argv.length) {
|
|
5279
|
+
const token = argv[i];
|
|
5280
|
+
if (token === "--dry-run") {
|
|
5281
|
+
if (dryRun) return null;
|
|
5282
|
+
dryRun = true;
|
|
5283
|
+
} else if (token === "--force-remote") {
|
|
5284
|
+
if (forceRemote) return null;
|
|
5285
|
+
forceRemote = true;
|
|
5286
|
+
} else {
|
|
5287
|
+
return null;
|
|
5288
|
+
}
|
|
5289
|
+
i++;
|
|
5290
|
+
}
|
|
5291
|
+
if (dryRun && forceRemote) return null;
|
|
5292
|
+
return { dryRun, forceRemote };
|
|
5293
|
+
}
|
|
5294
|
+
|
|
4810
5295
|
// src/nomad.dispatch.push.ts
|
|
4811
5296
|
var REJECT2 = { ok: false, advance: 0 };
|
|
4812
5297
|
function applyBool2(seen, set) {
|
|
@@ -4865,7 +5350,7 @@ function parsePushArgs(argv) {
|
|
|
4865
5350
|
// package.json
|
|
4866
5351
|
var package_default = {
|
|
4867
5352
|
name: "claude-nomad",
|
|
4868
|
-
version: "0.
|
|
5353
|
+
version: "0.43.0",
|
|
4869
5354
|
type: "module",
|
|
4870
5355
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
4871
5356
|
keywords: [
|
|
@@ -4963,6 +5448,11 @@ var DEFAULT_HELP = [
|
|
|
4963
5448
|
"Commands:",
|
|
4964
5449
|
row(" pull", "Sync ~/.claude/ from the shared repo (settings, symlinks, sessions)."),
|
|
4965
5450
|
row(" --dry-run", "Run lock + git pull, then preview every mutation without writing."),
|
|
5451
|
+
row(" --force-remote", "Recover from a wedged repo (stuck mid-rebase or mid-merge):"),
|
|
5452
|
+
cont("abort the in-progress rebase/merge, park stranded commits on"),
|
|
5453
|
+
cont("nomad/stranded-<ts>, reset to origin/main, and re-pull. Refuses"),
|
|
5454
|
+
cont("if stranded or dirty tracked changes touch synced config (shared/,"),
|
|
5455
|
+
cont("hosts/, path-map.json). Cannot combine with --dry-run."),
|
|
4966
5456
|
"",
|
|
4967
5457
|
row(" push", "Rebase, run safety checks (gitleaks, gitlinks, allow-list), commit, push."),
|
|
4968
5458
|
row(" --dry-run", "Run pre-checks (rebase, gitleaks probe, gitlink scan) and preview"),
|
|
@@ -5055,15 +5545,15 @@ var DEFAULT_HELP = [
|
|
|
5055
5545
|
init_config();
|
|
5056
5546
|
init_utils();
|
|
5057
5547
|
init_utils_json();
|
|
5058
|
-
import { existsSync as
|
|
5059
|
-
import { join as
|
|
5548
|
+
import { existsSync as existsSync39, readFileSync as readFileSync12, readdirSync as readdirSync12 } from "node:fs";
|
|
5549
|
+
import { join as join45 } from "node:path";
|
|
5060
5550
|
function resumeCmd(sessionId) {
|
|
5061
5551
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
5062
5552
|
fail(`invalid session id: ${sessionId}`);
|
|
5063
5553
|
process.exit(1);
|
|
5064
5554
|
}
|
|
5065
|
-
const projectsRoot =
|
|
5066
|
-
if (!
|
|
5555
|
+
const projectsRoot = join45(CLAUDE_HOME, "projects");
|
|
5556
|
+
if (!existsSync39(projectsRoot)) {
|
|
5067
5557
|
fail(`${projectsRoot} does not exist`);
|
|
5068
5558
|
process.exit(1);
|
|
5069
5559
|
}
|
|
@@ -5077,8 +5567,8 @@ function resumeCmd(sessionId) {
|
|
|
5077
5567
|
fail(`no cwd field found in ${jsonlPath}`);
|
|
5078
5568
|
process.exit(1);
|
|
5079
5569
|
}
|
|
5080
|
-
const mapPath =
|
|
5081
|
-
if (!
|
|
5570
|
+
const mapPath = join45(REPO_HOME, "path-map.json");
|
|
5571
|
+
if (!existsSync39(mapPath)) {
|
|
5082
5572
|
fail("path-map.json missing");
|
|
5083
5573
|
process.exit(1);
|
|
5084
5574
|
}
|
|
@@ -5100,14 +5590,14 @@ function resumeCmd(sessionId) {
|
|
|
5100
5590
|
console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
|
|
5101
5591
|
}
|
|
5102
5592
|
function findTranscriptPath(projectsRoot, sessionId) {
|
|
5103
|
-
for (const dir of
|
|
5104
|
-
const candidate =
|
|
5105
|
-
if (
|
|
5593
|
+
for (const dir of readdirSync12(projectsRoot)) {
|
|
5594
|
+
const candidate = join45(projectsRoot, dir, `${sessionId}.jsonl`);
|
|
5595
|
+
if (existsSync39(candidate)) return candidate;
|
|
5106
5596
|
}
|
|
5107
5597
|
return null;
|
|
5108
5598
|
}
|
|
5109
5599
|
function extractRecordedCwd(jsonlPath) {
|
|
5110
|
-
for (const line of
|
|
5600
|
+
for (const line of readFileSync12(jsonlPath, "utf8").split("\n")) {
|
|
5111
5601
|
if (!line.trim()) continue;
|
|
5112
5602
|
try {
|
|
5113
5603
|
const obj = JSON.parse(line);
|
|
@@ -5174,15 +5664,12 @@ try {
|
|
|
5174
5664
|
console.log(package_default.version);
|
|
5175
5665
|
break;
|
|
5176
5666
|
case "pull": {
|
|
5177
|
-
const
|
|
5178
|
-
if (
|
|
5179
|
-
|
|
5180
|
-
} else if (sub === "--dry-run" && process.argv.length === 4) {
|
|
5181
|
-
cmdPull({ dryRun: true });
|
|
5182
|
-
} else {
|
|
5183
|
-
console.error("usage: nomad pull [--dry-run]");
|
|
5667
|
+
const pullArgs = parsePullArgs(process.argv);
|
|
5668
|
+
if (pullArgs === null) {
|
|
5669
|
+
console.error("usage: nomad pull [--dry-run] [--force-remote]");
|
|
5184
5670
|
process.exit(1);
|
|
5185
5671
|
}
|
|
5672
|
+
cmdPull({ dryRun: pullArgs.dryRun, forceRemote: pullArgs.forceRemote });
|
|
5186
5673
|
break;
|
|
5187
5674
|
}
|
|
5188
5675
|
case "push": {
|