claude-nomad 0.58.1 → 0.60.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 +53 -0
- package/README.md +62 -6
- package/dist/nomad.mjs +1560 -1106
- 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();
|
|
@@ -1493,6 +1353,75 @@ function stripGsdHookEntries(settings) {
|
|
|
1493
1353
|
}
|
|
1494
1354
|
return out;
|
|
1495
1355
|
}
|
|
1356
|
+
function keepMatcherEntry(entry) {
|
|
1357
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return null;
|
|
1358
|
+
const entryObj = entry;
|
|
1359
|
+
if (!Array.isArray(entryObj.hooks)) return null;
|
|
1360
|
+
const innerHooks = entryObj.hooks;
|
|
1361
|
+
const kept = innerHooks.filter((h2) => {
|
|
1362
|
+
if (h2 === null || typeof h2 !== "object" || Array.isArray(h2)) return false;
|
|
1363
|
+
const hookObj = h2;
|
|
1364
|
+
const cmd = hookObj.command;
|
|
1365
|
+
return isGsdHookEntry(typeof cmd === "string" ? cmd : "");
|
|
1366
|
+
});
|
|
1367
|
+
if (kept.length === 0) return null;
|
|
1368
|
+
return { ...entryObj, hooks: kept };
|
|
1369
|
+
}
|
|
1370
|
+
function keepEventMatchers(matchers) {
|
|
1371
|
+
if (!Array.isArray(matchers)) return null;
|
|
1372
|
+
const kept = [];
|
|
1373
|
+
for (const entry of matchers) {
|
|
1374
|
+
const result = keepMatcherEntry(entry);
|
|
1375
|
+
if (result !== null) kept.push(result);
|
|
1376
|
+
}
|
|
1377
|
+
return kept.length === 0 ? null : kept;
|
|
1378
|
+
}
|
|
1379
|
+
function keepGsdHookEntries(settings) {
|
|
1380
|
+
const hooksVal = settings.hooks;
|
|
1381
|
+
if (hooksVal === null || typeof hooksVal !== "object" || Array.isArray(hooksVal)) return {};
|
|
1382
|
+
const hooksObj = hooksVal;
|
|
1383
|
+
const keptHooks = {};
|
|
1384
|
+
for (const [event, matchers] of Object.entries(hooksObj)) {
|
|
1385
|
+
const kept = keepEventMatchers(matchers);
|
|
1386
|
+
if (kept !== null) keptHooks[event] = kept;
|
|
1387
|
+
}
|
|
1388
|
+
return Object.keys(keptHooks).length === 0 ? {} : { hooks: keptHooks };
|
|
1389
|
+
}
|
|
1390
|
+
function asPlainObject(value) {
|
|
1391
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
|
|
1392
|
+
return value;
|
|
1393
|
+
}
|
|
1394
|
+
function canonicalMatcherKey(m) {
|
|
1395
|
+
const obj = asPlainObject(m);
|
|
1396
|
+
if (obj === null) return JSON.stringify(m);
|
|
1397
|
+
return JSON.stringify(
|
|
1398
|
+
Object.keys(obj).sort((a, b) => a.localeCompare(b)).map((k) => [k, obj[k]])
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
function unionMatcherArrays(baseMatchers, gsdMatchers) {
|
|
1402
|
+
const seen = new Set(baseMatchers.map(canonicalMatcherKey));
|
|
1403
|
+
const merged = [...baseMatchers];
|
|
1404
|
+
for (const m of gsdMatchers) {
|
|
1405
|
+
const key = canonicalMatcherKey(m);
|
|
1406
|
+
if (!seen.has(key)) {
|
|
1407
|
+
merged.push(m);
|
|
1408
|
+
seen.add(key);
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return merged;
|
|
1412
|
+
}
|
|
1413
|
+
function graftGsdHookEntries(base, gsdOnly) {
|
|
1414
|
+
const gsdHooks = asPlainObject(gsdOnly.hooks);
|
|
1415
|
+
if (gsdHooks === null || Object.keys(gsdHooks).length === 0) return base;
|
|
1416
|
+
const baseHooks = asPlainObject(base.hooks);
|
|
1417
|
+
const mergedHooks = baseHooks ? { ...baseHooks } : {};
|
|
1418
|
+
for (const [event, gsdMatchers] of Object.entries(gsdHooks)) {
|
|
1419
|
+
if (!Array.isArray(gsdMatchers)) continue;
|
|
1420
|
+
const baseMatchers = mergedHooks[event];
|
|
1421
|
+
mergedHooks[event] = Array.isArray(baseMatchers) ? unionMatcherArrays(baseMatchers, gsdMatchers) : gsdMatchers;
|
|
1422
|
+
}
|
|
1423
|
+
return { ...base, hooks: mergedHooks };
|
|
1424
|
+
}
|
|
1496
1425
|
function matcherHasGsdEntry(entry) {
|
|
1497
1426
|
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
1498
1427
|
const entryObj = entry;
|
|
@@ -1630,50 +1559,294 @@ function buildCaptureSubset(merged, settings, opts) {
|
|
|
1630
1559
|
return out;
|
|
1631
1560
|
}
|
|
1632
1561
|
|
|
1633
|
-
// src/
|
|
1562
|
+
// src/extras-sync.core.ts
|
|
1634
1563
|
init_config();
|
|
1635
|
-
import { existsSync as
|
|
1636
|
-
import { join as
|
|
1564
|
+
import { cpSync as cpSync2, existsSync as existsSync2, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, rmSync as rmSync2 } from "node:fs";
|
|
1565
|
+
import { basename as basename2, join as join3, relative } from "node:path";
|
|
1566
|
+
|
|
1567
|
+
// src/extras-sync.collision.ts
|
|
1637
1568
|
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
|
-
}
|
|
1569
|
+
import { cpSync, existsSync, lstatSync, readdirSync, rmSync } from "node:fs";
|
|
1570
|
+
import { basename, join as join2 } from "node:path";
|
|
1571
|
+
function quoted(p) {
|
|
1572
|
+
return `"${p}"`;
|
|
1646
1573
|
}
|
|
1647
|
-
function
|
|
1648
|
-
if (
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1574
|
+
function stripCollidingDstSymlinks(src, dst, isExcluded) {
|
|
1575
|
+
if (!existsSync(dst)) return;
|
|
1576
|
+
for (const name of readdirSync(src)) {
|
|
1577
|
+
if (isExcluded(name)) continue;
|
|
1578
|
+
const dstPath = join2(dst, name);
|
|
1579
|
+
const dstStat = lstatSync(dstPath, { throwIfNoEntry: false });
|
|
1580
|
+
if (dstStat === void 0) continue;
|
|
1581
|
+
if (dstStat.isSymbolicLink()) {
|
|
1582
|
+
rmSync(dstPath, { recursive: true, force: true });
|
|
1583
|
+
} else if (dstStat.isDirectory() && lstatSync(join2(src, name)).isDirectory()) {
|
|
1584
|
+
stripCollidingDstSymlinks(join2(src, name), dstPath, isExcluded);
|
|
1585
|
+
}
|
|
1652
1586
|
}
|
|
1653
1587
|
}
|
|
1654
|
-
function
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
if (!existsSync4(target)) continue;
|
|
1664
|
-
if (dryRun) {
|
|
1665
|
-
emitAutoMove(onPreview, linkPath, ts, name);
|
|
1666
|
-
continue;
|
|
1588
|
+
function cpSyncGuarded(src, dst, filter, label) {
|
|
1589
|
+
try {
|
|
1590
|
+
cpSync(src, dst, { recursive: true, force: true, verbatimSymlinks: true, filter });
|
|
1591
|
+
} catch (err) {
|
|
1592
|
+
const e = err;
|
|
1593
|
+
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) {
|
|
1594
|
+
throw new NomadFatal(
|
|
1595
|
+
`${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`
|
|
1596
|
+
);
|
|
1667
1597
|
}
|
|
1668
|
-
|
|
1669
|
-
rmSync2(linkPath, { recursive: true, force: true });
|
|
1598
|
+
throw err;
|
|
1670
1599
|
}
|
|
1671
1600
|
}
|
|
1672
|
-
function
|
|
1673
|
-
const
|
|
1674
|
-
|
|
1675
|
-
|
|
1601
|
+
function prunePreservingBy(src, dst, isPreserved) {
|
|
1602
|
+
for (const name of readdirSync(dst)) {
|
|
1603
|
+
if (isPreserved(name)) continue;
|
|
1604
|
+
const dstPath = join2(dst, name);
|
|
1605
|
+
const srcStat = lstatSync(join2(src, name), { throwIfNoEntry: false });
|
|
1606
|
+
if (srcStat === void 0) {
|
|
1607
|
+
rmSync(dstPath, { recursive: true, force: true });
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
const dstStat = lstatSync(dstPath);
|
|
1611
|
+
if (srcStat.isDirectory() && dstStat.isDirectory()) {
|
|
1612
|
+
prunePreservingBy(join2(src, name), dstPath, isPreserved);
|
|
1613
|
+
} else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
|
|
1614
|
+
rmSync(dstPath, { recursive: true, force: true });
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
function copyExtrasFilteredPreservingBy(src, dst, isPreserved) {
|
|
1619
|
+
const dstStat = lstatSync(dst, { throwIfNoEntry: false });
|
|
1620
|
+
if (dstStat !== void 0) {
|
|
1621
|
+
if (dstStat.isDirectory()) {
|
|
1622
|
+
const srcStat = lstatSync(src, { throwIfNoEntry: false });
|
|
1623
|
+
if (srcStat !== void 0 && !srcStat.isDirectory()) {
|
|
1624
|
+
throw new NomadFatal(
|
|
1625
|
+
`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`
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
prunePreservingBy(src, dst, isPreserved);
|
|
1629
|
+
} else {
|
|
1630
|
+
rmSync(dst, { recursive: true, force: true });
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
stripCollidingDstSymlinks(src, dst, isPreserved);
|
|
1634
|
+
cpSync(src, dst, {
|
|
1635
|
+
recursive: true,
|
|
1636
|
+
force: true,
|
|
1637
|
+
verbatimSymlinks: true,
|
|
1638
|
+
filter: (srcEntry) => srcEntry === src || !isPreserved(basename(srcEntry))
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// src/extras-sync.guards.ts
|
|
1643
|
+
init_utils();
|
|
1644
|
+
init_config_sharedDirs_guard();
|
|
1645
|
+
import { isAbsolute, normalize, sep } from "node:path";
|
|
1646
|
+
function assertSafeLocalRoot(localRoot, logical) {
|
|
1647
|
+
if (!isAbsolute(localRoot)) {
|
|
1648
|
+
throw new NomadFatal(
|
|
1649
|
+
`invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1652
|
+
const canonical = process.platform === "win32" ? localRoot.split("/").join(sep) : localRoot;
|
|
1653
|
+
if (canonical !== normalize(canonical)) {
|
|
1654
|
+
throw new NomadFatal(
|
|
1655
|
+
`invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
|
|
1656
|
+
);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// src/extras-sync.core.ts
|
|
1661
|
+
init_utils();
|
|
1662
|
+
init_utils_json();
|
|
1663
|
+
function loadValidatedExtras(opts) {
|
|
1664
|
+
const repo = repoHome();
|
|
1665
|
+
const mapPath = join3(repo, "path-map.json");
|
|
1666
|
+
const repoExtras = join3(repo, "shared", "extras");
|
|
1667
|
+
if (!existsSync2(mapPath) || opts.requireRepoExtras === true && !existsSync2(repoExtras)) {
|
|
1668
|
+
if (opts.missingMsg !== void 0) log(opts.missingMsg);
|
|
1669
|
+
return null;
|
|
1670
|
+
}
|
|
1671
|
+
const map = readPathMap(mapPath);
|
|
1672
|
+
const extrasMap = map.extras ?? {};
|
|
1673
|
+
if (Object.keys(extrasMap).length === 0) return null;
|
|
1674
|
+
for (const logical of Object.keys(extrasMap)) {
|
|
1675
|
+
assertSafeLogical(logical);
|
|
1676
|
+
const localRoot = map.projects[logical]?.[HOST];
|
|
1677
|
+
if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
|
|
1678
|
+
}
|
|
1679
|
+
return { map, extrasMap };
|
|
1680
|
+
}
|
|
1681
|
+
function* eachExtrasTarget(v, counts) {
|
|
1682
|
+
const whitelist = SUPPORTED_EXTRAS;
|
|
1683
|
+
for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
|
|
1684
|
+
const localRoot = v.map.projects[logical]?.[HOST];
|
|
1685
|
+
if (!localRoot || localRoot === "TBD") {
|
|
1686
|
+
counts.unmapped++;
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
for (const dirname10 of dirnames) {
|
|
1690
|
+
if (!whitelist.includes(dirname10)) {
|
|
1691
|
+
counts.skipped++;
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
yield { logical, localRoot, dirname: dirname10 };
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
function copyExtrasOverlayFiltered(src, dst, blockSet) {
|
|
1699
|
+
stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
|
|
1700
|
+
cpSyncGuarded(
|
|
1701
|
+
src,
|
|
1702
|
+
dst,
|
|
1703
|
+
(srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry)),
|
|
1704
|
+
"copyExtrasOverlayFiltered"
|
|
1705
|
+
);
|
|
1706
|
+
}
|
|
1707
|
+
function copyExtrasOverlaySkipDiverged(src, dst, blockSet, divergedSet) {
|
|
1708
|
+
stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
|
|
1709
|
+
cpSyncGuarded(
|
|
1710
|
+
src,
|
|
1711
|
+
dst,
|
|
1712
|
+
(srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry)) && !divergedSet.has(relative(src, srcEntry)),
|
|
1713
|
+
"copyExtrasOverlaySkipDiverged"
|
|
1714
|
+
);
|
|
1715
|
+
}
|
|
1716
|
+
function copyExtras(src, dst) {
|
|
1717
|
+
rmSync2(dst, { recursive: true, force: true });
|
|
1718
|
+
cpSync2(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
|
|
1719
|
+
}
|
|
1720
|
+
function copyExtrasFileSkipDiverged(src, dst) {
|
|
1721
|
+
if (existsSync2(dst)) {
|
|
1722
|
+
let dstBuf;
|
|
1723
|
+
try {
|
|
1724
|
+
dstBuf = readFileSync2(dst);
|
|
1725
|
+
} catch {
|
|
1726
|
+
return;
|
|
1727
|
+
}
|
|
1728
|
+
if (!readFileSync2(src).equals(dstBuf)) return;
|
|
1729
|
+
}
|
|
1730
|
+
copyExtras(src, dst);
|
|
1731
|
+
}
|
|
1732
|
+
function extrasDenySet(dirname10) {
|
|
1733
|
+
return dirname10 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
|
|
1734
|
+
}
|
|
1735
|
+
function copyExtrasFiltered(src, dst, blockSet) {
|
|
1736
|
+
rmSync2(dst, { recursive: true, force: true });
|
|
1737
|
+
cpSync2(src, dst, {
|
|
1738
|
+
recursive: true,
|
|
1739
|
+
force: true,
|
|
1740
|
+
verbatimSymlinks: true,
|
|
1741
|
+
filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry))
|
|
1742
|
+
});
|
|
1743
|
+
}
|
|
1744
|
+
function prunePreservingDenied(src, dst, blockSet) {
|
|
1745
|
+
for (const name of readdirSync2(dst)) {
|
|
1746
|
+
if (isDeniedName(blockSet, name)) continue;
|
|
1747
|
+
const dstPath = join3(dst, name);
|
|
1748
|
+
const srcStat = lstatSync2(join3(src, name), { throwIfNoEntry: false });
|
|
1749
|
+
if (srcStat === void 0) {
|
|
1750
|
+
rmSync2(dstPath, { recursive: true, force: true });
|
|
1751
|
+
continue;
|
|
1752
|
+
}
|
|
1753
|
+
const dstStat = lstatSync2(dstPath);
|
|
1754
|
+
if (srcStat.isDirectory() && dstStat.isDirectory()) {
|
|
1755
|
+
prunePreservingDenied(join3(src, name), dstPath, blockSet);
|
|
1756
|
+
} else if (srcStat.isDirectory() !== dstStat.isDirectory()) {
|
|
1757
|
+
rmSync2(dstPath, { recursive: true, force: true });
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
function copyExtrasFilteredPreserving(src, dst, blockSet) {
|
|
1762
|
+
const dstStat = lstatSync2(dst, { throwIfNoEntry: false });
|
|
1763
|
+
if (dstStat !== void 0) {
|
|
1764
|
+
if (dstStat.isDirectory()) prunePreservingDenied(src, dst, blockSet);
|
|
1765
|
+
else rmSync2(dst, { recursive: true, force: true });
|
|
1766
|
+
}
|
|
1767
|
+
stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
|
|
1768
|
+
cpSync2(src, dst, {
|
|
1769
|
+
recursive: true,
|
|
1770
|
+
force: true,
|
|
1771
|
+
verbatimSymlinks: true,
|
|
1772
|
+
filter: (srcEntry) => srcEntry === src || !isDeniedName(blockSet, basename2(srcEntry))
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// src/links.ts
|
|
1777
|
+
init_utils();
|
|
1778
|
+
init_utils_fs();
|
|
1779
|
+
init_utils_json();
|
|
1780
|
+
function emitAutoMove(onPreview, linkPath, ts, name) {
|
|
1781
|
+
if (onPreview) {
|
|
1782
|
+
onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
|
|
1783
|
+
} else {
|
|
1784
|
+
log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
function emitCreate(onPreview, from, to) {
|
|
1788
|
+
if (onPreview) {
|
|
1789
|
+
onPreview({ kind: "create", from, to });
|
|
1790
|
+
} else {
|
|
1791
|
+
log(`would create symlink: ${from} -> ${to}`);
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
function emitCopy(onPreview, from, to) {
|
|
1795
|
+
if (onPreview) {
|
|
1796
|
+
onPreview({ kind: "copy", from, to });
|
|
1797
|
+
} else {
|
|
1798
|
+
log(`would copy: ${from} -> ${to}`);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
function isAlreadySymlink(linkPath) {
|
|
1802
|
+
return existsSync4(linkPath) && lstatSync4(linkPath).isSymbolicLink();
|
|
1803
|
+
}
|
|
1804
|
+
function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
|
|
1805
|
+
for (const name of linkNames) {
|
|
1806
|
+
const linkPath = join5(claude, name);
|
|
1807
|
+
const target = join5(repo, "shared", name);
|
|
1808
|
+
if (!existsSync4(linkPath)) continue;
|
|
1809
|
+
if (lstatSync4(linkPath).isSymbolicLink()) continue;
|
|
1810
|
+
if (!existsSync4(target)) continue;
|
|
1811
|
+
if (dryRun) {
|
|
1812
|
+
emitAutoMove(onPreview, linkPath, ts, name);
|
|
1813
|
+
continue;
|
|
1814
|
+
}
|
|
1815
|
+
backupBeforeWrite(linkPath, ts);
|
|
1816
|
+
rmSync3(linkPath, { recursive: true, force: true });
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
function copySharedLinkPull(src, dst) {
|
|
1820
|
+
copyExtrasFilteredPreservingBy(src, dst, (name) => isDeniedName(ALWAYS_NEVER_SYNC, name));
|
|
1821
|
+
}
|
|
1822
|
+
function applySharedLinksWin32(linkNames, claude, repo, ts, dryRun, onPreview) {
|
|
1823
|
+
for (const name of linkNames) {
|
|
1824
|
+
const target = join5(repo, "shared", name);
|
|
1825
|
+
if (!existsSync4(target)) continue;
|
|
1826
|
+
const linkPath = join5(claude, name);
|
|
1827
|
+
if (dryRun) {
|
|
1828
|
+
emitCopy(onPreview, linkPath, target);
|
|
1829
|
+
continue;
|
|
1830
|
+
}
|
|
1831
|
+
const stat = lstatSync4(linkPath, { throwIfNoEntry: false });
|
|
1832
|
+
if (stat !== void 0) {
|
|
1833
|
+
backupBeforeWrite(linkPath, ts);
|
|
1834
|
+
if (stat.isSymbolicLink()) {
|
|
1835
|
+
rmSync3(linkPath, { recursive: true, force: true });
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
copySharedLinkPull(target, linkPath);
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function applySharedLinks(ts, map, opts = {}) {
|
|
1842
|
+
const dryRun = opts.dryRun === true;
|
|
1843
|
+
const claude = claudeHome();
|
|
1844
|
+
const repo = repoHome();
|
|
1676
1845
|
const linkNames = allSharedLinks(map);
|
|
1846
|
+
if (process.platform === "win32") {
|
|
1847
|
+
applySharedLinksWin32(linkNames, claude, repo, ts, dryRun, opts.onPreview);
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1677
1850
|
runAutoMovePasses(linkNames, claude, repo, ts, dryRun, opts.onPreview);
|
|
1678
1851
|
for (const name of linkNames) {
|
|
1679
1852
|
const target = join5(repo, "shared", name);
|
|
@@ -1687,63 +1860,286 @@ function applySharedLinks(ts, map, opts = {}) {
|
|
|
1687
1860
|
ensureSymlink(linkPath, target);
|
|
1688
1861
|
}
|
|
1689
1862
|
}
|
|
1690
|
-
function
|
|
1691
|
-
|
|
1692
|
-
|
|
1863
|
+
function syncSharedLinksPush(map) {
|
|
1864
|
+
if (process.platform !== "win32") return;
|
|
1865
|
+
if (map === null) return;
|
|
1866
|
+
const claude = claudeHome();
|
|
1867
|
+
const repo = repoHome();
|
|
1868
|
+
for (const name of allSharedLinks(map)) {
|
|
1869
|
+
const localPath = join5(claude, name);
|
|
1870
|
+
const stat = lstatSync4(localPath, { throwIfNoEntry: false });
|
|
1871
|
+
if (stat === void 0) continue;
|
|
1872
|
+
if (stat.isSymbolicLink()) continue;
|
|
1873
|
+
copyExtrasFiltered(localPath, join5(repo, "shared", name), ALWAYS_NEVER_SYNC);
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
function readExistingSettings(settingsPath) {
|
|
1877
|
+
if (!existsSync4(settingsPath)) return { existing: {}, present: false, malformed: false };
|
|
1878
|
+
try {
|
|
1879
|
+
const parsed = readJson(settingsPath);
|
|
1880
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1881
|
+
return { existing: {}, present: true, malformed: true };
|
|
1882
|
+
}
|
|
1883
|
+
return { existing: parsed, present: true, malformed: false };
|
|
1884
|
+
} catch {
|
|
1885
|
+
return { existing: {}, present: true, malformed: true };
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
function emitDriftWarnings(merged, existing) {
|
|
1889
|
+
const drift = classifySettingsDrift(merged, existing);
|
|
1890
|
+
if (drift.behind.length > 0) {
|
|
1891
|
+
const { phrase, pronoun } = describeSettings(drift.behind);
|
|
1892
|
+
warn(
|
|
1893
|
+
`your settings.json is missing ${phrase} that the synced copy has; run 'nomad pull' to restore ${pronoun}.`
|
|
1894
|
+
);
|
|
1895
|
+
}
|
|
1896
|
+
const { promotable } = partitionByCaptureExclusion(drift.ahead);
|
|
1897
|
+
if (promotable.length > 0) {
|
|
1898
|
+
const { phrase, pronoun, verb } = describeSettings(promotable);
|
|
1899
|
+
warn(
|
|
1900
|
+
`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}.`
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
function regenerateSettings(ts, opts = {}) {
|
|
1905
|
+
const dryRun = opts.dryRun === true;
|
|
1906
|
+
const suppressDriftWarn = opts.suppressDriftWarn === true;
|
|
1907
|
+
const repo = repoHome();
|
|
1908
|
+
const claude = claudeHome();
|
|
1909
|
+
const basePath = join5(repo, "shared", "settings.base.json");
|
|
1910
|
+
const hostPath = join5(repo, "hosts", `${HOST}.json`);
|
|
1911
|
+
if (!existsSync4(basePath)) {
|
|
1912
|
+
die("repo not initialized; run 'nomad init' to scaffold");
|
|
1913
|
+
}
|
|
1914
|
+
const base = readJson(basePath);
|
|
1915
|
+
const hasOverrides = existsSync4(hostPath);
|
|
1916
|
+
const overrides = hasOverrides ? readJson(hostPath) : {};
|
|
1917
|
+
const merged = deepMerge(base, overrides);
|
|
1918
|
+
const settingsPath = join5(claude, "settings.json");
|
|
1919
|
+
const { existing, present, malformed } = readExistingSettings(settingsPath);
|
|
1920
|
+
if (!suppressDriftWarn && present) {
|
|
1921
|
+
if (malformed) {
|
|
1922
|
+
warn("existing settings.json is malformed; skipping drift-check and regenerating.");
|
|
1923
|
+
} else {
|
|
1924
|
+
emitDriftWarnings(merged, existing);
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1927
|
+
const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
|
|
1928
|
+
if (dryRun) {
|
|
1929
|
+
log(`would write settings.json (base + ${overrideLabel})`);
|
|
1930
|
+
return { label: overrideLabel };
|
|
1931
|
+
}
|
|
1932
|
+
backupBeforeWrite(settingsPath, ts);
|
|
1933
|
+
writeJsonAtomic(
|
|
1934
|
+
settingsPath,
|
|
1935
|
+
graftGsdHookEntries(stripGsdHookEntries(merged), keepGsdHookEntries(existing))
|
|
1936
|
+
);
|
|
1937
|
+
return { label: overrideLabel };
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
// src/commands.adopt.ts
|
|
1941
|
+
init_utils();
|
|
1942
|
+
init_utils_fs();
|
|
1943
|
+
init_utils_json();
|
|
1944
|
+
var ADOPT_PUSH_HINT = "run `nomad push` to share with other hosts";
|
|
1945
|
+
function lexists(p) {
|
|
1946
|
+
try {
|
|
1947
|
+
lstatSync5(p);
|
|
1948
|
+
return true;
|
|
1949
|
+
} catch {
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
function readMapIfPresent(repoHome2) {
|
|
1954
|
+
const mapPath = join6(repoHome2, "path-map.json");
|
|
1955
|
+
return existsSync5(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
1956
|
+
}
|
|
1957
|
+
function isConfiguredTarget(name, map) {
|
|
1958
|
+
return SHARED_LINKS.includes(name) || (map.sharedDirs?.includes(name) ?? false);
|
|
1959
|
+
}
|
|
1960
|
+
function isValidAdoptName(name) {
|
|
1961
|
+
if (SHARED_LINKS.includes(name)) return true;
|
|
1962
|
+
return isValidSharedDir(name);
|
|
1963
|
+
}
|
|
1964
|
+
function performAdoptMove(name, linkPath, sharedTarget, repo, backup) {
|
|
1965
|
+
const ts = freshBackupTs(backup);
|
|
1966
|
+
backupBeforeWrite(linkPath, ts);
|
|
1967
|
+
cpSync4(linkPath, sharedTarget, { recursive: true, force: true, preserveTimestamps: true });
|
|
1968
|
+
rmSync4(linkPath, { recursive: true, force: true });
|
|
1969
|
+
if (process.platform === "win32") {
|
|
1970
|
+
copySharedLinkPull(sharedTarget, linkPath);
|
|
1971
|
+
} else {
|
|
1972
|
+
ensureSymlink(linkPath, sharedTarget);
|
|
1973
|
+
}
|
|
1974
|
+
const rel = join6("shared", name);
|
|
1975
|
+
gitOrFatal(["add", "--", rel], `git add shared/${name}`, repo);
|
|
1976
|
+
log(`adopted ${name}; ${ADOPT_PUSH_HINT}`);
|
|
1977
|
+
}
|
|
1978
|
+
function cmdAdopt(name, opts = {}) {
|
|
1979
|
+
const dryRun = opts.dryRun === true;
|
|
1980
|
+
if (!isValidAdoptName(name)) {
|
|
1981
|
+
fail(`invalid name: ${JSON.stringify(name)}`);
|
|
1982
|
+
process.exit(1);
|
|
1983
|
+
}
|
|
1984
|
+
const repo = repoHome();
|
|
1985
|
+
const claude = claudeHome();
|
|
1986
|
+
const backup = backupBase();
|
|
1987
|
+
const map = readMapIfPresent(repo);
|
|
1988
|
+
if (!isConfiguredTarget(name, map)) {
|
|
1989
|
+
fail(
|
|
1990
|
+
`${name}: not a configured shared target. Add it to sharedDirs in path-map.json first, then re-run adopt.`
|
|
1991
|
+
);
|
|
1992
|
+
process.exit(1);
|
|
1993
|
+
}
|
|
1994
|
+
const linkPath = join6(claude, name);
|
|
1995
|
+
const sharedTarget = join6(repo, "shared", name);
|
|
1996
|
+
if (!existsSync5(linkPath)) {
|
|
1997
|
+
log(`${name}: nothing to adopt (not present in ~/.claude/)`);
|
|
1998
|
+
return;
|
|
1999
|
+
}
|
|
2000
|
+
if (lstatSync5(linkPath).isSymbolicLink()) {
|
|
2001
|
+
log(`${name}: already adopted (already a symlink)`);
|
|
2002
|
+
return;
|
|
2003
|
+
}
|
|
2004
|
+
if (process.platform === "win32" && lexists(sharedTarget)) {
|
|
2005
|
+
log(`${name}: already adopted (win32 copy-sync); run \`nomad pull\` to refresh the local copy`);
|
|
2006
|
+
return;
|
|
2007
|
+
}
|
|
2008
|
+
if (lexists(sharedTarget)) {
|
|
2009
|
+
fail(`${name}: shared/${name} already exists; would clobber. Remove it first.`);
|
|
2010
|
+
process.exit(1);
|
|
2011
|
+
}
|
|
2012
|
+
if (dryRun) {
|
|
2013
|
+
const ts = freshBackupTs(backup);
|
|
2014
|
+
log(`would backup: ${linkPath} -> backup/${ts}/${name}`);
|
|
2015
|
+
log(`would move: ${linkPath} -> shared/${name}`);
|
|
2016
|
+
log(
|
|
2017
|
+
process.platform === "win32" ? `would copy back: shared/${name} -> ${linkPath} (win32 copy-sync)` : `would relink: ${linkPath} -> shared/${name}`
|
|
2018
|
+
);
|
|
2019
|
+
log(`would stage: shared/${name}`);
|
|
2020
|
+
return;
|
|
2021
|
+
}
|
|
2022
|
+
try {
|
|
2023
|
+
performAdoptMove(name, linkPath, sharedTarget, repo, backup);
|
|
2024
|
+
} catch (err) {
|
|
2025
|
+
if (!(err instanceof NomadFatal)) throw err;
|
|
2026
|
+
fail(err.message);
|
|
2027
|
+
process.exitCode = 1;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
// src/commands.allow.ts
|
|
2032
|
+
init_config();
|
|
2033
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
2034
|
+
|
|
2035
|
+
// src/commands.redact.core.ts
|
|
2036
|
+
import { appendFileSync, readFileSync as readFileSync3 } from "node:fs";
|
|
2037
|
+
import { join as join7 } from "node:path";
|
|
2038
|
+
function collectMatchIntervals(content, findings) {
|
|
2039
|
+
const intervals = [];
|
|
2040
|
+
for (const f of findings) {
|
|
2041
|
+
const match = f.Match;
|
|
2042
|
+
if (match === "") continue;
|
|
2043
|
+
let from = 0;
|
|
2044
|
+
let pos = content.indexOf(match, from);
|
|
2045
|
+
while (pos !== -1) {
|
|
2046
|
+
intervals.push({ start: pos, end: pos + match.length, ruleId: f.RuleID });
|
|
2047
|
+
from = pos + match.length;
|
|
2048
|
+
pos = content.indexOf(match, from);
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
return intervals;
|
|
2052
|
+
}
|
|
2053
|
+
function mergeIntervals(intervals) {
|
|
2054
|
+
if (intervals.length === 0) return [];
|
|
2055
|
+
const sorted = [...intervals].sort((a, b) => a.start - b.start || b.end - a.end);
|
|
2056
|
+
let last = { ...sorted[0] };
|
|
2057
|
+
const merged = [last];
|
|
2058
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
2059
|
+
const cur = sorted[i];
|
|
2060
|
+
if (cur.start <= last.end) {
|
|
2061
|
+
if (cur.end > last.end) last.end = cur.end;
|
|
2062
|
+
} else {
|
|
2063
|
+
last = { ...cur };
|
|
2064
|
+
merged.push(last);
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
return merged;
|
|
2068
|
+
}
|
|
2069
|
+
function applyRedactions(content, findings) {
|
|
2070
|
+
const raw = collectMatchIntervals(content, findings);
|
|
2071
|
+
if (raw.length === 0) return content;
|
|
2072
|
+
const merged = mergeIntervals(raw);
|
|
2073
|
+
let result = content;
|
|
2074
|
+
for (let i = merged.length - 1; i >= 0; i--) {
|
|
2075
|
+
const { start, end, ruleId } = merged[i];
|
|
2076
|
+
result = result.slice(0, start) + `[REDACTED:${ruleId}]` + result.slice(end);
|
|
2077
|
+
}
|
|
2078
|
+
return result;
|
|
2079
|
+
}
|
|
2080
|
+
function formatFingerprint(fingerprint) {
|
|
2081
|
+
return fingerprint.replace(/[\r\n]/g, "") + "\n";
|
|
2082
|
+
}
|
|
2083
|
+
function isRecentlyModified(mtimeMs, nowMs, thresholdMs = 5 * 60 * 1e3) {
|
|
2084
|
+
return nowMs - mtimeMs < thresholdMs;
|
|
2085
|
+
}
|
|
2086
|
+
function isValidFingerprint(fingerprint) {
|
|
2087
|
+
return fingerprint.trim().length > 0 && fingerprint.length <= 512 && /^[^\r\n]+$/.test(fingerprint);
|
|
2088
|
+
}
|
|
2089
|
+
function isAlreadyPresent(line, lines) {
|
|
2090
|
+
return lines.includes(line);
|
|
2091
|
+
}
|
|
2092
|
+
function appendGitleaksIgnore(fingerprint, repo) {
|
|
2093
|
+
const sanitized = fingerprint.replace(/[\r\n]/g, "").trim();
|
|
2094
|
+
if (sanitized.length === 0) return;
|
|
2095
|
+
const ignPath = join7(repo, ".gitleaksignore");
|
|
2096
|
+
let raw;
|
|
2097
|
+
try {
|
|
2098
|
+
raw = readFileSync3(ignPath, "utf8");
|
|
2099
|
+
} catch {
|
|
2100
|
+
raw = "";
|
|
2101
|
+
}
|
|
2102
|
+
const existing = raw.split("\n").filter((l) => l.length > 0);
|
|
2103
|
+
if (isAlreadyPresent(sanitized, existing)) return;
|
|
2104
|
+
const needsLeadingNewline = raw.length > 0 && !raw.endsWith("\n");
|
|
2105
|
+
const prefix = needsLeadingNewline ? "\n" : "";
|
|
2106
|
+
appendFileSync(ignPath, prefix + formatFingerprint(sanitized), "utf8");
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// src/commands.allow.ts
|
|
2110
|
+
init_utils();
|
|
2111
|
+
function cmdAllow(fingerprints) {
|
|
1693
2112
|
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.");
|
|
2113
|
+
if (!existsSync6(repo)) die(`repo not cloned at ${repo}`);
|
|
2114
|
+
for (const fp of fingerprints) {
|
|
2115
|
+
if (!isValidFingerprint(fp)) {
|
|
2116
|
+
const shown = fp.replaceAll("\r", String.raw`\r`).replaceAll("\n", String.raw`\n`);
|
|
2117
|
+
fail(`invalid fingerprint: ${shown}`);
|
|
2118
|
+
process.exit(1);
|
|
1724
2119
|
}
|
|
1725
2120
|
}
|
|
1726
|
-
const
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
return { label: overrideLabel };
|
|
2121
|
+
for (const fp of fingerprints) {
|
|
2122
|
+
appendGitleaksIgnore(fp, repo);
|
|
2123
|
+
item(`allowed: ${fp}`);
|
|
1730
2124
|
}
|
|
1731
|
-
|
|
1732
|
-
writeJsonAtomic(settingsPath, stripGsdHookEntries(merged));
|
|
1733
|
-
return { label: overrideLabel };
|
|
2125
|
+
log(`allowed ${fingerprints.length} fingerprint(s)`);
|
|
1734
2126
|
}
|
|
1735
2127
|
|
|
1736
2128
|
// src/commands.capture-settings.ts
|
|
2129
|
+
init_config();
|
|
2130
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
2131
|
+
import { join as join9 } from "node:path";
|
|
2132
|
+
import { createInterface } from "node:readline/promises";
|
|
1737
2133
|
init_utils_fs();
|
|
1738
2134
|
init_utils_json();
|
|
1739
2135
|
|
|
1740
2136
|
// src/utils.lockfile.ts
|
|
1741
2137
|
init_config();
|
|
1742
2138
|
init_utils();
|
|
1743
|
-
import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as
|
|
1744
|
-
import { dirname as dirname2, join as
|
|
2139
|
+
import { closeSync as closeSync2, mkdirSync as mkdirSync2, openSync as openSync2, readFileSync as readFileSync4, unlinkSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2140
|
+
import { dirname as dirname2, join as join8 } from "node:path";
|
|
1745
2141
|
function lockFilePath() {
|
|
1746
|
-
return
|
|
2142
|
+
return join8(home(), ".cache", "claude-nomad", "nomad.lock");
|
|
1747
2143
|
}
|
|
1748
2144
|
function acquireLock(verb) {
|
|
1749
2145
|
const lp = lockFilePath();
|
|
@@ -1786,7 +2182,7 @@ function releaseLock(handle) {
|
|
|
1786
2182
|
function unlinkIfSamePid(expectedPidStr, lp) {
|
|
1787
2183
|
let current;
|
|
1788
2184
|
try {
|
|
1789
|
-
current =
|
|
2185
|
+
current = readFileSync4(lp, "utf8").trim();
|
|
1790
2186
|
} catch {
|
|
1791
2187
|
return false;
|
|
1792
2188
|
}
|
|
@@ -1801,7 +2197,7 @@ function unlinkIfSamePid(expectedPidStr, lp) {
|
|
|
1801
2197
|
function checkStaleAndRetry(verb, lp) {
|
|
1802
2198
|
let pidStr;
|
|
1803
2199
|
try {
|
|
1804
|
-
pidStr =
|
|
2200
|
+
pidStr = readFileSync4(lp, "utf8").trim();
|
|
1805
2201
|
} catch {
|
|
1806
2202
|
pidStr = "";
|
|
1807
2203
|
}
|
|
@@ -1869,30 +2265,30 @@ async function confirmCapture(destLabel, keys) {
|
|
|
1869
2265
|
}
|
|
1870
2266
|
}
|
|
1871
2267
|
function resolveCaptureDestination(repo, useHost) {
|
|
1872
|
-
const destPath = useHost ?
|
|
1873
|
-
const existing =
|
|
2268
|
+
const destPath = useHost ? join9(repo, "hosts", `${HOST}.json`) : join9(repo, "shared", "settings.base.json");
|
|
2269
|
+
const existing = existsSync7(destPath) ? readJson(destPath) : {};
|
|
1874
2270
|
return { destPath, existing };
|
|
1875
2271
|
}
|
|
1876
2272
|
async function cmdCaptureSettings(opts) {
|
|
1877
2273
|
const { host: useHost, dryRun } = opts;
|
|
1878
2274
|
const repo = repoHome();
|
|
1879
|
-
if (!
|
|
2275
|
+
if (!existsSync7(repo)) die(`repo not cloned at ${repo}`);
|
|
1880
2276
|
const handle = acquireLock("capture-settings");
|
|
1881
2277
|
if (handle === null) process.exit(0);
|
|
1882
2278
|
try {
|
|
1883
2279
|
const claude = claudeHome();
|
|
1884
|
-
const basePath =
|
|
1885
|
-
if (!
|
|
2280
|
+
const basePath = join9(repo, "shared", "settings.base.json");
|
|
2281
|
+
if (!existsSync7(basePath)) {
|
|
1886
2282
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
1887
2283
|
}
|
|
1888
|
-
const settingsPath =
|
|
1889
|
-
if (!
|
|
2284
|
+
const settingsPath = join9(claude, "settings.json");
|
|
2285
|
+
if (!existsSync7(settingsPath)) {
|
|
1890
2286
|
log("no ~/.claude/settings.json found; nothing to capture");
|
|
1891
2287
|
return;
|
|
1892
2288
|
}
|
|
1893
2289
|
const base = readJson(basePath);
|
|
1894
|
-
const hostPath =
|
|
1895
|
-
const overrides =
|
|
2290
|
+
const hostPath = join9(repo, "hosts", `${HOST}.json`);
|
|
2291
|
+
const overrides = existsSync7(hostPath) ? readJson(hostPath) : {};
|
|
1896
2292
|
const merged = deepMerge(base, overrides);
|
|
1897
2293
|
const settings = readJson(settingsPath);
|
|
1898
2294
|
const subset = buildCaptureSubset(merged, settings, { normalizeNodePath: !useHost });
|
|
@@ -1929,8 +2325,8 @@ async function cmdCaptureSettings(opts) {
|
|
|
1929
2325
|
// src/commands.clean.ts
|
|
1930
2326
|
init_config();
|
|
1931
2327
|
init_utils();
|
|
1932
|
-
import { existsSync as
|
|
1933
|
-
import { join as
|
|
2328
|
+
import { existsSync as existsSync8, lstatSync as lstatSync6, readdirSync as readdirSync3, rmSync as rmSync5, statSync as statSync2 } from "node:fs";
|
|
2329
|
+
import { join as join10 } from "node:path";
|
|
1934
2330
|
var TS_SHAPE = /^\d{8}-\d{6}(-\d+)?$/;
|
|
1935
2331
|
var DURATION_RE = /^(\d+)([dhm])$/;
|
|
1936
2332
|
var UNIT_MS = { d: 864e5, h: 36e5, m: 6e4 };
|
|
@@ -1944,8 +2340,8 @@ function parseDuration(s) {
|
|
|
1944
2340
|
return Number(m[1]) * UNIT_MS[m[2]];
|
|
1945
2341
|
}
|
|
1946
2342
|
function listBackupDirs(backupBase2) {
|
|
1947
|
-
if (!
|
|
1948
|
-
return
|
|
2343
|
+
if (!existsSync8(backupBase2)) return [];
|
|
2344
|
+
return readdirSync3(backupBase2).filter(isTsDir).map((name) => ({ name, mtimeMs: statSync2(join10(backupBase2, name)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1949
2345
|
}
|
|
1950
2346
|
function prunableByAge(dirs, olderThanMs, nowMs) {
|
|
1951
2347
|
return dirs.filter((d) => nowMs - d.mtimeMs > olderThanMs).map((d) => d.name);
|
|
@@ -1955,10 +2351,10 @@ function prunableByCount(dirs, keep) {
|
|
|
1955
2351
|
}
|
|
1956
2352
|
function safeDelete(backupBase2, name) {
|
|
1957
2353
|
if (!isTsDir(name)) return;
|
|
1958
|
-
const full =
|
|
1959
|
-
const st =
|
|
2354
|
+
const full = join10(backupBase2, name);
|
|
2355
|
+
const st = lstatSync6(full, { throwIfNoEntry: false });
|
|
1960
2356
|
if (!st || st.isSymbolicLink() || !st.isDirectory()) return;
|
|
1961
|
-
|
|
2357
|
+
rmSync5(full, { recursive: true, force: true });
|
|
1962
2358
|
}
|
|
1963
2359
|
function resolveTargets(dirs, olderThanMs, keep) {
|
|
1964
2360
|
if (keep !== void 0) return prunableByCount(dirs, keep);
|
|
@@ -1993,9 +2389,10 @@ function cmdClean(opts, backupBase2 = backupBase()) {
|
|
|
1993
2389
|
// src/commands.eject.ts
|
|
1994
2390
|
init_config();
|
|
1995
2391
|
init_utils();
|
|
2392
|
+
init_utils_fs();
|
|
1996
2393
|
init_utils_json();
|
|
1997
|
-
import { cpSync as
|
|
1998
|
-
import { join as
|
|
2394
|
+
import { cpSync as cpSync5, existsSync as existsSync9, lstatSync as lstatSync7, realpathSync, rmSync as rmSync6 } from "node:fs";
|
|
2395
|
+
import { join as join11, sep as sep3 } from "node:path";
|
|
1999
2396
|
function ejectChecklist() {
|
|
2000
2397
|
return [
|
|
2001
2398
|
"Manual steps remaining to finish leaving claude-nomad on this host:",
|
|
@@ -2011,33 +2408,33 @@ function errMessage(err) {
|
|
|
2011
2408
|
}
|
|
2012
2409
|
function lexists2(p) {
|
|
2013
2410
|
try {
|
|
2014
|
-
|
|
2411
|
+
lstatSync7(p);
|
|
2015
2412
|
return true;
|
|
2016
2413
|
} catch {
|
|
2017
2414
|
return false;
|
|
2018
2415
|
}
|
|
2019
2416
|
}
|
|
2020
2417
|
function readMapIfPresent2(repoHome2) {
|
|
2021
|
-
const mapPath =
|
|
2022
|
-
return
|
|
2418
|
+
const mapPath = join11(repoHome2, "path-map.json");
|
|
2419
|
+
return existsSync9(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
2023
2420
|
}
|
|
2024
2421
|
function classifyName(linkPath) {
|
|
2025
2422
|
if (!lexists2(linkPath)) return "absent";
|
|
2026
|
-
if (!
|
|
2027
|
-
if (!
|
|
2423
|
+
if (!lstatSync7(linkPath).isSymbolicLink()) return "skip-real";
|
|
2424
|
+
if (!existsSync9(linkPath)) return "dangling";
|
|
2028
2425
|
return "materialize";
|
|
2029
2426
|
}
|
|
2030
2427
|
function resolveSharedRoot(repoHome2) {
|
|
2031
2428
|
try {
|
|
2032
|
-
return realpathSync(
|
|
2429
|
+
return realpathSync(join11(repoHome2, "shared"));
|
|
2033
2430
|
} catch {
|
|
2034
2431
|
return die(
|
|
2035
|
-
`cannot resolve ${
|
|
2432
|
+
`cannot resolve ${join11(repoHome2, "shared")} (repo checkout incomplete). run \`nomad pull\` first, then re-run \`nomad eject\``
|
|
2036
2433
|
);
|
|
2037
2434
|
}
|
|
2038
2435
|
}
|
|
2039
2436
|
function isManagedTarget(target, sharedRoot) {
|
|
2040
|
-
return target.startsWith(sharedRoot +
|
|
2437
|
+
return target.startsWith(sharedRoot + sep3);
|
|
2041
2438
|
}
|
|
2042
2439
|
function materializeOne(name, linkPath, sharedRoot) {
|
|
2043
2440
|
const target = realpathSync(linkPath);
|
|
@@ -2047,33 +2444,36 @@ function materializeOne(name, linkPath, sharedRoot) {
|
|
|
2047
2444
|
}
|
|
2048
2445
|
const tmp = `${linkPath}.eject.tmp.${process.pid}.${Date.now()}`;
|
|
2049
2446
|
try {
|
|
2050
|
-
|
|
2051
|
-
|
|
2447
|
+
rmSync6(tmp, { recursive: true, force: true });
|
|
2448
|
+
cpSync5(target, tmp, {
|
|
2052
2449
|
recursive: true,
|
|
2053
2450
|
force: true,
|
|
2054
2451
|
dereference: true,
|
|
2055
2452
|
preserveTimestamps: true
|
|
2056
2453
|
});
|
|
2057
|
-
|
|
2058
|
-
|
|
2454
|
+
rmSync6(linkPath, { force: true });
|
|
2455
|
+
renameAtomicRetry(tmp, linkPath);
|
|
2059
2456
|
item(`ejected: ${name}`);
|
|
2060
2457
|
return true;
|
|
2061
2458
|
} catch (err) {
|
|
2062
2459
|
try {
|
|
2063
|
-
|
|
2460
|
+
rmSync6(tmp, { recursive: true, force: true });
|
|
2064
2461
|
} catch {
|
|
2065
2462
|
}
|
|
2066
2463
|
throw err;
|
|
2067
2464
|
}
|
|
2068
2465
|
}
|
|
2466
|
+
function skipRealMessage(name) {
|
|
2467
|
+
return process.platform === "win32" ? `already a real copy (win32 copy-sync): ${name}` : `skipped (not a symlink): ${name}`;
|
|
2468
|
+
}
|
|
2069
2469
|
function previewDryRun(names, classifications, claudeHome2, sharedRoot) {
|
|
2070
2470
|
for (const name of names) {
|
|
2071
2471
|
const cls = classifications.get(name);
|
|
2072
|
-
const linkPath =
|
|
2472
|
+
const linkPath = join11(claudeHome2, name);
|
|
2073
2473
|
if (cls === "absent") {
|
|
2074
2474
|
item(`skipped (absent): ${name}`);
|
|
2075
2475
|
} else if (cls === "skip-real") {
|
|
2076
|
-
item(
|
|
2476
|
+
item(skipRealMessage(name));
|
|
2077
2477
|
} else {
|
|
2078
2478
|
previewMaterialize(name, linkPath, sharedRoot);
|
|
2079
2479
|
}
|
|
@@ -2099,12 +2499,12 @@ function runLiveEject(names, classifications, claudeHome2, sharedRoot) {
|
|
|
2099
2499
|
let skipped = 0;
|
|
2100
2500
|
for (const name of names) {
|
|
2101
2501
|
const cls = classifications.get(name);
|
|
2102
|
-
const linkPath =
|
|
2502
|
+
const linkPath = join11(claudeHome2, name);
|
|
2103
2503
|
if (cls === "absent") {
|
|
2104
2504
|
item(`skipped (absent): ${name}`);
|
|
2105
2505
|
skipped++;
|
|
2106
2506
|
} else if (cls === "skip-real") {
|
|
2107
|
-
item(
|
|
2507
|
+
item(skipRealMessage(name));
|
|
2108
2508
|
skipped++;
|
|
2109
2509
|
} else if (materializeOneOrDie(name, linkPath, sharedRoot, done)) {
|
|
2110
2510
|
done.push(name);
|
|
@@ -2135,7 +2535,7 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
|
|
|
2135
2535
|
const names = allSharedLinks(map);
|
|
2136
2536
|
const classifications = /* @__PURE__ */ new Map();
|
|
2137
2537
|
for (const name of names) {
|
|
2138
|
-
classifications.set(name, classifyName(
|
|
2538
|
+
classifications.set(name, classifyName(join11(claudeHome2, name)));
|
|
2139
2539
|
}
|
|
2140
2540
|
const dangling = names.filter((n) => classifications.get(n) === "dangling");
|
|
2141
2541
|
if (dangling.length > 0) {
|
|
@@ -2158,14 +2558,14 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
|
|
|
2158
2558
|
}
|
|
2159
2559
|
|
|
2160
2560
|
// src/commands.doctor.ts
|
|
2161
|
-
import { existsSync as
|
|
2162
|
-
import { join as
|
|
2561
|
+
import { existsSync as existsSync34 } from "node:fs";
|
|
2562
|
+
import { join as join40 } from "node:path";
|
|
2163
2563
|
|
|
2164
2564
|
// src/commands.doctor.checks.repo.ts
|
|
2165
2565
|
init_color();
|
|
2166
2566
|
init_config();
|
|
2167
|
-
import { existsSync as
|
|
2168
|
-
import { join as
|
|
2567
|
+
import { existsSync as existsSync11, lstatSync as lstatSync8, readdirSync as readdirSync4, statSync as statSync3 } from "node:fs";
|
|
2568
|
+
import { join as join13 } from "node:path";
|
|
2169
2569
|
|
|
2170
2570
|
// src/commands.doctor.format.ts
|
|
2171
2571
|
init_color();
|
|
@@ -2243,15 +2643,15 @@ function readJsonSafe(path, label, section2) {
|
|
|
2243
2643
|
// src/init.classify.ts
|
|
2244
2644
|
init_config();
|
|
2245
2645
|
init_utils_json();
|
|
2246
|
-
import { existsSync as
|
|
2247
|
-
import { join as
|
|
2646
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
2647
|
+
import { join as join12 } from "node:path";
|
|
2248
2648
|
function classifyRepoState(repoHome2, host) {
|
|
2249
|
-
const basePath =
|
|
2250
|
-
const mapPath =
|
|
2251
|
-
const hostPath =
|
|
2252
|
-
const hasBase =
|
|
2253
|
-
const hasMap =
|
|
2254
|
-
const hasHost =
|
|
2649
|
+
const basePath = join12(repoHome2, "shared", "settings.base.json");
|
|
2650
|
+
const mapPath = join12(repoHome2, "path-map.json");
|
|
2651
|
+
const hostPath = join12(repoHome2, "hosts", `${host}.json`);
|
|
2652
|
+
const hasBase = existsSync10(basePath);
|
|
2653
|
+
const hasMap = existsSync10(mapPath);
|
|
2654
|
+
const hasHost = existsSync10(hostPath);
|
|
2255
2655
|
let mapEntryCount = 0;
|
|
2256
2656
|
if (hasMap) {
|
|
2257
2657
|
try {
|
|
@@ -2266,11 +2666,11 @@ function classifyRepoState(repoHome2, host) {
|
|
|
2266
2666
|
return "partial";
|
|
2267
2667
|
}
|
|
2268
2668
|
function reasonForPartial(repoHome2, host) {
|
|
2269
|
-
const basePath =
|
|
2270
|
-
const mapPath =
|
|
2271
|
-
const hostPath =
|
|
2272
|
-
if (!
|
|
2273
|
-
if (!
|
|
2669
|
+
const basePath = join12(repoHome2, "shared", "settings.base.json");
|
|
2670
|
+
const mapPath = join12(repoHome2, "path-map.json");
|
|
2671
|
+
const hostPath = join12(repoHome2, "hosts", `${host}.json`);
|
|
2672
|
+
if (!existsSync10(basePath)) return "- shared/settings.base.json missing";
|
|
2673
|
+
if (!existsSync10(mapPath)) return "- path-map.json missing";
|
|
2274
2674
|
let mapEntryCount;
|
|
2275
2675
|
try {
|
|
2276
2676
|
const map = readJson(mapPath);
|
|
@@ -2279,7 +2679,7 @@ function reasonForPartial(repoHome2, host) {
|
|
|
2279
2679
|
mapEntryCount = 0;
|
|
2280
2680
|
}
|
|
2281
2681
|
if (mapEntryCount === 0) return "- path-map.json.projects has no entries";
|
|
2282
|
-
if (!
|
|
2682
|
+
if (!existsSync10(hostPath)) return `- hosts/${host}.json missing`;
|
|
2283
2683
|
return "- partial state (unknown gap)";
|
|
2284
2684
|
}
|
|
2285
2685
|
|
|
@@ -2296,15 +2696,15 @@ function reportHostAndPaths(section2) {
|
|
|
2296
2696
|
if (isOverrideActive()) {
|
|
2297
2697
|
addItem(section2, `${dim(infoGlyph)} NOMAD_REPO: ${blue(repo)}`);
|
|
2298
2698
|
}
|
|
2299
|
-
addItem(section2, `${
|
|
2699
|
+
addItem(section2, `${existsSync11(repo) ? green(okGlyph) : yellow(warnGlyph)} repo: ${blue(repo)}`);
|
|
2300
2700
|
addItem(
|
|
2301
2701
|
section2,
|
|
2302
|
-
`${
|
|
2702
|
+
`${existsSync11(claude) ? green(okGlyph) : yellow(warnGlyph)} claude home: ${blue(claude)}`
|
|
2303
2703
|
);
|
|
2304
2704
|
}
|
|
2305
2705
|
function pathMapHostKeys() {
|
|
2306
|
-
const mapPath =
|
|
2307
|
-
if (!
|
|
2706
|
+
const mapPath = join13(repoHome(), "path-map.json");
|
|
2707
|
+
if (!existsSync11(mapPath)) return /* @__PURE__ */ new Set();
|
|
2308
2708
|
let raw;
|
|
2309
2709
|
try {
|
|
2310
2710
|
raw = readJson(mapPath);
|
|
@@ -2321,7 +2721,7 @@ function pathMapHostKeys() {
|
|
|
2321
2721
|
function hostOverrideLabels() {
|
|
2322
2722
|
let entries;
|
|
2323
2723
|
try {
|
|
2324
|
-
entries =
|
|
2724
|
+
entries = readdirSync4(join13(repoHome(), "hosts"));
|
|
2325
2725
|
} catch {
|
|
2326
2726
|
return /* @__PURE__ */ new Set();
|
|
2327
2727
|
}
|
|
@@ -2362,12 +2762,12 @@ function reportRepoState(section2) {
|
|
|
2362
2762
|
}
|
|
2363
2763
|
}
|
|
2364
2764
|
function repoHasSharedSource(name) {
|
|
2365
|
-
return
|
|
2765
|
+
return existsSync11(join13(repoHome(), "shared", name));
|
|
2366
2766
|
}
|
|
2367
2767
|
function classifySharedLink(name, p) {
|
|
2368
2768
|
let stat;
|
|
2369
2769
|
try {
|
|
2370
|
-
stat =
|
|
2770
|
+
stat = lstatSync8(p);
|
|
2371
2771
|
} catch (err) {
|
|
2372
2772
|
const code = err.code;
|
|
2373
2773
|
if (code === "ENOENT") {
|
|
@@ -2379,6 +2779,12 @@ function classifySharedLink(name, p) {
|
|
|
2379
2779
|
return { line: `${red(failGlyph)} ${name}: could not stat (${String(code)})`, fail: true };
|
|
2380
2780
|
}
|
|
2381
2781
|
if (!stat.isSymbolicLink()) {
|
|
2782
|
+
if (process.platform === "win32") {
|
|
2783
|
+
return {
|
|
2784
|
+
line: `${green(okGlyph)} ${name}: real copy (win32 copy-sync)`,
|
|
2785
|
+
fail: false
|
|
2786
|
+
};
|
|
2787
|
+
}
|
|
2382
2788
|
return {
|
|
2383
2789
|
line: `${red(failGlyph)} ${name}: NOT a symlink (blocks sync); run \`nomad adopt ${name}\` to fix`,
|
|
2384
2790
|
fail: true
|
|
@@ -2410,7 +2816,7 @@ function classifySymlinkTarget(name, p) {
|
|
|
2410
2816
|
function reportSharedLinks(section2, map) {
|
|
2411
2817
|
const claude = claudeHome();
|
|
2412
2818
|
for (const name of allSharedLinks(map)) {
|
|
2413
|
-
const p =
|
|
2819
|
+
const p = join13(claude, name);
|
|
2414
2820
|
const { line, fail: fail2 } = classifySharedLink(name, p);
|
|
2415
2821
|
addItem(section2, line);
|
|
2416
2822
|
if (fail2) process.exitCode = 1;
|
|
@@ -2419,10 +2825,10 @@ function reportSharedLinks(section2, map) {
|
|
|
2419
2825
|
function reportDroppedNamesMigration(section2) {
|
|
2420
2826
|
const claude = claudeHome();
|
|
2421
2827
|
for (const name of GSD_DROPPED_NAMES) {
|
|
2422
|
-
const p =
|
|
2828
|
+
const p = join13(claude, name);
|
|
2423
2829
|
let stat;
|
|
2424
2830
|
try {
|
|
2425
|
-
stat =
|
|
2831
|
+
stat = lstatSync8(p);
|
|
2426
2832
|
} catch {
|
|
2427
2833
|
continue;
|
|
2428
2834
|
}
|
|
@@ -2437,11 +2843,11 @@ function reportDroppedNamesMigration(section2) {
|
|
|
2437
2843
|
// src/commands.doctor.checks.settings.ts
|
|
2438
2844
|
init_color();
|
|
2439
2845
|
init_config();
|
|
2440
|
-
import { existsSync as
|
|
2441
|
-
import { join as
|
|
2846
|
+
import { existsSync as existsSync12, readdirSync as readdirSync5 } from "node:fs";
|
|
2847
|
+
import { join as join14 } from "node:path";
|
|
2442
2848
|
function loadBaseSettings(section2) {
|
|
2443
|
-
const basePath =
|
|
2444
|
-
if (!
|
|
2849
|
+
const basePath = join14(repoHome(), "shared", "settings.base.json");
|
|
2850
|
+
if (!existsSync12(basePath)) {
|
|
2445
2851
|
addItem(section2, `${red(failGlyph)} shared/settings.base.json missing at ${blue(basePath)}`);
|
|
2446
2852
|
process.exitCode = 1;
|
|
2447
2853
|
return null;
|
|
@@ -2449,8 +2855,8 @@ function loadBaseSettings(section2) {
|
|
|
2449
2855
|
return readJsonSafe(basePath, basePath, section2);
|
|
2450
2856
|
}
|
|
2451
2857
|
function loadAndReportSettings(section2) {
|
|
2452
|
-
const settingsPath =
|
|
2453
|
-
if (!
|
|
2858
|
+
const settingsPath = join14(claudeHome(), "settings.json");
|
|
2859
|
+
if (!existsSync12(settingsPath)) return null;
|
|
2454
2860
|
const settings = readJsonSafe(settingsPath, settingsPath, section2);
|
|
2455
2861
|
if (settings === null) return null;
|
|
2456
2862
|
const unknownKeys = Object.keys(settings).filter((k) => !KNOWN_SETTINGS_KEYS.has(k));
|
|
@@ -2466,13 +2872,13 @@ function loadAndReportSettings(section2) {
|
|
|
2466
2872
|
}
|
|
2467
2873
|
function reportHostOverrides(section2, base, settings) {
|
|
2468
2874
|
const repo = repoHome();
|
|
2469
|
-
const hostFile =
|
|
2875
|
+
const hostFile = join14(repo, "hosts", `${HOST}.json`);
|
|
2470
2876
|
let drift = [];
|
|
2471
2877
|
if (base !== null && settings !== null) {
|
|
2472
2878
|
const baseKeys = new Set(Object.keys(base));
|
|
2473
2879
|
drift = Object.keys(settings).filter((k) => !baseKeys.has(k));
|
|
2474
2880
|
}
|
|
2475
|
-
if (
|
|
2881
|
+
if (existsSync12(hostFile)) {
|
|
2476
2882
|
if (readJsonSafe(hostFile, hostFile, section2) !== null) {
|
|
2477
2883
|
addItem(section2, `${green(okGlyph)} host overrides: ${blue(hostFile)}`);
|
|
2478
2884
|
}
|
|
@@ -2481,9 +2887,9 @@ function reportHostOverrides(section2, base, settings) {
|
|
|
2481
2887
|
section2,
|
|
2482
2888
|
`${red(failGlyph)} no hosts/${HOST}.json AND settings.json has unbased keys ${JSON.stringify(drift)}`
|
|
2483
2889
|
);
|
|
2484
|
-
const hostsDir =
|
|
2485
|
-
if (
|
|
2486
|
-
const cands =
|
|
2890
|
+
const hostsDir = join14(repo, "hosts");
|
|
2891
|
+
if (existsSync12(hostsDir)) {
|
|
2892
|
+
const cands = readdirSync5(hostsDir).filter((f) => f.endsWith(".json"));
|
|
2487
2893
|
if (cands.length > 0) addItem(section2, `${dim(infoGlyph)} candidates: ${cands.join(", ")}`);
|
|
2488
2894
|
}
|
|
2489
2895
|
process.exitCode = 1;
|
|
@@ -2498,8 +2904,8 @@ function reportHostOverrides(section2, base, settings) {
|
|
|
2498
2904
|
// src/commands.doctor.checks.pathmap.ts
|
|
2499
2905
|
init_color();
|
|
2500
2906
|
init_config();
|
|
2501
|
-
import { existsSync as
|
|
2502
|
-
import { join as
|
|
2907
|
+
import { existsSync as existsSync13, readdirSync as readdirSync6 } from "node:fs";
|
|
2908
|
+
import { join as join15 } from "node:path";
|
|
2503
2909
|
init_utils_json();
|
|
2504
2910
|
function reportMappedProjects(section2, map) {
|
|
2505
2911
|
const mapped = Object.entries(map.projects).filter(([, hosts]) => hosts[HOST]);
|
|
@@ -2509,11 +2915,11 @@ function reportMappedProjects(section2, map) {
|
|
|
2509
2915
|
}
|
|
2510
2916
|
}
|
|
2511
2917
|
function reportUnmappedProjects(section2, map) {
|
|
2512
|
-
const localProjects =
|
|
2513
|
-
if (!
|
|
2918
|
+
const localProjects = join15(claudeHome(), "projects");
|
|
2919
|
+
if (!existsSync13(localProjects)) return;
|
|
2514
2920
|
let localDirs;
|
|
2515
2921
|
try {
|
|
2516
|
-
localDirs =
|
|
2922
|
+
localDirs = readdirSync6(localProjects);
|
|
2517
2923
|
} catch {
|
|
2518
2924
|
return;
|
|
2519
2925
|
}
|
|
@@ -2531,7 +2937,7 @@ function reportCurrentHostPathsMissing(section2, map) {
|
|
|
2531
2937
|
for (const [name, hosts] of Object.entries(map.projects)) {
|
|
2532
2938
|
const abspath = hosts[HOST];
|
|
2533
2939
|
if (!abspath || abspath === "TBD") continue;
|
|
2534
|
-
if (!
|
|
2940
|
+
if (!existsSync13(abspath)) {
|
|
2535
2941
|
addItem(
|
|
2536
2942
|
section2,
|
|
2537
2943
|
`${yellow(warnGlyph)} path-map: ${name} local path missing on ${HOST}: ${blue(abspath)}`
|
|
@@ -2562,8 +2968,8 @@ function reportPathCollisions(section2, map) {
|
|
|
2562
2968
|
else addItem(section2, `${green(okGlyph)} path-encoding: no collisions`);
|
|
2563
2969
|
}
|
|
2564
2970
|
function reportPathMap(section2) {
|
|
2565
|
-
const mapPath =
|
|
2566
|
-
if (!
|
|
2971
|
+
const mapPath = join15(repoHome(), "path-map.json");
|
|
2972
|
+
if (!existsSync13(mapPath)) {
|
|
2567
2973
|
addItem(section2, `${red(failGlyph)} path-map.json missing at ${blue(mapPath)}`);
|
|
2568
2974
|
process.exitCode = 1;
|
|
2569
2975
|
return;
|
|
@@ -2592,21 +2998,19 @@ function reportNeverSync(section2) {
|
|
|
2592
2998
|
|
|
2593
2999
|
// src/commands.doctor.checks.skills.ts
|
|
2594
3000
|
init_color();
|
|
2595
|
-
import { existsSync as
|
|
2596
|
-
import { join as
|
|
3001
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
3002
|
+
import { join as join16 } from "node:path";
|
|
2597
3003
|
init_config();
|
|
2598
3004
|
|
|
2599
3005
|
// src/extras-sync.diff.ts
|
|
2600
3006
|
init_utils();
|
|
2601
3007
|
import { execFileSync as execFileSync2 } from "node:child_process";
|
|
2602
|
-
import { relative as
|
|
3008
|
+
import { relative as relative3 } from "node:path";
|
|
2603
3009
|
function parseNameStatus(stdout) {
|
|
3010
|
+
const fields = stdout.split("\0").filter((f) => f.length > 0);
|
|
2604
3011
|
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) });
|
|
3012
|
+
for (let i = 0; i + 1 < fields.length; i += 2) {
|
|
3013
|
+
entries.push({ status: fields[i], path: fields[i + 1] });
|
|
2610
3014
|
}
|
|
2611
3015
|
return entries;
|
|
2612
3016
|
}
|
|
@@ -2619,13 +3023,13 @@ function parseDiffOutput(stdout) {
|
|
|
2619
3023
|
return parseNameStatus(stdout).map(labelEntry);
|
|
2620
3024
|
}
|
|
2621
3025
|
function parseModifiedPaths(stdout, a) {
|
|
2622
|
-
return parseNameStatus(stdout).filter((entry) => entry.status === "M").map((entry) =>
|
|
3026
|
+
return parseNameStatus(stdout).filter((entry) => entry.status === "M").map((entry) => relative3(a, entry.path));
|
|
2623
3027
|
}
|
|
2624
3028
|
function runNameStatusDiff(a, b, parse) {
|
|
2625
3029
|
try {
|
|
2626
3030
|
const stdout = execFileSync2(
|
|
2627
3031
|
"git",
|
|
2628
|
-
["diff", "--no-index", "--no-renames", "--name-status", a, b],
|
|
3032
|
+
["diff", "--no-index", "--no-renames", "-z", "--name-status", a, b],
|
|
2629
3033
|
{
|
|
2630
3034
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2631
3035
|
}
|
|
@@ -2670,13 +3074,13 @@ function isGsdDiffLine(line, localBase, sharedBase) {
|
|
|
2670
3074
|
return relative9.split("/")[0].startsWith(GSD_PREFIX);
|
|
2671
3075
|
}
|
|
2672
3076
|
function reportSkillsDivergence(section2) {
|
|
2673
|
-
const sharedSkills =
|
|
2674
|
-
const localSkills =
|
|
2675
|
-
if (!
|
|
3077
|
+
const sharedSkills = join16(repoHome(), "shared", "skills");
|
|
3078
|
+
const localSkills = join16(claudeHome(), "skills");
|
|
3079
|
+
if (!existsSync14(sharedSkills)) {
|
|
2676
3080
|
addItem(section2, `${dim(infoGlyph)} skills: no shared/skills/ to compare`);
|
|
2677
3081
|
return;
|
|
2678
3082
|
}
|
|
2679
|
-
if (!
|
|
3083
|
+
if (!existsSync14(localSkills)) {
|
|
2680
3084
|
addItem(section2, `${dim(infoGlyph)} skills: no local skills/ to compare`);
|
|
2681
3085
|
return;
|
|
2682
3086
|
}
|
|
@@ -2699,8 +3103,8 @@ function reportSkillsDivergence(section2) {
|
|
|
2699
3103
|
init_color();
|
|
2700
3104
|
init_config();
|
|
2701
3105
|
import { execFileSync as execFileSync5 } from "node:child_process";
|
|
2702
|
-
import { existsSync as
|
|
2703
|
-
import { join as
|
|
3106
|
+
import { existsSync as existsSync17 } from "node:fs";
|
|
3107
|
+
import { join as join20, relative as relative4 } from "node:path";
|
|
2704
3108
|
init_commands_pull_wedge();
|
|
2705
3109
|
init_push_checks();
|
|
2706
3110
|
init_utils();
|
|
@@ -2723,11 +3127,11 @@ function reportGitleaksProbe(section2) {
|
|
|
2723
3127
|
}
|
|
2724
3128
|
function reportGitlinks(section2) {
|
|
2725
3129
|
const repo = repoHome();
|
|
2726
|
-
const sharedDir =
|
|
2727
|
-
if (
|
|
3130
|
+
const sharedDir = join20(repo, "shared");
|
|
3131
|
+
if (existsSync17(sharedDir)) {
|
|
2728
3132
|
const gitlinks = findGitlinks(sharedDir);
|
|
2729
3133
|
for (const p of gitlinks) {
|
|
2730
|
-
const rel =
|
|
3134
|
+
const rel = relative4(repo, p).replaceAll("\\", "/");
|
|
2731
3135
|
addItem(
|
|
2732
3136
|
section2,
|
|
2733
3137
|
`${red(failGlyph)} gitlink: ${blue(rel)} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`
|
|
@@ -2822,13 +3226,13 @@ function reportGitIdentity(section2) {
|
|
|
2822
3226
|
|
|
2823
3227
|
// src/commands.doctor.checks.backups.ts
|
|
2824
3228
|
init_color();
|
|
2825
|
-
import { existsSync as
|
|
2826
|
-
import { join as
|
|
3229
|
+
import { existsSync as existsSync18, lstatSync as lstatSync9, readdirSync as readdirSync8 } from "node:fs";
|
|
3230
|
+
import { join as join21 } from "node:path";
|
|
2827
3231
|
init_config();
|
|
2828
3232
|
var TS_SHAPE2 = /^\d{8}-\d{6}(-\d+)?$/;
|
|
2829
3233
|
function safeReaddir(dir) {
|
|
2830
3234
|
try {
|
|
2831
|
-
return
|
|
3235
|
+
return readdirSync8(dir);
|
|
2832
3236
|
} catch {
|
|
2833
3237
|
return [];
|
|
2834
3238
|
}
|
|
@@ -2839,8 +3243,8 @@ var BYTES_PER_MB = 1024 * 1024;
|
|
|
2839
3243
|
function dirSizeBytes(dir) {
|
|
2840
3244
|
let bytes = 0;
|
|
2841
3245
|
for (const entry of safeReaddir(dir)) {
|
|
2842
|
-
const full =
|
|
2843
|
-
const st =
|
|
3246
|
+
const full = join21(dir, entry);
|
|
3247
|
+
const st = lstatSync9(full, { throwIfNoEntry: false });
|
|
2844
3248
|
if (!st) continue;
|
|
2845
3249
|
if (st.isSymbolicLink()) continue;
|
|
2846
3250
|
if (st.isDirectory()) bytes += dirSizeBytes(full);
|
|
@@ -2850,11 +3254,11 @@ function dirSizeBytes(dir) {
|
|
|
2850
3254
|
}
|
|
2851
3255
|
function totalSizeMb(backupBase2, dirs) {
|
|
2852
3256
|
let bytes = 0;
|
|
2853
|
-
for (const name of dirs) bytes += dirSizeBytes(
|
|
3257
|
+
for (const name of dirs) bytes += dirSizeBytes(join21(backupBase2, name));
|
|
2854
3258
|
return bytes / BYTES_PER_MB;
|
|
2855
3259
|
}
|
|
2856
3260
|
function reportBackupsCheck(section2, backupBase2 = backupBase()) {
|
|
2857
|
-
if (!
|
|
3261
|
+
if (!existsSync18(backupBase2)) return;
|
|
2858
3262
|
const dirs = safeReaddir(backupBase2).filter((n) => TS_SHAPE2.test(n));
|
|
2859
3263
|
const count = dirs.length;
|
|
2860
3264
|
const sizeMb = totalSizeMb(backupBase2, dirs);
|
|
@@ -2935,8 +3339,8 @@ function reportCheckRemote(section2) {
|
|
|
2935
3339
|
|
|
2936
3340
|
// src/commands.doctor.check-schema.ts
|
|
2937
3341
|
init_color();
|
|
2938
|
-
import { existsSync as
|
|
2939
|
-
import { join as
|
|
3342
|
+
import { existsSync as existsSync19 } from "node:fs";
|
|
3343
|
+
import { join as join22 } from "node:path";
|
|
2940
3344
|
init_config();
|
|
2941
3345
|
|
|
2942
3346
|
// src/http-fetch.ts
|
|
@@ -2973,8 +3377,8 @@ function fetchSchemaKeys() {
|
|
|
2973
3377
|
}
|
|
2974
3378
|
}
|
|
2975
3379
|
function reportCheckSchema(section2) {
|
|
2976
|
-
const settingsPath =
|
|
2977
|
-
if (!
|
|
3380
|
+
const settingsPath = join22(claudeHome(), "settings.json");
|
|
3381
|
+
if (!existsSync19(settingsPath)) {
|
|
2978
3382
|
addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json to check`);
|
|
2979
3383
|
return;
|
|
2980
3384
|
}
|
|
@@ -3004,18 +3408,18 @@ function reportCheckSchema(section2) {
|
|
|
3004
3408
|
init_color();
|
|
3005
3409
|
import { randomBytes } from "node:crypto";
|
|
3006
3410
|
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
3007
|
-
import { existsSync as
|
|
3411
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync6, readdirSync as readdirSync11, rmSync as rmSync10 } from "node:fs";
|
|
3008
3412
|
import { homedir as homedir4 } from "node:os";
|
|
3009
|
-
import { join as
|
|
3413
|
+
import { join as join27 } from "node:path";
|
|
3010
3414
|
|
|
3011
3415
|
// src/commands.doctor.check-shared.scan.ts
|
|
3012
3416
|
init_color();
|
|
3013
|
-
import { join as
|
|
3417
|
+
import { join as join24 } from "node:path";
|
|
3014
3418
|
init_config();
|
|
3015
3419
|
init_push_gitleaks();
|
|
3016
3420
|
function scrubPath(logical, sid, logicalToEncoded) {
|
|
3017
3421
|
const encoded = logicalToEncoded.get(logical) ?? logical;
|
|
3018
|
-
return
|
|
3422
|
+
return join24(claudeHome(), "projects", encoded, `${sid}.jsonl`);
|
|
3019
3423
|
}
|
|
3020
3424
|
function reportSessionFindings(section2, bySession) {
|
|
3021
3425
|
for (const [sid, counts] of bySession) {
|
|
@@ -3044,274 +3448,67 @@ function reportRemediation(section2, bySession, logicalBySession, logicalToEncod
|
|
|
3044
3448
|
}
|
|
3045
3449
|
addItem(section2, ` ${dim("- false positive? add a pattern to .gitleaks.toml")}`);
|
|
3046
3450
|
}
|
|
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
|
-
}
|
|
3177
|
-
}
|
|
3178
|
-
function stripCollidingDstSymlinks(src, dst, isExcluded) {
|
|
3179
|
-
if (!existsSync18(dst)) return;
|
|
3180
|
-
for (const name of readdirSync7(src)) {
|
|
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
|
-
}
|
|
3190
|
-
}
|
|
3191
|
-
}
|
|
3192
|
-
function cpSyncGuarded(src, dst, filter, label) {
|
|
3193
|
-
try {
|
|
3194
|
-
cpSync4(src, dst, { recursive: true, force: true, verbatimSymlinks: true, filter });
|
|
3195
|
-
} catch (err) {
|
|
3196
|
-
const e = err;
|
|
3197
|
-
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") {
|
|
3198
|
-
throw new NomadFatal(
|
|
3199
|
-
`${label}: type collision copying ${JSON.stringify(src)} -> ${JSON.stringify(dst)} (${e.path ?? "unknown path"}): a file/directory type changed upstream; run nomad pull --force-remote to recover`
|
|
3200
|
-
);
|
|
3201
|
-
}
|
|
3202
|
-
throw err;
|
|
3203
|
-
}
|
|
3204
|
-
}
|
|
3205
|
-
function copyExtrasOverlayFiltered(src, dst, blockSet) {
|
|
3206
|
-
stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
|
|
3207
|
-
cpSyncGuarded(
|
|
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 });
|
|
3226
|
-
}
|
|
3227
|
-
function copyExtrasFileSkipDiverged(src, dst) {
|
|
3228
|
-
if (existsSync18(dst)) {
|
|
3229
|
-
let dstBuf;
|
|
3230
|
-
try {
|
|
3231
|
-
dstBuf = readFileSync6(dst);
|
|
3232
|
-
} catch {
|
|
3233
|
-
return;
|
|
3234
|
-
}
|
|
3235
|
-
if (!readFileSync6(src).equals(dstBuf)) return;
|
|
3236
|
-
}
|
|
3237
|
-
copyExtras(src, dst);
|
|
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
|
-
});
|
|
3250
|
-
}
|
|
3251
|
-
function prunePreservingDenied(src, dst, blockSet) {
|
|
3252
|
-
for (const name of readdirSync7(dst)) {
|
|
3253
|
-
if (isDeniedName(blockSet, name)) continue;
|
|
3254
|
-
const dstPath = join23(dst, name);
|
|
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 });
|
|
3451
|
+
var SESSION_PATH_LOGICAL = /^shared\/projects\/([^/]+)\/([^/]+)\.jsonl$/;
|
|
3452
|
+
function emitClean(section2, staged) {
|
|
3453
|
+
addItem(section2, `${green(okGlyph)} ${staged} project(s) scanned, no leaks`);
|
|
3454
|
+
}
|
|
3455
|
+
function buildLogicalBySession(findings) {
|
|
3456
|
+
const logicalBySession = /* @__PURE__ */ new Map();
|
|
3457
|
+
for (const f of findings) {
|
|
3458
|
+
const m = SESSION_PATH_LOGICAL.exec(f.File);
|
|
3459
|
+
if (m?.[2] !== void 0 && !logicalBySession.has(m[2])) {
|
|
3460
|
+
logicalBySession.set(m[2], m[1] ?? "");
|
|
3265
3461
|
}
|
|
3266
3462
|
}
|
|
3463
|
+
return logicalBySession;
|
|
3267
3464
|
}
|
|
3268
|
-
function
|
|
3269
|
-
const
|
|
3270
|
-
|
|
3271
|
-
if (
|
|
3272
|
-
else rmSync7(dst, { recursive: true, force: true });
|
|
3465
|
+
function emitDescriptionLegend(section2, findings) {
|
|
3466
|
+
const descByRule = /* @__PURE__ */ new Map();
|
|
3467
|
+
for (const f of findings) {
|
|
3468
|
+
if (f.Description && !descByRule.has(f.RuleID)) descByRule.set(f.RuleID, f.Description);
|
|
3273
3469
|
}
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
});
|
|
3281
|
-
}
|
|
3282
|
-
function prunePreservingBy(src, dst, isPreserved) {
|
|
3283
|
-
for (const name of readdirSync7(dst)) {
|
|
3284
|
-
if (isPreserved(name)) continue;
|
|
3285
|
-
const dstPath = join23(dst, name);
|
|
3286
|
-
const srcStat = lstatSync8(join23(src, name), { throwIfNoEntry: false });
|
|
3287
|
-
if (srcStat === void 0) {
|
|
3288
|
-
rmSync7(dstPath, { recursive: true, force: true });
|
|
3289
|
-
continue;
|
|
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
|
-
}
|
|
3470
|
+
if (descByRule.size === 0) return;
|
|
3471
|
+
addItem(section2, "");
|
|
3472
|
+
addItem(section2, bold("Finding types"));
|
|
3473
|
+
for (const [rule, desc] of descByRule) {
|
|
3474
|
+
const ruleLabel = red(`- [${rule}]`);
|
|
3475
|
+
addItem(section2, ` ${ruleLabel}: ${dim(desc)}`);
|
|
3297
3476
|
}
|
|
3298
3477
|
}
|
|
3299
|
-
function
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3478
|
+
function scanAndReport(section2, tmpRoot, staged, logicalToEncoded) {
|
|
3479
|
+
let findings;
|
|
3480
|
+
try {
|
|
3481
|
+
findings = scanStagedTree(tmpRoot);
|
|
3482
|
+
} catch (err) {
|
|
3483
|
+
addItem(section2, `${red(failGlyph)} scan failed: ${err.message}`);
|
|
3484
|
+
process.exitCode = 1;
|
|
3485
|
+
return;
|
|
3304
3486
|
}
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3487
|
+
if (findings === null) {
|
|
3488
|
+
addItem(section2, `${red(failGlyph)} scan failed: no parseable gitleaks report`);
|
|
3489
|
+
process.exitCode = 1;
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
const { bySession, other } = partitionFindings(findings);
|
|
3493
|
+
if (bySession.size === 0 && other.length === 0) {
|
|
3494
|
+
emitClean(section2, staged);
|
|
3495
|
+
return;
|
|
3496
|
+
}
|
|
3497
|
+
if (other.length > 0) reportOtherFindings(section2, other);
|
|
3498
|
+
if (bySession.size > 0) reportSessionFindings(section2, bySession);
|
|
3499
|
+
if (bySession.size > 0) {
|
|
3500
|
+
reportRemediation(section2, bySession, buildLogicalBySession(findings), logicalToEncoded);
|
|
3501
|
+
}
|
|
3502
|
+
emitDescriptionLegend(section2, findings);
|
|
3312
3503
|
}
|
|
3313
3504
|
|
|
3505
|
+
// src/commands.doctor.check-shared.ts
|
|
3506
|
+
init_config();
|
|
3507
|
+
|
|
3314
3508
|
// src/remap.ts
|
|
3509
|
+
init_config_sharedDirs_guard();
|
|
3510
|
+
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";
|
|
3511
|
+
import { dirname as dirname4, join as join26, relative as relative5, sep as sep4 } from "node:path";
|
|
3315
3512
|
init_config();
|
|
3316
3513
|
|
|
3317
3514
|
// src/push-manifest.ts
|
|
@@ -3319,8 +3516,8 @@ init_config();
|
|
|
3319
3516
|
init_push_gitleaks_config();
|
|
3320
3517
|
init_utils_fs();
|
|
3321
3518
|
import { createHash } from "node:crypto";
|
|
3322
|
-
import { existsSync as
|
|
3323
|
-
import { dirname as dirname3, join as
|
|
3519
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync4, readdirSync as readdirSync9, readFileSync as readFileSync7, statSync as statSync4 } from "node:fs";
|
|
3520
|
+
import { dirname as dirname3, join as join25 } from "node:path";
|
|
3324
3521
|
function isChanged(prev, cur, hash) {
|
|
3325
3522
|
if (prev === void 0) return true;
|
|
3326
3523
|
if (prev.size !== cur.size) return true;
|
|
@@ -3360,8 +3557,8 @@ function hashFile(absPath) {
|
|
|
3360
3557
|
return createHash("sha256").update(readFileSync7(absPath)).digest("hex");
|
|
3361
3558
|
}
|
|
3362
3559
|
function enumerateDir(dir, results) {
|
|
3363
|
-
for (const entry of
|
|
3364
|
-
const fullPath =
|
|
3560
|
+
for (const entry of readdirSync9(dir)) {
|
|
3561
|
+
const fullPath = join25(dir, entry);
|
|
3365
3562
|
const st = statSync4(fullPath);
|
|
3366
3563
|
if (st.isDirectory()) {
|
|
3367
3564
|
enumerateDir(fullPath, results);
|
|
@@ -3372,8 +3569,8 @@ function enumerateDir(dir, results) {
|
|
|
3372
3569
|
}
|
|
3373
3570
|
function enumerateSourceFiles(projectDir) {
|
|
3374
3571
|
const results = [];
|
|
3375
|
-
for (const entry of
|
|
3376
|
-
const fullPath =
|
|
3572
|
+
for (const entry of readdirSync9(projectDir)) {
|
|
3573
|
+
const fullPath = join25(projectDir, entry);
|
|
3377
3574
|
const st = statSync4(fullPath);
|
|
3378
3575
|
if (st.isDirectory()) {
|
|
3379
3576
|
enumerateDir(fullPath, results);
|
|
@@ -3385,15 +3582,15 @@ function enumerateSourceFiles(projectDir) {
|
|
|
3385
3582
|
}
|
|
3386
3583
|
function fileIdentity(p) {
|
|
3387
3584
|
if (p === null) return "none::absent";
|
|
3388
|
-
if (!
|
|
3585
|
+
if (!existsSync20(p)) return `${p}::absent`;
|
|
3389
3586
|
return `${p}::${createHash("sha256").update(readFileSync7(p)).digest("hex")}`;
|
|
3390
3587
|
}
|
|
3391
3588
|
function computeConfigHash() {
|
|
3392
3589
|
const repo = repoHome();
|
|
3393
3590
|
const parts = [
|
|
3394
3591
|
fileIdentity(resolveTomlPath(repo)),
|
|
3395
|
-
fileIdentity(
|
|
3396
|
-
fileIdentity(
|
|
3592
|
+
fileIdentity(join25(repo, ".gitleaks.overlay.toml")),
|
|
3593
|
+
fileIdentity(join25(repo, ".gitleaksignore"))
|
|
3397
3594
|
];
|
|
3398
3595
|
return createHash("sha256").update(parts.join("\n")).digest("hex");
|
|
3399
3596
|
}
|
|
@@ -3427,10 +3624,10 @@ init_utils_json();
|
|
|
3427
3624
|
var TMP_SUFFIX = ".nomad-tmp";
|
|
3428
3625
|
function atomicMirror(src, dst, options) {
|
|
3429
3626
|
const tmp = `${dst}${TMP_SUFFIX}`;
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3627
|
+
rmSync9(tmp, { recursive: true, force: true });
|
|
3628
|
+
cpSync6(src, tmp, options);
|
|
3629
|
+
rmSync9(dst, { recursive: true, force: true });
|
|
3630
|
+
renameAtomicRetry(tmp, dst);
|
|
3434
3631
|
}
|
|
3435
3632
|
function overlaySessionDir(src, dst) {
|
|
3436
3633
|
stripCollidingDstSymlinks(src, dst, () => false);
|
|
@@ -3439,11 +3636,11 @@ function overlaySessionDir(src, dst) {
|
|
|
3439
3636
|
function copyFileAtomic(src, dst) {
|
|
3440
3637
|
mkdirSync5(dirname4(dst), { recursive: true });
|
|
3441
3638
|
const tmp = `${dst}.tmp.${process.pid}`;
|
|
3442
|
-
|
|
3443
|
-
|
|
3639
|
+
cpSync6(src, tmp, { force: true, preserveTimestamps: true });
|
|
3640
|
+
renameAtomicRetry(tmp, dst);
|
|
3444
3641
|
}
|
|
3445
3642
|
function hasDeltaForDir(sel, localDir) {
|
|
3446
|
-
const prefix = `${localDir}${
|
|
3643
|
+
const prefix = `${localDir}${sep4}`;
|
|
3447
3644
|
for (const p of sel.changed) if (p.startsWith(prefix)) return true;
|
|
3448
3645
|
for (const p of sel.deleted) if (p.startsWith(prefix)) return true;
|
|
3449
3646
|
return false;
|
|
@@ -3452,15 +3649,15 @@ function skipForSelection(sel, localDir) {
|
|
|
3452
3649
|
return sel !== void 0 && !hasDeltaForDir(sel, localDir);
|
|
3453
3650
|
}
|
|
3454
3651
|
function applySelective(sel, localDir, repoDst) {
|
|
3455
|
-
const prefix = `${localDir}${
|
|
3652
|
+
const prefix = `${localDir}${sep4}`;
|
|
3456
3653
|
for (const src of sel.changed) {
|
|
3457
3654
|
if (!src.startsWith(prefix)) continue;
|
|
3458
|
-
copyFileAtomic(src,
|
|
3655
|
+
copyFileAtomic(src, join26(repoDst, relative5(localDir, src)));
|
|
3459
3656
|
}
|
|
3460
3657
|
for (const src of sel.deleted) {
|
|
3461
3658
|
if (!src.startsWith(prefix)) continue;
|
|
3462
|
-
const dst =
|
|
3463
|
-
if (
|
|
3659
|
+
const dst = join26(repoDst, relative5(localDir, src));
|
|
3660
|
+
if (existsSync21(dst)) rmSync9(dst);
|
|
3464
3661
|
}
|
|
3465
3662
|
}
|
|
3466
3663
|
function copyDirJsonlOnly(src, dst) {
|
|
@@ -3470,7 +3667,7 @@ function copyDirJsonlOnly(src, dst) {
|
|
|
3470
3667
|
filter: (srcPath) => {
|
|
3471
3668
|
const rel = relative5(src, srcPath);
|
|
3472
3669
|
if (rel === "") return true;
|
|
3473
|
-
if (rel.split(
|
|
3670
|
+
if (rel.split(sep4).length > 1) return true;
|
|
3474
3671
|
if (statSync5(srcPath).isDirectory()) return true;
|
|
3475
3672
|
if (srcPath.endsWith(".jsonl")) return true;
|
|
3476
3673
|
item(`skip ${rel}: extension not in allowlist`);
|
|
@@ -3489,15 +3686,15 @@ function remapPull(ts, opts = {}) {
|
|
|
3489
3686
|
const wouldPull = [];
|
|
3490
3687
|
const repo = repoHome();
|
|
3491
3688
|
const claude = claudeHome();
|
|
3492
|
-
const mapPath =
|
|
3493
|
-
const repoProjects =
|
|
3494
|
-
if (!
|
|
3689
|
+
const mapPath = join26(repo, "path-map.json");
|
|
3690
|
+
const repoProjects = join26(repo, "shared", "projects");
|
|
3691
|
+
if (!existsSync21(mapPath) || !existsSync21(repoProjects)) {
|
|
3495
3692
|
const text = "no path-map or repo projects dir; skipping session remap";
|
|
3496
3693
|
emitPreview(opts.onPreview, { kind: "note", text }, text);
|
|
3497
3694
|
return { unmapped: 0, pulled, wouldPull };
|
|
3498
3695
|
}
|
|
3499
3696
|
const map = readPathMap(mapPath);
|
|
3500
|
-
const localProjects =
|
|
3697
|
+
const localProjects = join26(claude, "projects");
|
|
3501
3698
|
if (!dryRun) mkdirSync5(localProjects, { recursive: true });
|
|
3502
3699
|
for (const [logical, hosts] of Object.entries(map.projects)) {
|
|
3503
3700
|
assertSafeLogical(logical);
|
|
@@ -3507,9 +3704,9 @@ function remapPull(ts, opts = {}) {
|
|
|
3507
3704
|
continue;
|
|
3508
3705
|
}
|
|
3509
3706
|
assertSafeLocalRoot(localPath, logical);
|
|
3510
|
-
const src =
|
|
3511
|
-
if (!
|
|
3512
|
-
const dst =
|
|
3707
|
+
const src = join26(repoProjects, logical);
|
|
3708
|
+
if (!existsSync21(src)) continue;
|
|
3709
|
+
const dst = join26(localProjects, encodePath(localPath));
|
|
3513
3710
|
if (dryRun) {
|
|
3514
3711
|
wouldPull.push(logical);
|
|
3515
3712
|
emitPreview(
|
|
@@ -3527,12 +3724,12 @@ function remapPull(ts, opts = {}) {
|
|
|
3527
3724
|
}
|
|
3528
3725
|
function countLocalOnly(src, dst) {
|
|
3529
3726
|
let count = 0;
|
|
3530
|
-
for (const name of
|
|
3531
|
-
const dstPath =
|
|
3532
|
-
const srcPath =
|
|
3533
|
-
if (
|
|
3727
|
+
for (const name of readdirSync10(dst)) {
|
|
3728
|
+
const dstPath = join26(dst, name);
|
|
3729
|
+
const srcPath = join26(src, name);
|
|
3730
|
+
if (lstatSync10(dstPath).isDirectory()) {
|
|
3534
3731
|
count += countLocalOnly(srcPath, dstPath);
|
|
3535
|
-
} else if (
|
|
3732
|
+
} else if (lstatSync10(srcPath, { throwIfNoEntry: false }) === void 0) {
|
|
3536
3733
|
count++;
|
|
3537
3734
|
}
|
|
3538
3735
|
}
|
|
@@ -3541,20 +3738,20 @@ function countLocalOnly(src, dst) {
|
|
|
3541
3738
|
function scanLocalOnly() {
|
|
3542
3739
|
const repo = repoHome();
|
|
3543
3740
|
const claude = claudeHome();
|
|
3544
|
-
const mapPath =
|
|
3545
|
-
const repoProjects =
|
|
3546
|
-
if (!
|
|
3741
|
+
const mapPath = join26(repo, "path-map.json");
|
|
3742
|
+
const repoProjects = join26(repo, "shared", "projects");
|
|
3743
|
+
if (!existsSync21(mapPath) || !existsSync21(repoProjects)) return 0;
|
|
3547
3744
|
const map = readPathMap(mapPath);
|
|
3548
|
-
const localProjects =
|
|
3745
|
+
const localProjects = join26(claude, "projects");
|
|
3549
3746
|
let count = 0;
|
|
3550
3747
|
for (const [logical, hosts] of Object.entries(map.projects)) {
|
|
3551
3748
|
assertSafeLogical(logical);
|
|
3552
3749
|
const localPath = hosts[HOST];
|
|
3553
3750
|
if (!localPath || localPath === "TBD") continue;
|
|
3554
3751
|
assertSafeLocalRoot(localPath, logical);
|
|
3555
|
-
const dst =
|
|
3556
|
-
if (!
|
|
3557
|
-
count += countLocalOnly(
|
|
3752
|
+
const dst = join26(localProjects, encodePath(localPath));
|
|
3753
|
+
if (!existsSync21(dst)) continue;
|
|
3754
|
+
count += countLocalOnly(join26(repoProjects, logical), dst);
|
|
3558
3755
|
}
|
|
3559
3756
|
return count;
|
|
3560
3757
|
}
|
|
@@ -3589,26 +3786,26 @@ function remapPush(ts, opts = {}) {
|
|
|
3589
3786
|
const wouldPush = [];
|
|
3590
3787
|
const repo = repoHome();
|
|
3591
3788
|
const claude = claudeHome();
|
|
3592
|
-
const mapPath =
|
|
3593
|
-
if (!
|
|
3789
|
+
const mapPath = join26(repo, "path-map.json");
|
|
3790
|
+
if (!existsSync21(mapPath)) {
|
|
3594
3791
|
log("no path-map.json; skipping session export");
|
|
3595
3792
|
return { unmapped: 0, collisions: 0, pushed, wouldPush };
|
|
3596
3793
|
}
|
|
3597
3794
|
const map = readPathMap(mapPath);
|
|
3598
|
-
const localProjects =
|
|
3599
|
-
const repoProjects =
|
|
3795
|
+
const localProjects = join26(claude, "projects");
|
|
3796
|
+
const repoProjects = join26(repo, "shared", "projects");
|
|
3600
3797
|
const reverse = buildReverseMap(map);
|
|
3601
|
-
if (!
|
|
3798
|
+
if (!existsSync21(localProjects)) return { unmapped, collisions: 0, pushed, wouldPush };
|
|
3602
3799
|
if (!dryRun) mkdirSync5(repoProjects, { recursive: true });
|
|
3603
|
-
for (const dir of
|
|
3800
|
+
for (const dir of readdirSync10(localProjects)) {
|
|
3604
3801
|
if (dir.endsWith(TMP_SUFFIX)) continue;
|
|
3605
3802
|
const logical = reverse.get(dir);
|
|
3606
3803
|
if (!logical) {
|
|
3607
3804
|
unmapped++;
|
|
3608
3805
|
continue;
|
|
3609
3806
|
}
|
|
3610
|
-
const localDir =
|
|
3611
|
-
const repoDst =
|
|
3807
|
+
const localDir = join26(localProjects, dir);
|
|
3808
|
+
const repoDst = join26(repoProjects, logical);
|
|
3612
3809
|
if (skipForSelection(opts.selection, localDir)) continue;
|
|
3613
3810
|
if (dryRun) {
|
|
3614
3811
|
wouldPush.push(logical);
|
|
@@ -3632,8 +3829,8 @@ function buildScanTree(tmpRoot) {
|
|
|
3632
3829
|
const logicalToEncoded = /* @__PURE__ */ new Map();
|
|
3633
3830
|
let staged = 0;
|
|
3634
3831
|
const repo = repoHome();
|
|
3635
|
-
const mapPath =
|
|
3636
|
-
if (!
|
|
3832
|
+
const mapPath = join27(repo, "path-map.json");
|
|
3833
|
+
if (!existsSync22(mapPath)) return { logicalToEncoded, staged, malformed: false };
|
|
3637
3834
|
let map;
|
|
3638
3835
|
try {
|
|
3639
3836
|
map = readJson(mapPath);
|
|
@@ -3650,12 +3847,12 @@ function buildScanTree(tmpRoot) {
|
|
|
3650
3847
|
if (!p || p === "TBD") continue;
|
|
3651
3848
|
reverse.set(encodePath(p), logical);
|
|
3652
3849
|
}
|
|
3653
|
-
const localProjects =
|
|
3654
|
-
if (!
|
|
3655
|
-
for (const dir of
|
|
3850
|
+
const localProjects = join27(claudeHome(), "projects");
|
|
3851
|
+
if (!existsSync22(localProjects)) return { logicalToEncoded, staged, malformed: false };
|
|
3852
|
+
for (const dir of readdirSync11(localProjects)) {
|
|
3656
3853
|
const logical = reverse.get(dir);
|
|
3657
3854
|
if (!logical) continue;
|
|
3658
|
-
copyDirJsonlOnly(
|
|
3855
|
+
copyDirJsonlOnly(join27(localProjects, dir), join27(tmpRoot, "shared", "projects", logical));
|
|
3659
3856
|
logicalToEncoded.set(logical, dir);
|
|
3660
3857
|
staged++;
|
|
3661
3858
|
}
|
|
@@ -3686,11 +3883,11 @@ function ensureGitleaksReady(section2, gitleaksReady) {
|
|
|
3686
3883
|
}
|
|
3687
3884
|
function reportCheckShared(section2, gitleaksReady) {
|
|
3688
3885
|
if (!ensureGitleaksReady(section2, gitleaksReady)) return;
|
|
3689
|
-
const cacheDir =
|
|
3886
|
+
const cacheDir = join27(homedir4(), ".cache", "claude-nomad");
|
|
3690
3887
|
mkdirSync6(cacheDir, { recursive: true });
|
|
3691
3888
|
const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes(4).toString("hex")}`;
|
|
3692
|
-
const reportPath =
|
|
3693
|
-
const tmpRoot =
|
|
3889
|
+
const reportPath = join27(cacheDir, `check-shared-${stamp}.json`);
|
|
3890
|
+
const tmpRoot = join27(cacheDir, `check-shared-tree-${stamp}`);
|
|
3694
3891
|
try {
|
|
3695
3892
|
const { logicalToEncoded, staged, malformed } = buildScanTree(tmpRoot);
|
|
3696
3893
|
if (malformed) {
|
|
@@ -3704,15 +3901,15 @@ function reportCheckShared(section2, gitleaksReady) {
|
|
|
3704
3901
|
}
|
|
3705
3902
|
scanAndReport(section2, tmpRoot, staged, logicalToEncoded);
|
|
3706
3903
|
} finally {
|
|
3707
|
-
|
|
3708
|
-
|
|
3904
|
+
rmSync10(reportPath, { force: true });
|
|
3905
|
+
rmSync10(tmpRoot, { recursive: true, force: true });
|
|
3709
3906
|
}
|
|
3710
3907
|
}
|
|
3711
3908
|
|
|
3712
3909
|
// src/commands.doctor.checks.hooks.scope.ts
|
|
3713
3910
|
init_color();
|
|
3714
|
-
import { existsSync as
|
|
3715
|
-
import { dirname as dirname5, extname, join as
|
|
3911
|
+
import { existsSync as existsSync23, readFileSync as readFileSync8, readdirSync as readdirSync12, realpathSync as realpathSync2 } from "node:fs";
|
|
3912
|
+
import { dirname as dirname5, extname, join as join28 } from "node:path";
|
|
3716
3913
|
init_config();
|
|
3717
3914
|
function typeFromPackageJson(pkgPath) {
|
|
3718
3915
|
try {
|
|
@@ -3734,8 +3931,8 @@ function effectiveType(hookPath) {
|
|
|
3734
3931
|
}
|
|
3735
3932
|
let dir = dirname5(real);
|
|
3736
3933
|
for (; ; ) {
|
|
3737
|
-
const pkg =
|
|
3738
|
-
if (
|
|
3934
|
+
const pkg = join28(dir, "package.json");
|
|
3935
|
+
if (existsSync23(pkg)) return typeFromPackageJson(pkg);
|
|
3739
3936
|
const parent = dirname5(dir);
|
|
3740
3937
|
if (parent === dir) return "cjs";
|
|
3741
3938
|
dir = parent;
|
|
@@ -3764,7 +3961,7 @@ function remedy(family) {
|
|
|
3764
3961
|
}
|
|
3765
3962
|
function safeReaddir2(dir) {
|
|
3766
3963
|
try {
|
|
3767
|
-
return
|
|
3964
|
+
return readdirSync12(dir);
|
|
3768
3965
|
} catch {
|
|
3769
3966
|
return [];
|
|
3770
3967
|
}
|
|
@@ -3777,15 +3974,15 @@ function safeRead(path) {
|
|
|
3777
3974
|
}
|
|
3778
3975
|
}
|
|
3779
3976
|
function reportHookScopeCheck(section2) {
|
|
3780
|
-
const hooksDir =
|
|
3781
|
-
if (!
|
|
3977
|
+
const hooksDir = join28(claudeHome(), "hooks");
|
|
3978
|
+
if (!existsSync23(hooksDir)) {
|
|
3782
3979
|
addItem(section2, `${dim(infoGlyph)} no ~/.claude/hooks; skipping module-scope check`);
|
|
3783
3980
|
return;
|
|
3784
3981
|
}
|
|
3785
3982
|
let anyWarn = false;
|
|
3786
3983
|
for (const name of safeReaddir2(hooksDir)) {
|
|
3787
3984
|
if (extname(name) !== ".js") continue;
|
|
3788
|
-
const abs =
|
|
3985
|
+
const abs = join28(hooksDir, name);
|
|
3789
3986
|
const eff = effectiveType(abs);
|
|
3790
3987
|
if (eff === null) continue;
|
|
3791
3988
|
const src = safeRead(abs);
|
|
@@ -3805,8 +4002,8 @@ function reportHookScopeCheck(section2) {
|
|
|
3805
4002
|
|
|
3806
4003
|
// src/commands.doctor.checks.hooks.ts
|
|
3807
4004
|
init_color();
|
|
3808
|
-
import { existsSync as
|
|
3809
|
-
import { join as
|
|
4005
|
+
import { existsSync as existsSync24 } from "node:fs";
|
|
4006
|
+
import { join as join29 } from "node:path";
|
|
3810
4007
|
init_config();
|
|
3811
4008
|
function expandHome(token) {
|
|
3812
4009
|
const h2 = home();
|
|
@@ -3820,11 +4017,11 @@ function stripShellPunctuation(token) {
|
|
|
3820
4017
|
return stripped.slice(0, end);
|
|
3821
4018
|
}
|
|
3822
4019
|
function* claudePathsIn(command) {
|
|
3823
|
-
const claudePrefix = `${claudeHome()}/`;
|
|
4020
|
+
const claudePrefix = `${claudeHome().replaceAll("\\", "/")}/`;
|
|
3824
4021
|
for (const segment of command.split(/&&|\|\||;|\|/)) {
|
|
3825
4022
|
for (const raw of segment.trim().split(/\s+/).filter(Boolean)) {
|
|
3826
4023
|
const expanded = expandHome(stripShellPunctuation(raw));
|
|
3827
|
-
if (expanded.startsWith(claudePrefix)) yield expanded;
|
|
4024
|
+
if (expanded.replaceAll("\\", "/").startsWith(claudePrefix)) yield expanded;
|
|
3828
4025
|
}
|
|
3829
4026
|
}
|
|
3830
4027
|
}
|
|
@@ -3849,7 +4046,7 @@ function checkEventGroups(section2, event, groups) {
|
|
|
3849
4046
|
for (const group of groups) {
|
|
3850
4047
|
for (const cmd of commandsFromGroup(group)) {
|
|
3851
4048
|
for (const resolved of claudePathsIn(cmd)) {
|
|
3852
|
-
if (
|
|
4049
|
+
if (existsSync24(resolved)) continue;
|
|
3853
4050
|
addItem(
|
|
3854
4051
|
section2,
|
|
3855
4052
|
`${red(failGlyph)} hooks/${event}: command target missing: ${resolved} (run nomad pull)`
|
|
@@ -3862,8 +4059,8 @@ function checkEventGroups(section2, event, groups) {
|
|
|
3862
4059
|
return anyFail;
|
|
3863
4060
|
}
|
|
3864
4061
|
function reportHooksTargetCheck(section2) {
|
|
3865
|
-
const settingsPath =
|
|
3866
|
-
if (!
|
|
4062
|
+
const settingsPath = join29(claudeHome(), "settings.json");
|
|
4063
|
+
if (!existsSync24(settingsPath)) {
|
|
3867
4064
|
addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping hook target check`);
|
|
3868
4065
|
return;
|
|
3869
4066
|
}
|
|
@@ -3886,12 +4083,12 @@ function reportHooksTargetCheck(section2) {
|
|
|
3886
4083
|
|
|
3887
4084
|
// src/commands.doctor.checks.hooks.preserve-symlinks.ts
|
|
3888
4085
|
init_color();
|
|
3889
|
-
import { existsSync as
|
|
3890
|
-
import { join as
|
|
4086
|
+
import { existsSync as existsSync26, readFileSync as readFileSync9 } from "node:fs";
|
|
4087
|
+
import { join as join31 } from "node:path";
|
|
3891
4088
|
|
|
3892
4089
|
// src/commands.doctor.checks.hooks.preserve-symlinks.probe.ts
|
|
3893
|
-
import { closeSync as closeSync3, existsSync as
|
|
3894
|
-
import { dirname as dirname6, join as
|
|
4090
|
+
import { closeSync as closeSync3, existsSync as existsSync25, openSync as openSync3, readSync, realpathSync as realpathSync3 } from "node:fs";
|
|
4091
|
+
import { dirname as dirname6, join as join30, resolve as resolve2 } from "node:path";
|
|
3895
4092
|
function suppressedRanges(src) {
|
|
3896
4093
|
const ranges = [];
|
|
3897
4094
|
const re = /\/\*[\s\S]*?\*\/|\/\/[^\n]*|'[^']*'|"[^"]*"|`[^`]*`/g;
|
|
@@ -3923,12 +4120,12 @@ function topRelativeSpecifiers(src) {
|
|
|
3923
4120
|
}
|
|
3924
4121
|
function specifierIsMissing(specifier, baseDir) {
|
|
3925
4122
|
const base = resolve2(baseDir, specifier);
|
|
3926
|
-
if (
|
|
4123
|
+
if (existsSync25(base)) return false;
|
|
3927
4124
|
for (const ext of [".js", ".cjs", ".mjs"]) {
|
|
3928
|
-
if (
|
|
4125
|
+
if (existsSync25(base + ext)) return false;
|
|
3929
4126
|
}
|
|
3930
4127
|
for (const idx of ["index.js", "index.cjs", "index.mjs"]) {
|
|
3931
|
-
if (
|
|
4128
|
+
if (existsSync25(join30(base, idx))) return false;
|
|
3932
4129
|
}
|
|
3933
4130
|
return true;
|
|
3934
4131
|
}
|
|
@@ -3983,8 +4180,8 @@ function commandTokens(command) {
|
|
|
3983
4180
|
return tokens;
|
|
3984
4181
|
}
|
|
3985
4182
|
function readPathMapSafe() {
|
|
3986
|
-
const mapPath =
|
|
3987
|
-
if (!
|
|
4183
|
+
const mapPath = join31(repoHome(), "path-map.json");
|
|
4184
|
+
if (!existsSync26(mapPath)) return { projects: {} };
|
|
3988
4185
|
try {
|
|
3989
4186
|
return JSON.parse(readFileSync9(mapPath, "utf8"));
|
|
3990
4187
|
} catch {
|
|
@@ -3992,9 +4189,10 @@ function readPathMapSafe() {
|
|
|
3992
4189
|
}
|
|
3993
4190
|
}
|
|
3994
4191
|
function resolvesUnderSymlinkedShared(scriptPath, sharedLinkNames) {
|
|
4192
|
+
const normalizedScript = scriptPath.replaceAll("\\", "/");
|
|
3995
4193
|
for (const name of sharedLinkNames) {
|
|
3996
|
-
const prefix = `${claudeHome()}/${name}/`;
|
|
3997
|
-
if (
|
|
4194
|
+
const prefix = `${claudeHome().replaceAll("\\", "/")}/${name}/`;
|
|
4195
|
+
if (normalizedScript.startsWith(prefix)) return true;
|
|
3998
4196
|
}
|
|
3999
4197
|
return false;
|
|
4000
4198
|
}
|
|
@@ -4057,8 +4255,8 @@ function checkEventForPreserveSymlinks(section2, event, groups, sharedLinkNames)
|
|
|
4057
4255
|
return anyWarn;
|
|
4058
4256
|
}
|
|
4059
4257
|
function reportPreserveSymlinksCheck(section2) {
|
|
4060
|
-
const settingsPath =
|
|
4061
|
-
if (!
|
|
4258
|
+
const settingsPath = join31(claudeHome(), "settings.json");
|
|
4259
|
+
if (!existsSync26(settingsPath)) {
|
|
4062
4260
|
addItem(
|
|
4063
4261
|
section2,
|
|
4064
4262
|
`${dim(infoGlyph)} no ~/.claude/settings.json; skipping preserve-symlinks-main check`
|
|
@@ -4091,8 +4289,8 @@ function reportPreserveSymlinksCheck(section2) {
|
|
|
4091
4289
|
|
|
4092
4290
|
// src/commands.doctor.checks.settings-drift.ts
|
|
4093
4291
|
init_color();
|
|
4094
|
-
import { existsSync as
|
|
4095
|
-
import { join as
|
|
4292
|
+
import { existsSync as existsSync27, readFileSync as readFileSync10 } from "node:fs";
|
|
4293
|
+
import { join as join32 } from "node:path";
|
|
4096
4294
|
init_config();
|
|
4097
4295
|
init_utils_json();
|
|
4098
4296
|
function diffMergedSettings(merged, settings) {
|
|
@@ -4110,7 +4308,7 @@ function tryReadJson(filePath) {
|
|
|
4110
4308
|
}
|
|
4111
4309
|
}
|
|
4112
4310
|
function reportHooksBaseSelfCleanNote(section2) {
|
|
4113
|
-
const basePath =
|
|
4311
|
+
const basePath = join32(repoHome(), "shared", "settings.base.json");
|
|
4114
4312
|
const base = tryReadJson(basePath);
|
|
4115
4313
|
if (base === null) return;
|
|
4116
4314
|
if (!baseHasGsdHookEntries(base)) return;
|
|
@@ -4123,14 +4321,14 @@ function reportSettingsDriftCheck(section2) {
|
|
|
4123
4321
|
const claude = claudeHome();
|
|
4124
4322
|
const repo = repoHome();
|
|
4125
4323
|
const host = HOST;
|
|
4126
|
-
const settingsPath =
|
|
4127
|
-
const basePath =
|
|
4128
|
-
const hostPath =
|
|
4129
|
-
if (!
|
|
4324
|
+
const settingsPath = join32(claude, "settings.json");
|
|
4325
|
+
const basePath = join32(repo, "shared", "settings.base.json");
|
|
4326
|
+
const hostPath = join32(repo, "hosts", `${host}.json`);
|
|
4327
|
+
if (!existsSync27(settingsPath)) {
|
|
4130
4328
|
addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping merge-drift check`);
|
|
4131
4329
|
return;
|
|
4132
4330
|
}
|
|
4133
|
-
if (!
|
|
4331
|
+
if (!existsSync27(basePath)) {
|
|
4134
4332
|
addItem(
|
|
4135
4333
|
section2,
|
|
4136
4334
|
`${dim(infoGlyph)} shared/settings.base.json missing; skipping merge-drift check`
|
|
@@ -4149,7 +4347,7 @@ function reportSettingsDriftCheck(section2) {
|
|
|
4149
4347
|
if (settings === null) {
|
|
4150
4348
|
return;
|
|
4151
4349
|
}
|
|
4152
|
-
const hostExists =
|
|
4350
|
+
const hostExists = existsSync27(hostPath);
|
|
4153
4351
|
const hostObj = hostExists ? tryReadJson(hostPath) : null;
|
|
4154
4352
|
if (hostExists && hostObj === null) {
|
|
4155
4353
|
addItem(
|
|
@@ -4305,46 +4503,152 @@ function reportNodeEngineCheck(section2) {
|
|
|
4305
4503
|
addItem(section2, `${green(okGlyph)} node: ${process.version}`);
|
|
4306
4504
|
}
|
|
4307
4505
|
|
|
4506
|
+
// src/commands.doctor.checks.longpaths.ts
|
|
4507
|
+
init_color();
|
|
4508
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
4509
|
+
init_config();
|
|
4510
|
+
var PROBE_TIMEOUT_MS = 3e3;
|
|
4511
|
+
var LONGPATHS_REG_KEY = String.raw`HKLM\SYSTEM\CurrentControlSet\Control\FileSystem`;
|
|
4512
|
+
var LONGPATHS_REG_VALUE = "LongPathsEnabled";
|
|
4513
|
+
function probeGitLongpaths(run) {
|
|
4514
|
+
try {
|
|
4515
|
+
const out = run("git", ["-C", repoHome(), "config", "--get", "core.longpaths"], {
|
|
4516
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4517
|
+
timeout: PROBE_TIMEOUT_MS
|
|
4518
|
+
}).toString().trim();
|
|
4519
|
+
return out === "true" || out === "1";
|
|
4520
|
+
} catch {
|
|
4521
|
+
return false;
|
|
4522
|
+
}
|
|
4523
|
+
}
|
|
4524
|
+
function probeRegistryLongpaths(run) {
|
|
4525
|
+
try {
|
|
4526
|
+
const out = run("reg", ["query", LONGPATHS_REG_KEY, "/v", LONGPATHS_REG_VALUE], {
|
|
4527
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4528
|
+
timeout: PROBE_TIMEOUT_MS
|
|
4529
|
+
}).toString();
|
|
4530
|
+
return /0x1\b/.test(out);
|
|
4531
|
+
} catch {
|
|
4532
|
+
return false;
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
function addLongpathsRow(section2, label, enabled2, remediation) {
|
|
4536
|
+
if (enabled2) {
|
|
4537
|
+
addItem(section2, `${green(okGlyph)} ${label}: enabled`);
|
|
4538
|
+
return;
|
|
4539
|
+
}
|
|
4540
|
+
addItem(section2, `${yellow(warnGlyph)} ${label}: not enabled (${remediation})`);
|
|
4541
|
+
}
|
|
4542
|
+
function reportLongPathsCheck(section2, run = execFileSync10) {
|
|
4543
|
+
if (process.platform !== "win32") return;
|
|
4544
|
+
addLongpathsRow(
|
|
4545
|
+
section2,
|
|
4546
|
+
"git core.longpaths",
|
|
4547
|
+
probeGitLongpaths(run),
|
|
4548
|
+
"run `git config --global core.longpaths true`"
|
|
4549
|
+
);
|
|
4550
|
+
addLongpathsRow(
|
|
4551
|
+
section2,
|
|
4552
|
+
"OS long paths",
|
|
4553
|
+
probeRegistryLongpaths(run),
|
|
4554
|
+
"enable LongPathsEnabled in Local Group Policy or the registry (admin required)"
|
|
4555
|
+
);
|
|
4556
|
+
}
|
|
4557
|
+
function reportSyncModality(section2) {
|
|
4558
|
+
const modality = process.platform === "win32" ? "copy-sync (win32)" : "symlink (posix)";
|
|
4559
|
+
addItem(section2, `${dim(infoGlyph)} sync modality: ${modality}`);
|
|
4560
|
+
}
|
|
4561
|
+
|
|
4562
|
+
// src/commands.doctor.checks.crlf.ts
|
|
4563
|
+
init_color();
|
|
4564
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
4565
|
+
import { existsSync as existsSync28, readFileSync as readFileSync13 } from "node:fs";
|
|
4566
|
+
import { join as join33 } from "node:path";
|
|
4567
|
+
init_config();
|
|
4568
|
+
var PROBE_TIMEOUT_MS2 = 3e3;
|
|
4569
|
+
var GUARD_LINE = /^\*\s+-text\b/;
|
|
4570
|
+
function hasGitattributesGuard(repo) {
|
|
4571
|
+
try {
|
|
4572
|
+
const path = join33(repo, ".gitattributes");
|
|
4573
|
+
if (!existsSync28(path)) return false;
|
|
4574
|
+
const content = readFileSync13(path, "utf8");
|
|
4575
|
+
return content.split("\n").map((line) => line.trim()).some((line) => line !== "" && !line.startsWith("#") && GUARD_LINE.test(line));
|
|
4576
|
+
} catch {
|
|
4577
|
+
return false;
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
function probeAutocrlf(repo, run) {
|
|
4581
|
+
try {
|
|
4582
|
+
const out = run("git", ["-C", repo, "config", "core.autocrlf"], {
|
|
4583
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
4584
|
+
timeout: PROBE_TIMEOUT_MS2
|
|
4585
|
+
}).toString().trim();
|
|
4586
|
+
if (out === "true" || out === "input") return "active";
|
|
4587
|
+
if (out === "false") return "disabled";
|
|
4588
|
+
return "unset";
|
|
4589
|
+
} catch {
|
|
4590
|
+
return "unset";
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
var AUTOCRLF_RISK = {
|
|
4594
|
+
active: "core.autocrlf is actively converting line endings on checkout",
|
|
4595
|
+
disabled: "guarded on this host by core.autocrlf=false, but no .gitattributes to protect other hosts",
|
|
4596
|
+
unset: "no .gitattributes guard and core.autocrlf is unset (latent risk)"
|
|
4597
|
+
};
|
|
4598
|
+
function addExposedRow(section2, repo, verdict) {
|
|
4599
|
+
const risk = AUTOCRLF_RISK[verdict];
|
|
4600
|
+
const remediation = `add a .gitattributes with a \`* -text\` line, or run \`git config core.autocrlf false\`, in ${repo}`;
|
|
4601
|
+
addItem(section2, `${yellow(warnGlyph)} CRLF guard: ${risk}; ${remediation}`);
|
|
4602
|
+
}
|
|
4603
|
+
function reportCrlfGuardCheck(section2, run = execFileSync11) {
|
|
4604
|
+
const repo = repoHome();
|
|
4605
|
+
if (hasGitattributesGuard(repo)) {
|
|
4606
|
+
addItem(section2, `${green(okGlyph)} CRLF guard: .gitattributes (* -text) present`);
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
addExposedRow(section2, repo, probeAutocrlf(repo, run));
|
|
4610
|
+
}
|
|
4611
|
+
|
|
4308
4612
|
// src/spinner.ts
|
|
4309
4613
|
init_color();
|
|
4310
|
-
import { existsSync as
|
|
4614
|
+
import { existsSync as existsSync32 } from "node:fs";
|
|
4311
4615
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
4312
4616
|
import { Worker } from "node:worker_threads";
|
|
4313
4617
|
|
|
4314
4618
|
// src/commands.push.recovery.ts
|
|
4315
4619
|
init_config();
|
|
4316
|
-
import { readFileSync as
|
|
4317
|
-
import { join as
|
|
4620
|
+
import { readFileSync as readFileSync16, rmSync as rmSync12, writeFileSync as writeFileSync5 } from "node:fs";
|
|
4621
|
+
import { join as join38 } from "node:path";
|
|
4318
4622
|
import { createInterface as createInterface2 } from "node:readline/promises";
|
|
4319
4623
|
|
|
4320
4624
|
// src/commands.push.recovery.actions.ts
|
|
4321
4625
|
init_config();
|
|
4322
|
-
import { readFileSync as
|
|
4323
|
-
import { isAbsolute as isAbsolute2, resolve as resolve3, sep as
|
|
4626
|
+
import { readFileSync as readFileSync15 } from "node:fs";
|
|
4627
|
+
import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep6 } from "node:path";
|
|
4324
4628
|
|
|
4325
4629
|
// src/commands.push.recovery.redact.ts
|
|
4326
4630
|
init_config();
|
|
4327
4631
|
init_config_sharedDirs_guard();
|
|
4328
|
-
import { cpSync as
|
|
4329
|
-
import { dirname as dirname8, join as
|
|
4632
|
+
import { cpSync as cpSync7, existsSync as existsSync31, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
|
|
4633
|
+
import { dirname as dirname8, join as join36, sep as sep5 } from "node:path";
|
|
4330
4634
|
|
|
4331
4635
|
// src/commands.redact.ts
|
|
4332
4636
|
init_config();
|
|
4333
|
-
import { existsSync as
|
|
4334
|
-
import { dirname as dirname7, join as
|
|
4637
|
+
import { existsSync as existsSync30, statSync as statSync7 } from "node:fs";
|
|
4638
|
+
import { dirname as dirname7, join as join35 } from "node:path";
|
|
4335
4639
|
|
|
4336
4640
|
// src/commands.redact.subtree.ts
|
|
4337
|
-
import { existsSync as
|
|
4338
|
-
import { join as
|
|
4641
|
+
import { existsSync as existsSync29, lstatSync as lstatSync11, readFileSync as readFileSync14, readdirSync as readdirSync13, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4642
|
+
import { join as join34 } from "node:path";
|
|
4339
4643
|
init_utils_fs();
|
|
4340
4644
|
init_utils();
|
|
4341
4645
|
function collectFiles(dir, out) {
|
|
4342
|
-
if (!
|
|
4343
|
-
const st =
|
|
4646
|
+
if (!existsSync29(dir)) return;
|
|
4647
|
+
const st = lstatSync11(dir);
|
|
4344
4648
|
if (!st.isDirectory()) return;
|
|
4345
|
-
for (const entry of
|
|
4346
|
-
const abs =
|
|
4347
|
-
const lst =
|
|
4649
|
+
for (const entry of readdirSync13(dir)) {
|
|
4650
|
+
const abs = join34(dir, entry);
|
|
4651
|
+
const lst = lstatSync11(abs);
|
|
4348
4652
|
if (lst.isSymbolicLink()) continue;
|
|
4349
4653
|
if (lst.isDirectory()) {
|
|
4350
4654
|
collectFiles(abs, out);
|
|
@@ -4380,7 +4684,7 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
|
|
|
4380
4684
|
if (!dryRun && total > 0) {
|
|
4381
4685
|
for (const { path: filePath, findings } of dirty) {
|
|
4382
4686
|
backupBeforeWrite(filePath, ts);
|
|
4383
|
-
const before =
|
|
4687
|
+
const before = readFileSync14(filePath, "utf8");
|
|
4384
4688
|
const after = applyRedactions(before, findings);
|
|
4385
4689
|
if (after === before) {
|
|
4386
4690
|
log(
|
|
@@ -4395,10 +4699,10 @@ function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts,
|
|
|
4395
4699
|
|
|
4396
4700
|
// src/commands.pushed-history.ts
|
|
4397
4701
|
init_utils();
|
|
4398
|
-
import { execFileSync as
|
|
4702
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
4399
4703
|
function pushedRef(repo) {
|
|
4400
4704
|
try {
|
|
4401
|
-
const ref =
|
|
4705
|
+
const ref = execFileSync12("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
|
|
4402
4706
|
cwd: repo,
|
|
4403
4707
|
stdio: ["ignore", "pipe", "ignore"]
|
|
4404
4708
|
}).toString().trim();
|
|
@@ -4411,7 +4715,7 @@ function sessionInPushedHistory(id, repo) {
|
|
|
4411
4715
|
const ref = pushedRef(repo);
|
|
4412
4716
|
if (ref === null) return false;
|
|
4413
4717
|
try {
|
|
4414
|
-
const out =
|
|
4718
|
+
const out = execFileSync12(
|
|
4415
4719
|
"git",
|
|
4416
4720
|
[
|
|
4417
4721
|
"log",
|
|
@@ -4448,15 +4752,15 @@ init_utils_json();
|
|
|
4448
4752
|
init_utils();
|
|
4449
4753
|
function resolveLiveTranscript(id) {
|
|
4450
4754
|
try {
|
|
4451
|
-
const mapPath =
|
|
4452
|
-
if (!
|
|
4755
|
+
const mapPath = join35(repoHome(), "path-map.json");
|
|
4756
|
+
if (!existsSync30(mapPath)) return null;
|
|
4453
4757
|
const projects = readJson(mapPath).projects;
|
|
4454
4758
|
const claude = claudeHome();
|
|
4455
4759
|
for (const hostMap of Object.values(projects)) {
|
|
4456
4760
|
const abs = hostMap[HOST];
|
|
4457
4761
|
if (abs === void 0) continue;
|
|
4458
|
-
const live =
|
|
4459
|
-
if (
|
|
4762
|
+
const live = join35(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
4763
|
+
if (existsSync30(live)) return live;
|
|
4460
4764
|
}
|
|
4461
4765
|
return null;
|
|
4462
4766
|
} catch {
|
|
@@ -4476,17 +4780,17 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
|
|
|
4476
4780
|
}
|
|
4477
4781
|
const repo = repoHome();
|
|
4478
4782
|
const backup = backupBase();
|
|
4479
|
-
if (!
|
|
4783
|
+
if (!existsSync30(repo)) die(`repo not cloned at ${repo}`);
|
|
4480
4784
|
const handle = acquireLock("redact");
|
|
4481
4785
|
if (handle === null) process.exit(0);
|
|
4482
4786
|
try {
|
|
4483
4787
|
const localPath = resolveLiveTranscript(id);
|
|
4484
|
-
if (localPath === null || !
|
|
4788
|
+
if (localPath === null || !existsSync30(localPath)) {
|
|
4485
4789
|
fail(`could not resolve local transcript for session ${id} on this host`);
|
|
4486
4790
|
process.exitCode = 1;
|
|
4487
4791
|
return;
|
|
4488
4792
|
}
|
|
4489
|
-
const sessionDir =
|
|
4793
|
+
const sessionDir = join35(dirname7(localPath), id);
|
|
4490
4794
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
4491
4795
|
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
|
|
4492
4796
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
@@ -4602,8 +4906,8 @@ function resolveStagedDir(localPath, map, claude, repo) {
|
|
|
4602
4906
|
assertSafeLogical(logical);
|
|
4603
4907
|
const abs = hostMap[HOST];
|
|
4604
4908
|
if (abs === void 0) continue;
|
|
4605
|
-
if (localPath.startsWith(
|
|
4606
|
-
return
|
|
4909
|
+
if (localPath.startsWith(join36(claude, "projects", encodePath(abs)) + sep5)) {
|
|
4910
|
+
return join36(repo, "shared", "projects", logical);
|
|
4607
4911
|
}
|
|
4608
4912
|
}
|
|
4609
4913
|
return null;
|
|
@@ -4613,7 +4917,7 @@ function preflightRedactable(f, map, nowMs) {
|
|
|
4613
4917
|
if (sid === null) return "a finding has no resolvable session id (not a session transcript)";
|
|
4614
4918
|
const localPath = resolveLiveTranscript(sid);
|
|
4615
4919
|
if (localPath === null) return `session ${sid}: local transcript not found`;
|
|
4616
|
-
const sessionDir =
|
|
4920
|
+
const sessionDir = join36(dirname8(localPath), sid);
|
|
4617
4921
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
4618
4922
|
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
|
|
4619
4923
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
@@ -4643,7 +4947,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
4643
4947
|
`could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
4644
4948
|
);
|
|
4645
4949
|
}
|
|
4646
|
-
const sessionDir =
|
|
4950
|
+
const sessionDir = join36(dirname8(localPath), sid);
|
|
4647
4951
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
4648
4952
|
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
|
|
4649
4953
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
@@ -4677,26 +4981,26 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
4677
4981
|
);
|
|
4678
4982
|
}
|
|
4679
4983
|
mkdirSync7(stagedProjectDir, { recursive: true });
|
|
4680
|
-
|
|
4681
|
-
if (
|
|
4682
|
-
|
|
4984
|
+
cpSync7(localPath, join36(stagedProjectDir, `${sid}.jsonl`), { force: true });
|
|
4985
|
+
if (existsSync31(sessionDir)) {
|
|
4986
|
+
cpSync7(sessionDir, join36(stagedProjectDir, sid), { force: true, recursive: true });
|
|
4683
4987
|
}
|
|
4684
4988
|
return true;
|
|
4685
4989
|
}
|
|
4686
4990
|
|
|
4687
4991
|
// src/commands.push.recovery.drop.ts
|
|
4688
4992
|
init_config();
|
|
4689
|
-
import { rmSync as
|
|
4690
|
-
import { join as
|
|
4993
|
+
import { rmSync as rmSync11 } from "node:fs";
|
|
4994
|
+
import { join as join37 } from "node:path";
|
|
4691
4995
|
function dropSessionFromStaged(sid, map) {
|
|
4692
4996
|
const logicals = Object.keys(map.projects);
|
|
4693
4997
|
if (logicals.length === 0) return false;
|
|
4694
4998
|
const repo = repoHome();
|
|
4695
4999
|
for (const logical of logicals) {
|
|
4696
|
-
const jsonl =
|
|
4697
|
-
const dir =
|
|
4698
|
-
|
|
4699
|
-
|
|
5000
|
+
const jsonl = join37(repo, "shared", "projects", logical, `${sid}.jsonl`);
|
|
5001
|
+
const dir = join37(repo, "shared", "projects", logical, sid);
|
|
5002
|
+
rmSync11(jsonl, { force: true });
|
|
5003
|
+
rmSync11(dir, { recursive: true, force: true });
|
|
4700
5004
|
}
|
|
4701
5005
|
return true;
|
|
4702
5006
|
}
|
|
@@ -4727,10 +5031,10 @@ function makeDefaultReadLine(repo) {
|
|
|
4727
5031
|
try {
|
|
4728
5032
|
const repoRoot = resolve3(repo);
|
|
4729
5033
|
const target = resolve3(repoRoot, file);
|
|
4730
|
-
if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot +
|
|
5034
|
+
if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep6)) {
|
|
4731
5035
|
return null;
|
|
4732
5036
|
}
|
|
4733
|
-
const content =
|
|
5037
|
+
const content = readFileSync15(target, "utf8");
|
|
4734
5038
|
const lines = content.split(/\r?\n/);
|
|
4735
5039
|
const idx = line - 1;
|
|
4736
5040
|
if (idx < 0 || idx >= lines.length) return null;
|
|
@@ -4851,10 +5155,10 @@ function applyThenRescan(scanVerdict, repoHome2) {
|
|
|
4851
5155
|
return next;
|
|
4852
5156
|
}
|
|
4853
5157
|
function allowThenRescan(append, scanVerdict, repoHome2) {
|
|
4854
|
-
const ignPath =
|
|
5158
|
+
const ignPath = join38(repoHome2, ".gitleaksignore");
|
|
4855
5159
|
let before;
|
|
4856
5160
|
try {
|
|
4857
|
-
before =
|
|
5161
|
+
before = readFileSync16(ignPath, "utf8");
|
|
4858
5162
|
} catch {
|
|
4859
5163
|
before = null;
|
|
4860
5164
|
}
|
|
@@ -4862,7 +5166,7 @@ function allowThenRescan(append, scanVerdict, repoHome2) {
|
|
|
4862
5166
|
try {
|
|
4863
5167
|
return applyThenRescan(scanVerdict, repoHome2);
|
|
4864
5168
|
} catch (err) {
|
|
4865
|
-
if (before === null)
|
|
5169
|
+
if (before === null) rmSync12(ignPath, { force: true });
|
|
4866
5170
|
else writeFileSync5(ignPath, before, "utf8");
|
|
4867
5171
|
throw err;
|
|
4868
5172
|
}
|
|
@@ -4950,7 +5254,7 @@ function writeAnimatedDone(out, label, ms, useTTY) {
|
|
|
4950
5254
|
`);
|
|
4951
5255
|
}
|
|
4952
5256
|
function resolveWorkerPath(deps = {}) {
|
|
4953
|
-
const check = deps.existsSyncFn ??
|
|
5257
|
+
const check = deps.existsSyncFn ?? existsSync32;
|
|
4954
5258
|
const base = deps.baseUrl ?? import.meta.url;
|
|
4955
5259
|
const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
|
|
4956
5260
|
if (check(mjs)) return mjs;
|
|
@@ -5016,9 +5320,9 @@ function withSpinner(label, fn, deps) {
|
|
|
5016
5320
|
|
|
5017
5321
|
// src/commands.doctor.gitleaks-version.ts
|
|
5018
5322
|
init_color();
|
|
5019
|
-
import { execFileSync as
|
|
5020
|
-
import { existsSync as
|
|
5021
|
-
import { join as
|
|
5323
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
5324
|
+
import { existsSync as existsSync33 } from "node:fs";
|
|
5325
|
+
import { join as join39 } from "node:path";
|
|
5022
5326
|
init_config();
|
|
5023
5327
|
var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
|
|
5024
5328
|
var GITLEAKS_TIMEOUT_MS = 5e3;
|
|
@@ -5027,7 +5331,7 @@ function majorMinorOf(value) {
|
|
|
5027
5331
|
return m === null ? null : [m[1], m[2]];
|
|
5028
5332
|
}
|
|
5029
5333
|
function readGitleaksVersion(run, tomlExists) {
|
|
5030
|
-
const tomlPath =
|
|
5334
|
+
const tomlPath = join39(repoHome(), ".gitleaks.toml");
|
|
5031
5335
|
const args = ["version"];
|
|
5032
5336
|
if (tomlExists(tomlPath)) args.push("--config", tomlPath);
|
|
5033
5337
|
try {
|
|
@@ -5039,7 +5343,7 @@ function readGitleaksVersion(run, tomlExists) {
|
|
|
5039
5343
|
return null;
|
|
5040
5344
|
}
|
|
5041
5345
|
}
|
|
5042
|
-
function reportGitleaksVersionCheck(section2, run =
|
|
5346
|
+
function reportGitleaksVersionCheck(section2, run = execFileSync13, tomlExists = existsSync33) {
|
|
5043
5347
|
const raw = readGitleaksVersion(run, tomlExists);
|
|
5044
5348
|
if (raw === null) return;
|
|
5045
5349
|
const local = majorMinorOf(raw);
|
|
@@ -5059,9 +5363,9 @@ function reportGitleaksVersionCheck(section2, run = execFileSync11, tomlExists =
|
|
|
5059
5363
|
|
|
5060
5364
|
// src/commands.doctor.checks.deps.ts
|
|
5061
5365
|
init_color();
|
|
5062
|
-
import { execFileSync as
|
|
5366
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
5063
5367
|
var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
|
|
5064
|
-
var
|
|
5368
|
+
var PROBE_TIMEOUT_MS3 = 3e3;
|
|
5065
5369
|
var FETCHER_BASE = "HTTP fetcher";
|
|
5066
5370
|
function parseFirstVersion(line) {
|
|
5067
5371
|
const m = VERSION_TOKEN.exec(line);
|
|
@@ -5071,7 +5375,7 @@ function probeOptionalDep(bin, run) {
|
|
|
5071
5375
|
try {
|
|
5072
5376
|
const firstLine = run(bin, ["--version"], {
|
|
5073
5377
|
stdio: ["ignore", "pipe", "pipe"],
|
|
5074
|
-
timeout:
|
|
5378
|
+
timeout: PROBE_TIMEOUT_MS3
|
|
5075
5379
|
}).toString().split("\n")[0].trim();
|
|
5076
5380
|
const version = parseFirstVersion(firstLine);
|
|
5077
5381
|
return { status: "present", version };
|
|
@@ -5096,7 +5400,7 @@ function reportFetcherRow(section2, run) {
|
|
|
5096
5400
|
);
|
|
5097
5401
|
}
|
|
5098
5402
|
}
|
|
5099
|
-
function reportOptionalDeps(section2, run =
|
|
5403
|
+
function reportOptionalDeps(section2, run = execFileSync14) {
|
|
5100
5404
|
const gh = probeOptionalDep("gh", run);
|
|
5101
5405
|
if (gh.status === "present") {
|
|
5102
5406
|
addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
|
|
@@ -5111,11 +5415,11 @@ function reportOptionalDeps(section2, run = execFileSync12) {
|
|
|
5111
5415
|
|
|
5112
5416
|
// src/commands.doctor.actions-drift.ts
|
|
5113
5417
|
init_color();
|
|
5114
|
-
import { execFileSync as
|
|
5418
|
+
import { execFileSync as execFileSync16 } from "node:child_process";
|
|
5115
5419
|
init_config();
|
|
5116
5420
|
|
|
5117
5421
|
// src/gh-actions.ts
|
|
5118
|
-
import { execFileSync as
|
|
5422
|
+
import { execFileSync as execFileSync15 } from "node:child_process";
|
|
5119
5423
|
var GH_TIMEOUT_MS = 5e3;
|
|
5120
5424
|
function parseGitHubRemote(remoteUrl) {
|
|
5121
5425
|
const normalized = remoteUrl.trim().replace(/\/$/, "");
|
|
@@ -5123,7 +5427,7 @@ function parseGitHubRemote(remoteUrl) {
|
|
|
5123
5427
|
if (m === null) return null;
|
|
5124
5428
|
return { owner: m[1], repo: m[2] };
|
|
5125
5429
|
}
|
|
5126
|
-
function ghAuthStatus(run =
|
|
5430
|
+
function ghAuthStatus(run = execFileSync15) {
|
|
5127
5431
|
try {
|
|
5128
5432
|
run("gh", ["auth", "status"], {
|
|
5129
5433
|
stdio: ["ignore", "ignore", "ignore"],
|
|
@@ -5137,7 +5441,7 @@ function ghAuthStatus(run = execFileSync13) {
|
|
|
5137
5441
|
return "gh-probe-error";
|
|
5138
5442
|
}
|
|
5139
5443
|
}
|
|
5140
|
-
function isRepoPrivate(ref, run =
|
|
5444
|
+
function isRepoPrivate(ref, run = execFileSync15) {
|
|
5141
5445
|
const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
|
|
5142
5446
|
stdio: ["ignore", "pipe", "ignore"],
|
|
5143
5447
|
timeout: GH_TIMEOUT_MS
|
|
@@ -5145,7 +5449,7 @@ function isRepoPrivate(ref, run = execFileSync13) {
|
|
|
5145
5449
|
const parsed = JSON.parse(out);
|
|
5146
5450
|
return parsed.isPrivate === true;
|
|
5147
5451
|
}
|
|
5148
|
-
function isActionsEnabled(ref, run =
|
|
5452
|
+
function isActionsEnabled(ref, run = execFileSync15) {
|
|
5149
5453
|
const out = run(
|
|
5150
5454
|
"gh",
|
|
5151
5455
|
["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
|
|
@@ -5153,7 +5457,7 @@ function isActionsEnabled(ref, run = execFileSync13) {
|
|
|
5153
5457
|
).toString().trim();
|
|
5154
5458
|
return out === "true";
|
|
5155
5459
|
}
|
|
5156
|
-
function disableActions(ref, run =
|
|
5460
|
+
function disableActions(ref, run = execFileSync15) {
|
|
5157
5461
|
run(
|
|
5158
5462
|
"gh",
|
|
5159
5463
|
[
|
|
@@ -5167,7 +5471,7 @@ function disableActions(ref, run = execFileSync13) {
|
|
|
5167
5471
|
{ stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
|
|
5168
5472
|
);
|
|
5169
5473
|
}
|
|
5170
|
-
function readOriginRemote(cwd, run =
|
|
5474
|
+
function readOriginRemote(cwd, run = execFileSync15) {
|
|
5171
5475
|
return run("git", ["remote", "get-url", "origin"], {
|
|
5172
5476
|
cwd,
|
|
5173
5477
|
stdio: ["ignore", "pipe", "ignore"]
|
|
@@ -5175,7 +5479,7 @@ function readOriginRemote(cwd, run = execFileSync13) {
|
|
|
5175
5479
|
}
|
|
5176
5480
|
|
|
5177
5481
|
// src/commands.doctor.actions-drift.ts
|
|
5178
|
-
function reportActionsDrift(section2, run =
|
|
5482
|
+
function reportActionsDrift(section2, run = execFileSync16) {
|
|
5179
5483
|
let remote;
|
|
5180
5484
|
try {
|
|
5181
5485
|
remote = readOriginRemote(repoHome(), run);
|
|
@@ -5261,9 +5565,12 @@ function gatherDoctorSections(opts) {
|
|
|
5261
5565
|
reportHostAndPaths(host);
|
|
5262
5566
|
reportHostKeyAlignment(host);
|
|
5263
5567
|
reportRepoState(host);
|
|
5568
|
+
reportSyncModality(host);
|
|
5569
|
+
reportLongPathsCheck(host);
|
|
5570
|
+
reportCrlfGuardCheck(host);
|
|
5264
5571
|
const links = section("Shared links");
|
|
5265
|
-
const mapPath =
|
|
5266
|
-
const rawMap =
|
|
5572
|
+
const mapPath = join40(repoHome(), "path-map.json");
|
|
5573
|
+
const rawMap = existsSync34(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
|
|
5267
5574
|
const map = rawMap ?? { projects: {} };
|
|
5268
5575
|
reportSharedLinks(links, map);
|
|
5269
5576
|
reportDroppedNamesMigration(links);
|
|
@@ -5359,15 +5666,15 @@ function parseDoctorArgs(args) {
|
|
|
5359
5666
|
|
|
5360
5667
|
// src/commands.drop-session.ts
|
|
5361
5668
|
init_config();
|
|
5362
|
-
import { execFileSync as
|
|
5363
|
-
import { existsSync as
|
|
5364
|
-
import { join as
|
|
5669
|
+
import { execFileSync as execFileSync18 } from "node:child_process";
|
|
5670
|
+
import { existsSync as existsSync36, readdirSync as readdirSync14, statSync as statSync9 } from "node:fs";
|
|
5671
|
+
import { join as join42, relative as relative6 } from "node:path";
|
|
5365
5672
|
|
|
5366
5673
|
// src/commands.drop-session.git.ts
|
|
5367
|
-
import { execFileSync as
|
|
5674
|
+
import { execFileSync as execFileSync17 } from "node:child_process";
|
|
5368
5675
|
function expandStagedDir(dirRel, repo) {
|
|
5369
5676
|
try {
|
|
5370
|
-
const out =
|
|
5677
|
+
const out = execFileSync17("git", ["ls-files", "-z", "--", dirRel], {
|
|
5371
5678
|
cwd: repo,
|
|
5372
5679
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5373
5680
|
});
|
|
@@ -5378,7 +5685,8 @@ function expandStagedDir(dirRel, repo) {
|
|
|
5378
5685
|
}
|
|
5379
5686
|
function isTrackedInHead(rel, repo) {
|
|
5380
5687
|
try {
|
|
5381
|
-
|
|
5688
|
+
const treePath = process.platform === "win32" ? rel.replaceAll("\\", "/") : rel;
|
|
5689
|
+
execFileSync17("git", ["cat-file", "-e", `HEAD:${treePath}`], {
|
|
5382
5690
|
cwd: repo,
|
|
5383
5691
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5384
5692
|
});
|
|
@@ -5389,7 +5697,7 @@ function isTrackedInHead(rel, repo) {
|
|
|
5389
5697
|
}
|
|
5390
5698
|
function isInIndex(rel, repo) {
|
|
5391
5699
|
try {
|
|
5392
|
-
const out =
|
|
5700
|
+
const out = execFileSync17("git", ["ls-files", "--", rel], {
|
|
5393
5701
|
cwd: repo,
|
|
5394
5702
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5395
5703
|
});
|
|
@@ -5403,8 +5711,8 @@ function isInIndex(rel, repo) {
|
|
|
5403
5711
|
init_config();
|
|
5404
5712
|
init_utils();
|
|
5405
5713
|
init_utils_json();
|
|
5406
|
-
import { existsSync as
|
|
5407
|
-
import { join as
|
|
5714
|
+
import { existsSync as existsSync35 } from "node:fs";
|
|
5715
|
+
import { join as join41 } from "node:path";
|
|
5408
5716
|
var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
|
|
5409
5717
|
function reportScrubHint(id, matches) {
|
|
5410
5718
|
const live = resolveLiveTranscript2(id, matches);
|
|
@@ -5420,17 +5728,17 @@ function reportScrubHint(id, matches) {
|
|
|
5420
5728
|
}
|
|
5421
5729
|
function resolveLiveTranscript2(id, matches) {
|
|
5422
5730
|
try {
|
|
5423
|
-
const mapPath =
|
|
5424
|
-
if (!
|
|
5731
|
+
const mapPath = join41(repoHome(), "path-map.json");
|
|
5732
|
+
if (!existsSync35(mapPath)) return null;
|
|
5425
5733
|
const projects = readJson(mapPath).projects;
|
|
5426
5734
|
const claude = claudeHome();
|
|
5427
5735
|
for (const rel of matches) {
|
|
5428
|
-
const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
|
|
5736
|
+
const logical = SHARED_PROJECT_LOGICAL.exec(rel.replaceAll("\\", "/"))?.[1];
|
|
5429
5737
|
if (logical === void 0) continue;
|
|
5430
5738
|
const abs = projects[logical]?.[HOST];
|
|
5431
5739
|
if (abs === void 0) continue;
|
|
5432
|
-
const live =
|
|
5433
|
-
if (
|
|
5740
|
+
const live = join41(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
5741
|
+
if (existsSync35(live)) return live;
|
|
5434
5742
|
}
|
|
5435
5743
|
return null;
|
|
5436
5744
|
} catch {
|
|
@@ -5446,12 +5754,12 @@ function cmdDropSession(id) {
|
|
|
5446
5754
|
process.exit(1);
|
|
5447
5755
|
}
|
|
5448
5756
|
const repo = repoHome();
|
|
5449
|
-
if (!
|
|
5757
|
+
if (!existsSync36(repo)) die(`repo not cloned at ${repo}`);
|
|
5450
5758
|
const handle = acquireLock("drop-session");
|
|
5451
5759
|
if (handle === null) process.exit(0);
|
|
5452
5760
|
try {
|
|
5453
|
-
const repoProjects =
|
|
5454
|
-
if (!
|
|
5761
|
+
const repoProjects = join42(repo, "shared", "projects");
|
|
5762
|
+
if (!existsSync36(repoProjects)) {
|
|
5455
5763
|
throw new NomadFatal(`no staged session matches ${id}`);
|
|
5456
5764
|
}
|
|
5457
5765
|
const matches = collectMatches(repoProjects, id, repo);
|
|
@@ -5473,13 +5781,13 @@ function cmdDropSession(id) {
|
|
|
5473
5781
|
}
|
|
5474
5782
|
function collectMatches(repoProjects, id, repo) {
|
|
5475
5783
|
const matches = [];
|
|
5476
|
-
for (const logical of
|
|
5477
|
-
const candidate =
|
|
5478
|
-
if (
|
|
5784
|
+
for (const logical of readdirSync14(repoProjects)) {
|
|
5785
|
+
const candidate = join42(repoProjects, logical, `${id}.jsonl`);
|
|
5786
|
+
if (existsSync36(candidate)) {
|
|
5479
5787
|
matches.push(relative6(repo, candidate));
|
|
5480
5788
|
}
|
|
5481
|
-
const dir =
|
|
5482
|
-
if (
|
|
5789
|
+
const dir = join42(repoProjects, logical, id);
|
|
5790
|
+
if (existsSync36(dir) && statSync9(dir).isDirectory()) {
|
|
5483
5791
|
const dirRel = relative6(repo, dir);
|
|
5484
5792
|
const staged = expandStagedDir(dirRel, repo);
|
|
5485
5793
|
if (staged.length > 0) matches.push(...staged);
|
|
@@ -5495,12 +5803,12 @@ function unstageOne(rel, repo) {
|
|
|
5495
5803
|
}
|
|
5496
5804
|
try {
|
|
5497
5805
|
if (isTrackedInHead(rel, repo)) {
|
|
5498
|
-
|
|
5806
|
+
execFileSync18("git", ["restore", "--staged", "--worktree", "--", rel], {
|
|
5499
5807
|
cwd: repo,
|
|
5500
5808
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5501
5809
|
});
|
|
5502
5810
|
} else {
|
|
5503
|
-
|
|
5811
|
+
execFileSync18("git", ["rm", "--cached", "-f", "--", rel], {
|
|
5504
5812
|
cwd: repo,
|
|
5505
5813
|
stdio: ["ignore", "pipe", "pipe"]
|
|
5506
5814
|
});
|
|
@@ -5514,8 +5822,8 @@ function unstageOne(rel, repo) {
|
|
|
5514
5822
|
}
|
|
5515
5823
|
|
|
5516
5824
|
// src/commands.pull.ts
|
|
5517
|
-
import { existsSync as
|
|
5518
|
-
import { join as
|
|
5825
|
+
import { existsSync as existsSync41, mkdirSync as mkdirSync10 } from "node:fs";
|
|
5826
|
+
import { join as join48 } from "node:path";
|
|
5519
5827
|
|
|
5520
5828
|
// src/commands.push.sections.ts
|
|
5521
5829
|
init_color();
|
|
@@ -5597,19 +5905,25 @@ function summarySection(st) {
|
|
|
5597
5905
|
addItem(s, summaryRow("push", unmapped, st.remap.collisions, st.extras.skipped));
|
|
5598
5906
|
return s;
|
|
5599
5907
|
}
|
|
5600
|
-
function
|
|
5908
|
+
function buildPushTreeSections(st, verdict) {
|
|
5601
5909
|
const leakScan = section("Leak scan");
|
|
5602
5910
|
addItem(leakScan, verdict.verdictRow);
|
|
5603
|
-
|
|
5911
|
+
return [...syncedSections(st), leakScan, summarySection(st)];
|
|
5604
5912
|
}
|
|
5605
|
-
function
|
|
5913
|
+
function renderPushTree(st, verdict) {
|
|
5914
|
+
renderTree(buildPushTreeSections(st, verdict));
|
|
5915
|
+
}
|
|
5916
|
+
function buildNoScanSections(st, opts = {}) {
|
|
5606
5917
|
const sections = [];
|
|
5607
5918
|
if (opts.noMapHint === true) {
|
|
5608
5919
|
const pathMap = section("Path map");
|
|
5609
5920
|
addItem(pathMap, `${dim(infoGlyph)} no path-map.json (nothing to preview)`);
|
|
5610
5921
|
sections.push(pathMap);
|
|
5611
5922
|
}
|
|
5612
|
-
|
|
5923
|
+
return [...sections, ...syncedSections(st), summarySection(st)];
|
|
5924
|
+
}
|
|
5925
|
+
function renderNoScanTree(st, opts = {}) {
|
|
5926
|
+
renderTree(buildNoScanSections(st, opts));
|
|
5613
5927
|
}
|
|
5614
5928
|
|
|
5615
5929
|
// src/commands.pull.ts
|
|
@@ -5617,18 +5931,18 @@ init_config();
|
|
|
5617
5931
|
|
|
5618
5932
|
// src/extras-sync.ts
|
|
5619
5933
|
init_config();
|
|
5620
|
-
import { existsSync as
|
|
5621
|
-
import { join as
|
|
5934
|
+
import { existsSync as existsSync38, statSync as statSync10 } from "node:fs";
|
|
5935
|
+
import { join as join45 } from "node:path";
|
|
5622
5936
|
init_utils();
|
|
5623
5937
|
init_utils_json();
|
|
5624
5938
|
|
|
5625
5939
|
// src/extras-sync.remap.ts
|
|
5626
5940
|
init_config();
|
|
5627
|
-
import { existsSync as
|
|
5628
|
-
import { dirname as dirname9, join as
|
|
5941
|
+
import { existsSync as existsSync37, mkdirSync as mkdirSync8, readdirSync as readdirSync15, readFileSync as readFileSync17, realpathSync as realpathSync4, rmSync as rmSync13 } from "node:fs";
|
|
5942
|
+
import { dirname as dirname9, join as join44, sep as sep8 } from "node:path";
|
|
5629
5943
|
|
|
5630
5944
|
// src/extras-sync.planning-diff.ts
|
|
5631
|
-
import { join as
|
|
5945
|
+
import { join as join43, normalize as normalize2, sep as sep7 } from "node:path";
|
|
5632
5946
|
init_utils();
|
|
5633
5947
|
function processRecord(fields, i, changed, deleted) {
|
|
5634
5948
|
const status = fields[i];
|
|
@@ -5680,15 +5994,15 @@ function planningDeleteTargets(opts) {
|
|
|
5680
5994
|
const { deleted } = parsePlanningDiff(raw);
|
|
5681
5995
|
const logicalPrefix = "shared/extras/" + logical + "/";
|
|
5682
5996
|
const prefix = logicalPrefix + ".planning/";
|
|
5683
|
-
const planningRoot =
|
|
5684
|
-
const planningRootBoundary = planningRoot +
|
|
5997
|
+
const planningRoot = join43(localRoot, ".planning");
|
|
5998
|
+
const planningRootBoundary = planningRoot + sep7;
|
|
5685
5999
|
const targets = [];
|
|
5686
6000
|
for (const repoPath of deleted) {
|
|
5687
6001
|
if (!repoPath.startsWith(prefix)) {
|
|
5688
6002
|
continue;
|
|
5689
6003
|
}
|
|
5690
6004
|
const remainder = repoPath.slice(logicalPrefix.length);
|
|
5691
|
-
const candidate =
|
|
6005
|
+
const candidate = join43(localRoot, remainder);
|
|
5692
6006
|
const resolved = normalize2(candidate);
|
|
5693
6007
|
if (!resolved.startsWith(planningRootBoundary)) {
|
|
5694
6008
|
throw new NomadFatal(
|
|
@@ -5709,7 +6023,7 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
|
|
|
5709
6023
|
const would = [];
|
|
5710
6024
|
for (const t of eachExtrasTarget(v, counts)) {
|
|
5711
6025
|
const { src, dst } = paths(t);
|
|
5712
|
-
if (!
|
|
6026
|
+
if (!existsSync37(src)) continue;
|
|
5713
6027
|
const item2 = `${t.logical}/${t.dirname}`;
|
|
5714
6028
|
if (dryRun) {
|
|
5715
6029
|
would.push(item2);
|
|
@@ -5723,10 +6037,10 @@ function runExtrasOp(v, dryRun, paths, backup, copy) {
|
|
|
5723
6037
|
}
|
|
5724
6038
|
function pruneEmptyAncestors(target, planningRoot) {
|
|
5725
6039
|
let dir = dirname9(target);
|
|
5726
|
-
while (dir !== planningRoot && dir.startsWith(planningRoot +
|
|
6040
|
+
while (dir !== planningRoot && dir.startsWith(planningRoot + sep8)) {
|
|
5727
6041
|
try {
|
|
5728
|
-
if (
|
|
5729
|
-
|
|
6042
|
+
if (readdirSync15(dir).length > 0) break;
|
|
6043
|
+
rmSync13(dir, { recursive: true, force: true });
|
|
5730
6044
|
} catch {
|
|
5731
6045
|
break;
|
|
5732
6046
|
}
|
|
@@ -5741,23 +6055,23 @@ function tryRealpath(dir) {
|
|
|
5741
6055
|
}
|
|
5742
6056
|
}
|
|
5743
6057
|
function isInsidePlanningRoot(parentReal, rootReal) {
|
|
5744
|
-
return parentReal === rootReal || parentReal.startsWith(rootReal +
|
|
6058
|
+
return parentReal === rootReal || parentReal.startsWith(rootReal + sep8);
|
|
5745
6059
|
}
|
|
5746
6060
|
function deletePlanningTarget(target, planningRoot, repoCounterpart) {
|
|
5747
|
-
if (
|
|
6061
|
+
if (existsSync37(repoCounterpart)) return;
|
|
5748
6062
|
const parentReal = tryRealpath(dirname9(target));
|
|
5749
6063
|
if (parentReal === void 0) return;
|
|
5750
6064
|
const rootReal = tryRealpath(planningRoot);
|
|
5751
6065
|
if (rootReal === void 0) return;
|
|
5752
6066
|
if (!isInsidePlanningRoot(parentReal, rootReal)) return;
|
|
5753
|
-
|
|
6067
|
+
rmSync13(target, { recursive: true, force: true });
|
|
5754
6068
|
pruneEmptyAncestors(target, planningRoot);
|
|
5755
6069
|
}
|
|
5756
6070
|
function localDivergesFromPreDelete(target, pre, repoRel, repo) {
|
|
5757
|
-
if (!
|
|
6071
|
+
if (!existsSync37(target)) return false;
|
|
5758
6072
|
try {
|
|
5759
6073
|
const preBlob = gitCaptureBuffer(["show", `${pre}:${repoRel}`], repo);
|
|
5760
|
-
return !
|
|
6074
|
+
return !readFileSync17(target).equals(preBlob);
|
|
5761
6075
|
} catch {
|
|
5762
6076
|
return true;
|
|
5763
6077
|
}
|
|
@@ -5768,11 +6082,11 @@ function planningDiffArgs(pre, post, logical) {
|
|
|
5768
6082
|
function deletePairsFor(t, raw) {
|
|
5769
6083
|
return planningDeleteTargets({ raw, logical: t.logical, localRoot: t.localRoot }).map(
|
|
5770
6084
|
(target) => {
|
|
5771
|
-
const relToLocal = target.slice(t.localRoot.length +
|
|
6085
|
+
const relToLocal = target.slice(t.localRoot.length + sep8.length);
|
|
5772
6086
|
return {
|
|
5773
6087
|
target,
|
|
5774
6088
|
relToLocal,
|
|
5775
|
-
repoRel: `shared/extras/${t.logical}/${relToLocal.split(
|
|
6089
|
+
repoRel: `shared/extras/${t.logical}/${relToLocal.split(sep8).join("/")}`
|
|
5776
6090
|
};
|
|
5777
6091
|
}
|
|
5778
6092
|
);
|
|
@@ -5799,7 +6113,7 @@ function keptDeletePreview(v, prePostHeads, repo) {
|
|
|
5799
6113
|
return kept;
|
|
5800
6114
|
}
|
|
5801
6115
|
function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
|
|
5802
|
-
const repoExtras =
|
|
6116
|
+
const repoExtras = join44(repo, "shared", "extras");
|
|
5803
6117
|
for (const t of eachExtrasTarget(v, { unmapped: 0, skipped: 0 })) {
|
|
5804
6118
|
if (t.dirname !== ".planning") continue;
|
|
5805
6119
|
let raw;
|
|
@@ -5814,14 +6128,14 @@ function propagatePlanningDeletes(v, ts, prePostHeads, repo) {
|
|
|
5814
6128
|
}
|
|
5815
6129
|
const pairs = deletePairsFor(t, raw);
|
|
5816
6130
|
if (pairs.length === 0) continue;
|
|
5817
|
-
backupExtrasWrite(
|
|
5818
|
-
const planningRoot =
|
|
6131
|
+
backupExtrasWrite(join44(t.localRoot, t.dirname), ts, t.localRoot);
|
|
6132
|
+
const planningRoot = join44(t.localRoot, ".planning");
|
|
5819
6133
|
for (const { target, relToLocal, repoRel } of pairs) {
|
|
5820
6134
|
if (localDivergesFromPreDelete(target, prePostHeads.pre, repoRel, repo)) {
|
|
5821
6135
|
warn(keptDeleteWarnLine(t.logical, relToLocal));
|
|
5822
6136
|
continue;
|
|
5823
6137
|
}
|
|
5824
|
-
deletePlanningTarget(target, planningRoot,
|
|
6138
|
+
deletePlanningTarget(target, planningRoot, join44(repoExtras, t.logical, relToLocal));
|
|
5825
6139
|
}
|
|
5826
6140
|
}
|
|
5827
6141
|
}
|
|
@@ -5830,14 +6144,14 @@ function remapExtrasPush(ts, opts = {}) {
|
|
|
5830
6144
|
const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
|
|
5831
6145
|
if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
|
|
5832
6146
|
const repo = repoHome();
|
|
5833
|
-
const repoExtras =
|
|
6147
|
+
const repoExtras = join44(repo, "shared", "extras");
|
|
5834
6148
|
if (!dryRun) mkdirSync8(repoExtras, { recursive: true });
|
|
5835
6149
|
const { unmapped, skipped, done, would } = runExtrasOp(
|
|
5836
6150
|
v,
|
|
5837
6151
|
dryRun,
|
|
5838
6152
|
({ localRoot, logical, dirname: dirname10 }) => ({
|
|
5839
|
-
src:
|
|
5840
|
-
dst:
|
|
6153
|
+
src: join44(localRoot, dirname10),
|
|
6154
|
+
dst: join44(repoExtras, logical, dirname10)
|
|
5841
6155
|
}),
|
|
5842
6156
|
(dst) => backupRepoWrite(dst, ts, repo),
|
|
5843
6157
|
// Push copy routing per extra type:
|
|
@@ -5866,8 +6180,8 @@ function remapExtrasPull(ts, opts = {}) {
|
|
|
5866
6180
|
v,
|
|
5867
6181
|
dryRun,
|
|
5868
6182
|
({ localRoot, logical, dirname: dirname10 }) => ({
|
|
5869
|
-
src:
|
|
5870
|
-
dst:
|
|
6183
|
+
src: join44(repo, "shared", "extras", logical, dirname10),
|
|
6184
|
+
dst: join44(localRoot, dirname10)
|
|
5871
6185
|
}),
|
|
5872
6186
|
// Snapshot the host-side dst BEFORE the copy step touches it. Anchor on
|
|
5873
6187
|
// localRoot so the backup tree mirrors the project layout.
|
|
@@ -5914,17 +6228,17 @@ function divergenceCheckExtras(ts, prePostHeads) {
|
|
|
5914
6228
|
const v = loadValidatedExtras({});
|
|
5915
6229
|
if (v === null) return 0;
|
|
5916
6230
|
const counts = { unmapped: 0, skipped: 0 };
|
|
5917
|
-
const backupRoot =
|
|
6231
|
+
const backupRoot = join45(backupBase(), ts, "extras");
|
|
5918
6232
|
const repo = repoHome();
|
|
5919
6233
|
let divergedCount = 0;
|
|
5920
6234
|
for (const { logical, localRoot, dirname: dirname10 } of eachExtrasTarget(v, counts)) {
|
|
5921
|
-
const local =
|
|
5922
|
-
const repoEntry =
|
|
5923
|
-
if (!
|
|
6235
|
+
const local = join45(localRoot, dirname10);
|
|
6236
|
+
const repoEntry = join45(repo, "shared", "extras", logical, dirname10);
|
|
6237
|
+
if (!existsSync38(local) || !existsSync38(repoEntry)) continue;
|
|
5924
6238
|
const modified = listDivergingModified(local, repoEntry);
|
|
5925
6239
|
if (modified.length === 0) continue;
|
|
5926
6240
|
divergedCount += modified.length;
|
|
5927
|
-
const projectBackupRoot =
|
|
6241
|
+
const projectBackupRoot = join45(backupRoot, encodePath(localRoot));
|
|
5928
6242
|
warn(
|
|
5929
6243
|
divergenceWarnLine({
|
|
5930
6244
|
dirname: dirname10,
|
|
@@ -5946,8 +6260,8 @@ function divergenceCheckExtras(ts, prePostHeads) {
|
|
|
5946
6260
|
|
|
5947
6261
|
// src/skills-sync.ts
|
|
5948
6262
|
init_config();
|
|
5949
|
-
import { existsSync as
|
|
5950
|
-
import { join as
|
|
6263
|
+
import { existsSync as existsSync39, lstatSync as lstatSync12, mkdirSync as mkdirSync9, readdirSync as readdirSync16, rmSync as rmSync14 } from "node:fs";
|
|
6264
|
+
import { join as join46 } from "node:path";
|
|
5951
6265
|
init_utils_fs();
|
|
5952
6266
|
function isGsdOwned(name) {
|
|
5953
6267
|
return name.startsWith(GSD_PREFIX);
|
|
@@ -5956,7 +6270,7 @@ function isSkillExcluded(name) {
|
|
|
5956
6270
|
return isGsdOwned(name) || isDeniedName(ALWAYS_NEVER_SYNC, name);
|
|
5957
6271
|
}
|
|
5958
6272
|
function copySkillsPush(src, dst) {
|
|
5959
|
-
const srcNames =
|
|
6273
|
+
const srcNames = readdirSync16(src, { encoding: "utf8" });
|
|
5960
6274
|
const blockSet = /* @__PURE__ */ new Set([
|
|
5961
6275
|
...srcNames.filter((n) => isGsdOwned(n)),
|
|
5962
6276
|
...ALWAYS_NEVER_SYNC
|
|
@@ -5967,30 +6281,30 @@ function copySkillsPull(src, dst) {
|
|
|
5967
6281
|
copyExtrasFilteredPreservingBy(src, dst, isSkillExcluded);
|
|
5968
6282
|
}
|
|
5969
6283
|
function syncSkillsPull(ts) {
|
|
5970
|
-
const sharedSkills =
|
|
5971
|
-
if (!
|
|
5972
|
-
const localSkills =
|
|
5973
|
-
const dstStat =
|
|
6284
|
+
const sharedSkills = join46(repoHome(), "shared", "skills");
|
|
6285
|
+
if (!existsSync39(sharedSkills)) return;
|
|
6286
|
+
const localSkills = join46(claudeHome(), "skills");
|
|
6287
|
+
const dstStat = lstatSync12(localSkills, { throwIfNoEntry: false });
|
|
5974
6288
|
if (dstStat?.isSymbolicLink() === true) {
|
|
5975
6289
|
backupBeforeWrite(localSkills, ts);
|
|
5976
|
-
|
|
6290
|
+
rmSync14(localSkills, { recursive: true, force: true });
|
|
5977
6291
|
mkdirSync9(localSkills, { recursive: true });
|
|
5978
6292
|
}
|
|
5979
6293
|
copySkillsPull(sharedSkills, localSkills);
|
|
5980
6294
|
}
|
|
5981
6295
|
function syncSkillsPush() {
|
|
5982
|
-
const localSkills =
|
|
5983
|
-
const stat =
|
|
6296
|
+
const localSkills = join46(claudeHome(), "skills");
|
|
6297
|
+
const stat = lstatSync12(localSkills, { throwIfNoEntry: false });
|
|
5984
6298
|
if (stat === void 0) return;
|
|
5985
6299
|
if (stat.isSymbolicLink()) return;
|
|
5986
|
-
const sharedSkills =
|
|
6300
|
+
const sharedSkills = join46(repoHome(), "shared", "skills");
|
|
5987
6301
|
copySkillsPush(localSkills, sharedSkills);
|
|
5988
6302
|
}
|
|
5989
6303
|
|
|
5990
6304
|
// src/preview.ts
|
|
5991
6305
|
init_config();
|
|
5992
|
-
import { existsSync as
|
|
5993
|
-
import { join as
|
|
6306
|
+
import { existsSync as existsSync40 } from "node:fs";
|
|
6307
|
+
import { join as join47 } from "node:path";
|
|
5994
6308
|
|
|
5995
6309
|
// node_modules/diff/libesm/diff/base.js
|
|
5996
6310
|
var Diff = class {
|
|
@@ -6276,7 +6590,7 @@ function diffJsonStrings(currentJsonText, newJsonText) {
|
|
|
6276
6590
|
return lines.join("\n");
|
|
6277
6591
|
}
|
|
6278
6592
|
function readJsonOrNull(path) {
|
|
6279
|
-
if (!
|
|
6593
|
+
if (!existsSync40(path)) return null;
|
|
6280
6594
|
try {
|
|
6281
6595
|
return readJson(path);
|
|
6282
6596
|
} catch {
|
|
@@ -6290,12 +6604,12 @@ function previewSettings(basePath, hostPath, settingsPath) {
|
|
|
6290
6604
|
}
|
|
6291
6605
|
const notes = [];
|
|
6292
6606
|
const hostOverrides = readJsonOrNull(hostPath);
|
|
6293
|
-
if (hostOverrides === null &&
|
|
6607
|
+
if (hostOverrides === null && existsSync40(hostPath)) {
|
|
6294
6608
|
notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
|
|
6295
6609
|
}
|
|
6296
6610
|
const merged = stripGsdHookEntries(deepMerge(base, hostOverrides ?? {}));
|
|
6297
6611
|
const current = readJsonOrNull(settingsPath);
|
|
6298
|
-
if (current === null &&
|
|
6612
|
+
if (current === null && existsSync40(settingsPath)) {
|
|
6299
6613
|
return { diff: "", notes: [...notes, "malformed; skipping diff"] };
|
|
6300
6614
|
}
|
|
6301
6615
|
const strippedCurrent = stripGsdHookEntries(current ?? {});
|
|
@@ -6308,6 +6622,7 @@ function previewSettings(basePath, hostPath, settingsPath) {
|
|
|
6308
6622
|
return { diff, notes };
|
|
6309
6623
|
}
|
|
6310
6624
|
function formatLinkRow(e) {
|
|
6625
|
+
if (e.kind === "copy") return `would copy ${e.from} -> ${e.to}`;
|
|
6311
6626
|
return `${e.kind} ${e.from} -> ${e.to}`;
|
|
6312
6627
|
}
|
|
6313
6628
|
function formatSessionRow(e) {
|
|
@@ -6336,9 +6651,9 @@ function computePreview(ts, map, verb = "pull") {
|
|
|
6336
6651
|
onPreview: (e) => addItem(links, formatLinkRow(e))
|
|
6337
6652
|
});
|
|
6338
6653
|
const settingsResult = previewSettings(
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6654
|
+
join47(repo, "shared", "settings.base.json"),
|
|
6655
|
+
join47(repo, "hosts", `${HOST}.json`),
|
|
6656
|
+
join47(claude, "settings.json")
|
|
6342
6657
|
);
|
|
6343
6658
|
const settingsSection = buildSettingsSectionForPreview(settingsResult);
|
|
6344
6659
|
const sessions = section("Sessions");
|
|
@@ -6353,7 +6668,7 @@ function computePreview(ts, map, verb = "pull") {
|
|
|
6353
6668
|
const extras = section("Extras");
|
|
6354
6669
|
let extrasSkipped = 0;
|
|
6355
6670
|
let extrasUnmapped = 0;
|
|
6356
|
-
if (
|
|
6671
|
+
if (existsSync40(join47(repo, "path-map.json")) && existsSync40(join47(repo, "shared", "extras"))) {
|
|
6357
6672
|
const extrasResult = remapExtrasPull(ts, { dryRun: true });
|
|
6358
6673
|
for (const entry of extrasResult.wouldPull) {
|
|
6359
6674
|
addItem(extras, entry);
|
|
@@ -6378,9 +6693,9 @@ init_config();
|
|
|
6378
6693
|
init_commands_pull_wedge();
|
|
6379
6694
|
init_utils();
|
|
6380
6695
|
init_utils_fs();
|
|
6381
|
-
import { execFileSync as
|
|
6696
|
+
import { execFileSync as execFileSync19 } from "node:child_process";
|
|
6382
6697
|
function gitCapture(args, cwd) {
|
|
6383
|
-
return
|
|
6698
|
+
return execFileSync19("git", args, {
|
|
6384
6699
|
cwd,
|
|
6385
6700
|
stdio: ["ignore", "pipe", "pipe"],
|
|
6386
6701
|
maxBuffer: 64 * 1024 * 1024
|
|
@@ -6524,17 +6839,9 @@ function buildWetPullSections(ts, map, prePostHeads) {
|
|
|
6524
6839
|
const remapResult = withSpinner("Syncing sessions", () => remapPull(ts));
|
|
6525
6840
|
const extrasResult = remapExtrasPull(ts, { prePostHeads });
|
|
6526
6841
|
const localOnly = scanLocalOnly();
|
|
6842
|
+
const unmapped = remapResult.unmapped + extrasResult.unmapped;
|
|
6527
6843
|
const summary = section(PULL_SUMMARY_HEADER);
|
|
6528
|
-
addItem(
|
|
6529
|
-
summary,
|
|
6530
|
-
summaryRow(
|
|
6531
|
-
"pull",
|
|
6532
|
-
remapResult.unmapped + extrasResult.unmapped,
|
|
6533
|
-
0,
|
|
6534
|
-
extrasResult.skipped,
|
|
6535
|
-
localOnly
|
|
6536
|
-
)
|
|
6537
|
-
);
|
|
6844
|
+
addItem(summary, summaryRow("pull", unmapped, 0, extrasResult.skipped, localOnly));
|
|
6538
6845
|
return {
|
|
6539
6846
|
sections: [
|
|
6540
6847
|
buildSettingsSection(label),
|
|
@@ -6542,7 +6849,10 @@ function buildWetPullSections(ts, map, prePostHeads) {
|
|
|
6542
6849
|
buildExtrasSection(extrasResult.pulled, extrasResult.skipped),
|
|
6543
6850
|
summary
|
|
6544
6851
|
],
|
|
6545
|
-
localOnly
|
|
6852
|
+
localOnly,
|
|
6853
|
+
settingsLabel: label,
|
|
6854
|
+
unmapped,
|
|
6855
|
+
extrasSkipped: extrasResult.skipped
|
|
6546
6856
|
};
|
|
6547
6857
|
}
|
|
6548
6858
|
function handleWedge(repo, forceRemote) {
|
|
@@ -6566,39 +6876,56 @@ function handleWedge(repo, forceRemote) {
|
|
|
6566
6876
|
function runPullCore(opts = {}) {
|
|
6567
6877
|
const dryRun = opts.dryRun === true;
|
|
6568
6878
|
const forceRemote = opts.forceRemote === true;
|
|
6879
|
+
const compose = opts.compose === true;
|
|
6569
6880
|
const repo = repoHome();
|
|
6570
6881
|
const backup = backupBase();
|
|
6571
6882
|
const ts = freshBackupTs(backup);
|
|
6572
6883
|
handleWedge(repo, forceRemote);
|
|
6573
6884
|
if (!dryRun) {
|
|
6574
|
-
const backupRoot =
|
|
6885
|
+
const backupRoot = join48(backup, ts);
|
|
6575
6886
|
try {
|
|
6576
6887
|
mkdirSync10(backupRoot, { recursive: true });
|
|
6577
6888
|
} catch (err) {
|
|
6578
6889
|
die(`could not create backup dir: ${err.message}`);
|
|
6579
6890
|
}
|
|
6580
6891
|
}
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6892
|
+
if (!compose) {
|
|
6893
|
+
log(
|
|
6894
|
+
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
6895
|
+
);
|
|
6896
|
+
}
|
|
6584
6897
|
const prePostHeads = capturePrePostHeads(repo, () => {
|
|
6585
6898
|
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
|
|
6586
6899
|
});
|
|
6587
|
-
const mapPath =
|
|
6588
|
-
const map =
|
|
6900
|
+
const mapPath = join48(repo, "path-map.json");
|
|
6901
|
+
const map = existsSync41(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
6589
6902
|
const divergedKeptLocal = divergenceCheckExtras(ts, dryRun ? prePostHeads : void 0);
|
|
6590
6903
|
if (dryRun) {
|
|
6591
6904
|
computePreview(ts, map, "pull");
|
|
6592
6905
|
log("dry-run complete; no mutation");
|
|
6593
6906
|
return { tag: "dry" };
|
|
6594
6907
|
}
|
|
6595
|
-
const { sections, localOnly } = buildWetPullSections(
|
|
6596
|
-
|
|
6908
|
+
const { sections, localOnly, settingsLabel, unmapped, extrasSkipped } = buildWetPullSections(
|
|
6909
|
+
ts,
|
|
6910
|
+
map,
|
|
6911
|
+
prePostHeads
|
|
6912
|
+
);
|
|
6913
|
+
const incomingChanges = prePostHeads === void 0 ? true : prePostHeads.pre !== prePostHeads.post;
|
|
6914
|
+
return {
|
|
6915
|
+
tag: "wet",
|
|
6916
|
+
sections,
|
|
6917
|
+
localOnly,
|
|
6918
|
+
divergedKeptLocal,
|
|
6919
|
+
incomingChanges,
|
|
6920
|
+
settingsLabel,
|
|
6921
|
+
unmapped,
|
|
6922
|
+
extrasSkipped
|
|
6923
|
+
};
|
|
6597
6924
|
}
|
|
6598
6925
|
function cmdPull(opts = {}) {
|
|
6599
6926
|
const repo = repoHome();
|
|
6600
|
-
if (!
|
|
6601
|
-
if (!
|
|
6927
|
+
if (!existsSync41(repo)) die(`repo not cloned at ${repo}`);
|
|
6928
|
+
if (!existsSync41(join48(repo, "shared", "settings.base.json"))) {
|
|
6602
6929
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
6603
6930
|
}
|
|
6604
6931
|
const handle = acquireLock("pull");
|
|
@@ -6620,13 +6947,13 @@ function cmdPull(opts = {}) {
|
|
|
6620
6947
|
|
|
6621
6948
|
// src/commands.push.ts
|
|
6622
6949
|
init_config();
|
|
6623
|
-
import { existsSync as
|
|
6624
|
-
import { join as
|
|
6950
|
+
import { existsSync as existsSync45 } from "node:fs";
|
|
6951
|
+
import { join as join53 } from "node:path";
|
|
6625
6952
|
|
|
6626
6953
|
// src/commands.push.selection.ts
|
|
6627
6954
|
init_config();
|
|
6628
|
-
import { existsSync as
|
|
6629
|
-
import { join as
|
|
6955
|
+
import { existsSync as existsSync42, statSync as statSync11 } from "node:fs";
|
|
6956
|
+
import { join as join49 } from "node:path";
|
|
6630
6957
|
init_utils_json();
|
|
6631
6958
|
function buildCurrentMap(map) {
|
|
6632
6959
|
const current = {};
|
|
@@ -6635,8 +6962,8 @@ function buildCurrentMap(map) {
|
|
|
6635
6962
|
for (const [, hostMap] of Object.entries(map.projects)) {
|
|
6636
6963
|
const localPath = hostMap[HOST];
|
|
6637
6964
|
if (!localPath) continue;
|
|
6638
|
-
const localDir =
|
|
6639
|
-
if (!
|
|
6965
|
+
const localDir = join49(claude, "projects", encodePath(localPath));
|
|
6966
|
+
if (!existsSync42(localDir)) continue;
|
|
6640
6967
|
for (const f of enumerateSourceFiles(localDir)) {
|
|
6641
6968
|
const st = statSync11(f);
|
|
6642
6969
|
current[f] = { size: st.size, mtime: st.mtimeMs };
|
|
@@ -6667,7 +6994,7 @@ function computePushSelection(map, old, scannerVersion, configHash, fullScan) {
|
|
|
6667
6994
|
};
|
|
6668
6995
|
}
|
|
6669
6996
|
function loadSelectionForPush(mapPath, old, scannerVersion, configHash, fullScan) {
|
|
6670
|
-
const map =
|
|
6997
|
+
const map = existsSync42(mapPath) ? readPathMap(mapPath) : null;
|
|
6671
6998
|
const { selection, newManifest } = computePushSelection(
|
|
6672
6999
|
map,
|
|
6673
7000
|
old,
|
|
@@ -6769,14 +7096,14 @@ function enforceAllowList(statusPorcelain, map) {
|
|
|
6769
7096
|
|
|
6770
7097
|
// src/commands.push.settings.ts
|
|
6771
7098
|
init_config();
|
|
6772
|
-
import { existsSync as
|
|
6773
|
-
import { join as
|
|
7099
|
+
import { existsSync as existsSync43 } from "node:fs";
|
|
7100
|
+
import { join as join50 } from "node:path";
|
|
6774
7101
|
init_utils();
|
|
6775
7102
|
init_utils_fs();
|
|
6776
7103
|
init_utils_json();
|
|
6777
7104
|
function stripGsdHooksFromBase(repo, backup) {
|
|
6778
|
-
const basePath =
|
|
6779
|
-
if (!
|
|
7105
|
+
const basePath = join50(repo, "shared", "settings.base.json");
|
|
7106
|
+
if (!existsSync43(basePath)) return;
|
|
6780
7107
|
let base;
|
|
6781
7108
|
try {
|
|
6782
7109
|
base = readJson(basePath);
|
|
@@ -6790,14 +7117,14 @@ function stripGsdHooksFromBase(repo, backup) {
|
|
|
6790
7117
|
writeJsonAtomic(basePath, stripped);
|
|
6791
7118
|
}
|
|
6792
7119
|
function reportSettingsAheadDrift(repo) {
|
|
6793
|
-
const basePath =
|
|
6794
|
-
if (!
|
|
6795
|
-
const settingsPath =
|
|
6796
|
-
if (!
|
|
7120
|
+
const basePath = join50(repo, "shared", "settings.base.json");
|
|
7121
|
+
if (!existsSync43(basePath)) return;
|
|
7122
|
+
const settingsPath = join50(claudeHome(), "settings.json");
|
|
7123
|
+
if (!existsSync43(settingsPath)) return;
|
|
6797
7124
|
try {
|
|
6798
7125
|
const base = readJson(basePath);
|
|
6799
|
-
const hostPath =
|
|
6800
|
-
const overrides =
|
|
7126
|
+
const hostPath = join50(repo, "hosts", `${HOST}.json`);
|
|
7127
|
+
const overrides = existsSync43(hostPath) ? readJson(hostPath) : {};
|
|
6801
7128
|
const merged = deepMerge(base, overrides);
|
|
6802
7129
|
const settings = readJson(settingsPath);
|
|
6803
7130
|
const { ahead } = classifySettingsDrift(merged, settings);
|
|
@@ -6814,12 +7141,12 @@ function reportSettingsAheadDrift(repo) {
|
|
|
6814
7141
|
// src/commands.push.guards.ts
|
|
6815
7142
|
init_push_checks();
|
|
6816
7143
|
init_utils();
|
|
6817
|
-
import { join as
|
|
7144
|
+
import { join as join51, relative as relative7 } from "node:path";
|
|
6818
7145
|
function guardGitlinks(repo) {
|
|
6819
|
-
const gitlinks = findGitlinks(
|
|
7146
|
+
const gitlinks = findGitlinks(join51(repo, "shared"));
|
|
6820
7147
|
if (gitlinks.length === 0) return;
|
|
6821
7148
|
for (const p of gitlinks) {
|
|
6822
|
-
const rel = relative7(repo, p);
|
|
7149
|
+
const rel = relative7(repo, p).replaceAll("\\", "/");
|
|
6823
7150
|
fail(`gitlink: ${rel} would push as submodule (run: rm -rf ${rel} or remove the nested repo)`);
|
|
6824
7151
|
}
|
|
6825
7152
|
const noun = gitlinks.length === 1 ? "entry" : "entries";
|
|
@@ -6848,7 +7175,7 @@ init_config();
|
|
|
6848
7175
|
|
|
6849
7176
|
// src/push-global-config.ts
|
|
6850
7177
|
init_config();
|
|
6851
|
-
import { execFileSync as
|
|
7178
|
+
import { execFileSync as execFileSync20 } from "node:child_process";
|
|
6852
7179
|
var STATUS_LABELS = {
|
|
6853
7180
|
A: "add",
|
|
6854
7181
|
M: "modify",
|
|
@@ -6888,7 +7215,7 @@ function isInScope(filePath, exactPrefixes, dirPrefixes) {
|
|
|
6888
7215
|
}
|
|
6889
7216
|
function collectGlobalConfigChanges(repoHome2, hostname2, opts) {
|
|
6890
7217
|
const args = opts.staged ? ["diff", "--cached", "--name-status", "-z"] : ["diff", "HEAD", "--name-status", "-z"];
|
|
6891
|
-
const raw =
|
|
7218
|
+
const raw = execFileSync20("git", args, {
|
|
6892
7219
|
cwd: repoHome2,
|
|
6893
7220
|
stdio: ["ignore", "pipe", "pipe"]
|
|
6894
7221
|
}).toString();
|
|
@@ -6926,9 +7253,9 @@ init_color();
|
|
|
6926
7253
|
init_config();
|
|
6927
7254
|
init_config_sharedDirs_guard();
|
|
6928
7255
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
6929
|
-
import { copyFileSync, existsSync as
|
|
7256
|
+
import { copyFileSync, existsSync as existsSync44, mkdirSync as mkdirSync11, readdirSync as readdirSync17, rmSync as rmSync15 } from "node:fs";
|
|
6930
7257
|
import { homedir as homedir5 } from "node:os";
|
|
6931
|
-
import { join as
|
|
7258
|
+
import { join as join52, relative as relative8, sep as sep9 } from "node:path";
|
|
6932
7259
|
init_push_leak_verdict();
|
|
6933
7260
|
init_push_gitleaks();
|
|
6934
7261
|
init_utils_fs();
|
|
@@ -6936,11 +7263,11 @@ init_utils_json();
|
|
|
6936
7263
|
var NOTHING_TO_SCAN_ROW = `${dim(infoGlyph)} nothing to scan, no leaks`;
|
|
6937
7264
|
function stageSessionDir(localDir, dstDir, changed) {
|
|
6938
7265
|
if (changed !== void 0) {
|
|
6939
|
-
const prefix = `${localDir}${
|
|
7266
|
+
const prefix = `${localDir}${sep9}`;
|
|
6940
7267
|
const matching = [...changed].filter((p) => p.startsWith(prefix));
|
|
6941
7268
|
if (matching.length === 0) return false;
|
|
6942
7269
|
for (const src of matching) {
|
|
6943
|
-
copyFileAtomic(src,
|
|
7270
|
+
copyFileAtomic(src, join52(dstDir, relative8(localDir, src)));
|
|
6944
7271
|
}
|
|
6945
7272
|
return true;
|
|
6946
7273
|
}
|
|
@@ -6956,14 +7283,14 @@ function stageSessions(tmpRoot, map, changed) {
|
|
|
6956
7283
|
if (!p || p === "TBD") continue;
|
|
6957
7284
|
reverse.set(encodePath(p), logical);
|
|
6958
7285
|
}
|
|
6959
|
-
const localProjects =
|
|
6960
|
-
if (!
|
|
7286
|
+
const localProjects = join52(claudeHome(), "projects");
|
|
7287
|
+
if (!existsSync44(localProjects)) return 0;
|
|
6961
7288
|
let staged = 0;
|
|
6962
|
-
for (const dir of
|
|
7289
|
+
for (const dir of readdirSync17(localProjects)) {
|
|
6963
7290
|
const logical = reverse.get(dir);
|
|
6964
7291
|
if (!logical) continue;
|
|
6965
|
-
const localDir =
|
|
6966
|
-
const dstDir =
|
|
7292
|
+
const localDir = join52(localProjects, dir);
|
|
7293
|
+
const dstDir = join52(tmpRoot, "shared", "projects", logical);
|
|
6967
7294
|
if (stageSessionDir(localDir, dstDir, changed)) staged++;
|
|
6968
7295
|
}
|
|
6969
7296
|
return staged;
|
|
@@ -6979,9 +7306,9 @@ function stageExtras(tmpRoot, map) {
|
|
|
6979
7306
|
if (!localRoot || localRoot === "TBD") continue;
|
|
6980
7307
|
for (const dirname10 of dirnames) {
|
|
6981
7308
|
if (!whitelist.includes(dirname10)) continue;
|
|
6982
|
-
const src =
|
|
6983
|
-
if (!
|
|
6984
|
-
const dst =
|
|
7309
|
+
const src = join52(localRoot, dirname10);
|
|
7310
|
+
if (!existsSync44(src)) continue;
|
|
7311
|
+
const dst = join52(tmpRoot, "shared", "extras", logical, dirname10);
|
|
6985
7312
|
copyExtras(src, dst);
|
|
6986
7313
|
staged++;
|
|
6987
7314
|
}
|
|
@@ -6989,19 +7316,19 @@ function stageExtras(tmpRoot, map) {
|
|
|
6989
7316
|
return staged;
|
|
6990
7317
|
}
|
|
6991
7318
|
function previewPushLeaks(map, opts = {}) {
|
|
6992
|
-
const cacheDir =
|
|
7319
|
+
const cacheDir = join52(homedir5(), ".cache", "claude-nomad");
|
|
6993
7320
|
mkdirSync11(cacheDir, { recursive: true });
|
|
6994
7321
|
const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
|
|
6995
|
-
const tmpRoot =
|
|
7322
|
+
const tmpRoot = join52(cacheDir, `push-preview-tree-${stamp}`);
|
|
6996
7323
|
try {
|
|
6997
7324
|
const sessionCount = stageSessions(tmpRoot, map, opts.selection?.changed);
|
|
6998
7325
|
const extrasCount = stageExtras(tmpRoot, map);
|
|
6999
7326
|
if (sessionCount + extrasCount === 0) {
|
|
7000
7327
|
return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
|
|
7001
7328
|
}
|
|
7002
|
-
const ignoreFile =
|
|
7003
|
-
if (
|
|
7004
|
-
copyFileSync(ignoreFile,
|
|
7329
|
+
const ignoreFile = join52(repoHome(), ".gitleaksignore");
|
|
7330
|
+
if (existsSync44(ignoreFile)) {
|
|
7331
|
+
copyFileSync(ignoreFile, join52(tmpRoot, ".gitleaksignore"));
|
|
7005
7332
|
}
|
|
7006
7333
|
let findings;
|
|
7007
7334
|
try {
|
|
@@ -7014,13 +7341,13 @@ function previewPushLeaks(map, opts = {}) {
|
|
|
7014
7341
|
}
|
|
7015
7342
|
return verdictFromFindings(findings);
|
|
7016
7343
|
} finally {
|
|
7017
|
-
|
|
7344
|
+
rmSync15(tmpRoot, { recursive: true, force: true });
|
|
7018
7345
|
}
|
|
7019
7346
|
}
|
|
7020
7347
|
|
|
7021
7348
|
// src/commands.push.steps.ts
|
|
7022
7349
|
init_utils();
|
|
7023
|
-
async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
|
|
7350
|
+
async function commitAndPush(st, ts, map, resolution, repo, newManifest, render) {
|
|
7024
7351
|
gitOrFatal(["add", "-A"], "git add", repo);
|
|
7025
7352
|
const staged = parsePorcelainZ2(gitStatusPorcelainZ(repo));
|
|
7026
7353
|
const toDrop = staged.filter((p) => isGsdDropped(p));
|
|
@@ -7028,13 +7355,18 @@ async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
|
|
|
7028
7355
|
gitOrFatal(["restore", "--staged", "--", ...toDrop], "git restore --staged", repo);
|
|
7029
7356
|
}
|
|
7030
7357
|
if (staged.length === toDrop.length) {
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7358
|
+
const sections2 = buildNoScanSections(st);
|
|
7359
|
+
if (render) {
|
|
7360
|
+
log("nothing to commit");
|
|
7361
|
+
renderTree(sections2);
|
|
7362
|
+
}
|
|
7363
|
+
return { outcome: "nothing", sections: sections2 };
|
|
7034
7364
|
}
|
|
7035
7365
|
st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: true });
|
|
7036
7366
|
let verdict = withSpinner("Scanning for secrets", () => scanPushVerdict(repo));
|
|
7367
|
+
const hadLeak = verdict.leak;
|
|
7037
7368
|
if (verdict.leak) {
|
|
7369
|
+
if (!render) log("push (leak recovery)");
|
|
7038
7370
|
renderPushTree(st, verdict);
|
|
7039
7371
|
verdict = await resolveLeakFindings(verdict, ts, map, resolution);
|
|
7040
7372
|
}
|
|
@@ -7045,8 +7377,13 @@ async function commitAndPush(st, ts, map, resolution, repo, newManifest) {
|
|
|
7045
7377
|
} catch (err) {
|
|
7046
7378
|
warn(`could not write push manifest (next push will full-rescan): ${String(err)}`);
|
|
7047
7379
|
}
|
|
7048
|
-
|
|
7049
|
-
|
|
7380
|
+
if (hadLeak) {
|
|
7381
|
+
renderPushTree(st, verdict);
|
|
7382
|
+
return { outcome: "pushed", sections: [] };
|
|
7383
|
+
}
|
|
7384
|
+
const sections = buildPushTreeSections(st, verdict);
|
|
7385
|
+
if (render) renderTree(sections);
|
|
7386
|
+
return { outcome: "pushed", sections };
|
|
7050
7387
|
}
|
|
7051
7388
|
function runDryRunPreview(st, map, repo, selection) {
|
|
7052
7389
|
st.globalConfig = collectGlobalConfigChanges(repo, HOST, { staged: false });
|
|
@@ -7063,20 +7400,61 @@ function runDryRunPreview(st, map, repo, selection) {
|
|
|
7063
7400
|
init_push_checks();
|
|
7064
7401
|
init_utils();
|
|
7065
7402
|
init_utils_fs();
|
|
7403
|
+
function aheadOfUpstream(repo) {
|
|
7404
|
+
try {
|
|
7405
|
+
const raw = gitCaptureRaw(["rev-list", "--count", "@{u}..HEAD"], repo);
|
|
7406
|
+
return Number.parseInt(raw.trim(), 10) > 0;
|
|
7407
|
+
} catch {
|
|
7408
|
+
return false;
|
|
7409
|
+
}
|
|
7410
|
+
}
|
|
7411
|
+
function emptyStatusResult(st, compose, repo) {
|
|
7412
|
+
if (compose) {
|
|
7413
|
+
return {
|
|
7414
|
+
tag: "nothing",
|
|
7415
|
+
sections: buildNoScanSections(st),
|
|
7416
|
+
aheadOfOrigin: aheadOfUpstream(repo),
|
|
7417
|
+
globalConfigCount: st.globalConfig.length,
|
|
7418
|
+
collisions: st.remap.collisions
|
|
7419
|
+
};
|
|
7420
|
+
}
|
|
7421
|
+
log("nothing to commit");
|
|
7422
|
+
renderNoScanTree(st);
|
|
7423
|
+
return { tag: "nothing" };
|
|
7424
|
+
}
|
|
7425
|
+
function toPushCoreResult(outcome, sections, compose, repo, st) {
|
|
7426
|
+
if (!compose) return { tag: outcome };
|
|
7427
|
+
const globalConfigCount = st.globalConfig.length;
|
|
7428
|
+
const collisions = st.remap.collisions;
|
|
7429
|
+
if (outcome === "nothing") {
|
|
7430
|
+
return {
|
|
7431
|
+
tag: "nothing",
|
|
7432
|
+
sections,
|
|
7433
|
+
aheadOfOrigin: aheadOfUpstream(repo),
|
|
7434
|
+
globalConfigCount,
|
|
7435
|
+
collisions
|
|
7436
|
+
};
|
|
7437
|
+
}
|
|
7438
|
+
return { tag: "pushed", sections, globalConfigCount, collisions };
|
|
7439
|
+
}
|
|
7066
7440
|
async function runPushCore(opts = {}) {
|
|
7067
7441
|
const dryRun = opts.dryRun === true;
|
|
7068
7442
|
const redactAll = opts.redactAll === true;
|
|
7069
7443
|
const allowAll = opts.allowAll === true;
|
|
7070
7444
|
const allowRule = opts.allowRule;
|
|
7071
7445
|
const fullScan = opts.fullScan === true;
|
|
7446
|
+
const compose = opts.compose === true;
|
|
7447
|
+
const render = !compose;
|
|
7072
7448
|
const repo = repoHome();
|
|
7073
7449
|
const backup = backupBase();
|
|
7074
|
-
|
|
7450
|
+
if (!compose) {
|
|
7451
|
+
console.log(dryRun ? `push on host=${HOST} (dry-run)` : `push on host=${HOST}`);
|
|
7452
|
+
}
|
|
7075
7453
|
reportSettingsAheadDrift(repo);
|
|
7076
7454
|
const scannerVersion = probeGitleaks();
|
|
7077
7455
|
const configHash = computeConfigHash();
|
|
7078
7456
|
const old = readManifest(manifestPath());
|
|
7079
|
-
const mapPath =
|
|
7457
|
+
const mapPath = join53(repo, "path-map.json");
|
|
7080
7458
|
const { map, selection, newManifest } = loadSelectionForPush(
|
|
7081
7459
|
mapPath,
|
|
7082
7460
|
old,
|
|
@@ -7090,16 +7468,13 @@ async function runPushCore(opts = {}) {
|
|
|
7090
7468
|
const extras = withSpinner("Syncing extras", () => remapExtrasPush(ts, { dryRun }));
|
|
7091
7469
|
if (!dryRun) {
|
|
7092
7470
|
syncSkillsPush();
|
|
7471
|
+
syncSharedLinksPush(map);
|
|
7093
7472
|
stripGsdHooksFromBase(repo, backup);
|
|
7094
7473
|
}
|
|
7095
7474
|
const st = { dryRun, remap, extras, globalConfig: [] };
|
|
7096
7475
|
guardGitlinks(repo);
|
|
7097
7476
|
const status = gitStatusPorcelainZ(repo, { untrackedAll: true });
|
|
7098
|
-
if (!dryRun && !status)
|
|
7099
|
-
log("nothing to commit");
|
|
7100
|
-
renderNoScanTree(st);
|
|
7101
|
-
return { tag: "nothing" };
|
|
7102
|
-
}
|
|
7477
|
+
if (!dryRun && !status) return emptyStatusResult(st, compose, repo);
|
|
7103
7478
|
if (map === null) {
|
|
7104
7479
|
if (dryRun) {
|
|
7105
7480
|
runDryRunPreview(st, null, repo, selection);
|
|
@@ -7112,15 +7487,16 @@ async function runPushCore(opts = {}) {
|
|
|
7112
7487
|
runDryRunPreview(st, map, repo, selection);
|
|
7113
7488
|
return { tag: "dry" };
|
|
7114
7489
|
}
|
|
7115
|
-
const outcome = await commitAndPush(
|
|
7490
|
+
const { outcome, sections } = await commitAndPush(
|
|
7116
7491
|
st,
|
|
7117
7492
|
ts,
|
|
7118
7493
|
map,
|
|
7119
7494
|
{ redactAll, allowAll, allowRule },
|
|
7120
7495
|
repo,
|
|
7121
|
-
newManifest
|
|
7496
|
+
newManifest,
|
|
7497
|
+
render
|
|
7122
7498
|
);
|
|
7123
|
-
return outcome
|
|
7499
|
+
return toPushCoreResult(outcome, sections, compose, repo, st);
|
|
7124
7500
|
}
|
|
7125
7501
|
async function cmdPush(opts = {}) {
|
|
7126
7502
|
const dryRun = opts.dryRun === true;
|
|
@@ -7129,7 +7505,7 @@ async function cmdPush(opts = {}) {
|
|
|
7129
7505
|
const allowRule = opts.allowRule;
|
|
7130
7506
|
guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
|
|
7131
7507
|
const repo = repoHome();
|
|
7132
|
-
if (!
|
|
7508
|
+
if (!existsSync45(repo)) die(`repo not cloned at ${repo}`);
|
|
7133
7509
|
const handle = acquireLock("push");
|
|
7134
7510
|
if (handle === null) process.exit(0);
|
|
7135
7511
|
try {
|
|
@@ -7147,36 +7523,59 @@ async function cmdPush(opts = {}) {
|
|
|
7147
7523
|
}
|
|
7148
7524
|
|
|
7149
7525
|
// src/commands.sync.ts
|
|
7150
|
-
import { existsSync as
|
|
7151
|
-
import { join as
|
|
7526
|
+
import { existsSync as existsSync46 } from "node:fs";
|
|
7527
|
+
import { join as join54 } from "node:path";
|
|
7152
7528
|
init_config();
|
|
7529
|
+
init_color();
|
|
7153
7530
|
init_utils();
|
|
7154
7531
|
init_utils_fs();
|
|
7155
7532
|
init_utils_json();
|
|
7156
|
-
function pullHasNoSyncedItems(sections) {
|
|
7157
|
-
return sections.every(
|
|
7158
|
-
(s) => s.header === "Settings" || s.header === PULL_SUMMARY_HEADER || s.items.length === 0
|
|
7159
|
-
);
|
|
7160
|
-
}
|
|
7161
7533
|
function isNoopSync(pull, pushOutcome) {
|
|
7162
7534
|
if (!pushOutcome.ok) return false;
|
|
7163
7535
|
if (pull.localOnly !== 0 || pull.divergedKeptLocal !== 0) return false;
|
|
7164
7536
|
if (pushOutcome.result.tag !== "nothing") return false;
|
|
7165
|
-
|
|
7537
|
+
if (pushOutcome.result.aheadOfOrigin === true) return false;
|
|
7538
|
+
return !pull.incomingChanges;
|
|
7166
7539
|
}
|
|
7167
|
-
function
|
|
7168
|
-
const
|
|
7169
|
-
return
|
|
7540
|
+
function buildPullSummaryRow(pull) {
|
|
7541
|
+
const applied = pull.incomingChanges ? "upstream changes applied" : "no upstream changes";
|
|
7542
|
+
return `pull: ${applied}; settings regenerated (base + ${pull.settingsLabel})`;
|
|
7170
7543
|
}
|
|
7171
|
-
function
|
|
7172
|
-
|
|
7544
|
+
function countNoun(n, singular, plural) {
|
|
7545
|
+
return `${n} ${n === 1 ? singular : plural}`;
|
|
7546
|
+
}
|
|
7547
|
+
function pushedParenParts(pull, globalConfigCount) {
|
|
7548
|
+
const parts = [];
|
|
7549
|
+
if (pull.localOnly > 0) {
|
|
7550
|
+
parts.push(countNoun(pull.localOnly, "local-only session", "local-only sessions"));
|
|
7551
|
+
}
|
|
7173
7552
|
if (pull.divergedKeptLocal > 0) {
|
|
7174
|
-
|
|
7553
|
+
parts.push(countNoun(pull.divergedKeptLocal, "diverged extras file", "diverged extras files"));
|
|
7175
7554
|
}
|
|
7176
|
-
if (
|
|
7177
|
-
|
|
7555
|
+
if (globalConfigCount > 0) {
|
|
7556
|
+
parts.push(countNoun(globalConfigCount, "config file", "config files"));
|
|
7178
7557
|
}
|
|
7179
|
-
return
|
|
7558
|
+
return parts;
|
|
7559
|
+
}
|
|
7560
|
+
function buildPushSummaryRow(pull, result) {
|
|
7561
|
+
if (result.tag !== "pushed") return "push: nothing to push";
|
|
7562
|
+
const parts = pushedParenParts(pull, result.globalConfigCount ?? 0);
|
|
7563
|
+
return parts.length > 0 ? `push: pushed (${parts.join(", ")})` : "push: pushed";
|
|
7564
|
+
}
|
|
7565
|
+
function buildSkipAndCollisionRows(pull, result) {
|
|
7566
|
+
const rows = [];
|
|
7567
|
+
if (pull.unmapped > 0) {
|
|
7568
|
+
rows.push(`${dim(infoGlyph)} ${pull.unmapped} not in path-map (run nomad doctor to list)`);
|
|
7569
|
+
}
|
|
7570
|
+
if (pull.extrasSkipped > 0) {
|
|
7571
|
+
rows.push(`${dim(infoGlyph)} ${pull.extrasSkipped} extras skipped`);
|
|
7572
|
+
}
|
|
7573
|
+
const collisions = result.tag === "dry" ? void 0 : result.collisions;
|
|
7574
|
+
if (collisions !== void 0 && collisions > 0) {
|
|
7575
|
+
const phrase = countNoun(collisions, "collision", "collisions");
|
|
7576
|
+
rows.push(`${yellow(warnGlyph)} ${phrase} (run nomad doctor to list)`);
|
|
7577
|
+
}
|
|
7578
|
+
return rows;
|
|
7180
7579
|
}
|
|
7181
7580
|
function buildSyncSummarySection(pull, pushOutcome) {
|
|
7182
7581
|
const s = section("Sync summary");
|
|
@@ -7184,22 +7583,59 @@ function buildSyncSummarySection(pull, pushOutcome) {
|
|
|
7184
7583
|
addItem(s, `pull: applied, push: failed (${pushOutcome.message})`);
|
|
7185
7584
|
return s;
|
|
7186
7585
|
}
|
|
7187
|
-
|
|
7188
|
-
addItem(s,
|
|
7189
|
-
|
|
7586
|
+
const { result } = pushOutcome;
|
|
7587
|
+
addItem(s, buildPullSummaryRow(pull));
|
|
7588
|
+
addItem(s, buildPushSummaryRow(pull, result));
|
|
7589
|
+
if (result.tag === "nothing" && result.aheadOfOrigin === true) {
|
|
7590
|
+
addItem(s, "sync repo has unpushed commits");
|
|
7591
|
+
}
|
|
7592
|
+
for (const row2 of buildSkipAndCollisionRows(pull, result)) addItem(s, row2);
|
|
7190
7593
|
return s;
|
|
7191
7594
|
}
|
|
7192
|
-
|
|
7595
|
+
var SYNC_SECTION_ORDER = ["Settings", "Global config", "Sessions", "Extras", "Leak scan"];
|
|
7596
|
+
function isDroppedSummaryHeader(header) {
|
|
7597
|
+
return header === PULL_SUMMARY_HEADER || header === "Push summary";
|
|
7598
|
+
}
|
|
7599
|
+
function mergeSyncSections(pullSections, pushSections) {
|
|
7600
|
+
const byHeader = new Map(
|
|
7601
|
+
SYNC_SECTION_ORDER.map((header) => [header, section(header)])
|
|
7602
|
+
);
|
|
7603
|
+
for (const s of [...pullSections, ...pushSections]) {
|
|
7604
|
+
if (isDroppedSummaryHeader(s.header)) continue;
|
|
7605
|
+
let target = byHeader.get(s.header);
|
|
7606
|
+
if (target === void 0) {
|
|
7607
|
+
target = section(s.header);
|
|
7608
|
+
byHeader.set(s.header, target);
|
|
7609
|
+
}
|
|
7610
|
+
for (const item2 of s.items) {
|
|
7611
|
+
if (!target.items.includes(item2)) addItem(target, item2);
|
|
7612
|
+
}
|
|
7613
|
+
}
|
|
7614
|
+
return [...byHeader.values()].filter((s) => s.items.length > 0);
|
|
7615
|
+
}
|
|
7616
|
+
function pushSectionsOf(pushOutcome) {
|
|
7617
|
+
if (!pushOutcome.ok) return [];
|
|
7618
|
+
const result = pushOutcome.result;
|
|
7619
|
+
if (result.tag === "dry") return [];
|
|
7620
|
+
return result.sections ?? [];
|
|
7621
|
+
}
|
|
7622
|
+
function renderWetSync(pull, pushOutcome, verbose) {
|
|
7193
7623
|
if (isNoopSync(pull, pushOutcome)) {
|
|
7194
7624
|
ok("already in sync");
|
|
7195
7625
|
return;
|
|
7196
7626
|
}
|
|
7197
|
-
|
|
7198
|
-
|
|
7627
|
+
log(`sync on host=${HOST}`);
|
|
7628
|
+
const summary = buildSyncSummarySection(pull, pushOutcome);
|
|
7629
|
+
if (!verbose) {
|
|
7630
|
+
renderTree([summary]);
|
|
7631
|
+
return;
|
|
7632
|
+
}
|
|
7633
|
+
const merged = mergeSyncSections(pull.sections, pushSectionsOf(pushOutcome));
|
|
7634
|
+
renderTree([...merged, summary]);
|
|
7199
7635
|
}
|
|
7200
7636
|
async function runSyncPushHalf() {
|
|
7201
7637
|
try {
|
|
7202
|
-
const result = await runPushCore();
|
|
7638
|
+
const result = await runPushCore({ compose: true });
|
|
7203
7639
|
return { ok: true, result };
|
|
7204
7640
|
} catch (err) {
|
|
7205
7641
|
if (err instanceof NomadFatal) {
|
|
@@ -7209,25 +7645,26 @@ async function runSyncPushHalf() {
|
|
|
7209
7645
|
throw err;
|
|
7210
7646
|
}
|
|
7211
7647
|
}
|
|
7212
|
-
async function runSyncWet() {
|
|
7213
|
-
const pull = runPullCore();
|
|
7648
|
+
async function runSyncWet(verbose) {
|
|
7649
|
+
const pull = runPullCore({ compose: true });
|
|
7214
7650
|
const pushOutcome = await runSyncPushHalf();
|
|
7215
|
-
renderWetSync(pull, pushOutcome);
|
|
7651
|
+
renderWetSync(pull, pushOutcome, verbose);
|
|
7216
7652
|
}
|
|
7217
7653
|
async function runSyncDryRun(repo, backup) {
|
|
7218
7654
|
const ts = freshBackupTs(backup);
|
|
7219
|
-
const mapPath =
|
|
7220
|
-
const map =
|
|
7655
|
+
const mapPath = join54(repo, "path-map.json");
|
|
7656
|
+
const map = existsSync46(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
7221
7657
|
computePreview(ts, map, "pull");
|
|
7222
7658
|
log("push preview below is computed against pre-pull state (a real sync pushes after pull)");
|
|
7223
7659
|
await runPushCore({ dryRun: true });
|
|
7224
7660
|
}
|
|
7225
7661
|
async function cmdSync(opts = {}) {
|
|
7226
7662
|
const dryRun = opts.dryRun === true;
|
|
7663
|
+
const verbose = opts.verbose === true;
|
|
7227
7664
|
const repo = repoHome();
|
|
7228
7665
|
const backup = backupBase();
|
|
7229
|
-
if (!
|
|
7230
|
-
if (!
|
|
7666
|
+
if (!existsSync46(repo)) die(`repo not cloned at ${repo}`);
|
|
7667
|
+
if (!existsSync46(join54(repo, "shared", "settings.base.json"))) {
|
|
7231
7668
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
7232
7669
|
}
|
|
7233
7670
|
const handle = acquireLock("sync");
|
|
@@ -7236,7 +7673,7 @@ async function cmdSync(opts = {}) {
|
|
|
7236
7673
|
if (dryRun) {
|
|
7237
7674
|
await runSyncDryRun(repo, backup);
|
|
7238
7675
|
} else {
|
|
7239
|
-
await runSyncWet();
|
|
7676
|
+
await runSyncWet(verbose);
|
|
7240
7677
|
}
|
|
7241
7678
|
} catch (err) {
|
|
7242
7679
|
if (err instanceof NomadFatal) {
|
|
@@ -7251,25 +7688,31 @@ async function cmdSync(opts = {}) {
|
|
|
7251
7688
|
}
|
|
7252
7689
|
|
|
7253
7690
|
// src/commands.update.ts
|
|
7254
|
-
import { execFileSync as
|
|
7691
|
+
import { execFileSync as execFileSync21 } from "node:child_process";
|
|
7255
7692
|
init_utils();
|
|
7256
|
-
function readInstalledVersion(run =
|
|
7693
|
+
function readInstalledVersion(run = execFileSync21) {
|
|
7694
|
+
const isWin = process.platform === "win32";
|
|
7257
7695
|
try {
|
|
7258
|
-
return run("nomad", ["--version"], {
|
|
7696
|
+
return run(isWin ? "nomad.cmd" : "nomad", ["--version"], {
|
|
7697
|
+
encoding: "utf8",
|
|
7698
|
+
...isWin ? { shell: true } : {}
|
|
7699
|
+
}).toString().trim() || null;
|
|
7259
7700
|
} catch {
|
|
7260
7701
|
return null;
|
|
7261
7702
|
}
|
|
7262
7703
|
}
|
|
7263
|
-
function cmdUpdate(currentVersion, run =
|
|
7704
|
+
function cmdUpdate(currentVersion, run = execFileSync21) {
|
|
7264
7705
|
console.log(`Updating claude-nomad v${currentVersion}...`);
|
|
7706
|
+
const isWin = process.platform === "win32";
|
|
7265
7707
|
try {
|
|
7266
|
-
run("npm", ["update", "-g", "claude-nomad"], {
|
|
7708
|
+
run(isWin ? "npm.cmd" : "npm", ["update", "-g", "claude-nomad"], {
|
|
7267
7709
|
encoding: "utf8",
|
|
7268
7710
|
stdio: ["ignore", "pipe", "pipe"],
|
|
7269
7711
|
// Now that output is captured rather than inherited, execFileSync buffers
|
|
7270
7712
|
// it; lift the default 1 MiB ceiling so a noisy-but-successful npm run
|
|
7271
7713
|
// (deprecation/funding spam) cannot throw ENOBUFS.
|
|
7272
|
-
maxBuffer: 64 * 1024 * 1024
|
|
7714
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
7715
|
+
...isWin ? { shell: true } : {}
|
|
7273
7716
|
});
|
|
7274
7717
|
} catch (err) {
|
|
7275
7718
|
const e = err;
|
|
@@ -7294,18 +7737,18 @@ init_config();
|
|
|
7294
7737
|
|
|
7295
7738
|
// src/diff.ts
|
|
7296
7739
|
init_config();
|
|
7297
|
-
import { existsSync as
|
|
7298
|
-
import { join as
|
|
7740
|
+
import { existsSync as existsSync47 } from "node:fs";
|
|
7741
|
+
import { join as join55 } from "node:path";
|
|
7299
7742
|
init_utils();
|
|
7300
7743
|
init_utils_fs();
|
|
7301
7744
|
init_utils_json();
|
|
7302
7745
|
function cmdDiff() {
|
|
7303
7746
|
try {
|
|
7304
7747
|
const repo = repoHome();
|
|
7305
|
-
if (!
|
|
7748
|
+
if (!existsSync47(repo)) die(`repo not cloned at ${repo}`);
|
|
7306
7749
|
const ts = freshBackupTs(backupBase());
|
|
7307
|
-
const mapPath =
|
|
7308
|
-
const map =
|
|
7750
|
+
const mapPath = join55(repo, "path-map.json");
|
|
7751
|
+
const map = existsSync47(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
7309
7752
|
divergenceCheckExtras(ts);
|
|
7310
7753
|
computePreview(ts, map, "diff");
|
|
7311
7754
|
} catch (err) {
|
|
@@ -7320,19 +7763,19 @@ function cmdDiff() {
|
|
|
7320
7763
|
|
|
7321
7764
|
// src/init.ts
|
|
7322
7765
|
init_config();
|
|
7323
|
-
import { existsSync as
|
|
7324
|
-
import { join as
|
|
7766
|
+
import { existsSync as existsSync49, mkdirSync as mkdirSync12, writeFileSync as writeFileSync6 } from "node:fs";
|
|
7767
|
+
import { join as join57 } from "node:path";
|
|
7325
7768
|
|
|
7326
7769
|
// src/init.gh-onboard.ts
|
|
7327
7770
|
init_config();
|
|
7328
|
-
import { execFileSync as
|
|
7771
|
+
import { execFileSync as execFileSync22 } from "node:child_process";
|
|
7329
7772
|
init_utils();
|
|
7330
7773
|
var DEFAULT_REPO_NAME = "claude-nomad-config";
|
|
7331
7774
|
function isValidRepoName(name) {
|
|
7332
7775
|
return /^[A-Za-z0-9._-]{1,100}$/.test(name);
|
|
7333
7776
|
}
|
|
7334
7777
|
var GH_NETWORK_TIMEOUT_MS = 3e4;
|
|
7335
|
-
function ensureOriginRepo(repoName, run =
|
|
7778
|
+
function ensureOriginRepo(repoName, run = execFileSync22) {
|
|
7336
7779
|
if (!isValidRepoName(repoName)) {
|
|
7337
7780
|
die(
|
|
7338
7781
|
`invalid repo name: ${JSON.stringify(repoName)}. Use only letters, digits, hyphens, underscores, and dots (1-100 chars).`
|
|
@@ -7403,33 +7846,33 @@ init_config();
|
|
|
7403
7846
|
init_utils();
|
|
7404
7847
|
init_utils_fs();
|
|
7405
7848
|
init_utils_json();
|
|
7406
|
-
import { copyFileSync as copyFileSync2, cpSync as
|
|
7407
|
-
import { join as
|
|
7849
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync8, existsSync as existsSync48, rmSync as rmSync16, statSync as statSync12 } from "node:fs";
|
|
7850
|
+
import { join as join56 } from "node:path";
|
|
7408
7851
|
function snapshotIntoShared(map) {
|
|
7409
7852
|
const repo = repoHome();
|
|
7410
7853
|
const claude = claudeHome();
|
|
7411
7854
|
for (const name of allSharedLinks(map)) {
|
|
7412
|
-
const src =
|
|
7413
|
-
if (!
|
|
7414
|
-
const dst =
|
|
7855
|
+
const src = join56(claude, name);
|
|
7856
|
+
if (!existsSync48(src)) continue;
|
|
7857
|
+
const dst = join56(repo, "shared", name);
|
|
7415
7858
|
if (statSync12(src).isDirectory()) {
|
|
7416
|
-
const gk =
|
|
7417
|
-
if (
|
|
7418
|
-
|
|
7859
|
+
const gk = join56(dst, ".gitkeep");
|
|
7860
|
+
if (existsSync48(gk)) rmSync16(gk);
|
|
7861
|
+
cpSync8(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
7419
7862
|
} else {
|
|
7420
7863
|
copyFileSync2(src, dst);
|
|
7421
7864
|
}
|
|
7422
7865
|
log(`snapshotted shared/${name} from ${src}`);
|
|
7423
7866
|
}
|
|
7424
|
-
const userSettings =
|
|
7425
|
-
if (
|
|
7867
|
+
const userSettings = join56(claude, "settings.json");
|
|
7868
|
+
if (existsSync48(userSettings)) {
|
|
7426
7869
|
let parsed;
|
|
7427
7870
|
try {
|
|
7428
7871
|
parsed = readJson(userSettings);
|
|
7429
7872
|
} catch (err) {
|
|
7430
7873
|
return die(`malformed ${userSettings}: ${err.message}`);
|
|
7431
7874
|
}
|
|
7432
|
-
const hostFile =
|
|
7875
|
+
const hostFile = join56(repo, "hosts", `${HOST}.json`);
|
|
7433
7876
|
writeJsonAtomic(hostFile, parsed);
|
|
7434
7877
|
log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
|
|
7435
7878
|
}
|
|
@@ -7439,17 +7882,18 @@ function snapshotIntoShared(map) {
|
|
|
7439
7882
|
init_utils();
|
|
7440
7883
|
init_utils_fs();
|
|
7441
7884
|
var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.claude/CLAUDE.md by nomad pull -->\n";
|
|
7885
|
+
var GITATTRIBUTES = "# nomad: sync content is byte-managed, disable all line-ending conversion\n* -text\n";
|
|
7442
7886
|
var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
|
|
7443
7887
|
function preflightConflict(repoHome2) {
|
|
7444
7888
|
const candidates = [
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7448
|
-
|
|
7449
|
-
|
|
7889
|
+
join57(repoHome2, "shared", "settings.base.json"),
|
|
7890
|
+
join57(repoHome2, "shared", "CLAUDE.md"),
|
|
7891
|
+
join57(repoHome2, "path-map.json"),
|
|
7892
|
+
join57(repoHome2, "hosts"),
|
|
7893
|
+
join57(repoHome2, "shared")
|
|
7450
7894
|
];
|
|
7451
7895
|
for (const c of candidates) {
|
|
7452
|
-
if (
|
|
7896
|
+
if (existsSync49(c)) return c;
|
|
7453
7897
|
}
|
|
7454
7898
|
return null;
|
|
7455
7899
|
}
|
|
@@ -7467,26 +7911,28 @@ function cmdInit(opts = {}) {
|
|
|
7467
7911
|
die(`already initialized; refusing to clobber ${conflict}`);
|
|
7468
7912
|
}
|
|
7469
7913
|
ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
|
|
7470
|
-
mkdirSync12(
|
|
7471
|
-
mkdirSync12(
|
|
7914
|
+
mkdirSync12(join57(repo, "shared"), { recursive: true });
|
|
7915
|
+
mkdirSync12(join57(repo, "hosts"), { recursive: true });
|
|
7472
7916
|
for (const name of SHARED_KEEP_DIRS) {
|
|
7473
|
-
mkdirSync12(
|
|
7917
|
+
mkdirSync12(join57(repo, "shared", name), { recursive: true });
|
|
7474
7918
|
}
|
|
7475
|
-
const userClaudeMd =
|
|
7476
|
-
if (!snapshot || !
|
|
7477
|
-
writeFileSync6(
|
|
7919
|
+
const userClaudeMd = join57(claude, "CLAUDE.md");
|
|
7920
|
+
if (!snapshot || !existsSync49(userClaudeMd)) {
|
|
7921
|
+
writeFileSync6(join57(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
|
|
7478
7922
|
item("created shared/CLAUDE.md");
|
|
7479
7923
|
}
|
|
7480
7924
|
for (const name of SHARED_KEEP_DIRS) {
|
|
7481
|
-
writeFileSync6(
|
|
7925
|
+
writeFileSync6(join57(repo, "shared", name, ".gitkeep"), "");
|
|
7482
7926
|
item(`created shared/${name}/.gitkeep`);
|
|
7483
7927
|
}
|
|
7484
|
-
writeFileSync6(
|
|
7928
|
+
writeFileSync6(join57(repo, "hosts", ".gitkeep"), "");
|
|
7485
7929
|
item("created hosts/.gitkeep");
|
|
7486
|
-
writeJsonAtomic(
|
|
7930
|
+
writeJsonAtomic(join57(repo, "shared", "settings.base.json"), {});
|
|
7487
7931
|
item("created shared/settings.base.json");
|
|
7488
|
-
writeJsonAtomic(
|
|
7932
|
+
writeJsonAtomic(join57(repo, "path-map.json"), { projects: {} });
|
|
7489
7933
|
item("created path-map.json");
|
|
7934
|
+
writeFileSync6(join57(repo, ".gitattributes"), GITATTRIBUTES);
|
|
7935
|
+
item("created .gitattributes");
|
|
7490
7936
|
if (snapshot) {
|
|
7491
7937
|
snapshotIntoShared({ projects: {} });
|
|
7492
7938
|
log(`snapshot staged in shared/; review, then 'nomad push' to share with other hosts.`);
|
|
@@ -7551,21 +7997,21 @@ function maybeDisableRepoActions(repoHome2, run) {
|
|
|
7551
7997
|
// src/init.prompt.ts
|
|
7552
7998
|
init_config();
|
|
7553
7999
|
init_utils();
|
|
7554
|
-
import { existsSync as
|
|
7555
|
-
import { join as
|
|
8000
|
+
import { existsSync as existsSync50, readdirSync as readdirSync18, statSync as statSync13 } from "node:fs";
|
|
8001
|
+
import { join as join58 } from "node:path";
|
|
7556
8002
|
import { createInterface as createInterface3 } from "node:readline/promises";
|
|
7557
8003
|
function nonEmptyExists(path) {
|
|
7558
|
-
if (!
|
|
8004
|
+
if (!existsSync50(path)) return false;
|
|
7559
8005
|
try {
|
|
7560
|
-
if (statSync13(path).isDirectory()) return
|
|
8006
|
+
if (statSync13(path).isDirectory()) return readdirSync18(path).length > 0;
|
|
7561
8007
|
return true;
|
|
7562
8008
|
} catch {
|
|
7563
8009
|
return false;
|
|
7564
8010
|
}
|
|
7565
8011
|
}
|
|
7566
8012
|
function hasExistingClaudeConfig(claudeHome2) {
|
|
7567
|
-
if (
|
|
7568
|
-
return SHARED_LINKS.some((name) => nonEmptyExists(
|
|
8013
|
+
if (existsSync50(join58(claudeHome2, "settings.json"))) return true;
|
|
8014
|
+
return SHARED_LINKS.some((name) => nonEmptyExists(join58(claudeHome2, name)));
|
|
7569
8015
|
}
|
|
7570
8016
|
async function confirmSnapshotDefault(claudeHome2) {
|
|
7571
8017
|
if (process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
|
|
@@ -7852,24 +8298,27 @@ function parsePushArgs(argv) {
|
|
|
7852
8298
|
// src/nomad.dispatch.sync.ts
|
|
7853
8299
|
function parseSyncArgs(argv) {
|
|
7854
8300
|
let dryRun = false;
|
|
8301
|
+
let verbose = false;
|
|
7855
8302
|
let i = 3;
|
|
7856
8303
|
while (i < argv.length) {
|
|
7857
8304
|
const token = argv[i];
|
|
7858
8305
|
if (token === "--dry-run") {
|
|
7859
8306
|
if (dryRun) return null;
|
|
7860
8307
|
dryRun = true;
|
|
8308
|
+
} else if (token === "--verbose" || token === "--all" || token === "-v") {
|
|
8309
|
+
verbose = true;
|
|
7861
8310
|
} else {
|
|
7862
8311
|
return null;
|
|
7863
8312
|
}
|
|
7864
8313
|
i++;
|
|
7865
8314
|
}
|
|
7866
|
-
return { dryRun };
|
|
8315
|
+
return { dryRun, verbose };
|
|
7867
8316
|
}
|
|
7868
8317
|
|
|
7869
8318
|
// package.json
|
|
7870
8319
|
var package_default = {
|
|
7871
8320
|
name: "claude-nomad",
|
|
7872
|
-
version: "0.
|
|
8321
|
+
version: "0.60.0",
|
|
7873
8322
|
type: "module",
|
|
7874
8323
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
7875
8324
|
keywords: [
|
|
@@ -8004,10 +8453,15 @@ var DEFAULT_HELP = [
|
|
|
8004
8453
|
cont("Mutually exclusive with --redact-all/--allow. Cannot combine with --dry-run."),
|
|
8005
8454
|
"",
|
|
8006
8455
|
row(" sync", "Pull (retain-merge), then push, under one lock. One-shot; hides ordering."),
|
|
8456
|
+
cont("Compact by default: prints only the Sync summary."),
|
|
8007
8457
|
row(" --dry-run", "Stack the pull preview then the push preview; mutates nothing."),
|
|
8008
8458
|
cont("Fails fast on a pull-half error; a wedged repo hints at nomad pull --force-remote"),
|
|
8009
8459
|
cont("(sync itself takes no --force-remote). Mid-push leak recovery behaves exactly"),
|
|
8010
8460
|
cont("like nomad push."),
|
|
8461
|
+
row(
|
|
8462
|
+
" --verbose, --all, -v",
|
|
8463
|
+
"Show the full merged tree; default output is the Sync summary only."
|
|
8464
|
+
),
|
|
8011
8465
|
"",
|
|
8012
8466
|
row(" diff", "Offline preview of what `pull` would change against local repo state."),
|
|
8013
8467
|
cont("No git pull, no lock acquired."),
|
|
@@ -8105,15 +8559,15 @@ var DEFAULT_HELP = [
|
|
|
8105
8559
|
init_config();
|
|
8106
8560
|
init_utils();
|
|
8107
8561
|
init_utils_json();
|
|
8108
|
-
import { existsSync as
|
|
8109
|
-
import { join as
|
|
8562
|
+
import { existsSync as existsSync51, readFileSync as readFileSync18, readdirSync as readdirSync19 } from "node:fs";
|
|
8563
|
+
import { join as join59 } from "node:path";
|
|
8110
8564
|
function resumeCmd(sessionId) {
|
|
8111
8565
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
8112
8566
|
fail(`invalid session id: ${sessionId}`);
|
|
8113
8567
|
process.exit(1);
|
|
8114
8568
|
}
|
|
8115
|
-
const projectsRoot =
|
|
8116
|
-
if (!
|
|
8569
|
+
const projectsRoot = join59(claudeHome(), "projects");
|
|
8570
|
+
if (!existsSync51(projectsRoot)) {
|
|
8117
8571
|
fail(`${projectsRoot} does not exist`);
|
|
8118
8572
|
process.exit(1);
|
|
8119
8573
|
}
|
|
@@ -8127,8 +8581,8 @@ function resumeCmd(sessionId) {
|
|
|
8127
8581
|
fail(`no cwd field found in ${jsonlPath}`);
|
|
8128
8582
|
process.exit(1);
|
|
8129
8583
|
}
|
|
8130
|
-
const mapPath =
|
|
8131
|
-
if (!
|
|
8584
|
+
const mapPath = join59(repoHome(), "path-map.json");
|
|
8585
|
+
if (!existsSync51(mapPath)) {
|
|
8132
8586
|
fail("path-map.json missing");
|
|
8133
8587
|
process.exit(1);
|
|
8134
8588
|
}
|
|
@@ -8150,14 +8604,14 @@ function resumeCmd(sessionId) {
|
|
|
8150
8604
|
console.log(`cd ${shQuote(hit.localPath)} && claude --resume ${shQuote(sessionId)}`);
|
|
8151
8605
|
}
|
|
8152
8606
|
function findTranscriptPath(projectsRoot, sessionId) {
|
|
8153
|
-
for (const dir of
|
|
8154
|
-
const candidate =
|
|
8155
|
-
if (
|
|
8607
|
+
for (const dir of readdirSync19(projectsRoot)) {
|
|
8608
|
+
const candidate = join59(projectsRoot, dir, `${sessionId}.jsonl`);
|
|
8609
|
+
if (existsSync51(candidate)) return candidate;
|
|
8156
8610
|
}
|
|
8157
8611
|
return null;
|
|
8158
8612
|
}
|
|
8159
8613
|
function extractRecordedCwd(jsonlPath) {
|
|
8160
|
-
for (const line of
|
|
8614
|
+
for (const line of readFileSync18(jsonlPath, "utf8").split("\n")) {
|
|
8161
8615
|
if (!line.trim()) continue;
|
|
8162
8616
|
try {
|
|
8163
8617
|
const obj = JSON.parse(line);
|
|
@@ -8233,10 +8687,10 @@ try {
|
|
|
8233
8687
|
case "sync": {
|
|
8234
8688
|
const syncArgs = parseSyncArgs(process.argv);
|
|
8235
8689
|
if (syncArgs === null) {
|
|
8236
|
-
console.error("usage: nomad sync [--dry-run]");
|
|
8690
|
+
console.error("usage: nomad sync [--dry-run] [--verbose|--all|-v]");
|
|
8237
8691
|
process.exit(1);
|
|
8238
8692
|
}
|
|
8239
|
-
await cmdSync({ dryRun: syncArgs.dryRun });
|
|
8693
|
+
await cmdSync({ dryRun: syncArgs.dryRun, verbose: syncArgs.verbose });
|
|
8240
8694
|
break;
|
|
8241
8695
|
}
|
|
8242
8696
|
case "init": {
|