claude-nomad 0.58.1 → 0.59.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 +31 -0
- package/README.md +55 -0
- package/dist/nomad.mjs +1297 -1065
- package/package.json +1 -1
package/dist/nomad.mjs
CHANGED
|
@@ -445,6 +445,9 @@ var init_settings_keys = __esm({
|
|
|
445
445
|
import { homedir, hostname } from "node:os";
|
|
446
446
|
import { join, resolve } from "node:path";
|
|
447
447
|
function home() {
|
|
448
|
+
if (process.platform === "win32") {
|
|
449
|
+
return process.env.USERPROFILE || process.env.HOME || homedir();
|
|
450
|
+
}
|
|
448
451
|
return process.env.HOME || homedir();
|
|
449
452
|
}
|
|
450
453
|
function claudeHome() {
|
|
@@ -508,8 +511,10 @@ var init_config = __esm({
|
|
|
508
511
|
"path-map.json",
|
|
509
512
|
".gitleaksignore",
|
|
510
513
|
// written by nomad push Allow action
|
|
511
|
-
".gitleaks.overlay.toml"
|
|
514
|
+
".gitleaks.overlay.toml",
|
|
512
515
|
// user-owned gitleaks allowlist overlay layered on the bundled base
|
|
516
|
+
".gitattributes"
|
|
517
|
+
// scaffolded by nomad init as the CRLF guard doctor recommends adding
|
|
513
518
|
];
|
|
514
519
|
}
|
|
515
520
|
});
|
|
@@ -596,10 +601,10 @@ var init_utils_json = __esm({
|
|
|
596
601
|
// src/utils.fs.ts
|
|
597
602
|
import {
|
|
598
603
|
closeSync,
|
|
599
|
-
cpSync,
|
|
600
|
-
existsSync,
|
|
604
|
+
cpSync as cpSync3,
|
|
605
|
+
existsSync as existsSync3,
|
|
601
606
|
fsyncSync,
|
|
602
|
-
lstatSync,
|
|
607
|
+
lstatSync as lstatSync3,
|
|
603
608
|
mkdirSync,
|
|
604
609
|
openSync,
|
|
605
610
|
renameSync,
|
|
@@ -607,9 +612,32 @@ import {
|
|
|
607
612
|
symlinkSync,
|
|
608
613
|
writeFileSync
|
|
609
614
|
} from "node:fs";
|
|
610
|
-
import { dirname, join as
|
|
615
|
+
import { dirname, join as join4, relative as relative2, sep as sep2 } from "node:path";
|
|
616
|
+
function isRetryableRenameCode(code) {
|
|
617
|
+
return code === "EPERM" || code === "EBUSY";
|
|
618
|
+
}
|
|
619
|
+
function sleepSyncMs(ms) {
|
|
620
|
+
const view = new Int32Array(new SharedArrayBuffer(4));
|
|
621
|
+
Atomics.wait(view, 0, 0, ms);
|
|
622
|
+
}
|
|
623
|
+
function renameAtomicRetry(tmp, dst, renameFn = renameSync) {
|
|
624
|
+
if (process.platform !== "win32") {
|
|
625
|
+
renameFn(tmp, dst);
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
for (let attempt = 1; attempt <= RENAME_RETRY_MAX_ATTEMPTS; attempt++) {
|
|
629
|
+
try {
|
|
630
|
+
renameFn(tmp, dst);
|
|
631
|
+
return;
|
|
632
|
+
} catch (e) {
|
|
633
|
+
const code = e.code;
|
|
634
|
+
if (!isRetryableRenameCode(code) || attempt === RENAME_RETRY_MAX_ATTEMPTS) throw e;
|
|
635
|
+
sleepSyncMs(RENAME_RETRY_BACKOFF_MS);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
611
639
|
function writeJsonAtomic(path, data) {
|
|
612
|
-
const mode =
|
|
640
|
+
const mode = existsSync3(path) ? statSync(path).mode & 511 : 384;
|
|
613
641
|
const tmp = `${path}.tmp.${process.pid}`;
|
|
614
642
|
const fd = openSync(tmp, "w", mode);
|
|
615
643
|
try {
|
|
@@ -618,7 +646,7 @@ function writeJsonAtomic(path, data) {
|
|
|
618
646
|
} finally {
|
|
619
647
|
closeSync(fd);
|
|
620
648
|
}
|
|
621
|
-
|
|
649
|
+
renameAtomicRetry(tmp, path);
|
|
622
650
|
const dirFd = openSync(dirname(path), "r");
|
|
623
651
|
try {
|
|
624
652
|
fsyncSync(dirFd);
|
|
@@ -635,14 +663,14 @@ function nowTimestamp() {
|
|
|
635
663
|
}
|
|
636
664
|
function freshBackupTs(backupRoot) {
|
|
637
665
|
const base = nowTimestamp();
|
|
638
|
-
if (!
|
|
666
|
+
if (!existsSync3(join4(backupRoot, base))) return base;
|
|
639
667
|
let n = 1;
|
|
640
|
-
while (
|
|
668
|
+
while (existsSync3(join4(backupRoot, `${base}-${n}`))) n++;
|
|
641
669
|
return `${base}-${n}`;
|
|
642
670
|
}
|
|
643
671
|
function ensureSymlink(linkPath, target) {
|
|
644
|
-
if (
|
|
645
|
-
if (
|
|
672
|
+
if (existsSync3(linkPath)) {
|
|
673
|
+
if (lstatSync3(linkPath).isSymbolicLink()) return;
|
|
646
674
|
die(`${linkPath} exists and is not a symlink. Move it aside first.`);
|
|
647
675
|
}
|
|
648
676
|
mkdirSync(dirname(linkPath), { recursive: true });
|
|
@@ -650,39 +678,42 @@ function ensureSymlink(linkPath, target) {
|
|
|
650
678
|
log(`linked ${linkPath} -> ${target}`);
|
|
651
679
|
}
|
|
652
680
|
function backupUnder(absPath, anchor, destRoot) {
|
|
653
|
-
if (!
|
|
654
|
-
const rel =
|
|
655
|
-
if (rel === "" || rel === ".." || rel.startsWith(`..${
|
|
656
|
-
const dst =
|
|
681
|
+
if (!existsSync3(absPath)) return;
|
|
682
|
+
const rel = relative2(anchor, absPath);
|
|
683
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep2}`)) return;
|
|
684
|
+
const dst = join4(destRoot, rel);
|
|
657
685
|
mkdirSync(dirname(dst), { recursive: true });
|
|
658
|
-
|
|
686
|
+
cpSync3(absPath, dst, { recursive: true, force: false, preserveTimestamps: true });
|
|
659
687
|
}
|
|
660
688
|
function backupBeforeWrite(absPath, ts) {
|
|
661
|
-
backupUnder(absPath, claudeHome(),
|
|
689
|
+
backupUnder(absPath, claudeHome(), join4(backupBase(), ts));
|
|
662
690
|
}
|
|
663
691
|
function backupRepoWrite(absPath, ts, repoHome2) {
|
|
664
|
-
backupUnder(absPath, repoHome2,
|
|
692
|
+
backupUnder(absPath, repoHome2, join4(backupBase(), ts, "repo"));
|
|
665
693
|
}
|
|
666
694
|
function backupExtrasWrite(absPath, ts, projectRoot) {
|
|
667
|
-
backupUnder(absPath, projectRoot,
|
|
695
|
+
backupUnder(absPath, projectRoot, join4(backupBase(), ts, "extras", encodePath(projectRoot)));
|
|
668
696
|
}
|
|
697
|
+
var RENAME_RETRY_MAX_ATTEMPTS, RENAME_RETRY_BACKOFF_MS;
|
|
669
698
|
var init_utils_fs = __esm({
|
|
670
699
|
"src/utils.fs.ts"() {
|
|
671
700
|
"use strict";
|
|
672
701
|
init_config();
|
|
673
702
|
init_utils_json();
|
|
674
703
|
init_utils();
|
|
704
|
+
RENAME_RETRY_MAX_ATTEMPTS = 5;
|
|
705
|
+
RENAME_RETRY_BACKOFF_MS = 10;
|
|
675
706
|
}
|
|
676
707
|
});
|
|
677
708
|
|
|
678
709
|
// src/commands.pull.wedge.ts
|
|
679
710
|
import { execFileSync as execFileSync3 } from "node:child_process";
|
|
680
|
-
import { existsSync as
|
|
681
|
-
import { join as
|
|
711
|
+
import { existsSync as existsSync15 } from "node:fs";
|
|
712
|
+
import { join as join17 } from "node:path";
|
|
682
713
|
function detectWedge(repo) {
|
|
683
|
-
const g =
|
|
684
|
-
if (
|
|
685
|
-
if (
|
|
714
|
+
const g = join17(repo, ".git");
|
|
715
|
+
if (existsSync15(join17(g, "rebase-merge")) || existsSync15(join17(g, "rebase-apply"))) return "rebase";
|
|
716
|
+
if (existsSync15(join17(g, "MERGE_HEAD"))) return "merge";
|
|
686
717
|
return null;
|
|
687
718
|
}
|
|
688
719
|
function unmergedIndexPresent(repo) {
|
|
@@ -739,15 +770,15 @@ var init_commands_pull_wedge = __esm({
|
|
|
739
770
|
});
|
|
740
771
|
|
|
741
772
|
// src/push-gitleaks.config.ts
|
|
742
|
-
import { existsSync as
|
|
773
|
+
import { existsSync as existsSync16, mkdtempSync, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
743
774
|
import { tmpdir } from "node:os";
|
|
744
|
-
import { join as
|
|
775
|
+
import { join as join18 } from "node:path";
|
|
745
776
|
import { fileURLToPath } from "node:url";
|
|
746
777
|
function resolveTomlPath(repo = repoHome()) {
|
|
747
|
-
const repoToml =
|
|
748
|
-
if (
|
|
778
|
+
const repoToml = join18(repo, ".gitleaks.toml");
|
|
779
|
+
if (existsSync16(repoToml)) return repoToml;
|
|
749
780
|
const bundled = fileURLToPath(new URL("../.gitleaks.toml", import.meta.url));
|
|
750
|
-
return
|
|
781
|
+
return existsSync16(bundled) ? bundled : null;
|
|
751
782
|
}
|
|
752
783
|
function assertOverlayAllowlistsScoped(overlayBody) {
|
|
753
784
|
if (OVERLAY_INLINE_ALLOWLIST_RE.test(overlayBody)) {
|
|
@@ -785,17 +816,17 @@ function buildOverlayTempConfig(overlayBody, bundled) {
|
|
|
785
816
|
path = ${JSON.stringify(bundled)}
|
|
786
817
|
|
|
787
818
|
${overlayBody}`;
|
|
788
|
-
const tempPath = mkdtempSync(
|
|
789
|
-
const configPath =
|
|
819
|
+
const tempPath = mkdtempSync(join18(tmpdir(), "nomad-gitleaks-cfg-"));
|
|
820
|
+
const configPath = join18(tempPath, "config.toml");
|
|
790
821
|
writeFileSync3(configPath, tempBody, { mode: 384, flag: "wx" });
|
|
791
822
|
return { configPath, tempPath };
|
|
792
823
|
}
|
|
793
824
|
function resolveTomlConfig() {
|
|
794
825
|
const repo = repoHome();
|
|
795
|
-
const overlayPath =
|
|
796
|
-
const repoToml =
|
|
826
|
+
const overlayPath = join18(repo, ".gitleaks.overlay.toml");
|
|
827
|
+
const repoToml = join18(repo, ".gitleaks.toml");
|
|
797
828
|
const bundled = resolveTomlPath(repo);
|
|
798
|
-
if (!
|
|
829
|
+
if (!existsSync16(overlayPath)) {
|
|
799
830
|
return { path: bundled, tempPath: null };
|
|
800
831
|
}
|
|
801
832
|
if (bundled === repoToml) {
|
|
@@ -808,7 +839,7 @@ function resolveTomlConfig() {
|
|
|
808
839
|
return { path: null, tempPath: null };
|
|
809
840
|
}
|
|
810
841
|
try {
|
|
811
|
-
const overlayBody =
|
|
842
|
+
const overlayBody = readFileSync5(overlayPath, "utf8");
|
|
812
843
|
if (OVERLAY_EXTEND_RE.test(overlayBody)) {
|
|
813
844
|
throw new NomadFatal(
|
|
814
845
|
".gitleaks.overlay.toml must not contain an [extend] block; it is generated automatically. Remove the [extend] section and retry."
|
|
@@ -842,9 +873,9 @@ var init_push_gitleaks_config = __esm({
|
|
|
842
873
|
|
|
843
874
|
// src/push-checks.ts
|
|
844
875
|
import { execFileSync as execFileSync4 } from "node:child_process";
|
|
845
|
-
import { readdirSync as
|
|
876
|
+
import { readdirSync as readdirSync7, rmSync as rmSync7 } from "node:fs";
|
|
846
877
|
import { homedir as homedir2, platform } from "node:os";
|
|
847
|
-
import { join as
|
|
878
|
+
import { delimiter, join as join19 } from "node:path";
|
|
848
879
|
function gitleaksInstallHint() {
|
|
849
880
|
const head = "gitleaks not on PATH (required for nomad push). Install:";
|
|
850
881
|
const plat = platform();
|
|
@@ -852,6 +883,11 @@ function gitleaksInstallHint() {
|
|
|
852
883
|
return `${head}
|
|
853
884
|
brew install gitleaks`;
|
|
854
885
|
}
|
|
886
|
+
if (plat === "win32") {
|
|
887
|
+
return `${head}
|
|
888
|
+
winget install gitleaks.gitleaks
|
|
889
|
+
(or: scoop install gitleaks)`;
|
|
890
|
+
}
|
|
855
891
|
if (plat === "linux") {
|
|
856
892
|
const archMap = { x64: "x64", arm64: "arm64", arm: "armv7" };
|
|
857
893
|
const arch = archMap[process.arch];
|
|
@@ -865,7 +901,7 @@ function gitleaksInstallHint() {
|
|
|
865
901
|
" ~/.local/bin/gitleaks version # verify"
|
|
866
902
|
];
|
|
867
903
|
const localBin = `${homedir2()}/.local/bin`;
|
|
868
|
-
const paths = (process.env.PATH ?? "").split(
|
|
904
|
+
const paths = (process.env.PATH ?? "").split(delimiter);
|
|
869
905
|
if (!paths.includes(localBin)) {
|
|
870
906
|
lines.push(
|
|
871
907
|
" 3. ~/.local/bin is not on PATH; add to your shell rc:",
|
|
@@ -882,12 +918,12 @@ function findGitlinks(dir) {
|
|
|
882
918
|
function walk(current) {
|
|
883
919
|
let entries;
|
|
884
920
|
try {
|
|
885
|
-
entries =
|
|
921
|
+
entries = readdirSync7(current, { withFileTypes: true });
|
|
886
922
|
} catch {
|
|
887
923
|
return;
|
|
888
924
|
}
|
|
889
925
|
for (const e of entries) {
|
|
890
|
-
const p =
|
|
926
|
+
const p = join19(current, e.name);
|
|
891
927
|
if (e.name === ".git") {
|
|
892
928
|
hits.push(p);
|
|
893
929
|
continue;
|
|
@@ -909,7 +945,7 @@ function probeGitleaks() {
|
|
|
909
945
|
if (e.code === "ENOENT") throw new NomadFatal(gitleaksInstallHint());
|
|
910
946
|
throw new NomadFatal(`gitleaks --version failed: ${e.message}`);
|
|
911
947
|
} finally {
|
|
912
|
-
if (tempPath !== null)
|
|
948
|
+
if (tempPath !== null) rmSync7(tempPath, { recursive: true, force: true });
|
|
913
949
|
}
|
|
914
950
|
}
|
|
915
951
|
function wedgePreflight(wedge) {
|
|
@@ -946,12 +982,12 @@ var init_push_checks = __esm({
|
|
|
946
982
|
|
|
947
983
|
// src/push-gitleaks.scan.ts
|
|
948
984
|
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
949
|
-
import { mkdirSync as mkdirSync3, readFileSync as
|
|
985
|
+
import { mkdirSync as mkdirSync3, readFileSync as readFileSync6, rmSync as rmSync8 } from "node:fs";
|
|
950
986
|
import { homedir as homedir3 } from "node:os";
|
|
951
|
-
import { join as
|
|
987
|
+
import { join as join23 } from "node:path";
|
|
952
988
|
function readGitleaksReport(reportPath) {
|
|
953
989
|
try {
|
|
954
|
-
const raw =
|
|
990
|
+
const raw = readFileSync6(reportPath, "utf8");
|
|
955
991
|
const parsed = JSON.parse(raw);
|
|
956
992
|
if (!Array.isArray(parsed)) return null;
|
|
957
993
|
return parsed;
|
|
@@ -960,9 +996,9 @@ function readGitleaksReport(reportPath) {
|
|
|
960
996
|
}
|
|
961
997
|
}
|
|
962
998
|
function scanStagedTree(repoDir, forwardStreams = false) {
|
|
963
|
-
const cacheDir =
|
|
999
|
+
const cacheDir = join23(homedir3(), ".cache", "claude-nomad");
|
|
964
1000
|
mkdirSync3(cacheDir, { recursive: true });
|
|
965
|
-
const reportPath =
|
|
1001
|
+
const reportPath = join23(cacheDir, `gitleaks-${nowTimestamp()}-${process.pid}.json`);
|
|
966
1002
|
const { path: toml, tempPath } = resolveTomlConfig();
|
|
967
1003
|
const args = [
|
|
968
1004
|
"protect",
|
|
@@ -990,14 +1026,14 @@ function scanStagedTree(repoDir, forwardStreams = false) {
|
|
|
990
1026
|
}
|
|
991
1027
|
return report;
|
|
992
1028
|
} finally {
|
|
993
|
-
if (tempPath !== null)
|
|
994
|
-
|
|
1029
|
+
if (tempPath !== null) rmSync8(tempPath, { recursive: true, force: true });
|
|
1030
|
+
rmSync8(reportPath, { force: true });
|
|
995
1031
|
}
|
|
996
1032
|
}
|
|
997
1033
|
function scanFile(filePath, forwardStreams = false) {
|
|
998
|
-
const cacheDir =
|
|
1034
|
+
const cacheDir = join23(homedir3(), ".cache", "claude-nomad");
|
|
999
1035
|
mkdirSync3(cacheDir, { recursive: true });
|
|
1000
|
-
const reportPath =
|
|
1036
|
+
const reportPath = join23(cacheDir, `gitleaks-file-${nowTimestamp()}-${process.pid}.json`);
|
|
1001
1037
|
const { path: toml, tempPath } = resolveTomlConfig();
|
|
1002
1038
|
const args = [
|
|
1003
1039
|
"detect",
|
|
@@ -1025,8 +1061,8 @@ function scanFile(filePath, forwardStreams = false) {
|
|
|
1025
1061
|
}
|
|
1026
1062
|
return report;
|
|
1027
1063
|
} finally {
|
|
1028
|
-
if (tempPath !== null)
|
|
1029
|
-
|
|
1064
|
+
if (tempPath !== null) rmSync8(tempPath, { recursive: true, force: true });
|
|
1065
|
+
rmSync8(reportPath, { force: true });
|
|
1030
1066
|
}
|
|
1031
1067
|
}
|
|
1032
1068
|
var init_push_gitleaks_scan = __esm({
|
|
@@ -1228,189 +1264,13 @@ var init_push_leak_verdict = __esm({
|
|
|
1228
1264
|
// src/commands.adopt.ts
|
|
1229
1265
|
init_config();
|
|
1230
1266
|
init_config_sharedDirs_guard();
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
init_utils_json();
|
|
1234
|
-
import { cpSync as cpSync2, existsSync as existsSync2, lstatSync as lstatSync2, rmSync } from "node:fs";
|
|
1235
|
-
import { join as join3 } from "node:path";
|
|
1236
|
-
var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
|
|
1237
|
-
function lexists(p) {
|
|
1238
|
-
try {
|
|
1239
|
-
lstatSync2(p);
|
|
1240
|
-
return true;
|
|
1241
|
-
} catch {
|
|
1242
|
-
return false;
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
function readMapIfPresent(repoHome2) {
|
|
1246
|
-
const mapPath = join3(repoHome2, "path-map.json");
|
|
1247
|
-
return existsSync2(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
1248
|
-
}
|
|
1249
|
-
function isConfiguredTarget(name, map) {
|
|
1250
|
-
return SHARED_LINKS.includes(name) || (map.sharedDirs?.includes(name) ?? false);
|
|
1251
|
-
}
|
|
1252
|
-
function isValidAdoptName(name) {
|
|
1253
|
-
if (SHARED_LINKS.includes(name)) return true;
|
|
1254
|
-
return isValidSharedDir(name);
|
|
1255
|
-
}
|
|
1256
|
-
function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
|
|
1257
|
-
const ts = freshBackupTs(backup);
|
|
1258
|
-
backupBeforeWrite(linkPath, ts);
|
|
1259
|
-
cpSync2(linkPath, sharedTarget, { recursive: true, force: true, preserveTimestamps: true });
|
|
1260
|
-
rmSync(linkPath, { recursive: true, force: true });
|
|
1261
|
-
ensureSymlink(linkPath, sharedTarget);
|
|
1262
|
-
const rel = join3("shared", name);
|
|
1263
|
-
gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
|
|
1264
|
-
log(`adopted ${name}; ${ADOPT_PUSH_HINT}`);
|
|
1265
|
-
}
|
|
1266
|
-
function cmdAdopt(name, opts = {}) {
|
|
1267
|
-
const dryRun = opts.dryRun === true;
|
|
1268
|
-
if (!isValidAdoptName(name)) {
|
|
1269
|
-
fail(`invalid name: ${JSON.stringify(name)}`);
|
|
1270
|
-
process.exit(1);
|
|
1271
|
-
}
|
|
1272
|
-
const repo = repoHome();
|
|
1273
|
-
const claude = claudeHome();
|
|
1274
|
-
const backup = backupBase();
|
|
1275
|
-
const map = readMapIfPresent(repo);
|
|
1276
|
-
if (!isConfiguredTarget(name, map)) {
|
|
1277
|
-
fail(
|
|
1278
|
-
`${name}: not a configured shared target. Add it to sharedDirs in path-map.json first, then re-run adopt.`
|
|
1279
|
-
);
|
|
1280
|
-
process.exit(1);
|
|
1281
|
-
}
|
|
1282
|
-
const linkPath = join3(claude, name);
|
|
1283
|
-
const sharedTarget = join3(repo, "shared", name);
|
|
1284
|
-
if (!existsSync2(linkPath)) {
|
|
1285
|
-
log(`${name}: nothing to adopt (not present in ~/.claude/)`);
|
|
1286
|
-
return;
|
|
1287
|
-
}
|
|
1288
|
-
if (lstatSync2(linkPath).isSymbolicLink()) {
|
|
1289
|
-
log(`${name}: already adopted (already a symlink)`);
|
|
1290
|
-
return;
|
|
1291
|
-
}
|
|
1292
|
-
if (lexists(sharedTarget)) {
|
|
1293
|
-
fail(`${name}: shared/${name} already exists; would clobber. Remove it first.`);
|
|
1294
|
-
process.exit(1);
|
|
1295
|
-
}
|
|
1296
|
-
if (dryRun) {
|
|
1297
|
-
const ts = freshBackupTs(backup);
|
|
1298
|
-
log(`would backup: ${linkPath} -> backup/${ts}/${name}`);
|
|
1299
|
-
log(`would move: ${linkPath} -> shared/${name}`);
|
|
1300
|
-
log(`would stage: shared/${name}`);
|
|
1301
|
-
return;
|
|
1302
|
-
}
|
|
1303
|
-
try {
|
|
1304
|
-
performAdoptMove(name, linkPath, sharedTarget, repo, backup);
|
|
1305
|
-
} catch (err) {
|
|
1306
|
-
if (!(err instanceof NomadFatal)) throw err;
|
|
1307
|
-
fail(err.message);
|
|
1308
|
-
process.exitCode = 1;
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// src/commands.allow.ts
|
|
1313
|
-
init_config();
|
|
1314
|
-
import { existsSync as existsSync3 } from "node:fs";
|
|
1315
|
-
|
|
1316
|
-
// src/commands.redact.core.ts
|
|
1317
|
-
import { appendFileSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1318
|
-
import { join as join4 } from "node:path";
|
|
1319
|
-
function collectMatchIntervals(content, findings) {
|
|
1320
|
-
const intervals = [];
|
|
1321
|
-
for (const f of findings) {
|
|
1322
|
-
const match = f.Match;
|
|
1323
|
-
if (match === "") continue;
|
|
1324
|
-
let from = 0;
|
|
1325
|
-
let pos = content.indexOf(match, from);
|
|
1326
|
-
while (pos !== -1) {
|
|
1327
|
-
intervals.push({ start: pos, end: pos + match.length, ruleId: f.RuleID });
|
|
1328
|
-
from = pos + match.length;
|
|
1329
|
-
pos = content.indexOf(match, from);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
return intervals;
|
|
1333
|
-
}
|
|
1334
|
-
function mergeIntervals(intervals) {
|
|
1335
|
-
if (intervals.length === 0) return [];
|
|
1336
|
-
const sorted = [...intervals].sort((a, b) => a.start - b.start || b.end - a.end);
|
|
1337
|
-
let last = { ...sorted[0] };
|
|
1338
|
-
const merged = [last];
|
|
1339
|
-
for (let i = 1; i < sorted.length; i++) {
|
|
1340
|
-
const cur = sorted[i];
|
|
1341
|
-
if (cur.start <= last.end) {
|
|
1342
|
-
if (cur.end > last.end) last.end = cur.end;
|
|
1343
|
-
} else {
|
|
1344
|
-
last = { ...cur };
|
|
1345
|
-
merged.push(last);
|
|
1346
|
-
}
|
|
1347
|
-
}
|
|
1348
|
-
return merged;
|
|
1349
|
-
}
|
|
1350
|
-
function applyRedactions(content, findings) {
|
|
1351
|
-
const raw = collectMatchIntervals(content, findings);
|
|
1352
|
-
if (raw.length === 0) return content;
|
|
1353
|
-
const merged = mergeIntervals(raw);
|
|
1354
|
-
let result = content;
|
|
1355
|
-
for (let i = merged.length - 1; i >= 0; i--) {
|
|
1356
|
-
const { start, end, ruleId } = merged[i];
|
|
1357
|
-
result = result.slice(0, start) + `[REDACTED:${ruleId}]` + result.slice(end);
|
|
1358
|
-
}
|
|
1359
|
-
return result;
|
|
1360
|
-
}
|
|
1361
|
-
function formatFingerprint(fingerprint) {
|
|
1362
|
-
return fingerprint.replace(/[\r\n]/g, "") + "\n";
|
|
1363
|
-
}
|
|
1364
|
-
function isRecentlyModified(mtimeMs, nowMs, thresholdMs = 5 * 60 * 1e3) {
|
|
1365
|
-
return nowMs - mtimeMs < thresholdMs;
|
|
1366
|
-
}
|
|
1367
|
-
function isValidFingerprint(fingerprint) {
|
|
1368
|
-
return fingerprint.trim().length > 0 && fingerprint.length <= 512 && /^[^\r\n]+$/.test(fingerprint);
|
|
1369
|
-
}
|
|
1370
|
-
function isAlreadyPresent(line, lines) {
|
|
1371
|
-
return lines.includes(line);
|
|
1372
|
-
}
|
|
1373
|
-
function appendGitleaksIgnore(fingerprint, repo) {
|
|
1374
|
-
const sanitized = fingerprint.replace(/[\r\n]/g, "").trim();
|
|
1375
|
-
if (sanitized.length === 0) return;
|
|
1376
|
-
const ignPath = join4(repo, ".gitleaksignore");
|
|
1377
|
-
let raw;
|
|
1378
|
-
try {
|
|
1379
|
-
raw = readFileSync2(ignPath, "utf8");
|
|
1380
|
-
} catch {
|
|
1381
|
-
raw = "";
|
|
1382
|
-
}
|
|
1383
|
-
const existing = raw.split("\n").filter((l) => l.length > 0);
|
|
1384
|
-
if (isAlreadyPresent(sanitized, existing)) return;
|
|
1385
|
-
const needsLeadingNewline = raw.length > 0 && !raw.endsWith("\n");
|
|
1386
|
-
const prefix = needsLeadingNewline ? "\n" : "";
|
|
1387
|
-
appendFileSync(ignPath, prefix + formatFingerprint(sanitized), "utf8");
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// src/commands.allow.ts
|
|
1391
|
-
init_utils();
|
|
1392
|
-
function cmdAllow(fingerprints) {
|
|
1393
|
-
const repo = repoHome();
|
|
1394
|
-
if (!existsSync3(repo)) die(`repo not cloned at ${repo}`);
|
|
1395
|
-
for (const fp of fingerprints) {
|
|
1396
|
-
if (!isValidFingerprint(fp)) {
|
|
1397
|
-
const shown = fp.replaceAll("\r", String.raw`\r`).replaceAll("\n", String.raw`\n`);
|
|
1398
|
-
fail(`invalid fingerprint: ${shown}`);
|
|
1399
|
-
process.exit(1);
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
for (const fp of fingerprints) {
|
|
1403
|
-
appendGitleaksIgnore(fp, repo);
|
|
1404
|
-
item(`allowed: ${fp}`);
|
|
1405
|
-
}
|
|
1406
|
-
log(`allowed ${fingerprints.length} fingerprint(s)`);
|
|
1407
|
-
}
|
|
1267
|
+
import { cpSync as cpSync4, existsSync as existsSync5, lstatSync as lstatSync5, rmSync as rmSync4 } from "node:fs";
|
|
1268
|
+
import { join as join6 } from "node:path";
|
|
1408
1269
|
|
|
1409
|
-
// src/
|
|
1270
|
+
// src/links.ts
|
|
1410
1271
|
init_config();
|
|
1411
|
-
import { existsSync as
|
|
1412
|
-
import { join as
|
|
1413
|
-
import { createInterface } from "node:readline/promises";
|
|
1272
|
+
import { existsSync as existsSync4, lstatSync as lstatSync4, rmSync as rmSync3 } from "node:fs";
|
|
1273
|
+
import { join as join5 } from "node:path";
|
|
1414
1274
|
|
|
1415
1275
|
// src/hooks-filter.ts
|
|
1416
1276
|
init_config();
|
|
@@ -1630,120 +1490,569 @@ function buildCaptureSubset(merged, settings, opts) {
|
|
|
1630
1490
|
return out;
|
|
1631
1491
|
}
|
|
1632
1492
|
|
|
1633
|
-
// src/
|
|
1493
|
+
// src/extras-sync.core.ts
|
|
1634
1494
|
init_config();
|
|
1635
|
-
import { existsSync as
|
|
1636
|
-
import { join as
|
|
1495
|
+
import { cpSync as cpSync2, existsSync as existsSync2, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
|
|
1496
|
+
import { basename as basename2, join as join3, relative } from "node:path";
|
|
1497
|
+
|
|
1498
|
+
// src/extras-sync.collision.ts
|
|
1637
1499
|
init_utils();
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
function
|
|
1641
|
-
|
|
1642
|
-
onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
|
|
1643
|
-
} else {
|
|
1644
|
-
log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
|
|
1645
|
-
}
|
|
1500
|
+
import { cpSync, existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
|
|
1501
|
+
import { basename, join as join2 } from "node:path";
|
|
1502
|
+
function quoted(p) {
|
|
1503
|
+
return `"${p}"`;
|
|
1646
1504
|
}
|
|
1647
|
-
function
|
|
1648
|
-
if (
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1505
|
+
function stripCollidingDstSymlinks(src, dst, isExcluded) {
|
|
1506
|
+
if (!existsSync(dst)) return;
|
|
1507
|
+
for (const name of readdirSync(src)) {
|
|
1508
|
+
if (isExcluded(name)) continue;
|
|
1509
|
+
const dstPath = join2(dst, name);
|
|
1510
|
+
const dstStat = lstatSync(dstPath, { throwIfNoEntry: false });
|
|
1511
|
+
if (dstStat === void 0) continue;
|
|
1512
|
+
if (dstStat.isSymbolicLink()) {
|
|
1513
|
+
rmSync(dstPath, { recursive: true, force: true });
|
|
1514
|
+
} else if (dstStat.isDirectory() && lstatSync(join2(src, name)).isDirectory()) {
|
|
1515
|
+
stripCollidingDstSymlinks(join2(src, name), dstPath, isExcluded);
|
|
1516
|
+
}
|
|
1652
1517
|
}
|
|
1653
1518
|
}
|
|
1654
|
-
function
|
|
1655
|
-
|
|
1519
|
+
function cpSyncGuarded(src, dst, filter, label) {
|
|
1520
|
+
try {
|
|
1521
|
+
cpSync(src, dst, { recursive: true, force: true, verbatimSymlinks: true, filter });
|
|
1522
|
+
} catch (err) {
|
|
1523
|
+
const e = err;
|
|
1524
|
+
if (e.code === "EINVAL" || e.code === "ENOTEMPTY" || e.code === "ERR_FS_CP_NON_DIR_TO_DIR" || e.code === "ERR_FS_CP_DIR_TO_NON_DIR" || process.platform === "win32" && e.errno === 145) {
|
|
1525
|
+
throw new NomadFatal(
|
|
1526
|
+
`${label}: type collision copying ${quoted(src)} -> ${quoted(dst)} (${e.path ?? "unknown path"}): a file/directory type changed upstream; run nomad pull --force-remote to recover`
|
|
1527
|
+
);
|
|
1528
|
+
}
|
|
1529
|
+
throw err;
|
|
1530
|
+
}
|
|
1656
1531
|
}
|
|
1657
|
-
function
|
|
1658
|
-
for (const name of
|
|
1659
|
-
|
|
1660
|
-
const
|
|
1661
|
-
|
|
1662
|
-
if (
|
|
1663
|
-
|
|
1664
|
-
if (dryRun) {
|
|
1665
|
-
emitAutoMove(onPreview, linkPath, ts, name);
|
|
1532
|
+
function prunePreservingBy(src, dst, isPreserved) {
|
|
1533
|
+
for (const name of readdirSync(dst)) {
|
|
1534
|
+
if (isPreserved(name)) continue;
|
|
1535
|
+
const dstPath = join2(dst, name);
|
|
1536
|
+
const srcStat = lstatSync(join2(src, name), { throwIfNoEntry: false });
|
|
1537
|
+
if (srcStat === void 0) {
|
|
1538
|
+
rmSync(dstPath, { recursive: true, force: true });
|
|
1666
1539
|
continue;
|
|
1667
1540
|
}
|
|
1668
|
-
|
|
1669
|
-
|
|
1541
|
+
const dstStat = lstatSync(dstPath);
|
|
1542
|
+
if (srcStat.isDirectory() && dstStat.isDirectory()) {
|
|
1543
|
+
prunePreservingBy(join2(src, name), dstPath, isPreserved);
|
|
1544
|
+
} else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
|
|
1545
|
+
rmSync(dstPath, { recursive: true, force: true });
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
|
|
1550
|
+
const dstStat = lstatSync(dst, { throwIfNoEntry: false });
|
|
1551
|
+
if (dstStat !== void 0) {
|
|
1552
|
+
if (dstStat.isDirectory()) {
|
|
1553
|
+
const srcStat = lstatSync(src, { throwIfNoEntry: false });
|
|
1554
|
+
if (srcStat !== void 0 && !srcStat.isDirectory()) {
|
|
1555
|
+
throw new NomadFatal(
|
|
1556
|
+
`copyExtrasFilteredPreservingBy: type mismatch copying ${quoted(src)} -> ${quoted(dst)}: the repo entry is a file but the local entry is a directory; run nomad pull --force-remote to recover`
|
|
1557
|
+
);
|
|
1558
|
+
}
|
|
1559
|
+
prunePreservingBy(src, dst, isPreserved);
|
|
1560
|
+
} else {
|
|
1561
|
+
rmSync(dst, { recursive: true, force: true });
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
stripCollidingDstSymlinks(src, dst, isPreserved);
|
|
1565
|
+
cpSync(src, dst, {
|
|
1566
|
+
recursive: true,
|
|
1567
|
+
force: true,
|
|
1568
|
+
verbatimSymlinks: true,
|
|
1569
|
+
filter: (srcEntry) => srcEntry === src || !isPreserved(basename(srcEntry))
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
// src/extras-sync.guards.ts
|
|
1574
|
+
init_utils();
|
|
1575
|
+
init_config_sharedDirs_guard();
|
|
1576
|
+
import { isAbsolute, normalize, sep } from "node:path";
|
|
1577
|
+
function assertSafeLocalRoot(localRoot, logical) {
|
|
1578
|
+
if (!isAbsolute(localRoot)) {
|
|
1579
|
+
throw new NomadFatal(
|
|
1580
|
+
`invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
|
|
1581
|
+
);
|
|
1670
1582
|
}
|
|
1583
|
+
const canonical = process.platform === "win32" ? localRoot.split("/").join(sep) : localRoot;
|
|
1584
|
+
if (canonical !== normalize(canonical)) {
|
|
1585
|
+
throw new NomadFatal(
|
|
1586
|
+
`invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// src/extras-sync.core.ts
|
|
1592
|
+
init_utils();
|
|
1593
|
+
init_utils_json();
|
|
1594
|
+
function loadValidatedExtras(opts) {
|
|
1595
|
+
const repo = repoHome();
|
|
1596
|
+
const mapPath = join3(repo, "path-map.json");
|
|
1597
|
+
const repoExtras = join3(repo, "shared", "extras");
|
|
1598
|
+
if (!existsSync2(mapPath) || opts.requireRepoExtras === true && !existsSync2(repoExtras)) {
|
|
1599
|
+
if (opts.missingMsg !== void 0) log(opts.missingMsg);
|
|
1600
|
+
return null;
|
|
1601
|
+
}
|
|
1602
|
+
const map = readPathMap(mapPath);
|
|
1603
|
+
const extrasMap = map.extras ?? {};
|
|
1604
|
+
if (Object.keys(extrasMap).length === 0) return null;
|
|
1605
|
+
for (const logical of Object.keys(extrasMap)) {
|
|
1606
|
+
assertSafeLogical(logical);
|
|
1607
|
+
const localRoot = map.projects[logical]?.[HOST];
|
|
1608
|
+
if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
|
|
1609
|
+
}
|
|
1610
|
+
return { map, extrasMap };
|
|
1611
|
+
}
|
|
1612
|
+
function* eachExtrasTarget(v, counts) {
|
|
1613
|
+
const whitelist = SUPPORTED_EXTRAS;
|
|
1614
|
+
for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
|
|
1615
|
+
const localRoot = v.map.projects[logical]?.[HOST];
|
|
1616
|
+
if (!localRoot || localRoot === "TBD") {
|
|
1617
|
+
counts.unmapped++;
|
|
1618
|
+
continue;
|
|
1619
|
+
}
|
|
1620
|
+
for (const dirname10 of dirnames) {
|
|
1621
|
+
if (!whitelist.includes(dirname10)) {
|
|
1622
|
+
counts.skipped++;
|
|
1623
|
+
continue;
|
|
1624
|
+
}
|
|
1625
|
+
yield { logical, localRoot, dirname: dirname10 };
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
function copyExtrasOverlayFiltered(src, dst, blockSet) {
|
|
1630
|
+
stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
|
|
1631
|
+
cpSyncGuarded(
|
|
1632
|
+
src,
|
|
1633
|
+
dst,
|
|
1634
|
+
(srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry)),
|
|
1635
|
+
"copyExtrasOverlayFiltered"
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
function copyExtrasOverlaySkipDiverged(src, dst, blockSet, divergedSet) {
|
|
1639
|
+
stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
|
|
1640
|
+
cpSyncGuarded(
|
|
1641
|
+
src,
|
|
1642
|
+
dst,
|
|
1643
|
+
(srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry)) && !divergedSet.has(relative(src, srcEntry)),
|
|
1644
|
+
"copyExtrasOverlaySkipDiverged"
|
|
1645
|
+
);
|
|
1646
|
+
}
|
|
1647
|
+
function copyExtras(src, dst) {
|
|
1648
|
+
rmSync2(dst, { recursive: true, force: true });
|
|
1649
|
+
cpSync2(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
|
|
1650
|
+
}
|
|
1651
|
+
function copyExtrasFileSkipDiverged(src, dst) {
|
|
1652
|
+
if (existsSync2(dst)) {
|
|
1653
|
+
let dstBuf;
|
|
1654
|
+
try {
|
|
1655
|
+
dstBuf = readFileSync2(dst);
|
|
1656
|
+
} catch {
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
if (!readFileSync2(src).equals(dstBuf)) return;
|
|
1660
|
+
}
|
|
1661
|
+
copyExtras(src, dst);
|
|
1662
|
+
}
|
|
1663
|
+
function extrasDenySet(dirname10) {
|
|
1664
|
+
return dirname10 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
|
|
1665
|
+
}
|
|
1666
|
+
function copyExtrasFiltered(src, dst, blockSet) {
|
|
1667
|
+
rmSync2(dst, { recursive: true, force: true });
|
|
1668
|
+
cpSync2(src, dst, {
|
|
1669
|
+
recursive: true,
|
|
1670
|
+
force: true,
|
|
1671
|
+
verbatimSymlinks: true,
|
|
1672
|
+
filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry))
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
function prunePreservingDenied(src, dst, blockSet) {
|
|
1676
|
+
for (const name of readdirSync2(dst)) {
|
|
1677
|
+
if (isDeniedName(blockSet, name)) continue;
|
|
1678
|
+
const dstPath = join3(dst, name);
|
|
1679
|
+
const srcStat = lstatSync2(join3(src, name), { throwIfNoEntry: false });
|
|
1680
|
+
if (srcStat === void 0) {
|
|
1681
|
+
rmSync2(dstPath, { recursive: true, force: true });
|
|
1682
|
+
continue;
|
|
1683
|
+
}
|
|
1684
|
+
const dstStat = lstatSync2(dstPath);
|
|
1685
|
+
if (srcStat.isDirectory() && dstStat.isDirectory()) {
|
|
1686
|
+
prunePreservingDenied(join3(src, name), dstPath, blockSet);
|
|
1687
|
+
} else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
|
|
1688
|
+
rmSync2(dstPath, { recursive: true, force: true });
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
function copyExtrasFilteredPreserving(src, dst, blockSet) {
|
|
1693
|
+
const dstStat = lstatSync2(dst, { throwIfNoEntry: false });
|
|
1694
|
+
if (dstStat !== void 0) {
|
|
1695
|
+
if (dstStat.isDirectory()) prunePreservingDenied(src, dst, blockSet);
|
|
1696
|
+
else rmSync2(dst, { recursive: true, force: true });
|
|
1697
|
+
}
|
|
1698
|
+
stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
|
|
1699
|
+
cpSync2(src, dst, {
|
|
1700
|
+
recursive: true,
|
|
1701
|
+
force: true,
|
|
1702
|
+
verbatimSymlinks: true,
|
|
1703
|
+
filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry))
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
// src/links.ts
|
|
1708
|
+
init_utils();
|
|
1709
|
+
init_utils_fs();
|
|
1710
|
+
init_utils_json();
|
|
1711
|
+
function emitAutoMove(onPreview, linkPath, ts, name) {
|
|
1712
|
+
if (onPreview) {
|
|
1713
|
+
onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
|
|
1714
|
+
} else {
|
|
1715
|
+
log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
function emitCreate(onPreview, from, to) {
|
|
1719
|
+
if (onPreview) {
|
|
1720
|
+
onPreview({ kind: "create", from, to });
|
|
1721
|
+
} else {
|
|
1722
|
+
log(`would create symlink: ${from} -> ${to}`);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
function emitCopy(onPreview, from, to) {
|
|
1726
|
+
if (onPreview) {
|
|
1727
|
+
onPreview({ kind: "copy", from, to });
|
|
1728
|
+
} else {
|
|
1729
|
+
log(`would copy: ${from} -> ${to}`);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
function isAlreadySymlink(linkPath) {
|
|
1733
|
+
return existsSync4(linkPath) && lstatSync4(linkPath).isSymbolicLink();
|
|
1734
|
+
}
|
|
1735
|
+
function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
|
|
1736
|
+
for (const name of linkNames) {
|
|
1737
|
+
const linkPath = join5(claude, name);
|
|
1738
|
+
const target = join5(repo, "shared", name);
|
|
1739
|
+
if (!existsSync4(linkPath)) continue;
|
|
1740
|
+
if (lstatSync4(linkPath).isSymbolicLink()) continue;
|
|
1741
|
+
if (!existsSync4(target)) continue;
|
|
1742
|
+
if (dryRun) {
|
|
1743
|
+
emitAutoMove(onPreview, linkPath, ts, name);
|
|
1744
|
+
continue;
|
|
1745
|
+
}
|
|
1746
|
+
backupBeforeWrite(linkPath, ts);
|
|
1747
|
+
rmSync3(linkPath, { recursive: true, force: true });
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
function copySharedLinkPull(src, dst) {
|
|
1751
|
+
copyExtrasFilteredPreservingBy(src, dst, (name) => isDeniedName(ALWAYS_NEVER_SYNC, name));
|
|
1752
|
+
}
|
|
1753
|
+
function applySharedLinksWin32(linkNames, claude, repo, ts, dryRun, onPreview) {
|
|
1754
|
+
for (const name of linkNames) {
|
|
1755
|
+
const target = join5(repo, "shared", name);
|
|
1756
|
+
if (!existsSync4(target)) continue;
|
|
1757
|
+
const linkPath = join5(claude, name);
|
|
1758
|
+
if (dryRun) {
|
|
1759
|
+
emitCopy(onPreview, linkPath, target);
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
const stat = lstatSync4(linkPath, { throwIfNoEntry: false });
|
|
1763
|
+
if (stat !== void 0) {
|
|
1764
|
+
backupBeforeWrite(linkPath, ts);
|
|
1765
|
+
if (stat.isSymbolicLink()) {
|
|
1766
|
+
rmSync3(linkPath, { recursive: true, force: true });
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
copySharedLinkPull(target, linkPath);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
function applySharedLinks(ts, map, opts = {}) {
|
|
1773
|
+
const dryRun = opts.dryRun === true;
|
|
1774
|
+
const claude = claudeHome();
|
|
1775
|
+
const repo = repoHome();
|
|
1776
|
+
const linkNames = allSharedLinks(map);
|
|
1777
|
+
if (process.platform === "win32") {
|
|
1778
|
+
applySharedLinksWin32(linkNames, claude, repo, ts, dryRun, opts.onPreview);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
runAutoMovePasses(linkNames, claude, repo, ts, dryRun, opts.onPreview);
|
|
1782
|
+
for (const name of linkNames) {
|
|
1783
|
+
const target = join5(repo, "shared", name);
|
|
1784
|
+
if (!existsSync4(target)) continue;
|
|
1785
|
+
const linkPath = join5(claude, name);
|
|
1786
|
+
if (isAlreadySymlink(linkPath)) continue;
|
|
1787
|
+
if (dryRun) {
|
|
1788
|
+
emitCreate(opts.onPreview, linkPath, target);
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
ensureSymlink(linkPath, target);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
function syncSharedLinksPush(map) {
|
|
1795
|
+
if (process.platform !== "win32") return;
|
|
1796
|
+
if (map === null) return;
|
|
1797
|
+
const claude = claudeHome();
|
|
1798
|
+
const repo = repoHome();
|
|
1799
|
+
for (const name of allSharedLinks(map)) {
|
|
1800
|
+
const localPath = join5(claude, name);
|
|
1801
|
+
const stat = lstatSync4(localPath, { throwIfNoEntry: false });
|
|
1802
|
+
if (stat === void 0) continue;
|
|
1803
|
+
if (stat.isSymbolicLink()) continue;
|
|
1804
|
+
copyExtrasFiltered(localPath, join5(repo, "shared", name), ALWAYS_NEVER_SYNC);
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
function regenerateSettings(ts, opts = {}) {
|
|
1808
|
+
const dryRun = opts.dryRun === true;
|
|
1809
|
+
const suppressDriftWarn = opts.suppressDriftWarn === true;
|
|
1810
|
+
const repo = repoHome();
|
|
1811
|
+
const claude = claudeHome();
|
|
1812
|
+
const basePath = join5(repo, "shared", "settings.base.json");
|
|
1813
|
+
const hostPath = join5(repo, "hosts", `${HOST}.json`);
|
|
1814
|
+
if (!existsSync4(basePath)) {
|
|
1815
|
+
die("repo not initialized; run 'nomad init' to scaffold");
|
|
1816
|
+
}
|
|
1817
|
+
const base = readJson(basePath);
|
|
1818
|
+
const hasOverrides = existsSync4(hostPath);
|
|
1819
|
+
const overrides = hasOverrides ? readJson(hostPath) : {};
|
|
1820
|
+
const merged = deepMerge(base, overrides);
|
|
1821
|
+
const settingsPath = join5(claude, "settings.json");
|
|
1822
|
+
if (!suppressDriftWarn && existsSync4(settingsPath)) {
|
|
1823
|
+
try {
|
|
1824
|
+
const existing = readJson(settingsPath);
|
|
1825
|
+
const drift = classifySettingsDrift(merged, existing);
|
|
1826
|
+
if (drift.behind.length > 0) {
|
|
1827
|
+
const { phrase, pronoun } = describeSettings(drift.behind);
|
|
1828
|
+
warn(
|
|
1829
|
+
`your settings.json is missing ${phrase} that the synced copy has; run 'nomad pull' to restore ${pronoun}.`
|
|
1830
|
+
);
|
|
1831
|
+
}
|
|
1832
|
+
const { promotable } = partitionByCaptureExclusion(drift.ahead);
|
|
1833
|
+
if (promotable.length > 0) {
|
|
1834
|
+
const { phrase, pronoun, verb } = describeSettings(promotable);
|
|
1835
|
+
warn(
|
|
1836
|
+
`your settings.json has ${phrase} that ${verb} not yet synced; run 'nomad capture-settings' to save ${pronoun} to the repo before the next pull overwrites ${pronoun}.`
|
|
1837
|
+
);
|
|
1838
|
+
}
|
|
1839
|
+
} catch {
|
|
1840
|
+
warn("existing settings.json is malformed; skipping drift-check and regenerating.");
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
|
|
1844
|
+
if (dryRun) {
|
|
1845
|
+
log(`would write settings.json (base + ${overrideLabel})`);
|
|
1846
|
+
return { label: overrideLabel };
|
|
1847
|
+
}
|
|
1848
|
+
backupBeforeWrite(settingsPath, ts);
|
|
1849
|
+
writeJsonAtomic(settingsPath, stripGsdHookEntries(merged));
|
|
1850
|
+
return { label: overrideLabel };
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// src/commands.adopt.ts
|
|
1854
|
+
init_utils();
|
|
1855
|
+
init_utils_fs();
|
|
1856
|
+
init_utils_json();
|
|
1857
|
+
var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
|
|
1858
|
+
function lexists(p) {
|
|
1859
|
+
try {
|
|
1860
|
+
lstatSync5(p);
|
|
1861
|
+
return true;
|
|
1862
|
+
} catch {
|
|
1863
|
+
return false;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
function readMapIfPresent(repoHome2) {
|
|
1867
|
+
const mapPath = join6(repoHome2, "path-map.json");
|
|
1868
|
+
return existsSync5(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
1869
|
+
}
|
|
1870
|
+
function isConfiguredTarget(name, map) {
|
|
1871
|
+
return SHARED_LINKS.includes(name) || (map.sharedDirs?.includes(name) ?? false);
|
|
1872
|
+
}
|
|
1873
|
+
function isValidAdoptName(name) {
|
|
1874
|
+
if (SHARED_LINKS.includes(name)) return true;
|
|
1875
|
+
return isValidSharedDir(name);
|
|
1876
|
+
}
|
|
1877
|
+
function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
|
|
1878
|
+
const ts = freshBackupTs(backup);
|
|
1879
|
+
backupBeforeWrite(linkPath, ts);
|
|
1880
|
+
cpSync4(linkPath, sharedTarget, { recursive: true, force: true, preserveTimestamps: true });
|
|
1881
|
+
rmSync4(linkPath, { recursive: true, force: true });
|
|
1882
|
+
if (process.platform === "win32") {
|
|
1883
|
+
copySharedLinkPull(sharedTarget, linkPath);
|
|
1884
|
+
} else {
|
|
1885
|
+
ensureSymlink(linkPath, sharedTarget);
|
|
1886
|
+
}
|
|
1887
|
+
const rel = join6("shared", name);
|
|
1888
|
+
gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
|
|
1889
|
+
log(`adopted ${name}; ${ADOPT_PUSH_HINT}`);
|
|
1890
|
+
}
|
|
1891
|
+
function cmdAdopt(name, opts = {}) {
|
|
1892
|
+
const dryRun = opts.dryRun === true;
|
|
1893
|
+
if (!isValidAdoptName(name)) {
|
|
1894
|
+
fail(`invalid name: ${JSON.stringify(name)}`);
|
|
1895
|
+
process.exit(1);
|
|
1896
|
+
}
|
|
1897
|
+
const repo = repoHome();
|
|
1898
|
+
const claude = claudeHome();
|
|
1899
|
+
const backup = backupBase();
|
|
1900
|
+
const map = readMapIfPresent(repo);
|
|
1901
|
+
if (!isConfiguredTarget(name, map)) {
|
|
1902
|
+
fail(
|
|
1903
|
+
`${name}: not a configured shared target. Add it to sharedDirs in path-map.json first, then re-run adopt.`
|
|
1904
|
+
);
|
|
1905
|
+
process.exit(1);
|
|
1906
|
+
}
|
|
1907
|
+
const linkPath = join6(claude, name);
|
|
1908
|
+
const sharedTarget = join6(repo, "shared", name);
|
|
1909
|
+
if (!existsSync5(linkPath)) {
|
|
1910
|
+
log(`${name}: nothing to adopt (not present in ~/.claude/)`);
|
|
1911
|
+
return;
|
|
1912
|
+
}
|
|
1913
|
+
if (lstatSync5(linkPath).isSymbolicLink()) {
|
|
1914
|
+
log(`${name}: already adopted (already a symlink)`);
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
if (process.platform === "win32" && lexists(sharedTarget)) {
|
|
1918
|
+
log(`${name}: already adopted (win32 copy-sync); run \`nomad pull\` to refresh the local copy`);
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
if (lexists(sharedTarget)) {
|
|
1922
|
+
fail(`${name}: shared/${name} already exists; would clobber. Remove it first.`);
|
|
1923
|
+
process.exit(1);
|
|
1924
|
+
}
|
|
1925
|
+
if (dryRun) {
|
|
1926
|
+
const ts = freshBackupTs(backup);
|
|
1927
|
+
log(`would backup: ${linkPath} -> backup/${ts}/${name}`);
|
|
1928
|
+
log(`would move: ${linkPath} -> shared/${name}`);
|
|
1929
|
+
log(
|
|
1930
|
+
process.platform === "win32" ? `would copy back: shared/${name} -> ${linkPath} (win32 copy-sync)` : `would relink: ${linkPath} -> shared/${name}`
|
|
1931
|
+
);
|
|
1932
|
+
log(`would stage: shared/${name}`);
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
try {
|
|
1936
|
+
performAdoptMove(name, linkPath, sharedTarget, repo, backup);
|
|
1937
|
+
} catch (err) {
|
|
1938
|
+
if (!(err instanceof NomadFatal)) throw err;
|
|
1939
|
+
fail(err.message);
|
|
1940
|
+
process.exitCode = 1;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// src/commands.allow.ts
|
|
1945
|
+
init_config();
|
|
1946
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
1947
|
+
|
|
1948
|
+
// src/commands.redact.core.ts
|
|
1949
|
+
import { appendFileSync, readFileSync as readFileSync3 } from "node:fs";
|
|
1950
|
+
import { join as join7 } from "node:path";
|
|
1951
|
+
function collectMatchIntervals(content, findings) {
|
|
1952
|
+
const intervals = [];
|
|
1953
|
+
for (const f of findings) {
|
|
1954
|
+
const match = f.Match;
|
|
1955
|
+
if (match === "") continue;
|
|
1956
|
+
let from = 0;
|
|
1957
|
+
let pos = content.indexOf(match, from);
|
|
1958
|
+
while (pos !== -1) {
|
|
1959
|
+
intervals.push({ start: pos, end: pos + match.length, ruleId: f.RuleID });
|
|
1960
|
+
from = pos + match.length;
|
|
1961
|
+
pos = content.indexOf(match, from);
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
return intervals;
|
|
1965
|
+
}
|
|
1966
|
+
function mergeIntervals(intervals) {
|
|
1967
|
+
if (intervals.length === 0) return [];
|
|
1968
|
+
const sorted = [...intervals].sort((a, b) => a.start - b.start || b.end - a.end);
|
|
1969
|
+
let last = { ...sorted[0] };
|
|
1970
|
+
const merged = [last];
|
|
1971
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
1972
|
+
const cur = sorted[i];
|
|
1973
|
+
if (cur.start <= last.end) {
|
|
1974
|
+
if (cur.end > last.end) last.end = cur.end;
|
|
1975
|
+
} else {
|
|
1976
|
+
last = { ...cur };
|
|
1977
|
+
merged.push(last);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return merged;
|
|
1981
|
+
}
|
|
1982
|
+
function applyRedactions(content, findings) {
|
|
1983
|
+
const raw = collectMatchIntervals(content, findings);
|
|
1984
|
+
if (raw.length === 0) return content;
|
|
1985
|
+
const merged = mergeIntervals(raw);
|
|
1986
|
+
let result = content;
|
|
1987
|
+
for (let i = merged.length - 1; i >= 0; i--) {
|
|
1988
|
+
const { start, end, ruleId } = merged[i];
|
|
1989
|
+
result = result.slice(0, start) + `[REDACTED:${ruleId}]` + result.slice(end);
|
|
1990
|
+
}
|
|
1991
|
+
return result;
|
|
1992
|
+
}
|
|
1993
|
+
function formatFingerprint(fingerprint) {
|
|
1994
|
+
return fingerprint.replace(/[\r\n]/g, "") + "\n";
|
|
1671
1995
|
}
|
|
1672
|
-
function
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1996
|
+
function isRecentlyModified(mtimeMs, nowMs, thresholdMs = 5 * 60 * 1e3) {
|
|
1997
|
+
return nowMs - mtimeMs < thresholdMs;
|
|
1998
|
+
}
|
|
1999
|
+
function isValidFingerprint(fingerprint) {
|
|
2000
|
+
return fingerprint.trim().length > 0 && fingerprint.length <= 512 && /^[^\r\n]+$/.test(fingerprint);
|
|
2001
|
+
}
|
|
2002
|
+
function isAlreadyPresent(line, lines) {
|
|
2003
|
+
return lines.includes(line);
|
|
2004
|
+
}
|
|
2005
|
+
function appendGitleaksIgnore(fingerprint, repo) {
|
|
2006
|
+
const sanitized = fingerprint.replace(/[\r\n]/g, "").trim();
|
|
2007
|
+
if (sanitized.length === 0) return;
|
|
2008
|
+
const ignPath = join7(repo, ".gitleaksignore");
|
|
2009
|
+
let raw;
|
|
2010
|
+
try {
|
|
2011
|
+
raw = readFileSync3(ignPath, "utf8");
|
|
2012
|
+
} catch {
|
|
2013
|
+
raw = "";
|
|
1688
2014
|
}
|
|
2015
|
+
const existing = raw.split("\n").filter((l) => l.length > 0);
|
|
2016
|
+
if (isAlreadyPresent(sanitized, existing)) return;
|
|
2017
|
+
const needsLeadingNewline = raw.length > 0 && !raw.endsWith("\n");
|
|
2018
|
+
const prefix = needsLeadingNewline ? "\n" : "";
|
|
2019
|
+
appendFileSync(ignPath, prefix + formatFingerprint(sanitized), "utf8");
|
|
1689
2020
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
2021
|
+
|
|
2022
|
+
// src/commands.allow.ts
|
|
2023
|
+
init_utils();
|
|
2024
|
+
function cmdAllow(fingerprints) {
|
|
1693
2025
|
const repo = repoHome();
|
|
1694
|
-
|
|
1695
|
-
const
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
const base = readJson(basePath);
|
|
1701
|
-
const hasOverrides = existsSync4(hostPath);
|
|
1702
|
-
const overrides = hasOverrides ? readJson(hostPath) : {};
|
|
1703
|
-
const merged = deepMerge(base, overrides);
|
|
1704
|
-
const settingsPath = join5(claude, "settings.json");
|
|
1705
|
-
if (!suppressDriftWarn && existsSync4(settingsPath)) {
|
|
1706
|
-
try {
|
|
1707
|
-
const existing = readJson(settingsPath);
|
|
1708
|
-
const drift = classifySettingsDrift(merged, existing);
|
|
1709
|
-
if (drift.behind.length > 0) {
|
|
1710
|
-
const { phrase, pronoun } = describeSettings(drift.behind);
|
|
1711
|
-
warn(
|
|
1712
|
-
`your settings.json is missing ${phrase} that the synced copy has; run 'nomad pull' to restore ${pronoun}.`
|
|
1713
|
-
);
|
|
1714
|
-
}
|
|
1715
|
-
const { promotable } = partitionByCaptureExclusion(drift.ahead);
|
|
1716
|
-
if (promotable.length > 0) {
|
|
1717
|
-
const { phrase, pronoun, verb } = describeSettings(promotable);
|
|
1718
|
-
warn(
|
|
1719
|
-
`your settings.json has ${phrase} that ${verb} not yet synced; run 'nomad capture-settings' to save ${pronoun} to the repo before the next pull overwrites ${pronoun}.`
|
|
1720
|
-
);
|
|
1721
|
-
}
|
|
1722
|
-
} catch {
|
|
1723
|
-
warn("existing settings.json is malformed; skipping drift-check and regenerating.");
|
|
2026
|
+
if (!existsSync6(repo)) die(`repo not cloned at ${repo}`);
|
|
2027
|
+
for (const fp of fingerprints) {
|
|
2028
|
+
if (!isValidFingerprint(fp)) {
|
|
2029
|
+
const shown = fp.replaceAll("\r", String.raw`\r`).replaceAll("\n", String.raw`\n`);
|
|
2030
|
+
fail(`invalid fingerprint: ${shown}`);
|
|
2031
|
+
process.exit(1);
|
|
1724
2032
|
}
|
|
1725
2033
|
}
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
return { label: overrideLabel };
|
|
2034
|
+
for (const fp of fingerprints) {
|
|
2035
|
+
appendGitleaksIgnore(fp, repo);
|
|
2036
|
+
item(`allowed: ${fp}`);
|
|
1730
2037
|
}
|
|
1731
|
-
|
|
1732
|
-
writeJsonAtomic(settingsPath, stripGsdHookEntries(merged));
|
|
1733
|
-
return { label: overrideLabel };
|
|
2038
|
+
log(`allowed ${fingerprints.length} fingerprint(s)`);
|
|
1734
2039
|
}
|
|
1735
2040
|
|
|
1736
2041
|
// src/commands.capture-settings.ts
|
|
2042
|
+
init_config();
|
|
2043
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
2044
|
+
import { join as join9 } from "node:path";
|
|
2045
|
+
import { createInterface } from "node:readline/promises";
|
|
1737
2046
|
init_utils_fs();
|
|
1738
2047
|
init_utils_json();
|
|
1739
2048
|
|
|
1740
2049
|
// src/utils.lockfile.ts
|
|
1741
2050
|
init_config();
|
|
1742
2051
|
init_utils();
|
|
1743
|
-
import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as
|
|
1744
|
-
import { dirname as dirname2, join as
|
|
2052
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2053
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
1745
2054
|
function lockFilePath() {
|
|
1746
|
-
return
|
|
2055
|
+
return join8(home(), ".cache", "claude-nomad", "nomad.lock");
|
|
1747
2056
|
}
|
|
1748
2057
|
function acquireLock(verb) {
|
|
1749
2058
|
const lp = lockFilePath();
|
|
@@ -1786,7 +2095,7 @@ function releaseLock(handle) {
|
|
|
1786
2095
|
function unlinkIfSamePid(expectedPidStr, lp) {
|
|
1787
2096
|
let current;
|
|
1788
2097
|
try {
|
|
1789
|
-
current =
|
|
2098
|
+
current = readFileSync4(lp, "utf8").trim();
|
|
1790
2099
|
} catch {
|
|
1791
2100
|
return false;
|
|
1792
2101
|
}
|
|
@@ -1801,7 +2110,7 @@ function unlinkIfSamePid(expectedPidStr, lp) {
|
|
|
1801
2110
|
function checkStaleAndRetry(verb, lp) {
|
|
1802
2111
|
let pidStr;
|
|
1803
2112
|
try {
|
|
1804
|
-
pidStr =
|
|
2113
|
+
pidStr = readFileSync4(lp, "utf8").trim();
|
|
1805
2114
|
} catch {
|
|
1806
2115
|
pidStr = "";
|
|
1807
2116
|
}
|
|
@@ -1869,30 +2178,30 @@ async function confirmCapture(destLabel, keys) {
|
|
|
1869
2178
|
}
|
|
1870
2179
|
}
|
|
1871
2180
|
function resolveCaptureDestination(repo, useHost) {
|
|
1872
|
-
const destPath = useHost ?
|
|
1873
|
-
const existing =
|
|
2181
|
+
const destPath = useHost ? join9(repo, "hosts", `${HOST}.json`) : join9(repo, "shared", "settings.base.json");
|
|
2182
|
+
const existing = existsSync7(destPath) ? readJson(destPath) : {};
|
|
1874
2183
|
return { destPath, existing };
|
|
1875
2184
|
}
|
|
1876
2185
|
async function cmdCaptureSettings(opts) {
|
|
1877
2186
|
const { host: useHost, dryRun } = opts;
|
|
1878
2187
|
const repo = repoHome();
|
|
1879
|
-
if (!
|
|
2188
|
+
if (!existsSync7(repo)) die(`repo not cloned at ${repo}`);
|
|
1880
2189
|
const handle = acquireLock("capture-settings");
|
|
1881
2190
|
if (handle === null) process.exit(0);
|
|
1882
2191
|
try {
|
|
1883
2192
|
const claude = claudeHome();
|
|
1884
|
-
const basePath =
|
|
1885
|
-
if (!
|
|
2193
|
+
const basePath = join9(repo, "shared", "settings.base.json");
|
|
2194
|
+
if (!existsSync7(basePath)) {
|
|
1886
2195
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
1887
2196
|
}
|
|
1888
|
-
const settingsPath =
|
|
1889
|
-
if (!
|
|
2197
|
+
const settingsPath = join9(claude, "settings.json");
|
|
2198
|
+
if (!existsSync7(settingsPath)) {
|
|
1890
2199
|
log("no ~/.claude/settings.json found; nothing to capture");
|
|
1891
2200
|
return;
|
|
1892
2201
|
}
|
|
1893
2202
|
const base = readJson(basePath);
|
|
1894
|
-
const hostPath =
|
|
1895
|
-
const overrides =
|
|
2203
|
+
const hostPath = join9(repo, "hosts", `${HOST}.json`);
|
|
2204
|
+
const overrides = existsSync7(hostPath) ? readJson(hostPath) : {};
|
|
1896
2205
|
const merged = deepMerge(base, overrides);
|
|
1897
2206
|
const settings = readJson(settingsPath);
|
|
1898
2207
|
const subset = buildCaptureSubset(merged, settings, { normalizeNodePath: !useHost });
|
|
@@ -1929,8 +2238,8 @@ async function cmdCaptureSettings(opts) {
|
|
|
1929
2238
|
// src/commands.clean.ts
|
|
1930
2239
|
init_config();
|
|
1931
2240
|
init_utils();
|
|
1932
|
-
import { existsSync as
|
|
1933
|
-
import { join as
|
|
2241
|
+
import { existsSync as existsSync8, lstatSync as lstatSync6, readdirSync as readdirSync3, rmSync as rmSync5, statSync as statSync2 } from "node:fs";
|
|
2242
|
+
import { join as join10 } from "node:path";
|
|
1934
2243
|
var TS_SHAPE = /^\d{8}-\d{6}(-\d+)?$/;
|
|
1935
2244
|
var DURATION_RE = /^(\d+)([dhm])$/;
|
|
1936
2245
|
var UNIT_MS = { d: 864e5, h: 36e5, m: 6e4 };
|
|
@@ -1944,8 +2253,8 @@ function parseDuration(s) {
|
|
|
1944
2253
|
return Number(m[1]) * UNIT_MS[m[2]];
|
|
1945
2254
|
}
|
|
1946
2255
|
function listBackupDirs(backupBase2) {
|
|
1947
|
-
if (!
|
|
1948
|
-
return
|
|
2256
|
+
if (!existsSync8(backupBase2)) return [];
|
|
2257
|
+
return readdirSync3(backupBase2).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join10(backupBase2, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1949
2258
|
}
|
|
1950
2259
|
function prunableByAge(dirs, olderThanMs, nowMs) {
|
|
1951
2260
|
return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
|
|
@@ -1955,10 +2264,10 @@ function prunableByCount(dirs, keep) {
|
|
|
1955
2264
|
}
|
|
1956
2265
|
function safeDelete(backupBase2, name) {
|
|
1957
2266
|
if (!isTsDir(name)) return;
|
|
1958
|
-
const full =
|
|
1959
|
-
const st =
|
|
2267
|
+
const full = join10(backupBase2, name);
|
|
2268
|
+
const st = lstatSync6(full, { throwIfNoEntry: false });
|
|
1960
2269
|
if (!st || st.isSymbolicLink() || !st.isDirectory()) return;
|
|
1961
|
-
|
|
2270
|
+
rmSync5(full, { recursive: true, force: true });
|
|
1962
2271
|
}
|
|
1963
2272
|
function resolveTargets(dirs, olderThanMs, keep) {
|
|
1964
2273
|
if (keep !== void 0) return prunableByCount(dirs, keep);
|
|
@@ -1993,9 +2302,10 @@ function cmdClean(opts, backupBase2 = backupBase()) {
|
|
|
1993
2302
|
// src/commands.eject.ts
|
|
1994
2303
|
init_config();
|
|
1995
2304
|
init_utils();
|
|
2305
|
+
init_utils_fs();
|
|
1996
2306
|
init_utils_json();
|
|
1997
|
-
import { cpSync as
|
|
1998
|
-
import { join as
|
|
2307
|
+
import { cpSync as cpSync5, existsSync as existsSync9, lstatSync as lstatSync7, realpathSync, rmSync as rmSync6 } from "node:fs";
|
|
2308
|
+
import { join as join11, sep as sep3 } from "node:path";
|
|
1999
2309
|
function ejectChecklist() {
|
|
2000
2310
|
return [
|
|
2001
2311
|
"Manual steps remaining to finish leaving claude-nomad on this host:",
|
|
@@ -2011,33 +2321,33 @@ function errMessage(err) {
|
|
|
2011
2321
|
}
|
|
2012
2322
|
function lexists2(p) {
|
|
2013
2323
|
try {
|
|
2014
|
-
|
|
2324
|
+
lstatSync7(p);
|
|
2015
2325
|
return true;
|
|
2016
2326
|
} catch {
|
|
2017
2327
|
return false;
|
|
2018
2328
|
}
|
|
2019
2329
|
}
|
|
2020
2330
|
function readMapIfPresent2(repoHome2) {
|
|
2021
|
-
const mapPath =
|
|
2022
|
-
return
|
|
2331
|
+
const mapPath = join11(repoHome2, "path-map.json");
|
|
2332
|
+
return existsSync9(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
2023
2333
|
}
|
|
2024
2334
|
function classifyName(linkPath) {
|
|
2025
2335
|
if (!lexists2(linkPath)) return "absent";
|
|
2026
|
-
if (!
|
|
2027
|
-
if (!
|
|
2336
|
+
if (!lstatSync7(linkPath).isSymbolicLink()) return "skip-real";
|
|
2337
|
+
if (!existsSync9(linkPath)) return "dangling";
|
|
2028
2338
|
return "materialize";
|
|
2029
2339
|
}
|
|
2030
2340
|
function resolveSharedRoot(repoHome2) {
|
|
2031
2341
|
try {
|
|
2032
|
-
return realpathSync(
|
|
2342
|
+
return realpathSync(join11(repoHome2, "shared"));
|
|
2033
2343
|
} catch {
|
|
2034
2344
|
return die(
|
|
2035
|
-
`cannot resolve ${
|
|
2345
|
+
`cannot resolve ${join11(repoHome2, "shared")} (repo checkout incomplete). run \`nomad pull\` first, then re-run \`nomad eject\``
|
|
2036
2346
|
);
|
|
2037
2347
|
}
|
|
2038
2348
|
}
|
|
2039
2349
|
function isManagedTarget(target, sharedRoot) {
|
|
2040
|
-
return target.startsWith(sharedRoot +
|
|
2350
|
+
return target.startsWith(sharedRoot + sep3);
|
|
2041
2351
|
}
|
|
2042
2352
|
function materializeOne(name, linkPath, sharedRoot) {
|
|
2043
2353
|
const target = realpathSync(linkPath);
|
|
@@ -2047,33 +2357,36 @@ function materializeOne(name, linkPath, sharedRoot) {
|
|
|
2047
2357
|
}
|
|
2048
2358
|
const tmp = `${linkPath}.eject.tmp.${process.pid}.${Date.now()}`;
|
|
2049
2359
|
try {
|
|
2050
|
-
|
|
2051
|
-
|
|
2360
|
+
rmSync6(tmp, { recursive: true, force: true });
|
|
2361
|
+
cpSync5(target, tmp, {
|
|
2052
2362
|
recursive: true,
|
|
2053
2363
|
force: true,
|
|
2054
2364
|
dereference: true,
|
|
2055
2365
|
preserveTimestamps: true
|
|
2056
2366
|
});
|
|
2057
|
-
|
|
2058
|
-
|
|
2367
|
+
rmSync6(linkPath, { force: true });
|
|
2368
|
+
renameAtomicRetry(tmp, linkPath);
|
|
2059
2369
|
item(`ejected: ${name}`);
|
|
2060
2370
|
return true;
|
|
2061
2371
|
} catch (err) {
|
|
2062
2372
|
try {
|
|
2063
|
-
|
|
2373
|
+
rmSync6(tmp, { recursive: true, force: true });
|
|
2064
2374
|
} catch {
|
|
2065
2375
|
}
|
|
2066
2376
|
throw err;
|
|
2067
2377
|
}
|
|
2068
2378
|
}
|
|
2379
|
+
function skipRealMessage(name) {
|
|
2380
|
+
return process.platform === "win32" ? `already a real copy (win32 copy-sync): ${name}` : `skipped (not a symlink): ${name}`;
|
|
2381
|
+
}
|
|
2069
2382
|
function previewDryRun(names, classifications, claudeHome2, sharedRoot) {
|
|
2070
2383
|
for (const name of names) {
|
|
2071
2384
|
const cls = classifications.get(name);
|
|
2072
|
-
const linkPath =
|
|
2385
|
+
const linkPath = join11(claudeHome2, name);
|
|
2073
2386
|
if (cls === "absent") {
|
|
2074
2387
|
item(`skipped (absent): ${name}`);
|
|
2075
2388
|
} else if (cls === "skip-real") {
|
|
2076
|
-
item(
|
|
2389
|
+
item(skipRealMessage(name));
|
|
2077
2390
|
} else {
|
|
2078
2391
|
previewMaterialize(name, linkPath, sharedRoot);
|
|
2079
2392
|
}
|
|
@@ -2099,12 +2412,12 @@ function runLiveEject(names, classifications, claudeHome2, sharedRoot) {
|
|
|
2099
2412
|
let skipped = 0;
|
|
2100
2413
|
for (const name of names) {
|
|
2101
2414
|
const cls = classifications.get(name);
|
|
2102
|
-
const linkPath =
|
|
2415
|
+
const linkPath = join11(claudeHome2, name);
|
|
2103
2416
|
if (cls === "absent") {
|
|
2104
2417
|
item(`skipped (absent): ${name}`);
|
|
2105
2418
|
skipped++;
|
|
2106
2419
|
} else if (cls === "skip-real") {
|
|
2107
|
-
item(
|
|
2420
|
+
item(skipRealMessage(name));
|
|
2108
2421
|
skipped++;
|
|
2109
2422
|
} else if (materializeOneOrDie(name, linkPath, sharedRoot, done)) {
|
|
2110
2423
|
done.push(name);
|
|
@@ -2135,7 +2448,7 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
|
|
|
2135
2448
|
const names = allSharedLinks(map);
|
|
2136
2449
|
const classifications = /* @__PURE__ */ new Map();
|
|
2137
2450
|
for (const name of names) {
|
|
2138
|
-
classifications.set(name, classifyName(
|
|
2451
|
+
classifications.set(name, classifyName(join11(claudeHome2, name)));
|
|
2139
2452
|
}
|
|
2140
2453
|
const dangling = names.filter((n) => classifications.get(n) === "dangling");
|
|
2141
2454
|
if (dangling.length > 0) {
|
|
@@ -2158,14 +2471,14 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
|
|
|
2158
2471
|
}
|
|
2159
2472
|
|
|
2160
2473
|
// src/commands.doctor.ts
|
|
2161
|
-
import { existsSync as
|
|
2162
|
-
import { join as
|
|
2474
|
+
import { existsSync as existsSync34 } from "node:fs";
|
|
2475
|
+
import { join as join40 } from "node:path";
|
|
2163
2476
|
|
|
2164
2477
|
// src/commands.doctor.checks.repo.ts
|
|
2165
2478
|
init_color();
|
|
2166
2479
|
init_config();
|
|
2167
|
-
import { existsSync as
|
|
2168
|
-
import { join as
|
|
2480
|
+
import { existsSync as existsSync11, lstatSync as lstatSync8, readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
|
|
2481
|
+
import { join as join13 } from "node:path";
|
|
2169
2482
|
|
|
2170
2483
|
// src/commands.doctor.format.ts
|
|
2171
2484
|
init_color();
|
|
@@ -2243,15 +2556,15 @@ function readJsonSafe(path, label, section2) {
|
|
|
2243
2556
|
// src/init.classify.ts
|
|
2244
2557
|
init_config();
|
|
2245
2558
|
init_utils_json();
|
|
2246
|
-
import { existsSync as
|
|
2247
|
-
import { join as
|
|
2559
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
2560
|
+
import { join as join12 } from "node:path";
|
|
2248
2561
|
function classifyRepoState(repoHome2, host) {
|
|
2249
|
-
const basePath =
|
|
2250
|
-
const mapPath =
|
|
2251
|
-
const hostPath =
|
|
2252
|
-
const hasBase =
|
|
2253
|
-
const hasMap =
|
|
2254
|
-
const hasHost =
|
|
2562
|
+
const basePath = join12(repoHome2, "shared", "settings.base.json");
|
|
2563
|
+
const mapPath = join12(repoHome2, "path-map.json");
|
|
2564
|
+
const hostPath = join12(repoHome2, "hosts", `${host}.json`);
|
|
2565
|
+
const hasBase = existsSync10(basePath);
|
|
2566
|
+
const hasMap = existsSync10(mapPath);
|
|
2567
|
+
const hasHost = existsSync10(hostPath);
|
|
2255
2568
|
let mapEntryCount = 0;
|
|
2256
2569
|
if (hasMap) {
|
|
2257
2570
|
try {
|
|
@@ -2266,11 +2579,11 @@ function classifyRepoState(repoHome2, host) {
|
|
|
2266
2579
|
return "partial";
|
|
2267
2580
|
}
|
|
2268
2581
|
function reasonForPartial(repoHome2, host) {
|
|
2269
|
-
const basePath =
|
|
2270
|
-
const mapPath =
|
|
2271
|
-
const hostPath =
|
|
2272
|
-
if (!
|
|
2273
|
-
if (!
|
|
2582
|
+
const basePath = join12(repoHome2, "shared", "settings.base.json");
|
|
2583
|
+
const mapPath = join12(repoHome2, "path-map.json");
|
|
2584
|
+
const hostPath = join12(repoHome2, "hosts", `${host}.json`);
|
|
2585
|
+
if (!existsSync10(basePath)) return "- shared/settings.base.json missing";
|
|
2586
|
+
if (!existsSync10(mapPath)) return "- path-map.json missing";
|
|
2274
2587
|
let mapEntryCount;
|
|
2275
2588
|
try {
|
|
2276
2589
|
const map = readJson(mapPath);
|
|
@@ -2279,7 +2592,7 @@ function reasonForPartial(repoHome2, host) {
|
|
|
2279
2592
|
mapEntryCount = 0;
|
|
2280
2593
|
}
|
|
2281
2594
|
if (mapEntryCount === 0) return "- path-map.json.projects has no entries";
|
|
2282
|
-
if (!
|
|
2595
|
+
if (!existsSync10(hostPath)) return `- hosts/${host}.json missing`;
|
|
2283
2596
|
return "- partial state (unknown gap)";
|
|
2284
2597
|
}
|
|
2285
2598
|
|
|
@@ -2296,15 +2609,15 @@ function reportHostAndPaths(section2) {
|
|
|
2296
2609
|
if (isOverrideActive()) {
|
|
2297
2610
|
addItem(section2, `${dim(infoGlyph)} NOMAD_REPO: ${blue(repo)}`);
|
|
2298
2611
|
}
|
|
2299
|
-
addItem(section2, `${
|
|
2612
|
+
addItem(section2, `${existsSync11(repo) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(repo)}`);
|
|
2300
2613
|
addItem(
|
|
2301
2614
|
section2,
|
|
2302
|
-
`${
|
|
2615
|
+
`${existsSync11(claude) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(claude)}`
|
|
2303
2616
|
);
|
|
2304
2617
|
}
|
|
2305
2618
|
function pathMapHostKeys() {
|
|
2306
|
-
const mapPath =
|
|
2307
|
-
if (!
|
|
2619
|
+
const mapPath = join13(repoHome(), "path-map.json");
|
|
2620
|
+
if (!existsSync11(mapPath)) return /* @__PURE__ */ new Set();
|
|
2308
2621
|
let raw;
|
|
2309
2622
|
try {
|
|
2310
2623
|
raw = readJson(mapPath);
|
|
@@ -2321,7 +2634,7 @@ function pathMapHostKeys() {
|
|
|
2321
2634
|
function hostOverrideLabels() {
|
|
2322
2635
|
let entries;
|
|
2323
2636
|
try {
|
|
2324
|
-
entries =
|
|
2637
|
+
entries = readdirSync4(join13(repoHome(), "hosts"));
|
|
2325
2638
|
} catch {
|
|
2326
2639
|
return /* @__PURE__ */ new Set();
|
|
2327
2640
|
}
|
|
@@ -2362,12 +2675,12 @@ function reportRepoState(section2) {
|
|
|
2362
2675
|
}
|
|
2363
2676
|
}
|
|
2364
2677
|
function repoHasSharedSource(name) {
|
|
2365
|
-
return
|
|
2678
|
+
return existsSync11(join13(repoHome(), "shared", name));
|
|
2366
2679
|
}
|
|
2367
2680
|
function classifySharedLink(name, p) {
|
|
2368
2681
|
let stat;
|
|
2369
2682
|
try {
|
|
2370
|
-
stat =
|
|
2683
|
+
stat = lstatSync8(p);
|
|
2371
2684
|
} catch (err) {
|
|
2372
2685
|
const code = err.code;
|
|
2373
2686
|
if (code === "ENOENT") {
|
|
@@ -2379,6 +2692,12 @@ function classifySharedLink(name, p) {
|
|
|
2379
2692
|
return { line: `${red(failGlyph)} ${name}: could not stat (${String(code)})`, fail: true };
|
|
2380
2693
|
}
|
|
2381
2694
|
if (!stat.isSymbolicLink()) {
|
|
2695
|
+
if (process.platform === "win32") {
|
|
2696
|
+
return {
|
|
2697
|
+
line: `${green(okGlyph)} ${name}: real copy (win32 copy-sync)`,
|
|
2698
|
+
fail: false
|
|
2699
|
+
};
|
|
2700
|
+
}
|
|
2382
2701
|
return {
|
|
2383
2702
|
line: `${red(failGlyph)} ${name}: NOT a symlink (blocks sync); run \`nomad adopt ${name}\` to fix`,
|
|
2384
2703
|
fail: true
|
|
@@ -2410,7 +2729,7 @@ function classifySymlinkTarget(name, p) {
|
|
|
2410
2729
|
function reportSharedLinks(section2, map) {
|
|
2411
2730
|
const claude = claudeHome();
|
|
2412
2731
|
for (const name of allSharedLinks(map)) {
|
|
2413
|
-
const p =
|
|
2732
|
+
const p = join13(claude, name);
|
|
2414
2733
|
const { line, fail: fail2 } = classifySharedLink(name, p);
|
|
2415
2734
|
addItem(section2, line);
|
|
2416
2735
|
if (fail2) process.exitCode = 1;
|
|
@@ -2419,10 +2738,10 @@ function reportSharedLinks(section2, map) {
|
|
|
2419
2738
|
function reportDroppedNamesMigration(section2) {
|
|
2420
2739
|
const claude = claudeHome();
|
|
2421
2740
|
for (const name of GSD_DROPPED_NAMES) {
|
|
2422
|
-
const p =
|
|
2741
|
+
const p = join13(claude, name);
|
|
2423
2742
|
let stat;
|
|
2424
2743
|
try {
|
|
2425
|
-
stat =
|
|
2744
|
+
stat = lstatSync8(p);
|
|
2426
2745
|
} catch {
|
|
2427
2746
|
continue;
|
|
2428
2747
|
}
|
|
@@ -2437,11 +2756,11 @@ function reportDroppedNamesMigration(section2) {
|
|
|
2437
2756
|
// src/commands.doctor.checks.settings.ts
|
|
2438
2757
|
init_color();
|
|
2439
2758
|
init_config();
|
|
2440
|
-
import { existsSync as
|
|
2441
|
-
import { join as
|
|
2759
|
+
import { existsSync as existsSync12, readdirSync as readdirSync5 } from "node:fs";
|
|
2760
|
+
import { join as join14 } from "node:path";
|
|
2442
2761
|
function loadBaseSettings(section2) {
|
|
2443
|
-
const basePath =
|
|
2444
|
-
if (!
|
|
2762
|
+
const basePath = join14(repoHome(), "shared", "settings.base.json");
|
|
2763
|
+
if (!existsSync12(basePath)) {
|
|
2445
2764
|
addItem(section2, `${red(failGlyph)} shared/settings.base.json missing at ${blue(basePath)}`);
|
|
2446
2765
|
process.exitCode = 1;
|
|
2447
2766
|
return null;
|
|
@@ -2449,8 +2768,8 @@ function loadBaseSettings(section2) {
|
|
|
2449
2768
|
return readJsonSafe(basePath, basePath, section2);
|
|
2450
2769
|
}
|
|
2451
2770
|
function loadAndReportSettings(section2) {
|
|
2452
|
-
const settingsPath =
|
|
2453
|
-
if (!
|
|
2771
|
+
const settingsPath = join14(claudeHome(), "settings.json");
|
|
2772
|
+
if (!existsSync12(settingsPath)) return null;
|
|
2454
2773
|
const settings = readJsonSafe(settingsPath, settingsPath, section2);
|
|
2455
2774
|
if (settings === null) return null;
|
|
2456
2775
|
const unknownKeys = Object.keys(settings).filter((k) => !KNOWN_SETTINGS_KEYS.has(k));
|
|
@@ -2466,13 +2785,13 @@ function loadAndReportSettings(section2) {
|
|
|
2466
2785
|
}
|
|
2467
2786
|
function reportHostOverrides(section2, base, settings) {
|
|
2468
2787
|
const repo = repoHome();
|
|
2469
|
-
const hostFile =
|
|
2788
|
+
const hostFile = join14(repo, "hosts", `${HOST}.json`);
|
|
2470
2789
|
let drift = [];
|
|
2471
2790
|
if (base !== null && settings !== null) {
|
|
2472
2791
|
const baseKeys = new Set(Object.keys(base));
|
|
2473
2792
|
drift = Object.keys(settings).filter((k) => !baseKeys.has(k));
|
|
2474
2793
|
}
|
|
2475
|
-
if (
|
|
2794
|
+
if (existsSync12(hostFile)) {
|
|
2476
2795
|
if (readJsonSafe(hostFile, hostFile, section2) !== null) {
|
|
2477
2796
|
addItem(section2, `${green(okGlyph)} host overrides: ${blue(hostFile)}`);
|
|
2478
2797
|
}
|
|
@@ -2481,9 +2800,9 @@ function reportHostOverrides(section2, base, settings) {
|
|
|
2481
2800
|
section2,
|
|
2482
2801
|
`${red(failGlyph)} no hosts/${HOST}.json AND settings.json has unbased keys ${JSON.stringify(drift)}`
|
|
2483
2802
|
);
|
|
2484
|
-
const hostsDir =
|
|
2485
|
-
if (
|
|
2486
|
-
const cands =
|
|
2803
|
+
const hostsDir = join14(repo, "hosts");
|
|
2804
|
+
if (existsSync12(hostsDir)) {
|
|
2805
|
+
const cands = readdirSync5(hostsDir).filter((f) => f.endsWith(".json"));
|
|
2487
2806
|
if (cands.length > 0) addItem(section2, `${dim(infoGlyph)} candidates: ${cands.join(", ")}`);
|
|
2488
2807
|
}
|
|
2489
2808
|
process.exitCode = 1;
|
|
@@ -2498,8 +2817,8 @@ function reportHostOverrides(section2, base, settings) {
|
|
|
2498
2817
|
// src/commands.doctor.checks.pathmap.ts
|
|
2499
2818
|
init_color();
|
|
2500
2819
|
init_config();
|
|
2501
|
-
import { existsSync as
|
|
2502
|
-
import { join as
|
|
2820
|
+
import { existsSync as existsSync13, readdirSync as readdirSync6 } from "node:fs";
|
|
2821
|
+
import { join as join15 } from "node:path";
|
|
2503
2822
|
init_utils_json();
|
|
2504
2823
|
function reportMappedProjects(section2, map) {
|
|
2505
2824
|
const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
|
|
@@ -2509,11 +2828,11 @@ function reportMappedProjects(section2, map) {
|
|
|
2509
2828
|
}
|
|
2510
2829
|
}
|
|
2511
2830
|
function reportUnmappedProjects(section2, map) {
|
|
2512
|
-
const localProjects =
|
|
2513
|
-
if (!
|
|
2831
|
+
const localProjects = join15(claudeHome(), "projects");
|
|
2832
|
+
if (!existsSync13(localProjects)) return;
|
|
2514
2833
|
let localDirs;
|
|
2515
2834
|
try {
|
|
2516
|
-
localDirs =
|
|
2835
|
+
localDirs = readdirSync6(localProjects);
|
|
2517
2836
|
} catch {
|
|
2518
2837
|
return;
|
|
2519
2838
|
}
|
|
@@ -2531,7 +2850,7 @@ function reportCurrentHostPathsMissing(section2, map) {
|
|
|
2531
2850
|
for (const [name, hosts] of Object.entries(map.projects)) {
|
|
2532
2851
|
const abspath = hosts[HOST];
|
|
2533
2852
|
if (!abspath || abspath === "TBD") continue;
|
|
2534
|
-
if (!
|
|
2853
|
+
if (!existsSync13(abspath)) {
|
|
2535
2854
|
addItem(
|
|
2536
2855
|
section2,
|
|
2537
2856
|
`${yellow(warnGlyph)} path-map: ${name} local path missing on ${HOST}: ${blue(abspath)}`
|
|
@@ -2562,8 +2881,8 @@ function reportPathCollisions(section2, map) {
|
|
|
2562
2881
|
else addItem(section2, `${green(okGlyph)} path-encoding: no collisions`);
|
|
2563
2882
|
}
|
|
2564
2883
|
function reportPathMap(section2) {
|
|
2565
|
-
const mapPath =
|
|
2566
|
-
if (!
|
|
2884
|
+
const mapPath = join15(repoHome(), "path-map.json");
|
|
2885
|
+
if (!existsSync13(mapPath)) {
|
|
2567
2886
|
addItem(section2, `${red(failGlyph)} path-map.json missing at ${blue(mapPath)}`);
|
|
2568
2887
|
process.exitCode = 1;
|
|
2569
2888
|
return;
|
|
@@ -2592,21 +2911,19 @@ function reportNeverSync(section2) {
|
|
|
2592
2911
|
|
|
2593
2912
|
// src/commands.doctor.checks.skills.ts
|
|
2594
2913
|
init_color();
|
|
2595
|
-
import { existsSync as
|
|
2596
|
-
import { join as
|
|
2914
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
2915
|
+
import { join as join16 } from "node:path";
|
|
2597
2916
|
init_config();
|
|
2598
2917
|
|
|
2599
2918
|
// src/extras-sync.diff.ts
|
|
2600
2919
|
init_utils();
|
|
2601
2920
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2602
|
-
import { relative as
|
|
2921
|
+
import { relative as relative3 } from "node:path";
|
|
2603
2922
|
function parseNameStatus(stdout) {
|
|
2923
|
+
const fields = stdout.split("\0").filter((f) => f.length > 0);
|
|
2604
2924
|
const entries = [];
|
|
2605
|
-
for (
|
|
2606
|
-
|
|
2607
|
-
const tab = line.indexOf(" ");
|
|
2608
|
-
if (tab === -1) continue;
|
|
2609
|
-
entries.push({ status: line.slice(0, tab), path: line.slice(tab + 1) });
|
|
2925
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
2926
|
+
entries.push({ status: fields[i], path: fields[i + 1] });
|
|
2610
2927
|
}
|
|
2611
2928
|
return entries;
|
|
2612
2929
|
}
|
|
@@ -2619,13 +2936,13 @@ function parseDiffOutput(stdout) {
|
|
|
2619
2936
|
return parseNameStatus(stdout).map(labelEntry);
|
|
2620
2937
|
}
|
|
2621
2938
|
function parseModifiedPaths(stdout, a) {
|
|
2622
|
-
return parseNameStatus(stdout).filter((entry) => entry.status === "M").map((entry) =>
|
|
2939
|
+
return parseNameStatus(stdout).filter((entry) => entry.status === "M").map((entry) => relative3(a, entry.path));
|
|
2623
2940
|
}
|
|
2624
2941
|
function runNameStatusDiff(a, b, parse) {
|
|
2625
2942
|
try {
|
|
2626
2943
|
const stdout = execFileSync2(
|
|
2627
2944
|
"git",
|
|
2628
|
-
["diff", "--no-index", "--no-renames", "--name-status", a, b],
|
|
2945
|
+
["diff", "--no-index", "--no-renames", "-z", "--name-status", a, b],
|
|
2629
2946
|
{
|
|
2630
2947
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2631
2948
|
}
|
|
@@ -2670,13 +2987,13 @@ function isGsdDiffLine(line, localBase, sharedBase) {
|
|
|
2670
2987
|
return relative9.split("/")[0].startsWith(GSD_PREFIX);
|
|
2671
2988
|
}
|
|
2672
2989
|
function reportSkillsDivergence(section2) {
|
|
2673
|
-
const sharedSkills =
|
|
2674
|
-
const localSkills =
|
|
2675
|
-
if (!
|
|
2990
|
+
const sharedSkills = join16(repoHome(), "shared", "skills");
|
|
2991
|
+
const localSkills = join16(claudeHome(), "skills");
|
|
2992
|
+
if (!existsSync14(sharedSkills)) {
|
|
2676
2993
|
addItem(section2, `${dim(infoGlyph)} skills: no shared/skills/ to compare`);
|
|
2677
2994
|
return;
|
|
2678
2995
|
}
|
|
2679
|
-
if (!
|
|
2996
|
+
if (!existsSync14(localSkills)) {
|
|
2680
2997
|
addItem(section2, `${dim(infoGlyph)} skills: no local skills/ to compare`);
|
|
2681
2998
|
return;
|
|
2682
2999
|
}
|
|
@@ -2699,8 +3016,8 @@ function reportSkillsDivergence(section2) {
|
|
|
2699
3016
|
init_color();
|
|
2700
3017
|
init_config();
|
|
2701
3018
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2702
|
-
import { existsSync as
|
|
2703
|
-
import { join as
|
|
3019
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
3020
|
+
import { join as join20, relative as relative4 } from "node:path";
|
|
2704
3021
|
init_commands_pull_wedge();
|
|
2705
3022
|
init_push_checks();
|
|
2706
3023
|
init_utils();
|
|
@@ -2723,11 +3040,11 @@ function reportGitleaksProbe(section2) {
|
|
|
2723
3040
|
}
|
|
2724
3041
|
function reportGitlinks(section2) {
|
|
2725
3042
|
const repo = repoHome();
|
|
2726
|
-
const sharedDir =
|
|
2727
|
-
if (
|
|
3043
|
+
const sharedDir = join20(repo, "shared");
|
|
3044
|
+
if (existsSync17(sharedDir)) {
|
|
2728
3045
|
const gitlinks = findGitlinks(sharedDir);
|
|
2729
3046
|
for (const p of gitlinks) {
|
|
2730
|
-
const rel =
|
|
3047
|
+
const rel = relative4(repo, p).replaceAll("\\", "/");
|
|
2731
3048
|
addItem(
|
|
2732
3049
|
section2,
|
|
2733
3050
|
`${red(failGlyph)} gitlink: ${blue(rel)} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`
|
|
@@ -2822,13 +3139,13 @@ function reportGitIdentity(section2) {
|
|
|
2822
3139
|
|
|
2823
3140
|
// src/commands.doctor.checks.backups.ts
|
|
2824
3141
|
init_color();
|
|
2825
|
-
import { existsSync as
|
|
2826
|
-
import { join as
|
|
3142
|
+
import { existsSync as existsSync18, lstatSync as lstatSync9, readdirSync as readdirSync8 } from "node:fs";
|
|
3143
|
+
import { join as join21 } from "node:path";
|
|
2827
3144
|
init_config();
|
|
2828
3145
|
var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
|
|
2829
3146
|
function safeReaddir(dir) {
|
|
2830
3147
|
try {
|
|
2831
|
-
return
|
|
3148
|
+
return readdirSync8(dir);
|
|
2832
3149
|
} catch {
|
|
2833
3150
|
return [];
|
|
2834
3151
|
}
|
|
@@ -2839,8 +3156,8 @@ var BYTES_PER_MB = 1024 * 1024;
|
|
|
2839
3156
|
function dirSizeBytes(dir) {
|
|
2840
3157
|
let bytes = 0;
|
|
2841
3158
|
for (const entry of safeReaddir(dir)) {
|
|
2842
|
-
const full =
|
|
2843
|
-
const st =
|
|
3159
|
+
const full = join21(dir, entry);
|
|
3160
|
+
const st = lstatSync9(full, { throwIfNoEntry: false });
|
|
2844
3161
|
if (!st) continue;
|
|
2845
3162
|
if (st.isSymbolicLink()) continue;
|
|
2846
3163
|
if (st.isDirectory()) bytes += dirSizeBytes(full);
|
|
@@ -2850,11 +3167,11 @@ function dirSizeBytes(dir) {
|
|
|
2850
3167
|
}
|
|
2851
3168
|
function totalSizeMb(backupBase2, dirs) {
|
|
2852
3169
|
let bytes = 0;
|
|
2853
|
-
for (const name of dirs) bytes += dirSizeBytes(
|
|
3170
|
+
for (const name of dirs) bytes += dirSizeBytes(join21(backupBase2, name));
|
|
2854
3171
|
return bytes / BYTES_PER_MB;
|
|
2855
3172
|
}
|
|
2856
3173
|
function reportBackupsCheck(section2, backupBase2 = backupBase()) {
|
|
2857
|
-
if (!
|
|
3174
|
+
if (!existsSync18(backupBase2)) return;
|
|
2858
3175
|
const dirs = safeReaddir(backupBase2).filter((n) => TS_SHAPE2.test(n));
|
|
2859
3176
|
const count = dirs.length;
|
|
2860
3177
|
const sizeMb = totalSizeMb(backupBase2, dirs);
|
|
@@ -2935,8 +3252,8 @@ function reportCheckRemote(section2) {
|
|
|
2935
3252
|
|
|
2936
3253
|
// src/commands.doctor.check-schema.ts
|
|
2937
3254
|
init_color();
|
|
2938
|
-
import { existsSync as
|
|
2939
|
-
import { join as
|
|
3255
|
+
import { existsSync as existsSync19 } from "node:fs";
|
|
3256
|
+
import { join as join22 } from "node:path";
|
|
2940
3257
|
init_config();
|
|
2941
3258
|
|
|
2942
3259
|
// src/http-fetch.ts
|
|
@@ -2973,8 +3290,8 @@ function fetchSchemaKeys() {
|
|
|
2973
3290
|
}
|
|
2974
3291
|
}
|
|
2975
3292
|
function reportCheckSchema(section2) {
|
|
2976
|
-
const settingsPath =
|
|
2977
|
-
if (!
|
|
3293
|
+
const settingsPath = join22(claudeHome(), "settings.json");
|
|
3294
|
+
if (!existsSync19(settingsPath)) {
|
|
2978
3295
|
addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
|
|
2979
3296
|
return;
|
|
2980
3297
|
}
|
|
@@ -3004,314 +3321,107 @@ function reportCheckSchema(section2) {
|
|
|
3004
3321
|
init_color();
|
|
3005
3322
|
import { randomBytes } from "node:crypto";
|
|
3006
3323
|
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
3007
|
-
import { existsSync as
|
|
3324
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync6, readdirSync as readdirSync11, rmSync as rmSync10 } from "node:fs";
|
|
3008
3325
|
import { homedir as homedir4 } from "node:os";
|
|
3009
|
-
import { join as
|
|
3326
|
+
import { join as join27 } from "node:path";
|
|
3010
3327
|
|
|
3011
3328
|
// src/commands.doctor.check-shared.scan.ts
|
|
3012
3329
|
init_color();
|
|
3013
|
-
import { join as
|
|
3330
|
+
import { join as join24 } from "node:path";
|
|
3014
3331
|
init_config();
|
|
3015
3332
|
init_push_gitleaks();
|
|
3016
3333
|
function scrubPath(logical, sid, logicalToEncoded) {
|
|
3017
3334
|
const encoded = logicalToEncoded.get(logical) ?? logical;
|
|
3018
|
-
return
|
|
3335
|
+
return join24(claudeHome(), "projects", encoded, `${sid}.jsonl`);
|
|
3019
3336
|
}
|
|
3020
3337
|
function reportSessionFindings(section2, bySession) {
|
|
3021
3338
|
for (const [sid, counts] of bySession) {
|
|
3022
3339
|
const summary = [...counts.entries()].map(([rule, n]) => `${rule} (${n})`).join(", ");
|
|
3023
3340
|
addItem(section2, `${red(failGlyph)} ${red(summary)} in session ${sid}`);
|
|
3024
3341
|
}
|
|
3025
|
-
process.exitCode = 1;
|
|
3026
|
-
}
|
|
3027
|
-
function reportOtherFindings(section2, other) {
|
|
3028
|
-
for (const f of other) {
|
|
3029
|
-
addItem(section2, `${red(failGlyph)} ${red(f.RuleID)} leak in ${f.File}`);
|
|
3030
|
-
}
|
|
3031
|
-
process.exitCode = 1;
|
|
3032
|
-
}
|
|
3033
|
-
function reportRemediation(section2, bySession, logicalBySession, logicalToEncoded) {
|
|
3034
|
-
addItem(section2, "");
|
|
3035
|
-
addItem(section2, bold("Remediation"));
|
|
3036
|
-
for (const [sid] of bySession) {
|
|
3037
|
-
const logical = logicalBySession.get(sid);
|
|
3038
|
-
if (logical !== void 0) {
|
|
3039
|
-
const rotateLine = dim(
|
|
3040
|
-
`- rotate the credential, then scrub ${scrubPath(logical, sid, logicalToEncoded)}`
|
|
3041
|
-
);
|
|
3042
|
-
addItem(section2, ` ${rotateLine}`);
|
|
3043
|
-
}
|
|
3044
|
-
}
|
|
3045
|
-
addItem(section2, ` ${dim("- false positive? add a pattern to .gitleaks.toml")}`);
|
|
3046
|
-
}
|
|
3047
|
-
var SESSION_PATH_LOGICAL = /^shared\/projects\/([^/]+)\/([^/]+)\.jsonl$/;
|
|
3048
|
-
function emitClean(section2, staged) {
|
|
3049
|
-
addItem(section2, `${green(okGlyph)} ${staged} project(s) scanned, no leaks`);
|
|
3050
|
-
}
|
|
3051
|
-
function buildLogicalBySession(findings) {
|
|
3052
|
-
const logicalBySession = /* @__PURE__ */ new Map();
|
|
3053
|
-
for (const f of findings) {
|
|
3054
|
-
const m = SESSION_PATH_LOGICAL.exec(f.File);
|
|
3055
|
-
if (m?.[2] !== void 0 && !logicalBySession.has(m[2])) {
|
|
3056
|
-
logicalBySession.set(m[2], m[1] ?? "");
|
|
3057
|
-
}
|
|
3058
|
-
}
|
|
3059
|
-
return logicalBySession;
|
|
3060
|
-
}
|
|
3061
|
-
function emitDescriptionLegend(section2, findings) {
|
|
3062
|
-
const descByRule = /* @__PURE__ */ new Map();
|
|
3063
|
-
for (const f of findings) {
|
|
3064
|
-
if (f.Description && !descByRule.has(f.RuleID)) descByRule.set(f.RuleID, f.Description);
|
|
3065
|
-
}
|
|
3066
|
-
if (descByRule.size === 0) return;
|
|
3067
|
-
addItem(section2, "");
|
|
3068
|
-
addItem(section2, bold("Finding types"));
|
|
3069
|
-
for (const [rule, desc] of descByRule) {
|
|
3070
|
-
const ruleLabel = red(`- [${rule}]`);
|
|
3071
|
-
addItem(section2, ` ${ruleLabel}: ${dim(desc)}`);
|
|
3072
|
-
}
|
|
3073
|
-
}
|
|
3074
|
-
function scanAndReport(section2, tmpRoot, staged, logicalToEncoded) {
|
|
3075
|
-
let findings;
|
|
3076
|
-
try {
|
|
3077
|
-
findings = scanStagedTree(tmpRoot);
|
|
3078
|
-
} catch (err) {
|
|
3079
|
-
addItem(section2, `${red(failGlyph)} scan failed: ${err.message}`);
|
|
3080
|
-
process.exitCode = 1;
|
|
3081
|
-
return;
|
|
3082
|
-
}
|
|
3083
|
-
if (findings === null) {
|
|
3084
|
-
addItem(section2, `${red(failGlyph)} scan failed: no parseable gitleaks report`);
|
|
3085
|
-
process.exitCode = 1;
|
|
3086
|
-
return;
|
|
3087
|
-
}
|
|
3088
|
-
const { bySession, other } = partitionFindings(findings);
|
|
3089
|
-
if (bySession.size === 0 && other.length === 0) {
|
|
3090
|
-
emitClean(section2, staged);
|
|
3091
|
-
return;
|
|
3092
|
-
}
|
|
3093
|
-
if (other.length > 0) reportOtherFindings(section2, other);
|
|
3094
|
-
if (bySession.size > 0) reportSessionFindings(section2, bySession);
|
|
3095
|
-
if (bySession.size > 0) {
|
|
3096
|
-
reportRemediation(section2, bySession, buildLogicalBySession(findings), logicalToEncoded);
|
|
3097
|
-
}
|
|
3098
|
-
emitDescriptionLegend(section2, findings);
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
// src/commands.doctor.check-shared.ts
|
|
3102
|
-
init_config();
|
|
3103
|
-
|
|
3104
|
-
// src/remap.ts
|
|
3105
|
-
init_config_sharedDirs_guard();
|
|
3106
|
-
import {
|
|
3107
|
-
cpSync as cpSync5,
|
|
3108
|
-
existsSync as existsSync20,
|
|
3109
|
-
lstatSync as lstatSync9,
|
|
3110
|
-
mkdirSync as mkdirSync5,
|
|
3111
|
-
readdirSync as readdirSync9,
|
|
3112
|
-
renameSync as renameSync3,
|
|
3113
|
-
rmSync as rmSync8,
|
|
3114
|
-
statSync as statSync5
|
|
3115
|
-
} from "node:fs";
|
|
3116
|
-
import { dirname as dirname4, join as join25, relative as relative5, sep as sep3 } from "node:path";
|
|
3117
|
-
|
|
3118
|
-
// src/extras-sync.core.ts
|
|
3119
|
-
init_config();
|
|
3120
|
-
import { cpSync as cpSync4, existsSync as existsSync18, lstatSync as lstatSync8, readdirSync as readdirSync7, readFileSync as readFileSync6, rmSync as rmSync7 } from "node:fs";
|
|
3121
|
-
import { basename, join as join23, relative as relative4 } from "node:path";
|
|
3122
|
-
|
|
3123
|
-
// src/extras-sync.guards.ts
|
|
3124
|
-
init_utils();
|
|
3125
|
-
init_config_sharedDirs_guard();
|
|
3126
|
-
import { isAbsolute, normalize } from "node:path";
|
|
3127
|
-
function assertSafeLocalRoot(localRoot, logical) {
|
|
3128
|
-
if (!isAbsolute(localRoot)) {
|
|
3129
|
-
throw new NomadFatal(
|
|
3130
|
-
`invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
|
|
3131
|
-
);
|
|
3132
|
-
}
|
|
3133
|
-
if (localRoot !== normalize(localRoot)) {
|
|
3134
|
-
throw new NomadFatal(
|
|
3135
|
-
`invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
|
|
3136
|
-
);
|
|
3137
|
-
}
|
|
3138
|
-
}
|
|
3139
|
-
|
|
3140
|
-
// src/extras-sync.core.ts
|
|
3141
|
-
init_utils();
|
|
3142
|
-
init_utils_json();
|
|
3143
|
-
function loadValidatedExtras(opts) {
|
|
3144
|
-
const repo = repoHome();
|
|
3145
|
-
const mapPath = join23(repo, "path-map.json");
|
|
3146
|
-
const repoExtras = join23(repo, "shared", "extras");
|
|
3147
|
-
if (!existsSync18(mapPath) || opts.requireRepoExtras === true && !existsSync18(repoExtras)) {
|
|
3148
|
-
if (opts.missingMsg !== void 0) log(opts.missingMsg);
|
|
3149
|
-
return null;
|
|
3150
|
-
}
|
|
3151
|
-
const map = readPathMap(mapPath);
|
|
3152
|
-
const extrasMap = map.extras ?? {};
|
|
3153
|
-
if (Object.keys(extrasMap).length === 0) return null;
|
|
3154
|
-
for (const logical of Object.keys(extrasMap)) {
|
|
3155
|
-
assertSafeLogical(logical);
|
|
3156
|
-
const localRoot = map.projects[logical]?.[HOST];
|
|
3157
|
-
if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
|
|
3158
|
-
}
|
|
3159
|
-
return { map, extrasMap };
|
|
3160
|
-
}
|
|
3161
|
-
function* eachExtrasTarget(v, counts) {
|
|
3162
|
-
const whitelist = SUPPORTED_EXTRAS;
|
|
3163
|
-
for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
|
|
3164
|
-
const localRoot = v.map.projects[logical]?.[HOST];
|
|
3165
|
-
if (!localRoot || localRoot === "TBD") {
|
|
3166
|
-
counts.unmapped++;
|
|
3167
|
-
continue;
|
|
3168
|
-
}
|
|
3169
|
-
for (const dirname10 of dirnames) {
|
|
3170
|
-
if (!whitelist.includes(dirname10)) {
|
|
3171
|
-
counts.skipped++;
|
|
3172
|
-
continue;
|
|
3173
|
-
}
|
|
3174
|
-
yield { logical, localRoot, dirname: dirname10 };
|
|
3175
|
-
}
|
|
3176
|
-
}
|
|
3342
|
+
process.exitCode = 1;
|
|
3177
3343
|
}
|
|
3178
|
-
function
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
if (isExcluded(name)) continue;
|
|
3182
|
-
const dstPath = join23(dst, name);
|
|
3183
|
-
const dstStat = lstatSync8(dstPath, { throwIfNoEntry: false });
|
|
3184
|
-
if (dstStat === void 0) continue;
|
|
3185
|
-
if (dstStat.isSymbolicLink()) {
|
|
3186
|
-
rmSync7(dstPath, { recursive: true, force: true });
|
|
3187
|
-
} else if (dstStat.isDirectory() && lstatSync8(join23(src, name)).isDirectory()) {
|
|
3188
|
-
stripCollidingDstSymlinks(join23(src, name), dstPath, isExcluded);
|
|
3189
|
-
}
|
|
3344
|
+
function reportOtherFindings(section2, other) {
|
|
3345
|
+
for (const f of other) {
|
|
3346
|
+
addItem(section2, `${red(failGlyph)} ${red(f.RuleID)} leak in ${f.File}`);
|
|
3190
3347
|
}
|
|
3348
|
+
process.exitCode = 1;
|
|
3191
3349
|
}
|
|
3192
|
-
function
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
const
|
|
3197
|
-
if (
|
|
3198
|
-
|
|
3199
|
-
|
|
3350
|
+
function reportRemediation(section2, bySession, logicalBySession, logicalToEncoded) {
|
|
3351
|
+
addItem(section2, "");
|
|
3352
|
+
addItem(section2, bold("Remediation"));
|
|
3353
|
+
for (const [sid] of bySession) {
|
|
3354
|
+
const logical = logicalBySession.get(sid);
|
|
3355
|
+
if (logical !== void 0) {
|
|
3356
|
+
const rotateLine = dim(
|
|
3357
|
+
`- rotate the credential, then scrub ${scrubPath(logical, sid, logicalToEncoded)}`
|
|
3200
3358
|
);
|
|
3359
|
+
addItem(section2, ` ${rotateLine}`);
|
|
3201
3360
|
}
|
|
3202
|
-
throw err;
|
|
3203
3361
|
}
|
|
3362
|
+
addItem(section2, ` ${dim("- false positive? add a pattern to .gitleaks.toml")}`);
|
|
3204
3363
|
}
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
src,
|
|
3209
|
-
dst,
|
|
3210
|
-
(srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry)),
|
|
3211
|
-
"copyExtrasOverlayFiltered"
|
|
3212
|
-
);
|
|
3213
|
-
}
|
|
3214
|
-
function copyExtrasOverlaySkipDiverged(src, dst, blockSet, divergedSet) {
|
|
3215
|
-
stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
|
|
3216
|
-
cpSyncGuarded(
|
|
3217
|
-
src,
|
|
3218
|
-
dst,
|
|
3219
|
-
(srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry)) && !divergedSet.has(relative4(src, srcEntry)),
|
|
3220
|
-
"copyExtrasOverlaySkipDiverged"
|
|
3221
|
-
);
|
|
3222
|
-
}
|
|
3223
|
-
function copyExtras(src, dst) {
|
|
3224
|
-
rmSync7(dst, { recursive: true, force: true });
|
|
3225
|
-
cpSync4(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
|
|
3364
|
+
var SESSION_PATH_LOGICAL = /^shared\/projects\/([^/]+)\/([^/]+)\.jsonl$/;
|
|
3365
|
+
function emitClean(section2, staged) {
|
|
3366
|
+
addItem(section2, `${green(okGlyph)} ${staged} project(s) scanned, no leaks`);
|
|
3226
3367
|
}
|
|
3227
|
-
function
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
return;
|
|
3368
|
+
function buildLogicalBySession(findings) {
|
|
3369
|
+
const logicalBySession = /* @__PURE__ */ new Map();
|
|
3370
|
+
for (const f of findings) {
|
|
3371
|
+
const m = SESSION_PATH_LOGICAL.exec(f.File);
|
|
3372
|
+
if (m?.[2] !== void 0 && !logicalBySession.has(m[2])) {
|
|
3373
|
+
logicalBySession.set(m[2], m[1] ?? "");
|
|
3234
3374
|
}
|
|
3235
|
-
if (!readFileSync6(src).equals(dstBuf)) return;
|
|
3236
3375
|
}
|
|
3237
|
-
|
|
3238
|
-
}
|
|
3239
|
-
function extrasDenySet(dirname10) {
|
|
3240
|
-
return dirname10 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
|
|
3241
|
-
}
|
|
3242
|
-
function copyExtrasFiltered(src, dst, blockSet) {
|
|
3243
|
-
rmSync7(dst, { recursive: true, force: true });
|
|
3244
|
-
cpSync4(src, dst, {
|
|
3245
|
-
recursive: true,
|
|
3246
|
-
force: true,
|
|
3247
|
-
verbatimSymlinks: true,
|
|
3248
|
-
filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
|
|
3249
|
-
});
|
|
3376
|
+
return logicalBySession;
|
|
3250
3377
|
}
|
|
3251
|
-
function
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
const srcStat = lstatSync8(join23(src, name), { throwIfNoEntry: false });
|
|
3256
|
-
if (srcStat === void 0) {
|
|
3257
|
-
rmSync7(dstPath, { recursive: true, force: true });
|
|
3258
|
-
continue;
|
|
3259
|
-
}
|
|
3260
|
-
const dstStat = lstatSync8(dstPath);
|
|
3261
|
-
if (srcStat.isDirectory() && dstStat.isDirectory()) {
|
|
3262
|
-
prunePreservingDenied(join23(src, name), dstPath, blockSet);
|
|
3263
|
-
} else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
|
|
3264
|
-
rmSync7(dstPath, { recursive: true, force: true });
|
|
3265
|
-
}
|
|
3378
|
+
function emitDescriptionLegend(section2, findings) {
|
|
3379
|
+
const descByRule = /* @__PURE__ */ new Map();
|
|
3380
|
+
for (const f of findings) {
|
|
3381
|
+
if (f.Description && !descByRule.has(f.RuleID)) descByRule.set(f.RuleID, f.Description);
|
|
3266
3382
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3383
|
+
if (descByRule.size === 0) return;
|
|
3384
|
+
addItem(section2, "");
|
|
3385
|
+
addItem(section2, bold("Finding types"));
|
|
3386
|
+
for (const [rule, desc] of descByRule) {
|
|
3387
|
+
const ruleLabel = red(`- [${rule}]`);
|
|
3388
|
+
addItem(section2, ` ${ruleLabel}: ${dim(desc)}`);
|
|
3273
3389
|
}
|
|
3274
|
-
stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
|
|
3275
|
-
cpSync4(src, dst, {
|
|
3276
|
-
recursive: true,
|
|
3277
|
-
force: true,
|
|
3278
|
-
verbatimSymlinks: true,
|
|
3279
|
-
filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename(srcEntry))
|
|
3280
|
-
});
|
|
3281
3390
|
}
|
|
3282
|
-
function
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
}
|
|
3291
|
-
const dstStat = lstatSync8(dstPath);
|
|
3292
|
-
if (srcStat.isDirectory() && dstStat.isDirectory()) {
|
|
3293
|
-
prunePreservingBy(join23(src, name), dstPath, isPreserved);
|
|
3294
|
-
} else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
|
|
3295
|
-
rmSync7(dstPath, { recursive: true, force: true });
|
|
3296
|
-
}
|
|
3391
|
+
function scanAndReport(section2, tmpRoot, staged, logicalToEncoded) {
|
|
3392
|
+
let findings;
|
|
3393
|
+
try {
|
|
3394
|
+
findings = scanStagedTree(tmpRoot);
|
|
3395
|
+
} catch (err) {
|
|
3396
|
+
addItem(section2, `${red(failGlyph)} scan failed: ${err.message}`);
|
|
3397
|
+
process.exitCode = 1;
|
|
3398
|
+
return;
|
|
3297
3399
|
}
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
if (dstStat.isDirectory()) prunePreservingBy(src, dst, isPreserved);
|
|
3303
|
-
else rmSync7(dst, { recursive: true, force: true });
|
|
3400
|
+
if (findings === null) {
|
|
3401
|
+
addItem(section2, `${red(failGlyph)} scan failed: no parseable gitleaks report`);
|
|
3402
|
+
process.exitCode = 1;
|
|
3403
|
+
return;
|
|
3304
3404
|
}
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3405
|
+
const { bySession, other } = partitionFindings(findings);
|
|
3406
|
+
if (bySession.size === 0 && other.length === 0) {
|
|
3407
|
+
emitClean(section2, staged);
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
if (other.length > 0) reportOtherFindings(section2, other);
|
|
3411
|
+
if (bySession.size > 0) reportSessionFindings(section2, bySession);
|
|
3412
|
+
if (bySession.size > 0) {
|
|
3413
|
+
reportRemediation(section2, bySession, buildLogicalBySession(findings), logicalToEncoded);
|
|
3414
|
+
}
|
|
3415
|
+
emitDescriptionLegend(section2, findings);
|
|
3312
3416
|
}
|
|
3313
3417
|
|
|
3418
|
+
// src/commands.doctor.check-shared.ts
|
|
3419
|
+
init_config();
|
|
3420
|
+
|
|
3314
3421
|
// src/remap.ts
|
|
3422
|
+
init_config_sharedDirs_guard();
|
|
3423
|
+
import { cpSync as cpSync6, existsSync as existsSync21, lstatSync as lstatSync10, mkdirSync as mkdirSync5, readdirSync as readdirSync10, rmSync as rmSync9, statSync as statSync5 } from "node:fs";
|
|
3424
|
+
import { dirname as dirname4, join as join26, relative as relative5, sep as sep4 } from "node:path";
|
|
3315
3425
|
init_config();
|
|
3316
3426
|
|
|
3317
3427
|
// src/push-manifest.ts
|
|
@@ -3319,8 +3429,8 @@ init_config();
|
|
|
3319
3429
|
init_push_gitleaks_config();
|
|
3320
3430
|
init_utils_fs();
|
|
3321
3431
|
import { createHash } from "node:crypto";
|
|
3322
|
-
import { existsSync as
|
|
3323
|
-
import { dirname as dirname3, join as
|
|
3432
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync4, readdirSync as readdirSync9, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
3433
|
+
import { dirname as dirname3, join as join25 } from "node:path";
|
|
3324
3434
|
function isChanged(prev, cur, hash) {
|
|
3325
3435
|
if (prev === void 0) return true;
|
|
3326
3436
|
if (prev.size !== cur.size) return true;
|
|
@@ -3360,8 +3470,8 @@ function hashFile(absPath) {
|
|
|
3360
3470
|
return createHash("sha256").update(readFileSync7(absPath)).digest("hex");
|
|
3361
3471
|
}
|
|
3362
3472
|
function enumerateDir(dir, results) {
|
|
3363
|
-
for (const entry of
|
|
3364
|
-
const fullPath =
|
|
3473
|
+
for (const entry of readdirSync9(dir)) {
|
|
3474
|
+
const fullPath = join25(dir, entry);
|
|
3365
3475
|
const st = statSync4(fullPath);
|
|
3366
3476
|
if (st.isDirectory()) {
|
|
3367
3477
|
enumerateDir(fullPath, results);
|
|
@@ -3372,8 +3482,8 @@ function enumerateDir(dir, results) {
|
|
|
3372
3482
|
}
|
|
3373
3483
|
function enumerateSourceFiles(projectDir) {
|
|
3374
3484
|
const results = [];
|
|
3375
|
-
for (const entry of
|
|
3376
|
-
const fullPath =
|
|
3485
|
+
for (const entry of readdirSync9(projectDir)) {
|
|
3486
|
+
const fullPath = join25(projectDir, entry);
|
|
3377
3487
|
const st = statSync4(fullPath);
|
|
3378
3488
|
if (st.isDirectory()) {
|
|
3379
3489
|
enumerateDir(fullPath, results);
|
|
@@ -3385,15 +3495,15 @@ function enumerateSourceFiles(projectDir) {
|
|
|
3385
3495
|
}
|
|
3386
3496
|
function fileIdentity(p) {
|
|
3387
3497
|
if (p === null) return "none::absent";
|
|
3388
|
-
if (!
|
|
3498
|
+
if (!existsSync20(p)) return `${p}::absent`;
|
|
3389
3499
|
return `${p}::${createHash("sha256").update(readFileSync7(p)).digest("hex")}`;
|
|
3390
3500
|
}
|
|
3391
3501
|
function computeConfigHash() {
|
|
3392
3502
|
const repo = repoHome();
|
|
3393
3503
|
const parts = [
|
|
3394
3504
|
fileIdentity(resolveTomlPath(repo)),
|
|
3395
|
-
fileIdentity(
|
|
3396
|
-
fileIdentity(
|
|
3505
|
+
fileIdentity(join25(repo, ".gitleaks.overlay.toml")),
|
|
3506
|
+
fileIdentity(join25(repo, ".gitleaksignore"))
|
|
3397
3507
|
];
|
|
3398
3508
|
return createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
3399
3509
|
}
|
|
@@ -3427,10 +3537,10 @@ init_utils_json();
|
|
|
3427
3537
|
var TMP_SUFFIX = ".nomad-tmp";
|
|
3428
3538
|
function atomicMirror(src, dst, options) {
|
|
3429
3539
|
const tmp = `${dst}${TMP_SUFFIX}`;
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3540
|
+
rmSync9(tmp, { recursive: true, force: true });
|
|
3541
|
+
cpSync6(src, tmp, options);
|
|
3542
|
+
rmSync9(dst, { recursive: true, force: true });
|
|
3543
|
+
renameAtomicRetry(tmp, dst);
|
|
3434
3544
|
}
|
|
3435
3545
|
function overlaySessionDir(src, dst) {
|
|
3436
3546
|
stripCollidingDstSymlinks(src, dst, () => false);
|
|
@@ -3439,11 +3549,11 @@ function overlaySessionDir(src, dst) {
|
|
|
3439
3549
|
function copyFileAtomic(src, dst) {
|
|
3440
3550
|
mkdirSync5(dirname4(dst), { recursive: true });
|
|
3441
3551
|
const tmp = `${dst}.tmp.${process.pid}`;
|
|
3442
|
-
|
|
3443
|
-
|
|
3552
|
+
cpSync6(src, tmp, { force: true, preserveTimestamps: true });
|
|
3553
|
+
renameAtomicRetry(tmp, dst);
|
|
3444
3554
|
}
|
|
3445
3555
|
function hasDeltaForDir(sel, localDir) {
|
|
3446
|
-
const prefix = `${localDir}${
|
|
3556
|
+
const prefix = `${localDir}${sep4}`;
|
|
3447
3557
|
for (const p of sel.changed) if (p.startsWith(prefix)) return true;
|
|
3448
3558
|
for (const p of sel.deleted) if (p.startsWith(prefix)) return true;
|
|
3449
3559
|
return false;
|
|
@@ -3452,15 +3562,15 @@ function skipForSelection(sel, localDir) {
|
|
|
3452
3562
|
return sel !== void 0 && !hasDeltaForDir(sel, localDir);
|
|
3453
3563
|
}
|
|
3454
3564
|
function applySelective(sel, localDir, repoDst) {
|
|
3455
|
-
const prefix = `${localDir}${
|
|
3565
|
+
const prefix = `${localDir}${sep4}`;
|
|
3456
3566
|
for (const src of sel.changed) {
|
|
3457
3567
|
if (!src.startsWith(prefix)) continue;
|
|
3458
|
-
copyFileAtomic(src,
|
|
3568
|
+
copyFileAtomic(src, join26(repoDst, relative5(localDir, src)));
|
|
3459
3569
|
}
|
|
3460
3570
|
for (const src of sel.deleted) {
|
|
3461
3571
|
if (!src.startsWith(prefix)) continue;
|
|
3462
|
-
const dst =
|
|
3463
|
-
if (
|
|
3572
|
+
const dst = join26(repoDst, relative5(localDir, src));
|
|
3573
|
+
if (existsSync21(dst)) rmSync9(dst);
|
|
3464
3574
|
}
|
|
3465
3575
|
}
|
|
3466
3576
|
function copyDirJsonlOnly(src, dst) {
|
|
@@ -3470,7 +3580,7 @@ function copyDirJsonlOnly(src, dst) {
|
|
|
3470
3580
|
filter: (srcPath) => {
|
|
3471
3581
|
const rel = relative5(src, srcPath);
|
|
3472
3582
|
if (rel === "") return true;
|
|
3473
|
-
if (rel.split(
|
|
3583
|
+
if (rel.split(sep4).length > 1) return true;
|
|
3474
3584
|
if (statSync5(srcPath).isDirectory()) return true;
|
|
3475
3585
|
if (srcPath.endsWith(".jsonl")) return true;
|
|
3476
3586
|
item(`skip ${rel}: extension not in allowlist`);
|
|
@@ -3489,15 +3599,15 @@ function remapPull(ts, opts = {}) {
|
|
|
3489
3599
|
const wouldPull = [];
|
|
3490
3600
|
const repo = repoHome();
|
|
3491
3601
|
const claude = claudeHome();
|
|
3492
|
-
const mapPath =
|
|
3493
|
-
const repoProjects =
|
|
3494
|
-
if (!
|
|
3602
|
+
const mapPath = join26(repo, "path-map.json");
|
|
3603
|
+
const repoProjects = join26(repo, "shared", "projects");
|
|
3604
|
+
if (!existsSync21(mapPath) || !existsSync21(repoProjects)) {
|
|
3495
3605
|
const text = "no path-map or repo projects dir; skipping session remap";
|
|
3496
3606
|
emitPreview(opts.onPreview, { kind: "note", text }, text);
|
|
3497
3607
|
return { unmapped: 0, pulled, wouldPull };
|
|
3498
3608
|
}
|
|
3499
3609
|
const map = readPathMap(mapPath);
|
|
3500
|
-
const localProjects =
|
|
3610
|
+
const localProjects = join26(claude, "projects");
|
|
3501
3611
|
if (!dryRun) mkdirSync5(localProjects, { recursive: true });
|
|
3502
3612
|
for (const [logical, hosts] of Object.entries(map.projects)) {
|
|
3503
3613
|
assertSafeLogical(logical);
|
|
@@ -3507,9 +3617,9 @@ function remapPull(ts, opts = {}) {
|
|
|
3507
3617
|
continue;
|
|
3508
3618
|
}
|
|
3509
3619
|
assertSafeLocalRoot(localPath, logical);
|
|
3510
|
-
const src =
|
|
3511
|
-
if (!
|
|
3512
|
-
const dst =
|
|
3620
|
+
const src = join26(repoProjects, logical);
|
|
3621
|
+
if (!existsSync21(src)) continue;
|
|
3622
|
+
const dst = join26(localProjects, encodePath(localPath));
|
|
3513
3623
|
if (dryRun) {
|
|
3514
3624
|
wouldPull.push(logical);
|
|
3515
3625
|
emitPreview(
|
|
@@ -3527,12 +3637,12 @@ function remapPull(ts, opts = {}) {
|
|
|
3527
3637
|
}
|
|
3528
3638
|
function countLocalOnly(src, dst) {
|
|
3529
3639
|
let count = 0;
|
|
3530
|
-
for (const name of
|
|
3531
|
-
const dstPath =
|
|
3532
|
-
const srcPath =
|
|
3533
|
-
if (
|
|
3640
|
+
for (const name of readdirSync10(dst)) {
|
|
3641
|
+
const dstPath = join26(dst, name);
|
|
3642
|
+
const srcPath = join26(src, name);
|
|
3643
|
+
if (lstatSync10(dstPath).isDirectory()) {
|
|
3534
3644
|
count += countLocalOnly(srcPath, dstPath);
|
|
3535
|
-
} else if (
|
|
3645
|
+
} else if (lstatSync10(srcPath, { throwIfNoEntry: false }) === void 0) {
|
|
3536
3646
|
count++;
|
|
3537
3647
|
}
|
|
3538
3648
|
}
|
|
@@ -3541,20 +3651,20 @@ function countLocalOnly(src, dst) {
|
|
|
3541
3651
|
function scanLocalOnly() {
|
|
3542
3652
|
const repo = repoHome();
|
|
3543
3653
|
const claude = claudeHome();
|
|
3544
|
-
const mapPath =
|
|
3545
|
-
const repoProjects =
|
|
3546
|
-
if (!
|
|
3654
|
+
const mapPath = join26(repo, "path-map.json");
|
|
3655
|
+
const repoProjects = join26(repo, "shared", "projects");
|
|
3656
|
+
if (!existsSync21(mapPath) || !existsSync21(repoProjects)) return 0;
|
|
3547
3657
|
const map = readPathMap(mapPath);
|
|
3548
|
-
const localProjects =
|
|
3658
|
+
const localProjects = join26(claude, "projects");
|
|
3549
3659
|
let count = 0;
|
|
3550
3660
|
for (const [logical, hosts] of Object.entries(map.projects)) {
|
|
3551
3661
|
assertSafeLogical(logical);
|
|
3552
3662
|
const localPath = hosts[HOST];
|
|
3553
3663
|
if (!localPath || localPath === "TBD") continue;
|
|
3554
3664
|
assertSafeLocalRoot(localPath, logical);
|
|
3555
|
-
const dst =
|
|
3556
|
-
if (!
|
|
3557
|
-
count += countLocalOnly(
|
|
3665
|
+
const dst = join26(localProjects, encodePath(localPath));
|
|
3666
|
+
if (!existsSync21(dst)) continue;
|
|
3667
|
+
count += countLocalOnly(join26(repoProjects, logical), dst);
|
|
3558
3668
|
}
|
|
3559
3669
|
return count;
|
|
3560
3670
|
}
|
|
@@ -3589,26 +3699,26 @@ function remapPush(ts, opts = {}) {
|
|
|
3589
3699
|
const wouldPush = [];
|
|
3590
3700
|
const repo = repoHome();
|
|
3591
3701
|
const claude = claudeHome();
|
|
3592
|
-
const mapPath =
|
|
3593
|
-
if (!
|
|
3702
|
+
const mapPath = join26(repo, "path-map.json");
|
|
3703
|
+
if (!existsSync21(mapPath)) {
|
|
3594
3704
|
log("no path-map.json; skipping session export");
|
|
3595
3705
|
return { unmapped: 0, collisions: 0, pushed, wouldPush };
|
|
3596
3706
|
}
|
|
3597
3707
|
const map = readPathMap(mapPath);
|
|
3598
|
-
const localProjects =
|
|
3599
|
-
const repoProjects =
|
|
3708
|
+
const localProjects = join26(claude, "projects");
|
|
3709
|
+
const repoProjects = join26(repo, "shared", "projects");
|
|
3600
3710
|
const reverse = buildReverseMap(map);
|
|
3601
|
-
if (!
|
|
3711
|
+
if (!existsSync21(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
|
|
3602
3712
|
if (!dryRun) mkdirSync5(repoProjects, { recursive: true });
|
|
3603
|
-
for (const dir of
|
|
3713
|
+
for (const dir of readdirSync10(localProjects)) {
|
|
3604
3714
|
if (dir.endsWith(TMP_SUFFIX)) continue;
|
|
3605
3715
|
const logical = reverse.get(dir);
|
|
3606
3716
|
if (!logical) {
|
|
3607
3717
|
unmapped++;
|
|
3608
3718
|
continue;
|
|
3609
3719
|
}
|
|
3610
|
-
const localDir =
|
|
3611
|
-
const repoDst =
|
|
3720
|
+
const localDir = join26(localProjects, dir);
|
|
3721
|
+
const repoDst = join26(repoProjects, logical);
|
|
3612
3722
|
if (skipForSelection(opts.selection, localDir)) continue;
|
|
3613
3723
|
if (dryRun) {
|
|
3614
3724
|
wouldPush.push(logical);
|
|
@@ -3632,8 +3742,8 @@ function buildScanTree(tmpRoot) {
|
|
|
3632
3742
|
const logicalToEncoded = /* @__PURE__ */ new Map();
|
|
3633
3743
|
let staged = 0;
|
|
3634
3744
|
const repo = repoHome();
|
|
3635
|
-
const mapPath =
|
|
3636
|
-
if (!
|
|
3745
|
+
const mapPath = join27(repo, "path-map.json");
|
|
3746
|
+
if (!existsSync22(mapPath)) return { logicalToEncoded, staged, malformed: false };
|
|
3637
3747
|
let map;
|
|
3638
3748
|
try {
|
|
3639
3749
|
map = readJson(mapPath);
|
|
@@ -3650,12 +3760,12 @@ function buildScanTree(tmpRoot) {
|
|
|
3650
3760
|
if (!p || p === "TBD") continue;
|
|
3651
3761
|
reverse.set(encodePath(p), logical);
|
|
3652
3762
|
}
|
|
3653
|
-
const localProjects =
|
|
3654
|
-
if (!
|
|
3655
|
-
for (const dir of
|
|
3763
|
+
const localProjects = join27(claudeHome(), "projects");
|
|
3764
|
+
if (!existsSync22(localProjects)) return { logicalToEncoded, staged, malformed: false };
|
|
3765
|
+
for (const dir of readdirSync11(localProjects)) {
|
|
3656
3766
|
const logical = reverse.get(dir);
|
|
3657
3767
|
if (!logical) continue;
|
|
3658
|
-
copyDirJsonlOnly(
|
|
3768
|
+
copyDirJsonlOnly(join27(localProjects, dir), join27(tmpRoot, "shared", "projects", logical));
|
|
3659
3769
|
logicalToEncoded.set(logical, dir);
|
|
3660
3770
|
staged++;
|
|
3661
3771
|
}
|
|
@@ -3686,11 +3796,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
|
|
|
3686
3796
|
}
|
|
3687
3797
|
function reportCheckShared(section2, gitleaksReady) {
|
|
3688
3798
|
if (!ensureGitleaksReady(section2, gitleaksReady)) return;
|
|
3689
|
-
const cacheDir =
|
|
3799
|
+
const cacheDir = join27(homedir4(), ".cache", "claude-nomad");
|
|
3690
3800
|
mkdirSync6(cacheDir, { recursive: true });
|
|
3691
3801
|
const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
3692
|
-
const reportPath =
|
|
3693
|
-
const tmpRoot =
|
|
3802
|
+
const reportPath = join27(cacheDir, `check-shared-${stamp}.json`);
|
|
3803
|
+
const tmpRoot = join27(cacheDir, `check-shared-tree-${stamp}`);
|
|
3694
3804
|
try {
|
|
3695
3805
|
const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
|
|
3696
3806
|
if (malformed) {
|
|
@@ -3704,15 +3814,15 @@ function reportCheckShared(section2, gitleaksReady) {
|
|
|
3704
3814
|
}
|
|
3705
3815
|
scanAndReport(section2, tmpRoot, staged, logicalToEncoded);
|
|
3706
3816
|
} finally {
|
|
3707
|
-
|
|
3708
|
-
|
|
3817
|
+
rmSync10(reportPath, { force: true });
|
|
3818
|
+
rmSync10(tmpRoot, { recursive: true, force: true });
|
|
3709
3819
|
}
|
|
3710
3820
|
}
|
|
3711
3821
|
|
|
3712
3822
|
// src/commands.doctor.checks.hooks.scope.ts
|
|
3713
3823
|
init_color();
|
|
3714
|
-
import { existsSync as
|
|
3715
|
-
import { dirname as dirname5, extname, join as
|
|
3824
|
+
import { existsSync as existsSync23, readFileSync as readFileSync8, readdirSync as readdirSync12, realpathSync as realpathSync2 } from "node:fs";
|
|
3825
|
+
import { dirname as dirname5, extname, join as join28 } from "node:path";
|
|
3716
3826
|
init_config();
|
|
3717
3827
|
function typeFromPackageJson(pkgPath) {
|
|
3718
3828
|
try {
|
|
@@ -3734,8 +3844,8 @@ function effectiveType(hookPath) {
|
|
|
3734
3844
|
}
|
|
3735
3845
|
let dir = dirname5(real);
|
|
3736
3846
|
for (; ; ) {
|
|
3737
|
-
const pkg =
|
|
3738
|
-
if (
|
|
3847
|
+
const pkg = join28(dir, "package.json");
|
|
3848
|
+
if (existsSync23(pkg)) return typeFromPackageJson(pkg);
|
|
3739
3849
|
const parent = dirname5(dir);
|
|
3740
3850
|
if (parent === dir) return "cjs";
|
|
3741
3851
|
dir = parent;
|
|
@@ -3764,7 +3874,7 @@ function remedy(family) {
|
|
|
3764
3874
|
}
|
|
3765
3875
|
function safeReaddir2(dir) {
|
|
3766
3876
|
try {
|
|
3767
|
-
return
|
|
3877
|
+
return readdirSync12(dir);
|
|
3768
3878
|
} catch {
|
|
3769
3879
|
return [];
|
|
3770
3880
|
}
|
|
@@ -3777,15 +3887,15 @@ function safeRead(path) {
|
|
|
3777
3887
|
}
|
|
3778
3888
|
}
|
|
3779
3889
|
function reportHookScopeCheck(section2) {
|
|
3780
|
-
const hooksDir =
|
|
3781
|
-
if (!
|
|
3890
|
+
const hooksDir = join28(claudeHome(), "hooks");
|
|
3891
|
+
if (!existsSync23(hooksDir)) {
|
|
3782
3892
|
addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
|
|
3783
3893
|
return;
|
|
3784
3894
|
}
|
|
3785
3895
|
let anyWarn = false;
|
|
3786
3896
|
for (const name of safeReaddir2(hooksDir)) {
|
|
3787
3897
|
if (extname(name) !== ".js") continue;
|
|
3788
|
-
const abs =
|
|
3898
|
+
const abs = join28(hooksDir, name);
|
|
3789
3899
|
const eff = effectiveType(abs);
|
|
3790
3900
|
if (eff === null) continue;
|
|
3791
3901
|
const src = safeRead(abs);
|
|
@@ -3805,8 +3915,8 @@ function reportHookScopeCheck(section2) {
|
|
|
3805
3915
|
|
|
3806
3916
|
// src/commands.doctor.checks.hooks.ts
|
|
3807
3917
|
init_color();
|
|
3808
|
-
import { existsSync as
|
|
3809
|
-
import { join as
|
|
3918
|
+
import { existsSync as existsSync24 } from "node:fs";
|
|
3919
|
+
import { join as join29 } from "node:path";
|
|
3810
3920
|
init_config();
|
|
3811
3921
|
function expandHome(token) {
|
|
3812
3922
|
const h2 = home();
|
|
@@ -3820,11 +3930,11 @@ function stripShellPunctuation(token) {
|
|
|
3820
3930
|
return stripped.slice(0, end);
|
|
3821
3931
|
}
|
|
3822
3932
|
function* claudePathsIn(command) {
|
|
3823
|
-
const claudePrefix = `${claudeHome()}/`;
|
|
3933
|
+
const claudePrefix = `${claudeHome().replaceAll("\\", "/")}/`;
|
|
3824
3934
|
for (const segment of command.split(/&&|\|\||;|\|/)) {
|
|
3825
3935
|
for (const raw of segment.trim().split(/\s+/).filter(Boolean)) {
|
|
3826
3936
|
const expanded = expandHome(stripShellPunctuation(raw));
|
|
3827
|
-
if (expanded.startsWith(claudePrefix)) yield expanded;
|
|
3937
|
+
if (expanded.replaceAll("\\", "/").startsWith(claudePrefix)) yield expanded;
|
|
3828
3938
|
}
|
|
3829
3939
|
}
|
|
3830
3940
|
}
|
|
@@ -3849,7 +3959,7 @@ function checkEventGroups(section2, event, groups) {
|
|
|
3849
3959
|
for (const group of groups) {
|
|
3850
3960
|
for (const cmd of commandsFromGroup(group)) {
|
|
3851
3961
|
for (const resolved of claudePathsIn(cmd)) {
|
|
3852
|
-
if (
|
|
3962
|
+
if (existsSync24(resolved)) continue;
|
|
3853
3963
|
addItem(
|
|
3854
3964
|
section2,
|
|
3855
3965
|
`${red(failGlyph)} hooks/${event}: command target missing: ${resolved} (run nomad pull)`
|
|
@@ -3862,8 +3972,8 @@ function checkEventGroups(section2, event, groups) {
|
|
|
3862
3972
|
return anyFail;
|
|
3863
3973
|
}
|
|
3864
3974
|
function reportHooksTargetCheck(section2) {
|
|
3865
|
-
const settingsPath =
|
|
3866
|
-
if (!
|
|
3975
|
+
const settingsPath = join29(claudeHome(), "settings.json");
|
|
3976
|
+
if (!existsSync24(settingsPath)) {
|
|
3867
3977
|
addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
|
|
3868
3978
|
return;
|
|
3869
3979
|
}
|
|
@@ -3886,12 +3996,12 @@ function reportHooksTargetCheck(section2) {
|
|
|
3886
3996
|
|
|
3887
3997
|
// src/commands.doctor.checks.hooks.preserve-symlinks.ts
|
|
3888
3998
|
init_color();
|
|
3889
|
-
import { existsSync as
|
|
3890
|
-
import { join as
|
|
3999
|
+
import { existsSync as existsSync26, readFileSync as readFileSync9 } from "node:fs";
|
|
4000
|
+
import { join as join31 } from "node:path";
|
|
3891
4001
|
|
|
3892
4002
|
// src/commands.doctor.checks.hooks.preserve-symlinks.probe.ts
|
|
3893
|
-
import { closeSync as closeSync3, existsSync as
|
|
3894
|
-
import { dirname as dirname6, join as
|
|
4003
|
+
import { closeSync as closeSync3, existsSync as existsSync25, openSync as openSync3, readSync, realpathSync as realpathSync3 } from "node:fs";
|
|
4004
|
+
import { dirname as dirname6, join as join30, resolve as resolve2 } from "node:path";
|
|
3895
4005
|
function suppressedRanges(src) {
|
|
3896
4006
|
const ranges = [];
|
|
3897
4007
|
const re = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'[^']*'|"[^"]*"|`[^`]*`/g;
|
|
@@ -3923,12 +4033,12 @@ function topRelativeSpecifiers(src) {
|
|
|
3923
4033
|
}
|
|
3924
4034
|
function specifierIsMissing(specifier, baseDir) {
|
|
3925
4035
|
const base = resolve2(baseDir, specifier);
|
|
3926
|
-
if (
|
|
4036
|
+
if (existsSync25(base)) return false;
|
|
3927
4037
|
for (const ext of [".js", ".cjs", ".mjs"]) {
|
|
3928
|
-
if (
|
|
4038
|
+
if (existsSync25(base + ext)) return false;
|
|
3929
4039
|
}
|
|
3930
4040
|
for (const idx of ["index.js", "index.cjs", "index.mjs"]) {
|
|
3931
|
-
if (
|
|
4041
|
+
if (existsSync25(join30(base, idx))) return false;
|
|
3932
4042
|
}
|
|
3933
4043
|
return true;
|
|
3934
4044
|
}
|
|
@@ -3983,8 +4093,8 @@ function commandTokens(command) {
|
|
|
3983
4093
|
return tokens;
|
|
3984
4094
|
}
|
|
3985
4095
|
function readPathMapSafe() {
|
|
3986
|
-
const mapPath =
|
|
3987
|
-
if (!
|
|
4096
|
+
const mapPath = join31(repoHome(), "path-map.json");
|
|
4097
|
+
if (!existsSync26(mapPath)) return { projects: {} };
|
|
3988
4098
|
try {
|
|
3989
4099
|
return JSON.parse(readFileSync9(mapPath, "utf8"));
|
|
3990
4100
|
} catch {
|
|
@@ -3992,9 +4102,10 @@ function readPathMapSafe() {
|
|
|
3992
4102
|
}
|
|
3993
4103
|
}
|
|
3994
4104
|
function resolvesUnderSymlinkedShared(scriptPath, sharedLinkNames) {
|
|
4105
|
+
const normalizedScript = scriptPath.replaceAll("\\", "/");
|
|
3995
4106
|
for (const name of sharedLinkNames) {
|
|
3996
|
-
const prefix = `${claudeHome()}/${name}/`;
|
|
3997
|
-
if (
|
|
4107
|
+
const prefix = `${claudeHome().replaceAll("\\", "/")}/${name}/`;
|
|
4108
|
+
if (normalizedScript.startsWith(prefix)) return true;
|
|
3998
4109
|
}
|
|
3999
4110
|
return false;
|
|
4000
4111
|
}
|
|
@@ -4057,8 +4168,8 @@ function checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames)
|
|
|
4057
4168
|
return anyWarn;
|
|
4058
4169
|
}
|
|
4059
4170
|
function reportPreserveSymlinksCheck(section2) {
|
|
4060
|
-
const settingsPath =
|
|
4061
|
-
if (!
|
|
4171
|
+
const settingsPath = join31(claudeHome(), "settings.json");
|
|
4172
|
+
if (!existsSync26(settingsPath)) {
|
|
4062
4173
|
addItem(
|
|
4063
4174
|
section2,
|
|
4064
4175
|
`${dim(infoGlyph)} no ~/.claude/settings.json; skipping preserve-symlinks-main check`
|
|
@@ -4091,8 +4202,8 @@ function reportPreserveSymlinksCheck(section2) {
|
|
|
4091
4202
|
|
|
4092
4203
|
// src/commands.doctor.checks.settings-drift.ts
|
|
4093
4204
|
init_color();
|
|
4094
|
-
import { existsSync as
|
|
4095
|
-
import { join as
|
|
4205
|
+
import { existsSync as existsSync27, readFileSync as readFileSync10 } from "node:fs";
|
|
4206
|
+
import { join as join32 } from "node:path";
|
|
4096
4207
|
init_config();
|
|
4097
4208
|
init_utils_json();
|
|
4098
4209
|
function diffMergedSettings(merged, settings) {
|
|
@@ -4110,7 +4221,7 @@ function tryReadJson(filePath) {
|
|
|
4110
4221
|
}
|
|
4111
4222
|
}
|
|
4112
4223
|
function reportHooksBaseSelfCleanNote(section2) {
|
|
4113
|
-
const basePath =
|
|
4224
|
+
const basePath = join32(repoHome(), "shared", "settings.base.json");
|
|
4114
4225
|
const base = tryReadJson(basePath);
|
|
4115
4226
|
if (base === null) return;
|
|
4116
4227
|
if (!baseHasGsdHookEntries(base)) return;
|
|
@@ -4123,14 +4234,14 @@ function reportSettingsDriftCheck(section2) {
|
|
|
4123
4234
|
const claude = claudeHome();
|
|
4124
4235
|
const repo = repoHome();
|
|
4125
4236
|
const host = HOST;
|
|
4126
|
-
const settingsPath =
|
|
4127
|
-
const basePath =
|
|
4128
|
-
const hostPath =
|
|
4129
|
-
if (!
|
|
4237
|
+
const settingsPath = join32(claude, "settings.json");
|
|
4238
|
+
const basePath = join32(repo, "shared", "settings.base.json");
|
|
4239
|
+
const hostPath = join32(repo, "hosts", `${host}.json`);
|
|
4240
|
+
if (!existsSync27(settingsPath)) {
|
|
4130
4241
|
addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping merge-drift check`);
|
|
4131
4242
|
return;
|
|
4132
4243
|
}
|
|
4133
|
-
if (!
|
|
4244
|
+
if (!existsSync27(basePath)) {
|
|
4134
4245
|
addItem(
|
|
4135
4246
|
section2,
|
|
4136
4247
|
`${dim(infoGlyph)} shared/settings.base.json missing; skipping merge-drift check`
|
|
@@ -4149,7 +4260,7 @@ function reportSettingsDriftCheck(section2) {
|
|
|
4149
4260
|
if (settings === null) {
|
|
4150
4261
|
return;
|
|
4151
4262
|
}
|
|
4152
|
-
const hostExists =
|
|
4263
|
+
const hostExists = existsSync27(hostPath);
|
|
4153
4264
|
const hostObj = hostExists ? tryReadJson(hostPath) : null;
|
|
4154
4265
|
if (hostExists && hostObj === null) {
|
|
4155
4266
|
addItem(
|
|
@@ -4305,46 +4416,152 @@ function reportNodeEngineCheck(section2) {
|
|
|
4305
4416
|
addItem(section2, `${green(okGlyph)} node: ${process.version}`);
|
|
4306
4417
|
}
|
|
4307
4418
|
|
|
4419
|
+
// src/commands.doctor.checks.longpaths.ts
|
|
4420
|
+
init_color();
|
|
4421
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
4422
|
+
init_config();
|
|
4423
|
+
var PROBE_TIMEOUT_MS = 3e3;
|
|
4424
|
+
var LONGPATHS_REG_KEY = String.raw`HKLM\SYSTEM\CurrentControlSet\Control\FileSystem`;
|
|
4425
|
+
var LONGPATHS_REG_VALUE = "LongPathsEnabled";
|
|
4426
|
+
function probeGitLongpaths(run) {
|
|
4427
|
+
try {
|
|
4428
|
+
const out = run("git", ["-C", repoHome(), "config", "--get", "core.longpaths"], {
|
|
4429
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4430
|
+
timeout: PROBE_TIMEOUT_MS
|
|
4431
|
+
}).toString().trim();
|
|
4432
|
+
return out === "true" || out === "1";
|
|
4433
|
+
} catch {
|
|
4434
|
+
return false;
|
|
4435
|
+
}
|
|
4436
|
+
}
|
|
4437
|
+
function probeRegistryLongpaths(run) {
|
|
4438
|
+
try {
|
|
4439
|
+
const out = run("reg", ["query", LONGPATHS_REG_KEY, "/v", LONGPATHS_REG_VALUE], {
|
|
4440
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4441
|
+
timeout: PROBE_TIMEOUT_MS
|
|
4442
|
+
}).toString();
|
|
4443
|
+
return /0x1\b/.test(out);
|
|
4444
|
+
} catch {
|
|
4445
|
+
return false;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
function addLongpathsRow(section2, label, enabled2, remediation) {
|
|
4449
|
+
if (enabled2) {
|
|
4450
|
+
addItem(section2, `${green(okGlyph)} ${label}: enabled`);
|
|
4451
|
+
return;
|
|
4452
|
+
}
|
|
4453
|
+
addItem(section2, `${yellow(warnGlyph)} ${label}: not enabled (${remediation})`);
|
|
4454
|
+
}
|
|
4455
|
+
function reportLongPathsCheck(section2, run = execFileSync10) {
|
|
4456
|
+
if (process.platform !== "win32") return;
|
|
4457
|
+
addLongpathsRow(
|
|
4458
|
+
section2,
|
|
4459
|
+
"git core.longpaths",
|
|
4460
|
+
probeGitLongpaths(run),
|
|
4461
|
+
"run `git config --global core.longpaths true`"
|
|
4462
|
+
);
|
|
4463
|
+
addLongpathsRow(
|
|
4464
|
+
section2,
|
|
4465
|
+
"OS long paths",
|
|
4466
|
+
probeRegistryLongpaths(run),
|
|
4467
|
+
"enable LongPathsEnabled in Local Group Policy or the registry (admin required)"
|
|
4468
|
+
);
|
|
4469
|
+
}
|
|
4470
|
+
function reportSyncModality(section2) {
|
|
4471
|
+
const modality = process.platform === "win32" ? "copy-sync (win32)" : "symlink (posix)";
|
|
4472
|
+
addItem(section2, `${dim(infoGlyph)} sync modality: ${modality}`);
|
|
4473
|
+
}
|
|
4474
|
+
|
|
4475
|
+
// src/commands.doctor.checks.crlf.ts
|
|
4476
|
+
init_color();
|
|
4477
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
4478
|
+
import { existsSync as existsSync28, readFileSync as readFileSync13 } from "node:fs";
|
|
4479
|
+
import { join as join33 } from "node:path";
|
|
4480
|
+
init_config();
|
|
4481
|
+
var PROBE_TIMEOUT_MS2 = 3e3;
|
|
4482
|
+
var GUARD_LINE = /^\*\s+-text\b/;
|
|
4483
|
+
function hasGitattributesGuard(repo) {
|
|
4484
|
+
try {
|
|
4485
|
+
const path = join33(repo, ".gitattributes");
|
|
4486
|
+
if (!existsSync28(path)) return false;
|
|
4487
|
+
const content = readFileSync13(path, "utf8");
|
|
4488
|
+
return content.split("\n").map((line) => line.trim()).some((line) => line !== "" && !line.startsWith("#") && GUARD_LINE.test(line));
|
|
4489
|
+
} catch {
|
|
4490
|
+
return false;
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
function probeAutocrlf(repo, run) {
|
|
4494
|
+
try {
|
|
4495
|
+
const out = run("git", ["-C", repo, "config", "core.autocrlf"], {
|
|
4496
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4497
|
+
timeout: PROBE_TIMEOUT_MS2
|
|
4498
|
+
}).toString().trim();
|
|
4499
|
+
if (out === "true" || out === "input") return "active";
|
|
4500
|
+
if (out === "false") return "disabled";
|
|
4501
|
+
return "unset";
|
|
4502
|
+
} catch {
|
|
4503
|
+
return "unset";
|
|
4504
|
+
}
|
|
4505
|
+
}
|
|
4506
|
+
var AUTOCRLF_RISK = {
|
|
4507
|
+
active: "core.autocrlf is actively converting line endings on checkout",
|
|
4508
|
+
disabled: "guarded on this host by core.autocrlf=false, but no .gitattributes to protect other hosts",
|
|
4509
|
+
unset: "no .gitattributes guard and core.autocrlf is unset (latent risk)"
|
|
4510
|
+
};
|
|
4511
|
+
function addExposedRow(section2, repo, verdict) {
|
|
4512
|
+
const risk = AUTOCRLF_RISK[verdict];
|
|
4513
|
+
const remediation = `add a .gitattributes with a \`* -text\` line, or run \`git config core.autocrlf false\`, in ${repo}`;
|
|
4514
|
+
addItem(section2, `${yellow(warnGlyph)} CRLF guard: ${risk}; ${remediation}`);
|
|
4515
|
+
}
|
|
4516
|
+
function reportCrlfGuardCheck(section2, run = execFileSync11) {
|
|
4517
|
+
const repo = repoHome();
|
|
4518
|
+
if (hasGitattributesGuard(repo)) {
|
|
4519
|
+
addItem(section2, `${green(okGlyph)} CRLF guard: .gitattributes (* -text) present`);
|
|
4520
|
+
return;
|
|
4521
|
+
}
|
|
4522
|
+
addExposedRow(section2, repo, probeAutocrlf(repo, run));
|
|
4523
|
+
}
|
|
4524
|
+
|
|
4308
4525
|
// src/spinner.ts
|
|
4309
4526
|
init_color();
|
|
4310
|
-
import { existsSync as
|
|
4527
|
+
import { existsSync as existsSync32 } from "node:fs";
|
|
4311
4528
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4312
4529
|
import { Worker } from "node:worker_threads";
|
|
4313
4530
|
|
|
4314
4531
|
// src/commands.push.recovery.ts
|
|
4315
4532
|
init_config();
|
|
4316
|
-
import { readFileSync as
|
|
4317
|
-
import { join as
|
|
4533
|
+
import { readFileSync as readFileSync16, rmSync as rmSync12, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4534
|
+
import { join as join38 } from "node:path";
|
|
4318
4535
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
4319
4536
|
|
|
4320
4537
|
// src/commands.push.recovery.actions.ts
|
|
4321
4538
|
init_config();
|
|
4322
|
-
import { readFileSync as
|
|
4323
|
-
import { isAbsolute as isAbsolute2, resolve as resolve3, sep as
|
|
4539
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
4540
|
+
import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep6 } from "node:path";
|
|
4324
4541
|
|
|
4325
4542
|
// src/commands.push.recovery.redact.ts
|
|
4326
4543
|
init_config();
|
|
4327
4544
|
init_config_sharedDirs_guard();
|
|
4328
|
-
import { cpSync as
|
|
4329
|
-
import { dirname as dirname8, join as
|
|
4545
|
+
import { cpSync as cpSync7, existsSync as existsSync31, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
|
|
4546
|
+
import { dirname as dirname8, join as join36, sep as sep5 } from "node:path";
|
|
4330
4547
|
|
|
4331
4548
|
// src/commands.redact.ts
|
|
4332
4549
|
init_config();
|
|
4333
|
-
import { existsSync as
|
|
4334
|
-
import { dirname as dirname7, join as
|
|
4550
|
+
import { existsSync as existsSync30, statSync as statSync7 } from "node:fs";
|
|
4551
|
+
import { dirname as dirname7, join as join35 } from "node:path";
|
|
4335
4552
|
|
|
4336
4553
|
// src/commands.redact.subtree.ts
|
|
4337
|
-
import { existsSync as
|
|
4338
|
-
import { join as
|
|
4554
|
+
import { existsSync as existsSync29, lstatSync as lstatSync11, readFileSync as readFileSync14, readdirSync as readdirSync13, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4555
|
+
import { join as join34 } from "node:path";
|
|
4339
4556
|
init_utils_fs();
|
|
4340
4557
|
init_utils();
|
|
4341
4558
|
function collectFiles(dir, out) {
|
|
4342
|
-
if (!
|
|
4343
|
-
const st =
|
|
4559
|
+
if (!existsSync29(dir)) return;
|
|
4560
|
+
const st = lstatSync11(dir);
|
|
4344
4561
|
if (!st.isDirectory()) return;
|
|
4345
|
-
for (const entry of
|
|
4346
|
-
const abs =
|
|
4347
|
-
const lst =
|
|
4562
|
+
for (const entry of readdirSync13(dir)) {
|
|
4563
|
+
const abs = join34(dir, entry);
|
|
4564
|
+
const lst = lstatSync11(abs);
|
|
4348
4565
|
if (lst.isSymbolicLink()) continue;
|
|
4349
4566
|
if (lst.isDirectory()) {
|
|
4350
4567
|
collectFiles(abs, out);
|
|
@@ -4380,7 +4597,7 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
|
|
|
4380
4597
|
if (!dryRun && total > 0) {
|
|
4381
4598
|
for (const { path: filePath, findings } of dirty) {
|
|
4382
4599
|
backupBeforeWrite(filePath, ts);
|
|
4383
|
-
const before =
|
|
4600
|
+
const before = readFileSync14(filePath, "utf8");
|
|
4384
4601
|
const after = applyRedactions(before, findings);
|
|
4385
4602
|
if (after === before) {
|
|
4386
4603
|
log(
|
|
@@ -4395,10 +4612,10 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
|
|
|
4395
4612
|
|
|
4396
4613
|
// src/commands.pushed-history.ts
|
|
4397
4614
|
init_utils();
|
|
4398
|
-
import { execFileSync as
|
|
4615
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
4399
4616
|
function pushedRef(repo) {
|
|
4400
4617
|
try {
|
|
4401
|
-
const ref =
|
|
4618
|
+
const ref = execFileSync12("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
|
|
4402
4619
|
cwd: repo,
|
|
4403
4620
|
stdio: ["ignore", "pipe", "ignore"]
|
|
4404
4621
|
}).toString().trim();
|
|
@@ -4411,7 +4628,7 @@ function sessionInPushedHistory(id, repo) {
|
|
|
4411
4628
|
const ref = pushedRef(repo);
|
|
4412
4629
|
if (ref === null) return false;
|
|
4413
4630
|
try {
|
|
4414
|
-
const out =
|
|
4631
|
+
const out = execFileSync12(
|
|
4415
4632
|
"git",
|
|
4416
4633
|
[
|
|
4417
4634
|
"log",
|
|
@@ -4448,15 +4665,15 @@ init_utils_json();
|
|
|
4448
4665
|
init_utils();
|
|
4449
4666
|
function resolveLiveTranscript(id) {
|
|
4450
4667
|
try {
|
|
4451
|
-
const mapPath =
|
|
4452
|
-
if (!
|
|
4668
|
+
const mapPath = join35(repoHome(), "path-map.json");
|
|
4669
|
+
if (!existsSync30(mapPath)) return null;
|
|
4453
4670
|
const projects = readJson(mapPath).projects;
|
|
4454
4671
|
const claude = claudeHome();
|
|
4455
4672
|
for (const hostMap of Object.values(projects)) {
|
|
4456
4673
|
const abs = hostMap[HOST];
|
|
4457
4674
|
if (abs === void 0) continue;
|
|
4458
|
-
const live =
|
|
4459
|
-
if (
|
|
4675
|
+
const live = join35(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
4676
|
+
if (existsSync30(live)) return live;
|
|
4460
4677
|
}
|
|
4461
4678
|
return null;
|
|
4462
4679
|
} catch {
|
|
@@ -4476,17 +4693,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
|
|
|
4476
4693
|
}
|
|
4477
4694
|
const repo = repoHome();
|
|
4478
4695
|
const backup = backupBase();
|
|
4479
|
-
if (!
|
|
4696
|
+
if (!existsSync30(repo)) die(`repo not cloned at ${repo}`);
|
|
4480
4697
|
const handle = acquireLock("redact");
|
|
4481
4698
|
if (handle === null) process.exit(0);
|
|
4482
4699
|
try {
|
|
4483
4700
|
const localPath = resolveLiveTranscript(id);
|
|
4484
|
-
if (localPath === null || !
|
|
4701
|
+
if (localPath === null || !existsSync30(localPath)) {
|
|
4485
4702
|
fail(`could not resolve local transcript for session ${id} on this host`);
|
|
4486
4703
|
process.exitCode = 1;
|
|
4487
4704
|
return;
|
|
4488
4705
|
}
|
|
4489
|
-
const sessionDir =
|
|
4706
|
+
const sessionDir = join35(dirname7(localPath), id);
|
|
4490
4707
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
4491
4708
|
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
|
|
4492
4709
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
@@ -4602,8 +4819,8 @@ function resolveStagedDir(localPath, map, claude, repo) {
|
|
|
4602
4819
|
assertSafeLogical(logical);
|
|
4603
4820
|
const abs = hostMap[HOST];
|
|
4604
4821
|
if (abs === void 0) continue;
|
|
4605
|
-
if (localPath.startsWith(
|
|
4606
|
-
return
|
|
4822
|
+
if (localPath.startsWith(join36(claude, "projects", encodePath(abs)) + sep5)) {
|
|
4823
|
+
return join36(repo, "shared", "projects", logical);
|
|
4607
4824
|
}
|
|
4608
4825
|
}
|
|
4609
4826
|
return null;
|
|
@@ -4613,7 +4830,7 @@ function preflightRedactable(f, map, nowMs) {
|
|
|
4613
4830
|
if (sid === null) return "a finding has no resolvable session id (not a session transcript)";
|
|
4614
4831
|
const localPath = resolveLiveTranscript(sid);
|
|
4615
4832
|
if (localPath === null) return `session ${sid}: local transcript not found`;
|
|
4616
|
-
const sessionDir =
|
|
4833
|
+
const sessionDir = join36(dirname8(localPath), sid);
|
|
4617
4834
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
4618
4835
|
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
|
|
4619
4836
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
@@ -4643,7 +4860,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
4643
4860
|
`could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
4644
4861
|
);
|
|
4645
4862
|
}
|
|
4646
|
-
const sessionDir =
|
|
4863
|
+
const sessionDir = join36(dirname8(localPath), sid);
|
|
4647
4864
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
4648
4865
|
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
|
|
4649
4866
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
@@ -4677,26 +4894,26 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
4677
4894
|
);
|
|
4678
4895
|
}
|
|
4679
4896
|
mkdirSync7(stagedProjectDir, { recursive: true });
|
|
4680
|
-
|
|
4681
|
-
if (
|
|
4682
|
-
|
|
4897
|
+
cpSync7(localPath, join36(stagedProjectDir, `${sid}.jsonl`), { force: true });
|
|
4898
|
+
if (existsSync31(sessionDir)) {
|
|
4899
|
+
cpSync7(sessionDir, join36(stagedProjectDir, sid), { force: true, recursive: true });
|
|
4683
4900
|
}
|
|
4684
4901
|
return true;
|
|
4685
4902
|
}
|
|
4686
4903
|
|
|
4687
4904
|
// src/commands.push.recovery.drop.ts
|
|
4688
4905
|
init_config();
|
|
4689
|
-
import { rmSync as
|
|
4690
|
-
import { join as
|
|
4906
|
+
import { rmSync as rmSync11 } from "node:fs";
|
|
4907
|
+
import { join as join37 } from "node:path";
|
|
4691
4908
|
function dropSessionFromStaged(sid, map) {
|
|
4692
4909
|
const logicals = Object.keys(map.projects);
|
|
4693
4910
|
if (logicals.length === 0) return false;
|
|
4694
4911
|
const repo = repoHome();
|
|
4695
4912
|
for (const logical of logicals) {
|
|
4696
|
-
const jsonl =
|
|
4697
|
-
const dir =
|
|
4698
|
-
|
|
4699
|
-
|
|
4913
|
+
const jsonl = join37(repo, "shared", "projects", logical, `${sid}.jsonl`);
|
|
4914
|
+
const dir = join37(repo, "shared", "projects", logical, sid);
|
|
4915
|
+
rmSync11(jsonl, { force: true });
|
|
4916
|
+
rmSync11(dir, { recursive: true, force: true });
|
|
4700
4917
|
}
|
|
4701
4918
|
return true;
|
|
4702
4919
|
}
|
|
@@ -4727,10 +4944,10 @@ function makeDefaultReadLine(repo) {
|
|
|
4727
4944
|
try {
|
|
4728
4945
|
const repoRoot = resolve3(repo);
|
|
4729
4946
|
const target = resolve3(repoRoot, file);
|
|
4730
|
-
if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot +
|
|
4947
|
+
if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep6)) {
|
|
4731
4948
|
return null;
|
|
4732
4949
|
}
|
|
4733
|
-
const content =
|
|
4950
|
+
const content = readFileSync15(target, "utf8");
|
|
4734
4951
|
const lines = content.split(/\r?\n/);
|
|
4735
4952
|
const idx = line - 1;
|
|
4736
4953
|
if (idx < 0 || idx >= lines.length) return null;
|
|
@@ -4851,10 +5068,10 @@ function applyThenRescan(scanVerdict, repoHome2) {
|
|
|
4851
5068
|
return next;
|
|
4852
5069
|
}
|
|
4853
5070
|
function allowThenRescan(append, scanVerdict, repoHome2) {
|
|
4854
|
-
const ignPath =
|
|
5071
|
+
const ignPath = join38(repoHome2, ".gitleaksignore");
|
|
4855
5072
|
let before;
|
|
4856
5073
|
try {
|
|
4857
|
-
before =
|
|
5074
|
+
before = readFileSync16(ignPath, "utf8");
|
|
4858
5075
|
} catch {
|
|
4859
5076
|
before = null;
|
|
4860
5077
|
}
|
|
@@ -4862,7 +5079,7 @@ function allowThenRescan(append, scanVerdict, repoHome2) {
|
|
|
4862
5079
|
try {
|
|
4863
5080
|
return applyThenRescan(scanVerdict, repoHome2);
|
|
4864
5081
|
} catch (err) {
|
|
4865
|
-
if (before === null)
|
|
5082
|
+
if (before === null) rmSync12(ignPath, { force: true });
|
|
4866
5083
|
else writeFileSync5(ignPath, before, "utf8");
|
|
4867
5084
|
throw err;
|
|
4868
5085
|
}
|
|
@@ -4950,7 +5167,7 @@ function writeAnimatedDone(out, label, ms, useTTY) {
|
|
|
4950
5167
|
`);
|
|
4951
5168
|
}
|
|
4952
5169
|
function resolveWorkerPath(deps = {}) {
|
|
4953
|
-
const check = deps.existsSyncFn ??
|
|
5170
|
+
const check = deps.existsSyncFn ?? existsSync32;
|
|
4954
5171
|
const base = deps.baseUrl ?? import.meta.url;
|
|
4955
5172
|
const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
|
|
4956
5173
|
if (check(mjs)) return mjs;
|
|
@@ -5016,9 +5233,9 @@ function withSpinner(label, fn, deps) {
|
|
|
5016
5233
|
|
|
5017
5234
|
// src/commands.doctor.gitleaks-version.ts
|
|
5018
5235
|
init_color();
|
|
5019
|
-
import { execFileSync as
|
|
5020
|
-
import { existsSync as
|
|
5021
|
-
import { join as
|
|
5236
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
5237
|
+
import { existsSync as existsSync33 } from "node:fs";
|
|
5238
|
+
import { join as join39 } from "node:path";
|
|
5022
5239
|
init_config();
|
|
5023
5240
|
var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
|
|
5024
5241
|
var GITLEAKS_TIMEOUT_MS = 5e3;
|
|
@@ -5027,7 +5244,7 @@ function majorMinorOf(value) {
|
|
|
5027
5244
|
return m === null ? null : [m[1], m[2]];
|
|
5028
5245
|
}
|
|
5029
5246
|
function readGitleaksVersion(run, tomlExists) {
|
|
5030
|
-
const tomlPath =
|
|
5247
|
+
const tomlPath = join39(repoHome(), ".gitleaks.toml");
|
|
5031
5248
|
const args = ["version"];
|
|
5032
5249
|
if (tomlExists(tomlPath)) args.push("--config", tomlPath);
|
|
5033
5250
|
try {
|
|
@@ -5039,7 +5256,7 @@ function readGitleaksVersion(run, tomlExists) {
|
|
|
5039
5256
|
return null;
|
|
5040
5257
|
}
|
|
5041
5258
|
}
|
|
5042
|
-
function reportGitleaksVersionCheck(section2, run =
|
|
5259
|
+
function reportGitleaksVersionCheck(section2, run = execFileSync13, tomlExists = existsSync33) {
|
|
5043
5260
|
const raw = readGitleaksVersion(run, tomlExists);
|
|
5044
5261
|
if (raw === null) return;
|
|
5045
5262
|
const local = majorMinorOf(raw);
|
|
@@ -5059,9 +5276,9 @@ function reportGitleaksVersionCheck(section2, run = execFileSync11, tomlExists =
|
|
|
5059
5276
|
|
|
5060
5277
|
// src/commands.doctor.checks.deps.ts
|
|
5061
5278
|
init_color();
|
|
5062
|
-
import { execFileSync as
|
|
5279
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
5063
5280
|
var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
|
|
5064
|
-
var
|
|
5281
|
+
var PROBE_TIMEOUT_MS3 = 3e3;
|
|
5065
5282
|
var FETCHER_BASE = "HTTP fetcher";
|
|
5066
5283
|
function parseFirstVersion(line) {
|
|
5067
5284
|
const m = VERSION_TOKEN.exec(line);
|
|
@@ -5071,7 +5288,7 @@ function probeOptionalDep(bin, run) {
|
|
|
5071
5288
|
try {
|
|
5072
5289
|
const firstLine = run(bin, ["--version"], {
|
|
5073
5290
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5074
|
-
timeout:
|
|
5291
|
+
timeout: PROBE_TIMEOUT_MS3
|
|
5075
5292
|
}).toString().split("\n")[0].trim();
|
|
5076
5293
|
const version = parseFirstVersion(firstLine);
|
|
5077
5294
|
return { status: "present", version };
|
|
@@ -5096,7 +5313,7 @@ function reportFetcherRow(section2, run) {
|
|
|
5096
5313
|
);
|
|
5097
5314
|
}
|
|
5098
5315
|
}
|
|
5099
|
-
function reportOptionalDeps(section2, run =
|
|
5316
|
+
function reportOptionalDeps(section2, run = execFileSync14) {
|
|
5100
5317
|
const gh = probeOptionalDep("gh", run);
|
|
5101
5318
|
if (gh.status === "present") {
|
|
5102
5319
|
addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
|
|
@@ -5111,11 +5328,11 @@ function reportOptionalDeps(section2, run = execFileSync12) {
|
|
|
5111
5328
|
|
|
5112
5329
|
// src/commands.doctor.actions-drift.ts
|
|
5113
5330
|
init_color();
|
|
5114
|
-
import { execFileSync as
|
|
5331
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
5115
5332
|
init_config();
|
|
5116
5333
|
|
|
5117
5334
|
// src/gh-actions.ts
|
|
5118
|
-
import { execFileSync as
|
|
5335
|
+
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
5119
5336
|
var GH_TIMEOUT_MS = 5e3;
|
|
5120
5337
|
function parseGitHubRemote(remoteUrl) {
|
|
5121
5338
|
const normalized = remoteUrl.trim().replace(/\/$/, "");
|
|
@@ -5123,7 +5340,7 @@ function parseGitHubRemote(remoteUrl) {
|
|
|
5123
5340
|
if (m === null) return null;
|
|
5124
5341
|
return { owner: m[1], repo: m[2] };
|
|
5125
5342
|
}
|
|
5126
|
-
function ghAuthStatus(run =
|
|
5343
|
+
function ghAuthStatus(run = execFileSync15) {
|
|
5127
5344
|
try {
|
|
5128
5345
|
run("gh", ["auth", "status"], {
|
|
5129
5346
|
stdio: ["ignore", "ignore", "ignore"],
|
|
@@ -5137,7 +5354,7 @@ function ghAuthStatus(run = execFileSync13) {
|
|
|
5137
5354
|
return "gh-probe-error";
|
|
5138
5355
|
}
|
|
5139
5356
|
}
|
|
5140
|
-
function isRepoPrivate(ref, run =
|
|
5357
|
+
function isRepoPrivate(ref, run = execFileSync15) {
|
|
5141
5358
|
const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
|
|
5142
5359
|
stdio: ["ignore", "pipe", "ignore"],
|
|
5143
5360
|
timeout: GH_TIMEOUT_MS
|
|
@@ -5145,7 +5362,7 @@ function isRepoPrivate(ref, run = execFileSync13) {
|
|
|
5145
5362
|
const parsed = JSON.parse(out);
|
|
5146
5363
|
return parsed.isPrivate === true;
|
|
5147
5364
|
}
|
|
5148
|
-
function isActionsEnabled(ref, run =
|
|
5365
|
+
function isActionsEnabled(ref, run = execFileSync15) {
|
|
5149
5366
|
const out = run(
|
|
5150
5367
|
"gh",
|
|
5151
5368
|
["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
|
|
@@ -5153,7 +5370,7 @@ function isActionsEnabled(ref, run = execFileSync13) {
|
|
|
5153
5370
|
).toString().trim();
|
|
5154
5371
|
return out === "true";
|
|
5155
5372
|
}
|
|
5156
|
-
function disableActions(ref, run =
|
|
5373
|
+
function disableActions(ref, run = execFileSync15) {
|
|
5157
5374
|
run(
|
|
5158
5375
|
"gh",
|
|
5159
5376
|
[
|
|
@@ -5167,7 +5384,7 @@ function disableActions(ref, run = execFileSync13) {
|
|
|
5167
5384
|
{ stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
|
|
5168
5385
|
);
|
|
5169
5386
|
}
|
|
5170
|
-
function readOriginRemote(cwd, run =
|
|
5387
|
+
function readOriginRemote(cwd, run = execFileSync15) {
|
|
5171
5388
|
return run("git", ["remote", "get-url", "origin"], {
|
|
5172
5389
|
cwd,
|
|
5173
5390
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -5175,7 +5392,7 @@ function readOriginRemote(cwd, run = execFileSync13) {
|
|
|
5175
5392
|
}
|
|
5176
5393
|
|
|
5177
5394
|
// src/commands.doctor.actions-drift.ts
|
|
5178
|
-
function reportActionsDrift(section2, run =
|
|
5395
|
+
function reportActionsDrift(section2, run = execFileSync16) {
|
|
5179
5396
|
let remote;
|
|
5180
5397
|
try {
|
|
5181
5398
|
remote = readOriginRemote(repoHome(), run);
|
|
@@ -5261,9 +5478,12 @@ function gatherDoctorSections(opts) {
|
|
|
5261
5478
|
reportHostAndPaths(host);
|
|
5262
5479
|
reportHostKeyAlignment(host);
|
|
5263
5480
|
reportRepoState(host);
|
|
5481
|
+
reportSyncModality(host);
|
|
5482
|
+
reportLongPathsCheck(host);
|
|
5483
|
+
reportCrlfGuardCheck(host);
|
|
5264
5484
|
const links = section("Shared links");
|
|
5265
|
-
const mapPath =
|
|
5266
|
-
const rawMap =
|
|
5485
|
+
const mapPath = join40(repoHome(), "path-map.json");
|
|
5486
|
+
const rawMap = existsSync34(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
|
|
5267
5487
|
const map = rawMap ?? { projects: {} };
|
|
5268
5488
|
reportSharedLinks(links, map);
|
|
5269
5489
|
reportDroppedNamesMigration(links);
|
|
@@ -5359,15 +5579,15 @@ function parseDoctorArgs(args) {
|
|
|
5359
5579
|
|
|
5360
5580
|
// src/commands.drop-session.ts
|
|
5361
5581
|
init_config();
|
|
5362
|
-
import { execFileSync as
|
|
5363
|
-
import { existsSync as
|
|
5364
|
-
import { join as
|
|
5582
|
+
import { execFileSync as execFileSync18 } from "node:child_process";
|
|
5583
|
+
import { existsSync as existsSync36, readdirSync as readdirSync14, statSync as statSync9 } from "node:fs";
|
|
5584
|
+
import { join as join42, relative as relative6 } from "node:path";
|
|
5365
5585
|
|
|
5366
5586
|
// src/commands.drop-session.git.ts
|
|
5367
|
-
import { execFileSync as
|
|
5587
|
+
import { execFileSync as execFileSync17 } from "node:child_process";
|
|
5368
5588
|
function expandStagedDir(dirRel, repo) {
|
|
5369
5589
|
try {
|
|
5370
|
-
const out =
|
|
5590
|
+
const out = execFileSync17("git", ["ls-files", "-z", "--", dirRel], {
|
|
5371
5591
|
cwd: repo,
|
|
5372
5592
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5373
5593
|
});
|
|
@@ -5378,7 +5598,8 @@ function expandStagedDir(dirRel, repo) {
|
|
|
5378
5598
|
}
|
|
5379
5599
|
function isTrackedInHead(rel, repo) {
|
|
5380
5600
|
try {
|
|
5381
|
-
|
|
5601
|
+
const treePath = process.platform === "win32" ? rel.replaceAll("\\", "/") : rel;
|
|
5602
|
+
execFileSync17("git", ["cat-file", "-e", `HEAD:${treePath}`], {
|
|
5382
5603
|
cwd: repo,
|
|
5383
5604
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5384
5605
|
});
|
|
@@ -5389,7 +5610,7 @@ function isTrackedInHead(rel, repo) {
|
|
|
5389
5610
|
}
|
|
5390
5611
|
function isInIndex(rel, repo) {
|
|
5391
5612
|
try {
|
|
5392
|
-
const out =
|
|
5613
|
+
const out = execFileSync17("git", ["ls-files", "--", rel], {
|
|
5393
5614
|
cwd: repo,
|
|
5394
5615
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5395
5616
|
});
|
|
@@ -5403,8 +5624,8 @@ function isInIndex(rel, repo) {
|
|
|
5403
5624
|
init_config();
|
|
5404
5625
|
init_utils();
|
|
5405
5626
|
init_utils_json();
|
|
5406
|
-
import { existsSync as
|
|
5407
|
-
import { join as
|
|
5627
|
+
import { existsSync as existsSync35 } from "node:fs";
|
|
5628
|
+
import { join as join41 } from "node:path";
|
|
5408
5629
|
var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
|
|
5409
5630
|
function reportScrubHint(id, matches) {
|
|
5410
5631
|
const live = resolveLiveTranscript2(id, matches);
|
|
@@ -5420,17 +5641,17 @@ function reportScrubHint(id, matches) {
|
|
|
5420
5641
|
}
|
|
5421
5642
|
function resolveLiveTranscript2(id, matches) {
|
|
5422
5643
|
try {
|
|
5423
|
-
const mapPath =
|
|
5424
|
-
if (!
|
|
5644
|
+
const mapPath = join41(repoHome(), "path-map.json");
|
|
5645
|
+
if (!existsSync35(mapPath)) return null;
|
|
5425
5646
|
const projects = readJson(mapPath).projects;
|
|
5426
5647
|
const claude = claudeHome();
|
|
5427
5648
|
for (const rel of matches) {
|
|
5428
|
-
const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
|
|
5649
|
+
const logical = SHARED_PROJECT_LOGICAL.exec(rel.replaceAll("\\", "/"))?.[1];
|
|
5429
5650
|
if (logical === void 0) continue;
|
|
5430
5651
|
const abs = projects[logical]?.[HOST];
|
|
5431
5652
|
if (abs === void 0) continue;
|
|
5432
|
-
const live =
|
|
5433
|
-
if (
|
|
5653
|
+
const live = join41(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
5654
|
+
if (existsSync35(live)) return live;
|
|
5434
5655
|
}
|
|
5435
5656
|
return null;
|
|
5436
5657
|
} catch {
|
|
@@ -5446,12 +5667,12 @@ function cmdDropSession(id) {
|
|
|
5446
5667
|
process.exit(1);
|
|
5447
5668
|
}
|
|
5448
5669
|
const repo = repoHome();
|
|
5449
|
-
if (!
|
|
5670
|
+
if (!existsSync36(repo)) die(`repo not cloned at ${repo}`);
|
|
5450
5671
|
const handle = acquireLock("drop-session");
|
|
5451
5672
|
if (handle === null) process.exit(0);
|
|
5452
5673
|
try {
|
|
5453
|
-
const repoProjects =
|
|
5454
|
-
if (!
|
|
5674
|
+
const repoProjects = join42(repo, "shared", "projects");
|
|
5675
|
+
if (!existsSync36(repoProjects)) {
|
|
5455
5676
|
throw new NomadFatal(`no staged session matches ${id}`);
|
|
5456
5677
|
}
|
|
5457
5678
|
const matches = collectMatches(repoProjects, id, repo);
|
|
@@ -5473,13 +5694,13 @@ function cmdDropSession(id) {
|
|
|
5473
5694
|
}
|
|
5474
5695
|
function collectMatches(repoProjects, id, repo) {
|
|
5475
5696
|
const matches = [];
|
|
5476
|
-
for (const logical of
|
|
5477
|
-
const candidate =
|
|
5478
|
-
if (
|
|
5697
|
+
for (const logical of readdirSync14(repoProjects)) {
|
|
5698
|
+
const candidate = join42(repoProjects, logical, `${id}.jsonl`);
|
|
5699
|
+
if (existsSync36(candidate)) {
|
|
5479
5700
|
matches.push(relative6(repo, candidate));
|
|
5480
5701
|
}
|
|
5481
|
-
const dir =
|
|
5482
|
-
if (
|
|
5702
|
+
const dir = join42(repoProjects, logical, id);
|
|
5703
|
+
if (existsSync36(dir) && statSync9(dir).isDirectory()) {
|
|
5483
5704
|
const dirRel = relative6(repo, dir);
|
|
5484
5705
|
const staged = expandStagedDir(dirRel, repo);
|
|
5485
5706
|
if (staged.length > 0) matches.push(...staged);
|
|
@@ -5495,12 +5716,12 @@ function unstageOne(rel, repo) {
|
|
|
5495
5716
|
}
|
|
5496
5717
|
try {
|
|
5497
5718
|
if (isTrackedInHead(rel, repo)) {
|
|
5498
|
-
|
|
5719
|
+
execFileSync18("git", ["restore", "--staged", "--worktree", "--", rel], {
|
|
5499
5720
|
cwd: repo,
|
|
5500
5721
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5501
5722
|
});
|
|
5502
5723
|
} else {
|
|
5503
|
-
|
|
5724
|
+
execFileSync18("git", ["rm", "--cached", "-f", "--", rel], {
|
|
5504
5725
|
cwd: repo,
|
|
5505
5726
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5506
5727
|
});
|
|
@@ -5514,8 +5735,8 @@ function unstageOne(rel, repo) {
|
|
|
5514
5735
|
}
|
|
5515
5736
|
|
|
5516
5737
|
// src/commands.pull.ts
|
|
5517
|
-
import { existsSync as
|
|
5518
|
-
import { join as
|
|
5738
|
+
import { existsSync as existsSync41, mkdirSync as mkdirSync10 } from "node:fs";
|
|
5739
|
+
import { join as join48 } from "node:path";
|
|
5519
5740
|
|
|
5520
5741
|
// src/commands.push.sections.ts
|
|
5521
5742
|
init_color();
|
|
@@ -5617,18 +5838,18 @@ init_config();
|
|
|
5617
5838
|
|
|
5618
5839
|
// src/extras-sync.ts
|
|
5619
5840
|
init_config();
|
|
5620
|
-
import { existsSync as
|
|
5621
|
-
import { join as
|
|
5841
|
+
import { existsSync as existsSync38, statSync as statSync10 } from "node:fs";
|
|
5842
|
+
import { join as join45 } from "node:path";
|
|
5622
5843
|
init_utils();
|
|
5623
5844
|
init_utils_json();
|
|
5624
5845
|
|
|
5625
5846
|
// src/extras-sync.remap.ts
|
|
5626
5847
|
init_config();
|
|
5627
|
-
import { existsSync as
|
|
5628
|
-
import { dirname as dirname9, join as
|
|
5848
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync17, realpathSync as realpathSync4, rmSync as rmSync13 } from "node:fs";
|
|
5849
|
+
import { dirname as dirname9, join as join44, sep as sep8 } from "node:path";
|
|
5629
5850
|
|
|
5630
5851
|
// src/extras-sync.planning-diff.ts
|
|
5631
|
-
import { join as
|
|
5852
|
+
import { join as join43, normalize as normalize2, sep as sep7 } from "node:path";
|
|
5632
5853
|
init_utils();
|
|
5633
5854
|
function processRecord(fields, i, changed, deleted) {
|
|
5634
5855
|
const status = fields[i];
|
|
@@ -5680,15 +5901,15 @@ function planningDeleteTargets(opts) {
|
|
|
5680
5901
|
const { deleted } = parsePlanningDiff(raw);
|
|
5681
5902
|
const logicalPrefix = "shared/extras/" + logical + "/";
|
|
5682
5903
|
const prefix = logicalPrefix + ".planning/";
|
|
5683
|
-
const planningRoot =
|
|
5684
|
-
const planningRootBoundary = planningRoot +
|
|
5904
|
+
const planningRoot = join43(localRoot, ".planning");
|
|
5905
|
+
const planningRootBoundary = planningRoot + sep7;
|
|
5685
5906
|
const targets = [];
|
|
5686
5907
|
for (const repoPath of deleted) {
|
|
5687
5908
|
if (!repoPath.startsWith(prefix)) {
|
|
5688
5909
|
continue;
|
|
5689
5910
|
}
|
|
5690
5911
|
const remainder = repoPath.slice(logicalPrefix.length);
|
|
5691
|
-
const candidate =
|
|
5912
|
+
const candidate = join43(localRoot, remainder);
|
|
5692
5913
|
const resolved = normalize2(candidate);
|
|
5693
5914
|
if (!resolved.startsWith(planningRootBoundary)) {
|
|
5694
5915
|
throw new NomadFatal(
|
|
@@ -5709,7 +5930,7 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
|
|
|
5709
5930
|
const would = [];
|
|
5710
5931
|
for (const t of eachExtrasTarget(v, counts)) {
|
|
5711
5932
|
const { src, dst } = paths(t);
|
|
5712
|
-
if (!
|
|
5933
|
+
if (!existsSync37(src)) continue;
|
|
5713
5934
|
const item2 = `${t.logical}/${t.dirname}`;
|
|
5714
5935
|
if (dryRun) {
|
|
5715
5936
|
would.push(item2);
|
|
@@ -5723,10 +5944,10 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
|
|
|
5723
5944
|
}
|
|
5724
5945
|
function pruneEmptyAncestors(target, planningRoot) {
|
|
5725
5946
|
let dir = dirname9(target);
|
|
5726
|
-
while (dir !== planningRoot && dir.startsWith(planningRoot +
|
|
5947
|
+
while (dir !== planningRoot && dir.startsWith(planningRoot + sep8)) {
|
|
5727
5948
|
try {
|
|
5728
|
-
if (
|
|
5729
|
-
|
|
5949
|
+
if (readdirSync15(dir).length > 0) break;
|
|
5950
|
+
rmSync13(dir, { recursive: true, force: true });
|
|
5730
5951
|
} catch {
|
|
5731
5952
|
break;
|
|
5732
5953
|
}
|
|
@@ -5741,23 +5962,23 @@ function tryRealpath(dir) {
|
|
|
5741
5962
|
}
|
|
5742
5963
|
}
|
|
5743
5964
|
function isInsidePlanningRoot(parentReal, rootReal) {
|
|
5744
|
-
return parentReal === rootReal || parentReal.startsWith(rootReal +
|
|
5965
|
+
return parentReal === rootReal || parentReal.startsWith(rootReal + sep8);
|
|
5745
5966
|
}
|
|
5746
5967
|
function deletePlanningTarget(target, planningRoot, repoCounterpart) {
|
|
5747
|
-
if (
|
|
5968
|
+
if (existsSync37(repoCounterpart)) return;
|
|
5748
5969
|
const parentReal = tryRealpath(dirname9(target));
|
|
5749
5970
|
if (parentReal === void 0) return;
|
|
5750
5971
|
const rootReal = tryRealpath(planningRoot);
|
|
5751
5972
|
if (rootReal === void 0) return;
|
|
5752
5973
|
if (!isInsidePlanningRoot(parentReal, rootReal)) return;
|
|
5753
|
-
|
|
5974
|
+
rmSync13(target, { recursive: true, force: true });
|
|
5754
5975
|
pruneEmptyAncestors(target, planningRoot);
|
|
5755
5976
|
}
|
|
5756
5977
|
function localDivergesFromPreDelete(target, pre, repoRel, repo) {
|
|
5757
|
-
if (!
|
|
5978
|
+
if (!existsSync37(target)) return false;
|
|
5758
5979
|
try {
|
|
5759
5980
|
const preBlob = gitCaptureBuffer(["show", `${pre}:${repoRel}`], repo);
|
|
5760
|
-
return !
|
|
5981
|
+
return !readFileSync17(target).equals(preBlob);
|
|
5761
5982
|
} catch {
|
|
5762
5983
|
return true;
|
|
5763
5984
|
}
|
|
@@ -5768,11 +5989,11 @@ function planningDiffArgs(pre, post, logical) {
|
|
|
5768
5989
|
function deletePairsFor(t, raw) {
|
|
5769
5990
|
return planningDeleteTargets({ raw, logical: t.logical, localRoot: t.localRoot }).map(
|
|
5770
5991
|
(target) => {
|
|
5771
|
-
const relToLocal = target.slice(t.localRoot.length +
|
|
5992
|
+
const relToLocal = target.slice(t.localRoot.length + sep8.length);
|
|
5772
5993
|
return {
|
|
5773
5994
|
target,
|
|
5774
5995
|
relToLocal,
|
|
5775
|
-
repoRel: `shared/extras/${t.logical}/${relToLocal.split(
|
|
5996
|
+
repoRel: `shared/extras/${t.logical}/${relToLocal.split(sep8).join("/")}`
|
|
5776
5997
|
};
|
|
5777
5998
|
}
|
|
5778
5999
|
);
|
|
@@ -5799,7 +6020,7 @@ function keptDeletePreview(v, prePostHeads, repo) {
|
|
|
5799
6020
|
return kept;
|
|
5800
6021
|
}
|
|
5801
6022
|
function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
|
|
5802
|
-
const repoExtras =
|
|
6023
|
+
const repoExtras = join44(repo, "shared", "extras");
|
|
5803
6024
|
for (const t of eachExtrasTarget(v, { unmapped: 0, skipped: 0 })) {
|
|
5804
6025
|
if (t.dirname !== ".planning") continue;
|
|
5805
6026
|
let raw;
|
|
@@ -5814,14 +6035,14 @@ function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
|
|
|
5814
6035
|
}
|
|
5815
6036
|
const pairs = deletePairsFor(t, raw);
|
|
5816
6037
|
if (pairs.length === 0) continue;
|
|
5817
|
-
backupExtrasWrite(
|
|
5818
|
-
const planningRoot =
|
|
6038
|
+
backupExtrasWrite(join44(t.localRoot, t.dirname), ts, t.localRoot);
|
|
6039
|
+
const planningRoot = join44(t.localRoot, ".planning");
|
|
5819
6040
|
for (const { target, relToLocal, repoRel } of pairs) {
|
|
5820
6041
|
if (localDivergesFromPreDelete(target, prePostHeads.pre, repoRel, repo)) {
|
|
5821
6042
|
warn(keptDeleteWarnLine(t.logical, relToLocal));
|
|
5822
6043
|
continue;
|
|
5823
6044
|
}
|
|
5824
|
-
deletePlanningTarget(target, planningRoot,
|
|
6045
|
+
deletePlanningTarget(target, planningRoot, join44(repoExtras, t.logical, relToLocal));
|
|
5825
6046
|
}
|
|
5826
6047
|
}
|
|
5827
6048
|
}
|
|
@@ -5830,14 +6051,14 @@ function remapExtrasPush(ts, opts = {}) {
|
|
|
5830
6051
|
const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
|
|
5831
6052
|
if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
|
|
5832
6053
|
const repo = repoHome();
|
|
5833
|
-
const repoExtras =
|
|
6054
|
+
const repoExtras = join44(repo, "shared", "extras");
|
|
5834
6055
|
if (!dryRun) mkdirSync8(repoExtras, { recursive: true });
|
|
5835
6056
|
const { unmapped, skipped, done, would } = runExtrasOp(
|
|
5836
6057
|
v,
|
|
5837
6058
|
dryRun,
|
|
5838
6059
|
({ localRoot, logical, dirname: dirname10 }) => ({
|
|
5839
|
-
src:
|
|
5840
|
-
dst:
|
|
6060
|
+
src: join44(localRoot, dirname10),
|
|
6061
|
+
dst: join44(repoExtras, logical, dirname10)
|
|
5841
6062
|
}),
|
|
5842
6063
|
(dst) => backupRepoWrite(dst, ts, repo),
|
|
5843
6064
|
// Push copy routing per extra type:
|
|
@@ -5866,8 +6087,8 @@ function remapExtrasPull(ts, opts = {}) {
|
|
|
5866
6087
|
v,
|
|
5867
6088
|
dryRun,
|
|
5868
6089
|
({ localRoot, logical, dirname: dirname10 }) => ({
|
|
5869
|
-
src:
|
|
5870
|
-
dst:
|
|
6090
|
+
src: join44(repo, "shared", "extras", logical, dirname10),
|
|
6091
|
+
dst: join44(localRoot, dirname10)
|
|
5871
6092
|
}),
|
|
5872
6093
|
// Snapshot the host-side dst BEFORE the copy step touches it. Anchor on
|
|
5873
6094
|
// localRoot so the backup tree mirrors the project layout.
|
|
@@ -5914,17 +6135,17 @@ function divergenceCheckExtras(ts, prePostHeads) {
|
|
|
5914
6135
|
const v = loadValidatedExtras({});
|
|
5915
6136
|
if (v === null) return 0;
|
|
5916
6137
|
const counts = { unmapped: 0, skipped: 0 };
|
|
5917
|
-
const backupRoot =
|
|
6138
|
+
const backupRoot = join45(backupBase(), ts, "extras");
|
|
5918
6139
|
const repo = repoHome();
|
|
5919
6140
|
let divergedCount = 0;
|
|
5920
6141
|
for (const { logical, localRoot, dirname: dirname10 } of eachExtrasTarget(v, counts)) {
|
|
5921
|
-
const local =
|
|
5922
|
-
const repoEntry =
|
|
5923
|
-
if (!
|
|
6142
|
+
const local = join45(localRoot, dirname10);
|
|
6143
|
+
const repoEntry = join45(repo, "shared", "extras", logical, dirname10);
|
|
6144
|
+
if (!existsSync38(local) || !existsSync38(repoEntry)) continue;
|
|
5924
6145
|
const modified = listDivergingModified(local, repoEntry);
|
|
5925
6146
|
if (modified.length === 0) continue;
|
|
5926
6147
|
divergedCount += modified.length;
|
|
5927
|
-
const projectBackupRoot =
|
|
6148
|
+
const projectBackupRoot = join45(backupRoot, encodePath(localRoot));
|
|
5928
6149
|
warn(
|
|
5929
6150
|
divergenceWarnLine({
|
|
5930
6151
|
dirname: dirname10,
|
|
@@ -5946,8 +6167,8 @@ function divergenceCheckExtras(ts, prePostHeads) {
|
|
|
5946
6167
|
|
|
5947
6168
|
// src/skills-sync.ts
|
|
5948
6169
|
init_config();
|
|
5949
|
-
import { existsSync as
|
|
5950
|
-
import { join as
|
|
6170
|
+
import { existsSync as existsSync39, lstatSync as lstatSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync16, rmSync as rmSync14 } from "node:fs";
|
|
6171
|
+
import { join as join46 } from "node:path";
|
|
5951
6172
|
init_utils_fs();
|
|
5952
6173
|
function isGsdOwned(name) {
|
|
5953
6174
|
return name.startsWith(GSD_PREFIX);
|
|
@@ -5956,7 +6177,7 @@ function isSkillExcluded(name) {
|
|
|
5956
6177
|
return isGsdOwned(name) || isDeniedName(ALWAYS_NEVER_SYNC, name);
|
|
5957
6178
|
}
|
|
5958
6179
|
function copySkillsPush(src, dst) {
|
|
5959
|
-
const srcNames =
|
|
6180
|
+
const srcNames = readdirSync16(src, { encoding: "utf8" });
|
|
5960
6181
|
const blockSet = /* @__PURE__ */ new Set([
|
|
5961
6182
|
...srcNames.filter((n) => isGsdOwned(n)),
|
|
5962
6183
|
...ALWAYS_NEVER_SYNC
|
|
@@ -5967,30 +6188,30 @@ function copySkillsPull(src, dst) {
|
|
|
5967
6188
|
copyExtrasFilteredPreservingBy(src, dst, isSkillExcluded);
|
|
5968
6189
|
}
|
|
5969
6190
|
function syncSkillsPull(ts) {
|
|
5970
|
-
const sharedSkills =
|
|
5971
|
-
if (!
|
|
5972
|
-
const localSkills =
|
|
5973
|
-
const dstStat =
|
|
6191
|
+
const sharedSkills = join46(repoHome(), "shared", "skills");
|
|
6192
|
+
if (!existsSync39(sharedSkills)) return;
|
|
6193
|
+
const localSkills = join46(claudeHome(), "skills");
|
|
6194
|
+
const dstStat = lstatSync12(localSkills, { throwIfNoEntry: false });
|
|
5974
6195
|
if (dstStat?.isSymbolicLink() === true) {
|
|
5975
6196
|
backupBeforeWrite(localSkills, ts);
|
|
5976
|
-
|
|
6197
|
+
rmSync14(localSkills, { recursive: true, force: true });
|
|
5977
6198
|
mkdirSync9(localSkills, { recursive: true });
|
|
5978
6199
|
}
|
|
5979
6200
|
copySkillsPull(sharedSkills, localSkills);
|
|
5980
6201
|
}
|
|
5981
6202
|
function syncSkillsPush() {
|
|
5982
|
-
const localSkills =
|
|
5983
|
-
const stat =
|
|
6203
|
+
const localSkills = join46(claudeHome(), "skills");
|
|
6204
|
+
const stat = lstatSync12(localSkills, { throwIfNoEntry: false });
|
|
5984
6205
|
if (stat === void 0) return;
|
|
5985
6206
|
if (stat.isSymbolicLink()) return;
|
|
5986
|
-
const sharedSkills =
|
|
6207
|
+
const sharedSkills = join46(repoHome(), "shared", "skills");
|
|
5987
6208
|
copySkillsPush(localSkills, sharedSkills);
|
|
5988
6209
|
}
|
|
5989
6210
|
|
|
5990
6211
|
// src/preview.ts
|
|
5991
6212
|
init_config();
|
|
5992
|
-
import { existsSync as
|
|
5993
|
-
import { join as
|
|
6213
|
+
import { existsSync as existsSync40 } from "node:fs";
|
|
6214
|
+
import { join as join47 } from "node:path";
|
|
5994
6215
|
|
|
5995
6216
|
// node_modules/diff/libesm/diff/base.js
|
|
5996
6217
|
var Diff = class {
|
|
@@ -6276,7 +6497,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
|
|
|
6276
6497
|
return lines.join("\n");
|
|
6277
6498
|
}
|
|
6278
6499
|
function readJsonOrNull(path) {
|
|
6279
|
-
if (!
|
|
6500
|
+
if (!existsSync40(path)) return null;
|
|
6280
6501
|
try {
|
|
6281
6502
|
return readJson(path);
|
|
6282
6503
|
} catch {
|
|
@@ -6290,12 +6511,12 @@ function previewSettings(basePath, hostPath, settingsPath) {
|
|
|
6290
6511
|
}
|
|
6291
6512
|
const notes = [];
|
|
6292
6513
|
const hostOverrides = readJsonOrNull(hostPath);
|
|
6293
|
-
if (hostOverrides === null &&
|
|
6514
|
+
if (hostOverrides === null && existsSync40(hostPath)) {
|
|
6294
6515
|
notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
|
|
6295
6516
|
}
|
|
6296
6517
|
const merged = stripGsdHookEntries(deepMerge(base, hostOverrides ?? {}));
|
|
6297
6518
|
const current = readJsonOrNull(settingsPath);
|
|
6298
|
-
if (current === null &&
|
|
6519
|
+
if (current === null && existsSync40(settingsPath)) {
|
|
6299
6520
|
return { diff: "", notes: [...notes, "malformed; skipping diff"] };
|
|
6300
6521
|
}
|
|
6301
6522
|
const strippedCurrent = stripGsdHookEntries(current ?? {});
|
|
@@ -6308,6 +6529,7 @@ function previewSettings(basePath, hostPath, settingsPath) {
|
|
|
6308
6529
|
return { diff, notes };
|
|
6309
6530
|
}
|
|
6310
6531
|
function formatLinkRow(e) {
|
|
6532
|
+
if (e.kind === "copy") return `would copy ${e.from} -> ${e.to}`;
|
|
6311
6533
|
return `${e.kind} ${e.from} -> ${e.to}`;
|
|
6312
6534
|
}
|
|
6313
6535
|
function formatSessionRow(e) {
|
|
@@ -6336,9 +6558,9 @@ function computePreview(ts, map, verb = "pull") {
|
|
|
6336
6558
|
onPreview: (e) => addItem(links, formatLinkRow(e))
|
|
6337
6559
|
});
|
|
6338
6560
|
const settingsResult = previewSettings(
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6561
|
+
join47(repo, "shared", "settings.base.json"),
|
|
6562
|
+
join47(repo, "hosts", `${HOST}.json`),
|
|
6563
|
+
join47(claude, "settings.json")
|
|
6342
6564
|
);
|
|
6343
6565
|
const settingsSection = buildSettingsSectionForPreview(settingsResult);
|
|
6344
6566
|
const sessions = section("Sessions");
|
|
@@ -6353,7 +6575,7 @@ function computePreview(ts, map, verb = "pull") {
|
|
|
6353
6575
|
const extras = section("Extras");
|
|
6354
6576
|
let extrasSkipped = 0;
|
|
6355
6577
|
let extrasUnmapped = 0;
|
|
6356
|
-
if (
|
|
6578
|
+
if (existsSync40(join47(repo, "path-map.json")) && existsSync40(join47(repo, "shared", "extras"))) {
|
|
6357
6579
|
const extrasResult = remapExtrasPull(ts, { dryRun: true });
|
|
6358
6580
|
for (const entry of extrasResult.wouldPull) {
|
|
6359
6581
|
addItem(extras, entry);
|
|
@@ -6378,9 +6600,9 @@ init_config();
|
|
|
6378
6600
|
init_commands_pull_wedge();
|
|
6379
6601
|
init_utils();
|
|
6380
6602
|
init_utils_fs();
|
|
6381
|
-
import { execFileSync as
|
|
6603
|
+
import { execFileSync as execFileSync19 } from "node:child_process";
|
|
6382
6604
|
function gitCapture(args, cwd) {
|
|
6383
|
-
return
|
|
6605
|
+
return execFileSync19("git", args, {
|
|
6384
6606
|
cwd,
|
|
6385
6607
|
stdio: ["ignore", "pipe", "pipe"],
|
|
6386
6608
|
maxBuffer: 64 * 1024 * 1024
|
|
@@ -6571,7 +6793,7 @@ function runPullCore(opts = {}) {
|
|
|
6571
6793
|
const ts = freshBackupTs(backup);
|
|
6572
6794
|
handleWedge(repo, forceRemote);
|
|
6573
6795
|
if (!dryRun) {
|
|
6574
|
-
const backupRoot =
|
|
6796
|
+
const backupRoot = join48(backup, ts);
|
|
6575
6797
|
try {
|
|
6576
6798
|
mkdirSync10(backupRoot, { recursive: true });
|
|
6577
6799
|
} catch (err) {
|
|
@@ -6584,8 +6806,8 @@ function runPullCore(opts = {}) {
|
|
|
6584
6806
|
const prePostHeads = capturePrePostHeads(repo, () => {
|
|
6585
6807
|
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
|
|
6586
6808
|
});
|
|
6587
|
-
const mapPath =
|
|
6588
|
-
const map =
|
|
6809
|
+
const mapPath = join48(repo, "path-map.json");
|
|
6810
|
+
const map = existsSync41(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
6589
6811
|
const divergedKeptLocal = divergenceCheckExtras(ts, dryRun ? prePostHeads : void 0);
|
|
6590
6812
|
if (dryRun) {
|
|
6591
6813
|
computePreview(ts, map, "pull");
|
|
@@ -6597,8 +6819,8 @@ function runPullCore(opts = {}) {
|
|
|
6597
6819
|
}
|
|
6598
6820
|
function cmdPull(opts = {}) {
|
|
6599
6821
|
const repo = repoHome();
|
|
6600
|
-
if (!
|
|
6601
|
-
if (!
|
|
6822
|
+
if (!existsSync41(repo)) die(`repo not cloned at ${repo}`);
|
|
6823
|
+
if (!existsSync41(join48(repo, "shared", "settings.base.json"))) {
|
|
6602
6824
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
6603
6825
|
}
|
|
6604
6826
|
const handle = acquireLock("pull");
|
|
@@ -6620,13 +6842,13 @@ function cmdPull(opts = {}) {
|
|
|
6620
6842
|
|
|
6621
6843
|
// src/commands.push.ts
|
|
6622
6844
|
init_config();
|
|
6623
|
-
import { existsSync as
|
|
6624
|
-
import { join as
|
|
6845
|
+
import { existsSync as existsSync45 } from "node:fs";
|
|
6846
|
+
import { join as join53 } from "node:path";
|
|
6625
6847
|
|
|
6626
6848
|
// src/commands.push.selection.ts
|
|
6627
6849
|
init_config();
|
|
6628
|
-
import { existsSync as
|
|
6629
|
-
import { join as
|
|
6850
|
+
import { existsSync as existsSync42, statSync as statSync11 } from "node:fs";
|
|
6851
|
+
import { join as join49 } from "node:path";
|
|
6630
6852
|
init_utils_json();
|
|
6631
6853
|
function buildCurrentMap(map) {
|
|
6632
6854
|
const current = {};
|
|
@@ -6635,8 +6857,8 @@ function buildCurrentMap(map) {
|
|
|
6635
6857
|
for (const [, hostMap] of Object.entries(map.projects)) {
|
|
6636
6858
|
const localPath = hostMap[HOST];
|
|
6637
6859
|
if (!localPath) continue;
|
|
6638
|
-
const localDir =
|
|
6639
|
-
if (!
|
|
6860
|
+
const localDir = join49(claude, "projects", encodePath(localPath));
|
|
6861
|
+
if (!existsSync42(localDir)) continue;
|
|
6640
6862
|
for (const f of enumerateSourceFiles(localDir)) {
|
|
6641
6863
|
const st = statSync11(f);
|
|
6642
6864
|
current[f] = { size: st.size, mtime: st.mtimeMs };
|
|
@@ -6667,7 +6889,7 @@ function computePushSelection(map, old, scannerVersion, configHash, fullScan) {
|
|
|
6667
6889
|
};
|
|
6668
6890
|
}
|
|
6669
6891
|
function loadSelectionForPush(mapPath, old, scannerVersion, configHash, fullScan) {
|
|
6670
|
-
const map =
|
|
6892
|
+
const map = existsSync42(mapPath) ? readPathMap(mapPath) : null;
|
|
6671
6893
|
const { selection, newManifest } = computePushSelection(
|
|
6672
6894
|
map,
|
|
6673
6895
|
old,
|
|
@@ -6769,14 +6991,14 @@ function enforceAllowList(statusPorcelain, map) {
|
|
|
6769
6991
|
|
|
6770
6992
|
// src/commands.push.settings.ts
|
|
6771
6993
|
init_config();
|
|
6772
|
-
import { existsSync as
|
|
6773
|
-
import { join as
|
|
6994
|
+
import { existsSync as existsSync43 } from "node:fs";
|
|
6995
|
+
import { join as join50 } from "node:path";
|
|
6774
6996
|
init_utils();
|
|
6775
6997
|
init_utils_fs();
|
|
6776
6998
|
init_utils_json();
|
|
6777
6999
|
function stripGsdHooksFromBase(repo, backup) {
|
|
6778
|
-
const basePath =
|
|
6779
|
-
if (!
|
|
7000
|
+
const basePath = join50(repo, "shared", "settings.base.json");
|
|
7001
|
+
if (!existsSync43(basePath)) return;
|
|
6780
7002
|
let base;
|
|
6781
7003
|
try {
|
|
6782
7004
|
base = readJson(basePath);
|
|
@@ -6790,14 +7012,14 @@ function stripGsdHooksFromBase(repo, backup) {
|
|
|
6790
7012
|
writeJsonAtomic(basePath, stripped);
|
|
6791
7013
|
}
|
|
6792
7014
|
function reportSettingsAheadDrift(repo) {
|
|
6793
|
-
const basePath =
|
|
6794
|
-
if (!
|
|
6795
|
-
const settingsPath =
|
|
6796
|
-
if (!
|
|
7015
|
+
const basePath = join50(repo, "shared", "settings.base.json");
|
|
7016
|
+
if (!existsSync43(basePath)) return;
|
|
7017
|
+
const settingsPath = join50(claudeHome(), "settings.json");
|
|
7018
|
+
if (!existsSync43(settingsPath)) return;
|
|
6797
7019
|
try {
|
|
6798
7020
|
const base = readJson(basePath);
|
|
6799
|
-
const hostPath =
|
|
6800
|
-
const overrides =
|
|
7021
|
+
const hostPath = join50(repo, "hosts", `${HOST}.json`);
|
|
7022
|
+
const overrides = existsSync43(hostPath) ? readJson(hostPath) : {};
|
|
6801
7023
|
const merged = deepMerge(base, overrides);
|
|
6802
7024
|
const settings = readJson(settingsPath);
|
|
6803
7025
|
const { ahead } = classifySettingsDrift(merged, settings);
|
|
@@ -6814,12 +7036,12 @@ function reportSettingsAheadDrift(repo) {
|
|
|
6814
7036
|
// src/commands.push.guards.ts
|
|
6815
7037
|
init_push_checks();
|
|
6816
7038
|
init_utils();
|
|
6817
|
-
import { join as
|
|
7039
|
+
import { join as join51, relative as relative7 } from "node:path";
|
|
6818
7040
|
function guardGitlinks(repo) {
|
|
6819
|
-
const gitlinks = findGitlinks(
|
|
7041
|
+
const gitlinks = findGitlinks(join51(repo, "shared"));
|
|
6820
7042
|
if (gitlinks.length === 0) return;
|
|
6821
7043
|
for (const p of gitlinks) {
|
|
6822
|
-
const rel = relative7(repo, p);
|
|
7044
|
+
const rel = relative7(repo, p).replaceAll("\\", "/");
|
|
6823
7045
|
fail(`gitlink: ${rel} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`);
|
|
6824
7046
|
}
|
|
6825
7047
|
const noun = gitlinks.length === 1 ? "entry" : "entries";
|
|
@@ -6848,7 +7070,7 @@ init_config();
|
|
|
6848
7070
|
|
|
6849
7071
|
// src/push-global-config.ts
|
|
6850
7072
|
init_config();
|
|
6851
|
-
import { execFileSync as
|
|
7073
|
+
import { execFileSync as execFileSync20 } from "node:child_process";
|
|
6852
7074
|
var STATUS_LABELS = {
|
|
6853
7075
|
A: "add",
|
|
6854
7076
|
M: "modify",
|
|
@@ -6888,7 +7110,7 @@ function isInScope(filePath, exactPrefixes, dirPrefixes) {
|
|
|
6888
7110
|
}
|
|
6889
7111
|
function collectGlobalConfigChanges(repoHome2, hostname2, opts) {
|
|
6890
7112
|
const args = opts.staged ? ["diff", "--cached", "--name-status", "-z"] : ["diff", "HEAD", "--name-status", "-z"];
|
|
6891
|
-
const raw =
|
|
7113
|
+
const raw = execFileSync20("git", args, {
|
|
6892
7114
|
cwd: repoHome2,
|
|
6893
7115
|
stdio: ["ignore", "pipe", "pipe"]
|
|
6894
7116
|
}).toString();
|
|
@@ -6926,9 +7148,9 @@ init_color();
|
|
|
6926
7148
|
init_config();
|
|
6927
7149
|
init_config_sharedDirs_guard();
|
|
6928
7150
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
6929
|
-
import { copyFileSync, existsSync as
|
|
7151
|
+
import { copyFileSync, existsSync as existsSync44, mkdirSync as mkdirSync11, readdirSync as readdirSync17, rmSync as rmSync15 } from "node:fs";
|
|
6930
7152
|
import { homedir as homedir5 } from "node:os";
|
|
6931
|
-
import { join as
|
|
7153
|
+
import { join as join52, relative as relative8, sep as sep9 } from "node:path";
|
|
6932
7154
|
init_push_leak_verdict();
|
|
6933
7155
|
init_push_gitleaks();
|
|
6934
7156
|
init_utils_fs();
|
|
@@ -6936,11 +7158,11 @@ init_utils_json();
|
|
|
6936
7158
|
var NOTHING_TO_SCAN_ROW = `${dim(infoGlyph)} nothing to scan, no leaks`;
|
|
6937
7159
|
function stageSessionDir(localDir, dstDir, changed) {
|
|
6938
7160
|
if (changed !== void 0) {
|
|
6939
|
-
const prefix = `${localDir}${
|
|
7161
|
+
const prefix = `${localDir}${sep9}`;
|
|
6940
7162
|
const matching = [...changed].filter((p) => p.startsWith(prefix));
|
|
6941
7163
|
if (matching.length === 0) return false;
|
|
6942
7164
|
for (const src of matching) {
|
|
6943
|
-
copyFileAtomic(src,
|
|
7165
|
+
copyFileAtomic(src, join52(dstDir, relative8(localDir, src)));
|
|
6944
7166
|
}
|
|
6945
7167
|
return true;
|
|
6946
7168
|
}
|
|
@@ -6956,14 +7178,14 @@ function stageSessions(tmpRoot, map, changed) {
|
|
|
6956
7178
|
if (!p || p === "TBD") continue;
|
|
6957
7179
|
reverse.set(encodePath(p), logical);
|
|
6958
7180
|
}
|
|
6959
|
-
const localProjects =
|
|
6960
|
-
if (!
|
|
7181
|
+
const localProjects = join52(claudeHome(), "projects");
|
|
7182
|
+
if (!existsSync44(localProjects)) return 0;
|
|
6961
7183
|
let staged = 0;
|
|
6962
|
-
for (const dir of
|
|
7184
|
+
for (const dir of readdirSync17(localProjects)) {
|
|
6963
7185
|
const logical = reverse.get(dir);
|
|
6964
7186
|
if (!logical) continue;
|
|
6965
|
-
const localDir =
|
|
6966
|
-
const dstDir =
|
|
7187
|
+
const localDir = join52(localProjects, dir);
|
|
7188
|
+
const dstDir = join52(tmpRoot, "shared", "projects", logical);
|
|
6967
7189
|
if (stageSessionDir(localDir, dstDir, changed)) staged++;
|
|
6968
7190
|
}
|
|
6969
7191
|
return staged;
|
|
@@ -6979,9 +7201,9 @@ function stageExtras(tmpRoot, map) {
|
|
|
6979
7201
|
if (!localRoot || localRoot === "TBD") continue;
|
|
6980
7202
|
for (const dirname10 of dirnames) {
|
|
6981
7203
|
if (!whitelist.includes(dirname10)) continue;
|
|
6982
|
-
const src =
|
|
6983
|
-
if (!
|
|
6984
|
-
const dst =
|
|
7204
|
+
const src = join52(localRoot, dirname10);
|
|
7205
|
+
if (!existsSync44(src)) continue;
|
|
7206
|
+
const dst = join52(tmpRoot, "shared", "extras", logical, dirname10);
|
|
6985
7207
|
copyExtras(src, dst);
|
|
6986
7208
|
staged++;
|
|
6987
7209
|
}
|
|
@@ -6989,19 +7211,19 @@ function stageExtras(tmpRoot, map) {
|
|
|
6989
7211
|
return staged;
|
|
6990
7212
|
}
|
|
6991
7213
|
function previewPushLeaks(map, opts = {}) {
|
|
6992
|
-
const cacheDir =
|
|
7214
|
+
const cacheDir = join52(homedir5(), ".cache", "claude-nomad");
|
|
6993
7215
|
mkdirSync11(cacheDir, { recursive: true });
|
|
6994
7216
|
const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
|
|
6995
|
-
const tmpRoot =
|
|
7217
|
+
const tmpRoot = join52(cacheDir, `push-preview-tree-${stamp}`);
|
|
6996
7218
|
try {
|
|
6997
7219
|
const sessionCount = stageSessions(tmpRoot, map, opts.selection?.changed);
|
|
6998
7220
|
const extrasCount = stageExtras(tmpRoot, map);
|
|
6999
7221
|
if (sessionCount + extrasCount === 0) {
|
|
7000
7222
|
return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
|
|
7001
7223
|
}
|
|
7002
|
-
const ignoreFile =
|
|
7003
|
-
if (
|
|
7004
|
-
copyFileSync(ignoreFile,
|
|
7224
|
+
const ignoreFile = join52(repoHome(), ".gitleaksignore");
|
|
7225
|
+
if (existsSync44(ignoreFile)) {
|
|
7226
|
+
copyFileSync(ignoreFile, join52(tmpRoot, ".gitleaksignore"));
|
|
7005
7227
|
}
|
|
7006
7228
|
let findings;
|
|
7007
7229
|
try {
|
|
@@ -7014,7 +7236,7 @@ function previewPushLeaks(map, opts = {}) {
|
|
|
7014
7236
|
}
|
|
7015
7237
|
return verdictFromFindings(findings);
|
|
7016
7238
|
} finally {
|
|
7017
|
-
|
|
7239
|
+
rmSync15(tmpRoot, { recursive: true, force: true });
|
|
7018
7240
|
}
|
|
7019
7241
|
}
|
|
7020
7242
|
|
|
@@ -7076,7 +7298,7 @@ async function runPushCore(opts = {}) {
|
|
|
7076
7298
|
const scannerVersion = probeGitleaks();
|
|
7077
7299
|
const configHash = computeConfigHash();
|
|
7078
7300
|
const old = readManifest(manifestPath());
|
|
7079
|
-
const mapPath =
|
|
7301
|
+
const mapPath = join53(repo, "path-map.json");
|
|
7080
7302
|
const { map, selection, newManifest } = loadSelectionForPush(
|
|
7081
7303
|
mapPath,
|
|
7082
7304
|
old,
|
|
@@ -7090,6 +7312,7 @@ async function runPushCore(opts = {}) {
|
|
|
7090
7312
|
const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
|
|
7091
7313
|
if (!dryRun) {
|
|
7092
7314
|
syncSkillsPush();
|
|
7315
|
+
syncSharedLinksPush(map);
|
|
7093
7316
|
stripGsdHooksFromBase(repo, backup);
|
|
7094
7317
|
}
|
|
7095
7318
|
const st = { dryRun, remap, extras, globalConfig: [] };
|
|
@@ -7129,7 +7352,7 @@ async function cmdPush(opts = {}) {
|
|
|
7129
7352
|
const allowRule = opts.allowRule;
|
|
7130
7353
|
guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
|
|
7131
7354
|
const repo = repoHome();
|
|
7132
|
-
if (!
|
|
7355
|
+
if (!existsSync45(repo)) die(`repo not cloned at ${repo}`);
|
|
7133
7356
|
const handle = acquireLock("push");
|
|
7134
7357
|
if (handle === null) process.exit(0);
|
|
7135
7358
|
try {
|
|
@@ -7147,8 +7370,8 @@ async function cmdPush(opts = {}) {
|
|
|
7147
7370
|
}
|
|
7148
7371
|
|
|
7149
7372
|
// src/commands.sync.ts
|
|
7150
|
-
import { existsSync as
|
|
7151
|
-
import { join as
|
|
7373
|
+
import { existsSync as existsSync46 } from "node:fs";
|
|
7374
|
+
import { join as join54 } from "node:path";
|
|
7152
7375
|
init_config();
|
|
7153
7376
|
init_utils();
|
|
7154
7377
|
init_utils_fs();
|
|
@@ -7216,8 +7439,8 @@ async function runSyncWet() {
|
|
|
7216
7439
|
}
|
|
7217
7440
|
async function runSyncDryRun(repo, backup) {
|
|
7218
7441
|
const ts = freshBackupTs(backup);
|
|
7219
|
-
const mapPath =
|
|
7220
|
-
const map =
|
|
7442
|
+
const mapPath = join54(repo, "path-map.json");
|
|
7443
|
+
const map = existsSync46(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
7221
7444
|
computePreview(ts, map, "pull");
|
|
7222
7445
|
log("push preview below is computed against pre-pull state (a real sync pushes after pull)");
|
|
7223
7446
|
await runPushCore({ dryRun: true });
|
|
@@ -7226,8 +7449,8 @@ async function cmdSync(opts = {}) {
|
|
|
7226
7449
|
const dryRun = opts.dryRun === true;
|
|
7227
7450
|
const repo = repoHome();
|
|
7228
7451
|
const backup = backupBase();
|
|
7229
|
-
if (!
|
|
7230
|
-
if (!
|
|
7452
|
+
if (!existsSync46(repo)) die(`repo not cloned at ${repo}`);
|
|
7453
|
+
if (!existsSync46(join54(repo, "shared", "settings.base.json"))) {
|
|
7231
7454
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
7232
7455
|
}
|
|
7233
7456
|
const handle = acquireLock("sync");
|
|
@@ -7251,25 +7474,31 @@ async function cmdSync(opts = {}) {
|
|
|
7251
7474
|
}
|
|
7252
7475
|
|
|
7253
7476
|
// src/commands.update.ts
|
|
7254
|
-
import { execFileSync as
|
|
7477
|
+
import { execFileSync as execFileSync21 } from "node:child_process";
|
|
7255
7478
|
init_utils();
|
|
7256
|
-
function readInstalledVersion(run =
|
|
7479
|
+
function readInstalledVersion(run = execFileSync21) {
|
|
7480
|
+
const isWin = process.platform === "win32";
|
|
7257
7481
|
try {
|
|
7258
|
-
return run("nomad", ["--version"], {
|
|
7482
|
+
return run(isWin ? "nomad.cmd" : "nomad", ["--version"], {
|
|
7483
|
+
encoding: "utf8",
|
|
7484
|
+
...isWin ? { shell: true } : {}
|
|
7485
|
+
}).toString().trim() || null;
|
|
7259
7486
|
} catch {
|
|
7260
7487
|
return null;
|
|
7261
7488
|
}
|
|
7262
7489
|
}
|
|
7263
|
-
function cmdUpdate(currentVersion, run =
|
|
7490
|
+
function cmdUpdate(currentVersion, run = execFileSync21) {
|
|
7264
7491
|
console.log(`Updating claude-nomad v${currentVersion}...`);
|
|
7492
|
+
const isWin = process.platform === "win32";
|
|
7265
7493
|
try {
|
|
7266
|
-
run("npm", ["update", "-g", "claude-nomad"], {
|
|
7494
|
+
run(isWin ? "npm.cmd" : "npm", ["update", "-g", "claude-nomad"], {
|
|
7267
7495
|
encoding: "utf8",
|
|
7268
7496
|
stdio: ["ignore", "pipe", "pipe"],
|
|
7269
7497
|
// Now that output is captured rather than inherited, execFileSync buffers
|
|
7270
7498
|
// it; lift the default 1 MiB ceiling so a noisy-but-successful npm run
|
|
7271
7499
|
// (deprecation/funding spam) cannot throw ENOBUFS.
|
|
7272
|
-
maxBuffer: 64 * 1024 * 1024
|
|
7500
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
7501
|
+
...isWin ? { shell: true } : {}
|
|
7273
7502
|
});
|
|
7274
7503
|
} catch (err) {
|
|
7275
7504
|
const e = err;
|
|
@@ -7294,18 +7523,18 @@ init_config();
|
|
|
7294
7523
|
|
|
7295
7524
|
// src/diff.ts
|
|
7296
7525
|
init_config();
|
|
7297
|
-
import { existsSync as
|
|
7298
|
-
import { join as
|
|
7526
|
+
import { existsSync as existsSync47 } from "node:fs";
|
|
7527
|
+
import { join as join55 } from "node:path";
|
|
7299
7528
|
init_utils();
|
|
7300
7529
|
init_utils_fs();
|
|
7301
7530
|
init_utils_json();
|
|
7302
7531
|
function cmdDiff() {
|
|
7303
7532
|
try {
|
|
7304
7533
|
const repo = repoHome();
|
|
7305
|
-
if (!
|
|
7534
|
+
if (!existsSync47(repo)) die(`repo not cloned at ${repo}`);
|
|
7306
7535
|
const ts = freshBackupTs(backupBase());
|
|
7307
|
-
const mapPath =
|
|
7308
|
-
const map =
|
|
7536
|
+
const mapPath = join55(repo, "path-map.json");
|
|
7537
|
+
const map = existsSync47(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
7309
7538
|
divergenceCheckExtras(ts);
|
|
7310
7539
|
computePreview(ts, map, "diff");
|
|
7311
7540
|
} catch (err) {
|
|
@@ -7320,19 +7549,19 @@ function cmdDiff() {
|
|
|
7320
7549
|
|
|
7321
7550
|
// src/init.ts
|
|
7322
7551
|
init_config();
|
|
7323
|
-
import { existsSync as
|
|
7324
|
-
import { join as
|
|
7552
|
+
import { existsSync as existsSync49, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7553
|
+
import { join as join57 } from "node:path";
|
|
7325
7554
|
|
|
7326
7555
|
// src/init.gh-onboard.ts
|
|
7327
7556
|
init_config();
|
|
7328
|
-
import { execFileSync as
|
|
7557
|
+
import { execFileSync as execFileSync22 } from "node:child_process";
|
|
7329
7558
|
init_utils();
|
|
7330
7559
|
var DEFAULT_REPO_NAME = "claude-nomad-config";
|
|
7331
7560
|
function isValidRepoName(name) {
|
|
7332
7561
|
return /^[A-Za-z0-9._-]{1,100}$/.test(name);
|
|
7333
7562
|
}
|
|
7334
7563
|
var GH_NETWORK_TIMEOUT_MS = 3e4;
|
|
7335
|
-
function ensureOriginRepo(repoName, run =
|
|
7564
|
+
function ensureOriginRepo(repoName, run = execFileSync22) {
|
|
7336
7565
|
if (!isValidRepoName(repoName)) {
|
|
7337
7566
|
die(
|
|
7338
7567
|
`invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
|
|
@@ -7403,33 +7632,33 @@ init_config();
|
|
|
7403
7632
|
init_utils();
|
|
7404
7633
|
init_utils_fs();
|
|
7405
7634
|
init_utils_json();
|
|
7406
|
-
import { copyFileSync as copyFileSync2, cpSync as
|
|
7407
|
-
import { join as
|
|
7635
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync8, existsSync as existsSync48, rmSync as rmSync16, statSync as statSync12 } from "node:fs";
|
|
7636
|
+
import { join as join56 } from "node:path";
|
|
7408
7637
|
function snapshotIntoShared(map) {
|
|
7409
7638
|
const repo = repoHome();
|
|
7410
7639
|
const claude = claudeHome();
|
|
7411
7640
|
for (const name of allSharedLinks(map)) {
|
|
7412
|
-
const src =
|
|
7413
|
-
if (!
|
|
7414
|
-
const dst =
|
|
7641
|
+
const src = join56(claude, name);
|
|
7642
|
+
if (!existsSync48(src)) continue;
|
|
7643
|
+
const dst = join56(repo, "shared", name);
|
|
7415
7644
|
if (statSync12(src).isDirectory()) {
|
|
7416
|
-
const gk =
|
|
7417
|
-
if (
|
|
7418
|
-
|
|
7645
|
+
const gk = join56(dst, ".gitkeep");
|
|
7646
|
+
if (existsSync48(gk)) rmSync16(gk);
|
|
7647
|
+
cpSync8(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
7419
7648
|
} else {
|
|
7420
7649
|
copyFileSync2(src, dst);
|
|
7421
7650
|
}
|
|
7422
7651
|
log(`snapshotted shared/${name} from ${src}`);
|
|
7423
7652
|
}
|
|
7424
|
-
const userSettings =
|
|
7425
|
-
if (
|
|
7653
|
+
const userSettings = join56(claude, "settings.json");
|
|
7654
|
+
if (existsSync48(userSettings)) {
|
|
7426
7655
|
let parsed;
|
|
7427
7656
|
try {
|
|
7428
7657
|
parsed = readJson(userSettings);
|
|
7429
7658
|
} catch (err) {
|
|
7430
7659
|
return die(`malformed ${userSettings}: ${err.message}`);
|
|
7431
7660
|
}
|
|
7432
|
-
const hostFile =
|
|
7661
|
+
const hostFile = join56(repo, "hosts", `${HOST}.json`);
|
|
7433
7662
|
writeJsonAtomic(hostFile, parsed);
|
|
7434
7663
|
log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
|
|
7435
7664
|
}
|
|
@@ -7439,17 +7668,18 @@ function snapshotIntoShared(map) {
|
|
|
7439
7668
|
init_utils();
|
|
7440
7669
|
init_utils_fs();
|
|
7441
7670
|
var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.claude/CLAUDE.md by nomad pull -->\n";
|
|
7671
|
+
var GITATTRIBUTES = "# nomad: sync content is byte-managed, disable all line-ending conversion\n* -text\n";
|
|
7442
7672
|
var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
|
|
7443
7673
|
function preflightConflict(repoHome2) {
|
|
7444
7674
|
const candidates = [
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7675
|
+
join57(repoHome2, "shared", "settings.base.json"),
|
|
7676
|
+
join57(repoHome2, "shared", "CLAUDE.md"),
|
|
7677
|
+
join57(repoHome2, "path-map.json"),
|
|
7678
|
+
join57(repoHome2, "hosts"),
|
|
7679
|
+
join57(repoHome2, "shared")
|
|
7450
7680
|
];
|
|
7451
7681
|
for (const c of candidates) {
|
|
7452
|
-
if (
|
|
7682
|
+
if (existsSync49(c)) return c;
|
|
7453
7683
|
}
|
|
7454
7684
|
return null;
|
|
7455
7685
|
}
|
|
@@ -7467,26 +7697,28 @@ function cmdInit(opts = {}) {
|
|
|
7467
7697
|
die(`already initialized; refusing to clobber ${conflict}`);
|
|
7468
7698
|
}
|
|
7469
7699
|
ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
|
|
7470
|
-
mkdirSync12(
|
|
7471
|
-
mkdirSync12(
|
|
7700
|
+
mkdirSync12(join57(repo, "shared"), { recursive: true });
|
|
7701
|
+
mkdirSync12(join57(repo, "hosts"), { recursive: true });
|
|
7472
7702
|
for (const name of SHARED_KEEP_DIRS) {
|
|
7473
|
-
mkdirSync12(
|
|
7703
|
+
mkdirSync12(join57(repo, "shared", name), { recursive: true });
|
|
7474
7704
|
}
|
|
7475
|
-
const userClaudeMd =
|
|
7476
|
-
if (!snapshot || !
|
|
7477
|
-
writeFileSync6(
|
|
7705
|
+
const userClaudeMd = join57(claude, "CLAUDE.md");
|
|
7706
|
+
if (!snapshot || !existsSync49(userClaudeMd)) {
|
|
7707
|
+
writeFileSync6(join57(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
|
|
7478
7708
|
item("created shared/CLAUDE.md");
|
|
7479
7709
|
}
|
|
7480
7710
|
for (const name of SHARED_KEEP_DIRS) {
|
|
7481
|
-
writeFileSync6(
|
|
7711
|
+
writeFileSync6(join57(repo, "shared", name, ".gitkeep"), "");
|
|
7482
7712
|
item(`created shared/${name}/.gitkeep`);
|
|
7483
7713
|
}
|
|
7484
|
-
writeFileSync6(
|
|
7714
|
+
writeFileSync6(join57(repo, "hosts", ".gitkeep"), "");
|
|
7485
7715
|
item("created hosts/.gitkeep");
|
|
7486
|
-
writeJsonAtomic(
|
|
7716
|
+
writeJsonAtomic(join57(repo, "shared", "settings.base.json"), {});
|
|
7487
7717
|
item("created shared/settings.base.json");
|
|
7488
|
-
writeJsonAtomic(
|
|
7718
|
+
writeJsonAtomic(join57(repo, "path-map.json"), { projects: {} });
|
|
7489
7719
|
item("created path-map.json");
|
|
7720
|
+
writeFileSync6(join57(repo, ".gitattributes"), GITATTRIBUTES);
|
|
7721
|
+
item("created .gitattributes");
|
|
7490
7722
|
if (snapshot) {
|
|
7491
7723
|
snapshotIntoShared({ projects: {} });
|
|
7492
7724
|
log(`snapshot staged in shared/; review, then 'nomad push' to share with other hosts.`);
|
|
@@ -7551,21 +7783,21 @@ function maybeDisableRepoActions(repoHome2, run) {
|
|
|
7551
7783
|
// src/init.prompt.ts
|
|
7552
7784
|
init_config();
|
|
7553
7785
|
init_utils();
|
|
7554
|
-
import { existsSync as
|
|
7555
|
-
import { join as
|
|
7786
|
+
import { existsSync as existsSync50, readdirSync as readdirSync18, statSync as statSync13 } from "node:fs";
|
|
7787
|
+
import { join as join58 } from "node:path";
|
|
7556
7788
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
7557
7789
|
function nonEmptyExists(path) {
|
|
7558
|
-
if (!
|
|
7790
|
+
if (!existsSync50(path)) return false;
|
|
7559
7791
|
try {
|
|
7560
|
-
if (statSync13(path).isDirectory()) return
|
|
7792
|
+
if (statSync13(path).isDirectory()) return readdirSync18(path).length > 0;
|
|
7561
7793
|
return true;
|
|
7562
7794
|
} catch {
|
|
7563
7795
|
return false;
|
|
7564
7796
|
}
|
|
7565
7797
|
}
|
|
7566
7798
|
function hasExistingClaudeConfig(claudeHome2) {
|
|
7567
|
-
if (
|
|
7568
|
-
return SHARED_LINKS.some((name) => nonEmptyExists(
|
|
7799
|
+
if (existsSync50(join58(claudeHome2, "settings.json"))) return true;
|
|
7800
|
+
return SHARED_LINKS.some((name) => nonEmptyExists(join58(claudeHome2, name)));
|
|
7569
7801
|
}
|
|
7570
7802
|
async function confirmSnapshotDefault(claudeHome2) {
|
|
7571
7803
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
@@ -7869,7 +8101,7 @@ function parseSyncArgs(argv) {
|
|
|
7869
8101
|
// package.json
|
|
7870
8102
|
var package_default = {
|
|
7871
8103
|
name: "claude-nomad",
|
|
7872
|
-
version: "0.
|
|
8104
|
+
version: "0.59.0",
|
|
7873
8105
|
type: "module",
|
|
7874
8106
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
7875
8107
|
keywords: [
|
|
@@ -8105,15 +8337,15 @@ var DEFAULT_HELP = [
|
|
|
8105
8337
|
init_config();
|
|
8106
8338
|
init_utils();
|
|
8107
8339
|
init_utils_json();
|
|
8108
|
-
import { existsSync as
|
|
8109
|
-
import { join as
|
|
8340
|
+
import { existsSync as existsSync51, readFileSync as readFileSync18, readdirSync as readdirSync19 } from "node:fs";
|
|
8341
|
+
import { join as join59 } from "node:path";
|
|
8110
8342
|
function resumeCmd(sessionId) {
|
|
8111
8343
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
8112
8344
|
fail(`invalid session id: ${sessionId}`);
|
|
8113
8345
|
process.exit(1);
|
|
8114
8346
|
}
|
|
8115
|
-
const projectsRoot =
|
|
8116
|
-
if (!
|
|
8347
|
+
const projectsRoot = join59(claudeHome(), "projects");
|
|
8348
|
+
if (!existsSync51(projectsRoot)) {
|
|
8117
8349
|
fail(`${projectsRoot} does not exist`);
|
|
8118
8350
|
process.exit(1);
|
|
8119
8351
|
}
|
|
@@ -8127,8 +8359,8 @@ function resumeCmd(sessionId) {
|
|
|
8127
8359
|
fail(`no cwd field found in ${jsonlPath}`);
|
|
8128
8360
|
process.exit(1);
|
|
8129
8361
|
}
|
|
8130
|
-
const mapPath =
|
|
8131
|
-
if (!
|
|
8362
|
+
const mapPath = join59(repoHome(), "path-map.json");
|
|
8363
|
+
if (!existsSync51(mapPath)) {
|
|
8132
8364
|
fail("path-map.json missing");
|
|
8133
8365
|
process.exit(1);
|
|
8134
8366
|
}
|
|
@@ -8150,14 +8382,14 @@ function resumeCmd(sessionId) {
|
|
|
8150
8382
|
console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
|
|
8151
8383
|
}
|
|
8152
8384
|
function findTranscriptPath(projectsRoot, sessionId) {
|
|
8153
|
-
for (const dir of
|
|
8154
|
-
const candidate =
|
|
8155
|
-
if (
|
|
8385
|
+
for (const dir of readdirSync19(projectsRoot)) {
|
|
8386
|
+
const candidate = join59(projectsRoot, dir, `${sessionId}.jsonl`);
|
|
8387
|
+
if (existsSync51(candidate)) return candidate;
|
|
8156
8388
|
}
|
|
8157
8389
|
return null;
|
|
8158
8390
|
}
|
|
8159
8391
|
function extractRecordedCwd(jsonlPath) {
|
|
8160
|
-
for (const line of
|
|
8392
|
+
for (const line of readFileSync18(jsonlPath, "utf8").split("\n")) {
|
|
8161
8393
|
if (!line.trim()) continue;
|
|
8162
8394
|
try {
|
|
8163
8395
|
const obj = JSON.parse(line);
|