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