claude-nomad 0.45.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +49 -0
- package/dist/nomad.mjs +1682 -1577
- package/package.json +1 -2
- package/shared/.gitignore +0 -9
package/dist/nomad.mjs
CHANGED
|
@@ -33,7 +33,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
33
33
|
));
|
|
34
34
|
|
|
35
35
|
// src/config.never-sync.ts
|
|
36
|
-
var NEVER_SYNC;
|
|
36
|
+
var NEVER_SYNC, CLAUDE_EXTRA_NEVER_SYNC;
|
|
37
37
|
var init_config_never_sync = __esm({
|
|
38
38
|
"src/config.never-sync.ts"() {
|
|
39
39
|
"use strict";
|
|
@@ -62,6 +62,7 @@ var init_config_never_sync = __esm({
|
|
|
62
62
|
"security",
|
|
63
63
|
"sessions"
|
|
64
64
|
]);
|
|
65
|
+
CLAUDE_EXTRA_NEVER_SYNC = /* @__PURE__ */ new Set([...NEVER_SYNC, "projects"]);
|
|
65
66
|
}
|
|
66
67
|
});
|
|
67
68
|
|
|
@@ -407,7 +408,7 @@ var init_config = __esm({
|
|
|
407
408
|
"my-statusline.cjs",
|
|
408
409
|
"hooks"
|
|
409
410
|
];
|
|
410
|
-
SUPPORTED_EXTRAS = [".planning", "CLAUDE.md"];
|
|
411
|
+
SUPPORTED_EXTRAS = [".planning", "CLAUDE.md", ".claude"];
|
|
411
412
|
ALWAYS_NEVER_SYNC = /* @__PURE__ */ new Set([
|
|
412
413
|
".claude.json",
|
|
413
414
|
".credentials.json",
|
|
@@ -1400,8 +1401,8 @@ function cmdEject(opts = {}, roots = defaultEjectRoots()) {
|
|
|
1400
1401
|
}
|
|
1401
1402
|
|
|
1402
1403
|
// src/commands.doctor.ts
|
|
1403
|
-
import { existsSync as
|
|
1404
|
-
import { join as
|
|
1404
|
+
import { existsSync as existsSync27 } from "node:fs";
|
|
1405
|
+
import { join as join33 } from "node:path";
|
|
1405
1406
|
|
|
1406
1407
|
// src/commands.doctor.checks.repo.ts
|
|
1407
1408
|
init_color();
|
|
@@ -2931,420 +2932,144 @@ function reportNodeEngineCheck(section2) {
|
|
|
2931
2932
|
addItem(section2, `${green(okGlyph)} node: ${process.version} (satisfies >=${min})`);
|
|
2932
2933
|
}
|
|
2933
2934
|
|
|
2934
|
-
// src/
|
|
2935
|
+
// src/spinner.ts
|
|
2935
2936
|
init_color();
|
|
2936
|
-
import {
|
|
2937
|
-
import {
|
|
2938
|
-
import {
|
|
2937
|
+
import { existsSync as existsSync25 } from "node:fs";
|
|
2938
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
2939
|
+
import { Worker } from "node:worker_threads";
|
|
2940
|
+
|
|
2941
|
+
// src/commands.push.recovery.ts
|
|
2939
2942
|
init_config();
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
const m = SEMVER_MAJOR_MINOR.exec(value);
|
|
2944
|
-
return m === null ? null : [m[1], m[2]];
|
|
2945
|
-
}
|
|
2946
|
-
function readGitleaksVersion(run, tomlExists) {
|
|
2947
|
-
const tomlPath = join26(repoHome(), ".gitleaks.toml");
|
|
2948
|
-
const args = ["version"];
|
|
2949
|
-
if (tomlExists(tomlPath)) args.push("--config", tomlPath);
|
|
2950
|
-
try {
|
|
2951
|
-
return run("gitleaks", args, {
|
|
2952
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2953
|
-
timeout: GITLEAKS_TIMEOUT_MS
|
|
2954
|
-
}).toString().trim();
|
|
2955
|
-
} catch {
|
|
2956
|
-
return null;
|
|
2957
|
-
}
|
|
2958
|
-
}
|
|
2959
|
-
function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync22) {
|
|
2960
|
-
const raw = readGitleaksVersion(run, tomlExists);
|
|
2961
|
-
if (raw === null) return;
|
|
2962
|
-
const local = majorMinorOf(raw);
|
|
2963
|
-
if (local === null) return;
|
|
2964
|
-
const pin = majorMinorOf(GITLEAKS_PINNED_VERSION);
|
|
2965
|
-
if (pin === null) return;
|
|
2966
|
-
const sameMajorMinor = local[0] === pin[0] && local[1] === pin[1];
|
|
2967
|
-
if (sameMajorMinor) {
|
|
2968
|
-
addItem(section2, `${green(okGlyph)} gitleaks: ${raw} (matches pinned ${pin[0]}.${pin[1]})`);
|
|
2969
|
-
return;
|
|
2970
|
-
}
|
|
2971
|
-
addItem(
|
|
2972
|
-
section2,
|
|
2973
|
-
`${yellow(warnGlyph)} gitleaks: ${raw} -> ${GITLEAKS_PINNED_VERSION} (CI pins this; local drift may change scan results)`
|
|
2974
|
-
);
|
|
2975
|
-
}
|
|
2943
|
+
import { readFileSync as readFileSync13, rmSync as rmSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
2944
|
+
import { join as join31 } from "node:path";
|
|
2945
|
+
import { createInterface } from "node:readline/promises";
|
|
2976
2946
|
|
|
2977
|
-
// src/commands.
|
|
2978
|
-
|
|
2979
|
-
import {
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
}
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2947
|
+
// src/commands.push.recovery.actions.ts
|
|
2948
|
+
init_config();
|
|
2949
|
+
import { readFileSync as readFileSync12 } from "node:fs";
|
|
2950
|
+
import { isAbsolute, resolve as resolve3, sep as sep4 } from "node:path";
|
|
2951
|
+
|
|
2952
|
+
// src/commands.push.recovery.redact.ts
|
|
2953
|
+
init_config();
|
|
2954
|
+
init_config_sharedDirs_guard();
|
|
2955
|
+
import { cpSync as cpSync5, existsSync as existsSync24, mkdirSync as mkdirSync6, statSync as statSync7 } from "node:fs";
|
|
2956
|
+
import { dirname as dirname6, join as join29, sep as sep3 } from "node:path";
|
|
2957
|
+
|
|
2958
|
+
// src/commands.redact.ts
|
|
2959
|
+
init_config();
|
|
2960
|
+
import { existsSync as existsSync23, statSync as statSync6 } from "node:fs";
|
|
2961
|
+
import { dirname as dirname5, join as join28 } from "node:path";
|
|
2962
|
+
|
|
2963
|
+
// src/commands.redact.subtree.ts
|
|
2964
|
+
import { existsSync as existsSync22, lstatSync as lstatSync7, readFileSync as readFileSync10, readdirSync as readdirSync9, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2965
|
+
import { join as join26 } from "node:path";
|
|
2966
|
+
init_utils_fs();
|
|
2967
|
+
function collectFiles(dir, out) {
|
|
2968
|
+
if (!existsSync22(dir)) return;
|
|
2969
|
+
const st = lstatSync7(dir);
|
|
2970
|
+
if (!st.isDirectory()) return;
|
|
2971
|
+
for (const entry of readdirSync9(dir)) {
|
|
2972
|
+
const abs = join26(dir, entry);
|
|
2973
|
+
const lst = lstatSync7(abs);
|
|
2974
|
+
if (lst.isSymbolicLink()) continue;
|
|
2975
|
+
if (lst.isDirectory()) {
|
|
2976
|
+
collectFiles(abs, out);
|
|
2977
|
+
continue;
|
|
2998
2978
|
}
|
|
2999
|
-
|
|
2979
|
+
if (lst.isFile()) out.push(abs);
|
|
3000
2980
|
}
|
|
3001
2981
|
}
|
|
3002
|
-
function
|
|
3003
|
-
const
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
`${yellow(warnGlyph)} ${FETCHER_BASE} (curl or wget): not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
|
|
3013
|
-
);
|
|
2982
|
+
function listSubtreeFiles(sessionDir) {
|
|
2983
|
+
const out = [];
|
|
2984
|
+
collectFiles(sessionDir, out);
|
|
2985
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
2986
|
+
}
|
|
2987
|
+
function newestSubtreeMtimeMs(mainPath, subtreeFiles, statMtime = (p) => statSync5(p).mtimeMs) {
|
|
2988
|
+
let newest = statMtime(mainPath);
|
|
2989
|
+
for (const filePath of subtreeFiles) {
|
|
2990
|
+
const t = statMtime(filePath);
|
|
2991
|
+
if (t > newest) newest = t;
|
|
3014
2992
|
}
|
|
2993
|
+
return newest;
|
|
3015
2994
|
}
|
|
3016
|
-
function
|
|
3017
|
-
const
|
|
3018
|
-
if (
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
);
|
|
2995
|
+
function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts, scan, dryRun) {
|
|
2996
|
+
const dirty = [];
|
|
2997
|
+
if (mainFindings.length > 0) dirty.push({ path: mainPath, findings: mainFindings });
|
|
2998
|
+
for (const filePath of subtreeFiles) {
|
|
2999
|
+
const raw = scan(filePath);
|
|
3000
|
+
if (raw === null || raw.length === 0) continue;
|
|
3001
|
+
const filtered = rule === void 0 ? raw : raw.filter((f) => f.RuleID === rule);
|
|
3002
|
+
if (filtered.length === 0) continue;
|
|
3003
|
+
dirty.push({ path: filePath, findings: filtered });
|
|
3025
3004
|
}
|
|
3026
|
-
|
|
3005
|
+
const total = dirty.reduce((n, e) => n + e.findings.length, 0);
|
|
3006
|
+
if (!dryRun && total > 0) {
|
|
3007
|
+
for (const { path: filePath, findings } of dirty) {
|
|
3008
|
+
backupBeforeWrite(filePath, ts);
|
|
3009
|
+
writeFileSync3(filePath, applyRedactions(readFileSync10(filePath, "utf8"), findings), "utf8");
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
return { total, dirty };
|
|
3027
3013
|
}
|
|
3028
3014
|
|
|
3029
|
-
// src/commands.
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3015
|
+
// src/commands.redact.ts
|
|
3016
|
+
init_push_gitleaks_scan();
|
|
3017
|
+
init_utils_fs();
|
|
3018
|
+
init_utils_json();
|
|
3019
|
+
init_utils();
|
|
3033
3020
|
|
|
3034
|
-
// src/
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
return { owner: m[1], repo: m[2] };
|
|
3021
|
+
// src/utils.lockfile.ts
|
|
3022
|
+
init_config();
|
|
3023
|
+
init_utils();
|
|
3024
|
+
import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync11, unlinkSync, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3025
|
+
import { dirname as dirname4, join as join27 } from "node:path";
|
|
3026
|
+
function lockFilePath() {
|
|
3027
|
+
return join27(home(), ".cache", "claude-nomad", "nomad.lock");
|
|
3042
3028
|
}
|
|
3043
|
-
function
|
|
3029
|
+
function acquireLock(verb) {
|
|
3030
|
+
const lp = lockFilePath();
|
|
3031
|
+
mkdirSync5(dirname4(lp), { recursive: true });
|
|
3044
3032
|
try {
|
|
3045
|
-
|
|
3046
|
-
|
|
3047
|
-
|
|
3048
|
-
})
|
|
3049
|
-
|
|
3033
|
+
const fd = openSync3(lp, "wx");
|
|
3034
|
+
try {
|
|
3035
|
+
writeFileSync4(fd, String(process.pid));
|
|
3036
|
+
} catch (writeErr) {
|
|
3037
|
+
try {
|
|
3038
|
+
closeSync3(fd);
|
|
3039
|
+
} catch {
|
|
3040
|
+
}
|
|
3041
|
+
try {
|
|
3042
|
+
unlinkSync(lp);
|
|
3043
|
+
} catch {
|
|
3044
|
+
}
|
|
3045
|
+
throw writeErr;
|
|
3046
|
+
}
|
|
3047
|
+
return { fd, path: lp };
|
|
3050
3048
|
} catch (err) {
|
|
3051
|
-
const
|
|
3052
|
-
if (
|
|
3053
|
-
|
|
3054
|
-
return "gh-probe-error";
|
|
3049
|
+
const code = err.code;
|
|
3050
|
+
if (code !== "EEXIST") throw err;
|
|
3051
|
+
return checkStaleAndRetry(verb, lp);
|
|
3055
3052
|
}
|
|
3056
3053
|
}
|
|
3057
|
-
function
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
timeout: GH_TIMEOUT_MS
|
|
3061
|
-
}).toString();
|
|
3062
|
-
const parsed = JSON.parse(out);
|
|
3063
|
-
return parsed.isPrivate === true;
|
|
3064
|
-
}
|
|
3065
|
-
function isActionsEnabled(ref, run = execFileSync9) {
|
|
3066
|
-
const out = run(
|
|
3067
|
-
"gh",
|
|
3068
|
-
["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
|
|
3069
|
-
{ stdio: ["ignore", "pipe", "ignore"], timeout: GH_TIMEOUT_MS }
|
|
3070
|
-
).toString().trim();
|
|
3071
|
-
return out === "true";
|
|
3072
|
-
}
|
|
3073
|
-
function disableActions(ref, run = execFileSync9) {
|
|
3074
|
-
run(
|
|
3075
|
-
"gh",
|
|
3076
|
-
[
|
|
3077
|
-
"api",
|
|
3078
|
-
"-X",
|
|
3079
|
-
"PUT",
|
|
3080
|
-
`repos/${ref.owner}/${ref.repo}/actions/permissions`,
|
|
3081
|
-
"-F",
|
|
3082
|
-
"enabled=false"
|
|
3083
|
-
],
|
|
3084
|
-
{ stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
|
|
3085
|
-
);
|
|
3086
|
-
}
|
|
3087
|
-
function readOriginRemote(cwd, run = execFileSync9) {
|
|
3088
|
-
return run("git", ["remote", "get-url", "origin"], {
|
|
3089
|
-
cwd,
|
|
3090
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
3091
|
-
}).toString().trim();
|
|
3092
|
-
}
|
|
3093
|
-
|
|
3094
|
-
// src/commands.doctor.actions-drift.ts
|
|
3095
|
-
function reportActionsDrift(section2, run = execFileSync10) {
|
|
3096
|
-
let remote;
|
|
3054
|
+
function releaseLock(handle) {
|
|
3055
|
+
if (handle === null) return;
|
|
3056
|
+
const lp = handle.path;
|
|
3097
3057
|
try {
|
|
3098
|
-
|
|
3058
|
+
closeSync3(handle.fd);
|
|
3099
3059
|
} catch {
|
|
3100
|
-
return;
|
|
3101
3060
|
}
|
|
3102
|
-
const ref = parseGitHubRemote(remote);
|
|
3103
|
-
if (ref === null) return;
|
|
3104
|
-
const auth = ghAuthStatus(run);
|
|
3105
|
-
if (auth === "gh-not-installed" || auth === "gh-not-authed") return;
|
|
3106
|
-
let isPrivate;
|
|
3107
3061
|
try {
|
|
3108
|
-
|
|
3109
|
-
} catch {
|
|
3110
|
-
|
|
3062
|
+
unlinkSync(lp);
|
|
3063
|
+
} catch (err) {
|
|
3064
|
+
if (err.code !== "ENOENT") throw err;
|
|
3111
3065
|
}
|
|
3112
|
-
|
|
3113
|
-
|
|
3066
|
+
}
|
|
3067
|
+
function unlinkIfSamePid(expectedPidStr, lp) {
|
|
3068
|
+
let current;
|
|
3114
3069
|
try {
|
|
3115
|
-
|
|
3070
|
+
current = readFileSync11(lp, "utf8").trim();
|
|
3116
3071
|
} catch {
|
|
3117
|
-
return;
|
|
3118
|
-
}
|
|
3119
|
-
if (!enabled2) return;
|
|
3120
|
-
addItem(
|
|
3121
|
-
section2,
|
|
3122
|
-
`${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')`
|
|
3123
|
-
);
|
|
3124
|
-
}
|
|
3125
|
-
|
|
3126
|
-
// src/commands.doctor.verdict.ts
|
|
3127
|
-
init_color();
|
|
3128
|
-
function isFailLine(item2) {
|
|
3129
|
-
return item2.includes(failGlyph);
|
|
3130
|
-
}
|
|
3131
|
-
function isWarnLine(item2) {
|
|
3132
|
-
return !isFailLine(item2) && item2.includes(warnGlyph);
|
|
3133
|
-
}
|
|
3134
|
-
function buildVerdictSection(sections) {
|
|
3135
|
-
const summary = section("Summary");
|
|
3136
|
-
const lines = sections.flatMap((s) => s.items).map((item2) => item2.replace(/^\t/, ""));
|
|
3137
|
-
const failures = lines.filter(isFailLine);
|
|
3138
|
-
const warnings = lines.filter(isWarnLine);
|
|
3139
|
-
for (const line of [...failures, ...warnings]) addItem(summary, line);
|
|
3140
|
-
if (failures.length > 0) {
|
|
3141
|
-
addItem(
|
|
3142
|
-
summary,
|
|
3143
|
-
`${red(failGlyph)} ${failures.length} failure(s), ${warnings.length} warning(s)`
|
|
3144
|
-
);
|
|
3145
|
-
} else if (warnings.length > 0) {
|
|
3146
|
-
addItem(summary, `${yellow(warnGlyph)} ${warnings.length} warning(s)`);
|
|
3147
|
-
} else {
|
|
3148
|
-
addItem(summary, `${green(okGlyph)} healthy`);
|
|
3149
|
-
}
|
|
3150
|
-
return summary;
|
|
3151
|
-
}
|
|
3152
|
-
|
|
3153
|
-
// src/commands.doctor.ts
|
|
3154
|
-
function cmdDoctor(opts = {}) {
|
|
3155
|
-
const host = section("Environment");
|
|
3156
|
-
reportHostAndPaths(host);
|
|
3157
|
-
reportRepoState(host);
|
|
3158
|
-
const links = section("Shared links");
|
|
3159
|
-
const mapPath = join27(repoHome(), "path-map.json");
|
|
3160
|
-
const rawMap = existsSync23(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
|
|
3161
|
-
const map = rawMap ?? { projects: {} };
|
|
3162
|
-
reportSharedLinks(links, map);
|
|
3163
|
-
const hooksScan = section("Hook targets");
|
|
3164
|
-
reportHooksTargetCheck(hooksScan);
|
|
3165
|
-
reportHookScopeCheck(hooksScan);
|
|
3166
|
-
reportPreserveSymlinksCheck(hooksScan);
|
|
3167
|
-
const settings = section("Settings");
|
|
3168
|
-
const base = loadBaseSettings(settings);
|
|
3169
|
-
const parsedSettings = loadAndReportSettings(settings);
|
|
3170
|
-
reportHostOverrides(settings, base, parsedSettings);
|
|
3171
|
-
reportSettingsDriftCheck(settings);
|
|
3172
|
-
const pathMap = section("Path map");
|
|
3173
|
-
reportPathMap(pathMap);
|
|
3174
|
-
const neverSync = section("Never-sync");
|
|
3175
|
-
reportNeverSync(neverSync);
|
|
3176
|
-
const repository = section("Repository");
|
|
3177
|
-
const gitleaksReady = reportGitleaksProbe(repository);
|
|
3178
|
-
reportGitlinks(repository);
|
|
3179
|
-
reportRemote(repository);
|
|
3180
|
-
reportRebaseClean(repository);
|
|
3181
|
-
reportRebaseState(repository);
|
|
3182
|
-
reportActionsDrift(repository);
|
|
3183
|
-
const nomadVersion = section("Nomad Version");
|
|
3184
|
-
reportVersionCheck(nomadVersion);
|
|
3185
|
-
const housekeeping = section("Housekeeping");
|
|
3186
|
-
reportBackupsCheck(housekeeping);
|
|
3187
|
-
const depVersions = section("Dependency Versions");
|
|
3188
|
-
reportNodeEngineCheck(depVersions);
|
|
3189
|
-
reportGitleaksVersionCheck(depVersions);
|
|
3190
|
-
reportOptionalDeps(depVersions);
|
|
3191
|
-
const sharedScan = section("Shared scan");
|
|
3192
|
-
if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
|
|
3193
|
-
const schemaScan = section("Schema scan");
|
|
3194
|
-
if (opts.checkSchema === true) reportCheckSchema(schemaScan);
|
|
3195
|
-
const body = [
|
|
3196
|
-
nomadVersion,
|
|
3197
|
-
depVersions,
|
|
3198
|
-
host,
|
|
3199
|
-
links,
|
|
3200
|
-
hooksScan,
|
|
3201
|
-
settings,
|
|
3202
|
-
pathMap,
|
|
3203
|
-
neverSync,
|
|
3204
|
-
repository,
|
|
3205
|
-
housekeeping,
|
|
3206
|
-
sharedScan,
|
|
3207
|
-
schemaScan
|
|
3208
|
-
];
|
|
3209
|
-
renderDoctor([...body, buildVerdictSection(body)]);
|
|
3210
|
-
}
|
|
3211
|
-
|
|
3212
|
-
// src/commands.drop-session.ts
|
|
3213
|
-
init_config();
|
|
3214
|
-
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
3215
|
-
import { existsSync as existsSync25, readdirSync as readdirSync9, statSync as statSync5 } from "node:fs";
|
|
3216
|
-
import { join as join30, relative as relative4 } from "node:path";
|
|
3217
|
-
|
|
3218
|
-
// src/commands.drop-session.git.ts
|
|
3219
|
-
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
3220
|
-
function expandStagedDir(dirRel, repo) {
|
|
3221
|
-
try {
|
|
3222
|
-
const out = execFileSync11("git", ["ls-files", "-z", "--", dirRel], {
|
|
3223
|
-
cwd: repo,
|
|
3224
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3225
|
-
});
|
|
3226
|
-
return out.toString().split("\0").filter((p) => p !== "");
|
|
3227
|
-
} catch {
|
|
3228
|
-
return [];
|
|
3229
|
-
}
|
|
3230
|
-
}
|
|
3231
|
-
function isTrackedInHead(rel, repo) {
|
|
3232
|
-
try {
|
|
3233
|
-
execFileSync11("git", ["cat-file", "-e", `HEAD:${rel}`], {
|
|
3234
|
-
cwd: repo,
|
|
3235
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3236
|
-
});
|
|
3237
|
-
return true;
|
|
3238
|
-
} catch {
|
|
3239
|
-
return false;
|
|
3240
|
-
}
|
|
3241
|
-
}
|
|
3242
|
-
function isInIndex(rel, repo) {
|
|
3243
|
-
try {
|
|
3244
|
-
const out = execFileSync11("git", ["ls-files", "--", rel], {
|
|
3245
|
-
cwd: repo,
|
|
3246
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3247
|
-
});
|
|
3248
|
-
return out.toString().trim() !== "";
|
|
3249
|
-
} catch {
|
|
3250
|
-
return false;
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
|
|
3254
|
-
// src/commands.drop-session.scrub-hint.ts
|
|
3255
|
-
init_config();
|
|
3256
|
-
init_utils();
|
|
3257
|
-
init_utils_json();
|
|
3258
|
-
import { existsSync as existsSync24 } from "node:fs";
|
|
3259
|
-
import { join as join28 } from "node:path";
|
|
3260
|
-
var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
|
|
3261
|
-
function reportScrubHint(id, matches) {
|
|
3262
|
-
const live = resolveLiveTranscript(id, matches);
|
|
3263
|
-
const target = live ?? `~/.claude/projects/<encoded>/${id}.jsonl`;
|
|
3264
|
-
log(
|
|
3265
|
-
`note: this only un-stages the session from the next push.
|
|
3266
|
-
The local source still contains the secret, so nomad push re-stages it
|
|
3267
|
-
on the next run and nomad doctor --check-shared keeps reporting it.
|
|
3268
|
-
To fully remediate: rotate the credential, then run:
|
|
3269
|
-
nomad redact ${id}
|
|
3270
|
-
(or scrub ${target} manually)`
|
|
3271
|
-
);
|
|
3272
|
-
}
|
|
3273
|
-
function resolveLiveTranscript(id, matches) {
|
|
3274
|
-
try {
|
|
3275
|
-
const mapPath = join28(repoHome(), "path-map.json");
|
|
3276
|
-
if (!existsSync24(mapPath)) return null;
|
|
3277
|
-
const projects = readJson(mapPath).projects;
|
|
3278
|
-
const claude = claudeHome();
|
|
3279
|
-
for (const rel of matches) {
|
|
3280
|
-
const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
|
|
3281
|
-
if (logical === void 0) continue;
|
|
3282
|
-
const abs = projects[logical]?.[HOST];
|
|
3283
|
-
if (abs === void 0) continue;
|
|
3284
|
-
const live = join28(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
3285
|
-
if (existsSync24(live)) return live;
|
|
3286
|
-
}
|
|
3287
|
-
return null;
|
|
3288
|
-
} catch {
|
|
3289
|
-
return null;
|
|
3290
|
-
}
|
|
3291
|
-
}
|
|
3292
|
-
|
|
3293
|
-
// src/commands.drop-session.ts
|
|
3294
|
-
init_utils();
|
|
3295
|
-
|
|
3296
|
-
// src/utils.lockfile.ts
|
|
3297
|
-
init_config();
|
|
3298
|
-
init_utils();
|
|
3299
|
-
import { closeSync as closeSync3, mkdirSync as mkdirSync5, openSync as openSync3, readFileSync as readFileSync10, unlinkSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
3300
|
-
import { dirname as dirname4, join as join29 } from "node:path";
|
|
3301
|
-
function lockFilePath() {
|
|
3302
|
-
return join29(home(), ".cache", "claude-nomad", "nomad.lock");
|
|
3303
|
-
}
|
|
3304
|
-
function acquireLock(verb) {
|
|
3305
|
-
const lp = lockFilePath();
|
|
3306
|
-
mkdirSync5(dirname4(lp), { recursive: true });
|
|
3307
|
-
try {
|
|
3308
|
-
const fd = openSync3(lp, "wx");
|
|
3309
|
-
try {
|
|
3310
|
-
writeFileSync3(fd, String(process.pid));
|
|
3311
|
-
} catch (writeErr) {
|
|
3312
|
-
try {
|
|
3313
|
-
closeSync3(fd);
|
|
3314
|
-
} catch {
|
|
3315
|
-
}
|
|
3316
|
-
try {
|
|
3317
|
-
unlinkSync(lp);
|
|
3318
|
-
} catch {
|
|
3319
|
-
}
|
|
3320
|
-
throw writeErr;
|
|
3321
|
-
}
|
|
3322
|
-
return { fd, path: lp };
|
|
3323
|
-
} catch (err) {
|
|
3324
|
-
const code = err.code;
|
|
3325
|
-
if (code !== "EEXIST") throw err;
|
|
3326
|
-
return checkStaleAndRetry(verb, lp);
|
|
3327
|
-
}
|
|
3328
|
-
}
|
|
3329
|
-
function releaseLock(handle) {
|
|
3330
|
-
if (handle === null) return;
|
|
3331
|
-
const lp = handle.path;
|
|
3332
|
-
try {
|
|
3333
|
-
closeSync3(handle.fd);
|
|
3334
|
-
} catch {
|
|
3335
|
-
}
|
|
3336
|
-
try {
|
|
3337
|
-
unlinkSync(lp);
|
|
3338
|
-
} catch (err) {
|
|
3339
|
-
if (err.code !== "ENOENT") throw err;
|
|
3340
|
-
}
|
|
3341
|
-
}
|
|
3342
|
-
function unlinkIfSamePid(expectedPidStr, lp) {
|
|
3343
|
-
let current;
|
|
3344
|
-
try {
|
|
3345
|
-
current = readFileSync10(lp, "utf8").trim();
|
|
3346
|
-
} catch {
|
|
3347
|
-
return false;
|
|
3072
|
+
return false;
|
|
3348
3073
|
}
|
|
3349
3074
|
if (current !== expectedPidStr) return false;
|
|
3350
3075
|
try {
|
|
@@ -3357,7 +3082,7 @@ function unlinkIfSamePid(expectedPidStr, lp) {
|
|
|
3357
3082
|
function checkStaleAndRetry(verb, lp) {
|
|
3358
3083
|
let pidStr;
|
|
3359
3084
|
try {
|
|
3360
|
-
pidStr =
|
|
3085
|
+
pidStr = readFileSync11(lp, "utf8").trim();
|
|
3361
3086
|
} catch {
|
|
3362
3087
|
pidStr = "";
|
|
3363
3088
|
}
|
|
@@ -3386,7 +3111,7 @@ function retryOnce(verb, lp) {
|
|
|
3386
3111
|
try {
|
|
3387
3112
|
const fd = openSync3(lp, "wx");
|
|
3388
3113
|
try {
|
|
3389
|
-
|
|
3114
|
+
writeFileSync4(fd, String(process.pid));
|
|
3390
3115
|
} catch {
|
|
3391
3116
|
try {
|
|
3392
3117
|
closeSync3(fd);
|
|
@@ -3406,193 +3131,59 @@ function retryOnce(verb, lp) {
|
|
|
3406
3131
|
}
|
|
3407
3132
|
}
|
|
3408
3133
|
|
|
3409
|
-
// src/commands.
|
|
3410
|
-
function
|
|
3134
|
+
// src/commands.redact.ts
|
|
3135
|
+
function resolveLiveTranscript(id) {
|
|
3136
|
+
try {
|
|
3137
|
+
const mapPath = join28(repoHome(), "path-map.json");
|
|
3138
|
+
if (!existsSync23(mapPath)) return null;
|
|
3139
|
+
const projects = readJson(mapPath).projects;
|
|
3140
|
+
const claude = claudeHome();
|
|
3141
|
+
for (const hostMap of Object.values(projects)) {
|
|
3142
|
+
const abs = hostMap[HOST];
|
|
3143
|
+
if (abs === void 0) continue;
|
|
3144
|
+
const live = join28(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
3145
|
+
if (existsSync23(live)) return live;
|
|
3146
|
+
}
|
|
3147
|
+
return null;
|
|
3148
|
+
} catch {
|
|
3149
|
+
return null;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
function resolveRedactFindings(localPath, rawFindings, rule, scan) {
|
|
3153
|
+
const source = rawFindings ?? scan(localPath);
|
|
3154
|
+
if (source === null) return null;
|
|
3155
|
+
return source.filter((f) => rule === void 0 || f.RuleID === rule);
|
|
3156
|
+
}
|
|
3157
|
+
function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
|
|
3158
|
+
const { id, rule, dryRun = false, findings: rawFindings } = opts;
|
|
3411
3159
|
if (id.length === 0 || id.length > 128 || !/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
3412
3160
|
fail(`invalid session id: ${id}`);
|
|
3413
3161
|
process.exit(1);
|
|
3414
3162
|
}
|
|
3415
3163
|
const repo = repoHome();
|
|
3416
|
-
|
|
3417
|
-
|
|
3164
|
+
const backup = backupBase();
|
|
3165
|
+
if (!existsSync23(repo)) die(`repo not cloned at ${repo}`);
|
|
3166
|
+
const handle = acquireLock("redact");
|
|
3418
3167
|
if (handle === null) process.exit(0);
|
|
3419
3168
|
try {
|
|
3420
|
-
const
|
|
3421
|
-
if (!
|
|
3422
|
-
|
|
3169
|
+
const localPath = resolveLiveTranscript(id);
|
|
3170
|
+
if (localPath === null || !existsSync23(localPath)) {
|
|
3171
|
+
fail(`could not resolve local transcript for session ${id} on this host`);
|
|
3172
|
+
process.exitCode = 1;
|
|
3173
|
+
return;
|
|
3423
3174
|
}
|
|
3424
|
-
const
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
} finally {
|
|
3437
|
-
releaseLock(handle);
|
|
3438
|
-
}
|
|
3439
|
-
}
|
|
3440
|
-
function collectMatches(repoProjects, id, repo) {
|
|
3441
|
-
const matches = [];
|
|
3442
|
-
for (const logical of readdirSync9(repoProjects)) {
|
|
3443
|
-
const candidate = join30(repoProjects, logical, `${id}.jsonl`);
|
|
3444
|
-
if (existsSync25(candidate)) {
|
|
3445
|
-
matches.push(relative4(repo, candidate));
|
|
3446
|
-
}
|
|
3447
|
-
const dir = join30(repoProjects, logical, id);
|
|
3448
|
-
if (existsSync25(dir) && statSync5(dir).isDirectory()) {
|
|
3449
|
-
const dirRel = relative4(repo, dir);
|
|
3450
|
-
const staged = expandStagedDir(dirRel, repo);
|
|
3451
|
-
if (staged.length > 0) matches.push(...staged);
|
|
3452
|
-
else matches.push(dirRel);
|
|
3453
|
-
}
|
|
3454
|
-
}
|
|
3455
|
-
return matches;
|
|
3456
|
-
}
|
|
3457
|
-
function unstageOne(rel, repo) {
|
|
3458
|
-
if (!isInIndex(rel, repo)) {
|
|
3459
|
-
item(`dropped ${rel} (already absent from index)`);
|
|
3460
|
-
return;
|
|
3461
|
-
}
|
|
3462
|
-
try {
|
|
3463
|
-
if (isTrackedInHead(rel, repo)) {
|
|
3464
|
-
execFileSync12("git", ["restore", "--staged", "--worktree", "--", rel], {
|
|
3465
|
-
cwd: repo,
|
|
3466
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3467
|
-
});
|
|
3468
|
-
} else {
|
|
3469
|
-
execFileSync12("git", ["rm", "--cached", "-f", "--", rel], {
|
|
3470
|
-
cwd: repo,
|
|
3471
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3472
|
-
});
|
|
3473
|
-
}
|
|
3474
|
-
} catch (err) {
|
|
3475
|
-
const e = err;
|
|
3476
|
-
const detail = e.stderr?.toString().trim() ?? e.message;
|
|
3477
|
-
throw new NomadFatal(`git failed to unstage ${rel}: ${detail}`);
|
|
3478
|
-
}
|
|
3479
|
-
item(`dropped ${rel}`);
|
|
3480
|
-
}
|
|
3481
|
-
|
|
3482
|
-
// src/commands.redact.ts
|
|
3483
|
-
init_config();
|
|
3484
|
-
import { existsSync as existsSync27, statSync as statSync7 } from "node:fs";
|
|
3485
|
-
import { dirname as dirname5, join as join32 } from "node:path";
|
|
3486
|
-
|
|
3487
|
-
// src/commands.redact.subtree.ts
|
|
3488
|
-
import { existsSync as existsSync26, lstatSync as lstatSync7, readFileSync as readFileSync11, readdirSync as readdirSync10, statSync as statSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
3489
|
-
import { join as join31 } from "node:path";
|
|
3490
|
-
init_utils_fs();
|
|
3491
|
-
function collectFiles(dir, out) {
|
|
3492
|
-
if (!existsSync26(dir)) return;
|
|
3493
|
-
const st = lstatSync7(dir);
|
|
3494
|
-
if (!st.isDirectory()) return;
|
|
3495
|
-
for (const entry of readdirSync10(dir)) {
|
|
3496
|
-
const abs = join31(dir, entry);
|
|
3497
|
-
const lst = lstatSync7(abs);
|
|
3498
|
-
if (lst.isSymbolicLink()) continue;
|
|
3499
|
-
if (lst.isDirectory()) {
|
|
3500
|
-
collectFiles(abs, out);
|
|
3501
|
-
continue;
|
|
3502
|
-
}
|
|
3503
|
-
if (lst.isFile()) out.push(abs);
|
|
3504
|
-
}
|
|
3505
|
-
}
|
|
3506
|
-
function listSubtreeFiles(sessionDir) {
|
|
3507
|
-
const out = [];
|
|
3508
|
-
collectFiles(sessionDir, out);
|
|
3509
|
-
return out.sort((a, b) => a.localeCompare(b));
|
|
3510
|
-
}
|
|
3511
|
-
function newestSubtreeMtimeMs(mainPath, subtreeFiles, statMtime = (p) => statSync6(p).mtimeMs) {
|
|
3512
|
-
let newest = statMtime(mainPath);
|
|
3513
|
-
for (const filePath of subtreeFiles) {
|
|
3514
|
-
const t = statMtime(filePath);
|
|
3515
|
-
if (t > newest) newest = t;
|
|
3516
|
-
}
|
|
3517
|
-
return newest;
|
|
3518
|
-
}
|
|
3519
|
-
function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts, scan, dryRun) {
|
|
3520
|
-
const dirty = [];
|
|
3521
|
-
if (mainFindings.length > 0) dirty.push({ path: mainPath, findings: mainFindings });
|
|
3522
|
-
for (const filePath of subtreeFiles) {
|
|
3523
|
-
const raw = scan(filePath);
|
|
3524
|
-
if (raw === null || raw.length === 0) continue;
|
|
3525
|
-
const filtered = rule === void 0 ? raw : raw.filter((f) => f.RuleID === rule);
|
|
3526
|
-
if (filtered.length === 0) continue;
|
|
3527
|
-
dirty.push({ path: filePath, findings: filtered });
|
|
3528
|
-
}
|
|
3529
|
-
const total = dirty.reduce((n, e) => n + e.findings.length, 0);
|
|
3530
|
-
if (!dryRun && total > 0) {
|
|
3531
|
-
for (const { path: filePath, findings } of dirty) {
|
|
3532
|
-
backupBeforeWrite(filePath, ts);
|
|
3533
|
-
writeFileSync4(filePath, applyRedactions(readFileSync11(filePath, "utf8"), findings), "utf8");
|
|
3534
|
-
}
|
|
3535
|
-
}
|
|
3536
|
-
return { total, dirty };
|
|
3537
|
-
}
|
|
3538
|
-
|
|
3539
|
-
// src/commands.redact.ts
|
|
3540
|
-
init_push_gitleaks_scan();
|
|
3541
|
-
init_utils_fs();
|
|
3542
|
-
init_utils_json();
|
|
3543
|
-
init_utils();
|
|
3544
|
-
function resolveLiveTranscript2(id) {
|
|
3545
|
-
try {
|
|
3546
|
-
const mapPath = join32(repoHome(), "path-map.json");
|
|
3547
|
-
if (!existsSync27(mapPath)) return null;
|
|
3548
|
-
const projects = readJson(mapPath).projects;
|
|
3549
|
-
const claude = claudeHome();
|
|
3550
|
-
for (const hostMap of Object.values(projects)) {
|
|
3551
|
-
const abs = hostMap[HOST];
|
|
3552
|
-
if (abs === void 0) continue;
|
|
3553
|
-
const live = join32(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
3554
|
-
if (existsSync27(live)) return live;
|
|
3555
|
-
}
|
|
3556
|
-
return null;
|
|
3557
|
-
} catch {
|
|
3558
|
-
return null;
|
|
3559
|
-
}
|
|
3560
|
-
}
|
|
3561
|
-
function resolveRedactFindings(localPath, rawFindings, rule, scan) {
|
|
3562
|
-
const source = rawFindings ?? scan(localPath);
|
|
3563
|
-
if (source === null) return null;
|
|
3564
|
-
return source.filter((f) => rule === void 0 || f.RuleID === rule);
|
|
3565
|
-
}
|
|
3566
|
-
function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
|
|
3567
|
-
const { id, rule, dryRun = false, findings: rawFindings } = opts;
|
|
3568
|
-
if (id.length === 0 || id.length > 128 || !/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
3569
|
-
fail(`invalid session id: ${id}`);
|
|
3570
|
-
process.exit(1);
|
|
3571
|
-
}
|
|
3572
|
-
const repo = repoHome();
|
|
3573
|
-
const backup = backupBase();
|
|
3574
|
-
if (!existsSync27(repo)) die(`repo not cloned at ${repo}`);
|
|
3575
|
-
const handle = acquireLock("redact");
|
|
3576
|
-
if (handle === null) process.exit(0);
|
|
3577
|
-
try {
|
|
3578
|
-
const localPath = resolveLiveTranscript2(id);
|
|
3579
|
-
if (localPath === null || !existsSync27(localPath)) {
|
|
3580
|
-
fail(`could not resolve local transcript for session ${id} on this host`);
|
|
3581
|
-
process.exitCode = 1;
|
|
3582
|
-
return;
|
|
3583
|
-
}
|
|
3584
|
-
const sessionDir = join32(dirname5(localPath), id);
|
|
3585
|
-
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
3586
|
-
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
|
|
3587
|
-
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
3588
|
-
log(
|
|
3589
|
-
`session ${id} was modified recently and may be active.
|
|
3590
|
-
Refusing to rewrite a potentially live transcript.
|
|
3591
|
-
To proceed: wait for the session to end, then re-run nomad redact.
|
|
3592
|
-
Or drop from the staged tree: nomad drop-session ${id}
|
|
3593
|
-
Or skip this finding during nomad push.`
|
|
3594
|
-
);
|
|
3595
|
-
return;
|
|
3175
|
+
const sessionDir = join28(dirname5(localPath), id);
|
|
3176
|
+
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
3177
|
+
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync6(p).mtimeMs);
|
|
3178
|
+
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
3179
|
+
log(
|
|
3180
|
+
`session ${id} was modified recently and may be active.
|
|
3181
|
+
Refusing to rewrite a potentially live transcript.
|
|
3182
|
+
To proceed: wait for the session to end, then re-run nomad redact.
|
|
3183
|
+
Or drop from the staged tree: nomad drop-session ${id}
|
|
3184
|
+
Or skip this finding during nomad push.`
|
|
3185
|
+
);
|
|
3186
|
+
return;
|
|
3596
3187
|
}
|
|
3597
3188
|
const mainFindings = resolveRedactFindings(localPath, rawFindings, rule, scan);
|
|
3598
3189
|
if (mainFindings === null) {
|
|
@@ -3633,1142 +3224,1651 @@ ${lines}`);
|
|
|
3633
3224
|
}
|
|
3634
3225
|
}
|
|
3635
3226
|
|
|
3636
|
-
// src/commands.
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
3640
|
-
// src/commands.push.sections.ts
|
|
3641
|
-
init_color();
|
|
3642
|
-
|
|
3643
|
-
// src/summary.ts
|
|
3644
|
-
init_color();
|
|
3227
|
+
// src/commands.push.recovery.redact.ts
|
|
3228
|
+
init_push_gitleaks_scan();
|
|
3229
|
+
init_utils_json();
|
|
3645
3230
|
init_utils();
|
|
3646
|
-
function summaryText(verb, unmapped, collisions = 0, extrasSkipped = 0) {
|
|
3647
|
-
const extras = extrasSkipped > 0 ? `, ${extrasSkipped} extras skipped` : "";
|
|
3648
|
-
if (verb === "push") {
|
|
3649
|
-
if (unmapped === 0 && collisions === 0 && extrasSkipped === 0) {
|
|
3650
|
-
return { text: "summary: clean", clean: true };
|
|
3651
|
-
}
|
|
3652
|
-
const base = `summary: ${unmapped} unmapped on push, ${collisions} collisions`;
|
|
3653
|
-
return { text: `${base}${extras} (run nomad doctor to list)`, clean: false };
|
|
3654
|
-
}
|
|
3655
|
-
if (unmapped === 0 && extrasSkipped === 0) {
|
|
3656
|
-
return { text: "summary: clean", clean: true };
|
|
3657
|
-
}
|
|
3658
|
-
return {
|
|
3659
|
-
text: `summary: ${unmapped} unmapped on ${verb}${extras} (run nomad doctor to list)`,
|
|
3660
|
-
clean: false
|
|
3661
|
-
};
|
|
3662
|
-
}
|
|
3663
|
-
function summaryRow(verb, unmapped, collisions = 0, extrasSkipped = 0) {
|
|
3664
|
-
const { text, clean } = summaryText(verb, unmapped, collisions, extrasSkipped);
|
|
3665
|
-
return clean ? `${green(okGlyph)} ${text}` : `${yellow(warnGlyph)} ${text}`;
|
|
3666
|
-
}
|
|
3667
3231
|
|
|
3668
|
-
// src/commands.push.
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
return s;
|
|
3677
|
-
}
|
|
3678
|
-
function buildSessionsSection(items, unmapped) {
|
|
3679
|
-
const s = section("Sessions");
|
|
3680
|
-
for (const logical of items) addItem(s, `${green(okGlyph)} ${logical}`);
|
|
3681
|
-
const skip = collapsedSkipRow(unmapped, "not in path-map (run nomad doctor to list)");
|
|
3682
|
-
if (skip !== null) addItem(s, skip);
|
|
3683
|
-
return s;
|
|
3684
|
-
}
|
|
3685
|
-
function buildExtrasSection(items, extrasSkipped) {
|
|
3686
|
-
const s = section("Extras");
|
|
3687
|
-
for (const entry of items) addItem(s, `${green(okGlyph)} ${entry}`);
|
|
3688
|
-
const skip = collapsedSkipRow(extrasSkipped, "extras skipped");
|
|
3689
|
-
if (skip !== null) addItem(s, skip);
|
|
3690
|
-
return s;
|
|
3691
|
-
}
|
|
3692
|
-
function syncedSections(st) {
|
|
3693
|
-
const sessions = st.dryRun ? st.remap.wouldPush : st.remap.pushed;
|
|
3694
|
-
const extras = st.dryRun ? st.extras.wouldPush : st.extras.pushed;
|
|
3695
|
-
return [
|
|
3696
|
-
buildSessionsSection(sessions, st.remap.unmapped),
|
|
3697
|
-
buildExtrasSection(extras, st.extras.skipped)
|
|
3698
|
-
];
|
|
3232
|
+
// src/commands.push.recovery.seams.ts
|
|
3233
|
+
init_push_gitleaks();
|
|
3234
|
+
var MASK_LEAD = 4;
|
|
3235
|
+
var MASK_BODY = "************";
|
|
3236
|
+
var CONTEXT_WINDOW = 40;
|
|
3237
|
+
var CONTROL_CHARS = /[\x00-\x1f\x7f]/g;
|
|
3238
|
+
function findingKey(f) {
|
|
3239
|
+
return `${f.File}:${f.StartLine}:${f.StartColumn}:${f.RuleID}`;
|
|
3699
3240
|
}
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
const
|
|
3703
|
-
|
|
3704
|
-
|
|
3241
|
+
var VALID_SID = /^[A-Za-z0-9_-]+$/;
|
|
3242
|
+
function sessionIdFromFinding(f) {
|
|
3243
|
+
const m = SESSION_PATH.exec(f.File) ?? /^shared\/projects\/[^/]+\/([^/]+)\//.exec(f.File);
|
|
3244
|
+
if (m === null) return null;
|
|
3245
|
+
const sid = m[1];
|
|
3246
|
+
return VALID_SID.test(sid) ? sid : null;
|
|
3705
3247
|
}
|
|
3706
|
-
function
|
|
3707
|
-
const
|
|
3708
|
-
|
|
3709
|
-
|
|
3248
|
+
function parseAction(raw) {
|
|
3249
|
+
const t = raw.trim().toLowerCase();
|
|
3250
|
+
if (t === "r" || t === "redact") return "redact";
|
|
3251
|
+
if (t === "a" || t === "allow") return "allow";
|
|
3252
|
+
if (t === "d" || t === "drop") return "drop";
|
|
3253
|
+
return "skip";
|
|
3710
3254
|
}
|
|
3711
|
-
function
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3255
|
+
function maskSecret(secret) {
|
|
3256
|
+
return secret.slice(0, MASK_LEAD) + MASK_BODY;
|
|
3257
|
+
}
|
|
3258
|
+
function buildFindingContext(finding, readLine) {
|
|
3259
|
+
const raw = readLine(finding.File, finding.StartLine);
|
|
3260
|
+
if (raw !== null) {
|
|
3261
|
+
const len = raw.length;
|
|
3262
|
+
const startCol = Math.max(1, Math.min(finding.StartColumn, len + 1));
|
|
3263
|
+
const endCol = Math.max(startCol, Math.min(finding.EndColumn, len));
|
|
3264
|
+
const spanStart = startCol - 1;
|
|
3265
|
+
const spanEnd = endCol;
|
|
3266
|
+
const secret = raw.slice(spanStart, spanEnd);
|
|
3267
|
+
const masked = maskSecret(secret);
|
|
3268
|
+
const fullPrefix = raw.slice(0, spanStart);
|
|
3269
|
+
const fullSuffix = raw.slice(spanEnd);
|
|
3270
|
+
const prefixTruncated = fullPrefix.length > CONTEXT_WINDOW;
|
|
3271
|
+
const suffixTruncated = fullSuffix.length > CONTEXT_WINDOW;
|
|
3272
|
+
const prefix = prefixTruncated ? fullPrefix.slice(fullPrefix.length - CONTEXT_WINDOW) : fullPrefix;
|
|
3273
|
+
const suffix = suffixTruncated ? fullSuffix.slice(0, CONTEXT_WINDOW) : fullSuffix;
|
|
3274
|
+
const excerpt = (prefixTruncated ? "..." : "") + prefix + masked + suffix + (suffixTruncated ? "..." : "");
|
|
3275
|
+
const stripped = excerpt.replace(CONTROL_CHARS, "");
|
|
3276
|
+
if (stripped.trim().length > 0) return stripped;
|
|
3277
|
+
}
|
|
3278
|
+
if (finding.Match.length > 0) {
|
|
3279
|
+
return maskSecret(finding.Match).replace(CONTROL_CHARS, "");
|
|
3717
3280
|
}
|
|
3718
|
-
|
|
3281
|
+
return null;
|
|
3719
3282
|
}
|
|
3720
3283
|
|
|
3721
|
-
// src/commands.
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
// src/extras-sync.diff.ts
|
|
3730
|
-
init_utils();
|
|
3731
|
-
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
3732
|
-
function labelDiffLine(line) {
|
|
3733
|
-
const tab = line.indexOf(" ");
|
|
3734
|
-
if (tab === -1) return line;
|
|
3735
|
-
const status = line.slice(0, tab);
|
|
3736
|
-
const path = line.slice(tab + 1);
|
|
3737
|
-
if (status === "D") return `${path} (local only)`;
|
|
3738
|
-
if (status === "A") return `${path} (repo only)`;
|
|
3739
|
-
return path;
|
|
3740
|
-
}
|
|
3741
|
-
function parseDiffOutput(stdout) {
|
|
3742
|
-
return stdout.split("\n").filter((line) => line.length > 0).map(labelDiffLine);
|
|
3743
|
-
}
|
|
3744
|
-
function listDivergingFiles(a, b) {
|
|
3745
|
-
try {
|
|
3746
|
-
const stdout = execFileSync13("git", ["diff", "--no-index", "--name-status", a, b], {
|
|
3747
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
3748
|
-
}).toString();
|
|
3749
|
-
return parseDiffOutput(stdout);
|
|
3750
|
-
} catch (err) {
|
|
3751
|
-
const e = err;
|
|
3752
|
-
if (e.status === 1 && e.stdout !== void 0) {
|
|
3753
|
-
return parseDiffOutput(e.stdout.toString());
|
|
3754
|
-
}
|
|
3755
|
-
if (e.code === "ENOENT") {
|
|
3756
|
-
warn(`git not on PATH; divergence check skipped for ${a}`);
|
|
3757
|
-
return [];
|
|
3284
|
+
// src/commands.push.recovery.redact.ts
|
|
3285
|
+
function resolveStagedDir(localPath, map, claude, repo) {
|
|
3286
|
+
for (const [logical, hostMap] of Object.entries(map.projects)) {
|
|
3287
|
+
assertSafeLogical(logical);
|
|
3288
|
+
const abs = hostMap[HOST];
|
|
3289
|
+
if (abs === void 0) continue;
|
|
3290
|
+
if (localPath.startsWith(join29(claude, "projects", encodePath(abs)) + sep3)) {
|
|
3291
|
+
return join29(repo, "shared", "projects", logical);
|
|
3758
3292
|
}
|
|
3759
|
-
warn(`divergence check failed for ${a}: ${e.message ?? String(err)}`);
|
|
3760
|
-
return [];
|
|
3761
3293
|
}
|
|
3294
|
+
return null;
|
|
3762
3295
|
}
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
if (!isAbsolute(localRoot)) {
|
|
3775
|
-
throw new NomadFatal(
|
|
3776
|
-
`invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
|
|
3296
|
+
function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
3297
|
+
const refuse = (msg) => {
|
|
3298
|
+
log(msg);
|
|
3299
|
+
return false;
|
|
3300
|
+
};
|
|
3301
|
+
const claude = claudeHome();
|
|
3302
|
+
const repo = repoHome();
|
|
3303
|
+
const sid = sessionIdFromFinding(f);
|
|
3304
|
+
if (sid === null) {
|
|
3305
|
+
return refuse(
|
|
3306
|
+
`could not locate the local transcript for this finding; choose Skip or Drop session.`
|
|
3777
3307
|
);
|
|
3778
3308
|
}
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3309
|
+
const localPath = resolveLiveTranscript(sid);
|
|
3310
|
+
if (localPath === null) {
|
|
3311
|
+
return refuse(
|
|
3312
|
+
`could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
3782
3313
|
);
|
|
3783
3314
|
}
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
const repoExtras = join33(repo, "shared", "extras");
|
|
3793
|
-
if (!existsSync28(mapPath) || opts.requireRepoExtras === true && !existsSync28(repoExtras)) {
|
|
3794
|
-
if (opts.missingMsg !== void 0) log(opts.missingMsg);
|
|
3795
|
-
return null;
|
|
3796
|
-
}
|
|
3797
|
-
const map = readPathMap(mapPath);
|
|
3798
|
-
const extrasMap = map.extras ?? {};
|
|
3799
|
-
if (Object.keys(extrasMap).length === 0) return null;
|
|
3800
|
-
for (const logical of Object.keys(extrasMap)) {
|
|
3801
|
-
assertSafeLogical(logical);
|
|
3802
|
-
const localRoot = map.projects[logical]?.[HOST];
|
|
3803
|
-
if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
|
|
3315
|
+
const sessionDir = join29(dirname6(localPath), sid);
|
|
3316
|
+
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
3317
|
+
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync7(p).mtimeMs);
|
|
3318
|
+
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
3319
|
+
return refuse(
|
|
3320
|
+
`session ${sid} looks active (modified within the last 5 minutes); refusing to redact, no changes made.
|
|
3321
|
+
End the session and choose Redact again, or choose Drop session (holds this session back from the push, local copy kept) or Skip.`
|
|
3322
|
+
);
|
|
3804
3323
|
}
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
const localRoot = v.map.projects[logical]?.[HOST];
|
|
3811
|
-
if (!localRoot || localRoot === "TBD") {
|
|
3812
|
-
counts.unmapped++;
|
|
3813
|
-
continue;
|
|
3814
|
-
}
|
|
3815
|
-
for (const dirname7 of dirnames) {
|
|
3816
|
-
if (!whitelist.includes(dirname7)) {
|
|
3817
|
-
counts.skipped++;
|
|
3818
|
-
continue;
|
|
3819
|
-
}
|
|
3820
|
-
yield { logical, localRoot, dirname: dirname7 };
|
|
3821
|
-
}
|
|
3324
|
+
const stagedProjectDir = resolveStagedDir(localPath, map, claude, repo);
|
|
3325
|
+
if (stagedProjectDir === null) {
|
|
3326
|
+
return refuse(
|
|
3327
|
+
`could not map the local transcript for session ${sid} to a staged copy; choose Drop session or Skip.`
|
|
3328
|
+
);
|
|
3822
3329
|
}
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
cpSync5(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
|
|
3827
|
-
}
|
|
3828
|
-
|
|
3829
|
-
// src/extras-sync.ts
|
|
3830
|
-
init_utils();
|
|
3831
|
-
init_utils_json();
|
|
3832
|
-
|
|
3833
|
-
// src/extras-sync.remap.ts
|
|
3834
|
-
init_config();
|
|
3835
|
-
import { existsSync as existsSync29, mkdirSync as mkdirSync6 } from "node:fs";
|
|
3836
|
-
import { join as join34 } from "node:path";
|
|
3837
|
-
init_utils_fs();
|
|
3838
|
-
function runExtrasOp(v, dryRun, paths, backup) {
|
|
3839
|
-
const counts = { unmapped: 0, skipped: 0 };
|
|
3840
|
-
const done = [];
|
|
3841
|
-
const would = [];
|
|
3842
|
-
for (const t of eachExtrasTarget(v, counts)) {
|
|
3843
|
-
const { src, dst } = paths(t);
|
|
3844
|
-
if (!existsSync29(src)) continue;
|
|
3845
|
-
const item2 = `${t.logical}/${t.dirname}`;
|
|
3846
|
-
if (dryRun) {
|
|
3847
|
-
would.push(item2);
|
|
3848
|
-
continue;
|
|
3849
|
-
}
|
|
3850
|
-
backup(dst, t.localRoot);
|
|
3851
|
-
copyExtras(src, dst);
|
|
3852
|
-
done.push(item2);
|
|
3330
|
+
const mainFindings = scan(localPath);
|
|
3331
|
+
if (mainFindings === null) {
|
|
3332
|
+
return refuse(`re-scan of the transcript failed; choose Skip or Drop session.`);
|
|
3853
3333
|
}
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
if (!dryRun) mkdirSync6(repoExtras, { recursive: true });
|
|
3863
|
-
const { unmapped, skipped, done, would } = runExtrasOp(
|
|
3864
|
-
v,
|
|
3865
|
-
dryRun,
|
|
3866
|
-
({ localRoot, logical, dirname: dirname7 }) => ({
|
|
3867
|
-
src: join34(localRoot, dirname7),
|
|
3868
|
-
dst: join34(repoExtras, logical, dirname7)
|
|
3869
|
-
}),
|
|
3870
|
-
(dst) => backupRepoWrite(dst, ts, repo)
|
|
3871
|
-
);
|
|
3872
|
-
return { unmapped, skipped, pushed: done, wouldPush: would };
|
|
3873
|
-
}
|
|
3874
|
-
function remapExtrasPull(ts, opts = {}) {
|
|
3875
|
-
const dryRun = opts.dryRun === true;
|
|
3876
|
-
const v = loadValidatedExtras({
|
|
3877
|
-
requireRepoExtras: true,
|
|
3878
|
-
missingMsg: "no path-map or repo extras dir; skipping extras remap"
|
|
3879
|
-
});
|
|
3880
|
-
if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
|
|
3881
|
-
const repoExtras = join34(repoHome(), "shared", "extras");
|
|
3882
|
-
const { unmapped, skipped, done, would } = runExtrasOp(
|
|
3883
|
-
v,
|
|
3884
|
-
dryRun,
|
|
3885
|
-
({ localRoot, logical, dirname: dirname7 }) => ({
|
|
3886
|
-
src: join34(repoExtras, logical, dirname7),
|
|
3887
|
-
dst: join34(localRoot, dirname7)
|
|
3888
|
-
}),
|
|
3889
|
-
// Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
|
|
3890
|
-
// localRoot so the backup tree mirrors the project layout.
|
|
3891
|
-
(dst, localRoot) => backupExtrasWrite(dst, ts, localRoot)
|
|
3334
|
+
const { total: anyTotal } = applySubtreeRedactions(
|
|
3335
|
+
localPath,
|
|
3336
|
+
mainFindings,
|
|
3337
|
+
subtreeFiles,
|
|
3338
|
+
void 0,
|
|
3339
|
+
ts,
|
|
3340
|
+
scan,
|
|
3341
|
+
false
|
|
3892
3342
|
);
|
|
3893
|
-
|
|
3343
|
+
if (anyTotal === 0) {
|
|
3344
|
+
return refuse(
|
|
3345
|
+
`nothing to redact in the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
3346
|
+
);
|
|
3347
|
+
}
|
|
3348
|
+
mkdirSync6(stagedProjectDir, { recursive: true });
|
|
3349
|
+
cpSync5(localPath, join29(stagedProjectDir, `${sid}.jsonl`), { force: true });
|
|
3350
|
+
if (existsSync24(sessionDir)) {
|
|
3351
|
+
cpSync5(sessionDir, join29(stagedProjectDir, sid), { force: true, recursive: true });
|
|
3352
|
+
}
|
|
3353
|
+
return true;
|
|
3894
3354
|
}
|
|
3895
3355
|
|
|
3896
|
-
// src/
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
const
|
|
3356
|
+
// src/commands.push.recovery.drop.ts
|
|
3357
|
+
init_config();
|
|
3358
|
+
import { rmSync as rmSync8 } from "node:fs";
|
|
3359
|
+
import { join as join30 } from "node:path";
|
|
3360
|
+
function dropSessionFromStaged(sid, map) {
|
|
3361
|
+
const logicals = Object.keys(map.projects);
|
|
3362
|
+
if (logicals.length === 0) return false;
|
|
3902
3363
|
const repo = repoHome();
|
|
3903
|
-
for (const
|
|
3904
|
-
const
|
|
3905
|
-
const
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
if (diff.length === 0) continue;
|
|
3909
|
-
const projectBackupRoot = join35(backupRoot, encodePath(localRoot));
|
|
3910
|
-
warn(
|
|
3911
|
-
`local ${dirname7} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
|
|
3912
|
-
);
|
|
3913
|
-
for (const f of diff) warn(` ${f}`);
|
|
3364
|
+
for (const logical of logicals) {
|
|
3365
|
+
const jsonl = join30(repo, "shared", "projects", logical, `${sid}.jsonl`);
|
|
3366
|
+
const dir = join30(repo, "shared", "projects", logical, sid);
|
|
3367
|
+
rmSync8(jsonl, { force: true });
|
|
3368
|
+
rmSync8(dir, { recursive: true, force: true });
|
|
3914
3369
|
}
|
|
3370
|
+
return true;
|
|
3915
3371
|
}
|
|
3916
3372
|
|
|
3917
|
-
// src/
|
|
3918
|
-
|
|
3373
|
+
// src/commands.push.recovery.actions.ts
|
|
3374
|
+
init_push_gitleaks_scan();
|
|
3919
3375
|
init_utils();
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
import { existsSync as existsSync31, lstatSync as lstatSync8, rmSync as rmSync9 } from "node:fs";
|
|
3923
|
-
import { join as join36 } from "node:path";
|
|
3924
|
-
function emitAutoMove(onPreview, linkPath, ts, name) {
|
|
3925
|
-
if (onPreview) {
|
|
3926
|
-
onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
|
|
3927
|
-
} else {
|
|
3928
|
-
log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
|
|
3929
|
-
}
|
|
3376
|
+
function applyAllow(f, repo) {
|
|
3377
|
+
appendGitleaksIgnore(f.Fingerprint, repo);
|
|
3930
3378
|
}
|
|
3931
|
-
function
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
} else {
|
|
3935
|
-
log(`would create symlink: ${from} -> ${to}`);
|
|
3379
|
+
function allowAllFindings(findings, repo) {
|
|
3380
|
+
for (const f of findings) {
|
|
3381
|
+
appendGitleaksIgnore(f.Fingerprint, repo);
|
|
3936
3382
|
}
|
|
3937
3383
|
}
|
|
3938
|
-
function
|
|
3939
|
-
|
|
3940
|
-
const
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
const linkPath = join36(claude, name);
|
|
3945
|
-
const target = join36(repo, "shared", name);
|
|
3946
|
-
if (!existsSync31(linkPath)) continue;
|
|
3947
|
-
if (lstatSync8(linkPath).isSymbolicLink()) continue;
|
|
3948
|
-
if (!existsSync31(target)) continue;
|
|
3949
|
-
if (dryRun) {
|
|
3950
|
-
emitAutoMove(opts.onPreview, linkPath, ts, name);
|
|
3951
|
-
continue;
|
|
3952
|
-
}
|
|
3953
|
-
backupBeforeWrite(linkPath, ts);
|
|
3954
|
-
rmSync9(linkPath, { recursive: true, force: true });
|
|
3955
|
-
}
|
|
3956
|
-
for (const name of linkNames) {
|
|
3957
|
-
const target = join36(repo, "shared", name);
|
|
3958
|
-
if (!existsSync31(target)) continue;
|
|
3959
|
-
if (dryRun) {
|
|
3960
|
-
emitCreate(opts.onPreview, join36(claude, name), target);
|
|
3961
|
-
continue;
|
|
3384
|
+
function allowFindingsByRule(findings, ruleId, repo) {
|
|
3385
|
+
let count = 0;
|
|
3386
|
+
for (const f of findings) {
|
|
3387
|
+
if (f.RuleID === ruleId) {
|
|
3388
|
+
appendGitleaksIgnore(f.Fingerprint, repo);
|
|
3389
|
+
count++;
|
|
3962
3390
|
}
|
|
3963
|
-
ensureSymlink(join36(claude, name), target);
|
|
3964
3391
|
}
|
|
3392
|
+
return count;
|
|
3965
3393
|
}
|
|
3966
|
-
function
|
|
3967
|
-
|
|
3968
|
-
const repo = repoHome();
|
|
3969
|
-
const claude = claudeHome();
|
|
3970
|
-
const basePath = join36(repo, "shared", "settings.base.json");
|
|
3971
|
-
const hostPath = join36(repo, "hosts", `${HOST}.json`);
|
|
3972
|
-
if (!existsSync31(basePath)) {
|
|
3973
|
-
die("repo not initialized; run 'nomad init' to scaffold");
|
|
3974
|
-
}
|
|
3975
|
-
const base = readJson(basePath);
|
|
3976
|
-
const hasOverrides = existsSync31(hostPath);
|
|
3977
|
-
const overrides = hasOverrides ? readJson(hostPath) : {};
|
|
3978
|
-
const merged = deepMerge(base, overrides);
|
|
3979
|
-
const settingsPath = join36(claude, "settings.json");
|
|
3980
|
-
if (!hasOverrides && existsSync31(settingsPath)) {
|
|
3394
|
+
function makeDefaultReadLine(repo) {
|
|
3395
|
+
return (file, line) => {
|
|
3981
3396
|
try {
|
|
3982
|
-
const
|
|
3983
|
-
const
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
warn(
|
|
3987
|
-
`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.`
|
|
3988
|
-
);
|
|
3397
|
+
const repoRoot = resolve3(repo);
|
|
3398
|
+
const target = resolve3(repoRoot, file);
|
|
3399
|
+
if (isAbsolute(file) || target !== repoRoot && !target.startsWith(repoRoot + sep4)) {
|
|
3400
|
+
return null;
|
|
3989
3401
|
}
|
|
3402
|
+
const content = readFileSync12(target, "utf8");
|
|
3403
|
+
const lines = content.split(/\r?\n/);
|
|
3404
|
+
const idx = line - 1;
|
|
3405
|
+
if (idx < 0 || idx >= lines.length) return null;
|
|
3406
|
+
return lines[idx] ?? null;
|
|
3990
3407
|
} catch {
|
|
3991
|
-
|
|
3408
|
+
return null;
|
|
3992
3409
|
}
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
async function collectActions(findings, prompt, readLine) {
|
|
3413
|
+
const reader = readLine ?? makeDefaultReadLine(repoHome());
|
|
3414
|
+
const actions = /* @__PURE__ */ new Map();
|
|
3415
|
+
for (const f of findings) {
|
|
3416
|
+
const sid = sessionIdFromFinding(f);
|
|
3417
|
+
const ctx = buildFindingContext(f, reader);
|
|
3418
|
+
const header = `
|
|
3419
|
+
Finding: ${f.RuleID} in ${f.File} line ${f.StartLine}` + (sid === null ? "" : ` (session: ${sid})`) + (ctx === null ? "" : `
|
|
3420
|
+
context: ${ctx}`) + "\n [R]edact [A]llow [D]rop session [S]kip (default)\n";
|
|
3421
|
+
actions.set(findingKey(f), parseAction(await prompt(header + "> ")));
|
|
3993
3422
|
}
|
|
3994
|
-
|
|
3995
|
-
if (dryRun) {
|
|
3996
|
-
log(`would write settings.json (base + ${overrideLabel})`);
|
|
3997
|
-
return { label: overrideLabel };
|
|
3998
|
-
}
|
|
3999
|
-
backupBeforeWrite(settingsPath, ts);
|
|
4000
|
-
writeJsonAtomic(settingsPath, merged);
|
|
4001
|
-
return { label: overrideLabel };
|
|
3423
|
+
return actions;
|
|
4002
3424
|
}
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
diff(oldStr, newStr, options = {}) {
|
|
4012
|
-
let callback;
|
|
4013
|
-
if (typeof options === "function") {
|
|
4014
|
-
callback = options;
|
|
4015
|
-
options = {};
|
|
4016
|
-
} else if ("callback" in options) {
|
|
4017
|
-
callback = options.callback;
|
|
4018
|
-
}
|
|
4019
|
-
const oldString = this.castInput(oldStr, options);
|
|
4020
|
-
const newString = this.castInput(newStr, options);
|
|
4021
|
-
const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
|
|
4022
|
-
const newTokens = this.removeEmpty(this.tokenize(newString, options));
|
|
4023
|
-
return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
|
|
3425
|
+
function dispatchOne(f, ctx) {
|
|
3426
|
+
const action = ctx.actions.get(findingKey(f)) ?? "skip";
|
|
3427
|
+
if (action === "skip") return;
|
|
3428
|
+
const sid = sessionIdFromFinding(f);
|
|
3429
|
+
if (sid !== null && ctx.droppedSids.has(sid)) return;
|
|
3430
|
+
if (action === "allow") {
|
|
3431
|
+
applyAllow(f, ctx.repo);
|
|
3432
|
+
return;
|
|
4024
3433
|
}
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
}, 0);
|
|
4033
|
-
return void 0;
|
|
4034
|
-
} else {
|
|
4035
|
-
return value;
|
|
4036
|
-
}
|
|
4037
|
-
};
|
|
4038
|
-
const newLen = newTokens.length, oldLen = oldTokens.length;
|
|
4039
|
-
let editLength = 1;
|
|
4040
|
-
let maxEditLength = newLen + oldLen;
|
|
4041
|
-
if (options.maxEditLength != null) {
|
|
4042
|
-
maxEditLength = Math.min(maxEditLength, options.maxEditLength);
|
|
4043
|
-
}
|
|
4044
|
-
const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
|
|
4045
|
-
const abortAfterTimestamp = Date.now() + maxExecutionTime;
|
|
4046
|
-
const bestPath = [{ oldPos: -1, lastComponent: void 0 }];
|
|
4047
|
-
let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
|
|
4048
|
-
if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
4049
|
-
return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
|
|
4050
|
-
}
|
|
4051
|
-
let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
|
|
4052
|
-
const execEditLength = () => {
|
|
4053
|
-
for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
|
|
4054
|
-
let basePath;
|
|
4055
|
-
const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
|
|
4056
|
-
if (removePath) {
|
|
4057
|
-
bestPath[diagonalPath - 1] = void 0;
|
|
4058
|
-
}
|
|
4059
|
-
let canAdd = false;
|
|
4060
|
-
if (addPath) {
|
|
4061
|
-
const addPathNewPos = addPath.oldPos - diagonalPath;
|
|
4062
|
-
canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
|
|
4063
|
-
}
|
|
4064
|
-
const canRemove = removePath && removePath.oldPos + 1 < oldLen;
|
|
4065
|
-
if (!canAdd && !canRemove) {
|
|
4066
|
-
bestPath[diagonalPath] = void 0;
|
|
4067
|
-
continue;
|
|
4068
|
-
}
|
|
4069
|
-
if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
|
|
4070
|
-
basePath = this.addToPath(addPath, true, false, 0, options);
|
|
4071
|
-
} else {
|
|
4072
|
-
basePath = this.addToPath(removePath, false, true, 1, options);
|
|
4073
|
-
}
|
|
4074
|
-
newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
|
|
4075
|
-
if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
4076
|
-
return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
|
|
4077
|
-
} else {
|
|
4078
|
-
bestPath[diagonalPath] = basePath;
|
|
4079
|
-
if (basePath.oldPos + 1 >= oldLen) {
|
|
4080
|
-
maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
|
|
4081
|
-
}
|
|
4082
|
-
if (newPos + 1 >= newLen) {
|
|
4083
|
-
minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
|
|
4084
|
-
}
|
|
4085
|
-
}
|
|
4086
|
-
}
|
|
4087
|
-
editLength++;
|
|
4088
|
-
};
|
|
4089
|
-
if (callback) {
|
|
4090
|
-
(function exec() {
|
|
4091
|
-
setTimeout(function() {
|
|
4092
|
-
if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
|
|
4093
|
-
return callback(void 0);
|
|
4094
|
-
}
|
|
4095
|
-
if (!execEditLength()) {
|
|
4096
|
-
exec();
|
|
4097
|
-
}
|
|
4098
|
-
}, 0);
|
|
4099
|
-
})();
|
|
4100
|
-
} else {
|
|
4101
|
-
while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
|
|
4102
|
-
const ret = execEditLength();
|
|
4103
|
-
if (ret) {
|
|
4104
|
-
return ret;
|
|
4105
|
-
}
|
|
4106
|
-
}
|
|
3434
|
+
if (sid === null) return;
|
|
3435
|
+
if (action === "drop") {
|
|
3436
|
+
ctx.droppedSids.add(sid);
|
|
3437
|
+
if (ctx.drop(sid, ctx.map)) {
|
|
3438
|
+
log(
|
|
3439
|
+
`dropped session ${sid} from this push (local transcript kept; the secret remains in your local copy)`
|
|
3440
|
+
);
|
|
4107
3441
|
}
|
|
3442
|
+
return;
|
|
4108
3443
|
}
|
|
4109
|
-
|
|
4110
|
-
|
|
4111
|
-
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
4112
|
-
return {
|
|
4113
|
-
oldPos: path.oldPos + oldPosInc,
|
|
4114
|
-
lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
|
|
4115
|
-
};
|
|
4116
|
-
} else {
|
|
4117
|
-
return {
|
|
4118
|
-
oldPos: path.oldPos + oldPosInc,
|
|
4119
|
-
lastComponent: { count: 1, added, removed, previousComponent: last }
|
|
4120
|
-
};
|
|
4121
|
-
}
|
|
3444
|
+
if (action === "redact" && !ctx.redactedSids.has(sid)) {
|
|
3445
|
+
if (applyRedact(f, ctx.ts, ctx.map, ctx.nowMs, ctx.scan)) ctx.redactedSids.add(sid);
|
|
4122
3446
|
}
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
3447
|
+
}
|
|
3448
|
+
function dispatchActions(findings, actions, opts) {
|
|
3449
|
+
const { ts, map, nowMs, repo, scan = scanFile, drop = dropSessionFromStaged } = opts;
|
|
3450
|
+
const ctx = {
|
|
3451
|
+
actions,
|
|
3452
|
+
ts,
|
|
3453
|
+
map,
|
|
3454
|
+
nowMs,
|
|
3455
|
+
repo,
|
|
3456
|
+
scan,
|
|
3457
|
+
drop,
|
|
3458
|
+
redactedSids: /* @__PURE__ */ new Set(),
|
|
3459
|
+
droppedSids: /* @__PURE__ */ new Set()
|
|
3460
|
+
};
|
|
3461
|
+
for (const f of findings) {
|
|
3462
|
+
dispatchOne(f, ctx);
|
|
4139
3463
|
}
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
3464
|
+
}
|
|
3465
|
+
function redactAllFindings(findings, ts, map, nowMs, scan = scanFile) {
|
|
3466
|
+
const redactedSids = /* @__PURE__ */ new Set();
|
|
3467
|
+
for (const f of findings) {
|
|
3468
|
+
const sid = sessionIdFromFinding(f);
|
|
3469
|
+
if (sid === null || redactedSids.has(sid)) continue;
|
|
3470
|
+
if (applyRedact(f, ts, map, nowMs, scan)) redactedSids.add(sid);
|
|
4146
3471
|
}
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
3472
|
+
}
|
|
3473
|
+
|
|
3474
|
+
// src/commands.push.recovery.ts
|
|
3475
|
+
init_push_gitleaks_scan();
|
|
3476
|
+
init_push_gitleaks();
|
|
3477
|
+
init_utils();
|
|
3478
|
+
function isTTY(stdin = process.stdin, stdout = process.stdout) {
|
|
3479
|
+
return stdin.isTTY === true && stdout.isTTY === true;
|
|
3480
|
+
}
|
|
3481
|
+
function hasUnresolved(actions) {
|
|
3482
|
+
for (const action of actions.values()) {
|
|
3483
|
+
if (action === "skip") return true;
|
|
4155
3484
|
}
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
3485
|
+
return false;
|
|
3486
|
+
}
|
|
3487
|
+
function printRecoveryLegend(print = console.log) {
|
|
3488
|
+
print("");
|
|
3489
|
+
print("Recovery actions:");
|
|
3490
|
+
print(" Redact - scrub the secret from the local transcript, push the cleaned copy");
|
|
3491
|
+
print(" Allow - mark as false positive (adds a .gitleaksignore fingerprint), push as-is");
|
|
3492
|
+
print(" Drop session - exclude this session from this push (local transcript kept, running");
|
|
3493
|
+
print(" session is not stopped)");
|
|
3494
|
+
print(" Skip - leave unresolved (the push aborts)");
|
|
3495
|
+
print("");
|
|
3496
|
+
}
|
|
3497
|
+
function applyThenRescan(scanVerdict, repoHome2) {
|
|
3498
|
+
gitOrFatal(["add", "-A"], "git add", repoHome2);
|
|
3499
|
+
const next = scanVerdict(repoHome2);
|
|
3500
|
+
if (next.leak) {
|
|
3501
|
+
const { bySession, other } = partitionFindings(next.findings);
|
|
3502
|
+
throw new NomadFatal(buildSessionAwareFatal(bySession, other));
|
|
4159
3503
|
}
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
|
|
3504
|
+
return next;
|
|
3505
|
+
}
|
|
3506
|
+
function allowThenRescan(append, scanVerdict, repoHome2) {
|
|
3507
|
+
const ignPath = join31(repoHome2, ".gitleaksignore");
|
|
3508
|
+
let before;
|
|
3509
|
+
try {
|
|
3510
|
+
before = readFileSync13(ignPath, "utf8");
|
|
3511
|
+
} catch {
|
|
3512
|
+
before = null;
|
|
3513
|
+
}
|
|
3514
|
+
append();
|
|
3515
|
+
try {
|
|
3516
|
+
return applyThenRescan(scanVerdict, repoHome2);
|
|
3517
|
+
} catch (err) {
|
|
3518
|
+
if (before === null) rmSync9(ignPath, { force: true });
|
|
3519
|
+
else writeFileSync5(ignPath, before, "utf8");
|
|
3520
|
+
throw err;
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
function makeRealPrompt() {
|
|
3524
|
+
return async (prompt) => {
|
|
3525
|
+
const rl = createInterface({
|
|
3526
|
+
input: process.stdin,
|
|
3527
|
+
output: process.stdout,
|
|
3528
|
+
terminal: true
|
|
3529
|
+
});
|
|
3530
|
+
try {
|
|
3531
|
+
return await rl.question(prompt);
|
|
3532
|
+
} finally {
|
|
3533
|
+
rl.close();
|
|
3534
|
+
}
|
|
3535
|
+
};
|
|
3536
|
+
}
|
|
3537
|
+
async function resolveLeakFindings(verdict, ts, map, deps = {}) {
|
|
3538
|
+
const {
|
|
3539
|
+
isTTYCheck = isTTY,
|
|
3540
|
+
nowMs = Date.now,
|
|
3541
|
+
redactAll = false,
|
|
3542
|
+
allowAll = false,
|
|
3543
|
+
allowRule,
|
|
3544
|
+
makePrompt: makePromptFn = makeRealPrompt,
|
|
3545
|
+
scan = scanFile,
|
|
3546
|
+
printLegend = printRecoveryLegend
|
|
3547
|
+
} = deps;
|
|
3548
|
+
const scanVerdict = deps.scanVerdict ?? (await Promise.resolve().then(() => (init_push_leak_verdict(), push_leak_verdict_exports))).scanPushVerdict;
|
|
3549
|
+
const repo = repoHome();
|
|
3550
|
+
let current = verdict;
|
|
3551
|
+
if (redactAll) {
|
|
3552
|
+
redactAllFindings(current.findings, ts, map, nowMs, scan);
|
|
3553
|
+
return applyThenRescan(scanVerdict, repo);
|
|
3554
|
+
}
|
|
3555
|
+
if (allowAll) {
|
|
3556
|
+
return allowThenRescan(() => allowAllFindings(current.findings, repo), scanVerdict, repo);
|
|
3557
|
+
}
|
|
3558
|
+
if (allowRule !== void 0) {
|
|
3559
|
+
return allowThenRescan(
|
|
3560
|
+
() => {
|
|
3561
|
+
const matched = allowFindingsByRule(current.findings, allowRule, repo);
|
|
3562
|
+
if (matched === 0) log(`no findings matched rule ${allowRule}; re-scanning`);
|
|
3563
|
+
},
|
|
3564
|
+
scanVerdict,
|
|
3565
|
+
repo
|
|
3566
|
+
);
|
|
3567
|
+
}
|
|
3568
|
+
if (!isTTYCheck()) {
|
|
3569
|
+
throw new NomadFatal(current.recovery ?? "gitleaks detected secrets");
|
|
3570
|
+
}
|
|
3571
|
+
const prompt = makePromptFn();
|
|
3572
|
+
printLegend();
|
|
3573
|
+
while (current.leak && current.findings.length > 0) {
|
|
3574
|
+
const actions = await collectActions(current.findings, prompt);
|
|
3575
|
+
if (hasUnresolved(actions)) {
|
|
3576
|
+
const unresolved = current.findings.filter((f) => actions.get(findingKey(f)) === "skip");
|
|
3577
|
+
const { bySession, other } = partitionFindings(unresolved);
|
|
3578
|
+
throw new NomadFatal(buildSessionAwareFatal(bySession, other));
|
|
3579
|
+
}
|
|
3580
|
+
dispatchActions(current.findings, actions, { ts, map, nowMs, repo, scan });
|
|
3581
|
+
gitOrFatal(["add", "-A"], "git add", repo);
|
|
3582
|
+
current = scanVerdict(repo);
|
|
3583
|
+
}
|
|
3584
|
+
return current;
|
|
3585
|
+
}
|
|
3586
|
+
|
|
3587
|
+
// src/spinner.ts
|
|
3588
|
+
function formatElapsed(ms) {
|
|
3589
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
3590
|
+
}
|
|
3591
|
+
function writePlainStart(out, label) {
|
|
3592
|
+
out.write(`${label}...
|
|
3593
|
+
`);
|
|
3594
|
+
}
|
|
3595
|
+
function writePlainDone(out, label, ms) {
|
|
3596
|
+
out.write(`${label} done (${formatElapsed(ms)})
|
|
3597
|
+
`);
|
|
3598
|
+
}
|
|
3599
|
+
function writeAnimatedDone(out, label, ms, useTTY) {
|
|
3600
|
+
out.write("\r\x1B[K");
|
|
3601
|
+
const glyph = useTTY ? green(okGlyph) : okGlyph;
|
|
3602
|
+
out.write(`${glyph} ${label} (${formatElapsed(ms)})
|
|
3603
|
+
`);
|
|
3604
|
+
}
|
|
3605
|
+
function resolveWorkerPath(deps = {}) {
|
|
3606
|
+
const check = deps.existsSyncFn ?? existsSync25;
|
|
3607
|
+
const base = deps.baseUrl ?? import.meta.url;
|
|
3608
|
+
const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
|
|
3609
|
+
if (check(mjs)) return mjs;
|
|
3610
|
+
return fileURLToPath4(new URL("./spinner.worker.ts", base));
|
|
3611
|
+
}
|
|
3612
|
+
function makeRealWorker() {
|
|
3613
|
+
return new Worker(resolveWorkerPath());
|
|
3614
|
+
}
|
|
3615
|
+
function startSpinner(label, deps = {}) {
|
|
3616
|
+
const ttyCheck = deps.isTTYCheck ?? (() => isTTY());
|
|
3617
|
+
const env = deps.env ?? process.env;
|
|
3618
|
+
const out = deps.out ?? process.stderr;
|
|
3619
|
+
const now = deps.now ?? Date.now;
|
|
3620
|
+
const startMs = now();
|
|
3621
|
+
const animate = ttyCheck() && !env.CI;
|
|
3622
|
+
let worker = null;
|
|
3623
|
+
let degraded = false;
|
|
3624
|
+
let finalized = false;
|
|
3625
|
+
if (animate) {
|
|
3626
|
+
const factory = deps.makeWorker ?? makeRealWorker;
|
|
3627
|
+
try {
|
|
3628
|
+
worker = factory();
|
|
3629
|
+
worker.unref?.();
|
|
3630
|
+
worker.postMessage({ type: "start", label });
|
|
3631
|
+
} catch {
|
|
3632
|
+
degraded = true;
|
|
3633
|
+
worker = null;
|
|
3634
|
+
writePlainStart(out, label);
|
|
3635
|
+
}
|
|
3636
|
+
} else {
|
|
3637
|
+
writePlainStart(out, label);
|
|
3638
|
+
}
|
|
3639
|
+
function finalize(success, doneLabel) {
|
|
3640
|
+
if (finalized) return;
|
|
3641
|
+
finalized = true;
|
|
3642
|
+
const dl = doneLabel ?? label;
|
|
3643
|
+
const elapsed = now() - startMs;
|
|
3644
|
+
if (animate && !degraded && worker !== null) {
|
|
3645
|
+
worker.postMessage({ type: "pause" });
|
|
3646
|
+
worker.terminate();
|
|
3647
|
+
worker = null;
|
|
3648
|
+
if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
|
|
3649
|
+
else out.write("\r\x1B[K");
|
|
3650
|
+
} else if (success) {
|
|
3651
|
+
writePlainDone(out, dl, elapsed);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
return {
|
|
3655
|
+
succeed: (doneLabel) => finalize(true, doneLabel),
|
|
3656
|
+
stop: () => finalize(false)
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
function withSpinner(label, fn, deps) {
|
|
3660
|
+
const sp = startSpinner(label, deps);
|
|
3661
|
+
try {
|
|
3662
|
+
const result = fn();
|
|
3663
|
+
sp.succeed();
|
|
3664
|
+
return result;
|
|
3665
|
+
} finally {
|
|
3666
|
+
sp.stop();
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
// src/commands.doctor.gitleaks-version.ts
|
|
3671
|
+
init_color();
|
|
3672
|
+
import { execFileSync as execFileSync7 } from "node:child_process";
|
|
3673
|
+
import { existsSync as existsSync26 } from "node:fs";
|
|
3674
|
+
import { join as join32 } from "node:path";
|
|
3675
|
+
init_config();
|
|
3676
|
+
var SEMVER_MAJOR_MINOR = /^(\d+)\.(\d+)\.\d+$/;
|
|
3677
|
+
var GITLEAKS_TIMEOUT_MS = 5e3;
|
|
3678
|
+
function majorMinorOf(value) {
|
|
3679
|
+
const m = SEMVER_MAJOR_MINOR.exec(value);
|
|
3680
|
+
return m === null ? null : [m[1], m[2]];
|
|
3681
|
+
}
|
|
3682
|
+
function readGitleaksVersion(run, tomlExists) {
|
|
3683
|
+
const tomlPath = join32(repoHome(), ".gitleaks.toml");
|
|
3684
|
+
const args = ["version"];
|
|
3685
|
+
if (tomlExists(tomlPath)) args.push("--config", tomlPath);
|
|
3686
|
+
try {
|
|
3687
|
+
return run("gitleaks", args, {
|
|
3688
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3689
|
+
timeout: GITLEAKS_TIMEOUT_MS
|
|
3690
|
+
}).toString().trim();
|
|
3691
|
+
} catch {
|
|
3692
|
+
return null;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
function reportGitleaksVersionCheck(section2, run = execFileSync7, tomlExists = existsSync26) {
|
|
3696
|
+
const raw = readGitleaksVersion(run, tomlExists);
|
|
3697
|
+
if (raw === null) return;
|
|
3698
|
+
const local = majorMinorOf(raw);
|
|
3699
|
+
if (local === null) return;
|
|
3700
|
+
const pin = majorMinorOf(GITLEAKS_PINNED_VERSION);
|
|
3701
|
+
if (pin === null) return;
|
|
3702
|
+
const sameMajorMinor = local[0] === pin[0] && local[1] === pin[1];
|
|
3703
|
+
if (sameMajorMinor) {
|
|
3704
|
+
addItem(section2, `${green(okGlyph)} gitleaks: ${raw} (matches pinned ${pin[0]}.${pin[1]})`);
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3707
|
+
addItem(
|
|
3708
|
+
section2,
|
|
3709
|
+
`${yellow(warnGlyph)} gitleaks: ${raw} -> ${GITLEAKS_PINNED_VERSION} (CI pins this; local drift may change scan results)`
|
|
3710
|
+
);
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
// src/commands.doctor.checks.deps.ts
|
|
3714
|
+
init_color();
|
|
3715
|
+
import { execFileSync as execFileSync8 } from "node:child_process";
|
|
3716
|
+
var VERSION_TOKEN = /(\d{1,9}\.\d{1,9}\.\d{1,9})/;
|
|
3717
|
+
var PROBE_TIMEOUT_MS = 3e3;
|
|
3718
|
+
var FETCHER_BASE = "HTTP fetcher";
|
|
3719
|
+
function parseFirstVersion(line) {
|
|
3720
|
+
const m = VERSION_TOKEN.exec(line);
|
|
3721
|
+
return m ? m[1] : null;
|
|
3722
|
+
}
|
|
3723
|
+
function probeOptionalDep(bin, run) {
|
|
3724
|
+
try {
|
|
3725
|
+
const firstLine = run(bin, ["--version"], {
|
|
3726
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
3727
|
+
timeout: PROBE_TIMEOUT_MS
|
|
3728
|
+
}).toString().split("\n")[0].trim();
|
|
3729
|
+
const version = parseFirstVersion(firstLine);
|
|
3730
|
+
return { status: "present", version };
|
|
3731
|
+
} catch (err) {
|
|
3732
|
+
if (err.code === "ENOENT") {
|
|
3733
|
+
return { status: "not-installed" };
|
|
3734
|
+
}
|
|
3735
|
+
return { status: "present", version: null };
|
|
3736
|
+
}
|
|
3737
|
+
}
|
|
3738
|
+
function reportFetcherRow(section2, run) {
|
|
3739
|
+
const curl = probeOptionalDep("curl", run);
|
|
3740
|
+
const wget = probeOptionalDep("wget", run);
|
|
3741
|
+
if (curl.status === "present") {
|
|
3742
|
+
addItem(section2, `${green(okGlyph)} ${FETCHER_BASE}: curl ${curl.version ?? "(present)"}`);
|
|
3743
|
+
} else if (wget.status === "present") {
|
|
3744
|
+
addItem(section2, `${green(okGlyph)} ${FETCHER_BASE}: wget ${wget.version ?? "(present)"}`);
|
|
3745
|
+
} else {
|
|
3746
|
+
addItem(
|
|
3747
|
+
section2,
|
|
3748
|
+
`${yellow(warnGlyph)} ${FETCHER_BASE} (curl or wget): not installed (optional; needed for release-version staleness check + nomad doctor --check-schema)`
|
|
3749
|
+
);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
function reportOptionalDeps(section2, run = execFileSync8) {
|
|
3753
|
+
const gh = probeOptionalDep("gh", run);
|
|
3754
|
+
if (gh.status === "present") {
|
|
3755
|
+
addItem(section2, `${green(okGlyph)} gh: ${gh.version ?? "present"}`);
|
|
3756
|
+
} else {
|
|
3757
|
+
addItem(
|
|
3758
|
+
section2,
|
|
3759
|
+
`${yellow(warnGlyph)} gh: not installed (optional; needed for nomad init Actions auto-disable + the Actions-drift check)`
|
|
3760
|
+
);
|
|
3761
|
+
}
|
|
3762
|
+
reportFetcherRow(section2, run);
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// src/commands.doctor.actions-drift.ts
|
|
3766
|
+
init_color();
|
|
3767
|
+
import { execFileSync as execFileSync10 } from "node:child_process";
|
|
3768
|
+
init_config();
|
|
3769
|
+
|
|
3770
|
+
// src/gh-actions.ts
|
|
3771
|
+
import { execFileSync as execFileSync9 } from "node:child_process";
|
|
3772
|
+
var GH_TIMEOUT_MS = 5e3;
|
|
3773
|
+
function parseGitHubRemote(remoteUrl) {
|
|
3774
|
+
const normalized = remoteUrl.trim().replace(/\/$/, "");
|
|
3775
|
+
const m = /github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/.exec(normalized);
|
|
3776
|
+
if (m === null) return null;
|
|
3777
|
+
return { owner: m[1], repo: m[2] };
|
|
3778
|
+
}
|
|
3779
|
+
function ghAuthStatus(run = execFileSync9) {
|
|
3780
|
+
try {
|
|
3781
|
+
run("gh", ["auth", "status"], {
|
|
3782
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
3783
|
+
timeout: GH_TIMEOUT_MS
|
|
3784
|
+
});
|
|
3785
|
+
return null;
|
|
3786
|
+
} catch (err) {
|
|
3787
|
+
const e = err;
|
|
3788
|
+
if (e.code === "ENOENT") return "gh-not-installed";
|
|
3789
|
+
if (typeof e.status === "number") return "gh-not-authed";
|
|
3790
|
+
return "gh-probe-error";
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
function isRepoPrivate(ref, run = execFileSync9) {
|
|
3794
|
+
const out = run("gh", ["repo", "view", `${ref.owner}/${ref.repo}`, "--json", "isPrivate"], {
|
|
3795
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
3796
|
+
timeout: GH_TIMEOUT_MS
|
|
3797
|
+
}).toString();
|
|
3798
|
+
const parsed = JSON.parse(out);
|
|
3799
|
+
return parsed.isPrivate === true;
|
|
3800
|
+
}
|
|
3801
|
+
function isActionsEnabled(ref, run = execFileSync9) {
|
|
3802
|
+
const out = run(
|
|
3803
|
+
"gh",
|
|
3804
|
+
["api", `repos/${ref.owner}/${ref.repo}/actions/permissions`, "--jq", ".enabled"],
|
|
3805
|
+
{ stdio: ["ignore", "pipe", "ignore"], timeout: GH_TIMEOUT_MS }
|
|
3806
|
+
).toString().trim();
|
|
3807
|
+
return out === "true";
|
|
3808
|
+
}
|
|
3809
|
+
function disableActions(ref, run = execFileSync9) {
|
|
3810
|
+
run(
|
|
3811
|
+
"gh",
|
|
3812
|
+
[
|
|
3813
|
+
"api",
|
|
3814
|
+
"-X",
|
|
3815
|
+
"PUT",
|
|
3816
|
+
`repos/${ref.owner}/${ref.repo}/actions/permissions`,
|
|
3817
|
+
"-F",
|
|
3818
|
+
"enabled=false"
|
|
3819
|
+
],
|
|
3820
|
+
{ stdio: ["ignore", "ignore", "pipe"], timeout: GH_TIMEOUT_MS }
|
|
3821
|
+
);
|
|
3822
|
+
}
|
|
3823
|
+
function readOriginRemote(cwd, run = execFileSync9) {
|
|
3824
|
+
return run("git", ["remote", "get-url", "origin"], {
|
|
3825
|
+
cwd,
|
|
3826
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3827
|
+
}).toString().trim();
|
|
3828
|
+
}
|
|
3829
|
+
|
|
3830
|
+
// src/commands.doctor.actions-drift.ts
|
|
3831
|
+
function reportActionsDrift(section2, run = execFileSync10) {
|
|
3832
|
+
let remote;
|
|
3833
|
+
try {
|
|
3834
|
+
remote = readOriginRemote(repoHome(), run);
|
|
3835
|
+
} catch {
|
|
3836
|
+
return;
|
|
3837
|
+
}
|
|
3838
|
+
const ref = parseGitHubRemote(remote);
|
|
3839
|
+
if (ref === null) return;
|
|
3840
|
+
const auth = ghAuthStatus(run);
|
|
3841
|
+
if (auth === "gh-not-installed" || auth === "gh-not-authed") return;
|
|
3842
|
+
let isPrivate;
|
|
3843
|
+
try {
|
|
3844
|
+
isPrivate = isRepoPrivate(ref, run);
|
|
3845
|
+
} catch {
|
|
3846
|
+
return;
|
|
3847
|
+
}
|
|
3848
|
+
if (!isPrivate) return;
|
|
3849
|
+
let enabled2;
|
|
3850
|
+
try {
|
|
3851
|
+
enabled2 = isActionsEnabled(ref, run);
|
|
3852
|
+
} catch {
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3855
|
+
if (!enabled2) return;
|
|
3856
|
+
addItem(
|
|
3857
|
+
section2,
|
|
3858
|
+
`${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')`
|
|
3859
|
+
);
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
// src/commands.doctor.verdict.ts
|
|
3863
|
+
init_color();
|
|
3864
|
+
function isFailLine(item2) {
|
|
3865
|
+
return item2.includes(failGlyph);
|
|
3866
|
+
}
|
|
3867
|
+
function isWarnLine(item2) {
|
|
3868
|
+
return !isFailLine(item2) && item2.includes(warnGlyph);
|
|
3869
|
+
}
|
|
3870
|
+
function buildVerdictSection(sections) {
|
|
3871
|
+
const summary = section("Summary");
|
|
3872
|
+
const lines = sections.flatMap((s) => s.items).map((item2) => item2.replace(/^\t/, ""));
|
|
3873
|
+
const failures = lines.filter(isFailLine);
|
|
3874
|
+
const warnings = lines.filter(isWarnLine);
|
|
3875
|
+
for (const line of [...failures, ...warnings]) addItem(summary, line);
|
|
3876
|
+
if (failures.length > 0) {
|
|
3877
|
+
addItem(
|
|
3878
|
+
summary,
|
|
3879
|
+
`${red(failGlyph)} ${failures.length} failure(s), ${warnings.length} warning(s)`
|
|
3880
|
+
);
|
|
3881
|
+
} else if (warnings.length > 0) {
|
|
3882
|
+
addItem(summary, `${yellow(warnGlyph)} ${warnings.length} warning(s)`);
|
|
3883
|
+
} else {
|
|
3884
|
+
addItem(summary, `${green(okGlyph)} healthy`);
|
|
4163
3885
|
}
|
|
4164
|
-
|
|
4165
|
-
|
|
3886
|
+
return summary;
|
|
3887
|
+
}
|
|
3888
|
+
|
|
3889
|
+
// src/commands.doctor.ts
|
|
3890
|
+
function gatherDoctorSections(opts) {
|
|
3891
|
+
const host = section("Environment");
|
|
3892
|
+
reportHostAndPaths(host);
|
|
3893
|
+
reportRepoState(host);
|
|
3894
|
+
const links = section("Shared links");
|
|
3895
|
+
const mapPath = join33(repoHome(), "path-map.json");
|
|
3896
|
+
const rawMap = existsSync27(mapPath) ? readJsonSafe(mapPath, mapPath, links) : null;
|
|
3897
|
+
const map = rawMap ?? { projects: {} };
|
|
3898
|
+
reportSharedLinks(links, map);
|
|
3899
|
+
const hooksScan = section("Hook targets");
|
|
3900
|
+
reportHooksTargetCheck(hooksScan);
|
|
3901
|
+
reportHookScopeCheck(hooksScan);
|
|
3902
|
+
reportPreserveSymlinksCheck(hooksScan);
|
|
3903
|
+
const settings = section("Settings");
|
|
3904
|
+
const base = loadBaseSettings(settings);
|
|
3905
|
+
const parsedSettings = loadAndReportSettings(settings);
|
|
3906
|
+
reportHostOverrides(settings, base, parsedSettings);
|
|
3907
|
+
reportSettingsDriftCheck(settings);
|
|
3908
|
+
const pathMap = section("Path map");
|
|
3909
|
+
reportPathMap(pathMap);
|
|
3910
|
+
const neverSync = section("Never-sync");
|
|
3911
|
+
reportNeverSync(neverSync);
|
|
3912
|
+
const repository = section("Repository");
|
|
3913
|
+
const gitleaksReady = reportGitleaksProbe(repository);
|
|
3914
|
+
reportGitlinks(repository);
|
|
3915
|
+
reportRemote(repository);
|
|
3916
|
+
reportRebaseClean(repository);
|
|
3917
|
+
reportRebaseState(repository);
|
|
3918
|
+
reportActionsDrift(repository);
|
|
3919
|
+
const nomadVersion = section("Nomad Version");
|
|
3920
|
+
reportVersionCheck(nomadVersion);
|
|
3921
|
+
const housekeeping = section("Housekeeping");
|
|
3922
|
+
reportBackupsCheck(housekeeping);
|
|
3923
|
+
const depVersions = section("Dependency Versions");
|
|
3924
|
+
reportNodeEngineCheck(depVersions);
|
|
3925
|
+
reportGitleaksVersionCheck(depVersions);
|
|
3926
|
+
reportOptionalDeps(depVersions);
|
|
3927
|
+
const sharedScan = section("Shared scan");
|
|
3928
|
+
if (opts.checkShared === true) reportCheckShared(sharedScan, gitleaksReady);
|
|
3929
|
+
const schemaScan = section("Schema scan");
|
|
3930
|
+
if (opts.checkSchema === true) reportCheckSchema(schemaScan);
|
|
3931
|
+
const body = [
|
|
3932
|
+
nomadVersion,
|
|
3933
|
+
depVersions,
|
|
3934
|
+
host,
|
|
3935
|
+
links,
|
|
3936
|
+
hooksScan,
|
|
3937
|
+
settings,
|
|
3938
|
+
pathMap,
|
|
3939
|
+
neverSync,
|
|
3940
|
+
repository,
|
|
3941
|
+
housekeeping,
|
|
3942
|
+
sharedScan,
|
|
3943
|
+
schemaScan
|
|
3944
|
+
];
|
|
3945
|
+
return [...body, buildVerdictSection(body)];
|
|
3946
|
+
}
|
|
3947
|
+
function cmdDoctor(opts = {}) {
|
|
3948
|
+
const makeSpinner = opts.startSpinner ?? startSpinner;
|
|
3949
|
+
const sp = makeSpinner("Running checks");
|
|
3950
|
+
let report;
|
|
3951
|
+
try {
|
|
3952
|
+
report = gatherDoctorSections(opts);
|
|
3953
|
+
} finally {
|
|
3954
|
+
sp.stop();
|
|
4166
3955
|
}
|
|
4167
|
-
|
|
4168
|
-
|
|
3956
|
+
renderDoctor(report);
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
// src/commands.drop-session.ts
|
|
3960
|
+
init_config();
|
|
3961
|
+
import { execFileSync as execFileSync12 } from "node:child_process";
|
|
3962
|
+
import { existsSync as existsSync29, readdirSync as readdirSync10, statSync as statSync8 } from "node:fs";
|
|
3963
|
+
import { join as join35, relative as relative4 } from "node:path";
|
|
3964
|
+
|
|
3965
|
+
// src/commands.drop-session.git.ts
|
|
3966
|
+
import { execFileSync as execFileSync11 } from "node:child_process";
|
|
3967
|
+
function expandStagedDir(dirRel, repo) {
|
|
3968
|
+
try {
|
|
3969
|
+
const out = execFileSync11("git", ["ls-files", "-z", "--", dirRel], {
|
|
3970
|
+
cwd: repo,
|
|
3971
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3972
|
+
});
|
|
3973
|
+
return out.toString().split("\0").filter((p) => p !== "");
|
|
3974
|
+
} catch {
|
|
3975
|
+
return [];
|
|
4169
3976
|
}
|
|
4170
|
-
|
|
3977
|
+
}
|
|
3978
|
+
function isTrackedInHead(rel, repo) {
|
|
3979
|
+
try {
|
|
3980
|
+
execFileSync11("git", ["cat-file", "-e", `HEAD:${rel}`], {
|
|
3981
|
+
cwd: repo,
|
|
3982
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3983
|
+
});
|
|
3984
|
+
return true;
|
|
3985
|
+
} catch {
|
|
4171
3986
|
return false;
|
|
4172
3987
|
}
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4200
|
-
|
|
4201
|
-
|
|
4202
|
-
|
|
4203
|
-
|
|
4204
|
-
|
|
4205
|
-
|
|
3988
|
+
}
|
|
3989
|
+
function isInIndex(rel, repo) {
|
|
3990
|
+
try {
|
|
3991
|
+
const out = execFileSync11("git", ["ls-files", "--", rel], {
|
|
3992
|
+
cwd: repo,
|
|
3993
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
3994
|
+
});
|
|
3995
|
+
return out.toString().trim() !== "";
|
|
3996
|
+
} catch {
|
|
3997
|
+
return false;
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
|
|
4001
|
+
// src/commands.drop-session.scrub-hint.ts
|
|
4002
|
+
init_config();
|
|
4003
|
+
init_utils();
|
|
4004
|
+
init_utils_json();
|
|
4005
|
+
import { existsSync as existsSync28 } from "node:fs";
|
|
4006
|
+
import { join as join34 } from "node:path";
|
|
4007
|
+
var SHARED_PROJECT_LOGICAL = /^shared\/projects\/([^/]+)\//;
|
|
4008
|
+
function reportScrubHint(id, matches) {
|
|
4009
|
+
const live = resolveLiveTranscript2(id, matches);
|
|
4010
|
+
const target = live ?? `~/.claude/projects/<encoded>/${id}.jsonl`;
|
|
4011
|
+
log(
|
|
4012
|
+
`note: this only un-stages the session from the next push.
|
|
4013
|
+
The local source still contains the secret, so nomad push re-stages it
|
|
4014
|
+
on the next run and nomad doctor --check-shared keeps reporting it.
|
|
4015
|
+
To fully remediate: rotate the credential, then run:
|
|
4016
|
+
nomad redact ${id}
|
|
4017
|
+
(or scrub ${target} manually)`
|
|
4018
|
+
);
|
|
4019
|
+
}
|
|
4020
|
+
function resolveLiveTranscript2(id, matches) {
|
|
4021
|
+
try {
|
|
4022
|
+
const mapPath = join34(repoHome(), "path-map.json");
|
|
4023
|
+
if (!existsSync28(mapPath)) return null;
|
|
4024
|
+
const projects = readJson(mapPath).projects;
|
|
4025
|
+
const claude = claudeHome();
|
|
4026
|
+
for (const rel of matches) {
|
|
4027
|
+
const logical = SHARED_PROJECT_LOGICAL.exec(rel)?.[1];
|
|
4028
|
+
if (logical === void 0) continue;
|
|
4029
|
+
const abs = projects[logical]?.[HOST];
|
|
4030
|
+
if (abs === void 0) continue;
|
|
4031
|
+
const live = join34(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
4032
|
+
if (existsSync28(live)) return live;
|
|
4206
4033
|
}
|
|
4207
|
-
return
|
|
4034
|
+
return null;
|
|
4035
|
+
} catch {
|
|
4036
|
+
return null;
|
|
4208
4037
|
}
|
|
4209
|
-
}
|
|
4038
|
+
}
|
|
4210
4039
|
|
|
4211
|
-
//
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4040
|
+
// src/commands.drop-session.ts
|
|
4041
|
+
init_utils();
|
|
4042
|
+
function cmdDropSession(id) {
|
|
4043
|
+
if (id.length === 0 || id.length > 128 || !/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
4044
|
+
fail(`invalid session id: ${id}`);
|
|
4045
|
+
process.exit(1);
|
|
4216
4046
|
}
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
}
|
|
4225
|
-
} else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
|
|
4226
|
-
if (left.endsWith("\n")) {
|
|
4227
|
-
left = left.slice(0, -1);
|
|
4228
|
-
}
|
|
4229
|
-
if (right.endsWith("\n")) {
|
|
4230
|
-
right = right.slice(0, -1);
|
|
4231
|
-
}
|
|
4047
|
+
const repo = repoHome();
|
|
4048
|
+
if (!existsSync29(repo)) die(`repo not cloned at ${repo}`);
|
|
4049
|
+
const handle = acquireLock("drop-session");
|
|
4050
|
+
if (handle === null) process.exit(0);
|
|
4051
|
+
try {
|
|
4052
|
+
const repoProjects = join35(repo, "shared", "projects");
|
|
4053
|
+
if (!existsSync29(repoProjects)) {
|
|
4054
|
+
throw new NomadFatal(`no staged session matches ${id}`);
|
|
4232
4055
|
}
|
|
4233
|
-
|
|
4056
|
+
const matches = collectMatches(repoProjects, id, repo);
|
|
4057
|
+
if (matches.length === 0) {
|
|
4058
|
+
throw new NomadFatal(`no staged session matches ${id}`);
|
|
4059
|
+
}
|
|
4060
|
+
for (const rel of matches) unstageOne(rel, repo);
|
|
4061
|
+
reportScrubHint(id, matches);
|
|
4062
|
+
} catch (err) {
|
|
4063
|
+
if (!(err instanceof NomadFatal)) {
|
|
4064
|
+
throw err;
|
|
4065
|
+
}
|
|
4066
|
+
fail(err.message);
|
|
4067
|
+
process.exitCode = 1;
|
|
4068
|
+
} finally {
|
|
4069
|
+
releaseLock(handle);
|
|
4234
4070
|
}
|
|
4235
|
-
};
|
|
4236
|
-
var lineDiff = new LineDiff();
|
|
4237
|
-
function diffLines(oldStr, newStr, options) {
|
|
4238
|
-
return lineDiff.diff(oldStr, newStr, options);
|
|
4239
4071
|
}
|
|
4240
|
-
function
|
|
4241
|
-
|
|
4242
|
-
|
|
4072
|
+
function collectMatches(repoProjects, id, repo) {
|
|
4073
|
+
const matches = [];
|
|
4074
|
+
for (const logical of readdirSync10(repoProjects)) {
|
|
4075
|
+
const candidate = join35(repoProjects, logical, `${id}.jsonl`);
|
|
4076
|
+
if (existsSync29(candidate)) {
|
|
4077
|
+
matches.push(relative4(repo, candidate));
|
|
4078
|
+
}
|
|
4079
|
+
const dir = join35(repoProjects, logical, id);
|
|
4080
|
+
if (existsSync29(dir) && statSync8(dir).isDirectory()) {
|
|
4081
|
+
const dirRel = relative4(repo, dir);
|
|
4082
|
+
const staged = expandStagedDir(dirRel, repo);
|
|
4083
|
+
if (staged.length > 0) matches.push(...staged);
|
|
4084
|
+
else matches.push(dirRel);
|
|
4085
|
+
}
|
|
4243
4086
|
}
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4087
|
+
return matches;
|
|
4088
|
+
}
|
|
4089
|
+
function unstageOne(rel, repo) {
|
|
4090
|
+
if (!isInIndex(rel, repo)) {
|
|
4091
|
+
item(`dropped ${rel} (already absent from index)`);
|
|
4092
|
+
return;
|
|
4247
4093
|
}
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4094
|
+
try {
|
|
4095
|
+
if (isTrackedInHead(rel, repo)) {
|
|
4096
|
+
execFileSync12("git", ["restore", "--staged", "--worktree", "--", rel], {
|
|
4097
|
+
cwd: repo,
|
|
4098
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4099
|
+
});
|
|
4252
4100
|
} else {
|
|
4253
|
-
|
|
4101
|
+
execFileSync12("git", ["rm", "--cached", "-f", "--", rel], {
|
|
4102
|
+
cwd: repo,
|
|
4103
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4104
|
+
});
|
|
4254
4105
|
}
|
|
4106
|
+
} catch (err) {
|
|
4107
|
+
const e = err;
|
|
4108
|
+
const detail = e.stderr?.toString().trim() ?? e.message;
|
|
4109
|
+
throw new NomadFatal(`git failed to unstage ${rel}: ${detail}`);
|
|
4255
4110
|
}
|
|
4256
|
-
|
|
4111
|
+
item(`dropped ${rel}`);
|
|
4257
4112
|
}
|
|
4258
4113
|
|
|
4259
|
-
// src/
|
|
4114
|
+
// src/commands.pull.ts
|
|
4115
|
+
import { existsSync as existsSync35, mkdirSync as mkdirSync8 } from "node:fs";
|
|
4116
|
+
import { join as join41 } from "node:path";
|
|
4117
|
+
|
|
4118
|
+
// src/commands.push.sections.ts
|
|
4260
4119
|
init_color();
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
let prefix;
|
|
4270
|
-
if (part.removed) prefix = (line) => red(`-${line}`);
|
|
4271
|
-
else if (part.added) prefix = (line) => green(`+${line}`);
|
|
4272
|
-
else prefix = (line) => ` ${line}`;
|
|
4273
|
-
for (const line of partLines) {
|
|
4274
|
-
lines.push(prefix(line));
|
|
4120
|
+
|
|
4121
|
+
// src/summary.ts
|
|
4122
|
+
init_utils();
|
|
4123
|
+
function summaryText(verb, unmapped, collisions = 0, extrasSkipped = 0) {
|
|
4124
|
+
const extras = extrasSkipped > 0 ? `, ${extrasSkipped} extras skipped` : "";
|
|
4125
|
+
if (verb === "push") {
|
|
4126
|
+
if (unmapped === 0 && collisions === 0 && extrasSkipped === 0) {
|
|
4127
|
+
return { text: "summary: clean", clean: true };
|
|
4275
4128
|
}
|
|
4129
|
+
const base = `summary: ${unmapped} unmapped on push, ${collisions} collisions`;
|
|
4130
|
+
return { text: `${base}${extras} (run nomad doctor to list)`, clean: false };
|
|
4276
4131
|
}
|
|
4277
|
-
|
|
4132
|
+
if (unmapped === 0 && extrasSkipped === 0) {
|
|
4133
|
+
return { text: "summary: clean", clean: true };
|
|
4134
|
+
}
|
|
4135
|
+
return {
|
|
4136
|
+
text: `summary: ${unmapped} unmapped on ${verb}${extras} (run nomad doctor to list)`,
|
|
4137
|
+
clean: false
|
|
4138
|
+
};
|
|
4139
|
+
}
|
|
4140
|
+
function summaryRow(verb, unmapped, collisions = 0, extrasSkipped = 0) {
|
|
4141
|
+
const { text } = summaryText(verb, unmapped, collisions, extrasSkipped);
|
|
4142
|
+
return text.replace(/^summary: /, "");
|
|
4278
4143
|
}
|
|
4279
4144
|
|
|
4280
|
-
// src/
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4145
|
+
// src/commands.push.sections.ts
|
|
4146
|
+
function collapsedSkipRow(n, noun) {
|
|
4147
|
+
if (n <= 0) return null;
|
|
4148
|
+
return `${dim(infoGlyph)} ${n} ${noun}`;
|
|
4149
|
+
}
|
|
4150
|
+
function buildSettingsSection(label) {
|
|
4151
|
+
const s = section("Settings");
|
|
4152
|
+
addItem(s, `${green(okGlyph)} settings.json (base + ${label})`);
|
|
4153
|
+
return s;
|
|
4154
|
+
}
|
|
4155
|
+
function buildSessionsSection(items, unmapped) {
|
|
4156
|
+
const s = section("Sessions");
|
|
4157
|
+
for (const logical of items) addItem(s, `${green(okGlyph)} ${logical}`);
|
|
4158
|
+
const skip = collapsedSkipRow(unmapped, "not in path-map (run nomad doctor to list)");
|
|
4159
|
+
if (skip !== null) addItem(s, skip);
|
|
4160
|
+
return s;
|
|
4161
|
+
}
|
|
4162
|
+
function buildExtrasSection(items, extrasSkipped) {
|
|
4163
|
+
const s = section("Extras");
|
|
4164
|
+
for (const entry of items) addItem(s, `${green(okGlyph)} ${entry}`);
|
|
4165
|
+
const skip = collapsedSkipRow(extrasSkipped, "extras skipped");
|
|
4166
|
+
if (skip !== null) addItem(s, skip);
|
|
4167
|
+
return s;
|
|
4168
|
+
}
|
|
4169
|
+
function syncedSections(st) {
|
|
4170
|
+
const sessions = st.dryRun ? st.remap.wouldPush : st.remap.pushed;
|
|
4171
|
+
const extras = st.dryRun ? st.extras.wouldPush : st.extras.pushed;
|
|
4172
|
+
return [
|
|
4173
|
+
buildSessionsSection(sessions, st.remap.unmapped),
|
|
4174
|
+
buildExtrasSection(extras, st.extras.skipped)
|
|
4289
4175
|
];
|
|
4290
|
-
return lines.join("\n");
|
|
4291
4176
|
}
|
|
4292
|
-
function
|
|
4293
|
-
|
|
4177
|
+
function summarySection(st) {
|
|
4178
|
+
const s = section("Summary");
|
|
4179
|
+
const unmapped = st.remap.unmapped + st.extras.unmapped;
|
|
4180
|
+
addItem(s, summaryRow("push", unmapped, st.remap.collisions, st.extras.skipped));
|
|
4181
|
+
return s;
|
|
4182
|
+
}
|
|
4183
|
+
function renderPushTree(st, verdict) {
|
|
4184
|
+
const leakScan = section("Leak scan");
|
|
4185
|
+
addItem(leakScan, verdict.verdictRow);
|
|
4186
|
+
renderTree([...syncedSections(st), leakScan, summarySection(st)]);
|
|
4187
|
+
}
|
|
4188
|
+
function renderNoScanTree(st, opts = {}) {
|
|
4189
|
+
const sections = [];
|
|
4190
|
+
if (opts.noMapHint === true) {
|
|
4191
|
+
const pathMap = section("Path map");
|
|
4192
|
+
addItem(pathMap, `${dim(infoGlyph)} no path-map.json (nothing to preview)`);
|
|
4193
|
+
sections.push(pathMap);
|
|
4194
|
+
}
|
|
4195
|
+
renderTree([...sections, ...syncedSections(st), summarySection(st)]);
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
// src/commands.pull.ts
|
|
4199
|
+
init_config();
|
|
4200
|
+
|
|
4201
|
+
// src/extras-sync.ts
|
|
4202
|
+
init_config();
|
|
4203
|
+
import { existsSync as existsSync32 } from "node:fs";
|
|
4204
|
+
import { join as join38 } from "node:path";
|
|
4205
|
+
|
|
4206
|
+
// src/extras-sync.diff.ts
|
|
4207
|
+
init_utils();
|
|
4208
|
+
import { execFileSync as execFileSync13 } from "node:child_process";
|
|
4209
|
+
function labelDiffLine(line) {
|
|
4210
|
+
const tab = line.indexOf(" ");
|
|
4211
|
+
if (tab === -1) return line;
|
|
4212
|
+
const status = line.slice(0, tab);
|
|
4213
|
+
const path = line.slice(tab + 1);
|
|
4214
|
+
if (status === "D") return `${path} (local only)`;
|
|
4215
|
+
if (status === "A") return `${path} (repo only)`;
|
|
4216
|
+
return path;
|
|
4217
|
+
}
|
|
4218
|
+
function parseDiffOutput(stdout) {
|
|
4219
|
+
return stdout.split("\n").filter((line) => line.length > 0).map(labelDiffLine);
|
|
4220
|
+
}
|
|
4221
|
+
function listDivergingFiles(a, b) {
|
|
4294
4222
|
try {
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4223
|
+
const stdout = execFileSync13("git", ["diff", "--no-index", "--name-status", a, b], {
|
|
4224
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
4225
|
+
}).toString();
|
|
4226
|
+
return parseDiffOutput(stdout);
|
|
4227
|
+
} catch (err) {
|
|
4228
|
+
const e = err;
|
|
4229
|
+
if (e.status === 1 && e.stdout !== void 0) {
|
|
4230
|
+
return parseDiffOutput(e.stdout.toString());
|
|
4231
|
+
}
|
|
4232
|
+
if (e.code === "ENOENT") {
|
|
4233
|
+
warn(`git not on PATH; divergence check skipped for ${a}`);
|
|
4234
|
+
return [];
|
|
4235
|
+
}
|
|
4236
|
+
warn(`divergence check failed for ${a}: ${e.message ?? String(err)}`);
|
|
4237
|
+
return [];
|
|
4298
4238
|
}
|
|
4299
4239
|
}
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
|
|
4303
|
-
|
|
4240
|
+
|
|
4241
|
+
// src/extras-sync.core.ts
|
|
4242
|
+
init_config();
|
|
4243
|
+
import { cpSync as cpSync6, existsSync as existsSync30, rmSync as rmSync10 } from "node:fs";
|
|
4244
|
+
import { basename, join as join36 } from "node:path";
|
|
4245
|
+
|
|
4246
|
+
// src/extras-sync.guards.ts
|
|
4247
|
+
init_utils();
|
|
4248
|
+
init_config_sharedDirs_guard();
|
|
4249
|
+
import { isAbsolute as isAbsolute2, normalize } from "node:path";
|
|
4250
|
+
function assertSafeLocalRoot(localRoot, logical) {
|
|
4251
|
+
if (!isAbsolute2(localRoot)) {
|
|
4252
|
+
throw new NomadFatal(
|
|
4253
|
+
`invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be absolute)`
|
|
4254
|
+
);
|
|
4304
4255
|
}
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4256
|
+
if (localRoot !== normalize(localRoot)) {
|
|
4257
|
+
throw new NomadFatal(
|
|
4258
|
+
`invalid localRoot for ${logical} in path-map.json: ${JSON.stringify(localRoot)} (must be already-normalized; no '..' or redundant segments)`
|
|
4259
|
+
);
|
|
4309
4260
|
}
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
// src/extras-sync.core.ts
|
|
4264
|
+
init_utils();
|
|
4265
|
+
init_utils_json();
|
|
4266
|
+
function loadValidatedExtras(opts) {
|
|
4267
|
+
const repo = repoHome();
|
|
4268
|
+
const mapPath = join36(repo, "path-map.json");
|
|
4269
|
+
const repoExtras = join36(repo, "shared", "extras");
|
|
4270
|
+
if (!existsSync30(mapPath) || opts.requireRepoExtras === true && !existsSync30(repoExtras)) {
|
|
4271
|
+
if (opts.missingMsg !== void 0) log(opts.missingMsg);
|
|
4272
|
+
return null;
|
|
4314
4273
|
}
|
|
4315
|
-
const
|
|
4316
|
-
const
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4274
|
+
const map = readPathMap(mapPath);
|
|
4275
|
+
const extrasMap = map.extras ?? {};
|
|
4276
|
+
if (Object.keys(extrasMap).length === 0) return null;
|
|
4277
|
+
for (const logical of Object.keys(extrasMap)) {
|
|
4278
|
+
assertSafeLogical(logical);
|
|
4279
|
+
const localRoot = map.projects[logical]?.[HOST];
|
|
4280
|
+
if (localRoot && localRoot !== "TBD") assertSafeLocalRoot(localRoot, logical);
|
|
4281
|
+
}
|
|
4282
|
+
return { map, extrasMap };
|
|
4322
4283
|
}
|
|
4323
|
-
function
|
|
4324
|
-
|
|
4284
|
+
function* eachExtrasTarget(v, counts) {
|
|
4285
|
+
const whitelist = SUPPORTED_EXTRAS;
|
|
4286
|
+
for (const [logical, dirnames] of Object.entries(v.extrasMap)) {
|
|
4287
|
+
const localRoot = v.map.projects[logical]?.[HOST];
|
|
4288
|
+
if (!localRoot || localRoot === "TBD") {
|
|
4289
|
+
counts.unmapped++;
|
|
4290
|
+
continue;
|
|
4291
|
+
}
|
|
4292
|
+
for (const dirname7 of dirnames) {
|
|
4293
|
+
if (!whitelist.includes(dirname7)) {
|
|
4294
|
+
counts.skipped++;
|
|
4295
|
+
continue;
|
|
4296
|
+
}
|
|
4297
|
+
yield { logical, localRoot, dirname: dirname7 };
|
|
4298
|
+
}
|
|
4299
|
+
}
|
|
4325
4300
|
}
|
|
4326
|
-
function
|
|
4327
|
-
|
|
4301
|
+
function copyExtras(src, dst) {
|
|
4302
|
+
rmSync10(dst, { recursive: true, force: true });
|
|
4303
|
+
cpSync6(src, dst, { recursive: true, force: true, verbatimSymlinks: true });
|
|
4328
4304
|
}
|
|
4329
|
-
function
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4305
|
+
function extrasDenySet(dirname7) {
|
|
4306
|
+
return dirname7 === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
|
|
4307
|
+
}
|
|
4308
|
+
function copyExtrasFiltered(src, dst, blockSet) {
|
|
4309
|
+
rmSync10(dst, { recursive: true, force: true });
|
|
4310
|
+
cpSync6(src, dst, {
|
|
4311
|
+
recursive: true,
|
|
4312
|
+
force: true,
|
|
4313
|
+
verbatimSymlinks: true,
|
|
4314
|
+
filter: (srcEntry) => srcEntry === src || !blockSet.has(basename(srcEntry))
|
|
4315
|
+
});
|
|
4316
|
+
}
|
|
4317
|
+
|
|
4318
|
+
// src/extras-sync.ts
|
|
4319
|
+
init_utils();
|
|
4320
|
+
init_utils_json();
|
|
4321
|
+
|
|
4322
|
+
// src/extras-sync.remap.ts
|
|
4323
|
+
init_config();
|
|
4324
|
+
import { existsSync as existsSync31, mkdirSync as mkdirSync7 } from "node:fs";
|
|
4325
|
+
import { join as join37 } from "node:path";
|
|
4326
|
+
init_utils_fs();
|
|
4327
|
+
function runExtrasOp(v, dryRun, paths, backup, copy) {
|
|
4328
|
+
const counts = { unmapped: 0, skipped: 0 };
|
|
4329
|
+
const done = [];
|
|
4330
|
+
const would = [];
|
|
4331
|
+
for (const t of eachExtrasTarget(v, counts)) {
|
|
4332
|
+
const { src, dst } = paths(t);
|
|
4333
|
+
if (!existsSync31(src)) continue;
|
|
4334
|
+
const item2 = `${t.logical}/${t.dirname}`;
|
|
4335
|
+
if (dryRun) {
|
|
4336
|
+
would.push(item2);
|
|
4337
|
+
continue;
|
|
4334
4338
|
}
|
|
4339
|
+
backup(dst, t.localRoot);
|
|
4340
|
+
copy(src, dst, t.dirname);
|
|
4341
|
+
done.push(item2);
|
|
4335
4342
|
}
|
|
4336
|
-
|
|
4337
|
-
addItem(s, `note: ${note}`);
|
|
4338
|
-
}
|
|
4339
|
-
return s;
|
|
4343
|
+
return { ...counts, done, would };
|
|
4340
4344
|
}
|
|
4341
|
-
function
|
|
4345
|
+
function remapExtrasPush(ts, opts = {}) {
|
|
4346
|
+
const dryRun = opts.dryRun === true;
|
|
4347
|
+
const v = loadValidatedExtras({ missingMsg: "no path-map.json; skipping extras push" });
|
|
4348
|
+
if (v === null) return { unmapped: 0, skipped: 0, pushed: [], wouldPush: [] };
|
|
4342
4349
|
const repo = repoHome();
|
|
4343
|
-
const
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4350
|
+
const repoExtras = join37(repo, "shared", "extras");
|
|
4351
|
+
if (!dryRun) mkdirSync7(repoExtras, { recursive: true });
|
|
4352
|
+
const { unmapped, skipped, done, would } = runExtrasOp(
|
|
4353
|
+
v,
|
|
4354
|
+
dryRun,
|
|
4355
|
+
({ localRoot, logical, dirname: dirname7 }) => ({
|
|
4356
|
+
src: join37(localRoot, dirname7),
|
|
4357
|
+
dst: join37(repoExtras, logical, dirname7)
|
|
4358
|
+
}),
|
|
4359
|
+
(dst) => backupRepoWrite(dst, ts, repo),
|
|
4360
|
+
// Push filters every extra by its per-name denylist: `.claude` gets the
|
|
4361
|
+
// full NEVER_SYNC boundary, `.planning` keeps the narrow ALWAYS_NEVER_SYNC.
|
|
4362
|
+
(src, dst, dirname7) => copyExtrasFiltered(src, dst, extrasDenySet(dirname7))
|
|
4355
4363
|
);
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4364
|
+
return { unmapped, skipped, pushed: done, wouldPush: would };
|
|
4365
|
+
}
|
|
4366
|
+
function remapExtrasPull(ts, opts = {}) {
|
|
4367
|
+
const dryRun = opts.dryRun === true;
|
|
4368
|
+
const v = loadValidatedExtras({
|
|
4369
|
+
requireRepoExtras: true,
|
|
4370
|
+
missingMsg: "no path-map or repo extras dir; skipping extras remap"
|
|
4361
4371
|
});
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
|
|
4372
|
+
if (v === null) return { unmapped: 0, skipped: 0, pulled: [], wouldPull: [] };
|
|
4373
|
+
const repoExtras = join37(repoHome(), "shared", "extras");
|
|
4374
|
+
const { unmapped, skipped, done, would } = runExtrasOp(
|
|
4375
|
+
v,
|
|
4376
|
+
dryRun,
|
|
4377
|
+
({ localRoot, logical, dirname: dirname7 }) => ({
|
|
4378
|
+
src: join37(repoExtras, logical, dirname7),
|
|
4379
|
+
dst: join37(localRoot, dirname7)
|
|
4380
|
+
}),
|
|
4381
|
+
// Snapshot the host-side dst BEFORE copyExtras clobbers it. Anchor on
|
|
4382
|
+
// localRoot so the backup tree mirrors the project layout.
|
|
4383
|
+
(dst, localRoot) => backupExtrasWrite(dst, ts, localRoot),
|
|
4384
|
+
// Pull filters `.claude` against its NEVER_SYNC boundary so a repo poisoned
|
|
4385
|
+
// out-of-band cannot restore a blocked per-host file (e.g. settings.local.json)
|
|
4386
|
+
// onto this host. Other extras use the exact-mirror copyExtras: the repo is
|
|
4387
|
+
// clean once push filters, and exact mirror is the documented restore for
|
|
4388
|
+
// `.planning`.
|
|
4389
|
+
(src, dst, dirname7) => dirname7 === ".claude" ? copyExtrasFiltered(src, dst, extrasDenySet(dirname7)) : copyExtras(src, dst)
|
|
4390
|
+
);
|
|
4391
|
+
return { unmapped, skipped, pulled: done, wouldPull: would };
|
|
4366
4392
|
}
|
|
4367
4393
|
|
|
4368
|
-
// src/
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4394
|
+
// src/extras-sync.ts
|
|
4395
|
+
function divergenceCheckExtras(ts) {
|
|
4396
|
+
const v = loadValidatedExtras({});
|
|
4397
|
+
if (v === null) return;
|
|
4398
|
+
const counts = { unmapped: 0, skipped: 0 };
|
|
4399
|
+
const backupRoot = join38(backupBase(), ts, "extras");
|
|
4400
|
+
const repo = repoHome();
|
|
4401
|
+
for (const { logical, localRoot, dirname: dirname7 } of eachExtrasTarget(v, counts)) {
|
|
4402
|
+
const local = join38(localRoot, dirname7);
|
|
4403
|
+
const repoEntry = join38(repo, "shared", "extras", logical, dirname7);
|
|
4404
|
+
if (!existsSync32(local) || !existsSync32(repoEntry)) continue;
|
|
4405
|
+
const diff = listDivergingFiles(local, repoEntry);
|
|
4406
|
+
if (diff.length === 0) continue;
|
|
4407
|
+
const projectBackupRoot = join38(backupRoot, encodePath(localRoot));
|
|
4408
|
+
warn(
|
|
4409
|
+
`local ${dirname7} for ${logical} diverges from origin in ${diff.length} file(s); next remapExtrasPull will overwrite them (backups at ${projectBackupRoot}/)`
|
|
4410
|
+
);
|
|
4411
|
+
for (const f of diff) warn(` ${f}`);
|
|
4412
|
+
}
|
|
4413
|
+
}
|
|
4379
4414
|
|
|
4380
|
-
// src/
|
|
4415
|
+
// src/links.ts
|
|
4381
4416
|
init_config();
|
|
4382
|
-
init_config_sharedDirs_guard();
|
|
4383
|
-
import { cpSync as cpSync6, existsSync as existsSync33, mkdirSync as mkdirSync7, statSync as statSync8 } from "node:fs";
|
|
4384
|
-
import { dirname as dirname6, join as join38, sep as sep3 } from "node:path";
|
|
4385
|
-
init_push_gitleaks_scan();
|
|
4386
|
-
init_utils_json();
|
|
4387
4417
|
init_utils();
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4418
|
+
init_utils_fs();
|
|
4419
|
+
init_utils_json();
|
|
4420
|
+
import { existsSync as existsSync33, lstatSync as lstatSync8, rmSync as rmSync11 } from "node:fs";
|
|
4421
|
+
import { join as join39 } from "node:path";
|
|
4422
|
+
function emitAutoMove(onPreview, linkPath, ts, name) {
|
|
4423
|
+
if (onPreview) {
|
|
4424
|
+
onPreview({ kind: "auto-move", from: linkPath, to: `backup/${ts}/${name}` });
|
|
4425
|
+
} else {
|
|
4426
|
+
log(`would auto-move non-symlink: ${linkPath} -> backup/${ts}/${name}`);
|
|
4427
|
+
}
|
|
4393
4428
|
}
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4429
|
+
function emitCreate(onPreview, from, to) {
|
|
4430
|
+
if (onPreview) {
|
|
4431
|
+
onPreview({ kind: "create", from, to });
|
|
4432
|
+
} else {
|
|
4433
|
+
log(`would create symlink: ${from} -> ${to}`);
|
|
4434
|
+
}
|
|
4400
4435
|
}
|
|
4401
|
-
function
|
|
4402
|
-
|
|
4403
|
-
if (t === "r" || t === "redact") return "redact";
|
|
4404
|
-
if (t === "a" || t === "allow") return "allow";
|
|
4405
|
-
if (t === "d" || t === "drop") return "drop";
|
|
4406
|
-
return "skip";
|
|
4436
|
+
function isAlreadySymlink(linkPath) {
|
|
4437
|
+
return existsSync33(linkPath) && lstatSync8(linkPath).isSymbolicLink();
|
|
4407
4438
|
}
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
if (
|
|
4415
|
-
if (
|
|
4416
|
-
|
|
4439
|
+
function runAutoMovePasses(linkNames, claude, repo, ts, dryRun, onPreview) {
|
|
4440
|
+
for (const name of linkNames) {
|
|
4441
|
+
const linkPath = join39(claude, name);
|
|
4442
|
+
const target = join39(repo, "shared", name);
|
|
4443
|
+
if (!existsSync33(linkPath)) continue;
|
|
4444
|
+
if (lstatSync8(linkPath).isSymbolicLink()) continue;
|
|
4445
|
+
if (!existsSync33(target)) continue;
|
|
4446
|
+
if (dryRun) {
|
|
4447
|
+
emitAutoMove(onPreview, linkPath, ts, name);
|
|
4448
|
+
continue;
|
|
4417
4449
|
}
|
|
4450
|
+
backupBeforeWrite(linkPath, ts);
|
|
4451
|
+
rmSync11(linkPath, { recursive: true, force: true });
|
|
4418
4452
|
}
|
|
4419
|
-
return null;
|
|
4420
4453
|
}
|
|
4421
|
-
function
|
|
4422
|
-
const
|
|
4423
|
-
log(msg);
|
|
4424
|
-
return false;
|
|
4425
|
-
};
|
|
4454
|
+
function applySharedLinks(ts, map, opts = {}) {
|
|
4455
|
+
const dryRun = opts.dryRun === true;
|
|
4426
4456
|
const claude = claudeHome();
|
|
4427
4457
|
const repo = repoHome();
|
|
4428
|
-
const
|
|
4429
|
-
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
);
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
const sessionDir = join38(dirname6(localPath), sid);
|
|
4441
|
-
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
4442
|
-
const subtreeMtime = newestSubtreeMtimeMs(localPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
|
|
4443
|
-
if (isRecentlyModified(subtreeMtime, nowMs())) {
|
|
4444
|
-
return refuse(
|
|
4445
|
-
`session ${sid} looks active (modified within the last 5 minutes); refusing to redact, no changes made.
|
|
4446
|
-
End the session and choose Redact again, or choose Drop session (holds this session back from the push, local copy kept) or Skip.`
|
|
4447
|
-
);
|
|
4448
|
-
}
|
|
4449
|
-
const stagedProjectDir = resolveStagedDir(localPath, map, claude, repo);
|
|
4450
|
-
if (stagedProjectDir === null) {
|
|
4451
|
-
return refuse(
|
|
4452
|
-
`could not map the local transcript for session ${sid} to a staged copy; choose Drop session or Skip.`
|
|
4453
|
-
);
|
|
4458
|
+
const linkNames = allSharedLinks(map);
|
|
4459
|
+
runAutoMovePasses(linkNames, claude, repo, ts, dryRun, opts.onPreview);
|
|
4460
|
+
for (const name of linkNames) {
|
|
4461
|
+
const target = join39(repo, "shared", name);
|
|
4462
|
+
if (!existsSync33(target)) continue;
|
|
4463
|
+
const linkPath = join39(claude, name);
|
|
4464
|
+
if (isAlreadySymlink(linkPath)) continue;
|
|
4465
|
+
if (dryRun) {
|
|
4466
|
+
emitCreate(opts.onPreview, linkPath, target);
|
|
4467
|
+
continue;
|
|
4468
|
+
}
|
|
4469
|
+
ensureSymlink(linkPath, target);
|
|
4454
4470
|
}
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4471
|
+
}
|
|
4472
|
+
function regenerateSettings(ts, opts = {}) {
|
|
4473
|
+
const dryRun = opts.dryRun === true;
|
|
4474
|
+
const repo = repoHome();
|
|
4475
|
+
const claude = claudeHome();
|
|
4476
|
+
const basePath = join39(repo, "shared", "settings.base.json");
|
|
4477
|
+
const hostPath = join39(repo, "hosts", `${HOST}.json`);
|
|
4478
|
+
if (!existsSync33(basePath)) {
|
|
4479
|
+
die("repo not initialized; run 'nomad init' to scaffold");
|
|
4458
4480
|
}
|
|
4459
|
-
const
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4481
|
+
const base = readJson(basePath);
|
|
4482
|
+
const hasOverrides = existsSync33(hostPath);
|
|
4483
|
+
const overrides = hasOverrides ? readJson(hostPath) : {};
|
|
4484
|
+
const merged = deepMerge(base, overrides);
|
|
4485
|
+
const settingsPath = join39(claude, "settings.json");
|
|
4486
|
+
if (!hasOverrides && existsSync33(settingsPath)) {
|
|
4487
|
+
try {
|
|
4488
|
+
const existing = readJson(settingsPath);
|
|
4489
|
+
const baseKeys = new Set(Object.keys(base));
|
|
4490
|
+
const drift = Object.keys(existing).filter((k) => !baseKeys.has(k));
|
|
4491
|
+
if (drift.length > 0) {
|
|
4492
|
+
warn(
|
|
4493
|
+
`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.`
|
|
4494
|
+
);
|
|
4495
|
+
}
|
|
4496
|
+
} catch {
|
|
4497
|
+
warn("existing settings.json is malformed; skipping drift-check and regenerating.");
|
|
4498
|
+
}
|
|
4472
4499
|
}
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4500
|
+
const overrideLabel = hasOverrides ? `${HOST}.json` : "no host overrides";
|
|
4501
|
+
if (dryRun) {
|
|
4502
|
+
log(`would write settings.json (base + ${overrideLabel})`);
|
|
4503
|
+
return { label: overrideLabel };
|
|
4477
4504
|
}
|
|
4478
|
-
|
|
4505
|
+
backupBeforeWrite(settingsPath, ts);
|
|
4506
|
+
writeJsonAtomic(settingsPath, merged);
|
|
4507
|
+
return { label: overrideLabel };
|
|
4479
4508
|
}
|
|
4480
4509
|
|
|
4481
|
-
// src/
|
|
4510
|
+
// src/preview.ts
|
|
4482
4511
|
init_config();
|
|
4483
|
-
import {
|
|
4484
|
-
import { join as
|
|
4485
|
-
function dropSessionFromStaged(sid, map) {
|
|
4486
|
-
const logicals = Object.keys(map.projects);
|
|
4487
|
-
if (logicals.length === 0) return false;
|
|
4488
|
-
const repo = repoHome();
|
|
4489
|
-
for (const logical of logicals) {
|
|
4490
|
-
const jsonl = join39(repo, "shared", "projects", logical, `${sid}.jsonl`);
|
|
4491
|
-
const dir = join39(repo, "shared", "projects", logical, sid);
|
|
4492
|
-
rmSync10(jsonl, { force: true });
|
|
4493
|
-
rmSync10(dir, { recursive: true, force: true });
|
|
4494
|
-
}
|
|
4495
|
-
return true;
|
|
4496
|
-
}
|
|
4512
|
+
import { existsSync as existsSync34 } from "node:fs";
|
|
4513
|
+
import { join as join40 } from "node:path";
|
|
4497
4514
|
|
|
4498
|
-
//
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4515
|
+
// node_modules/diff/libesm/diff/base.js
|
|
4516
|
+
var Diff = class {
|
|
4517
|
+
diff(oldStr, newStr, options = {}) {
|
|
4518
|
+
let callback;
|
|
4519
|
+
if (typeof options === "function") {
|
|
4520
|
+
callback = options;
|
|
4521
|
+
options = {};
|
|
4522
|
+
} else if ("callback" in options) {
|
|
4523
|
+
callback = options.callback;
|
|
4524
|
+
}
|
|
4525
|
+
const oldString = this.castInput(oldStr, options);
|
|
4526
|
+
const newString = this.castInput(newStr, options);
|
|
4527
|
+
const oldTokens = this.removeEmpty(this.tokenize(oldString, options));
|
|
4528
|
+
const newTokens = this.removeEmpty(this.tokenize(newString, options));
|
|
4529
|
+
return this.diffWithOptionsObj(oldTokens, newTokens, options, callback);
|
|
4507
4530
|
}
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4531
|
+
diffWithOptionsObj(oldTokens, newTokens, options, callback) {
|
|
4532
|
+
var _a;
|
|
4533
|
+
const done = (value) => {
|
|
4534
|
+
value = this.postProcess(value, options);
|
|
4535
|
+
if (callback) {
|
|
4536
|
+
setTimeout(function() {
|
|
4537
|
+
callback(value);
|
|
4538
|
+
}, 0);
|
|
4539
|
+
return void 0;
|
|
4540
|
+
} else {
|
|
4541
|
+
return value;
|
|
4542
|
+
}
|
|
4543
|
+
};
|
|
4544
|
+
const newLen = newTokens.length, oldLen = oldTokens.length;
|
|
4545
|
+
let editLength = 1;
|
|
4546
|
+
let maxEditLength = newLen + oldLen;
|
|
4547
|
+
if (options.maxEditLength != null) {
|
|
4548
|
+
maxEditLength = Math.min(maxEditLength, options.maxEditLength);
|
|
4549
|
+
}
|
|
4550
|
+
const maxExecutionTime = (_a = options.timeout) !== null && _a !== void 0 ? _a : Infinity;
|
|
4551
|
+
const abortAfterTimestamp = Date.now() + maxExecutionTime;
|
|
4552
|
+
const bestPath = [{ oldPos: -1, lastComponent: void 0 }];
|
|
4553
|
+
let newPos = this.extractCommon(bestPath[0], newTokens, oldTokens, 0, options);
|
|
4554
|
+
if (bestPath[0].oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
4555
|
+
return done(this.buildValues(bestPath[0].lastComponent, newTokens, oldTokens));
|
|
4556
|
+
}
|
|
4557
|
+
let minDiagonalToConsider = -Infinity, maxDiagonalToConsider = Infinity;
|
|
4558
|
+
const execEditLength = () => {
|
|
4559
|
+
for (let diagonalPath = Math.max(minDiagonalToConsider, -editLength); diagonalPath <= Math.min(maxDiagonalToConsider, editLength); diagonalPath += 2) {
|
|
4560
|
+
let basePath;
|
|
4561
|
+
const removePath = bestPath[diagonalPath - 1], addPath = bestPath[diagonalPath + 1];
|
|
4562
|
+
if (removePath) {
|
|
4563
|
+
bestPath[diagonalPath - 1] = void 0;
|
|
4564
|
+
}
|
|
4565
|
+
let canAdd = false;
|
|
4566
|
+
if (addPath) {
|
|
4567
|
+
const addPathNewPos = addPath.oldPos - diagonalPath;
|
|
4568
|
+
canAdd = addPath && 0 <= addPathNewPos && addPathNewPos < newLen;
|
|
4569
|
+
}
|
|
4570
|
+
const canRemove = removePath && removePath.oldPos + 1 < oldLen;
|
|
4571
|
+
if (!canAdd && !canRemove) {
|
|
4572
|
+
bestPath[diagonalPath] = void 0;
|
|
4573
|
+
continue;
|
|
4574
|
+
}
|
|
4575
|
+
if (!canRemove || canAdd && removePath.oldPos < addPath.oldPos) {
|
|
4576
|
+
basePath = this.addToPath(addPath, true, false, 0, options);
|
|
4577
|
+
} else {
|
|
4578
|
+
basePath = this.addToPath(removePath, false, true, 1, options);
|
|
4579
|
+
}
|
|
4580
|
+
newPos = this.extractCommon(basePath, newTokens, oldTokens, diagonalPath, options);
|
|
4581
|
+
if (basePath.oldPos + 1 >= oldLen && newPos + 1 >= newLen) {
|
|
4582
|
+
return done(this.buildValues(basePath.lastComponent, newTokens, oldTokens)) || true;
|
|
4583
|
+
} else {
|
|
4584
|
+
bestPath[diagonalPath] = basePath;
|
|
4585
|
+
if (basePath.oldPos + 1 >= oldLen) {
|
|
4586
|
+
maxDiagonalToConsider = Math.min(maxDiagonalToConsider, diagonalPath - 1);
|
|
4587
|
+
}
|
|
4588
|
+
if (newPos + 1 >= newLen) {
|
|
4589
|
+
minDiagonalToConsider = Math.max(minDiagonalToConsider, diagonalPath + 1);
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
}
|
|
4593
|
+
editLength++;
|
|
4594
|
+
};
|
|
4595
|
+
if (callback) {
|
|
4596
|
+
(function exec() {
|
|
4597
|
+
setTimeout(function() {
|
|
4598
|
+
if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
|
|
4599
|
+
return callback(void 0);
|
|
4600
|
+
}
|
|
4601
|
+
if (!execEditLength()) {
|
|
4602
|
+
exec();
|
|
4603
|
+
}
|
|
4604
|
+
}, 0);
|
|
4605
|
+
})();
|
|
4606
|
+
} else {
|
|
4607
|
+
while (editLength <= maxEditLength && Date.now() <= abortAfterTimestamp) {
|
|
4608
|
+
const ret = execEditLength();
|
|
4609
|
+
if (ret) {
|
|
4610
|
+
return ret;
|
|
4611
|
+
}
|
|
4612
|
+
}
|
|
4515
4613
|
}
|
|
4516
4614
|
}
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
}
|
|
4529
|
-
|
|
4530
|
-
const action = ctx.actions.get(findingKey(f)) ?? "skip";
|
|
4531
|
-
if (action === "skip") return;
|
|
4532
|
-
const sid = sessionIdFromFinding(f);
|
|
4533
|
-
if (sid !== null && ctx.droppedSids.has(sid)) return;
|
|
4534
|
-
if (action === "allow") {
|
|
4535
|
-
applyAllow(f, ctx.repo);
|
|
4536
|
-
return;
|
|
4615
|
+
addToPath(path, added, removed, oldPosInc, options) {
|
|
4616
|
+
const last = path.lastComponent;
|
|
4617
|
+
if (last && !options.oneChangePerToken && last.added === added && last.removed === removed) {
|
|
4618
|
+
return {
|
|
4619
|
+
oldPos: path.oldPos + oldPosInc,
|
|
4620
|
+
lastComponent: { count: last.count + 1, added, removed, previousComponent: last.previousComponent }
|
|
4621
|
+
};
|
|
4622
|
+
} else {
|
|
4623
|
+
return {
|
|
4624
|
+
oldPos: path.oldPos + oldPosInc,
|
|
4625
|
+
lastComponent: { count: 1, added, removed, previousComponent: last }
|
|
4626
|
+
};
|
|
4627
|
+
}
|
|
4537
4628
|
}
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4629
|
+
extractCommon(basePath, newTokens, oldTokens, diagonalPath, options) {
|
|
4630
|
+
const newLen = newTokens.length, oldLen = oldTokens.length;
|
|
4631
|
+
let oldPos = basePath.oldPos, newPos = oldPos - diagonalPath, commonCount = 0;
|
|
4632
|
+
while (newPos + 1 < newLen && oldPos + 1 < oldLen && this.equals(oldTokens[oldPos + 1], newTokens[newPos + 1], options)) {
|
|
4633
|
+
newPos++;
|
|
4634
|
+
oldPos++;
|
|
4635
|
+
commonCount++;
|
|
4636
|
+
if (options.oneChangePerToken) {
|
|
4637
|
+
basePath.lastComponent = { count: 1, previousComponent: basePath.lastComponent, added: false, removed: false };
|
|
4638
|
+
}
|
|
4545
4639
|
}
|
|
4546
|
-
|
|
4640
|
+
if (commonCount && !options.oneChangePerToken) {
|
|
4641
|
+
basePath.lastComponent = { count: commonCount, previousComponent: basePath.lastComponent, added: false, removed: false };
|
|
4642
|
+
}
|
|
4643
|
+
basePath.oldPos = oldPos;
|
|
4644
|
+
return newPos;
|
|
4547
4645
|
}
|
|
4548
|
-
|
|
4549
|
-
if (
|
|
4646
|
+
equals(left, right, options) {
|
|
4647
|
+
if (options.comparator) {
|
|
4648
|
+
return options.comparator(left, right);
|
|
4649
|
+
} else {
|
|
4650
|
+
return left === right || !!options.ignoreCase && left.toLowerCase() === right.toLowerCase();
|
|
4651
|
+
}
|
|
4550
4652
|
}
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
repo,
|
|
4560
|
-
scan,
|
|
4561
|
-
drop,
|
|
4562
|
-
redactedSids: /* @__PURE__ */ new Set(),
|
|
4563
|
-
droppedSids: /* @__PURE__ */ new Set()
|
|
4564
|
-
};
|
|
4565
|
-
for (const f of findings) {
|
|
4566
|
-
dispatchOne(f, ctx);
|
|
4653
|
+
removeEmpty(array) {
|
|
4654
|
+
const ret = [];
|
|
4655
|
+
for (let i = 0; i < array.length; i++) {
|
|
4656
|
+
if (array[i]) {
|
|
4657
|
+
ret.push(array[i]);
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
return ret;
|
|
4567
4661
|
}
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
for (const f of findings) {
|
|
4572
|
-
const sid = sessionIdFromFinding(f);
|
|
4573
|
-
if (sid === null || redactedSids.has(sid)) continue;
|
|
4574
|
-
if (applyRedact(f, ts, map, nowMs, scan)) redactedSids.add(sid);
|
|
4662
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4663
|
+
castInput(value, options) {
|
|
4664
|
+
return value;
|
|
4575
4665
|
}
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
init_push_gitleaks_scan();
|
|
4580
|
-
init_push_gitleaks();
|
|
4581
|
-
init_utils();
|
|
4582
|
-
function isTTY(stdin = process.stdin, stdout = process.stdout) {
|
|
4583
|
-
return stdin.isTTY === true && stdout.isTTY === true;
|
|
4584
|
-
}
|
|
4585
|
-
function hasUnresolved(actions) {
|
|
4586
|
-
for (const action of actions.values()) {
|
|
4587
|
-
if (action === "skip") return true;
|
|
4666
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
4667
|
+
tokenize(value, options) {
|
|
4668
|
+
return Array.from(value);
|
|
4588
4669
|
}
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
function printRecoveryLegend(print = console.log) {
|
|
4592
|
-
print("");
|
|
4593
|
-
print("Recovery actions:");
|
|
4594
|
-
print(" Redact - scrub the secret from the local transcript, push the cleaned copy");
|
|
4595
|
-
print(" Allow - mark as false positive (adds a .gitleaksignore fingerprint), push as-is");
|
|
4596
|
-
print(" Drop session - exclude this session from this push (local transcript kept, running");
|
|
4597
|
-
print(" session is not stopped)");
|
|
4598
|
-
print(" Skip - leave unresolved (the push aborts)");
|
|
4599
|
-
print("");
|
|
4600
|
-
}
|
|
4601
|
-
function applyThenRescan(scanVerdict, repoHome2) {
|
|
4602
|
-
gitOrFatal(["add", "-A"], "git add", repoHome2);
|
|
4603
|
-
const next = scanVerdict(repoHome2);
|
|
4604
|
-
if (next.leak) {
|
|
4605
|
-
const { bySession, other } = partitionFindings(next.findings);
|
|
4606
|
-
throw new NomadFatal(buildSessionAwareFatal(bySession, other));
|
|
4670
|
+
join(chars) {
|
|
4671
|
+
return chars.join("");
|
|
4607
4672
|
}
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
function allowThenRescan(append, scanVerdict, repoHome2) {
|
|
4611
|
-
const ignPath = join40(repoHome2, ".gitleaksignore");
|
|
4612
|
-
let before;
|
|
4613
|
-
try {
|
|
4614
|
-
before = readFileSync12(ignPath, "utf8");
|
|
4615
|
-
} catch {
|
|
4616
|
-
before = null;
|
|
4673
|
+
postProcess(changeObjects, options) {
|
|
4674
|
+
return changeObjects;
|
|
4617
4675
|
}
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
return applyThenRescan(scanVerdict, repoHome2);
|
|
4621
|
-
} catch (err) {
|
|
4622
|
-
if (before === null) rmSync11(ignPath, { force: true });
|
|
4623
|
-
else writeFileSync5(ignPath, before, "utf8");
|
|
4624
|
-
throw err;
|
|
4676
|
+
get useLongestToken() {
|
|
4677
|
+
return false;
|
|
4625
4678
|
}
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4679
|
+
buildValues(lastComponent, newTokens, oldTokens) {
|
|
4680
|
+
const components = [];
|
|
4681
|
+
let nextComponent;
|
|
4682
|
+
while (lastComponent) {
|
|
4683
|
+
components.push(lastComponent);
|
|
4684
|
+
nextComponent = lastComponent.previousComponent;
|
|
4685
|
+
delete lastComponent.previousComponent;
|
|
4686
|
+
lastComponent = nextComponent;
|
|
4687
|
+
}
|
|
4688
|
+
components.reverse();
|
|
4689
|
+
const componentLen = components.length;
|
|
4690
|
+
let componentPos = 0, newPos = 0, oldPos = 0;
|
|
4691
|
+
for (; componentPos < componentLen; componentPos++) {
|
|
4692
|
+
const component = components[componentPos];
|
|
4693
|
+
if (!component.removed) {
|
|
4694
|
+
if (!component.added && this.useLongestToken) {
|
|
4695
|
+
let value = newTokens.slice(newPos, newPos + component.count);
|
|
4696
|
+
value = value.map(function(value2, i) {
|
|
4697
|
+
const oldValue = oldTokens[oldPos + i];
|
|
4698
|
+
return oldValue.length > value2.length ? oldValue : value2;
|
|
4699
|
+
});
|
|
4700
|
+
component.value = this.join(value);
|
|
4701
|
+
} else {
|
|
4702
|
+
component.value = this.join(newTokens.slice(newPos, newPos + component.count));
|
|
4703
|
+
}
|
|
4704
|
+
newPos += component.count;
|
|
4705
|
+
if (!component.added) {
|
|
4706
|
+
oldPos += component.count;
|
|
4707
|
+
}
|
|
4708
|
+
} else {
|
|
4709
|
+
component.value = this.join(oldTokens.slice(oldPos, oldPos + component.count));
|
|
4710
|
+
oldPos += component.count;
|
|
4711
|
+
}
|
|
4638
4712
|
}
|
|
4639
|
-
|
|
4640
|
-
}
|
|
4641
|
-
async function resolveLeakFindings(verdict, ts, map, deps = {}) {
|
|
4642
|
-
const {
|
|
4643
|
-
isTTYCheck = isTTY,
|
|
4644
|
-
nowMs = Date.now,
|
|
4645
|
-
redactAll = false,
|
|
4646
|
-
allowAll = false,
|
|
4647
|
-
allowRule,
|
|
4648
|
-
makePrompt: makePromptFn = makeRealPrompt,
|
|
4649
|
-
scan = scanFile,
|
|
4650
|
-
printLegend = printRecoveryLegend
|
|
4651
|
-
} = deps;
|
|
4652
|
-
const scanVerdict = deps.scanVerdict ?? (await Promise.resolve().then(() => (init_push_leak_verdict(), push_leak_verdict_exports))).scanPushVerdict;
|
|
4653
|
-
const repo = repoHome();
|
|
4654
|
-
let current = verdict;
|
|
4655
|
-
if (redactAll) {
|
|
4656
|
-
redactAllFindings(current.findings, ts, map, nowMs, scan);
|
|
4657
|
-
return applyThenRescan(scanVerdict, repo);
|
|
4713
|
+
return components;
|
|
4658
4714
|
}
|
|
4659
|
-
|
|
4660
|
-
|
|
4715
|
+
};
|
|
4716
|
+
|
|
4717
|
+
// node_modules/diff/libesm/diff/line.js
|
|
4718
|
+
var LineDiff = class extends Diff {
|
|
4719
|
+
constructor() {
|
|
4720
|
+
super(...arguments);
|
|
4721
|
+
this.tokenize = tokenize;
|
|
4661
4722
|
}
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
()
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
)
|
|
4723
|
+
equals(left, right, options) {
|
|
4724
|
+
if (options.ignoreWhitespace) {
|
|
4725
|
+
if (!options.newlineIsToken || !left.includes("\n")) {
|
|
4726
|
+
left = left.trim();
|
|
4727
|
+
}
|
|
4728
|
+
if (!options.newlineIsToken || !right.includes("\n")) {
|
|
4729
|
+
right = right.trim();
|
|
4730
|
+
}
|
|
4731
|
+
} else if (options.ignoreNewlineAtEof && !options.newlineIsToken) {
|
|
4732
|
+
if (left.endsWith("\n")) {
|
|
4733
|
+
left = left.slice(0, -1);
|
|
4734
|
+
}
|
|
4735
|
+
if (right.endsWith("\n")) {
|
|
4736
|
+
right = right.slice(0, -1);
|
|
4737
|
+
}
|
|
4738
|
+
}
|
|
4739
|
+
return super.equals(left, right, options);
|
|
4671
4740
|
}
|
|
4672
|
-
|
|
4673
|
-
|
|
4741
|
+
};
|
|
4742
|
+
var lineDiff = new LineDiff();
|
|
4743
|
+
function diffLines(oldStr, newStr, options) {
|
|
4744
|
+
return lineDiff.diff(oldStr, newStr, options);
|
|
4745
|
+
}
|
|
4746
|
+
function tokenize(value, options) {
|
|
4747
|
+
if (options.stripTrailingCr) {
|
|
4748
|
+
value = value.replace(/\r\n/g, "\n");
|
|
4674
4749
|
}
|
|
4675
|
-
const
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4750
|
+
const retLines = [], linesAndNewlines = value.split(/(\n|\r\n)/);
|
|
4751
|
+
if (!linesAndNewlines[linesAndNewlines.length - 1]) {
|
|
4752
|
+
linesAndNewlines.pop();
|
|
4753
|
+
}
|
|
4754
|
+
for (let i = 0; i < linesAndNewlines.length; i++) {
|
|
4755
|
+
const line = linesAndNewlines[i];
|
|
4756
|
+
if (i % 2 && !options.newlineIsToken) {
|
|
4757
|
+
retLines[retLines.length - 1] += line;
|
|
4758
|
+
} else {
|
|
4759
|
+
retLines.push(line);
|
|
4683
4760
|
}
|
|
4684
|
-
dispatchActions(current.findings, actions, { ts, map, nowMs, repo, scan });
|
|
4685
|
-
gitOrFatal(["add", "-A"], "git add", repo);
|
|
4686
|
-
current = scanVerdict(repo);
|
|
4687
4761
|
}
|
|
4688
|
-
return
|
|
4762
|
+
return retLines;
|
|
4689
4763
|
}
|
|
4690
4764
|
|
|
4691
|
-
// src/
|
|
4692
|
-
|
|
4693
|
-
|
|
4765
|
+
// src/diff-lines.ts
|
|
4766
|
+
init_color();
|
|
4767
|
+
function diffLinesToUnified(oldStr, newStr) {
|
|
4768
|
+
const parts = diffLines(oldStr, newStr);
|
|
4769
|
+
const lines = [];
|
|
4770
|
+
for (const part of parts) {
|
|
4771
|
+
const partLines = part.value.split("\n");
|
|
4772
|
+
if (partLines.at(-1) === "") {
|
|
4773
|
+
partLines.pop();
|
|
4774
|
+
}
|
|
4775
|
+
let prefix;
|
|
4776
|
+
if (part.removed) prefix = (line) => red(`-${line}`);
|
|
4777
|
+
else if (part.added) prefix = (line) => green(`+${line}`);
|
|
4778
|
+
else prefix = (line) => ` ${line}`;
|
|
4779
|
+
for (const line of partLines) {
|
|
4780
|
+
lines.push(prefix(line));
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
return lines;
|
|
4694
4784
|
}
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4785
|
+
|
|
4786
|
+
// src/preview.ts
|
|
4787
|
+
init_utils_json();
|
|
4788
|
+
var CANONICAL_ORDER_NOTE = "settings.json will be rewritten in canonical key order; no value changes";
|
|
4789
|
+
function diffJsonStrings(currentJsonText, newJsonText) {
|
|
4790
|
+
if (currentJsonText === newJsonText) return "";
|
|
4791
|
+
const lines = [
|
|
4792
|
+
"--- ~/.claude/settings.json",
|
|
4793
|
+
"+++ would write",
|
|
4794
|
+
...diffLinesToUnified(currentJsonText, newJsonText)
|
|
4795
|
+
];
|
|
4796
|
+
return lines.join("\n");
|
|
4698
4797
|
}
|
|
4699
|
-
function
|
|
4700
|
-
|
|
4701
|
-
|
|
4798
|
+
function readJsonOrNull(path) {
|
|
4799
|
+
if (!existsSync34(path)) return null;
|
|
4800
|
+
try {
|
|
4801
|
+
return readJson(path);
|
|
4802
|
+
} catch {
|
|
4803
|
+
return null;
|
|
4804
|
+
}
|
|
4702
4805
|
}
|
|
4703
|
-
function
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4806
|
+
function previewSettings(basePath, hostPath, settingsPath) {
|
|
4807
|
+
const base = readJsonOrNull(basePath);
|
|
4808
|
+
if (base === null) {
|
|
4809
|
+
return { diff: "", notes: ["section skipped (base or current missing)"] };
|
|
4810
|
+
}
|
|
4811
|
+
const notes = [];
|
|
4812
|
+
const hostOverrides = readJsonOrNull(hostPath);
|
|
4813
|
+
if (hostOverrides === null && existsSync34(hostPath)) {
|
|
4814
|
+
notes.push(`malformed hosts/${HOST}.json; ignoring overrides`);
|
|
4815
|
+
}
|
|
4816
|
+
const merged = deepMerge(base, hostOverrides ?? {});
|
|
4817
|
+
const current = readJsonOrNull(settingsPath);
|
|
4818
|
+
if (current === null && existsSync34(settingsPath)) {
|
|
4819
|
+
return { diff: "", notes: [...notes, "malformed; skipping diff"] };
|
|
4820
|
+
}
|
|
4821
|
+
const rawEqual = JSON.stringify(current ?? {}, null, 2) === JSON.stringify(merged, null, 2);
|
|
4822
|
+
const diff = diffJsonStrings(
|
|
4823
|
+
JSON.stringify(sortKeysDeep(current ?? {}), null, 2),
|
|
4824
|
+
JSON.stringify(sortKeysDeep(merged), null, 2)
|
|
4825
|
+
);
|
|
4826
|
+
if (diff === "" && !rawEqual) notes.push(CANONICAL_ORDER_NOTE);
|
|
4827
|
+
return { diff, notes };
|
|
4708
4828
|
}
|
|
4709
|
-
function
|
|
4710
|
-
|
|
4711
|
-
const base = deps.baseUrl ?? import.meta.url;
|
|
4712
|
-
const mjs = fileURLToPath4(new URL("./nomad.worker.mjs", base));
|
|
4713
|
-
if (check(mjs)) return mjs;
|
|
4714
|
-
return fileURLToPath4(new URL("./spinner.worker.ts", base));
|
|
4829
|
+
function formatLinkRow(e) {
|
|
4830
|
+
return `${e.kind} ${e.from} -> ${e.to}`;
|
|
4715
4831
|
}
|
|
4716
|
-
function
|
|
4717
|
-
return
|
|
4832
|
+
function formatSessionRow(e) {
|
|
4833
|
+
return e.kind === "overwrite" ? `overwrite ${e.dst} (from ${e.src})` : e.text;
|
|
4718
4834
|
}
|
|
4719
|
-
function
|
|
4720
|
-
const
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
const startMs = now();
|
|
4725
|
-
const animate = ttyCheck() && !env.CI;
|
|
4726
|
-
let worker = null;
|
|
4727
|
-
let degraded = false;
|
|
4728
|
-
let finalized = false;
|
|
4729
|
-
if (animate) {
|
|
4730
|
-
const factory = deps.makeWorker ?? makeRealWorker;
|
|
4731
|
-
try {
|
|
4732
|
-
worker = factory();
|
|
4733
|
-
worker.unref?.();
|
|
4734
|
-
worker.postMessage({ type: "start", label });
|
|
4735
|
-
} catch {
|
|
4736
|
-
degraded = true;
|
|
4737
|
-
worker = null;
|
|
4738
|
-
writePlainStart(out, label);
|
|
4835
|
+
function buildSettingsSectionForPreview(result) {
|
|
4836
|
+
const s = section("settings.json", true);
|
|
4837
|
+
if (result.diff !== "") {
|
|
4838
|
+
for (const line of result.diff.split("\n")) {
|
|
4839
|
+
addItem(s, line);
|
|
4739
4840
|
}
|
|
4740
|
-
} else {
|
|
4741
|
-
writePlainStart(out, label);
|
|
4742
4841
|
}
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
finalized = true;
|
|
4746
|
-
const dl = doneLabel ?? label;
|
|
4747
|
-
const elapsed = now() - startMs;
|
|
4748
|
-
if (animate && !degraded && worker !== null) {
|
|
4749
|
-
worker.postMessage({ type: "pause" });
|
|
4750
|
-
worker.terminate();
|
|
4751
|
-
worker = null;
|
|
4752
|
-
if (success) writeAnimatedDone(out, dl, elapsed, ttyCheck());
|
|
4753
|
-
else out.write("\r\x1B[K");
|
|
4754
|
-
} else if (success) {
|
|
4755
|
-
writePlainDone(out, dl, elapsed);
|
|
4756
|
-
}
|
|
4842
|
+
for (const note of result.notes) {
|
|
4843
|
+
addItem(s, `note: ${note}`);
|
|
4757
4844
|
}
|
|
4758
|
-
return
|
|
4759
|
-
succeed: (doneLabel) => finalize(true, doneLabel),
|
|
4760
|
-
stop: () => finalize(false)
|
|
4761
|
-
};
|
|
4845
|
+
return s;
|
|
4762
4846
|
}
|
|
4763
|
-
function
|
|
4764
|
-
const
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
4771
|
-
|
|
4847
|
+
function computePreview(ts, map, verb = "pull") {
|
|
4848
|
+
const repo = repoHome();
|
|
4849
|
+
const claude = claudeHome();
|
|
4850
|
+
console.log(`would pull on host=${HOST} (dry-run; no mutation)`);
|
|
4851
|
+
console.log("");
|
|
4852
|
+
const links = section("Symlinks");
|
|
4853
|
+
applySharedLinks(ts, map, {
|
|
4854
|
+
dryRun: true,
|
|
4855
|
+
onPreview: (e) => addItem(links, formatLinkRow(e))
|
|
4856
|
+
});
|
|
4857
|
+
const settingsResult = previewSettings(
|
|
4858
|
+
join40(repo, "shared", "settings.base.json"),
|
|
4859
|
+
join40(repo, "hosts", `${HOST}.json`),
|
|
4860
|
+
join40(claude, "settings.json")
|
|
4861
|
+
);
|
|
4862
|
+
const settingsSection = buildSettingsSectionForPreview(settingsResult);
|
|
4863
|
+
const sessions = section("Sessions");
|
|
4864
|
+
const remapResult = remapPull(ts, {
|
|
4865
|
+
dryRun: true,
|
|
4866
|
+
onPreview: (e) => addItem(sessions, formatSessionRow(e))
|
|
4867
|
+
});
|
|
4868
|
+
const summary = section("Summary");
|
|
4869
|
+
addItem(summary, summaryRow(verb, remapResult.unmapped));
|
|
4870
|
+
renderTree([links, settingsSection, sessions, summary]);
|
|
4871
|
+
return { unmapped: remapResult.unmapped, collisions: 0 };
|
|
4772
4872
|
}
|
|
4773
4873
|
|
|
4774
4874
|
// src/commands.pull.recovery.ts
|
|
@@ -4977,9 +5077,15 @@ function isAllowed(path, allowed) {
|
|
|
4977
5077
|
}
|
|
4978
5078
|
return false;
|
|
4979
5079
|
}
|
|
5080
|
+
function blockSetFor(segments) {
|
|
5081
|
+
if (segments[0] !== "shared" || segments[1] !== "extras") return NEVER_SYNC;
|
|
5082
|
+
return segments[3] === ".claude" ? CLAUDE_EXTRA_NEVER_SYNC : ALWAYS_NEVER_SYNC;
|
|
5083
|
+
}
|
|
4980
5084
|
function isNeverSync(path) {
|
|
4981
|
-
const
|
|
4982
|
-
|
|
5085
|
+
const segments = path.split("/");
|
|
5086
|
+
const blockSet = blockSetFor(segments);
|
|
5087
|
+
const scan = segments[0] === "shared" && segments[1] === "extras" ? segments.slice(4) : segments;
|
|
5088
|
+
for (const segment of scan) {
|
|
4983
5089
|
if (blockSet.has(segment)) return true;
|
|
4984
5090
|
}
|
|
4985
5091
|
return false;
|
|
@@ -5724,7 +5830,7 @@ function parsePushArgs(argv) {
|
|
|
5724
5830
|
// package.json
|
|
5725
5831
|
var package_default = {
|
|
5726
5832
|
name: "claude-nomad",
|
|
5727
|
-
version: "0.
|
|
5833
|
+
version: "0.47.0",
|
|
5728
5834
|
type: "module",
|
|
5729
5835
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
5730
5836
|
keywords: [
|
|
@@ -5747,7 +5853,6 @@ var package_default = {
|
|
|
5747
5853
|
},
|
|
5748
5854
|
files: [
|
|
5749
5855
|
"dist/",
|
|
5750
|
-
"shared/.gitignore",
|
|
5751
5856
|
".gitleaks.toml",
|
|
5752
5857
|
"README.md",
|
|
5753
5858
|
"CHANGELOG.md",
|
|
@@ -5926,7 +6031,7 @@ var DEFAULT_HELP = [
|
|
|
5926
6031
|
init_config();
|
|
5927
6032
|
init_utils();
|
|
5928
6033
|
init_utils_json();
|
|
5929
|
-
import { existsSync as existsSync41, readFileSync as
|
|
6034
|
+
import { existsSync as existsSync41, readFileSync as readFileSync14, readdirSync as readdirSync12 } from "node:fs";
|
|
5930
6035
|
import { join as join47 } from "node:path";
|
|
5931
6036
|
function resumeCmd(sessionId) {
|
|
5932
6037
|
if (!/^[A-Za-z0-9_-]+$/.test(sessionId) || sessionId.length > 128) {
|
|
@@ -5978,7 +6083,7 @@ function findTranscriptPath(projectsRoot, sessionId) {
|
|
|
5978
6083
|
return null;
|
|
5979
6084
|
}
|
|
5980
6085
|
function extractRecordedCwd(jsonlPath) {
|
|
5981
|
-
for (const line of
|
|
6086
|
+
for (const line of readFileSync14(jsonlPath, "utf8").split("\n")) {
|
|
5982
6087
|
if (!line.trim()) continue;
|
|
5983
6088
|
try {
|
|
5984
6089
|
const obj = JSON.parse(line);
|