claude-nomad 0.44.1 → 0.46.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/.gitleaks.toml +21 -0
- package/CHANGELOG.md +40 -0
- package/README.md +3 -1
- package/dist/nomad.mjs +1793 -1581
- package/package.json +1 -1
package/dist/nomad.mjs
CHANGED
|
@@ -1400,8 +1400,8 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
|
|
|
1400
1400
|
}
|
|
1401
1401
|
|
|
1402
1402
|
// src/commands.doctor.ts
|
|
1403
|
-
import { existsSync as
|
|
1404
|
-
import { join as
|
|
1403
|
+
import { existsSync as existsSync27 } from "node:fs";
|
|
1404
|
+
import { join as join33 } from "node:path";
|
|
1405
1405
|
|
|
1406
1406
|
// src/commands.doctor.checks.repo.ts
|
|
1407
1407
|
init_color();
|
|
@@ -2686,17 +2686,150 @@ function reportPreserveSymlinksCheck(section2) {
|
|
|
2686
2686
|
}
|
|
2687
2687
|
}
|
|
2688
2688
|
|
|
2689
|
+
// src/commands.doctor.checks.settings-drift.ts
|
|
2690
|
+
init_color();
|
|
2691
|
+
import { existsSync as existsSync21, readFileSync as readFileSync7 } from "node:fs";
|
|
2692
|
+
import { join as join25 } from "node:path";
|
|
2693
|
+
init_config();
|
|
2694
|
+
init_utils_json();
|
|
2695
|
+
function arraysEqual(a, b) {
|
|
2696
|
+
if (a.length !== b.length) return false;
|
|
2697
|
+
for (let i = 0; i < a.length; i++) {
|
|
2698
|
+
if (!deepEqual(a[i], b[i])) return false;
|
|
2699
|
+
}
|
|
2700
|
+
return true;
|
|
2701
|
+
}
|
|
2702
|
+
function objectsEqual(a, b) {
|
|
2703
|
+
const aKeys = Object.keys(a);
|
|
2704
|
+
const bKeys = Object.keys(b);
|
|
2705
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
2706
|
+
for (const k of aKeys) {
|
|
2707
|
+
if (!Object.hasOwn(b, k)) return false;
|
|
2708
|
+
if (!deepEqual(a[k], b[k])) return false;
|
|
2709
|
+
}
|
|
2710
|
+
return true;
|
|
2711
|
+
}
|
|
2712
|
+
function deepEqual(a, b) {
|
|
2713
|
+
if (a === b) return true;
|
|
2714
|
+
if (a === null || b === null) return false;
|
|
2715
|
+
if (Array.isArray(a) && Array.isArray(b)) return arraysEqual(a, b);
|
|
2716
|
+
if (Array.isArray(a) || Array.isArray(b)) return false;
|
|
2717
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
2718
|
+
return objectsEqual(a, b);
|
|
2719
|
+
}
|
|
2720
|
+
return false;
|
|
2721
|
+
}
|
|
2722
|
+
function diffMergedSettings(merged, settings) {
|
|
2723
|
+
const missing = [];
|
|
2724
|
+
const changed = [];
|
|
2725
|
+
const extra = [];
|
|
2726
|
+
const settingsKeys = new Set(Object.keys(settings));
|
|
2727
|
+
for (const key of Object.keys(merged)) {
|
|
2728
|
+
if (!settingsKeys.has(key)) {
|
|
2729
|
+
missing.push(key);
|
|
2730
|
+
} else if (!deepEqual(merged[key], settings[key])) {
|
|
2731
|
+
changed.push(key);
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
const mergedKeys = new Set(Object.keys(merged));
|
|
2735
|
+
for (const key of Object.keys(settings)) {
|
|
2736
|
+
if (!mergedKeys.has(key)) extra.push(key);
|
|
2737
|
+
}
|
|
2738
|
+
const collator = (a, b) => a.localeCompare(b, "en");
|
|
2739
|
+
return {
|
|
2740
|
+
missing: missing.toSorted(collator),
|
|
2741
|
+
changed: changed.toSorted(collator),
|
|
2742
|
+
extra: extra.toSorted(collator)
|
|
2743
|
+
};
|
|
2744
|
+
}
|
|
2745
|
+
function tryReadJson(filePath) {
|
|
2746
|
+
try {
|
|
2747
|
+
const raw = readFileSync7(filePath, "utf8");
|
|
2748
|
+
const parsed = JSON.parse(raw);
|
|
2749
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
2750
|
+
return parsed;
|
|
2751
|
+
} catch {
|
|
2752
|
+
return null;
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
function reportSettingsDriftCheck(section2) {
|
|
2756
|
+
const claude = claudeHome();
|
|
2757
|
+
const repo = repoHome();
|
|
2758
|
+
const host = HOST;
|
|
2759
|
+
const settingsPath = join25(claude, "settings.json");
|
|
2760
|
+
const basePath = join25(repo, "shared", "settings.base.json");
|
|
2761
|
+
const hostPath = join25(repo, "hosts", `${host}.json`);
|
|
2762
|
+
if (!existsSync21(settingsPath)) {
|
|
2763
|
+
addItem(section2, `${dim(infoGlyph)} no ~/.claude/settings.json; skipping merge-drift check`);
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
if (!existsSync21(basePath)) {
|
|
2767
|
+
addItem(
|
|
2768
|
+
section2,
|
|
2769
|
+
`${dim(infoGlyph)} shared/settings.base.json missing; skipping merge-drift check`
|
|
2770
|
+
);
|
|
2771
|
+
return;
|
|
2772
|
+
}
|
|
2773
|
+
const base = tryReadJson(basePath);
|
|
2774
|
+
if (base === null) {
|
|
2775
|
+
addItem(
|
|
2776
|
+
section2,
|
|
2777
|
+
`${dim(infoGlyph)} shared/settings.base.json unparseable; skipping merge-drift check`
|
|
2778
|
+
);
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
const settings = tryReadJson(settingsPath);
|
|
2782
|
+
if (settings === null) {
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
const hostExists = existsSync21(hostPath);
|
|
2786
|
+
const hostObj = hostExists ? tryReadJson(hostPath) : null;
|
|
2787
|
+
if (hostExists && hostObj === null) {
|
|
2788
|
+
addItem(
|
|
2789
|
+
section2,
|
|
2790
|
+
`${yellow(warnGlyph)} hosts/${host}.json unparseable; 'nomad pull' will fail (fix the host file)`
|
|
2791
|
+
);
|
|
2792
|
+
return;
|
|
2793
|
+
}
|
|
2794
|
+
const merged = deepMerge(base, hostObj ?? {});
|
|
2795
|
+
const { missing, changed, extra } = diffMergedSettings(merged, settings);
|
|
2796
|
+
emitDriftRows(section2, missing, changed, extra, host, hostExists);
|
|
2797
|
+
}
|
|
2798
|
+
function emitDriftRows(section2, missing, changed, extra, host, hostFileExists) {
|
|
2799
|
+
if (missing.length > 0) {
|
|
2800
|
+
addItem(
|
|
2801
|
+
section2,
|
|
2802
|
+
`${yellow(warnGlyph)} settings.json drift: merged keys missing locally: ${missing.join(", ")} (external writer clobbered settings.json; run 'nomad pull')`
|
|
2803
|
+
);
|
|
2804
|
+
}
|
|
2805
|
+
if (changed.length > 0) {
|
|
2806
|
+
addItem(
|
|
2807
|
+
section2,
|
|
2808
|
+
`${yellow(warnGlyph)} settings.json drift: merged keys with changed values: ${changed.join(", ")} (run 'nomad pull')`
|
|
2809
|
+
);
|
|
2810
|
+
}
|
|
2811
|
+
if (extra.length > 0 && hostFileExists) {
|
|
2812
|
+
addItem(
|
|
2813
|
+
section2,
|
|
2814
|
+
`${dim(infoGlyph)} settings.json has ${extra.length} local-only key(s) not in base+host merge: ${extra.join(", ")} (promotion candidates for shared/settings.base.json or hosts/${host}.json)`
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
if (missing.length === 0 && changed.length === 0 && extra.length === 0) {
|
|
2818
|
+
addItem(section2, `${green(okGlyph)} settings.json matches base+host merge`);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2689
2822
|
// src/commands.doctor.ts
|
|
2690
2823
|
init_config();
|
|
2691
2824
|
|
|
2692
2825
|
// src/commands.doctor.engine.ts
|
|
2693
2826
|
init_color();
|
|
2694
|
-
import { readFileSync as
|
|
2827
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
2695
2828
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
2696
2829
|
|
|
2697
2830
|
// src/commands.doctor.version.ts
|
|
2698
2831
|
init_color();
|
|
2699
|
-
import { readFileSync as
|
|
2832
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
2700
2833
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
2701
2834
|
init_config();
|
|
2702
2835
|
var STRICT_SEMVER = /^\d+\.\d+\.\d+$/;
|
|
@@ -2713,7 +2846,7 @@ function compareSemver(a, b) {
|
|
|
2713
2846
|
function readLocalVersion() {
|
|
2714
2847
|
try {
|
|
2715
2848
|
const pkgPath = fileURLToPath2(new URL("../package.json", import.meta.url));
|
|
2716
|
-
const parsed = JSON.parse(
|
|
2849
|
+
const parsed = JSON.parse(readFileSync8(pkgPath, "utf8"));
|
|
2717
2850
|
if (typeof parsed.version === "string" && parsed.version.length > 0) {
|
|
2718
2851
|
return parsed.version;
|
|
2719
2852
|
}
|
|
@@ -2772,7 +2905,7 @@ function parseMinVersion(spec) {
|
|
|
2772
2905
|
function readEnginesNode() {
|
|
2773
2906
|
try {
|
|
2774
2907
|
const pkgPath = fileURLToPath3(new URL("../package.json", import.meta.url));
|
|
2775
|
-
const parsed = JSON.parse(
|
|
2908
|
+
const parsed = JSON.parse(readFileSync9(pkgPath, "utf8"));
|
|
2776
2909
|
const node = parsed.engines?.node;
|
|
2777
2910
|
if (typeof node === "string" && node.length > 0) return node;
|
|
2778
2911
|
return null;
|
|
@@ -2798,398 +2931,123 @@ function reportNodeEngineCheck(section2) {
|
|
|
2798
2931
|
addItem(section2, `${green(okGlyph)} node: ${process.version} (satisfies >=${min})`);
|
|
2799
2932
|
}
|
|
2800
2933
|
|
|
2801
|
-
// src/
|
|
2934
|
+
// src/spinner.ts
|
|
2802
2935
|
init_color();
|
|
2803
|
-
import {
|
|
2804
|
-
import {
|
|
2805
|
-
import {
|
|
2936
|
+
import { existsSync as existsSync25 } from "node:fs";
|
|
2937
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2938
|
+
import { Worker } from "node:worker_threads";
|
|
2939
|
+
|
|
2940
|
+
// src/commands.push.recovery.ts
|
|
2806
2941
|
init_config();
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
const m = SEMVER_MAJOR_MINOR.exec(value);
|
|
2811
|
-
return m === null ? null : [m[1], m[2]];
|
|
2812
|
-
}
|
|
2813
|
-
function readGitleaksVersion(run, tomlExists) {
|
|
2814
|
-
const tomlPath = join25(repoHome(), ".gitleaks.toml");
|
|
2815
|
-
const args = ["version"];
|
|
2816
|
-
if (tomlExists(tomlPath)) args.push("--config", tomlPath);
|
|
2817
|
-
try {
|
|
2818
|
-
return run("gitleaks", args, {
|
|
2819
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2820
|
-
timeout: GITLEAKS_TIMEOUT_MS
|
|
2821
|
-
}).toString().trim();
|
|
2822
|
-
} catch {
|
|
2823
|
-
return null;
|
|
2824
|
-
}
|
|
2825
|
-
}
|
|
2826
|
-
function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync21) {
|
|
2827
|
-
const raw = readGitleaksVersion(run, tomlExists);
|
|
2828
|
-
if (raw === null) return;
|
|
2829
|
-
const local = majorMinorOf(raw);
|
|
2830
|
-
if (local === null) return;
|
|
2831
|
-
const pin = majorMinorOf(GITLEAKS_PINNED_VERSION);
|
|
2832
|
-
if (pin === null) return;
|
|
2833
|
-
const sameMajorMinor = local[0] === pin[0] && local[1] === pin[1];
|
|
2834
|
-
if (sameMajorMinor) {
|
|
2835
|
-
addItem(section2, `${green(okGlyph)} gitleaks: ${raw} (matches pinned ${pin[0]}.${pin[1]})`);
|
|
2836
|
-
return;
|
|
2837
|
-
}
|
|
2838
|
-
addItem(
|
|
2839
|
-
section2,
|
|
2840
|
-
`${yellow(warnGlyph)} gitleaks: ${raw} -> ${GITLEAKS_PINNED_VERSION} (CI pins this; local drift may change scan results)`
|
|
2841
|
-
);
|
|
2842
|
-
}
|
|
2942
|
+
import { readFileSync as readFileSync13, rmSync as rmSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2943
|
+
import { join as join31 } from "node:path";
|
|
2944
|
+
import { createInterface } from "node:readline/promises";
|
|
2843
2945
|
|
|
2844
|
-
// src/commands.
|
|
2845
|
-
|
|
2846
|
-
import {
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
}
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2946
|
+
// src/commands.push.recovery.actions.ts
|
|
2947
|
+
init_config();
|
|
2948
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
2949
|
+
import { isAbsolute, resolve as resolve3, sep as sep4 } from "node:path";
|
|
2950
|
+
|
|
2951
|
+
// src/commands.push.recovery.redact.ts
|
|
2952
|
+
init_config();
|
|
2953
|
+
init_config_sharedDirs_guard();
|
|
2954
|
+
import { cpSync as cpSync5, existsSync as existsSync24, mkdirSync as mkdirSync6, statSync as statSync7 } from "node:fs";
|
|
2955
|
+
import { dirname as dirname6, join as join29, sep as sep3 } from "node:path";
|
|
2956
|
+
|
|
2957
|
+
// src/commands.redact.ts
|
|
2958
|
+
init_config();
|
|
2959
|
+
import { existsSync as existsSync23, statSync as statSync6 } from "node:fs";
|
|
2960
|
+
import { dirname as dirname5, join as join28 } from "node:path";
|
|
2961
|
+
|
|
2962
|
+
// src/commands.redact.subtree.ts
|
|
2963
|
+
import { existsSync as existsSync22, lstatSync as lstatSync7, readFileSync as readFileSync10, readdirSync as readdirSync9, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2964
|
+
import { join as join26 } from "node:path";
|
|
2965
|
+
init_utils_fs();
|
|
2966
|
+
function collectFiles(dir, out) {
|
|
2967
|
+
if (!existsSync22(dir)) return;
|
|
2968
|
+
const st = lstatSync7(dir);
|
|
2969
|
+
if (!st.isDirectory()) return;
|
|
2970
|
+
for (const entry of readdirSync9(dir)) {
|
|
2971
|
+
const abs = join26(dir, entry);
|
|
2972
|
+
const lst = lstatSync7(abs);
|
|
2973
|
+
if (lst.isSymbolicLink()) continue;
|
|
2974
|
+
if (lst.isDirectory()) {
|
|
2975
|
+
collectFiles(abs, out);
|
|
2976
|
+
continue;
|
|
2865
2977
|
}
|
|
2866
|
-
|
|
2978
|
+
if (lst.isFile()) out.push(abs);
|
|
2867
2979
|
}
|
|
2868
2980
|
}
|
|
2869
|
-
function
|
|
2870
|
-
const
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
`${yellow(warnGlyph)} ${FETCHER_BASE} (curl or wget): not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
|
|
2880
|
-
);
|
|
2981
|
+
function listSubtreeFiles(sessionDir) {
|
|
2982
|
+
const out = [];
|
|
2983
|
+
collectFiles(sessionDir, out);
|
|
2984
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
2985
|
+
}
|
|
2986
|
+
function newestSubtreeMtimeMs(mainPath, subtreeFiles, statMtime = (p) => statSync5(p).mtimeMs) {
|
|
2987
|
+
let newest = statMtime(mainPath);
|
|
2988
|
+
for (const filePath of subtreeFiles) {
|
|
2989
|
+
const t = statMtime(filePath);
|
|
2990
|
+
if (t > newest) newest = t;
|
|
2881
2991
|
}
|
|
2992
|
+
return newest;
|
|
2882
2993
|
}
|
|
2883
|
-
function
|
|
2884
|
-
const
|
|
2885
|
-
if (
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
);
|
|
2994
|
+
function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts, scan, dryRun) {
|
|
2995
|
+
const dirty = [];
|
|
2996
|
+
if (mainFindings.length > 0) dirty.push({ path: mainPath, findings: mainFindings });
|
|
2997
|
+
for (const filePath of subtreeFiles) {
|
|
2998
|
+
const raw = scan(filePath);
|
|
2999
|
+
if (raw === null || raw.length === 0) continue;
|
|
3000
|
+
const filtered = rule === void 0 ? raw : raw.filter((f) => f.RuleID === rule);
|
|
3001
|
+
if (filtered.length === 0) continue;
|
|
3002
|
+
dirty.push({ path: filePath, findings: filtered });
|
|
2892
3003
|
}
|
|
2893
|
-
|
|
3004
|
+
const total = dirty.reduce((n, e) => n + e.findings.length, 0);
|
|
3005
|
+
if (!dryRun && total > 0) {
|
|
3006
|
+
for (const { path: filePath, findings } of dirty) {
|
|
3007
|
+
backupBeforeWrite(filePath, ts);
|
|
3008
|
+
writeFileSync3(filePath, applyRedactions(readFileSync10(filePath, "utf8"), findings), "utf8");
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
return { total, dirty };
|
|
2894
3012
|
}
|
|
2895
3013
|
|
|
2896
|
-
// src/commands.
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
3014
|
+
// src/commands.redact.ts
|
|
3015
|
+
init_push_gitleaks_scan();
|
|
3016
|
+
init_utils_fs();
|
|
3017
|
+
init_utils_json();
|
|
3018
|
+
init_utils();
|
|
2900
3019
|
|
|
2901
|
-
// src/
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
return { owner: m[1], repo: m[2] };
|
|
3020
|
+
// src/utils.lockfile.ts
|
|
3021
|
+
init_config();
|
|
3022
|
+
init_utils();
|
|
3023
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync11, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3024
|
+
import { dirname as dirname4, join as join27 } from "node:path";
|
|
3025
|
+
function lockFilePath() {
|
|
3026
|
+
return join27(home(), ".cache", "claude-nomad", "nomad.lock");
|
|
2909
3027
|
}
|
|
2910
|
-
function
|
|
3028
|
+
function acquireLock(verb) {
|
|
3029
|
+
const lp = lockFilePath();
|
|
3030
|
+
mkdirSync5(dirname4(lp), { recursive: true });
|
|
2911
3031
|
try {
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
})
|
|
2916
|
-
|
|
3032
|
+
const fd = openSync3(lp, "wx");
|
|
3033
|
+
try {
|
|
3034
|
+
writeFileSync4(fd, String(process.pid));
|
|
3035
|
+
} catch (writeErr) {
|
|
3036
|
+
try {
|
|
3037
|
+
closeSync3(fd);
|
|
3038
|
+
} catch {
|
|
3039
|
+
}
|
|
3040
|
+
try {
|
|
3041
|
+
unlinkSync(lp);
|
|
3042
|
+
} catch {
|
|
3043
|
+
}
|
|
3044
|
+
throw writeErr;
|
|
3045
|
+
}
|
|
3046
|
+
return { fd, path: lp };
|
|
2917
3047
|
} catch (err) {
|
|
2918
|
-
const
|
|
2919
|
-
if (
|
|
2920
|
-
|
|
2921
|
-
return "gh-probe-error";
|
|
2922
|
-
}
|
|
2923
|
-
}
|
|
2924
|
-
function isRepoPrivate(ref, run = execFileSync9) {
|
|
2925
|
-
const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
|
|
2926
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
2927
|
-
timeout: GH_TIMEOUT_MS
|
|
2928
|
-
}).toString();
|
|
2929
|
-
const parsed = JSON.parse(out);
|
|
2930
|
-
return parsed.isPrivate === true;
|
|
2931
|
-
}
|
|
2932
|
-
function isActionsEnabled(ref, run = execFileSync9) {
|
|
2933
|
-
const out = run(
|
|
2934
|
-
"gh",
|
|
2935
|
-
["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
|
|
2936
|
-
{ stdio: ["ignore", "pipe", "ignore"], timeout: GH_TIMEOUT_MS }
|
|
2937
|
-
).toString().trim();
|
|
2938
|
-
return out === "true";
|
|
2939
|
-
}
|
|
2940
|
-
function disableActions(ref, run = execFileSync9) {
|
|
2941
|
-
run(
|
|
2942
|
-
"gh",
|
|
2943
|
-
[
|
|
2944
|
-
"api",
|
|
2945
|
-
"-X",
|
|
2946
|
-
"PUT",
|
|
2947
|
-
`repos/${ref.owner}/${ref.repo}/actions/permissions`,
|
|
2948
|
-
"-F",
|
|
2949
|
-
"enabled=false"
|
|
2950
|
-
],
|
|
2951
|
-
{ stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
|
|
2952
|
-
);
|
|
2953
|
-
}
|
|
2954
|
-
function readOriginRemote(cwd, run = execFileSync9) {
|
|
2955
|
-
return run("git", ["remote", "get-url", "origin"], {
|
|
2956
|
-
cwd,
|
|
2957
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
2958
|
-
}).toString().trim();
|
|
2959
|
-
}
|
|
2960
|
-
|
|
2961
|
-
// src/commands.doctor.actions-drift.ts
|
|
2962
|
-
function reportActionsDrift(section2, run = execFileSync10) {
|
|
2963
|
-
let remote;
|
|
2964
|
-
try {
|
|
2965
|
-
remote = readOriginRemote(repoHome(), run);
|
|
2966
|
-
} catch {
|
|
2967
|
-
return;
|
|
2968
|
-
}
|
|
2969
|
-
const ref = parseGitHubRemote(remote);
|
|
2970
|
-
if (ref === null) return;
|
|
2971
|
-
const auth = ghAuthStatus(run);
|
|
2972
|
-
if (auth === "gh-not-installed" || auth === "gh-not-authed") return;
|
|
2973
|
-
let isPrivate;
|
|
2974
|
-
try {
|
|
2975
|
-
isPrivate = isRepoPrivate(ref, run);
|
|
2976
|
-
} catch {
|
|
2977
|
-
return;
|
|
2978
|
-
}
|
|
2979
|
-
if (!isPrivate) return;
|
|
2980
|
-
let enabled2;
|
|
2981
|
-
try {
|
|
2982
|
-
enabled2 = isActionsEnabled(ref, run);
|
|
2983
|
-
} catch {
|
|
2984
|
-
return;
|
|
2985
|
-
}
|
|
2986
|
-
if (!enabled2) return;
|
|
2987
|
-
addItem(
|
|
2988
|
-
section2,
|
|
2989
|
-
`${yellow(warnGlyph)} Actions: enabled on private repo ${ref.owner}/${ref.repo} (re-disable with 'gh api -X PUT repos/${ref.owner}/${ref.repo}/actions/permissions -F enabled=false')`
|
|
2990
|
-
);
|
|
2991
|
-
}
|
|
2992
|
-
|
|
2993
|
-
// src/commands.doctor.verdict.ts
|
|
2994
|
-
init_color();
|
|
2995
|
-
function isFailLine(item2) {
|
|
2996
|
-
return item2.includes(failGlyph);
|
|
2997
|
-
}
|
|
2998
|
-
function isWarnLine(item2) {
|
|
2999
|
-
return !isFailLine(item2) && item2.includes(warnGlyph);
|
|
3000
|
-
}
|
|
3001
|
-
function buildVerdictSection(sections) {
|
|
3002
|
-
const summary = section("Summary");
|
|
3003
|
-
const lines = sections.flatMap((s) => s.items).map((item2) => item2.replace(/^\t/, ""));
|
|
3004
|
-
const failures = lines.filter(isFailLine);
|
|
3005
|
-
const warnings = lines.filter(isWarnLine);
|
|
3006
|
-
for (const line of [...failures, ...warnings]) addItem(summary, line);
|
|
3007
|
-
if (failures.length > 0) {
|
|
3008
|
-
addItem(
|
|
3009
|
-
summary,
|
|
3010
|
-
`${red(failGlyph)} ${failures.length} failure(s), ${warnings.length} warning(s)`
|
|
3011
|
-
);
|
|
3012
|
-
} else if (warnings.length > 0) {
|
|
3013
|
-
addItem(summary, `${yellow(warnGlyph)} ${warnings.length} warning(s)`);
|
|
3014
|
-
} else {
|
|
3015
|
-
addItem(summary, `${green(okGlyph)} healthy`);
|
|
3016
|
-
}
|
|
3017
|
-
return summary;
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
// src/commands.doctor.ts
|
|
3021
|
-
function cmdDoctor(opts = {}) {
|
|
3022
|
-
const host = section("Environment");
|
|
3023
|
-
reportHostAndPaths(host);
|
|
3024
|
-
reportRepoState(host);
|
|
3025
|
-
const links = section("Shared links");
|
|
3026
|
-
const mapPath = join26(repoHome(), "path-map.json");
|
|
3027
|
-
const rawMap = existsSync22(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
|
|
3028
|
-
const map = rawMap ?? { projects: {} };
|
|
3029
|
-
reportSharedLinks(links, map);
|
|
3030
|
-
const hooksScan = section("Hook targets");
|
|
3031
|
-
reportHooksTargetCheck(hooksScan);
|
|
3032
|
-
reportHookScopeCheck(hooksScan);
|
|
3033
|
-
reportPreserveSymlinksCheck(hooksScan);
|
|
3034
|
-
const settings = section("Settings");
|
|
3035
|
-
const base = loadBaseSettings(settings);
|
|
3036
|
-
const parsedSettings = loadAndReportSettings(settings);
|
|
3037
|
-
reportHostOverrides(settings, base, parsedSettings);
|
|
3038
|
-
const pathMap = section("Path map");
|
|
3039
|
-
reportPathMap(pathMap);
|
|
3040
|
-
const neverSync = section("Never-sync");
|
|
3041
|
-
reportNeverSync(neverSync);
|
|
3042
|
-
const repository = section("Repository");
|
|
3043
|
-
const gitleaksReady = reportGitleaksProbe(repository);
|
|
3044
|
-
reportGitlinks(repository);
|
|
3045
|
-
reportRemote(repository);
|
|
3046
|
-
reportRebaseClean(repository);
|
|
3047
|
-
reportRebaseState(repository);
|
|
3048
|
-
reportActionsDrift(repository);
|
|
3049
|
-
const nomadVersion = section("Nomad Version");
|
|
3050
|
-
reportVersionCheck(nomadVersion);
|
|
3051
|
-
const housekeeping = section("Housekeeping");
|
|
3052
|
-
reportBackupsCheck(housekeeping);
|
|
3053
|
-
const depVersions = section("Dependency Versions");
|
|
3054
|
-
reportNodeEngineCheck(depVersions);
|
|
3055
|
-
reportGitleaksVersionCheck(depVersions);
|
|
3056
|
-
reportOptionalDeps(depVersions);
|
|
3057
|
-
const sharedScan = section("Shared scan");
|
|
3058
|
-
if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
|
|
3059
|
-
const schemaScan = section("Schema scan");
|
|
3060
|
-
if (opts.checkSchema === true) reportCheckSchema(schemaScan);
|
|
3061
|
-
const body = [
|
|
3062
|
-
nomadVersion,
|
|
3063
|
-
depVersions,
|
|
3064
|
-
host,
|
|
3065
|
-
links,
|
|
3066
|
-
hooksScan,
|
|
3067
|
-
settings,
|
|
3068
|
-
pathMap,
|
|
3069
|
-
neverSync,
|
|
3070
|
-
repository,
|
|
3071
|
-
housekeeping,
|
|
3072
|
-
sharedScan,
|
|
3073
|
-
schemaScan
|
|
3074
|
-
];
|
|
3075
|
-
renderDoctor([...body, buildVerdictSection(body)]);
|
|
3076
|
-
}
|
|
3077
|
-
|
|
3078
|
-
// src/commands.drop-session.ts
|
|
3079
|
-
init_config();
|
|
3080
|
-
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
3081
|
-
import { existsSync as existsSync24, readdirSync as readdirSync9, statSync as statSync5 } from "node:fs";
|
|
3082
|
-
import { join as join29, relative as relative4 } from "node:path";
|
|
3083
|
-
|
|
3084
|
-
// src/commands.drop-session.git.ts
|
|
3085
|
-
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
3086
|
-
function expandStagedDir(dirRel, repo) {
|
|
3087
|
-
try {
|
|
3088
|
-
const out = execFileSync11("git", ["ls-files", "-z", "--", dirRel], {
|
|
3089
|
-
cwd: repo,
|
|
3090
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3091
|
-
});
|
|
3092
|
-
return out.toString().split("\0").filter((p) => p !== "");
|
|
3093
|
-
} catch {
|
|
3094
|
-
return [];
|
|
3095
|
-
}
|
|
3096
|
-
}
|
|
3097
|
-
function isTrackedInHead(rel, repo) {
|
|
3098
|
-
try {
|
|
3099
|
-
execFileSync11("git", ["cat-file", "-e", `HEAD:${rel}`], {
|
|
3100
|
-
cwd: repo,
|
|
3101
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3102
|
-
});
|
|
3103
|
-
return true;
|
|
3104
|
-
} catch {
|
|
3105
|
-
return false;
|
|
3106
|
-
}
|
|
3107
|
-
}
|
|
3108
|
-
function isInIndex(rel, repo) {
|
|
3109
|
-
try {
|
|
3110
|
-
const out = execFileSync11("git", ["ls-files", "--", rel], {
|
|
3111
|
-
cwd: repo,
|
|
3112
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3113
|
-
});
|
|
3114
|
-
return out.toString().trim() !== "";
|
|
3115
|
-
} catch {
|
|
3116
|
-
return false;
|
|
3117
|
-
}
|
|
3118
|
-
}
|
|
3119
|
-
|
|
3120
|
-
// src/commands.drop-session.scrub-hint.ts
|
|
3121
|
-
init_config();
|
|
3122
|
-
init_utils();
|
|
3123
|
-
init_utils_json();
|
|
3124
|
-
import { existsSync as existsSync23 } from "node:fs";
|
|
3125
|
-
import { join as join27 } from "node:path";
|
|
3126
|
-
var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
|
|
3127
|
-
function reportScrubHint(id, matches) {
|
|
3128
|
-
const live = resolveLiveTranscript(id, matches);
|
|
3129
|
-
const target = live ?? `~/.claude/projects/<encoded>/${id}.jsonl`;
|
|
3130
|
-
log(
|
|
3131
|
-
`note: this only un-stages the session from the next push.
|
|
3132
|
-
The local source still contains the secret, so nomad push re-stages it
|
|
3133
|
-
on the next run and nomad doctor --check-shared keeps reporting it.
|
|
3134
|
-
To fully remediate: rotate the credential, then run:
|
|
3135
|
-
nomad redact ${id}
|
|
3136
|
-
(or scrub ${target} manually)`
|
|
3137
|
-
);
|
|
3138
|
-
}
|
|
3139
|
-
function resolveLiveTranscript(id, matches) {
|
|
3140
|
-
try {
|
|
3141
|
-
const mapPath = join27(repoHome(), "path-map.json");
|
|
3142
|
-
if (!existsSync23(mapPath)) return null;
|
|
3143
|
-
const projects = readJson(mapPath).projects;
|
|
3144
|
-
const claude = claudeHome();
|
|
3145
|
-
for (const rel of matches) {
|
|
3146
|
-
const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
|
|
3147
|
-
if (logical === void 0) continue;
|
|
3148
|
-
const abs = projects[logical]?.[HOST];
|
|
3149
|
-
if (abs === void 0) continue;
|
|
3150
|
-
const live = join27(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
3151
|
-
if (existsSync23(live)) return live;
|
|
3152
|
-
}
|
|
3153
|
-
return null;
|
|
3154
|
-
} catch {
|
|
3155
|
-
return null;
|
|
3156
|
-
}
|
|
3157
|
-
}
|
|
3158
|
-
|
|
3159
|
-
// src/commands.drop-session.ts
|
|
3160
|
-
init_utils();
|
|
3161
|
-
|
|
3162
|
-
// src/utils.lockfile.ts
|
|
3163
|
-
init_config();
|
|
3164
|
-
init_utils();
|
|
3165
|
-
import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync9, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3166
|
-
import { dirname as dirname4, join as join28 } from "node:path";
|
|
3167
|
-
function lockFilePath() {
|
|
3168
|
-
return join28(home(), ".cache", "claude-nomad", "nomad.lock");
|
|
3169
|
-
}
|
|
3170
|
-
function acquireLock(verb) {
|
|
3171
|
-
const lp = lockFilePath();
|
|
3172
|
-
mkdirSync5(dirname4(lp), { recursive: true });
|
|
3173
|
-
try {
|
|
3174
|
-
const fd = openSync3(lp, "wx");
|
|
3175
|
-
try {
|
|
3176
|
-
writeFileSync3(fd, String(process.pid));
|
|
3177
|
-
} catch (writeErr) {
|
|
3178
|
-
try {
|
|
3179
|
-
closeSync3(fd);
|
|
3180
|
-
} catch {
|
|
3181
|
-
}
|
|
3182
|
-
try {
|
|
3183
|
-
unlinkSync(lp);
|
|
3184
|
-
} catch {
|
|
3185
|
-
}
|
|
3186
|
-
throw writeErr;
|
|
3187
|
-
}
|
|
3188
|
-
return { fd, path: lp };
|
|
3189
|
-
} catch (err) {
|
|
3190
|
-
const code = err.code;
|
|
3191
|
-
if (code !== "EEXIST") throw err;
|
|
3192
|
-
return checkStaleAndRetry(verb, lp);
|
|
3048
|
+
const code = err.code;
|
|
3049
|
+
if (code !== "EEXIST") throw err;
|
|
3050
|
+
return checkStaleAndRetry(verb, lp);
|
|
3193
3051
|
}
|
|
3194
3052
|
}
|
|
3195
3053
|
function releaseLock(handle) {
|
|
@@ -3208,7 +3066,7 @@ function releaseLock(handle) {
|
|
|
3208
3066
|
function unlinkIfSamePid(expectedPidStr, lp) {
|
|
3209
3067
|
let current;
|
|
3210
3068
|
try {
|
|
3211
|
-
current =
|
|
3069
|
+
current = readFileSync11(lp, "utf8").trim();
|
|
3212
3070
|
} catch {
|
|
3213
3071
|
return false;
|
|
3214
3072
|
}
|
|
@@ -3223,7 +3081,7 @@ function unlinkIfSamePid(expectedPidStr, lp) {
|
|
|
3223
3081
|
function checkStaleAndRetry(verb, lp) {
|
|
3224
3082
|
let pidStr;
|
|
3225
3083
|
try {
|
|
3226
|
-
pidStr =
|
|
3084
|
+
pidStr = readFileSync11(lp, "utf8").trim();
|
|
3227
3085
|
} catch {
|
|
3228
3086
|
pidStr = "";
|
|
3229
3087
|
}
|
|
@@ -3252,7 +3110,7 @@ function retryOnce(verb, lp) {
|
|
|
3252
3110
|
try {
|
|
3253
3111
|
const fd = openSync3(lp, "wx");
|
|
3254
3112
|
try {
|
|
3255
|
-
|
|
3113
|
+
writeFileSync4(fd, String(process.pid));
|
|
3256
3114
|
} catch {
|
|
3257
3115
|
try {
|
|
3258
3116
|
closeSync3(fd);
|
|
@@ -3272,152 +3130,18 @@ function retryOnce(verb, lp) {
|
|
|
3272
3130
|
}
|
|
3273
3131
|
}
|
|
3274
3132
|
|
|
3275
|
-
// src/commands.drop-session.ts
|
|
3276
|
-
function cmdDropSession(id) {
|
|
3277
|
-
if (id.length === 0 || id.length > 128 || !/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
3278
|
-
fail(`invalid session id: ${id}`);
|
|
3279
|
-
process.exit(1);
|
|
3280
|
-
}
|
|
3281
|
-
const repo = repoHome();
|
|
3282
|
-
if (!existsSync24(repo)) die(`repo not cloned at ${repo}`);
|
|
3283
|
-
const handle = acquireLock("drop-session");
|
|
3284
|
-
if (handle === null) process.exit(0);
|
|
3285
|
-
try {
|
|
3286
|
-
const repoProjects = join29(repo, "shared", "projects");
|
|
3287
|
-
if (!existsSync24(repoProjects)) {
|
|
3288
|
-
throw new NomadFatal(`no staged session matches ${id}`);
|
|
3289
|
-
}
|
|
3290
|
-
const matches = collectMatches(repoProjects, id, repo);
|
|
3291
|
-
if (matches.length === 0) {
|
|
3292
|
-
throw new NomadFatal(`no staged session matches ${id}`);
|
|
3293
|
-
}
|
|
3294
|
-
for (const rel of matches) unstageOne(rel, repo);
|
|
3295
|
-
reportScrubHint(id, matches);
|
|
3296
|
-
} catch (err) {
|
|
3297
|
-
if (!(err instanceof NomadFatal)) {
|
|
3298
|
-
throw err;
|
|
3299
|
-
}
|
|
3300
|
-
fail(err.message);
|
|
3301
|
-
process.exitCode = 1;
|
|
3302
|
-
} finally {
|
|
3303
|
-
releaseLock(handle);
|
|
3304
|
-
}
|
|
3305
|
-
}
|
|
3306
|
-
function collectMatches(repoProjects, id, repo) {
|
|
3307
|
-
const matches = [];
|
|
3308
|
-
for (const logical of readdirSync9(repoProjects)) {
|
|
3309
|
-
const candidate = join29(repoProjects, logical, `${id}.jsonl`);
|
|
3310
|
-
if (existsSync24(candidate)) {
|
|
3311
|
-
matches.push(relative4(repo, candidate));
|
|
3312
|
-
}
|
|
3313
|
-
const dir = join29(repoProjects, logical, id);
|
|
3314
|
-
if (existsSync24(dir) && statSync5(dir).isDirectory()) {
|
|
3315
|
-
const dirRel = relative4(repo, dir);
|
|
3316
|
-
const staged = expandStagedDir(dirRel, repo);
|
|
3317
|
-
if (staged.length > 0) matches.push(...staged);
|
|
3318
|
-
else matches.push(dirRel);
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
return matches;
|
|
3322
|
-
}
|
|
3323
|
-
function unstageOne(rel, repo) {
|
|
3324
|
-
if (!isInIndex(rel, repo)) {
|
|
3325
|
-
item(`dropped ${rel} (already absent from index)`);
|
|
3326
|
-
return;
|
|
3327
|
-
}
|
|
3328
|
-
try {
|
|
3329
|
-
if (isTrackedInHead(rel, repo)) {
|
|
3330
|
-
execFileSync12("git", ["restore", "--staged", "--worktree", "--", rel], {
|
|
3331
|
-
cwd: repo,
|
|
3332
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3333
|
-
});
|
|
3334
|
-
} else {
|
|
3335
|
-
execFileSync12("git", ["rm", "--cached", "-f", "--", rel], {
|
|
3336
|
-
cwd: repo,
|
|
3337
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3338
|
-
});
|
|
3339
|
-
}
|
|
3340
|
-
} catch (err) {
|
|
3341
|
-
const e = err;
|
|
3342
|
-
const detail = e.stderr?.toString().trim() ?? e.message;
|
|
3343
|
-
throw new NomadFatal(`git failed to unstage ${rel}: ${detail}`);
|
|
3344
|
-
}
|
|
3345
|
-
item(`dropped ${rel}`);
|
|
3346
|
-
}
|
|
3347
|
-
|
|
3348
|
-
// src/commands.redact.ts
|
|
3349
|
-
init_config();
|
|
3350
|
-
import { existsSync as existsSync26, statSync as statSync7 } from "node:fs";
|
|
3351
|
-
import { dirname as dirname5, join as join31 } from "node:path";
|
|
3352
|
-
|
|
3353
|
-
// src/commands.redact.subtree.ts
|
|
3354
|
-
import { existsSync as existsSync25, lstatSync as lstatSync7, readFileSync as readFileSync10, readdirSync as readdirSync10, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3355
|
-
import { join as join30 } from "node:path";
|
|
3356
|
-
init_utils_fs();
|
|
3357
|
-
function collectFiles(dir, out) {
|
|
3358
|
-
if (!existsSync25(dir)) return;
|
|
3359
|
-
const st = lstatSync7(dir);
|
|
3360
|
-
if (!st.isDirectory()) return;
|
|
3361
|
-
for (const entry of readdirSync10(dir)) {
|
|
3362
|
-
const abs = join30(dir, entry);
|
|
3363
|
-
const lst = lstatSync7(abs);
|
|
3364
|
-
if (lst.isSymbolicLink()) continue;
|
|
3365
|
-
if (lst.isDirectory()) {
|
|
3366
|
-
collectFiles(abs, out);
|
|
3367
|
-
continue;
|
|
3368
|
-
}
|
|
3369
|
-
if (lst.isFile()) out.push(abs);
|
|
3370
|
-
}
|
|
3371
|
-
}
|
|
3372
|
-
function listSubtreeFiles(sessionDir) {
|
|
3373
|
-
const out = [];
|
|
3374
|
-
collectFiles(sessionDir, out);
|
|
3375
|
-
return out.sort((a, b) => a.localeCompare(b));
|
|
3376
|
-
}
|
|
3377
|
-
function newestSubtreeMtimeMs(mainPath, subtreeFiles, statMtime = (p) => statSync6(p).mtimeMs) {
|
|
3378
|
-
let newest = statMtime(mainPath);
|
|
3379
|
-
for (const filePath of subtreeFiles) {
|
|
3380
|
-
const t = statMtime(filePath);
|
|
3381
|
-
if (t > newest) newest = t;
|
|
3382
|
-
}
|
|
3383
|
-
return newest;
|
|
3384
|
-
}
|
|
3385
|
-
function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts, scan, dryRun) {
|
|
3386
|
-
const dirty = [];
|
|
3387
|
-
if (mainFindings.length > 0) dirty.push({ path: mainPath, findings: mainFindings });
|
|
3388
|
-
for (const filePath of subtreeFiles) {
|
|
3389
|
-
const raw = scan(filePath);
|
|
3390
|
-
if (raw === null || raw.length === 0) continue;
|
|
3391
|
-
const filtered = rule === void 0 ? raw : raw.filter((f) => f.RuleID === rule);
|
|
3392
|
-
if (filtered.length === 0) continue;
|
|
3393
|
-
dirty.push({ path: filePath, findings: filtered });
|
|
3394
|
-
}
|
|
3395
|
-
const total = dirty.reduce((n, e) => n + e.findings.length, 0);
|
|
3396
|
-
if (!dryRun && total > 0) {
|
|
3397
|
-
for (const { path: filePath, findings } of dirty) {
|
|
3398
|
-
backupBeforeWrite(filePath, ts);
|
|
3399
|
-
writeFileSync4(filePath, applyRedactions(readFileSync10(filePath, "utf8"), findings), "utf8");
|
|
3400
|
-
}
|
|
3401
|
-
}
|
|
3402
|
-
return { total, dirty };
|
|
3403
|
-
}
|
|
3404
|
-
|
|
3405
3133
|
// src/commands.redact.ts
|
|
3406
|
-
|
|
3407
|
-
init_utils_fs();
|
|
3408
|
-
init_utils_json();
|
|
3409
|
-
init_utils();
|
|
3410
|
-
function resolveLiveTranscript2(id) {
|
|
3134
|
+
function resolveLiveTranscript(id) {
|
|
3411
3135
|
try {
|
|
3412
|
-
const mapPath =
|
|
3413
|
-
if (!
|
|
3136
|
+
const mapPath = join28(repoHome(), "path-map.json");
|
|
3137
|
+
if (!existsSync23(mapPath)) return null;
|
|
3414
3138
|
const projects = readJson(mapPath).projects;
|
|
3415
3139
|
const claude = claudeHome();
|
|
3416
3140
|
for (const hostMap of Object.values(projects)) {
|
|
3417
3141
|
const abs = hostMap[HOST];
|
|
3418
3142
|
if (abs === void 0) continue;
|
|
3419
|
-
const live =
|
|
3420
|
-
if (
|
|
3143
|
+
const live = join28(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
3144
|
+
if (existsSync23(live)) return live;
|
|
3421
3145
|
}
|
|
3422
3146
|
return null;
|
|
3423
3147
|
} catch {
|
|
@@ -3437,19 +3161,19 @@ function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
|
|
|
3437
3161
|
}
|
|
3438
3162
|
const repo = repoHome();
|
|
3439
3163
|
const backup = backupBase();
|
|
3440
|
-
if (!
|
|
3164
|
+
if (!existsSync23(repo)) die(`repo not cloned at ${repo}`);
|
|
3441
3165
|
const handle = acquireLock("redact");
|
|
3442
3166
|
if (handle === null) process.exit(0);
|
|
3443
3167
|
try {
|
|
3444
|
-
const localPath =
|
|
3445
|
-
if (localPath === null || !
|
|
3168
|
+
const localPath = resolveLiveTranscript(id);
|
|
3169
|
+
if (localPath === null || !existsSync23(localPath)) {
|
|
3446
3170
|
fail(`could not resolve local transcript for session ${id} on this host`);
|
|
3447
3171
|
process.exitCode = 1;
|
|
3448
3172
|
return;
|
|
3449
3173
|
}
|
|
3450
|
-
const sessionDir =
|
|
3174
|
+
const sessionDir = join28(dirname5(localPath), id);
|
|
3451
3175
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
3452
|
-
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) =>
|
|
3176
|
+
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync6(p).mtimeMs);
|
|
3453
3177
|
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
3454
3178
|
log(
|
|
3455
3179
|
`session ${id} was modified recently and may be active.
|
|
@@ -3499,1142 +3223,1630 @@ ${lines}`);
|
|
|
3499
3223
|
}
|
|
3500
3224
|
}
|
|
3501
3225
|
|
|
3502
|
-
// src/commands.
|
|
3503
|
-
|
|
3504
|
-
|
|
3226
|
+
// src/commands.push.recovery.redact.ts
|
|
3227
|
+
init_push_gitleaks_scan();
|
|
3228
|
+
init_utils_json();
|
|
3229
|
+
init_utils();
|
|
3505
3230
|
|
|
3506
|
-
// src/commands.push.
|
|
3507
|
-
|
|
3231
|
+
// src/commands.push.recovery.seams.ts
|
|
3232
|
+
init_push_gitleaks();
|
|
3233
|
+
var MASK_LEAD = 4;
|
|
3234
|
+
var MASK_BODY = "************";
|
|
3235
|
+
var CONTEXT_WINDOW = 40;
|
|
3236
|
+
var CONTROL_CHARS = /[\x00-\x1f\x7f]/g;
|
|
3237
|
+
function findingKey(f) {
|
|
3238
|
+
return `${f.File}:${f.StartLine}:${f.StartColumn}:${f.RuleID}`;
|
|
3239
|
+
}
|
|
3240
|
+
var VALID_SID = /^[A-Za-z0-9_-]+$/;
|
|
3241
|
+
function sessionIdFromFinding(f) {
|
|
3242
|
+
const m = SESSION_PATH.exec(f.File) ?? /^shared\/projects\/[^/]+\/([^/]+)\//.exec(f.File);
|
|
3243
|
+
if (m === null) return null;
|
|
3244
|
+
const sid = m[1];
|
|
3245
|
+
return VALID_SID.test(sid) ? sid : null;
|
|
3246
|
+
}
|
|
3247
|
+
function parseAction(raw) {
|
|
3248
|
+
const t = raw.trim().toLowerCase();
|
|
3249
|
+
if (t === "r" || t === "redact") return "redact";
|
|
3250
|
+
if (t === "a" || t === "allow") return "allow";
|
|
3251
|
+
if (t === "d" || t === "drop") return "drop";
|
|
3252
|
+
return "skip";
|
|
3253
|
+
}
|
|
3254
|
+
function maskSecret(secret) {
|
|
3255
|
+
return secret.slice(0, MASK_LEAD) + MASK_BODY;
|
|
3256
|
+
}
|
|
3257
|
+
function buildFindingContext(finding, readLine) {
|
|
3258
|
+
const raw = readLine(finding.File, finding.StartLine);
|
|
3259
|
+
if (raw !== null) {
|
|
3260
|
+
const len = raw.length;
|
|
3261
|
+
const startCol = Math.max(1, Math.min(finding.StartColumn, len + 1));
|
|
3262
|
+
const endCol = Math.max(startCol, Math.min(finding.EndColumn, len));
|
|
3263
|
+
const spanStart = startCol - 1;
|
|
3264
|
+
const spanEnd = endCol;
|
|
3265
|
+
const secret = raw.slice(spanStart, spanEnd);
|
|
3266
|
+
const masked = maskSecret(secret);
|
|
3267
|
+
const fullPrefix = raw.slice(0, spanStart);
|
|
3268
|
+
const fullSuffix = raw.slice(spanEnd);
|
|
3269
|
+
const prefixTruncated = fullPrefix.length > CONTEXT_WINDOW;
|
|
3270
|
+
const suffixTruncated = fullSuffix.length > CONTEXT_WINDOW;
|
|
3271
|
+
const prefix = prefixTruncated ? fullPrefix.slice(fullPrefix.length - CONTEXT_WINDOW) : fullPrefix;
|
|
3272
|
+
const suffix = suffixTruncated ? fullSuffix.slice(0, CONTEXT_WINDOW) : fullSuffix;
|
|
3273
|
+
const excerpt = (prefixTruncated ? "..." : "") + prefix + masked + suffix + (suffixTruncated ? "..." : "");
|
|
3274
|
+
const stripped = excerpt.replace(CONTROL_CHARS, "");
|
|
3275
|
+
if (stripped.trim().length > 0) return stripped;
|
|
3276
|
+
}
|
|
3277
|
+
if (finding.Match.length > 0) {
|
|
3278
|
+
return maskSecret(finding.Match).replace(CONTROL_CHARS, "");
|
|
3279
|
+
}
|
|
3280
|
+
return null;
|
|
3281
|
+
}
|
|
3508
3282
|
|
|
3509
|
-
// src/
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
if (
|
|
3516
|
-
return
|
|
3283
|
+
// src/commands.push.recovery.redact.ts
|
|
3284
|
+
function resolveStagedDir(localPath, map, claude, repo) {
|
|
3285
|
+
for (const [logical, hostMap] of Object.entries(map.projects)) {
|
|
3286
|
+
assertSafeLogical(logical);
|
|
3287
|
+
const abs = hostMap[HOST];
|
|
3288
|
+
if (abs === void 0) continue;
|
|
3289
|
+
if (localPath.startsWith(join29(claude, "projects", encodePath(abs)) + sep3)) {
|
|
3290
|
+
return join29(repo, "shared", "projects", logical);
|
|
3517
3291
|
}
|
|
3518
|
-
const base = `summary: ${unmapped} unmapped on push, ${collisions} collisions`;
|
|
3519
|
-
return { text: `${base}${extras} (run nomad doctor to list)`, clean: false };
|
|
3520
|
-
}
|
|
3521
|
-
if (unmapped === 0 && extrasSkipped === 0) {
|
|
3522
|
-
return { text: "summary: clean", clean: true };
|
|
3523
3292
|
}
|
|
3524
|
-
return
|
|
3525
|
-
|
|
3526
|
-
|
|
3293
|
+
return null;
|
|
3294
|
+
}
|
|
3295
|
+
function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
3296
|
+
const refuse = (msg) => {
|
|
3297
|
+
log(msg);
|
|
3298
|
+
return false;
|
|
3527
3299
|
};
|
|
3300
|
+
const claude = claudeHome();
|
|
3301
|
+
const repo = repoHome();
|
|
3302
|
+
const sid = sessionIdFromFinding(f);
|
|
3303
|
+
if (sid === null) {
|
|
3304
|
+
return refuse(
|
|
3305
|
+
`could not locate the local transcript for this finding; choose Skip or Drop session.`
|
|
3306
|
+
);
|
|
3307
|
+
}
|
|
3308
|
+
const localPath = resolveLiveTranscript(sid);
|
|
3309
|
+
if (localPath === null) {
|
|
3310
|
+
return refuse(
|
|
3311
|
+
`could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
3312
|
+
);
|
|
3313
|
+
}
|
|
3314
|
+
const sessionDir = join29(dirname6(localPath), sid);
|
|
3315
|
+
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
3316
|
+
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
|
|
3317
|
+
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
3318
|
+
return refuse(
|
|
3319
|
+
`session ${sid} looks active (modified within the last 5 minutes); refusing to redact, no changes made.
|
|
3320
|
+
End the session and choose Redact again, or choose Drop session (holds this session back from the push, local copy kept) or Skip.`
|
|
3321
|
+
);
|
|
3322
|
+
}
|
|
3323
|
+
const stagedProjectDir = resolveStagedDir(localPath, map, claude, repo);
|
|
3324
|
+
if (stagedProjectDir === null) {
|
|
3325
|
+
return refuse(
|
|
3326
|
+
`could not map the local transcript for session ${sid} to a staged copy; choose Drop session or Skip.`
|
|
3327
|
+
);
|
|
3328
|
+
}
|
|
3329
|
+
const mainFindings = scan(localPath);
|
|
3330
|
+
if (mainFindings === null) {
|
|
3331
|
+
return refuse(`re-scan of the transcript failed; choose Skip or Drop session.`);
|
|
3332
|
+
}
|
|
3333
|
+
const { total: anyTotal } = applySubtreeRedactions(
|
|
3334
|
+
localPath,
|
|
3335
|
+
mainFindings,
|
|
3336
|
+
subtreeFiles,
|
|
3337
|
+
void 0,
|
|
3338
|
+
ts,
|
|
3339
|
+
scan,
|
|
3340
|
+
false
|
|
3341
|
+
);
|
|
3342
|
+
if (anyTotal === 0) {
|
|
3343
|
+
return refuse(
|
|
3344
|
+
`nothing to redact in the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
3345
|
+
);
|
|
3346
|
+
}
|
|
3347
|
+
mkdirSync6(stagedProjectDir, { recursive: true });
|
|
3348
|
+
cpSync5(localPath, join29(stagedProjectDir, `${sid}.jsonl`), { force: true });
|
|
3349
|
+
if (existsSync24(sessionDir)) {
|
|
3350
|
+
cpSync5(sessionDir, join29(stagedProjectDir, sid), { force: true, recursive: true });
|
|
3351
|
+
}
|
|
3352
|
+
return true;
|
|
3528
3353
|
}
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3354
|
+
|
|
3355
|
+
// src/commands.push.recovery.drop.ts
|
|
3356
|
+
init_config();
|
|
3357
|
+
import { rmSync as rmSync8 } from "node:fs";
|
|
3358
|
+
import { join as join30 } from "node:path";
|
|
3359
|
+
function dropSessionFromStaged(sid, map) {
|
|
3360
|
+
const logicals = Object.keys(map.projects);
|
|
3361
|
+
if (logicals.length === 0) return false;
|
|
3362
|
+
const repo = repoHome();
|
|
3363
|
+
for (const logical of logicals) {
|
|
3364
|
+
const jsonl = join30(repo, "shared", "projects", logical, `${sid}.jsonl`);
|
|
3365
|
+
const dir = join30(repo, "shared", "projects", logical, sid);
|
|
3366
|
+
rmSync8(jsonl, { force: true });
|
|
3367
|
+
rmSync8(dir, { recursive: true, force: true });
|
|
3368
|
+
}
|
|
3369
|
+
return true;
|
|
3532
3370
|
}
|
|
3533
3371
|
|
|
3534
|
-
// src/commands.push.
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3372
|
+
// src/commands.push.recovery.actions.ts
|
|
3373
|
+
init_push_gitleaks_scan();
|
|
3374
|
+
init_utils();
|
|
3375
|
+
function applyAllow(f, repo) {
|
|
3376
|
+
appendGitleaksIgnore(f.Fingerprint, repo);
|
|
3538
3377
|
}
|
|
3539
|
-
function
|
|
3540
|
-
const
|
|
3541
|
-
|
|
3542
|
-
|
|
3378
|
+
function allowAllFindings(findings, repo) {
|
|
3379
|
+
for (const f of findings) {
|
|
3380
|
+
appendGitleaksIgnore(f.Fingerprint, repo);
|
|
3381
|
+
}
|
|
3543
3382
|
}
|
|
3544
|
-
function
|
|
3545
|
-
|
|
3546
|
-
for (const
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3383
|
+
function allowFindingsByRule(findings, ruleId, repo) {
|
|
3384
|
+
let count = 0;
|
|
3385
|
+
for (const f of findings) {
|
|
3386
|
+
if (f.RuleID === ruleId) {
|
|
3387
|
+
appendGitleaksIgnore(f.Fingerprint, repo);
|
|
3388
|
+
count++;
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
return count;
|
|
3550
3392
|
}
|
|
3551
|
-
function
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3393
|
+
function makeDefaultReadLine(repo) {
|
|
3394
|
+
return (file, line) => {
|
|
3395
|
+
try {
|
|
3396
|
+
const repoRoot = resolve3(repo);
|
|
3397
|
+
const target = resolve3(repoRoot, file);
|
|
3398
|
+
if (isAbsolute(file) || target !== repoRoot && !target.startsWith(repoRoot + sep4)) {
|
|
3399
|
+
return null;
|
|
3400
|
+
}
|
|
3401
|
+
const content = readFileSync12(target, "utf8");
|
|
3402
|
+
const lines = content.split(/\r?\n/);
|
|
3403
|
+
const idx = line - 1;
|
|
3404
|
+
if (idx < 0 || idx >= lines.length) return null;
|
|
3405
|
+
return lines[idx] ?? null;
|
|
3406
|
+
} catch {
|
|
3407
|
+
return null;
|
|
3408
|
+
}
|
|
3409
|
+
};
|
|
3557
3410
|
}
|
|
3558
|
-
function
|
|
3559
|
-
const
|
|
3560
|
-
const
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3411
|
+
async function collectActions(findings, prompt, readLine) {
|
|
3412
|
+
const reader = readLine ?? makeDefaultReadLine(repoHome());
|
|
3413
|
+
const actions = /* @__PURE__ */ new Map();
|
|
3414
|
+
for (const f of findings) {
|
|
3415
|
+
const sid = sessionIdFromFinding(f);
|
|
3416
|
+
const ctx = buildFindingContext(f, reader);
|
|
3417
|
+
const header = `
|
|
3418
|
+
Finding: ${f.RuleID} in ${f.File} line ${f.StartLine}` + (sid === null ? "" : ` (session: ${sid})`) + (ctx === null ? "" : `
|
|
3419
|
+
context: ${ctx}`) + "\n [R]edact [A]llow [D]rop session [S]kip (default)\n";
|
|
3420
|
+
actions.set(findingKey(f), parseAction(await prompt(header + "> ")));
|
|
3421
|
+
}
|
|
3422
|
+
return actions;
|
|
3565
3423
|
}
|
|
3566
|
-
function
|
|
3567
|
-
const
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
return
|
|
3424
|
+
function dispatchOne(f, ctx) {
|
|
3425
|
+
const action = ctx.actions.get(findingKey(f)) ?? "skip";
|
|
3426
|
+
if (action === "skip") return;
|
|
3427
|
+
const sid = sessionIdFromFinding(f);
|
|
3428
|
+
if (sid !== null && ctx.droppedSids.has(sid)) return;
|
|
3429
|
+
if (action === "allow") {
|
|
3430
|
+
applyAllow(f, ctx.repo);
|
|
3431
|
+
return;
|
|
3432
|
+
}
|
|
3433
|
+
if (sid === null) return;
|
|
3434
|
+
if (action === "drop") {
|
|
3435
|
+
ctx.droppedSids.add(sid);
|
|
3436
|
+
if (ctx.drop(sid, ctx.map)) {
|
|
3437
|
+
log(
|
|
3438
|
+
`dropped session ${sid} from this push (local transcript kept; the secret remains in your local copy)`
|
|
3439
|
+
);
|
|
3440
|
+
}
|
|
3441
|
+
return;
|
|
3442
|
+
}
|
|
3443
|
+
if (action === "redact" && !ctx.redactedSids.has(sid)) {
|
|
3444
|
+
if (applyRedact(f, ctx.ts, ctx.map, ctx.nowMs, ctx.scan)) ctx.redactedSids.add(sid);
|
|
3445
|
+
}
|
|
3571
3446
|
}
|
|
3572
|
-
function
|
|
3573
|
-
const
|
|
3574
|
-
|
|
3575
|
-
|
|
3447
|
+
function dispatchActions(findings, actions, opts) {
|
|
3448
|
+
const { ts, map, nowMs, repo, scan = scanFile, drop = dropSessionFromStaged } = opts;
|
|
3449
|
+
const ctx = {
|
|
3450
|
+
actions,
|
|
3451
|
+
ts,
|
|
3452
|
+
map,
|
|
3453
|
+
nowMs,
|
|
3454
|
+
repo,
|
|
3455
|
+
scan,
|
|
3456
|
+
drop,
|
|
3457
|
+
redactedSids: /* @__PURE__ */ new Set(),
|
|
3458
|
+
droppedSids: /* @__PURE__ */ new Set()
|
|
3459
|
+
};
|
|
3460
|
+
for (const f of findings) {
|
|
3461
|
+
dispatchOne(f, ctx);
|
|
3462
|
+
}
|
|
3576
3463
|
}
|
|
3577
|
-
function
|
|
3578
|
-
const
|
|
3579
|
-
|
|
3580
|
-
const
|
|
3581
|
-
|
|
3582
|
-
|
|
3464
|
+
function redactAllFindings(findings, ts, map, nowMs, scan = scanFile) {
|
|
3465
|
+
const redactedSids = /* @__PURE__ */ new Set();
|
|
3466
|
+
for (const f of findings) {
|
|
3467
|
+
const sid = sessionIdFromFinding(f);
|
|
3468
|
+
if (sid === null || redactedSids.has(sid)) continue;
|
|
3469
|
+
if (applyRedact(f, ts, map, nowMs, scan)) redactedSids.add(sid);
|
|
3583
3470
|
}
|
|
3584
|
-
renderTree([...sections, ...syncedSections(st), summarySection(st)]);
|
|
3585
3471
|
}
|
|
3586
3472
|
|
|
3587
|
-
// src/commands.
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
// src/extras-sync.ts
|
|
3591
|
-
init_config();
|
|
3592
|
-
import { existsSync as existsSync29 } from "node:fs";
|
|
3593
|
-
import { join as join34 } from "node:path";
|
|
3594
|
-
|
|
3595
|
-
// src/extras-sync.diff.ts
|
|
3473
|
+
// src/commands.push.recovery.ts
|
|
3474
|
+
init_push_gitleaks_scan();
|
|
3475
|
+
init_push_gitleaks();
|
|
3596
3476
|
init_utils();
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
const tab = line.indexOf(" ");
|
|
3600
|
-
if (tab === -1) return line;
|
|
3601
|
-
const status = line.slice(0, tab);
|
|
3602
|
-
const path = line.slice(tab + 1);
|
|
3603
|
-
if (status === "D") return `${path} (local only)`;
|
|
3604
|
-
if (status === "A") return `${path} (repo only)`;
|
|
3605
|
-
return path;
|
|
3477
|
+
function isTTY(stdin = process.stdin, stdout = process.stdout) {
|
|
3478
|
+
return stdin.isTTY === true && stdout.isTTY === true;
|
|
3606
3479
|
}
|
|
3607
|
-
function
|
|
3608
|
-
|
|
3480
|
+
function hasUnresolved(actions) {
|
|
3481
|
+
for (const action of actions.values()) {
|
|
3482
|
+
if (action === "skip") return true;
|
|
3483
|
+
}
|
|
3484
|
+
return false;
|
|
3609
3485
|
}
|
|
3610
|
-
function
|
|
3486
|
+
function printRecoveryLegend(print = console.log) {
|
|
3487
|
+
print("");
|
|
3488
|
+
print("Recovery actions:");
|
|
3489
|
+
print(" Redact - scrub the secret from the local transcript, push the cleaned copy");
|
|
3490
|
+
print(" Allow - mark as false positive (adds a .gitleaksignore fingerprint), push as-is");
|
|
3491
|
+
print(" Drop session - exclude this session from this push (local transcript kept, running");
|
|
3492
|
+
print(" session is not stopped)");
|
|
3493
|
+
print(" Skip - leave unresolved (the push aborts)");
|
|
3494
|
+
print("");
|
|
3495
|
+
}
|
|
3496
|
+
function applyThenRescan(scanVerdict, repoHome2) {
|
|
3497
|
+
gitOrFatal(["add", "-A"], "git add", repoHome2);
|
|
3498
|
+
const next = scanVerdict(repoHome2);
|
|
3499
|
+
if (next.leak) {
|
|
3500
|
+
const { bySession, other } = partitionFindings(next.findings);
|
|
3501
|
+
throw new NomadFatal(buildSessionAwareFatal(bySession, other));
|
|
3502
|
+
}
|
|
3503
|
+
return next;
|
|
3504
|
+
}
|
|
3505
|
+
function allowThenRescan(append, scanVerdict, repoHome2) {
|
|
3506
|
+
const ignPath = join31(repoHome2, ".gitleaksignore");
|
|
3507
|
+
let before;
|
|
3611
3508
|
try {
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3509
|
+
before = readFileSync13(ignPath, "utf8");
|
|
3510
|
+
} catch {
|
|
3511
|
+
before = null;
|
|
3512
|
+
}
|
|
3513
|
+
append();
|
|
3514
|
+
try {
|
|
3515
|
+
return applyThenRescan(scanVerdict, repoHome2);
|
|
3616
3516
|
} catch (err) {
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3620
|
-
}
|
|
3621
|
-
if (e.code === "ENOENT") {
|
|
3622
|
-
warn(`git not on PATH; divergence check skipped for ${a}`);
|
|
3623
|
-
return [];
|
|
3624
|
-
}
|
|
3625
|
-
warn(`divergence check failed for ${a}: ${e.message ?? String(err)}`);
|
|
3626
|
-
return [];
|
|
3517
|
+
if (before === null) rmSync9(ignPath, { force: true });
|
|
3518
|
+
else writeFileSync5(ignPath, before, "utf8");
|
|
3519
|
+
throw err;
|
|
3627
3520
|
}
|
|
3628
3521
|
}
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3522
|
+
function makeRealPrompt() {
|
|
3523
|
+
return async (prompt) => {
|
|
3524
|
+
const rl = createInterface({
|
|
3525
|
+
input: process.stdin,
|
|
3526
|
+
output: process.stdout,
|
|
3527
|
+
terminal: true
|
|
3528
|
+
});
|
|
3529
|
+
try {
|
|
3530
|
+
return await rl.question(prompt);
|
|
3531
|
+
} finally {
|
|
3532
|
+
rl.close();
|
|
3533
|
+
}
|
|
3534
|
+
};
|
|
3535
|
+
}
|
|
3536
|
+
async function resolveLeakFindings(verdict, ts, map, deps = {}) {
|
|
3537
|
+
const {
|
|
3538
|
+
isTTYCheck = isTTY,
|
|
3539
|
+
nowMs = Date.now,
|
|
3540
|
+
redactAll = false,
|
|
3541
|
+
allowAll = false,
|
|
3542
|
+
allowRule,
|
|
3543
|
+
makePrompt: makePromptFn = makeRealPrompt,
|
|
3544
|
+
scan = scanFile,
|
|
3545
|
+
printLegend = printRecoveryLegend
|
|
3546
|
+
} = deps;
|
|
3547
|
+
const scanVerdict = deps.scanVerdict ?? (await Promise.resolve().then(() => (init_push_leak_verdict(), push_leak_verdict_exports))).scanPushVerdict;
|
|
3548
|
+
const repo = repoHome();
|
|
3549
|
+
let current = verdict;
|
|
3550
|
+
if (redactAll) {
|
|
3551
|
+
redactAllFindings(current.findings, ts, map, nowMs, scan);
|
|
3552
|
+
return applyThenRescan(scanVerdict, repo);
|
|
3644
3553
|
}
|
|
3645
|
-
if (
|
|
3646
|
-
|
|
3647
|
-
|
|
3554
|
+
if (allowAll) {
|
|
3555
|
+
return allowThenRescan(() => allowAllFindings(current.findings, repo), scanVerdict, repo);
|
|
3556
|
+
}
|
|
3557
|
+
if (allowRule !== void 0) {
|
|
3558
|
+
return allowThenRescan(
|
|
3559
|
+
() => {
|
|
3560
|
+
const matched = allowFindingsByRule(current.findings, allowRule, repo);
|
|
3561
|
+
if (matched === 0) log(`no findings matched rule ${allowRule}; re-scanning`);
|
|
3562
|
+
},
|
|
3563
|
+
scanVerdict,
|
|
3564
|
+
repo
|
|
3648
3565
|
);
|
|
3649
3566
|
}
|
|
3567
|
+
if (!isTTYCheck()) {
|
|
3568
|
+
throw new NomadFatal(current.recovery ?? "gitleaks detected secrets");
|
|
3569
|
+
}
|
|
3570
|
+
const prompt = makePromptFn();
|
|
3571
|
+
printLegend();
|
|
3572
|
+
while (current.leak && current.findings.length > 0) {
|
|
3573
|
+
const actions = await collectActions(current.findings, prompt);
|
|
3574
|
+
if (hasUnresolved(actions)) {
|
|
3575
|
+
const unresolved = current.findings.filter((f) => actions.get(findingKey(f)) === "skip");
|
|
3576
|
+
const { bySession, other } = partitionFindings(unresolved);
|
|
3577
|
+
throw new NomadFatal(buildSessionAwareFatal(bySession, other));
|
|
3578
|
+
}
|
|
3579
|
+
dispatchActions(current.findings, actions, { ts, map, nowMs, repo, scan });
|
|
3580
|
+
gitOrFatal(["add", "-A"], "git add", repo);
|
|
3581
|
+
current = scanVerdict(repo);
|
|
3582
|
+
}
|
|
3583
|
+
return current;
|
|
3650
3584
|
}
|
|
3651
3585
|
|
|
3652
|
-
// src/
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
function loadValidatedExtras(opts) {
|
|
3656
|
-
const repo = repoHome();
|
|
3657
|
-
const mapPath = join32(repo, "path-map.json");
|
|
3658
|
-
const repoExtras = join32(repo, "shared", "extras");
|
|
3659
|
-
if (!existsSync27(mapPath) || opts.requireRepoExtras === true && !existsSync27(repoExtras)) {
|
|
3660
|
-
if (opts.missingMsg !== void 0) log(opts.missingMsg);
|
|
3661
|
-
return null;
|
|
3662
|
-
}
|
|
3663
|
-
const map = readPathMap(mapPath);
|
|
3664
|
-
const extrasMap = map.extras ?? {};
|
|
3665
|
-
if (Object.keys(extrasMap).length === 0) return null;
|
|
3666
|
-
for (const logical of Object.keys(extrasMap)) {
|
|
3667
|
-
assertSafeLogical(logical);
|
|
3668
|
-
const localRoot = map.projects[logical]?.[HOST];
|
|
3669
|
-
if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
|
|
3670
|
-
}
|
|
3671
|
-
return { map, extrasMap };
|
|
3586
|
+
// src/spinner.ts
|
|
3587
|
+
function formatElapsed(ms) {
|
|
3588
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
3672
3589
|
}
|
|
3673
|
-
function
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3590
|
+
function writePlainStart(out, label) {
|
|
3591
|
+
out.write(`${label}...
|
|
3592
|
+
`);
|
|
3593
|
+
}
|
|
3594
|
+
function writePlainDone(out, label, ms) {
|
|
3595
|
+
out.write(`${label} done (${formatElapsed(ms)})
|
|
3596
|
+
`);
|
|
3597
|
+
}
|
|
3598
|
+
function writeAnimatedDone(out, label, ms, useTTY) {
|
|
3599
|
+
out.write("\r\x1B[K");
|
|
3600
|
+
const glyph = useTTY ? green(okGlyph) : okGlyph;
|
|
3601
|
+
out.write(`${glyph} ${label} (${formatElapsed(ms)})
|
|
3602
|
+
`);
|
|
3603
|
+
}
|
|
3604
|
+
function resolveWorkerPath(deps = {}) {
|
|
3605
|
+
const check = deps.existsSyncFn ?? existsSync25;
|
|
3606
|
+
const base = deps.baseUrl ?? import.meta.url;
|
|
3607
|
+
const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
|
|
3608
|
+
if (check(mjs)) return mjs;
|
|
3609
|
+
return fileURLToPath4(new URL("./spinner.worker.ts", base));
|
|
3610
|
+
}
|
|
3611
|
+
function makeRealWorker() {
|
|
3612
|
+
return new Worker(resolveWorkerPath());
|
|
3613
|
+
}
|
|
3614
|
+
function startSpinner(label, deps = {}) {
|
|
3615
|
+
const ttyCheck = deps.isTTYCheck ?? (() => isTTY());
|
|
3616
|
+
const env = deps.env ?? process.env;
|
|
3617
|
+
const out = deps.out ?? process.stderr;
|
|
3618
|
+
const now = deps.now ?? Date.now;
|
|
3619
|
+
const startMs = now();
|
|
3620
|
+
const animate = ttyCheck() && !env.CI;
|
|
3621
|
+
let worker = null;
|
|
3622
|
+
let degraded = false;
|
|
3623
|
+
let finalized = false;
|
|
3624
|
+
if (animate) {
|
|
3625
|
+
const factory = deps.makeWorker ?? makeRealWorker;
|
|
3626
|
+
try {
|
|
3627
|
+
worker = factory();
|
|
3628
|
+
worker.unref?.();
|
|
3629
|
+
worker.postMessage({ type: "start", label });
|
|
3630
|
+
} catch {
|
|
3631
|
+
degraded = true;
|
|
3632
|
+
worker = null;
|
|
3633
|
+
writePlainStart(out, label);
|
|
3680
3634
|
}
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3635
|
+
} else {
|
|
3636
|
+
writePlainStart(out, label);
|
|
3637
|
+
}
|
|
3638
|
+
function finalize(success, doneLabel) {
|
|
3639
|
+
if (finalized) return;
|
|
3640
|
+
finalized = true;
|
|
3641
|
+
const dl = doneLabel ?? label;
|
|
3642
|
+
const elapsed = now() - startMs;
|
|
3643
|
+
if (animate && !degraded && worker !== null) {
|
|
3644
|
+
worker.postMessage({ type: "pause" });
|
|
3645
|
+
worker.terminate();
|
|
3646
|
+
worker = null;
|
|
3647
|
+
if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
|
|
3648
|
+
else out.write("\r\x1B[K");
|
|
3649
|
+
} else if (success) {
|
|
3650
|
+
writePlainDone(out, dl, elapsed);
|
|
3687
3651
|
}
|
|
3688
3652
|
}
|
|
3653
|
+
return {
|
|
3654
|
+
succeed: (doneLabel) => finalize(true, doneLabel),
|
|
3655
|
+
stop: () => finalize(false)
|
|
3656
|
+
};
|
|
3689
3657
|
}
|
|
3690
|
-
function
|
|
3691
|
-
|
|
3692
|
-
|
|
3658
|
+
function withSpinner(label, fn, deps) {
|
|
3659
|
+
const sp = startSpinner(label, deps);
|
|
3660
|
+
try {
|
|
3661
|
+
const result = fn();
|
|
3662
|
+
sp.succeed();
|
|
3663
|
+
return result;
|
|
3664
|
+
} finally {
|
|
3665
|
+
sp.stop();
|
|
3666
|
+
}
|
|
3693
3667
|
}
|
|
3694
3668
|
|
|
3695
|
-
// src/
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3669
|
+
// src/commands.doctor.gitleaks-version.ts
|
|
3670
|
+
init_color();
|
|
3671
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
3672
|
+
import { existsSync as existsSync26 } from "node:fs";
|
|
3673
|
+
import { join as join32 } from "node:path";
|
|
3700
3674
|
init_config();
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
const done = [];
|
|
3707
|
-
const would = [];
|
|
3708
|
-
for (const t of eachExtrasTarget(v, counts)) {
|
|
3709
|
-
const { src, dst } = paths(t);
|
|
3710
|
-
if (!existsSync28(src)) continue;
|
|
3711
|
-
const item2 = `${t.logical}/${t.dirname}`;
|
|
3712
|
-
if (dryRun) {
|
|
3713
|
-
would.push(item2);
|
|
3714
|
-
continue;
|
|
3715
|
-
}
|
|
3716
|
-
backup(dst, t.localRoot);
|
|
3717
|
-
copyExtras(src, dst);
|
|
3718
|
-
done.push(item2);
|
|
3719
|
-
}
|
|
3720
|
-
return { ...counts, done, would };
|
|
3675
|
+
var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
|
|
3676
|
+
var GITLEAKS_TIMEOUT_MS = 5e3;
|
|
3677
|
+
function majorMinorOf(value) {
|
|
3678
|
+
const m = SEMVER_MAJOR_MINOR.exec(value);
|
|
3679
|
+
return m === null ? null : [m[1], m[2]];
|
|
3721
3680
|
}
|
|
3722
|
-
function
|
|
3723
|
-
const
|
|
3724
|
-
const
|
|
3725
|
-
if (
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
dst: join33(repoExtras, logical, dirname7)
|
|
3735
|
-
}),
|
|
3736
|
-
(dst) => backupRepoWrite(dst, ts, repo)
|
|
3737
|
-
);
|
|
3738
|
-
return { unmapped, skipped, pushed: done, wouldPush: would };
|
|
3681
|
+
function readGitleaksVersion(run, tomlExists) {
|
|
3682
|
+
const tomlPath = join32(repoHome(), ".gitleaks.toml");
|
|
3683
|
+
const args = ["version"];
|
|
3684
|
+
if (tomlExists(tomlPath)) args.push("--config", tomlPath);
|
|
3685
|
+
try {
|
|
3686
|
+
return run("gitleaks", args, {
|
|
3687
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3688
|
+
timeout: GITLEAKS_TIMEOUT_MS
|
|
3689
|
+
}).toString().trim();
|
|
3690
|
+
} catch {
|
|
3691
|
+
return null;
|
|
3692
|
+
}
|
|
3739
3693
|
}
|
|
3740
|
-
function
|
|
3741
|
-
const
|
|
3742
|
-
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
if (
|
|
3747
|
-
const
|
|
3748
|
-
|
|
3749
|
-
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
})
|
|
3755
|
-
// Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
|
|
3756
|
-
// localRoot so the backup tree mirrors the project layout.
|
|
3757
|
-
(dst, localRoot) => backupExtrasWrite(dst, ts, localRoot)
|
|
3694
|
+
function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync26) {
|
|
3695
|
+
const raw = readGitleaksVersion(run, tomlExists);
|
|
3696
|
+
if (raw === null) return;
|
|
3697
|
+
const local = majorMinorOf(raw);
|
|
3698
|
+
if (local === null) return;
|
|
3699
|
+
const pin = majorMinorOf(GITLEAKS_PINNED_VERSION);
|
|
3700
|
+
if (pin === null) return;
|
|
3701
|
+
const sameMajorMinor = local[0] === pin[0] && local[1] === pin[1];
|
|
3702
|
+
if (sameMajorMinor) {
|
|
3703
|
+
addItem(section2, `${green(okGlyph)} gitleaks: ${raw} (matches pinned ${pin[0]}.${pin[1]})`);
|
|
3704
|
+
return;
|
|
3705
|
+
}
|
|
3706
|
+
addItem(
|
|
3707
|
+
section2,
|
|
3708
|
+
`${yellow(warnGlyph)} gitleaks: ${raw} -> ${GITLEAKS_PINNED_VERSION} (CI pins this; local drift may change scan results)`
|
|
3758
3709
|
);
|
|
3759
|
-
return { unmapped, skipped, pulled: done, wouldPull: would };
|
|
3760
3710
|
}
|
|
3761
3711
|
|
|
3762
|
-
// src/
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
);
|
|
3779
|
-
|
|
3712
|
+
// src/commands.doctor.checks.deps.ts
|
|
3713
|
+
init_color();
|
|
3714
|
+
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
3715
|
+
var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
|
|
3716
|
+
var PROBE_TIMEOUT_MS = 3e3;
|
|
3717
|
+
var FETCHER_BASE = "HTTP fetcher";
|
|
3718
|
+
function parseFirstVersion(line) {
|
|
3719
|
+
const m = VERSION_TOKEN.exec(line);
|
|
3720
|
+
return m ? m[1] : null;
|
|
3721
|
+
}
|
|
3722
|
+
function probeOptionalDep(bin, run) {
|
|
3723
|
+
try {
|
|
3724
|
+
const firstLine = run(bin, ["--version"], {
|
|
3725
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3726
|
+
timeout: PROBE_TIMEOUT_MS
|
|
3727
|
+
}).toString().split("\n")[0].trim();
|
|
3728
|
+
const version = parseFirstVersion(firstLine);
|
|
3729
|
+
return { status: "present", version };
|
|
3730
|
+
} catch (err) {
|
|
3731
|
+
if (err.code === "ENOENT") {
|
|
3732
|
+
return { status: "not-installed" };
|
|
3733
|
+
}
|
|
3734
|
+
return { status: "present", version: null };
|
|
3780
3735
|
}
|
|
3781
3736
|
}
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
import { join as join35 } from "node:path";
|
|
3790
|
-
function emitAutoMove(onPreview, linkPath, ts, name) {
|
|
3791
|
-
if (onPreview) {
|
|
3792
|
-
onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
|
|
3737
|
+
function reportFetcherRow(section2, run) {
|
|
3738
|
+
const curl = probeOptionalDep("curl", run);
|
|
3739
|
+
const wget = probeOptionalDep("wget", run);
|
|
3740
|
+
if (curl.status === "present") {
|
|
3741
|
+
addItem(section2, `${green(okGlyph)} ${FETCHER_BASE}: curl ${curl.version ?? "(present)"}`);
|
|
3742
|
+
} else if (wget.status === "present") {
|
|
3743
|
+
addItem(section2, `${green(okGlyph)} ${FETCHER_BASE}: wget ${wget.version ?? "(present)"}`);
|
|
3793
3744
|
} else {
|
|
3794
|
-
|
|
3745
|
+
addItem(
|
|
3746
|
+
section2,
|
|
3747
|
+
`${yellow(warnGlyph)} ${FETCHER_BASE} (curl or wget): not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
|
|
3748
|
+
);
|
|
3795
3749
|
}
|
|
3796
3750
|
}
|
|
3797
|
-
function
|
|
3798
|
-
|
|
3799
|
-
|
|
3751
|
+
function reportOptionalDeps(section2, run = execFileSync8) {
|
|
3752
|
+
const gh = probeOptionalDep("gh", run);
|
|
3753
|
+
if (gh.status === "present") {
|
|
3754
|
+
addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
|
|
3800
3755
|
} else {
|
|
3801
|
-
|
|
3756
|
+
addItem(
|
|
3757
|
+
section2,
|
|
3758
|
+
`${yellow(warnGlyph)} gh: not installed (optional; needed for nomad init Actions auto-disable + the Actions-drift check)`
|
|
3759
|
+
);
|
|
3802
3760
|
}
|
|
3803
|
-
|
|
3804
|
-
function applySharedLinks(ts, map, opts = {}) {
|
|
3805
|
-
const dryRun = opts.dryRun === true;
|
|
3806
|
-
const claude = claudeHome();
|
|
3807
|
-
const repo = repoHome();
|
|
3808
|
-
const linkNames = allSharedLinks(map);
|
|
3809
|
-
for (const name of linkNames) {
|
|
3810
|
-
const linkPath = join35(claude, name);
|
|
3811
|
-
const target = join35(repo, "shared", name);
|
|
3812
|
-
if (!existsSync30(linkPath)) continue;
|
|
3813
|
-
if (lstatSync8(linkPath).isSymbolicLink()) continue;
|
|
3814
|
-
if (!existsSync30(target)) continue;
|
|
3815
|
-
if (dryRun) {
|
|
3816
|
-
emitAutoMove(opts.onPreview, linkPath, ts, name);
|
|
3817
|
-
continue;
|
|
3818
|
-
}
|
|
3819
|
-
backupBeforeWrite(linkPath, ts);
|
|
3820
|
-
rmSync9(linkPath, { recursive: true, force: true });
|
|
3821
|
-
}
|
|
3822
|
-
for (const name of linkNames) {
|
|
3823
|
-
const target = join35(repo, "shared", name);
|
|
3824
|
-
if (!existsSync30(target)) continue;
|
|
3825
|
-
if (dryRun) {
|
|
3826
|
-
emitCreate(opts.onPreview, join35(claude, name), target);
|
|
3827
|
-
continue;
|
|
3828
|
-
}
|
|
3829
|
-
ensureSymlink(join35(claude, name), target);
|
|
3830
|
-
}
|
|
3831
|
-
}
|
|
3832
|
-
function regenerateSettings(ts, opts = {}) {
|
|
3833
|
-
const dryRun = opts.dryRun === true;
|
|
3834
|
-
const repo = repoHome();
|
|
3835
|
-
const claude = claudeHome();
|
|
3836
|
-
const basePath = join35(repo, "shared", "settings.base.json");
|
|
3837
|
-
const hostPath = join35(repo, "hosts", `${HOST}.json`);
|
|
3838
|
-
if (!existsSync30(basePath)) {
|
|
3839
|
-
die("repo not initialized; run 'nomad init' to scaffold");
|
|
3840
|
-
}
|
|
3841
|
-
const base = readJson(basePath);
|
|
3842
|
-
const hasOverrides = existsSync30(hostPath);
|
|
3843
|
-
const overrides = hasOverrides ? readJson(hostPath) : {};
|
|
3844
|
-
const merged = deepMerge(base, overrides);
|
|
3845
|
-
const settingsPath = join35(claude, "settings.json");
|
|
3846
|
-
if (!hasOverrides && existsSync30(settingsPath)) {
|
|
3847
|
-
try {
|
|
3848
|
-
const existing = readJson(settingsPath);
|
|
3849
|
-
const baseKeys = new Set(Object.keys(base));
|
|
3850
|
-
const drift = Object.keys(existing).filter((k) => !baseKeys.has(k));
|
|
3851
|
-
if (drift.length > 0) {
|
|
3852
|
-
warn(
|
|
3853
|
-
`no hosts/${HOST}.json found; existing settings has unbased keys ${JSON.stringify(drift)}. Set NOMAD_HOST to match a hosts/*.json or rerun 'nomad doctor' for candidates.`
|
|
3854
|
-
);
|
|
3855
|
-
}
|
|
3856
|
-
} catch {
|
|
3857
|
-
warn("existing settings.json is malformed; skipping drift-check and regenerating.");
|
|
3858
|
-
}
|
|
3859
|
-
}
|
|
3860
|
-
const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
|
|
3861
|
-
if (dryRun) {
|
|
3862
|
-
log(`would write settings.json (base + ${overrideLabel})`);
|
|
3863
|
-
return { label: overrideLabel };
|
|
3864
|
-
}
|
|
3865
|
-
backupBeforeWrite(settingsPath, ts);
|
|
3866
|
-
writeJsonAtomic(settingsPath, merged);
|
|
3867
|
-
return { label: overrideLabel };
|
|
3761
|
+
reportFetcherRow(section2, run);
|
|
3868
3762
|
}
|
|
3869
3763
|
|
|
3870
|
-
// src/
|
|
3764
|
+
// src/commands.doctor.actions-drift.ts
|
|
3765
|
+
init_color();
|
|
3766
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
3871
3767
|
init_config();
|
|
3872
|
-
import { existsSync as existsSync31 } from "node:fs";
|
|
3873
|
-
import { join as join36 } from "node:path";
|
|
3874
3768
|
|
|
3875
|
-
//
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
setTimeout(function() {
|
|
3897
|
-
callback(value);
|
|
3898
|
-
}, 0);
|
|
3899
|
-
return void 0;
|
|
3900
|
-
} else {
|
|
3901
|
-
return value;
|
|
3902
|
-
}
|
|
3903
|
-
};
|
|
3904
|
-
const newLen = newTokens.length, oldLen = oldTokens.length;
|
|
3905
|
-
let editLength = 1;
|
|
3906
|
-
let maxEditLength = newLen + oldLen;
|
|
3907
|
-
if (options.maxEditLength != null) {
|
|
3908
|
-
maxEditLength = Math.min(maxEditLength, options.maxEditLength);
|
|
3909
|
-
}
|
|
3910
|
-
const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
|
|
3911
|
-
const abortAfterTimestamp = Date.now() + maxExecutionTime;
|
|
3912
|
-
const bestPath = [{ oldPos: -1, lastComponent: void 0 }];
|
|
3913
|
-
let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
|
|
3914
|
-
if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
3915
|
-
return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
|
|
3916
|
-
}
|
|
3917
|
-
let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
|
|
3918
|
-
const execEditLength = () => {
|
|
3919
|
-
for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
|
|
3920
|
-
let basePath;
|
|
3921
|
-
const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
|
|
3922
|
-
if (removePath) {
|
|
3923
|
-
bestPath[diagonalPath - 1] = void 0;
|
|
3924
|
-
}
|
|
3925
|
-
let canAdd = false;
|
|
3926
|
-
if (addPath) {
|
|
3927
|
-
const addPathNewPos = addPath.oldPos - diagonalPath;
|
|
3928
|
-
canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
|
|
3929
|
-
}
|
|
3930
|
-
const canRemove = removePath && removePath.oldPos + 1 < oldLen;
|
|
3931
|
-
if (!canAdd && !canRemove) {
|
|
3932
|
-
bestPath[diagonalPath] = void 0;
|
|
3933
|
-
continue;
|
|
3934
|
-
}
|
|
3935
|
-
if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
|
|
3936
|
-
basePath = this.addToPath(addPath, true, false, 0, options);
|
|
3937
|
-
} else {
|
|
3938
|
-
basePath = this.addToPath(removePath, false, true, 1, options);
|
|
3939
|
-
}
|
|
3940
|
-
newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
|
|
3941
|
-
if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
3942
|
-
return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
|
|
3943
|
-
} else {
|
|
3944
|
-
bestPath[diagonalPath] = basePath;
|
|
3945
|
-
if (basePath.oldPos + 1 >= oldLen) {
|
|
3946
|
-
maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
|
|
3947
|
-
}
|
|
3948
|
-
if (newPos + 1 >= newLen) {
|
|
3949
|
-
minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
|
|
3950
|
-
}
|
|
3951
|
-
}
|
|
3952
|
-
}
|
|
3953
|
-
editLength++;
|
|
3954
|
-
};
|
|
3955
|
-
if (callback) {
|
|
3956
|
-
(function exec() {
|
|
3957
|
-
setTimeout(function() {
|
|
3958
|
-
if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
|
|
3959
|
-
return callback(void 0);
|
|
3960
|
-
}
|
|
3961
|
-
if (!execEditLength()) {
|
|
3962
|
-
exec();
|
|
3963
|
-
}
|
|
3964
|
-
}, 0);
|
|
3965
|
-
})();
|
|
3966
|
-
} else {
|
|
3967
|
-
while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
|
|
3968
|
-
const ret = execEditLength();
|
|
3969
|
-
if (ret) {
|
|
3970
|
-
return ret;
|
|
3971
|
-
}
|
|
3972
|
-
}
|
|
3973
|
-
}
|
|
3974
|
-
}
|
|
3975
|
-
addToPath(path, added, removed, oldPosInc, options) {
|
|
3976
|
-
const last = path.lastComponent;
|
|
3977
|
-
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
3978
|
-
return {
|
|
3979
|
-
oldPos: path.oldPos + oldPosInc,
|
|
3980
|
-
lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
|
|
3981
|
-
};
|
|
3982
|
-
} else {
|
|
3983
|
-
return {
|
|
3984
|
-
oldPos: path.oldPos + oldPosInc,
|
|
3985
|
-
lastComponent: { count: 1, added, removed, previousComponent: last }
|
|
3986
|
-
};
|
|
3987
|
-
}
|
|
3769
|
+
// src/gh-actions.ts
|
|
3770
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
3771
|
+
var GH_TIMEOUT_MS = 5e3;
|
|
3772
|
+
function parseGitHubRemote(remoteUrl) {
|
|
3773
|
+
const normalized = remoteUrl.trim().replace(/\/$/, "");
|
|
3774
|
+
const m = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/.exec(normalized);
|
|
3775
|
+
if (m === null) return null;
|
|
3776
|
+
return { owner: m[1], repo: m[2] };
|
|
3777
|
+
}
|
|
3778
|
+
function ghAuthStatus(run = execFileSync9) {
|
|
3779
|
+
try {
|
|
3780
|
+
run("gh", ["auth", "status"], {
|
|
3781
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
3782
|
+
timeout: GH_TIMEOUT_MS
|
|
3783
|
+
});
|
|
3784
|
+
return null;
|
|
3785
|
+
} catch (err) {
|
|
3786
|
+
const e = err;
|
|
3787
|
+
if (e.code === "ENOENT") return "gh-not-installed";
|
|
3788
|
+
if (typeof e.status === "number") return "gh-not-authed";
|
|
3789
|
+
return "gh-probe-error";
|
|
3988
3790
|
}
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
}
|
|
4003
|
-
|
|
4004
|
-
|
|
3791
|
+
}
|
|
3792
|
+
function isRepoPrivate(ref, run = execFileSync9) {
|
|
3793
|
+
const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
|
|
3794
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3795
|
+
timeout: GH_TIMEOUT_MS
|
|
3796
|
+
}).toString();
|
|
3797
|
+
const parsed = JSON.parse(out);
|
|
3798
|
+
return parsed.isPrivate === true;
|
|
3799
|
+
}
|
|
3800
|
+
function isActionsEnabled(ref, run = execFileSync9) {
|
|
3801
|
+
const out = run(
|
|
3802
|
+
"gh",
|
|
3803
|
+
["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
|
|
3804
|
+
{ stdio: ["ignore", "pipe", "ignore"], timeout: GH_TIMEOUT_MS }
|
|
3805
|
+
).toString().trim();
|
|
3806
|
+
return out === "true";
|
|
3807
|
+
}
|
|
3808
|
+
function disableActions(ref, run = execFileSync9) {
|
|
3809
|
+
run(
|
|
3810
|
+
"gh",
|
|
3811
|
+
[
|
|
3812
|
+
"api",
|
|
3813
|
+
"-X",
|
|
3814
|
+
"PUT",
|
|
3815
|
+
`repos/${ref.owner}/${ref.repo}/actions/permissions`,
|
|
3816
|
+
"-F",
|
|
3817
|
+
"enabled=false"
|
|
3818
|
+
],
|
|
3819
|
+
{ stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
|
|
3820
|
+
);
|
|
3821
|
+
}
|
|
3822
|
+
function readOriginRemote(cwd, run = execFileSync9) {
|
|
3823
|
+
return run("git", ["remote", "get-url", "origin"], {
|
|
3824
|
+
cwd,
|
|
3825
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3826
|
+
}).toString().trim();
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
// src/commands.doctor.actions-drift.ts
|
|
3830
|
+
function reportActionsDrift(section2, run = execFileSync10) {
|
|
3831
|
+
let remote;
|
|
3832
|
+
try {
|
|
3833
|
+
remote = readOriginRemote(repoHome(), run);
|
|
3834
|
+
} catch {
|
|
3835
|
+
return;
|
|
4005
3836
|
}
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
3837
|
+
const ref = parseGitHubRemote(remote);
|
|
3838
|
+
if (ref === null) return;
|
|
3839
|
+
const auth = ghAuthStatus(run);
|
|
3840
|
+
if (auth === "gh-not-installed" || auth === "gh-not-authed") return;
|
|
3841
|
+
let isPrivate;
|
|
3842
|
+
try {
|
|
3843
|
+
isPrivate = isRepoPrivate(ref, run);
|
|
3844
|
+
} catch {
|
|
3845
|
+
return;
|
|
4012
3846
|
}
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
}
|
|
4020
|
-
return ret;
|
|
3847
|
+
if (!isPrivate) return;
|
|
3848
|
+
let enabled2;
|
|
3849
|
+
try {
|
|
3850
|
+
enabled2 = isActionsEnabled(ref, run);
|
|
3851
|
+
} catch {
|
|
3852
|
+
return;
|
|
4021
3853
|
}
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
3854
|
+
if (!enabled2) return;
|
|
3855
|
+
addItem(
|
|
3856
|
+
section2,
|
|
3857
|
+
`${yellow(warnGlyph)} Actions: enabled on private repo ${ref.owner}/${ref.repo} (re-disable with 'gh api -X PUT repos/${ref.owner}/${ref.repo}/actions/permissions -F enabled=false')`
|
|
3858
|
+
);
|
|
3859
|
+
}
|
|
3860
|
+
|
|
3861
|
+
// src/commands.doctor.verdict.ts
|
|
3862
|
+
init_color();
|
|
3863
|
+
function isFailLine(item2) {
|
|
3864
|
+
return item2.includes(failGlyph);
|
|
3865
|
+
}
|
|
3866
|
+
function isWarnLine(item2) {
|
|
3867
|
+
return !isFailLine(item2) && item2.includes(warnGlyph);
|
|
3868
|
+
}
|
|
3869
|
+
function buildVerdictSection(sections) {
|
|
3870
|
+
const summary = section("Summary");
|
|
3871
|
+
const lines = sections.flatMap((s) => s.items).map((item2) => item2.replace(/^\t/, ""));
|
|
3872
|
+
const failures = lines.filter(isFailLine);
|
|
3873
|
+
const warnings = lines.filter(isWarnLine);
|
|
3874
|
+
for (const line of [...failures, ...warnings]) addItem(summary, line);
|
|
3875
|
+
if (failures.length > 0) {
|
|
3876
|
+
addItem(
|
|
3877
|
+
summary,
|
|
3878
|
+
`${red(failGlyph)} ${failures.length} failure(s), ${warnings.length} warning(s)`
|
|
3879
|
+
);
|
|
3880
|
+
} else if (warnings.length > 0) {
|
|
3881
|
+
addItem(summary, `${yellow(warnGlyph)} ${warnings.length} warning(s)`);
|
|
3882
|
+
} else {
|
|
3883
|
+
addItem(summary, `${green(okGlyph)} healthy`);
|
|
4025
3884
|
}
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
3885
|
+
return summary;
|
|
3886
|
+
}
|
|
3887
|
+
|
|
3888
|
+
// src/commands.doctor.ts
|
|
3889
|
+
function gatherDoctorSections(opts) {
|
|
3890
|
+
const host = section("Environment");
|
|
3891
|
+
reportHostAndPaths(host);
|
|
3892
|
+
reportRepoState(host);
|
|
3893
|
+
const links = section("Shared links");
|
|
3894
|
+
const mapPath = join33(repoHome(), "path-map.json");
|
|
3895
|
+
const rawMap = existsSync27(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
|
|
3896
|
+
const map = rawMap ?? { projects: {} };
|
|
3897
|
+
reportSharedLinks(links, map);
|
|
3898
|
+
const hooksScan = section("Hook targets");
|
|
3899
|
+
reportHooksTargetCheck(hooksScan);
|
|
3900
|
+
reportHookScopeCheck(hooksScan);
|
|
3901
|
+
reportPreserveSymlinksCheck(hooksScan);
|
|
3902
|
+
const settings = section("Settings");
|
|
3903
|
+
const base = loadBaseSettings(settings);
|
|
3904
|
+
const parsedSettings = loadAndReportSettings(settings);
|
|
3905
|
+
reportHostOverrides(settings, base, parsedSettings);
|
|
3906
|
+
reportSettingsDriftCheck(settings);
|
|
3907
|
+
const pathMap = section("Path map");
|
|
3908
|
+
reportPathMap(pathMap);
|
|
3909
|
+
const neverSync = section("Never-sync");
|
|
3910
|
+
reportNeverSync(neverSync);
|
|
3911
|
+
const repository = section("Repository");
|
|
3912
|
+
const gitleaksReady = reportGitleaksProbe(repository);
|
|
3913
|
+
reportGitlinks(repository);
|
|
3914
|
+
reportRemote(repository);
|
|
3915
|
+
reportRebaseClean(repository);
|
|
3916
|
+
reportRebaseState(repository);
|
|
3917
|
+
reportActionsDrift(repository);
|
|
3918
|
+
const nomadVersion = section("Nomad Version");
|
|
3919
|
+
reportVersionCheck(nomadVersion);
|
|
3920
|
+
const housekeeping = section("Housekeeping");
|
|
3921
|
+
reportBackupsCheck(housekeeping);
|
|
3922
|
+
const depVersions = section("Dependency Versions");
|
|
3923
|
+
reportNodeEngineCheck(depVersions);
|
|
3924
|
+
reportGitleaksVersionCheck(depVersions);
|
|
3925
|
+
reportOptionalDeps(depVersions);
|
|
3926
|
+
const sharedScan = section("Shared scan");
|
|
3927
|
+
if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
|
|
3928
|
+
const schemaScan = section("Schema scan");
|
|
3929
|
+
if (opts.checkSchema === true) reportCheckSchema(schemaScan);
|
|
3930
|
+
const body = [
|
|
3931
|
+
nomadVersion,
|
|
3932
|
+
depVersions,
|
|
3933
|
+
host,
|
|
3934
|
+
links,
|
|
3935
|
+
hooksScan,
|
|
3936
|
+
settings,
|
|
3937
|
+
pathMap,
|
|
3938
|
+
neverSync,
|
|
3939
|
+
repository,
|
|
3940
|
+
housekeeping,
|
|
3941
|
+
sharedScan,
|
|
3942
|
+
schemaScan
|
|
3943
|
+
];
|
|
3944
|
+
return [...body, buildVerdictSection(body)];
|
|
3945
|
+
}
|
|
3946
|
+
function cmdDoctor(opts = {}) {
|
|
3947
|
+
const makeSpinner = opts.startSpinner ?? startSpinner;
|
|
3948
|
+
const sp = makeSpinner("Running checks");
|
|
3949
|
+
let report;
|
|
3950
|
+
try {
|
|
3951
|
+
report = gatherDoctorSections(opts);
|
|
3952
|
+
} finally {
|
|
3953
|
+
sp.stop();
|
|
4029
3954
|
}
|
|
4030
|
-
|
|
4031
|
-
|
|
3955
|
+
renderDoctor(report);
|
|
3956
|
+
}
|
|
3957
|
+
|
|
3958
|
+
// src/commands.drop-session.ts
|
|
3959
|
+
init_config();
|
|
3960
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
3961
|
+
import { existsSync as existsSync29, readdirSync as readdirSync10, statSync as statSync8 } from "node:fs";
|
|
3962
|
+
import { join as join35, relative as relative4 } from "node:path";
|
|
3963
|
+
|
|
3964
|
+
// src/commands.drop-session.git.ts
|
|
3965
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
3966
|
+
function expandStagedDir(dirRel, repo) {
|
|
3967
|
+
try {
|
|
3968
|
+
const out = execFileSync11("git", ["ls-files", "-z", "--", dirRel], {
|
|
3969
|
+
cwd: repo,
|
|
3970
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3971
|
+
});
|
|
3972
|
+
return out.toString().split("\0").filter((p) => p !== "");
|
|
3973
|
+
} catch {
|
|
3974
|
+
return [];
|
|
4032
3975
|
}
|
|
4033
|
-
|
|
4034
|
-
|
|
3976
|
+
}
|
|
3977
|
+
function isTrackedInHead(rel, repo) {
|
|
3978
|
+
try {
|
|
3979
|
+
execFileSync11("git", ["cat-file", "-e", `HEAD:${rel}`], {
|
|
3980
|
+
cwd: repo,
|
|
3981
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3982
|
+
});
|
|
3983
|
+
return true;
|
|
3984
|
+
} catch {
|
|
3985
|
+
return false;
|
|
4035
3986
|
}
|
|
4036
|
-
|
|
3987
|
+
}
|
|
3988
|
+
function isInIndex(rel, repo) {
|
|
3989
|
+
try {
|
|
3990
|
+
const out = execFileSync11("git", ["ls-files", "--", rel], {
|
|
3991
|
+
cwd: repo,
|
|
3992
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3993
|
+
});
|
|
3994
|
+
return out.toString().trim() !== "";
|
|
3995
|
+
} catch {
|
|
4037
3996
|
return false;
|
|
4038
3997
|
}
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
}
|
|
3998
|
+
}
|
|
3999
|
+
|
|
4000
|
+
// src/commands.drop-session.scrub-hint.ts
|
|
4001
|
+
init_config();
|
|
4002
|
+
init_utils();
|
|
4003
|
+
init_utils_json();
|
|
4004
|
+
import { existsSync as existsSync28 } from "node:fs";
|
|
4005
|
+
import { join as join34 } from "node:path";
|
|
4006
|
+
var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
|
|
4007
|
+
function reportScrubHint(id, matches) {
|
|
4008
|
+
const live = resolveLiveTranscript2(id, matches);
|
|
4009
|
+
const target = live ?? `~/.claude/projects/<encoded>/${id}.jsonl`;
|
|
4010
|
+
log(
|
|
4011
|
+
`note: this only un-stages the session from the next push.
|
|
4012
|
+
The local source still contains the secret, so nomad push re-stages it
|
|
4013
|
+
on the next run and nomad doctor --check-shared keeps reporting it.
|
|
4014
|
+
To fully remediate: rotate the credential, then run:
|
|
4015
|
+
nomad redact ${id}
|
|
4016
|
+
(or scrub ${target} manually)`
|
|
4017
|
+
);
|
|
4018
|
+
}
|
|
4019
|
+
function resolveLiveTranscript2(id, matches) {
|
|
4020
|
+
try {
|
|
4021
|
+
const mapPath = join34(repoHome(), "path-map.json");
|
|
4022
|
+
if (!existsSync28(mapPath)) return null;
|
|
4023
|
+
const projects = readJson(mapPath).projects;
|
|
4024
|
+
const claude = claudeHome();
|
|
4025
|
+
for (const rel of matches) {
|
|
4026
|
+
const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
|
|
4027
|
+
if (logical === void 0) continue;
|
|
4028
|
+
const abs = projects[logical]?.[HOST];
|
|
4029
|
+
if (abs === void 0) continue;
|
|
4030
|
+
const live = join34(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
4031
|
+
if (existsSync28(live)) return live;
|
|
4072
4032
|
}
|
|
4073
|
-
return
|
|
4033
|
+
return null;
|
|
4034
|
+
} catch {
|
|
4035
|
+
return null;
|
|
4074
4036
|
}
|
|
4075
|
-
}
|
|
4037
|
+
}
|
|
4076
4038
|
|
|
4077
|
-
//
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4039
|
+
// src/commands.drop-session.ts
|
|
4040
|
+
init_utils();
|
|
4041
|
+
function cmdDropSession(id) {
|
|
4042
|
+
if (id.length === 0 || id.length > 128 || !/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
4043
|
+
fail(`invalid session id: ${id}`);
|
|
4044
|
+
process.exit(1);
|
|
4082
4045
|
}
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
}
|
|
4091
|
-
} else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
|
|
4092
|
-
if (left.endsWith("\n")) {
|
|
4093
|
-
left = left.slice(0, -1);
|
|
4094
|
-
}
|
|
4095
|
-
if (right.endsWith("\n")) {
|
|
4096
|
-
right = right.slice(0, -1);
|
|
4097
|
-
}
|
|
4046
|
+
const repo = repoHome();
|
|
4047
|
+
if (!existsSync29(repo)) die(`repo not cloned at ${repo}`);
|
|
4048
|
+
const handle = acquireLock("drop-session");
|
|
4049
|
+
if (handle === null) process.exit(0);
|
|
4050
|
+
try {
|
|
4051
|
+
const repoProjects = join35(repo, "shared", "projects");
|
|
4052
|
+
if (!existsSync29(repoProjects)) {
|
|
4053
|
+
throw new NomadFatal(`no staged session matches ${id}`);
|
|
4098
4054
|
}
|
|
4099
|
-
|
|
4055
|
+
const matches = collectMatches(repoProjects, id, repo);
|
|
4056
|
+
if (matches.length === 0) {
|
|
4057
|
+
throw new NomadFatal(`no staged session matches ${id}`);
|
|
4058
|
+
}
|
|
4059
|
+
for (const rel of matches) unstageOne(rel, repo);
|
|
4060
|
+
reportScrubHint(id, matches);
|
|
4061
|
+
} catch (err) {
|
|
4062
|
+
if (!(err instanceof NomadFatal)) {
|
|
4063
|
+
throw err;
|
|
4064
|
+
}
|
|
4065
|
+
fail(err.message);
|
|
4066
|
+
process.exitCode = 1;
|
|
4067
|
+
} finally {
|
|
4068
|
+
releaseLock(handle);
|
|
4100
4069
|
}
|
|
4101
|
-
};
|
|
4102
|
-
var lineDiff = new LineDiff();
|
|
4103
|
-
function diffLines(oldStr, newStr, options) {
|
|
4104
|
-
return lineDiff.diff(oldStr, newStr, options);
|
|
4105
4070
|
}
|
|
4106
|
-
function
|
|
4107
|
-
|
|
4108
|
-
|
|
4071
|
+
function collectMatches(repoProjects, id, repo) {
|
|
4072
|
+
const matches = [];
|
|
4073
|
+
for (const logical of readdirSync10(repoProjects)) {
|
|
4074
|
+
const candidate = join35(repoProjects, logical, `${id}.jsonl`);
|
|
4075
|
+
if (existsSync29(candidate)) {
|
|
4076
|
+
matches.push(relative4(repo, candidate));
|
|
4077
|
+
}
|
|
4078
|
+
const dir = join35(repoProjects, logical, id);
|
|
4079
|
+
if (existsSync29(dir) && statSync8(dir).isDirectory()) {
|
|
4080
|
+
const dirRel = relative4(repo, dir);
|
|
4081
|
+
const staged = expandStagedDir(dirRel, repo);
|
|
4082
|
+
if (staged.length > 0) matches.push(...staged);
|
|
4083
|
+
else matches.push(dirRel);
|
|
4084
|
+
}
|
|
4109
4085
|
}
|
|
4110
|
-
|
|
4111
|
-
|
|
4112
|
-
|
|
4086
|
+
return matches;
|
|
4087
|
+
}
|
|
4088
|
+
function unstageOne(rel, repo) {
|
|
4089
|
+
if (!isInIndex(rel, repo)) {
|
|
4090
|
+
item(`dropped ${rel} (already absent from index)`);
|
|
4091
|
+
return;
|
|
4113
4092
|
}
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4093
|
+
try {
|
|
4094
|
+
if (isTrackedInHead(rel, repo)) {
|
|
4095
|
+
execFileSync12("git", ["restore", "--staged", "--worktree", "--", rel], {
|
|
4096
|
+
cwd: repo,
|
|
4097
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4098
|
+
});
|
|
4118
4099
|
} else {
|
|
4119
|
-
|
|
4100
|
+
execFileSync12("git", ["rm", "--cached", "-f", "--", rel], {
|
|
4101
|
+
cwd: repo,
|
|
4102
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4103
|
+
});
|
|
4120
4104
|
}
|
|
4105
|
+
} catch (err) {
|
|
4106
|
+
const e = err;
|
|
4107
|
+
const detail = e.stderr?.toString().trim() ?? e.message;
|
|
4108
|
+
throw new NomadFatal(`git failed to unstage ${rel}: ${detail}`);
|
|
4121
4109
|
}
|
|
4122
|
-
|
|
4110
|
+
item(`dropped ${rel}`);
|
|
4123
4111
|
}
|
|
4124
4112
|
|
|
4125
|
-
// src/
|
|
4113
|
+
// src/commands.pull.ts
|
|
4114
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync8 } from "node:fs";
|
|
4115
|
+
import { join as join41 } from "node:path";
|
|
4116
|
+
|
|
4117
|
+
// src/commands.push.sections.ts
|
|
4126
4118
|
init_color();
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
let prefix;
|
|
4136
|
-
if (part.removed) prefix = (line) => red(`-${line}`);
|
|
4137
|
-
else if (part.added) prefix = (line) => green(`+${line}`);
|
|
4138
|
-
else prefix = (line) => ` ${line}`;
|
|
4139
|
-
for (const line of partLines) {
|
|
4140
|
-
lines.push(prefix(line));
|
|
4119
|
+
|
|
4120
|
+
// src/summary.ts
|
|
4121
|
+
init_utils();
|
|
4122
|
+
function summaryText(verb, unmapped, collisions = 0, extrasSkipped = 0) {
|
|
4123
|
+
const extras = extrasSkipped > 0 ? `, ${extrasSkipped} extras skipped` : "";
|
|
4124
|
+
if (verb === "push") {
|
|
4125
|
+
if (unmapped === 0 && collisions === 0 && extrasSkipped === 0) {
|
|
4126
|
+
return { text: "summary: clean", clean: true };
|
|
4141
4127
|
}
|
|
4128
|
+
const base = `summary: ${unmapped} unmapped on push, ${collisions} collisions`;
|
|
4129
|
+
return { text: `${base}${extras} (run nomad doctor to list)`, clean: false };
|
|
4142
4130
|
}
|
|
4143
|
-
|
|
4131
|
+
if (unmapped === 0 && extrasSkipped === 0) {
|
|
4132
|
+
return { text: "summary: clean", clean: true };
|
|
4133
|
+
}
|
|
4134
|
+
return {
|
|
4135
|
+
text: `summary: ${unmapped} unmapped on ${verb}${extras} (run nomad doctor to list)`,
|
|
4136
|
+
clean: false
|
|
4137
|
+
};
|
|
4138
|
+
}
|
|
4139
|
+
function summaryRow(verb, unmapped, collisions = 0, extrasSkipped = 0) {
|
|
4140
|
+
const { text } = summaryText(verb, unmapped, collisions, extrasSkipped);
|
|
4141
|
+
return text.replace(/^summary: /, "");
|
|
4144
4142
|
}
|
|
4145
4143
|
|
|
4146
|
-
// src/
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4144
|
+
// src/commands.push.sections.ts
|
|
4145
|
+
function collapsedSkipRow(n, noun) {
|
|
4146
|
+
if (n <= 0) return null;
|
|
4147
|
+
return `${dim(infoGlyph)} ${n} ${noun}`;
|
|
4148
|
+
}
|
|
4149
|
+
function buildSettingsSection(label) {
|
|
4150
|
+
const s = section("Settings");
|
|
4151
|
+
addItem(s, `${green(okGlyph)} settings.json (base + ${label})`);
|
|
4152
|
+
return s;
|
|
4153
|
+
}
|
|
4154
|
+
function buildSessionsSection(items, unmapped) {
|
|
4155
|
+
const s = section("Sessions");
|
|
4156
|
+
for (const logical of items) addItem(s, `${green(okGlyph)} ${logical}`);
|
|
4157
|
+
const skip = collapsedSkipRow(unmapped, "not in path-map (run nomad doctor to list)");
|
|
4158
|
+
if (skip !== null) addItem(s, skip);
|
|
4159
|
+
return s;
|
|
4160
|
+
}
|
|
4161
|
+
function buildExtrasSection(items, extrasSkipped) {
|
|
4162
|
+
const s = section("Extras");
|
|
4163
|
+
for (const entry of items) addItem(s, `${green(okGlyph)} ${entry}`);
|
|
4164
|
+
const skip = collapsedSkipRow(extrasSkipped, "extras skipped");
|
|
4165
|
+
if (skip !== null) addItem(s, skip);
|
|
4166
|
+
return s;
|
|
4167
|
+
}
|
|
4168
|
+
function syncedSections(st) {
|
|
4169
|
+
const sessions = st.dryRun ? st.remap.wouldPush : st.remap.pushed;
|
|
4170
|
+
const extras = st.dryRun ? st.extras.wouldPush : st.extras.pushed;
|
|
4171
|
+
return [
|
|
4172
|
+
buildSessionsSection(sessions, st.remap.unmapped),
|
|
4173
|
+
buildExtrasSection(extras, st.extras.skipped)
|
|
4155
4174
|
];
|
|
4156
|
-
return lines.join("\n");
|
|
4157
4175
|
}
|
|
4158
|
-
function
|
|
4159
|
-
|
|
4176
|
+
function summarySection(st) {
|
|
4177
|
+
const s = section("Summary");
|
|
4178
|
+
const unmapped = st.remap.unmapped + st.extras.unmapped;
|
|
4179
|
+
addItem(s, summaryRow("push", unmapped, st.remap.collisions, st.extras.skipped));
|
|
4180
|
+
return s;
|
|
4181
|
+
}
|
|
4182
|
+
function renderPushTree(st, verdict) {
|
|
4183
|
+
const leakScan = section("Leak scan");
|
|
4184
|
+
addItem(leakScan, verdict.verdictRow);
|
|
4185
|
+
renderTree([...syncedSections(st), leakScan, summarySection(st)]);
|
|
4186
|
+
}
|
|
4187
|
+
function renderNoScanTree(st, opts = {}) {
|
|
4188
|
+
const sections = [];
|
|
4189
|
+
if (opts.noMapHint === true) {
|
|
4190
|
+
const pathMap = section("Path map");
|
|
4191
|
+
addItem(pathMap, `${dim(infoGlyph)} no path-map.json (nothing to preview)`);
|
|
4192
|
+
sections.push(pathMap);
|
|
4193
|
+
}
|
|
4194
|
+
renderTree([...sections, ...syncedSections(st), summarySection(st)]);
|
|
4195
|
+
}
|
|
4196
|
+
|
|
4197
|
+
// src/commands.pull.ts
|
|
4198
|
+
init_config();
|
|
4199
|
+
|
|
4200
|
+
// src/extras-sync.ts
|
|
4201
|
+
init_config();
|
|
4202
|
+
import { existsSync as existsSync32 } from "node:fs";
|
|
4203
|
+
import { join as join38 } from "node:path";
|
|
4204
|
+
|
|
4205
|
+
// src/extras-sync.diff.ts
|
|
4206
|
+
init_utils();
|
|
4207
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
4208
|
+
function labelDiffLine(line) {
|
|
4209
|
+
const tab = line.indexOf(" ");
|
|
4210
|
+
if (tab === -1) return line;
|
|
4211
|
+
const status = line.slice(0, tab);
|
|
4212
|
+
const path = line.slice(tab + 1);
|
|
4213
|
+
if (status === "D") return `${path} (local only)`;
|
|
4214
|
+
if (status === "A") return `${path} (repo only)`;
|
|
4215
|
+
return path;
|
|
4216
|
+
}
|
|
4217
|
+
function parseDiffOutput(stdout) {
|
|
4218
|
+
return stdout.split("\n").filter((line) => line.length > 0).map(labelDiffLine);
|
|
4219
|
+
}
|
|
4220
|
+
function listDivergingFiles(a, b) {
|
|
4160
4221
|
try {
|
|
4161
|
-
|
|
4162
|
-
|
|
4163
|
-
|
|
4222
|
+
const stdout = execFileSync13("git", ["diff", "--no-index", "--name-status", a, b], {
|
|
4223
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4224
|
+
}).toString();
|
|
4225
|
+
return parseDiffOutput(stdout);
|
|
4226
|
+
} catch (err) {
|
|
4227
|
+
const e = err;
|
|
4228
|
+
if (e.status === 1 && e.stdout !== void 0) {
|
|
4229
|
+
return parseDiffOutput(e.stdout.toString());
|
|
4230
|
+
}
|
|
4231
|
+
if (e.code === "ENOENT") {
|
|
4232
|
+
warn(`git not on PATH; divergence check skipped for ${a}`);
|
|
4233
|
+
return [];
|
|
4234
|
+
}
|
|
4235
|
+
warn(`divergence check failed for ${a}: ${e.message ?? String(err)}`);
|
|
4236
|
+
return [];
|
|
4164
4237
|
}
|
|
4165
4238
|
}
|
|
4166
|
-
|
|
4167
|
-
|
|
4168
|
-
|
|
4169
|
-
|
|
4239
|
+
|
|
4240
|
+
// src/extras-sync.core.ts
|
|
4241
|
+
init_config();
|
|
4242
|
+
import { cpSync as cpSync6, existsSync as existsSync30, rmSync as rmSync10 } from "node:fs";
|
|
4243
|
+
import { join as join36 } from "node:path";
|
|
4244
|
+
|
|
4245
|
+
// src/extras-sync.guards.ts
|
|
4246
|
+
init_utils();
|
|
4247
|
+
init_config_sharedDirs_guard();
|
|
4248
|
+
import { isAbsolute as isAbsolute2, normalize } from "node:path";
|
|
4249
|
+
function assertSafeLocalRoot(localRoot, logical) {
|
|
4250
|
+
if (!isAbsolute2(localRoot)) {
|
|
4251
|
+
throw new NomadFatal(
|
|
4252
|
+
`invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
|
|
4253
|
+
);
|
|
4170
4254
|
}
|
|
4171
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4255
|
+
if (localRoot !== normalize(localRoot)) {
|
|
4256
|
+
throw new NomadFatal(
|
|
4257
|
+
`invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
|
|
4258
|
+
);
|
|
4175
4259
|
}
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4260
|
+
}
|
|
4261
|
+
|
|
4262
|
+
// src/extras-sync.core.ts
|
|
4263
|
+
init_utils();
|
|
4264
|
+
init_utils_json();
|
|
4265
|
+
function loadValidatedExtras(opts) {
|
|
4266
|
+
const repo = repoHome();
|
|
4267
|
+
const mapPath = join36(repo, "path-map.json");
|
|
4268
|
+
const repoExtras = join36(repo, "shared", "extras");
|
|
4269
|
+
if (!existsSync30(mapPath) || opts.requireRepoExtras === true && !existsSync30(repoExtras)) {
|
|
4270
|
+
if (opts.missingMsg !== void 0) log(opts.missingMsg);
|
|
4271
|
+
return null;
|
|
4180
4272
|
}
|
|
4181
|
-
const
|
|
4182
|
-
const
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4273
|
+
const map = readPathMap(mapPath);
|
|
4274
|
+
const extrasMap = map.extras ?? {};
|
|
4275
|
+
if (Object.keys(extrasMap).length === 0) return null;
|
|
4276
|
+
for (const logical of Object.keys(extrasMap)) {
|
|
4277
|
+
assertSafeLogical(logical);
|
|
4278
|
+
const localRoot = map.projects[logical]?.[HOST];
|
|
4279
|
+
if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
|
|
4280
|
+
}
|
|
4281
|
+
return { map, extrasMap };
|
|
4188
4282
|
}
|
|
4189
|
-
function
|
|
4190
|
-
|
|
4283
|
+
function* eachExtrasTarget(v, counts) {
|
|
4284
|
+
const whitelist = SUPPORTED_EXTRAS;
|
|
4285
|
+
for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
|
|
4286
|
+
const localRoot = v.map.projects[logical]?.[HOST];
|
|
4287
|
+
if (!localRoot || localRoot === "TBD") {
|
|
4288
|
+
counts.unmapped++;
|
|
4289
|
+
continue;
|
|
4290
|
+
}
|
|
4291
|
+
for (const dirname7 of dirnames) {
|
|
4292
|
+
if (!whitelist.includes(dirname7)) {
|
|
4293
|
+
counts.skipped++;
|
|
4294
|
+
continue;
|
|
4295
|
+
}
|
|
4296
|
+
yield { logical, localRoot, dirname: dirname7 };
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4191
4299
|
}
|
|
4192
|
-
function
|
|
4193
|
-
|
|
4300
|
+
function copyExtras(src, dst) {
|
|
4301
|
+
rmSync10(dst, { recursive: true, force: true });
|
|
4302
|
+
cpSync6(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
|
|
4194
4303
|
}
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4304
|
+
|
|
4305
|
+
// src/extras-sync.ts
|
|
4306
|
+
init_utils();
|
|
4307
|
+
init_utils_json();
|
|
4308
|
+
|
|
4309
|
+
// src/extras-sync.remap.ts
|
|
4310
|
+
init_config();
|
|
4311
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync7 } from "node:fs";
|
|
4312
|
+
import { join as join37 } from "node:path";
|
|
4313
|
+
init_utils_fs();
|
|
4314
|
+
function runExtrasOp(v, dryRun, paths, backup) {
|
|
4315
|
+
const counts = { unmapped: 0, skipped: 0 };
|
|
4316
|
+
const done = [];
|
|
4317
|
+
const would = [];
|
|
4318
|
+
for (const t of eachExtrasTarget(v, counts)) {
|
|
4319
|
+
const { src, dst } = paths(t);
|
|
4320
|
+
if (!existsSync31(src)) continue;
|
|
4321
|
+
const item2 = `${t.logical}/${t.dirname}`;
|
|
4322
|
+
if (dryRun) {
|
|
4323
|
+
would.push(item2);
|
|
4324
|
+
continue;
|
|
4200
4325
|
}
|
|
4326
|
+
backup(dst, t.localRoot);
|
|
4327
|
+
copyExtras(src, dst);
|
|
4328
|
+
done.push(item2);
|
|
4201
4329
|
}
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
4330
|
+
return { ...counts, done, would };
|
|
4331
|
+
}
|
|
4332
|
+
function remapExtrasPush(ts, opts = {}) {
|
|
4333
|
+
const dryRun = opts.dryRun === true;
|
|
4334
|
+
const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
|
|
4335
|
+
if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
|
|
4336
|
+
const repo = repoHome();
|
|
4337
|
+
const repoExtras = join37(repo, "shared", "extras");
|
|
4338
|
+
if (!dryRun) mkdirSync7(repoExtras, { recursive: true });
|
|
4339
|
+
const { unmapped, skipped, done, would } = runExtrasOp(
|
|
4340
|
+
v,
|
|
4341
|
+
dryRun,
|
|
4342
|
+
({ localRoot, logical, dirname: dirname7 }) => ({
|
|
4343
|
+
src: join37(localRoot, dirname7),
|
|
4344
|
+
dst: join37(repoExtras, logical, dirname7)
|
|
4345
|
+
}),
|
|
4346
|
+
(dst) => backupRepoWrite(dst, ts, repo)
|
|
4347
|
+
);
|
|
4348
|
+
return { unmapped, skipped, pushed: done, wouldPush: would };
|
|
4206
4349
|
}
|
|
4207
|
-
function
|
|
4208
|
-
const
|
|
4209
|
-
const
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
const links = section("Symlinks");
|
|
4213
|
-
applySharedLinks(ts, map, {
|
|
4214
|
-
dryRun: true,
|
|
4215
|
-
onPreview: (e) => addItem(links, formatLinkRow(e))
|
|
4350
|
+
function remapExtrasPull(ts, opts = {}) {
|
|
4351
|
+
const dryRun = opts.dryRun === true;
|
|
4352
|
+
const v = loadValidatedExtras({
|
|
4353
|
+
requireRepoExtras: true,
|
|
4354
|
+
missingMsg: "no path-map or repo extras dir; skipping extras remap"
|
|
4216
4355
|
});
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4356
|
+
if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
|
|
4357
|
+
const repoExtras = join37(repoHome(), "shared", "extras");
|
|
4358
|
+
const { unmapped, skipped, done, would } = runExtrasOp(
|
|
4359
|
+
v,
|
|
4360
|
+
dryRun,
|
|
4361
|
+
({ localRoot, logical, dirname: dirname7 }) => ({
|
|
4362
|
+
src: join37(repoExtras, logical, dirname7),
|
|
4363
|
+
dst: join37(localRoot, dirname7)
|
|
4364
|
+
}),
|
|
4365
|
+
// Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
|
|
4366
|
+
// localRoot so the backup tree mirrors the project layout.
|
|
4367
|
+
(dst, localRoot) => backupExtrasWrite(dst, ts, localRoot)
|
|
4221
4368
|
);
|
|
4222
|
-
|
|
4223
|
-
const sessions = section("Sessions");
|
|
4224
|
-
const remapResult = remapPull(ts, {
|
|
4225
|
-
dryRun: true,
|
|
4226
|
-
onPreview: (e) => addItem(sessions, formatSessionRow(e))
|
|
4227
|
-
});
|
|
4228
|
-
const summary = section("Summary");
|
|
4229
|
-
addItem(summary, summaryRow(verb, remapResult.unmapped));
|
|
4230
|
-
renderTree([links, settingsSection, sessions, summary]);
|
|
4231
|
-
return { unmapped: remapResult.unmapped, collisions: 0 };
|
|
4369
|
+
return { unmapped, skipped, pulled: done, wouldPull: would };
|
|
4232
4370
|
}
|
|
4233
4371
|
|
|
4234
|
-
// src/
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4372
|
+
// src/extras-sync.ts
|
|
4373
|
+
function divergenceCheckExtras(ts) {
|
|
4374
|
+
const v = loadValidatedExtras({});
|
|
4375
|
+
if (v === null) return;
|
|
4376
|
+
const counts = { unmapped: 0, skipped: 0 };
|
|
4377
|
+
const backupRoot = join38(backupBase(), ts, "extras");
|
|
4378
|
+
const repo = repoHome();
|
|
4379
|
+
for (const { logical, localRoot, dirname: dirname7 } of eachExtrasTarget(v, counts)) {
|
|
4380
|
+
const local = join38(localRoot, dirname7);
|
|
4381
|
+
const repoEntry = join38(repo, "shared", "extras", logical, dirname7);
|
|
4382
|
+
if (!existsSync32(local) || !existsSync32(repoEntry)) continue;
|
|
4383
|
+
const diff = listDivergingFiles(local, repoEntry);
|
|
4384
|
+
if (diff.length === 0) continue;
|
|
4385
|
+
const projectBackupRoot = join38(backupRoot, encodePath(localRoot));
|
|
4386
|
+
warn(
|
|
4387
|
+
`local ${dirname7} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
|
|
4388
|
+
);
|
|
4389
|
+
for (const f of diff) warn(` ${f}`);
|
|
4390
|
+
}
|
|
4391
|
+
}
|
|
4245
4392
|
|
|
4246
|
-
// src/
|
|
4393
|
+
// src/links.ts
|
|
4247
4394
|
init_config();
|
|
4248
|
-
init_config_sharedDirs_guard();
|
|
4249
|
-
import { cpSync as cpSync6, existsSync as existsSync32, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
|
|
4250
|
-
import { dirname as dirname6, join as join37, sep as sep3 } from "node:path";
|
|
4251
|
-
init_push_gitleaks_scan();
|
|
4252
|
-
init_utils_json();
|
|
4253
4395
|
init_utils();
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4396
|
+
init_utils_fs();
|
|
4397
|
+
init_utils_json();
|
|
4398
|
+
import { existsSync as existsSync33, lstatSync as lstatSync8, rmSync as rmSync11 } from "node:fs";
|
|
4399
|
+
import { join as join39 } from "node:path";
|
|
4400
|
+
function emitAutoMove(onPreview, linkPath, ts, name) {
|
|
4401
|
+
if (onPreview) {
|
|
4402
|
+
onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
|
|
4403
|
+
} else {
|
|
4404
|
+
log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
|
|
4405
|
+
}
|
|
4259
4406
|
}
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4407
|
+
function emitCreate(onPreview, from, to) {
|
|
4408
|
+
if (onPreview) {
|
|
4409
|
+
onPreview({ kind: "create", from, to });
|
|
4410
|
+
} else {
|
|
4411
|
+
log(`would create symlink: ${from} -> ${to}`);
|
|
4412
|
+
}
|
|
4266
4413
|
}
|
|
4267
|
-
function
|
|
4268
|
-
|
|
4269
|
-
if (t === "r" || t === "redact") return "redact";
|
|
4270
|
-
if (t === "a" || t === "allow") return "allow";
|
|
4271
|
-
if (t === "d" || t === "drop") return "drop";
|
|
4272
|
-
return "skip";
|
|
4414
|
+
function isAlreadySymlink(linkPath) {
|
|
4415
|
+
return existsSync33(linkPath) && lstatSync8(linkPath).isSymbolicLink();
|
|
4273
4416
|
}
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
if (
|
|
4281
|
-
if (
|
|
4282
|
-
|
|
4417
|
+
function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
|
|
4418
|
+
for (const name of linkNames) {
|
|
4419
|
+
const linkPath = join39(claude, name);
|
|
4420
|
+
const target = join39(repo, "shared", name);
|
|
4421
|
+
if (!existsSync33(linkPath)) continue;
|
|
4422
|
+
if (lstatSync8(linkPath).isSymbolicLink()) continue;
|
|
4423
|
+
if (!existsSync33(target)) continue;
|
|
4424
|
+
if (dryRun) {
|
|
4425
|
+
emitAutoMove(onPreview, linkPath, ts, name);
|
|
4426
|
+
continue;
|
|
4283
4427
|
}
|
|
4428
|
+
backupBeforeWrite(linkPath, ts);
|
|
4429
|
+
rmSync11(linkPath, { recursive: true, force: true });
|
|
4284
4430
|
}
|
|
4285
|
-
return null;
|
|
4286
4431
|
}
|
|
4287
|
-
function
|
|
4288
|
-
const
|
|
4289
|
-
log(msg);
|
|
4290
|
-
return false;
|
|
4291
|
-
};
|
|
4432
|
+
function applySharedLinks(ts, map, opts = {}) {
|
|
4433
|
+
const dryRun = opts.dryRun === true;
|
|
4292
4434
|
const claude = claudeHome();
|
|
4293
4435
|
const repo = repoHome();
|
|
4294
|
-
const
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
);
|
|
4299
|
-
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
const sessionDir = join37(dirname6(localPath), sid);
|
|
4307
|
-
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
4308
|
-
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
|
|
4309
|
-
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
4310
|
-
return refuse(
|
|
4311
|
-
`session ${sid} looks active (modified within the last 5 minutes); refusing to redact, no changes made.
|
|
4312
|
-
End the session and choose Redact again, or choose Drop session (holds this session back from the push, local copy kept) or Skip.`
|
|
4313
|
-
);
|
|
4314
|
-
}
|
|
4315
|
-
const stagedProjectDir = resolveStagedDir(localPath, map, claude, repo);
|
|
4316
|
-
if (stagedProjectDir === null) {
|
|
4317
|
-
return refuse(
|
|
4318
|
-
`could not map the local transcript for session ${sid} to a staged copy; choose Drop session or Skip.`
|
|
4319
|
-
);
|
|
4436
|
+
const linkNames = allSharedLinks(map);
|
|
4437
|
+
runAutoMovePasses(linkNames, claude, repo, ts, dryRun, opts.onPreview);
|
|
4438
|
+
for (const name of linkNames) {
|
|
4439
|
+
const target = join39(repo, "shared", name);
|
|
4440
|
+
if (!existsSync33(target)) continue;
|
|
4441
|
+
const linkPath = join39(claude, name);
|
|
4442
|
+
if (isAlreadySymlink(linkPath)) continue;
|
|
4443
|
+
if (dryRun) {
|
|
4444
|
+
emitCreate(opts.onPreview, linkPath, target);
|
|
4445
|
+
continue;
|
|
4446
|
+
}
|
|
4447
|
+
ensureSymlink(linkPath, target);
|
|
4320
4448
|
}
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4449
|
+
}
|
|
4450
|
+
function regenerateSettings(ts, opts = {}) {
|
|
4451
|
+
const dryRun = opts.dryRun === true;
|
|
4452
|
+
const repo = repoHome();
|
|
4453
|
+
const claude = claudeHome();
|
|
4454
|
+
const basePath = join39(repo, "shared", "settings.base.json");
|
|
4455
|
+
const hostPath = join39(repo, "hosts", `${HOST}.json`);
|
|
4456
|
+
if (!existsSync33(basePath)) {
|
|
4457
|
+
die("repo not initialized; run 'nomad init' to scaffold");
|
|
4324
4458
|
}
|
|
4325
|
-
const
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4459
|
+
const base = readJson(basePath);
|
|
4460
|
+
const hasOverrides = existsSync33(hostPath);
|
|
4461
|
+
const overrides = hasOverrides ? readJson(hostPath) : {};
|
|
4462
|
+
const merged = deepMerge(base, overrides);
|
|
4463
|
+
const settingsPath = join39(claude, "settings.json");
|
|
4464
|
+
if (!hasOverrides && existsSync33(settingsPath)) {
|
|
4465
|
+
try {
|
|
4466
|
+
const existing = readJson(settingsPath);
|
|
4467
|
+
const baseKeys = new Set(Object.keys(base));
|
|
4468
|
+
const drift = Object.keys(existing).filter((k) => !baseKeys.has(k));
|
|
4469
|
+
if (drift.length > 0) {
|
|
4470
|
+
warn(
|
|
4471
|
+
`no hosts/${HOST}.json found; existing settings has unbased keys ${JSON.stringify(drift)}. Set NOMAD_HOST to match a hosts/*.json or rerun 'nomad doctor' for candidates.`
|
|
4472
|
+
);
|
|
4473
|
+
}
|
|
4474
|
+
} catch {
|
|
4475
|
+
warn("existing settings.json is malformed; skipping drift-check and regenerating.");
|
|
4476
|
+
}
|
|
4338
4477
|
}
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4478
|
+
const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
|
|
4479
|
+
if (dryRun) {
|
|
4480
|
+
log(`would write settings.json (base + ${overrideLabel})`);
|
|
4481
|
+
return { label: overrideLabel };
|
|
4343
4482
|
}
|
|
4344
|
-
|
|
4483
|
+
backupBeforeWrite(settingsPath, ts);
|
|
4484
|
+
writeJsonAtomic(settingsPath, merged);
|
|
4485
|
+
return { label: overrideLabel };
|
|
4345
4486
|
}
|
|
4346
4487
|
|
|
4347
|
-
// src/
|
|
4488
|
+
// src/preview.ts
|
|
4348
4489
|
init_config();
|
|
4349
|
-
import {
|
|
4350
|
-
import { join as
|
|
4351
|
-
function dropSessionFromStaged(sid, map) {
|
|
4352
|
-
const logicals = Object.keys(map.projects);
|
|
4353
|
-
if (logicals.length === 0) return false;
|
|
4354
|
-
const repo = repoHome();
|
|
4355
|
-
for (const logical of logicals) {
|
|
4356
|
-
const jsonl = join38(repo, "shared", "projects", logical, `${sid}.jsonl`);
|
|
4357
|
-
const dir = join38(repo, "shared", "projects", logical, sid);
|
|
4358
|
-
rmSync10(jsonl, { force: true });
|
|
4359
|
-
rmSync10(dir, { recursive: true, force: true });
|
|
4360
|
-
}
|
|
4361
|
-
return true;
|
|
4362
|
-
}
|
|
4490
|
+
import { existsSync as existsSync34 } from "node:fs";
|
|
4491
|
+
import { join as join40 } from "node:path";
|
|
4363
4492
|
|
|
4364
|
-
//
|
|
4365
|
-
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4493
|
+
// node_modules/diff/libesm/diff/base.js
|
|
4494
|
+
var Diff = class {
|
|
4495
|
+
diff(oldStr, newStr, options = {}) {
|
|
4496
|
+
let callback;
|
|
4497
|
+
if (typeof options === "function") {
|
|
4498
|
+
callback = options;
|
|
4499
|
+
options = {};
|
|
4500
|
+
} else if ("callback" in options) {
|
|
4501
|
+
callback = options.callback;
|
|
4502
|
+
}
|
|
4503
|
+
const oldString = this.castInput(oldStr, options);
|
|
4504
|
+
const newString = this.castInput(newStr, options);
|
|
4505
|
+
const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
|
|
4506
|
+
const newTokens = this.removeEmpty(this.tokenize(newString, options));
|
|
4507
|
+
return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
|
|
4373
4508
|
}
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4509
|
+
diffWithOptionsObj(oldTokens, newTokens, options, callback) {
|
|
4510
|
+
var _a;
|
|
4511
|
+
const done = (value) => {
|
|
4512
|
+
value = this.postProcess(value, options);
|
|
4513
|
+
if (callback) {
|
|
4514
|
+
setTimeout(function() {
|
|
4515
|
+
callback(value);
|
|
4516
|
+
}, 0);
|
|
4517
|
+
return void 0;
|
|
4518
|
+
} else {
|
|
4519
|
+
return value;
|
|
4520
|
+
}
|
|
4521
|
+
};
|
|
4522
|
+
const newLen = newTokens.length, oldLen = oldTokens.length;
|
|
4523
|
+
let editLength = 1;
|
|
4524
|
+
let maxEditLength = newLen + oldLen;
|
|
4525
|
+
if (options.maxEditLength != null) {
|
|
4526
|
+
maxEditLength = Math.min(maxEditLength, options.maxEditLength);
|
|
4527
|
+
}
|
|
4528
|
+
const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
|
|
4529
|
+
const abortAfterTimestamp = Date.now() + maxExecutionTime;
|
|
4530
|
+
const bestPath = [{ oldPos: -1, lastComponent: void 0 }];
|
|
4531
|
+
let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
|
|
4532
|
+
if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
4533
|
+
return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
|
|
4534
|
+
}
|
|
4535
|
+
let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
|
|
4536
|
+
const execEditLength = () => {
|
|
4537
|
+
for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
|
|
4538
|
+
let basePath;
|
|
4539
|
+
const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
|
|
4540
|
+
if (removePath) {
|
|
4541
|
+
bestPath[diagonalPath - 1] = void 0;
|
|
4542
|
+
}
|
|
4543
|
+
let canAdd = false;
|
|
4544
|
+
if (addPath) {
|
|
4545
|
+
const addPathNewPos = addPath.oldPos - diagonalPath;
|
|
4546
|
+
canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
|
|
4547
|
+
}
|
|
4548
|
+
const canRemove = removePath && removePath.oldPos + 1 < oldLen;
|
|
4549
|
+
if (!canAdd && !canRemove) {
|
|
4550
|
+
bestPath[diagonalPath] = void 0;
|
|
4551
|
+
continue;
|
|
4552
|
+
}
|
|
4553
|
+
if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
|
|
4554
|
+
basePath = this.addToPath(addPath, true, false, 0, options);
|
|
4555
|
+
} else {
|
|
4556
|
+
basePath = this.addToPath(removePath, false, true, 1, options);
|
|
4557
|
+
}
|
|
4558
|
+
newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
|
|
4559
|
+
if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
4560
|
+
return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
|
|
4561
|
+
} else {
|
|
4562
|
+
bestPath[diagonalPath] = basePath;
|
|
4563
|
+
if (basePath.oldPos + 1 >= oldLen) {
|
|
4564
|
+
maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
|
|
4565
|
+
}
|
|
4566
|
+
if (newPos + 1 >= newLen) {
|
|
4567
|
+
minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
|
|
4568
|
+
}
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4571
|
+
editLength++;
|
|
4572
|
+
};
|
|
4573
|
+
if (callback) {
|
|
4574
|
+
(function exec() {
|
|
4575
|
+
setTimeout(function() {
|
|
4576
|
+
if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
|
|
4577
|
+
return callback(void 0);
|
|
4578
|
+
}
|
|
4579
|
+
if (!execEditLength()) {
|
|
4580
|
+
exec();
|
|
4581
|
+
}
|
|
4582
|
+
}, 0);
|
|
4583
|
+
})();
|
|
4584
|
+
} else {
|
|
4585
|
+
while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
|
|
4586
|
+
const ret = execEditLength();
|
|
4587
|
+
if (ret) {
|
|
4588
|
+
return ret;
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4381
4591
|
}
|
|
4382
4592
|
}
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
}
|
|
4395
|
-
|
|
4396
|
-
const action = ctx.actions.get(findingKey(f)) ?? "skip";
|
|
4397
|
-
if (action === "skip") return;
|
|
4398
|
-
const sid = sessionIdFromFinding(f);
|
|
4399
|
-
if (sid !== null && ctx.droppedSids.has(sid)) return;
|
|
4400
|
-
if (action === "allow") {
|
|
4401
|
-
applyAllow(f, ctx.repo);
|
|
4402
|
-
return;
|
|
4593
|
+
addToPath(path, added, removed, oldPosInc, options) {
|
|
4594
|
+
const last = path.lastComponent;
|
|
4595
|
+
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
4596
|
+
return {
|
|
4597
|
+
oldPos: path.oldPos + oldPosInc,
|
|
4598
|
+
lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
|
|
4599
|
+
};
|
|
4600
|
+
} else {
|
|
4601
|
+
return {
|
|
4602
|
+
oldPos: path.oldPos + oldPosInc,
|
|
4603
|
+
lastComponent: { count: 1, added, removed, previousComponent: last }
|
|
4604
|
+
};
|
|
4605
|
+
}
|
|
4403
4606
|
}
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4607
|
+
extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
|
|
4608
|
+
const newLen = newTokens.length, oldLen = oldTokens.length;
|
|
4609
|
+
let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
|
|
4610
|
+
while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
|
|
4611
|
+
newPos++;
|
|
4612
|
+
oldPos++;
|
|
4613
|
+
commonCount++;
|
|
4614
|
+
if (options.oneChangePerToken) {
|
|
4615
|
+
basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
|
|
4616
|
+
}
|
|
4411
4617
|
}
|
|
4412
|
-
|
|
4618
|
+
if (commonCount && !options.oneChangePerToken) {
|
|
4619
|
+
basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
|
|
4620
|
+
}
|
|
4621
|
+
basePath.oldPos = oldPos;
|
|
4622
|
+
return newPos;
|
|
4413
4623
|
}
|
|
4414
|
-
|
|
4415
|
-
if (
|
|
4624
|
+
equals(left, right, options) {
|
|
4625
|
+
if (options.comparator) {
|
|
4626
|
+
return options.comparator(left, right);
|
|
4627
|
+
} else {
|
|
4628
|
+
return left === right || !!options.ignoreCase && left.toLowerCase() === right.toLowerCase();
|
|
4629
|
+
}
|
|
4416
4630
|
}
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
4425
|
-
repo,
|
|
4426
|
-
scan,
|
|
4427
|
-
drop,
|
|
4428
|
-
redactedSids: /* @__PURE__ */ new Set(),
|
|
4429
|
-
droppedSids: /* @__PURE__ */ new Set()
|
|
4430
|
-
};
|
|
4431
|
-
for (const f of findings) {
|
|
4432
|
-
dispatchOne(f, ctx);
|
|
4631
|
+
removeEmpty(array) {
|
|
4632
|
+
const ret = [];
|
|
4633
|
+
for (let i = 0; i < array.length; i++) {
|
|
4634
|
+
if (array[i]) {
|
|
4635
|
+
ret.push(array[i]);
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
return ret;
|
|
4433
4639
|
}
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
for (const f of findings) {
|
|
4438
|
-
const sid = sessionIdFromFinding(f);
|
|
4439
|
-
if (sid === null || redactedSids.has(sid)) continue;
|
|
4440
|
-
if (applyRedact(f, ts, map, nowMs, scan)) redactedSids.add(sid);
|
|
4640
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4641
|
+
castInput(value, options) {
|
|
4642
|
+
return value;
|
|
4441
4643
|
}
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
init_push_gitleaks_scan();
|
|
4446
|
-
init_push_gitleaks();
|
|
4447
|
-
init_utils();
|
|
4448
|
-
function isTTY(stdin = process.stdin, stdout = process.stdout) {
|
|
4449
|
-
return stdin.isTTY === true && stdout.isTTY === true;
|
|
4450
|
-
}
|
|
4451
|
-
function hasUnresolved(actions) {
|
|
4452
|
-
for (const action of actions.values()) {
|
|
4453
|
-
if (action === "skip") return true;
|
|
4644
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4645
|
+
tokenize(value, options) {
|
|
4646
|
+
return Array.from(value);
|
|
4454
4647
|
}
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
function printRecoveryLegend(print = console.log) {
|
|
4458
|
-
print("");
|
|
4459
|
-
print("Recovery actions:");
|
|
4460
|
-
print(" Redact - scrub the secret from the local transcript, push the cleaned copy");
|
|
4461
|
-
print(" Allow - mark as false positive (adds a .gitleaksignore fingerprint), push as-is");
|
|
4462
|
-
print(" Drop session - exclude this session from this push (local transcript kept, running");
|
|
4463
|
-
print(" session is not stopped)");
|
|
4464
|
-
print(" Skip - leave unresolved (the push aborts)");
|
|
4465
|
-
print("");
|
|
4466
|
-
}
|
|
4467
|
-
function applyThenRescan(scanVerdict, repoHome2) {
|
|
4468
|
-
gitOrFatal(["add", "-A"], "git add", repoHome2);
|
|
4469
|
-
const next = scanVerdict(repoHome2);
|
|
4470
|
-
if (next.leak) {
|
|
4471
|
-
const { bySession, other } = partitionFindings(next.findings);
|
|
4472
|
-
throw new NomadFatal(buildSessionAwareFatal(bySession, other));
|
|
4648
|
+
join(chars) {
|
|
4649
|
+
return chars.join("");
|
|
4473
4650
|
}
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
function allowThenRescan(append, scanVerdict, repoHome2) {
|
|
4477
|
-
const ignPath = join39(repoHome2, ".gitleaksignore");
|
|
4478
|
-
let before;
|
|
4479
|
-
try {
|
|
4480
|
-
before = readFileSync11(ignPath, "utf8");
|
|
4481
|
-
} catch {
|
|
4482
|
-
before = null;
|
|
4651
|
+
postProcess(changeObjects, options) {
|
|
4652
|
+
return changeObjects;
|
|
4483
4653
|
}
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
return applyThenRescan(scanVerdict, repoHome2);
|
|
4487
|
-
} catch (err) {
|
|
4488
|
-
if (before === null) rmSync11(ignPath, { force: true });
|
|
4489
|
-
else writeFileSync5(ignPath, before, "utf8");
|
|
4490
|
-
throw err;
|
|
4654
|
+
get useLongestToken() {
|
|
4655
|
+
return false;
|
|
4491
4656
|
}
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
try {
|
|
4501
|
-
return await rl.question(prompt);
|
|
4502
|
-
} finally {
|
|
4503
|
-
rl.close();
|
|
4657
|
+
buildValues(lastComponent, newTokens, oldTokens) {
|
|
4658
|
+
const components = [];
|
|
4659
|
+
let nextComponent;
|
|
4660
|
+
while (lastComponent) {
|
|
4661
|
+
components.push(lastComponent);
|
|
4662
|
+
nextComponent = lastComponent.previousComponent;
|
|
4663
|
+
delete lastComponent.previousComponent;
|
|
4664
|
+
lastComponent = nextComponent;
|
|
4504
4665
|
}
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4666
|
+
components.reverse();
|
|
4667
|
+
const componentLen = components.length;
|
|
4668
|
+
let componentPos = 0, newPos = 0, oldPos = 0;
|
|
4669
|
+
for (; componentPos < componentLen; componentPos++) {
|
|
4670
|
+
const component = components[componentPos];
|
|
4671
|
+
if (!component.removed) {
|
|
4672
|
+
if (!component.added && this.useLongestToken) {
|
|
4673
|
+
let value = newTokens.slice(newPos, newPos + component.count);
|
|
4674
|
+
value = value.map(function(value2, i) {
|
|
4675
|
+
const oldValue = oldTokens[oldPos + i];
|
|
4676
|
+
return oldValue.length > value2.length ? oldValue : value2;
|
|
4677
|
+
});
|
|
4678
|
+
component.value = this.join(value);
|
|
4679
|
+
} else {
|
|
4680
|
+
component.value = this.join(newTokens.slice(newPos, newPos + component.count));
|
|
4681
|
+
}
|
|
4682
|
+
newPos += component.count;
|
|
4683
|
+
if (!component.added) {
|
|
4684
|
+
oldPos += component.count;
|
|
4685
|
+
}
|
|
4686
|
+
} else {
|
|
4687
|
+
component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
|
|
4688
|
+
oldPos += component.count;
|
|
4689
|
+
}
|
|
4690
|
+
}
|
|
4691
|
+
return components;
|
|
4524
4692
|
}
|
|
4525
|
-
|
|
4526
|
-
|
|
4693
|
+
};
|
|
4694
|
+
|
|
4695
|
+
// node_modules/diff/libesm/diff/line.js
|
|
4696
|
+
var LineDiff = class extends Diff {
|
|
4697
|
+
constructor() {
|
|
4698
|
+
super(...arguments);
|
|
4699
|
+
this.tokenize = tokenize;
|
|
4527
4700
|
}
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
()
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
)
|
|
4701
|
+
equals(left, right, options) {
|
|
4702
|
+
if (options.ignoreWhitespace) {
|
|
4703
|
+
if (!options.newlineIsToken || !left.includes("\n")) {
|
|
4704
|
+
left = left.trim();
|
|
4705
|
+
}
|
|
4706
|
+
if (!options.newlineIsToken || !right.includes("\n")) {
|
|
4707
|
+
right = right.trim();
|
|
4708
|
+
}
|
|
4709
|
+
} else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
|
|
4710
|
+
if (left.endsWith("\n")) {
|
|
4711
|
+
left = left.slice(0, -1);
|
|
4712
|
+
}
|
|
4713
|
+
if (right.endsWith("\n")) {
|
|
4714
|
+
right = right.slice(0, -1);
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
return super.equals(left, right, options);
|
|
4537
4718
|
}
|
|
4538
|
-
|
|
4539
|
-
|
|
4719
|
+
};
|
|
4720
|
+
var lineDiff = new LineDiff();
|
|
4721
|
+
function diffLines(oldStr, newStr, options) {
|
|
4722
|
+
return lineDiff.diff(oldStr, newStr, options);
|
|
4723
|
+
}
|
|
4724
|
+
function tokenize(value, options) {
|
|
4725
|
+
if (options.stripTrailingCr) {
|
|
4726
|
+
value = value.replace(/\r\n/g, "\n");
|
|
4540
4727
|
}
|
|
4541
|
-
const
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4728
|
+
const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
|
|
4729
|
+
if (!linesAndNewlines[linesAndNewlines.length - 1]) {
|
|
4730
|
+
linesAndNewlines.pop();
|
|
4731
|
+
}
|
|
4732
|
+
for (let i = 0; i < linesAndNewlines.length; i++) {
|
|
4733
|
+
const line = linesAndNewlines[i];
|
|
4734
|
+
if (i % 2 && !options.newlineIsToken) {
|
|
4735
|
+
retLines[retLines.length - 1] += line;
|
|
4736
|
+
} else {
|
|
4737
|
+
retLines.push(line);
|
|
4549
4738
|
}
|
|
4550
|
-
dispatchActions(current.findings, actions, { ts, map, nowMs, repo, scan });
|
|
4551
|
-
gitOrFatal(["add", "-A"], "git add", repo);
|
|
4552
|
-
current = scanVerdict(repo);
|
|
4553
4739
|
}
|
|
4554
|
-
return
|
|
4740
|
+
return retLines;
|
|
4555
4741
|
}
|
|
4556
4742
|
|
|
4557
|
-
// src/
|
|
4558
|
-
|
|
4559
|
-
|
|
4743
|
+
// src/diff-lines.ts
|
|
4744
|
+
init_color();
|
|
4745
|
+
function diffLinesToUnified(oldStr, newStr) {
|
|
4746
|
+
const parts = diffLines(oldStr, newStr);
|
|
4747
|
+
const lines = [];
|
|
4748
|
+
for (const part of parts) {
|
|
4749
|
+
const partLines = part.value.split("\n");
|
|
4750
|
+
if (partLines.at(-1) === "") {
|
|
4751
|
+
partLines.pop();
|
|
4752
|
+
}
|
|
4753
|
+
let prefix;
|
|
4754
|
+
if (part.removed) prefix = (line) => red(`-${line}`);
|
|
4755
|
+
else if (part.added) prefix = (line) => green(`+${line}`);
|
|
4756
|
+
else prefix = (line) => ` ${line}`;
|
|
4757
|
+
for (const line of partLines) {
|
|
4758
|
+
lines.push(prefix(line));
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
return lines;
|
|
4560
4762
|
}
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4763
|
+
|
|
4764
|
+
// src/preview.ts
|
|
4765
|
+
init_utils_json();
|
|
4766
|
+
var CANONICAL_ORDER_NOTE = "settings.json will be rewritten in canonical key order; no value changes";
|
|
4767
|
+
function diffJsonStrings(currentJsonText, newJsonText) {
|
|
4768
|
+
if (currentJsonText === newJsonText) return "";
|
|
4769
|
+
const lines = [
|
|
4770
|
+
"--- ~/.claude/settings.json",
|
|
4771
|
+
"+++ would write",
|
|
4772
|
+
...diffLinesToUnified(currentJsonText, newJsonText)
|
|
4773
|
+
];
|
|
4774
|
+
return lines.join("\n");
|
|
4564
4775
|
}
|
|
4565
|
-
function
|
|
4566
|
-
|
|
4567
|
-
|
|
4776
|
+
function readJsonOrNull(path) {
|
|
4777
|
+
if (!existsSync34(path)) return null;
|
|
4778
|
+
try {
|
|
4779
|
+
return readJson(path);
|
|
4780
|
+
} catch {
|
|
4781
|
+
return null;
|
|
4782
|
+
}
|
|
4568
4783
|
}
|
|
4569
|
-
function
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4784
|
+
function previewSettings(basePath, hostPath, settingsPath) {
|
|
4785
|
+
const base = readJsonOrNull(basePath);
|
|
4786
|
+
if (base === null) {
|
|
4787
|
+
return { diff: "", notes: ["section skipped (base or current missing)"] };
|
|
4788
|
+
}
|
|
4789
|
+
const notes = [];
|
|
4790
|
+
const hostOverrides = readJsonOrNull(hostPath);
|
|
4791
|
+
if (hostOverrides === null && existsSync34(hostPath)) {
|
|
4792
|
+
notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
|
|
4793
|
+
}
|
|
4794
|
+
const merged = deepMerge(base, hostOverrides ?? {});
|
|
4795
|
+
const current = readJsonOrNull(settingsPath);
|
|
4796
|
+
if (current === null && existsSync34(settingsPath)) {
|
|
4797
|
+
return { diff: "", notes: [...notes, "malformed; skipping diff"] };
|
|
4798
|
+
}
|
|
4799
|
+
const rawEqual = JSON.stringify(current ?? {}, null, 2) === JSON.stringify(merged, null, 2);
|
|
4800
|
+
const diff = diffJsonStrings(
|
|
4801
|
+
JSON.stringify(sortKeysDeep(current ?? {}), null, 2),
|
|
4802
|
+
JSON.stringify(sortKeysDeep(merged), null, 2)
|
|
4803
|
+
);
|
|
4804
|
+
if (diff === "" && !rawEqual) notes.push(CANONICAL_ORDER_NOTE);
|
|
4805
|
+
return { diff, notes };
|
|
4574
4806
|
}
|
|
4575
|
-
function
|
|
4576
|
-
|
|
4577
|
-
const base = deps.baseUrl ?? import.meta.url;
|
|
4578
|
-
const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
|
|
4579
|
-
if (check(mjs)) return mjs;
|
|
4580
|
-
return fileURLToPath4(new URL("./spinner.worker.ts", base));
|
|
4807
|
+
function formatLinkRow(e) {
|
|
4808
|
+
return `${e.kind} ${e.from} -> ${e.to}`;
|
|
4581
4809
|
}
|
|
4582
|
-
function
|
|
4583
|
-
return
|
|
4810
|
+
function formatSessionRow(e) {
|
|
4811
|
+
return e.kind === "overwrite" ? `overwrite ${e.dst} (from ${e.src})` : e.text;
|
|
4584
4812
|
}
|
|
4585
|
-
function
|
|
4586
|
-
const
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
const startMs = now();
|
|
4591
|
-
const animate = ttyCheck() && !env.CI;
|
|
4592
|
-
let worker = null;
|
|
4593
|
-
let degraded = false;
|
|
4594
|
-
let finalized = false;
|
|
4595
|
-
if (animate) {
|
|
4596
|
-
const factory = deps.makeWorker ?? makeRealWorker;
|
|
4597
|
-
try {
|
|
4598
|
-
worker = factory();
|
|
4599
|
-
worker.unref?.();
|
|
4600
|
-
worker.postMessage({ type: "start", label });
|
|
4601
|
-
} catch {
|
|
4602
|
-
degraded = true;
|
|
4603
|
-
worker = null;
|
|
4604
|
-
writePlainStart(out, label);
|
|
4813
|
+
function buildSettingsSectionForPreview(result) {
|
|
4814
|
+
const s = section("settings.json", true);
|
|
4815
|
+
if (result.diff !== "") {
|
|
4816
|
+
for (const line of result.diff.split("\n")) {
|
|
4817
|
+
addItem(s, line);
|
|
4605
4818
|
}
|
|
4606
|
-
} else {
|
|
4607
|
-
writePlainStart(out, label);
|
|
4608
4819
|
}
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
finalized = true;
|
|
4612
|
-
const dl = doneLabel ?? label;
|
|
4613
|
-
const elapsed = now() - startMs;
|
|
4614
|
-
if (animate && !degraded && worker !== null) {
|
|
4615
|
-
worker.postMessage({ type: "pause" });
|
|
4616
|
-
worker.terminate();
|
|
4617
|
-
worker = null;
|
|
4618
|
-
if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
|
|
4619
|
-
else out.write("\r\x1B[K");
|
|
4620
|
-
} else if (success) {
|
|
4621
|
-
writePlainDone(out, dl, elapsed);
|
|
4622
|
-
}
|
|
4820
|
+
for (const note of result.notes) {
|
|
4821
|
+
addItem(s, `note: ${note}`);
|
|
4623
4822
|
}
|
|
4624
|
-
return
|
|
4625
|
-
succeed: (doneLabel) => finalize(true, doneLabel),
|
|
4626
|
-
stop: () => finalize(false)
|
|
4627
|
-
};
|
|
4823
|
+
return s;
|
|
4628
4824
|
}
|
|
4629
|
-
function
|
|
4630
|
-
const
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4825
|
+
function computePreview(ts, map, verb = "pull") {
|
|
4826
|
+
const repo = repoHome();
|
|
4827
|
+
const claude = claudeHome();
|
|
4828
|
+
console.log(`would pull on host=${HOST} (dry-run; no mutation)`);
|
|
4829
|
+
console.log("");
|
|
4830
|
+
const links = section("Symlinks");
|
|
4831
|
+
applySharedLinks(ts, map, {
|
|
4832
|
+
dryRun: true,
|
|
4833
|
+
onPreview: (e) => addItem(links, formatLinkRow(e))
|
|
4834
|
+
});
|
|
4835
|
+
const settingsResult = previewSettings(
|
|
4836
|
+
join40(repo, "shared", "settings.base.json"),
|
|
4837
|
+
join40(repo, "hosts", `${HOST}.json`),
|
|
4838
|
+
join40(claude, "settings.json")
|
|
4839
|
+
);
|
|
4840
|
+
const settingsSection = buildSettingsSectionForPreview(settingsResult);
|
|
4841
|
+
const sessions = section("Sessions");
|
|
4842
|
+
const remapResult = remapPull(ts, {
|
|
4843
|
+
dryRun: true,
|
|
4844
|
+
onPreview: (e) => addItem(sessions, formatSessionRow(e))
|
|
4845
|
+
});
|
|
4846
|
+
const summary = section("Summary");
|
|
4847
|
+
addItem(summary, summaryRow(verb, remapResult.unmapped));
|
|
4848
|
+
renderTree([links, settingsSection, sessions, summary]);
|
|
4849
|
+
return { unmapped: remapResult.unmapped, collisions: 0 };
|
|
4638
4850
|
}
|
|
4639
4851
|
|
|
4640
4852
|
// src/commands.pull.recovery.ts
|
|
@@ -4781,8 +4993,8 @@ function cmdPull(opts = {}) {
|
|
|
4781
4993
|
const forceRemote = opts.forceRemote === true;
|
|
4782
4994
|
const repo = repoHome();
|
|
4783
4995
|
const backup = backupBase();
|
|
4784
|
-
if (!
|
|
4785
|
-
if (!
|
|
4996
|
+
if (!existsSync35(repo)) die(`repo not cloned at ${repo}`);
|
|
4997
|
+
if (!existsSync35(join41(repo, "shared", "settings.base.json"))) {
|
|
4786
4998
|
die("repo not initialized; run 'nomad init' to scaffold");
|
|
4787
4999
|
}
|
|
4788
5000
|
const handle = acquireLock("pull");
|
|
@@ -4791,7 +5003,7 @@ function cmdPull(opts = {}) {
|
|
|
4791
5003
|
const ts = freshBackupTs(backup);
|
|
4792
5004
|
handleWedge(repo, forceRemote);
|
|
4793
5005
|
if (!dryRun) {
|
|
4794
|
-
const backupRoot =
|
|
5006
|
+
const backupRoot = join41(backup, ts);
|
|
4795
5007
|
try {
|
|
4796
5008
|
mkdirSync8(backupRoot, { recursive: true });
|
|
4797
5009
|
} catch (err) {
|
|
@@ -4802,8 +5014,8 @@ function cmdPull(opts = {}) {
|
|
|
4802
5014
|
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
4803
5015
|
);
|
|
4804
5016
|
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
|
|
4805
|
-
const mapPath =
|
|
4806
|
-
const map =
|
|
5017
|
+
const mapPath = join41(repo, "path-map.json");
|
|
5018
|
+
const map = existsSync35(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
4807
5019
|
divergenceCheckExtras(ts);
|
|
4808
5020
|
if (dryRun) {
|
|
4809
5021
|
computePreview(ts, map, "pull");
|
|
@@ -4825,8 +5037,8 @@ function cmdPull(opts = {}) {
|
|
|
4825
5037
|
|
|
4826
5038
|
// src/commands.push.ts
|
|
4827
5039
|
init_config();
|
|
4828
|
-
import { existsSync as
|
|
4829
|
-
import { join as
|
|
5040
|
+
import { existsSync as existsSync37 } from "node:fs";
|
|
5041
|
+
import { join as join43, relative as relative5 } from "node:path";
|
|
4830
5042
|
|
|
4831
5043
|
// src/commands.push.allowlist.ts
|
|
4832
5044
|
init_config();
|
|
@@ -4906,9 +5118,9 @@ init_color();
|
|
|
4906
5118
|
init_config();
|
|
4907
5119
|
init_config_sharedDirs_guard();
|
|
4908
5120
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
4909
|
-
import { copyFileSync, existsSync as
|
|
5121
|
+
import { copyFileSync, existsSync as existsSync36, mkdirSync as mkdirSync9, readdirSync as readdirSync11, rmSync as rmSync12 } from "node:fs";
|
|
4910
5122
|
import { homedir as homedir5 } from "node:os";
|
|
4911
|
-
import { join as
|
|
5123
|
+
import { join as join42 } from "node:path";
|
|
4912
5124
|
init_push_leak_verdict();
|
|
4913
5125
|
init_push_gitleaks();
|
|
4914
5126
|
init_utils_fs();
|
|
@@ -4923,13 +5135,13 @@ function stageSessions(tmpRoot, map) {
|
|
|
4923
5135
|
if (!p || p === "TBD") continue;
|
|
4924
5136
|
reverse.set(encodePath(p), logical);
|
|
4925
5137
|
}
|
|
4926
|
-
const localProjects =
|
|
4927
|
-
if (!
|
|
5138
|
+
const localProjects = join42(claudeHome(), "projects");
|
|
5139
|
+
if (!existsSync36(localProjects)) return 0;
|
|
4928
5140
|
let staged = 0;
|
|
4929
5141
|
for (const dir of readdirSync11(localProjects)) {
|
|
4930
5142
|
const logical = reverse.get(dir);
|
|
4931
5143
|
if (!logical) continue;
|
|
4932
|
-
copyDirJsonlOnly(
|
|
5144
|
+
copyDirJsonlOnly(join42(localProjects, dir), join42(tmpRoot, "shared", "projects", logical));
|
|
4933
5145
|
staged++;
|
|
4934
5146
|
}
|
|
4935
5147
|
return staged;
|
|
@@ -4945,9 +5157,9 @@ function stageExtras(tmpRoot, map) {
|
|
|
4945
5157
|
if (!localRoot || localRoot === "TBD") continue;
|
|
4946
5158
|
for (const dirname7 of dirnames) {
|
|
4947
5159
|
if (!whitelist.includes(dirname7)) continue;
|
|
4948
|
-
const src =
|
|
4949
|
-
if (!
|
|
4950
|
-
const dst =
|
|
5160
|
+
const src = join42(localRoot, dirname7);
|
|
5161
|
+
if (!existsSync36(src)) continue;
|
|
5162
|
+
const dst = join42(tmpRoot, "shared", "extras", logical, dirname7);
|
|
4951
5163
|
copyExtras(src, dst);
|
|
4952
5164
|
staged++;
|
|
4953
5165
|
}
|
|
@@ -4955,19 +5167,19 @@ function stageExtras(tmpRoot, map) {
|
|
|
4955
5167
|
return staged;
|
|
4956
5168
|
}
|
|
4957
5169
|
function previewPushLeaks(map) {
|
|
4958
|
-
const cacheDir =
|
|
5170
|
+
const cacheDir = join42(homedir5(), ".cache", "claude-nomad");
|
|
4959
5171
|
mkdirSync9(cacheDir, { recursive: true });
|
|
4960
5172
|
const stamp = `${nowTimestamp()}-${process.pid}-${randomBytes2(4).toString("hex")}`;
|
|
4961
|
-
const tmpRoot =
|
|
5173
|
+
const tmpRoot = join42(cacheDir, `push-preview-tree-${stamp}`);
|
|
4962
5174
|
try {
|
|
4963
5175
|
const sessionCount = stageSessions(tmpRoot, map);
|
|
4964
5176
|
const extrasCount = stageExtras(tmpRoot, map);
|
|
4965
5177
|
if (sessionCount + extrasCount === 0) {
|
|
4966
5178
|
return { leak: false, verdictRow: NOTHING_TO_SCAN_ROW, recovery: null, findings: [] };
|
|
4967
5179
|
}
|
|
4968
|
-
const ignoreFile =
|
|
4969
|
-
if (
|
|
4970
|
-
copyFileSync(ignoreFile,
|
|
5180
|
+
const ignoreFile = join42(repoHome(), ".gitleaksignore");
|
|
5181
|
+
if (existsSync36(ignoreFile)) {
|
|
5182
|
+
copyFileSync(ignoreFile, join42(tmpRoot, ".gitleaksignore"));
|
|
4971
5183
|
}
|
|
4972
5184
|
let findings;
|
|
4973
5185
|
try {
|
|
@@ -4989,7 +5201,7 @@ init_utils();
|
|
|
4989
5201
|
init_utils_fs();
|
|
4990
5202
|
init_utils_json();
|
|
4991
5203
|
function guardGitlinks(repo) {
|
|
4992
|
-
const gitlinks = findGitlinks(
|
|
5204
|
+
const gitlinks = findGitlinks(join43(repo, "shared"));
|
|
4993
5205
|
if (gitlinks.length === 0) return;
|
|
4994
5206
|
for (const p of gitlinks) {
|
|
4995
5207
|
const rel = relative5(repo, p);
|
|
@@ -5043,7 +5255,7 @@ async function cmdPush(opts = {}) {
|
|
|
5043
5255
|
guardResolutionModeConflicts(dryRun, redactAll, allowAll, allowRule);
|
|
5044
5256
|
const repo = repoHome();
|
|
5045
5257
|
const backup = backupBase();
|
|
5046
|
-
if (!
|
|
5258
|
+
if (!existsSync37(repo)) die(`repo not cloned at ${repo}`);
|
|
5047
5259
|
const handle = acquireLock("push");
|
|
5048
5260
|
if (handle === null) process.exit(0);
|
|
5049
5261
|
try {
|
|
@@ -5061,8 +5273,8 @@ async function cmdPush(opts = {}) {
|
|
|
5061
5273
|
renderNoScanTree(st);
|
|
5062
5274
|
return;
|
|
5063
5275
|
}
|
|
5064
|
-
const mapPath =
|
|
5065
|
-
if (!
|
|
5276
|
+
const mapPath = join43(repo, "path-map.json");
|
|
5277
|
+
if (!existsSync37(mapPath)) {
|
|
5066
5278
|
if (dryRun) return runDryRunPreview(st, null);
|
|
5067
5279
|
die("path-map.json missing, cannot enforce push allow-list");
|
|
5068
5280
|
}
|
|
@@ -5102,18 +5314,18 @@ init_config();
|
|
|
5102
5314
|
|
|
5103
5315
|
// src/diff.ts
|
|
5104
5316
|
init_config();
|
|
5105
|
-
import { existsSync as
|
|
5106
|
-
import { join as
|
|
5317
|
+
import { existsSync as existsSync38 } from "node:fs";
|
|
5318
|
+
import { join as join44 } from "node:path";
|
|
5107
5319
|
init_utils();
|
|
5108
5320
|
init_utils_fs();
|
|
5109
5321
|
init_utils_json();
|
|
5110
5322
|
function cmdDiff() {
|
|
5111
5323
|
try {
|
|
5112
5324
|
const repo = repoHome();
|
|
5113
|
-
if (!
|
|
5325
|
+
if (!existsSync38(repo)) die(`repo not cloned at ${repo}`);
|
|
5114
5326
|
const ts = freshBackupTs(backupBase());
|
|
5115
|
-
const mapPath =
|
|
5116
|
-
const map =
|
|
5327
|
+
const mapPath = join44(repo, "path-map.json");
|
|
5328
|
+
const map = existsSync38(mapPath) ? readPathMap(mapPath) : { projects: {} };
|
|
5117
5329
|
computePreview(ts, map, "diff");
|
|
5118
5330
|
} catch (err) {
|
|
5119
5331
|
if (err instanceof NomadFatal) {
|
|
@@ -5127,8 +5339,8 @@ function cmdDiff() {
|
|
|
5127
5339
|
|
|
5128
5340
|
// src/init.ts
|
|
5129
5341
|
init_config();
|
|
5130
|
-
import { existsSync as
|
|
5131
|
-
import { join as
|
|
5342
|
+
import { existsSync as existsSync40, mkdirSync as mkdirSync10, writeFileSync as writeFileSync6 } from "node:fs";
|
|
5343
|
+
import { join as join46 } from "node:path";
|
|
5132
5344
|
|
|
5133
5345
|
// src/init.gh-onboard.ts
|
|
5134
5346
|
init_config();
|
|
@@ -5210,33 +5422,33 @@ init_config();
|
|
|
5210
5422
|
init_utils();
|
|
5211
5423
|
init_utils_fs();
|
|
5212
5424
|
init_utils_json();
|
|
5213
|
-
import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as
|
|
5214
|
-
import { join as
|
|
5425
|
+
import { copyFileSync as copyFileSync2, cpSync as cpSync7, existsSync as existsSync39, rmSync as rmSync13, statSync as statSync9 } from "node:fs";
|
|
5426
|
+
import { join as join45 } from "node:path";
|
|
5215
5427
|
function snapshotIntoShared(map) {
|
|
5216
5428
|
const repo = repoHome();
|
|
5217
5429
|
const claude = claudeHome();
|
|
5218
5430
|
for (const name of allSharedLinks(map)) {
|
|
5219
|
-
const src =
|
|
5220
|
-
if (!
|
|
5221
|
-
const dst =
|
|
5431
|
+
const src = join45(claude, name);
|
|
5432
|
+
if (!existsSync39(src)) continue;
|
|
5433
|
+
const dst = join45(repo, "shared", name);
|
|
5222
5434
|
if (statSync9(src).isDirectory()) {
|
|
5223
|
-
const gk =
|
|
5224
|
-
if (
|
|
5435
|
+
const gk = join45(dst, ".gitkeep");
|
|
5436
|
+
if (existsSync39(gk)) rmSync13(gk);
|
|
5225
5437
|
cpSync7(src, dst, { recursive: true, force: false, errorOnExist: true });
|
|
5226
5438
|
} else {
|
|
5227
5439
|
copyFileSync2(src, dst);
|
|
5228
5440
|
}
|
|
5229
5441
|
log(`snapshotted shared/${name} from ${src}`);
|
|
5230
5442
|
}
|
|
5231
|
-
const userSettings =
|
|
5232
|
-
if (
|
|
5443
|
+
const userSettings = join45(claude, "settings.json");
|
|
5444
|
+
if (existsSync39(userSettings)) {
|
|
5233
5445
|
let parsed;
|
|
5234
5446
|
try {
|
|
5235
5447
|
parsed = readJson(userSettings);
|
|
5236
5448
|
} catch (err) {
|
|
5237
5449
|
return die(`malformed ${userSettings}: ${err.message}`);
|
|
5238
5450
|
}
|
|
5239
|
-
const hostFile =
|
|
5451
|
+
const hostFile = join45(repo, "hosts", `${HOST}.json`);
|
|
5240
5452
|
writeJsonAtomic(hostFile, parsed);
|
|
5241
5453
|
log(`snapshotted hosts/${HOST}.json from ${userSettings}`);
|
|
5242
5454
|
}
|
|
@@ -5249,14 +5461,14 @@ var SHARED_CLAUDE_MD = "<!-- claude-nomad shared CLAUDE.md; symlinked into ~/.cl
|
|
|
5249
5461
|
var SHARED_KEEP_DIRS = ["agents", "skills", "commands", "rules", "hooks"];
|
|
5250
5462
|
function preflightConflict(repoHome2) {
|
|
5251
5463
|
const candidates = [
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5464
|
+
join46(repoHome2, "shared", "settings.base.json"),
|
|
5465
|
+
join46(repoHome2, "shared", "CLAUDE.md"),
|
|
5466
|
+
join46(repoHome2, "path-map.json"),
|
|
5467
|
+
join46(repoHome2, "hosts"),
|
|
5468
|
+
join46(repoHome2, "shared")
|
|
5257
5469
|
];
|
|
5258
5470
|
for (const c of candidates) {
|
|
5259
|
-
if (
|
|
5471
|
+
if (existsSync40(c)) return c;
|
|
5260
5472
|
}
|
|
5261
5473
|
return null;
|
|
5262
5474
|
}
|
|
@@ -5271,25 +5483,25 @@ function cmdInit(opts = {}) {
|
|
|
5271
5483
|
die(`already initialized; refusing to clobber ${conflict}`);
|
|
5272
5484
|
}
|
|
5273
5485
|
ensureOriginRepo(opts.repoName ?? DEFAULT_REPO_NAME, opts.run);
|
|
5274
|
-
mkdirSync10(
|
|
5275
|
-
mkdirSync10(
|
|
5486
|
+
mkdirSync10(join46(repo, "shared"), { recursive: true });
|
|
5487
|
+
mkdirSync10(join46(repo, "hosts"), { recursive: true });
|
|
5276
5488
|
for (const name of SHARED_KEEP_DIRS) {
|
|
5277
|
-
mkdirSync10(
|
|
5489
|
+
mkdirSync10(join46(repo, "shared", name), { recursive: true });
|
|
5278
5490
|
}
|
|
5279
|
-
const userClaudeMd =
|
|
5280
|
-
if (!snapshot || !
|
|
5281
|
-
writeFileSync6(
|
|
5491
|
+
const userClaudeMd = join46(claude, "CLAUDE.md");
|
|
5492
|
+
if (!snapshot || !existsSync40(userClaudeMd)) {
|
|
5493
|
+
writeFileSync6(join46(repo, "shared", "CLAUDE.md"), SHARED_CLAUDE_MD);
|
|
5282
5494
|
item("created shared/CLAUDE.md");
|
|
5283
5495
|
}
|
|
5284
5496
|
for (const name of SHARED_KEEP_DIRS) {
|
|
5285
|
-
writeFileSync6(
|
|
5497
|
+
writeFileSync6(join46(repo, "shared", name, ".gitkeep"), "");
|
|
5286
5498
|
item(`created shared/${name}/.gitkeep`);
|
|
5287
5499
|
}
|
|
5288
|
-
writeFileSync6(
|
|
5500
|
+
writeFileSync6(join46(repo, "hosts", ".gitkeep"), "");
|
|
5289
5501
|
item("created hosts/.gitkeep");
|
|
5290
|
-
writeJsonAtomic(
|
|
5502
|
+
writeJsonAtomic(join46(repo, "shared", "settings.base.json"), {});
|
|
5291
5503
|
item("created shared/settings.base.json");
|
|
5292
|
-
writeJsonAtomic(
|
|
5504
|
+
writeJsonAtomic(join46(repo, "path-map.json"), { projects: {} });
|
|
5293
5505
|
item("created path-map.json");
|
|
5294
5506
|
if (snapshot) {
|
|
5295
5507
|
snapshotIntoShared({ projects: {} });
|
|
@@ -5590,7 +5802,7 @@ function parsePushArgs(argv) {
|
|
|
5590
5802
|
// package.json
|
|
5591
5803
|
var package_default = {
|
|
5592
5804
|
name: "claude-nomad",
|
|
5593
|
-
version: "0.
|
|
5805
|
+
version: "0.46.0",
|
|
5594
5806
|
type: "module",
|
|
5595
5807
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
5596
5808
|
keywords: [
|
|
@@ -5792,15 +6004,15 @@ var DEFAULT_HELP = [
|
|
|
5792
6004
|
init_config();
|
|
5793
6005
|
init_utils();
|
|
5794
6006
|
init_utils_json();
|
|
5795
|
-
import { existsSync as
|
|
5796
|
-
import { join as
|
|
6007
|
+
import { existsSync as existsSync41, readFileSync as readFileSync14, readdirSync as readdirSync12 } from "node:fs";
|
|
6008
|
+
import { join as join47 } from "node:path";
|
|
5797
6009
|
function resumeCmd(sessionId) {
|
|
5798
6010
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
5799
6011
|
fail(`invalid session id: ${sessionId}`);
|
|
5800
6012
|
process.exit(1);
|
|
5801
6013
|
}
|
|
5802
|
-
const projectsRoot =
|
|
5803
|
-
if (!
|
|
6014
|
+
const projectsRoot = join47(claudeHome(), "projects");
|
|
6015
|
+
if (!existsSync41(projectsRoot)) {
|
|
5804
6016
|
fail(`${projectsRoot} does not exist`);
|
|
5805
6017
|
process.exit(1);
|
|
5806
6018
|
}
|
|
@@ -5814,8 +6026,8 @@ function resumeCmd(sessionId) {
|
|
|
5814
6026
|
fail(`no cwd field found in ${jsonlPath}`);
|
|
5815
6027
|
process.exit(1);
|
|
5816
6028
|
}
|
|
5817
|
-
const mapPath =
|
|
5818
|
-
if (!
|
|
6029
|
+
const mapPath = join47(repoHome(), "path-map.json");
|
|
6030
|
+
if (!existsSync41(mapPath)) {
|
|
5819
6031
|
fail("path-map.json missing");
|
|
5820
6032
|
process.exit(1);
|
|
5821
6033
|
}
|
|
@@ -5838,13 +6050,13 @@ function resumeCmd(sessionId) {
|
|
|
5838
6050
|
}
|
|
5839
6051
|
function findTranscriptPath(projectsRoot, sessionId) {
|
|
5840
6052
|
for (const dir of readdirSync12(projectsRoot)) {
|
|
5841
|
-
const candidate =
|
|
5842
|
-
if (
|
|
6053
|
+
const candidate = join47(projectsRoot, dir, `${sessionId}.jsonl`);
|
|
6054
|
+
if (existsSync41(candidate)) return candidate;
|
|
5843
6055
|
}
|
|
5844
6056
|
return null;
|
|
5845
6057
|
}
|
|
5846
6058
|
function extractRecordedCwd(jsonlPath) {
|
|
5847
|
-
for (const line of
|
|
6059
|
+
for (const line of readFileSync14(jsonlPath, "utf8").split("\n")) {
|
|
5848
6060
|
if (!line.trim()) continue;
|
|
5849
6061
|
try {
|
|
5850
6062
|
const obj = JSON.parse(line);
|