claude-nomad 0.62.6 → 0.63.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 +41 -0
- package/README.md +24 -9
- package/dist/nomad.mjs +721 -459
- package/package.json +1 -1
package/dist/nomad.mjs
CHANGED
|
@@ -1082,12 +1082,40 @@ import { execFileSync as execFileSync8 } from "node:child_process";
|
|
|
1082
1082
|
import { mkdirSync as mkdirSync3, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, rmSync as rmSync8 } from "node:fs";
|
|
1083
1083
|
import { homedir as homedir3 } from "node:os";
|
|
1084
1084
|
import { join as join23 } from "node:path";
|
|
1085
|
+
function toFinding(entry) {
|
|
1086
|
+
if (typeof entry !== "object" || entry === null) return null;
|
|
1087
|
+
const e = entry;
|
|
1088
|
+
const str = (v) => typeof v === "string" ? v : "";
|
|
1089
|
+
const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : 0;
|
|
1090
|
+
const file = str(e.File);
|
|
1091
|
+
const rule = str(e.RuleID);
|
|
1092
|
+
if (file === "" || rule === "") return null;
|
|
1093
|
+
return {
|
|
1094
|
+
RuleID: rule,
|
|
1095
|
+
File: file,
|
|
1096
|
+
StartLine: num(e.StartLine),
|
|
1097
|
+
EndLine: Number.isFinite(e.EndLine) ? e.EndLine : void 0,
|
|
1098
|
+
StartColumn: num(e.StartColumn),
|
|
1099
|
+
EndColumn: num(e.EndColumn),
|
|
1100
|
+
Match: str(e.Match),
|
|
1101
|
+
Fingerprint: str(e.Fingerprint),
|
|
1102
|
+
Description: typeof e.Description === "string" ? e.Description : void 0,
|
|
1103
|
+
Secret: typeof e.Secret === "string" ? e.Secret : void 0,
|
|
1104
|
+
Entropy: Number.isFinite(e.Entropy) ? e.Entropy : void 0
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1085
1107
|
function readGitleaksReport(reportPath) {
|
|
1086
1108
|
try {
|
|
1087
1109
|
const raw = readFileSync6(reportPath, "utf8");
|
|
1088
1110
|
const parsed = JSON.parse(raw);
|
|
1089
1111
|
if (!Array.isArray(parsed)) return null;
|
|
1090
|
-
|
|
1112
|
+
const findings = [];
|
|
1113
|
+
for (const entry of parsed) {
|
|
1114
|
+
const finding = toFinding(entry);
|
|
1115
|
+
if (finding === null) return null;
|
|
1116
|
+
findings.push(finding);
|
|
1117
|
+
}
|
|
1118
|
+
return findings;
|
|
1091
1119
|
} catch {
|
|
1092
1120
|
return null;
|
|
1093
1121
|
}
|
|
@@ -1796,6 +1824,10 @@ function* eachExtrasTarget(v, counts) {
|
|
|
1796
1824
|
}
|
|
1797
1825
|
}
|
|
1798
1826
|
function copyExtrasOverlayFiltered(src, dst, blockSet) {
|
|
1827
|
+
const dstStat = lstatSync2(dst, { throwIfNoEntry: false });
|
|
1828
|
+
if (dstStat !== void 0 && !dstStat.isDirectory()) {
|
|
1829
|
+
rmSync2(dst, { recursive: true, force: true });
|
|
1830
|
+
}
|
|
1799
1831
|
stripCollidingDstSymlinks(src, dst, (name) => isDeniedName(blockSet, name));
|
|
1800
1832
|
cpSyncGuarded(
|
|
1801
1833
|
src,
|
|
@@ -1961,16 +1993,32 @@ function applySharedLinks(ts, map, opts = {}) {
|
|
|
1961
1993
|
}
|
|
1962
1994
|
}
|
|
1963
1995
|
function syncSharedLinksPush(map) {
|
|
1996
|
+
mirrorSharedNames(map, { adoptNew: true, overlay: false });
|
|
1997
|
+
}
|
|
1998
|
+
function stageLocalSharedEdits(map, ts) {
|
|
1999
|
+
mirrorSharedNames(map, { adoptNew: false, overlay: true, backupTs: ts });
|
|
2000
|
+
}
|
|
2001
|
+
function mirrorOneSharedName(name, claude, repo, policy) {
|
|
2002
|
+
const localPath = join5(claude, name);
|
|
2003
|
+
const stat = lstatSync4(localPath, { throwIfNoEntry: false });
|
|
2004
|
+
if (stat === void 0) return;
|
|
2005
|
+
if (stat.isSymbolicLink()) return;
|
|
2006
|
+
const target = join5(repo, "shared", name);
|
|
2007
|
+
if (!policy.adoptNew && !existsSync4(target)) return;
|
|
2008
|
+
if (policy.backupTs !== void 0) backupRepoWrite(target, policy.backupTs, repo);
|
|
2009
|
+
if (policy.overlay && stat.isDirectory()) {
|
|
2010
|
+
copyExtrasOverlayFiltered(localPath, target, ALWAYS_NEVER_SYNC);
|
|
2011
|
+
} else {
|
|
2012
|
+
copyExtrasFiltered(localPath, target, ALWAYS_NEVER_SYNC);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
function mirrorSharedNames(map, policy) {
|
|
1964
2016
|
if (process.platform !== "win32") return;
|
|
1965
2017
|
if (map === null) return;
|
|
1966
2018
|
const claude = claudeHome();
|
|
1967
2019
|
const repo = repoHome();
|
|
1968
2020
|
for (const name of allSharedLinks(map)) {
|
|
1969
|
-
|
|
1970
|
-
const stat = lstatSync4(localPath, { throwIfNoEntry: false });
|
|
1971
|
-
if (stat === void 0) continue;
|
|
1972
|
-
if (stat.isSymbolicLink()) continue;
|
|
1973
|
-
copyExtrasFiltered(localPath, join5(repo, "shared", name), ALWAYS_NEVER_SYNC);
|
|
2021
|
+
mirrorOneSharedName(name, claude, repo, policy);
|
|
1974
2022
|
}
|
|
1975
2023
|
}
|
|
1976
2024
|
function readExistingSettings(settingsPath) {
|
|
@@ -4883,8 +4931,10 @@ function reportLongPathsCheck(section2, run = execFileSync12) {
|
|
|
4883
4931
|
"enable LongPathsEnabled in Local Group Policy or the registry (admin required)"
|
|
4884
4932
|
);
|
|
4885
4933
|
}
|
|
4934
|
+
var MODALITY_COPY_SYNC = "copy-sync (native Windows; local edits reach the repo on the next pull or push)";
|
|
4935
|
+
var MODALITY_SYMLINK = "symlink (posix)";
|
|
4886
4936
|
function reportSyncModality(section2) {
|
|
4887
|
-
const modality = process.platform === "win32" ?
|
|
4937
|
+
const modality = process.platform === "win32" ? MODALITY_COPY_SYNC : MODALITY_SYMLINK;
|
|
4888
4938
|
addItem(section2, `${dim(infoGlyph)} sync modality: ${modality}`);
|
|
4889
4939
|
}
|
|
4890
4940
|
|
|
@@ -4955,248 +5005,19 @@ init_config();
|
|
|
4955
5005
|
import { readFileSync as readFileSync17 } from "node:fs";
|
|
4956
5006
|
import { isAbsolute as isAbsolute2, resolve as resolve3, sep as sep8 } from "node:path";
|
|
4957
5007
|
|
|
4958
|
-
// src/commands.push.recovery.redact.ts
|
|
4959
|
-
init_config();
|
|
4960
|
-
init_config_sharedDirs_guard();
|
|
4961
|
-
import { cpSync as cpSync8, existsSync as existsSync31, mkdirSync as mkdirSync10 } from "node:fs";
|
|
4962
|
-
import { dirname as dirname11, join as join39, sep as sep6 } from "node:path";
|
|
4963
|
-
|
|
4964
|
-
// src/commands.redact.ts
|
|
4965
|
-
init_config();
|
|
4966
|
-
import { existsSync as existsSync30 } from "node:fs";
|
|
4967
|
-
import { dirname as dirname9, join as join37 } from "node:path";
|
|
4968
|
-
|
|
4969
|
-
// src/commands.redact.subtree.ts
|
|
4970
|
-
import { existsSync as existsSync29, lstatSync as lstatSync11, readFileSync as readFileSync14, readdirSync as readdirSync13, statSync as statSync6, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4971
|
-
import { join as join36 } from "node:path";
|
|
4972
|
-
init_utils_fs();
|
|
4973
|
-
init_utils();
|
|
4974
|
-
function collectFiles(dir, out) {
|
|
4975
|
-
if (!existsSync29(dir)) return;
|
|
4976
|
-
const st = lstatSync11(dir);
|
|
4977
|
-
if (!st.isDirectory()) return;
|
|
4978
|
-
for (const entry of readdirSync13(dir)) {
|
|
4979
|
-
const abs = join36(dir, entry);
|
|
4980
|
-
const lst = lstatSync11(abs);
|
|
4981
|
-
if (lst.isSymbolicLink()) continue;
|
|
4982
|
-
if (lst.isDirectory()) {
|
|
4983
|
-
collectFiles(abs, out);
|
|
4984
|
-
continue;
|
|
4985
|
-
}
|
|
4986
|
-
if (lst.isFile()) out.push(abs);
|
|
4987
|
-
}
|
|
4988
|
-
}
|
|
4989
|
-
function listSubtreeFiles(sessionDir) {
|
|
4990
|
-
const out = [];
|
|
4991
|
-
collectFiles(sessionDir, out);
|
|
4992
|
-
return out.sort((a, b) => a.localeCompare(b));
|
|
4993
|
-
}
|
|
4994
|
-
function newestSubtreeMtimeMs(mainPath, subtreeFiles, statMtime = (p) => statSync6(p).mtimeMs) {
|
|
4995
|
-
let newest = statMtime(mainPath);
|
|
4996
|
-
for (const filePath of subtreeFiles) {
|
|
4997
|
-
const t = statMtime(filePath);
|
|
4998
|
-
if (t > newest) newest = t;
|
|
4999
|
-
}
|
|
5000
|
-
return newest;
|
|
5001
|
-
}
|
|
5002
|
-
function isSubtreeActive(mainPath, subtreeFiles, now) {
|
|
5003
|
-
const subtreeMtime = newestSubtreeMtimeMs(mainPath, subtreeFiles, (p) => statSync6(p).mtimeMs);
|
|
5004
|
-
return isRecentlyModified(subtreeMtime, now);
|
|
5005
|
-
}
|
|
5006
|
-
function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts, scan, dryRun) {
|
|
5007
|
-
const dirty = [];
|
|
5008
|
-
if (mainFindings.length > 0) dirty.push({ path: mainPath, findings: mainFindings });
|
|
5009
|
-
for (const filePath of subtreeFiles) {
|
|
5010
|
-
const raw = scan(filePath);
|
|
5011
|
-
if (raw === null || raw.length === 0) continue;
|
|
5012
|
-
const filtered = rule === void 0 ? raw : raw.filter((f) => f.RuleID === rule);
|
|
5013
|
-
if (filtered.length === 0) continue;
|
|
5014
|
-
dirty.push({ path: filePath, findings: filtered });
|
|
5015
|
-
}
|
|
5016
|
-
const total = dirty.reduce((n, e) => n + e.findings.length, 0);
|
|
5017
|
-
if (!dryRun && total > 0) {
|
|
5018
|
-
for (const { path: filePath, findings } of dirty) {
|
|
5019
|
-
backupBeforeWrite(filePath, ts);
|
|
5020
|
-
const before = readFileSync14(filePath, "utf8");
|
|
5021
|
-
const after = applyRedactions(before, findings);
|
|
5022
|
-
if (after === before) {
|
|
5023
|
-
log(
|
|
5024
|
-
`warning: no redaction applied to ${filePath}: finding match values were not located in the file. Inspect it manually; the push re-scan still blocks a real leak.`
|
|
5025
|
-
);
|
|
5026
|
-
}
|
|
5027
|
-
writeFileSync6(filePath, after, "utf8");
|
|
5028
|
-
}
|
|
5029
|
-
}
|
|
5030
|
-
return { total, dirty };
|
|
5031
|
-
}
|
|
5032
|
-
|
|
5033
|
-
// src/commands.pushed-history.ts
|
|
5034
|
-
init_utils();
|
|
5035
|
-
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
5036
|
-
function pushedRef(repo) {
|
|
5037
|
-
try {
|
|
5038
|
-
const ref = execFileSync14("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
|
|
5039
|
-
cwd: repo,
|
|
5040
|
-
stdio: ["ignore", "pipe", "ignore"]
|
|
5041
|
-
}).toString().trim();
|
|
5042
|
-
return ref.length > 0 ? ref : null;
|
|
5043
|
-
} catch {
|
|
5044
|
-
return null;
|
|
5045
|
-
}
|
|
5046
|
-
}
|
|
5047
|
-
function sessionInPushedHistory(id, repo) {
|
|
5048
|
-
const ref = pushedRef(repo);
|
|
5049
|
-
if (ref === null) return false;
|
|
5050
|
-
try {
|
|
5051
|
-
const out = execFileSync14(
|
|
5052
|
-
"git",
|
|
5053
|
-
[
|
|
5054
|
-
"log",
|
|
5055
|
-
ref,
|
|
5056
|
-
"--oneline",
|
|
5057
|
-
"-1",
|
|
5058
|
-
"--",
|
|
5059
|
-
`shared/projects/*/${id}.jsonl`,
|
|
5060
|
-
`shared/projects/*/${id}/*`
|
|
5061
|
-
],
|
|
5062
|
-
{ cwd: repo, stdio: ["ignore", "pipe", "ignore"] }
|
|
5063
|
-
).toString().trim();
|
|
5064
|
-
return out.length > 0;
|
|
5065
|
-
} catch {
|
|
5066
|
-
return false;
|
|
5067
|
-
}
|
|
5068
|
-
}
|
|
5069
|
-
function warnIfSessionPushed(id, repo) {
|
|
5070
|
-
if (!sessionInPushedHistory(id, repo)) return;
|
|
5071
|
-
log(
|
|
5072
|
-
`warning: session ${id} is already in pushed history (origin).
|
|
5073
|
-
This command only changes your local copy and the next push; it does NOT
|
|
5074
|
-
remove the secret from commits already on the remote.
|
|
5075
|
-
To fully remediate a real secret: rotate the credential, then rewrite
|
|
5076
|
-
history (e.g. with git filter-repo) and force-push, coordinating with
|
|
5077
|
-
anyone else who has cloned the repo.`
|
|
5078
|
-
);
|
|
5079
|
-
}
|
|
5080
|
-
|
|
5081
|
-
// src/commands.redact.ts
|
|
5082
|
-
init_push_gitleaks_scan();
|
|
5083
|
-
init_exit_codes();
|
|
5084
|
-
init_utils_fs();
|
|
5085
|
-
init_utils_json();
|
|
5086
|
-
init_utils();
|
|
5087
|
-
function resolveLiveTranscript(id) {
|
|
5088
|
-
try {
|
|
5089
|
-
const mapPath = join37(repoHome(), "path-map.json");
|
|
5090
|
-
if (!existsSync30(mapPath)) return null;
|
|
5091
|
-
const projects = readJson(mapPath).projects;
|
|
5092
|
-
const claude = claudeHome();
|
|
5093
|
-
for (const hostMap of Object.values(projects)) {
|
|
5094
|
-
const abs = hostMap[HOST];
|
|
5095
|
-
if (abs === void 0) continue;
|
|
5096
|
-
const live = join37(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
5097
|
-
if (existsSync30(live)) return live;
|
|
5098
|
-
}
|
|
5099
|
-
return null;
|
|
5100
|
-
} catch {
|
|
5101
|
-
return null;
|
|
5102
|
-
}
|
|
5103
|
-
}
|
|
5104
|
-
function resolveRedactFindings(localPath, rawFindings, rule, scan) {
|
|
5105
|
-
const source = rawFindings ?? scan(localPath);
|
|
5106
|
-
if (source === null) return null;
|
|
5107
|
-
return source.filter((f) => rule === void 0 || f.RuleID === rule);
|
|
5108
|
-
}
|
|
5109
|
-
function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
|
|
5110
|
-
const { id, rule, dryRun = false, findings: rawFindings } = opts;
|
|
5111
|
-
if (id.length === 0 || id.length > 128 || !/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
5112
|
-
fail(`invalid session id: ${id}`);
|
|
5113
|
-
process.exit(EXIT.USAGE);
|
|
5114
|
-
}
|
|
5115
|
-
const repo = repoHome();
|
|
5116
|
-
const backup = backupBase();
|
|
5117
|
-
if (!existsSync30(repo)) die(`repo not cloned at ${repo}`);
|
|
5118
|
-
const handle = acquireLock("redact");
|
|
5119
|
-
if (handle === null) process.exit(0);
|
|
5120
|
-
try {
|
|
5121
|
-
const localPath = resolveLiveTranscript(id);
|
|
5122
|
-
if (localPath === null || !existsSync30(localPath)) {
|
|
5123
|
-
fail(`could not resolve local transcript for session ${id} on this host`);
|
|
5124
|
-
process.exitCode = 1;
|
|
5125
|
-
return;
|
|
5126
|
-
}
|
|
5127
|
-
const sessionDir = join37(dirname9(localPath), id);
|
|
5128
|
-
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
5129
|
-
if (isSubtreeActive(localPath, subtreeFiles, nowMs())) {
|
|
5130
|
-
log(
|
|
5131
|
-
`session ${id} was modified recently and may be active.
|
|
5132
|
-
Refusing to rewrite a potentially live transcript.
|
|
5133
|
-
To proceed: wait for the session to end, then re-run nomad redact.
|
|
5134
|
-
Or drop from the staged tree: nomad drop-session ${id}
|
|
5135
|
-
Or skip this finding during nomad push.`
|
|
5136
|
-
);
|
|
5137
|
-
return;
|
|
5138
|
-
}
|
|
5139
|
-
const mainFindings = resolveRedactFindings(localPath, rawFindings, rule, scan);
|
|
5140
|
-
if (mainFindings === null) {
|
|
5141
|
-
fail(`gitleaks scan failed for session ${id} (is gitleaks installed?)`);
|
|
5142
|
-
process.exitCode = 1;
|
|
5143
|
-
return;
|
|
5144
|
-
}
|
|
5145
|
-
const ts = freshBackupTs(backup);
|
|
5146
|
-
const { total: totalCount, dirty } = applySubtreeRedactions(
|
|
5147
|
-
localPath,
|
|
5148
|
-
mainFindings,
|
|
5149
|
-
subtreeFiles,
|
|
5150
|
-
rule,
|
|
5151
|
-
ts,
|
|
5152
|
-
scan,
|
|
5153
|
-
dryRun
|
|
5154
|
-
);
|
|
5155
|
-
if (totalCount === 0) {
|
|
5156
|
-
const ruleClause = rule === void 0 ? "" : ` for rule ${rule}`;
|
|
5157
|
-
log(`no findings${ruleClause} in session ${id}`);
|
|
5158
|
-
return;
|
|
5159
|
-
}
|
|
5160
|
-
if (dryRun) {
|
|
5161
|
-
const lines = dirty.flatMap((e) => e.findings.map((f) => ` ${e.path} line ${f.StartLine} [${f.RuleID}]`)).join("\n");
|
|
5162
|
-
log(`dry-run: would redact ${totalCount} finding(s) in session ${id}
|
|
5163
|
-
${lines}`);
|
|
5164
|
-
return;
|
|
5165
|
-
}
|
|
5166
|
-
log(`redacted ${totalCount} finding(s) in ${localPath} (backup: ${ts})`);
|
|
5167
|
-
warnIfSessionPushed(id, repo);
|
|
5168
|
-
} catch (err) {
|
|
5169
|
-
if (!(err instanceof NomadFatal)) {
|
|
5170
|
-
throw err;
|
|
5171
|
-
}
|
|
5172
|
-
fail(err.message);
|
|
5173
|
-
process.exitCode = 1;
|
|
5174
|
-
} finally {
|
|
5175
|
-
releaseLock(handle);
|
|
5176
|
-
}
|
|
5177
|
-
}
|
|
5178
|
-
|
|
5179
|
-
// src/commands.push.recovery.redact.ts
|
|
5180
|
-
init_push_gitleaks_scan();
|
|
5181
|
-
init_utils_json();
|
|
5182
|
-
init_utils();
|
|
5183
|
-
|
|
5184
|
-
// src/commands.push.recovery.seams.ts
|
|
5185
|
-
init_push_gitleaks();
|
|
5186
|
-
|
|
5187
5008
|
// src/commands.push.recovery.memory.ts
|
|
5188
5009
|
init_config();
|
|
5189
5010
|
init_config_sharedDirs_guard();
|
|
5190
5011
|
import {
|
|
5191
5012
|
cpSync as cpSync7,
|
|
5192
|
-
lstatSync as
|
|
5013
|
+
lstatSync as lstatSync11,
|
|
5193
5014
|
mkdirSync as mkdirSync9,
|
|
5194
|
-
readFileSync as
|
|
5015
|
+
readFileSync as readFileSync14,
|
|
5195
5016
|
realpathSync as realpathSync4,
|
|
5196
|
-
statSync as
|
|
5197
|
-
writeFileSync as
|
|
5017
|
+
statSync as statSync6,
|
|
5018
|
+
writeFileSync as writeFileSync6
|
|
5198
5019
|
} from "node:fs";
|
|
5199
|
-
import { dirname as
|
|
5020
|
+
import { dirname as dirname9, join as join36, sep as sep5 } from "node:path";
|
|
5200
5021
|
init_push_gitleaks_scan();
|
|
5201
5022
|
|
|
5202
5023
|
// src/rel-path-guard.ts
|
|
@@ -5225,15 +5046,15 @@ function resolveMemoryLocalPath(logical, relPath, map) {
|
|
|
5225
5046
|
if (!isSafeRelPath(relPath)) return null;
|
|
5226
5047
|
const abs = map.projects[logical]?.[HOST];
|
|
5227
5048
|
if (abs === void 0) return null;
|
|
5228
|
-
const memoryRoot =
|
|
5229
|
-
const localPath =
|
|
5049
|
+
const memoryRoot = join36(claudeHome(), "projects", encodePath(abs), "memory");
|
|
5050
|
+
const localPath = join36(memoryRoot, ...relPath.split("/"));
|
|
5230
5051
|
if (localPath !== memoryRoot && !localPath.startsWith(memoryRoot + sep5)) return null;
|
|
5231
5052
|
try {
|
|
5232
|
-
if (
|
|
5053
|
+
if (lstatSync11(localPath).isSymbolicLink()) return null;
|
|
5233
5054
|
const realLocal = realpathSync4(localPath);
|
|
5234
5055
|
const realRoot = realpathSync4(memoryRoot);
|
|
5235
5056
|
if (!realLocal.startsWith(realRoot + sep5)) return null;
|
|
5236
|
-
if (!
|
|
5057
|
+
if (!statSync6(realLocal).isFile()) return null;
|
|
5237
5058
|
} catch {
|
|
5238
5059
|
return null;
|
|
5239
5060
|
}
|
|
@@ -5272,30 +5093,181 @@ function applyMemoryRedact(f, ts, map, scan = scanFile) {
|
|
|
5272
5093
|
return refuse(`nothing to redact in ${logical}/memory/${relPath}; choose Skip.`);
|
|
5273
5094
|
}
|
|
5274
5095
|
backupBeforeWrite(localPath, ts);
|
|
5275
|
-
const before =
|
|
5096
|
+
const before = readFileSync14(localPath, "utf8");
|
|
5276
5097
|
const after = applyRedactions(before, findings);
|
|
5277
5098
|
if (after === before) {
|
|
5278
5099
|
warn(
|
|
5279
5100
|
`no redaction applied to ${logical}/memory/${relPath}: finding match values were not located in the file. Inspect it manually; the push re-scan still blocks a real leak.`
|
|
5280
5101
|
);
|
|
5281
5102
|
}
|
|
5282
|
-
|
|
5283
|
-
const dest =
|
|
5284
|
-
mkdirSync9(
|
|
5103
|
+
writeFileSync6(localPath, after, "utf8");
|
|
5104
|
+
const dest = join36(repoHome(), "shared", "projects", logical, "memory", ...relPath.split("/"));
|
|
5105
|
+
mkdirSync9(dirname9(dest), { recursive: true });
|
|
5285
5106
|
cpSync7(localPath, dest, { force: true });
|
|
5286
5107
|
return true;
|
|
5287
5108
|
}
|
|
5288
5109
|
|
|
5289
|
-
// src/commands.push.recovery.
|
|
5290
|
-
|
|
5291
|
-
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5110
|
+
// src/commands.push.recovery.skills.ts
|
|
5111
|
+
init_config();
|
|
5112
|
+
import {
|
|
5113
|
+
cpSync as cpSync8,
|
|
5114
|
+
lstatSync as lstatSync13,
|
|
5115
|
+
mkdirSync as mkdirSync11,
|
|
5116
|
+
readFileSync as readFileSync15,
|
|
5117
|
+
realpathSync as realpathSync5,
|
|
5118
|
+
statSync as statSync7,
|
|
5119
|
+
writeFileSync as writeFileSync7
|
|
5120
|
+
} from "node:fs";
|
|
5121
|
+
import { dirname as dirname10, join as join38, sep as sep6 } from "node:path";
|
|
5122
|
+
|
|
5123
|
+
// src/skills-sync.ts
|
|
5124
|
+
init_config();
|
|
5125
|
+
import { existsSync as existsSync29, lstatSync as lstatSync12, mkdirSync as mkdirSync10, readdirSync as readdirSync13, rmSync as rmSync13 } from "node:fs";
|
|
5126
|
+
import { join as join37 } from "node:path";
|
|
5127
|
+
|
|
5128
|
+
// src/skills-sync.tracked.ts
|
|
5129
|
+
init_utils();
|
|
5130
|
+
import { basename as basename3 } from "node:path";
|
|
5131
|
+
function trackedRootSkillsAt(ref, repo) {
|
|
5132
|
+
try {
|
|
5133
|
+
const raw = gitCaptureRaw(["ls-tree", "--name-only", "-z", ref, "--", "shared/skills/"], repo);
|
|
5134
|
+
const entries = raw.split("\0").filter((entry) => entry !== "");
|
|
5135
|
+
return new Set(entries.map((entry) => basename3(entry)));
|
|
5136
|
+
} catch {
|
|
5137
|
+
return /* @__PURE__ */ new Set();
|
|
5138
|
+
}
|
|
5296
5139
|
}
|
|
5297
|
-
|
|
5298
|
-
|
|
5140
|
+
|
|
5141
|
+
// src/skills-sync.ts
|
|
5142
|
+
init_utils_fs();
|
|
5143
|
+
function isGsdOwned(name) {
|
|
5144
|
+
return name.startsWith(GSD_PREFIX);
|
|
5145
|
+
}
|
|
5146
|
+
function isSkillExcluded(name) {
|
|
5147
|
+
return isGsdOwned(name) || isDeniedName(ALWAYS_NEVER_SYNC, name);
|
|
5148
|
+
}
|
|
5149
|
+
function copySkillsPush(src, dst) {
|
|
5150
|
+
const srcNames = readdirSync13(src, { encoding: "utf8" });
|
|
5151
|
+
const blockSet = /* @__PURE__ */ new Set([
|
|
5152
|
+
...srcNames.filter((n) => isGsdOwned(n)),
|
|
5153
|
+
...ALWAYS_NEVER_SYNC
|
|
5154
|
+
]);
|
|
5155
|
+
copyExtrasFiltered(src, dst, blockSet);
|
|
5156
|
+
}
|
|
5157
|
+
function copySkillsPull(src, dst, isRootPreserved) {
|
|
5158
|
+
copyExtrasFilteredPreservingBy(src, dst, isSkillExcluded, isRootPreserved);
|
|
5159
|
+
}
|
|
5160
|
+
function syncSkillsPull(ts, prePostHeads) {
|
|
5161
|
+
const sharedSkills = join37(repoHome(), "shared", "skills");
|
|
5162
|
+
if (!existsSync29(sharedSkills)) return;
|
|
5163
|
+
const localSkills = join37(claudeHome(), "skills");
|
|
5164
|
+
backupBeforeWrite(localSkills, ts);
|
|
5165
|
+
const dstStat = lstatSync12(localSkills, { throwIfNoEntry: false });
|
|
5166
|
+
if (dstStat?.isSymbolicLink() === true) {
|
|
5167
|
+
rmSync13(localSkills, { recursive: true, force: true });
|
|
5168
|
+
mkdirSync10(localSkills, { recursive: true });
|
|
5169
|
+
}
|
|
5170
|
+
const tracked = prePostHeads === void 0 ? /* @__PURE__ */ new Set() : trackedRootSkillsAt(prePostHeads.pre, repoHome());
|
|
5171
|
+
copySkillsPull(sharedSkills, localSkills, (name) => !tracked.has(name));
|
|
5172
|
+
}
|
|
5173
|
+
function syncSkillsPush() {
|
|
5174
|
+
const localSkills = join37(claudeHome(), "skills");
|
|
5175
|
+
const stat = lstatSync12(localSkills, { throwIfNoEntry: false });
|
|
5176
|
+
if (stat === void 0) return;
|
|
5177
|
+
if (stat.isSymbolicLink()) return;
|
|
5178
|
+
const sharedSkills = join37(repoHome(), "shared", "skills");
|
|
5179
|
+
copySkillsPush(localSkills, sharedSkills);
|
|
5180
|
+
}
|
|
5181
|
+
|
|
5182
|
+
// src/commands.push.recovery.skills.ts
|
|
5183
|
+
init_push_gitleaks_scan();
|
|
5184
|
+
init_utils_fs();
|
|
5185
|
+
init_utils();
|
|
5186
|
+
var SKILL_FINDING_PATH = /^shared\/skills\/([^/]+)\/(.+)$/;
|
|
5187
|
+
var SAFE_SKILL_NAME = /^[A-Za-z0-9._-]+$/;
|
|
5188
|
+
var SKILL_DIR_PATH = /^shared\/skills\/[^/]+\//;
|
|
5189
|
+
function skillFileFromFinding(f) {
|
|
5190
|
+
const m = SKILL_FINDING_PATH.exec(f.File);
|
|
5191
|
+
if (m?.[1] === void 0 || m[2] === void 0) return null;
|
|
5192
|
+
return { name: m[1], relPath: m[2] };
|
|
5193
|
+
}
|
|
5194
|
+
function isSkillFindingPath(f) {
|
|
5195
|
+
return SKILL_DIR_PATH.test(f.File);
|
|
5196
|
+
}
|
|
5197
|
+
function resolveSkillLocalPath(name, relPath) {
|
|
5198
|
+
if (!SAFE_SKILL_NAME.test(name) || name === "." || name === "..") return null;
|
|
5199
|
+
if (isGsdOwned(name)) return null;
|
|
5200
|
+
if (!isSafeRelPath(relPath)) return null;
|
|
5201
|
+
const skillsRoot = join38(claudeHome(), "skills");
|
|
5202
|
+
const skillRoot = join38(skillsRoot, name);
|
|
5203
|
+
const localPath = join38(skillRoot, ...relPath.split("/"));
|
|
5204
|
+
if (localPath !== skillRoot && !localPath.startsWith(skillRoot + sep6)) return null;
|
|
5205
|
+
try {
|
|
5206
|
+
if (lstatSync13(localPath).isSymbolicLink()) return null;
|
|
5207
|
+
const realLocal = realpathSync5(localPath);
|
|
5208
|
+
const realRoot = realpathSync5(skillsRoot);
|
|
5209
|
+
if (!realLocal.startsWith(realRoot + sep6)) return null;
|
|
5210
|
+
if (!statSync7(realLocal).isFile()) return null;
|
|
5211
|
+
} catch {
|
|
5212
|
+
return null;
|
|
5213
|
+
}
|
|
5214
|
+
return localPath;
|
|
5215
|
+
}
|
|
5216
|
+
function preflightSkillRedactable(f) {
|
|
5217
|
+
const parsed = skillFileFromFinding(f);
|
|
5218
|
+
if (parsed === null) return "a finding is not a skill file";
|
|
5219
|
+
const localPath = resolveSkillLocalPath(parsed.name, parsed.relPath);
|
|
5220
|
+
if (localPath === null) {
|
|
5221
|
+
return `skill file ${parsed.name}/${parsed.relPath}: local file not found or unresolvable`;
|
|
5222
|
+
}
|
|
5223
|
+
return null;
|
|
5224
|
+
}
|
|
5225
|
+
function applySkillRedact(f, ts, scan = scanFile) {
|
|
5226
|
+
const refuse = (msg) => {
|
|
5227
|
+
log(msg);
|
|
5228
|
+
return false;
|
|
5229
|
+
};
|
|
5230
|
+
const parsed = skillFileFromFinding(f);
|
|
5231
|
+
if (parsed === null) {
|
|
5232
|
+
return refuse("could not parse this finding as a skill file; choose Skip.");
|
|
5233
|
+
}
|
|
5234
|
+
const { name, relPath } = parsed;
|
|
5235
|
+
const localPath = resolveSkillLocalPath(name, relPath);
|
|
5236
|
+
if (localPath === null) {
|
|
5237
|
+
return refuse(`could not locate the local skill file for ${name}/${relPath}; choose Skip.`);
|
|
5238
|
+
}
|
|
5239
|
+
const findings = scan(localPath);
|
|
5240
|
+
if (findings === null) {
|
|
5241
|
+
return refuse(`re-scan of ${name}/${relPath} failed; choose Skip.`);
|
|
5242
|
+
}
|
|
5243
|
+
if (findings.length === 0) {
|
|
5244
|
+
return refuse(`nothing to redact in ${name}/${relPath}; choose Skip.`);
|
|
5245
|
+
}
|
|
5246
|
+
backupBeforeWrite(localPath, ts);
|
|
5247
|
+
const before = readFileSync15(localPath, "utf8");
|
|
5248
|
+
const after = applyRedactions(before, findings);
|
|
5249
|
+
if (after === before) {
|
|
5250
|
+
warn(
|
|
5251
|
+
`no redaction applied to ${name}/${relPath}: finding match values were not located in the file. Inspect it manually; the push re-scan still blocks a real leak.`
|
|
5252
|
+
);
|
|
5253
|
+
}
|
|
5254
|
+
writeFileSync7(localPath, after, "utf8");
|
|
5255
|
+
const dest = join38(repoHome(), "shared", "skills", name, ...relPath.split("/"));
|
|
5256
|
+
mkdirSync11(dirname10(dest), { recursive: true });
|
|
5257
|
+
cpSync8(localPath, dest, { force: true });
|
|
5258
|
+
return true;
|
|
5259
|
+
}
|
|
5260
|
+
|
|
5261
|
+
// src/commands.push.recovery.allow-gate.ts
|
|
5262
|
+
init_utils();
|
|
5263
|
+
|
|
5264
|
+
// src/commands.push.recovery.seams.ts
|
|
5265
|
+
init_push_gitleaks();
|
|
5266
|
+
function findingKey(f) {
|
|
5267
|
+
return `${f.File}:${f.StartLine}:${f.StartColumn}:${f.RuleID}`;
|
|
5268
|
+
}
|
|
5269
|
+
var VALID_SID = /^[A-Za-z0-9_-]+$/;
|
|
5270
|
+
var SUBTREE_PATH = /^shared\/projects\/[^/]+\/([^/]+)\/.+$/;
|
|
5299
5271
|
function sessionIdFromFinding(f) {
|
|
5300
5272
|
if (isMemoryFindingPath(f)) return null;
|
|
5301
5273
|
const m = SESSION_PATH.exec(f.File) ?? SUBTREE_PATH.exec(f.File);
|
|
@@ -5310,43 +5282,283 @@ function parseAction(raw) {
|
|
|
5310
5282
|
if (t === "d" || t === "drop") return "drop";
|
|
5311
5283
|
return "skip";
|
|
5312
5284
|
}
|
|
5313
|
-
|
|
5314
|
-
|
|
5285
|
+
|
|
5286
|
+
// src/commands.push.recovery.allow-gate.ts
|
|
5287
|
+
function isCleared(f, state) {
|
|
5288
|
+
const action = state.actions.get(findingKey(f)) ?? "skip";
|
|
5289
|
+
if (action === "allow") return true;
|
|
5290
|
+
const sid = sessionIdFromFinding(f);
|
|
5291
|
+
if (sid !== null) return state.droppedSids.has(sid) || state.redactedSids.has(sid);
|
|
5292
|
+
if (isMemoryFindingPath(f)) {
|
|
5293
|
+
const parsed = memoryFileFromFinding(f);
|
|
5294
|
+
return parsed !== null && state.redactedMemory.has(`${parsed.logical}/${parsed.relPath}`);
|
|
5295
|
+
}
|
|
5296
|
+
if (isSkillFindingPath(f)) {
|
|
5297
|
+
const parsed = skillFileFromFinding(f);
|
|
5298
|
+
return parsed !== null && state.redactedSkills.has(`${parsed.name}/${parsed.relPath}`);
|
|
5299
|
+
}
|
|
5300
|
+
return false;
|
|
5315
5301
|
}
|
|
5316
|
-
function
|
|
5317
|
-
const
|
|
5318
|
-
|
|
5319
|
-
|
|
5320
|
-
const
|
|
5321
|
-
|
|
5322
|
-
|
|
5323
|
-
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
const
|
|
5329
|
-
|
|
5330
|
-
|
|
5331
|
-
|
|
5332
|
-
|
|
5333
|
-
|
|
5334
|
-
if (
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5302
|
+
function byFingerprint(findings) {
|
|
5303
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5304
|
+
for (const f of findings) {
|
|
5305
|
+
if (f.Fingerprint.length === 0) continue;
|
|
5306
|
+
const existing = groups.get(f.Fingerprint);
|
|
5307
|
+
if (existing === void 0) groups.set(f.Fingerprint, [f]);
|
|
5308
|
+
else existing.push(f);
|
|
5309
|
+
}
|
|
5310
|
+
return groups;
|
|
5311
|
+
}
|
|
5312
|
+
function applyDeferredAllows(findings, state, repo) {
|
|
5313
|
+
for (const [fingerprint, peers] of byFingerprint(findings)) {
|
|
5314
|
+
const allows = peers.filter((p) => {
|
|
5315
|
+
if ((state.actions.get(findingKey(p)) ?? "skip") !== "allow") return false;
|
|
5316
|
+
const sid = sessionIdFromFinding(p);
|
|
5317
|
+
return sid === null || !state.droppedSids.has(sid);
|
|
5318
|
+
});
|
|
5319
|
+
if (allows.length === 0) continue;
|
|
5320
|
+
if (peers.every((p) => isCleared(p, state))) {
|
|
5321
|
+
appendGitleaksIgnore(fingerprint, repo);
|
|
5322
|
+
continue;
|
|
5323
|
+
}
|
|
5324
|
+
warn(
|
|
5325
|
+
`not allowing ${fingerprint}: another secret sharing that line was not cleaned, and the entry would suppress it too`
|
|
5326
|
+
);
|
|
5338
5327
|
}
|
|
5339
|
-
return null;
|
|
5340
5328
|
}
|
|
5341
5329
|
|
|
5342
5330
|
// src/commands.push.recovery.redact.ts
|
|
5331
|
+
init_config();
|
|
5332
|
+
init_config_sharedDirs_guard();
|
|
5333
|
+
import { cpSync as cpSync9, existsSync as existsSync32, mkdirSync as mkdirSync12 } from "node:fs";
|
|
5334
|
+
import { dirname as dirname12, join as join41, sep as sep7 } from "node:path";
|
|
5335
|
+
|
|
5336
|
+
// src/commands.redact.ts
|
|
5337
|
+
init_config();
|
|
5338
|
+
import { existsSync as existsSync31 } from "node:fs";
|
|
5339
|
+
import { dirname as dirname11, join as join40 } from "node:path";
|
|
5340
|
+
|
|
5341
|
+
// src/commands.redact.subtree.ts
|
|
5342
|
+
import { existsSync as existsSync30, lstatSync as lstatSync14, readFileSync as readFileSync16, readdirSync as readdirSync14, statSync as statSync8, writeFileSync as writeFileSync8 } from "node:fs";
|
|
5343
|
+
import { join as join39 } from "node:path";
|
|
5344
|
+
init_utils_fs();
|
|
5345
|
+
init_utils();
|
|
5346
|
+
function collectFiles(dir, out) {
|
|
5347
|
+
if (!existsSync30(dir)) return;
|
|
5348
|
+
const st = lstatSync14(dir);
|
|
5349
|
+
if (!st.isDirectory()) return;
|
|
5350
|
+
for (const entry of readdirSync14(dir)) {
|
|
5351
|
+
const abs = join39(dir, entry);
|
|
5352
|
+
const lst = lstatSync14(abs);
|
|
5353
|
+
if (lst.isSymbolicLink()) continue;
|
|
5354
|
+
if (lst.isDirectory()) {
|
|
5355
|
+
collectFiles(abs, out);
|
|
5356
|
+
continue;
|
|
5357
|
+
}
|
|
5358
|
+
if (lst.isFile()) out.push(abs);
|
|
5359
|
+
}
|
|
5360
|
+
}
|
|
5361
|
+
function listSubtreeFiles(sessionDir) {
|
|
5362
|
+
const out = [];
|
|
5363
|
+
collectFiles(sessionDir, out);
|
|
5364
|
+
return out.sort((a, b) => a.localeCompare(b));
|
|
5365
|
+
}
|
|
5366
|
+
function newestSubtreeMtimeMs(mainPath, subtreeFiles, statMtime = (p) => statSync8(p).mtimeMs) {
|
|
5367
|
+
let newest = statMtime(mainPath);
|
|
5368
|
+
for (const filePath of subtreeFiles) {
|
|
5369
|
+
const t = statMtime(filePath);
|
|
5370
|
+
if (t > newest) newest = t;
|
|
5371
|
+
}
|
|
5372
|
+
return newest;
|
|
5373
|
+
}
|
|
5374
|
+
function isSubtreeActive(mainPath, subtreeFiles, now) {
|
|
5375
|
+
const subtreeMtime = newestSubtreeMtimeMs(mainPath, subtreeFiles, (p) => statSync8(p).mtimeMs);
|
|
5376
|
+
return isRecentlyModified(subtreeMtime, now);
|
|
5377
|
+
}
|
|
5378
|
+
function applySubtreeRedactions(mainPath, mainFindings, subtreeFiles, rule, ts, scan, dryRun) {
|
|
5379
|
+
const dirty = [];
|
|
5380
|
+
if (mainFindings.length > 0) dirty.push({ path: mainPath, findings: mainFindings });
|
|
5381
|
+
for (const filePath of subtreeFiles) {
|
|
5382
|
+
const raw = scan(filePath);
|
|
5383
|
+
if (raw === null || raw.length === 0) continue;
|
|
5384
|
+
const filtered = rule === void 0 ? raw : raw.filter((f) => f.RuleID === rule);
|
|
5385
|
+
if (filtered.length === 0) continue;
|
|
5386
|
+
dirty.push({ path: filePath, findings: filtered });
|
|
5387
|
+
}
|
|
5388
|
+
const total = dirty.reduce((n, e) => n + e.findings.length, 0);
|
|
5389
|
+
if (!dryRun && total > 0) {
|
|
5390
|
+
for (const { path: filePath, findings } of dirty) {
|
|
5391
|
+
backupBeforeWrite(filePath, ts);
|
|
5392
|
+
const before = readFileSync16(filePath, "utf8");
|
|
5393
|
+
const after = applyRedactions(before, findings);
|
|
5394
|
+
if (after === before) {
|
|
5395
|
+
log(
|
|
5396
|
+
`warning: no redaction applied to ${filePath}: finding match values were not located in the file. Inspect it manually; the push re-scan still blocks a real leak.`
|
|
5397
|
+
);
|
|
5398
|
+
}
|
|
5399
|
+
writeFileSync8(filePath, after, "utf8");
|
|
5400
|
+
}
|
|
5401
|
+
}
|
|
5402
|
+
return { total, dirty };
|
|
5403
|
+
}
|
|
5404
|
+
|
|
5405
|
+
// src/commands.pushed-history.ts
|
|
5406
|
+
init_utils();
|
|
5407
|
+
import { execFileSync as execFileSync14 } from "node:child_process";
|
|
5408
|
+
function pushedRef(repo) {
|
|
5409
|
+
try {
|
|
5410
|
+
const ref = execFileSync14("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], {
|
|
5411
|
+
cwd: repo,
|
|
5412
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
5413
|
+
}).toString().trim();
|
|
5414
|
+
return ref.length > 0 ? ref : null;
|
|
5415
|
+
} catch {
|
|
5416
|
+
return null;
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
function sessionInPushedHistory(id, repo) {
|
|
5420
|
+
const ref = pushedRef(repo);
|
|
5421
|
+
if (ref === null) return false;
|
|
5422
|
+
try {
|
|
5423
|
+
const out = execFileSync14(
|
|
5424
|
+
"git",
|
|
5425
|
+
[
|
|
5426
|
+
"log",
|
|
5427
|
+
ref,
|
|
5428
|
+
"--oneline",
|
|
5429
|
+
"-1",
|
|
5430
|
+
"--",
|
|
5431
|
+
`shared/projects/*/${id}.jsonl`,
|
|
5432
|
+
`shared/projects/*/${id}/*`
|
|
5433
|
+
],
|
|
5434
|
+
{ cwd: repo, stdio: ["ignore", "pipe", "ignore"] }
|
|
5435
|
+
).toString().trim();
|
|
5436
|
+
return out.length > 0;
|
|
5437
|
+
} catch {
|
|
5438
|
+
return false;
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
function warnIfSessionPushed(id, repo) {
|
|
5442
|
+
if (!sessionInPushedHistory(id, repo)) return;
|
|
5443
|
+
log(
|
|
5444
|
+
`warning: session ${id} is already in pushed history (origin).
|
|
5445
|
+
This command only changes your local copy and the next push; it does NOT
|
|
5446
|
+
remove the secret from commits already on the remote.
|
|
5447
|
+
To fully remediate a real secret: rotate the credential, then rewrite
|
|
5448
|
+
history (e.g. with git filter-repo) and force-push, coordinating with
|
|
5449
|
+
anyone else who has cloned the repo.`
|
|
5450
|
+
);
|
|
5451
|
+
}
|
|
5452
|
+
|
|
5453
|
+
// src/commands.redact.ts
|
|
5454
|
+
init_push_gitleaks_scan();
|
|
5455
|
+
init_exit_codes();
|
|
5456
|
+
init_utils_fs();
|
|
5457
|
+
init_utils_json();
|
|
5458
|
+
init_utils();
|
|
5459
|
+
function resolveLiveTranscript(id) {
|
|
5460
|
+
try {
|
|
5461
|
+
const mapPath = join40(repoHome(), "path-map.json");
|
|
5462
|
+
if (!existsSync31(mapPath)) return null;
|
|
5463
|
+
const projects = readJson(mapPath).projects;
|
|
5464
|
+
const claude = claudeHome();
|
|
5465
|
+
for (const hostMap of Object.values(projects)) {
|
|
5466
|
+
const abs = hostMap[HOST];
|
|
5467
|
+
if (abs === void 0) continue;
|
|
5468
|
+
const live = join40(claude, "projects", encodePath(abs), `${id}.jsonl`);
|
|
5469
|
+
if (existsSync31(live)) return live;
|
|
5470
|
+
}
|
|
5471
|
+
return null;
|
|
5472
|
+
} catch {
|
|
5473
|
+
return null;
|
|
5474
|
+
}
|
|
5475
|
+
}
|
|
5476
|
+
function resolveRedactFindings(localPath, rawFindings, rule, scan) {
|
|
5477
|
+
const source = rawFindings ?? scan(localPath);
|
|
5478
|
+
if (source === null) return null;
|
|
5479
|
+
return source.filter((f) => rule === void 0 || f.RuleID === rule);
|
|
5480
|
+
}
|
|
5481
|
+
function cmdRedact(opts, nowMs = Date.now, scan = scanFile) {
|
|
5482
|
+
const { id, rule, dryRun = false, findings: rawFindings } = opts;
|
|
5483
|
+
if (id.length === 0 || id.length > 128 || !/^[A-Za-z0-9_-]+$/.test(id)) {
|
|
5484
|
+
fail(`invalid session id: ${id}`);
|
|
5485
|
+
process.exit(EXIT.USAGE);
|
|
5486
|
+
}
|
|
5487
|
+
const repo = repoHome();
|
|
5488
|
+
const backup = backupBase();
|
|
5489
|
+
if (!existsSync31(repo)) die(`repo not cloned at ${repo}`);
|
|
5490
|
+
const handle = acquireLock("redact");
|
|
5491
|
+
if (handle === null) process.exit(0);
|
|
5492
|
+
try {
|
|
5493
|
+
const localPath = resolveLiveTranscript(id);
|
|
5494
|
+
if (localPath === null || !existsSync31(localPath)) {
|
|
5495
|
+
fail(`could not resolve local transcript for session ${id} on this host`);
|
|
5496
|
+
process.exitCode = 1;
|
|
5497
|
+
return;
|
|
5498
|
+
}
|
|
5499
|
+
const sessionDir = join40(dirname11(localPath), id);
|
|
5500
|
+
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
5501
|
+
if (isSubtreeActive(localPath, subtreeFiles, nowMs())) {
|
|
5502
|
+
log(
|
|
5503
|
+
`session ${id} was modified recently and may be active.
|
|
5504
|
+
Refusing to rewrite a potentially live transcript.
|
|
5505
|
+
To proceed: wait for the session to end, then re-run nomad redact.
|
|
5506
|
+
Or drop from the staged tree: nomad drop-session ${id}
|
|
5507
|
+
Or skip this finding during nomad push.`
|
|
5508
|
+
);
|
|
5509
|
+
return;
|
|
5510
|
+
}
|
|
5511
|
+
const mainFindings = resolveRedactFindings(localPath, rawFindings, rule, scan);
|
|
5512
|
+
if (mainFindings === null) {
|
|
5513
|
+
fail(`gitleaks scan failed for session ${id} (is gitleaks installed?)`);
|
|
5514
|
+
process.exitCode = 1;
|
|
5515
|
+
return;
|
|
5516
|
+
}
|
|
5517
|
+
const ts = freshBackupTs(backup);
|
|
5518
|
+
const { total: totalCount, dirty } = applySubtreeRedactions(
|
|
5519
|
+
localPath,
|
|
5520
|
+
mainFindings,
|
|
5521
|
+
subtreeFiles,
|
|
5522
|
+
rule,
|
|
5523
|
+
ts,
|
|
5524
|
+
scan,
|
|
5525
|
+
dryRun
|
|
5526
|
+
);
|
|
5527
|
+
if (totalCount === 0) {
|
|
5528
|
+
const ruleClause = rule === void 0 ? "" : ` for rule ${rule}`;
|
|
5529
|
+
log(`no findings${ruleClause} in session ${id}`);
|
|
5530
|
+
return;
|
|
5531
|
+
}
|
|
5532
|
+
if (dryRun) {
|
|
5533
|
+
const lines = dirty.flatMap((e) => e.findings.map((f) => ` ${e.path} line ${f.StartLine} [${f.RuleID}]`)).join("\n");
|
|
5534
|
+
log(`dry-run: would redact ${totalCount} finding(s) in session ${id}
|
|
5535
|
+
${lines}`);
|
|
5536
|
+
return;
|
|
5537
|
+
}
|
|
5538
|
+
log(`redacted ${totalCount} finding(s) in ${localPath} (backup: ${ts})`);
|
|
5539
|
+
warnIfSessionPushed(id, repo);
|
|
5540
|
+
} catch (err) {
|
|
5541
|
+
if (!(err instanceof NomadFatal)) {
|
|
5542
|
+
throw err;
|
|
5543
|
+
}
|
|
5544
|
+
fail(err.message);
|
|
5545
|
+
process.exitCode = 1;
|
|
5546
|
+
} finally {
|
|
5547
|
+
releaseLock(handle);
|
|
5548
|
+
}
|
|
5549
|
+
}
|
|
5550
|
+
|
|
5551
|
+
// src/commands.push.recovery.redact.ts
|
|
5552
|
+
init_push_gitleaks_scan();
|
|
5553
|
+
init_utils_json();
|
|
5554
|
+
init_utils();
|
|
5343
5555
|
function resolveStagedDir(localPath, map, claude, repo) {
|
|
5344
5556
|
for (const [logical, hostMap] of Object.entries(map.projects)) {
|
|
5345
5557
|
assertSafeLogical(logical);
|
|
5346
5558
|
const abs = hostMap[HOST];
|
|
5347
5559
|
if (abs === void 0) continue;
|
|
5348
|
-
if (localPath.startsWith(
|
|
5349
|
-
return
|
|
5560
|
+
if (localPath.startsWith(join41(claude, "projects", encodePath(abs)) + sep7)) {
|
|
5561
|
+
return join41(repo, "shared", "projects", logical);
|
|
5350
5562
|
}
|
|
5351
5563
|
}
|
|
5352
5564
|
return null;
|
|
@@ -5356,7 +5568,7 @@ function preflightRedactable(f, map, nowMs) {
|
|
|
5356
5568
|
if (sid === null) return "a finding has no resolvable session id (not a session transcript)";
|
|
5357
5569
|
const localPath = resolveLiveTranscript(sid);
|
|
5358
5570
|
if (localPath === null) return `session ${sid}: local transcript not found`;
|
|
5359
|
-
const sessionDir =
|
|
5571
|
+
const sessionDir = join41(dirname12(localPath), sid);
|
|
5360
5572
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
5361
5573
|
if (isSubtreeActive(localPath, subtreeFiles, nowMs())) {
|
|
5362
5574
|
return `session ${sid}: looks active (modified within the last 5 minutes)`;
|
|
@@ -5385,7 +5597,7 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
5385
5597
|
`could not locate the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
5386
5598
|
);
|
|
5387
5599
|
}
|
|
5388
|
-
const sessionDir =
|
|
5600
|
+
const sessionDir = join41(dirname12(localPath), sid);
|
|
5389
5601
|
const subtreeFiles = listSubtreeFiles(sessionDir);
|
|
5390
5602
|
if (isSubtreeActive(localPath, subtreeFiles, nowMs())) {
|
|
5391
5603
|
return refuse(
|
|
@@ -5417,188 +5629,209 @@ function applyRedact(f, ts, map, nowMs, scan = scanFile) {
|
|
|
5417
5629
|
`nothing to redact in the local transcript for session ${sid}; choose Skip or Drop session.`
|
|
5418
5630
|
);
|
|
5419
5631
|
}
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
if (
|
|
5423
|
-
|
|
5632
|
+
mkdirSync12(stagedProjectDir, { recursive: true });
|
|
5633
|
+
cpSync9(localPath, join41(stagedProjectDir, `${sid}.jsonl`), { force: true });
|
|
5634
|
+
if (existsSync32(sessionDir)) {
|
|
5635
|
+
cpSync9(sessionDir, join41(stagedProjectDir, sid), { force: true, recursive: true });
|
|
5424
5636
|
}
|
|
5425
5637
|
return true;
|
|
5426
5638
|
}
|
|
5427
5639
|
|
|
5428
5640
|
// src/commands.push.recovery.drop.ts
|
|
5429
5641
|
init_config();
|
|
5430
|
-
import { rmSync as
|
|
5431
|
-
import { join as
|
|
5642
|
+
import { rmSync as rmSync14 } from "node:fs";
|
|
5643
|
+
import { join as join42 } from "node:path";
|
|
5432
5644
|
function dropSessionFromStaged(sid, map) {
|
|
5433
5645
|
const logicals = Object.keys(map.projects);
|
|
5434
5646
|
if (logicals.length === 0) return false;
|
|
5435
5647
|
const repo = repoHome();
|
|
5436
5648
|
for (const logical of logicals) {
|
|
5437
|
-
const jsonl =
|
|
5438
|
-
const dir =
|
|
5439
|
-
|
|
5440
|
-
|
|
5649
|
+
const jsonl = join42(repo, "shared", "projects", logical, `${sid}.jsonl`);
|
|
5650
|
+
const dir = join42(repo, "shared", "projects", logical, sid);
|
|
5651
|
+
rmSync14(jsonl, { force: true });
|
|
5652
|
+
rmSync14(dir, { recursive: true, force: true });
|
|
5441
5653
|
}
|
|
5442
5654
|
return true;
|
|
5443
5655
|
}
|
|
5444
5656
|
|
|
5445
|
-
// src/commands.push.recovery.
|
|
5446
|
-
|
|
5447
|
-
import {
|
|
5448
|
-
cpSync as cpSync9,
|
|
5449
|
-
lstatSync as lstatSync14,
|
|
5450
|
-
mkdirSync as mkdirSync12,
|
|
5451
|
-
readFileSync as readFileSync16,
|
|
5452
|
-
realpathSync as realpathSync5,
|
|
5453
|
-
statSync as statSync8,
|
|
5454
|
-
writeFileSync as writeFileSync8
|
|
5455
|
-
} from "node:fs";
|
|
5456
|
-
import { dirname as dirname12, join as join42, sep as sep7 } from "node:path";
|
|
5457
|
-
|
|
5458
|
-
// src/skills-sync.ts
|
|
5459
|
-
init_config();
|
|
5460
|
-
import { existsSync as existsSync32, lstatSync as lstatSync13, mkdirSync as mkdirSync11, readdirSync as readdirSync14, rmSync as rmSync14 } from "node:fs";
|
|
5461
|
-
import { join as join41 } from "node:path";
|
|
5462
|
-
|
|
5463
|
-
// src/skills-sync.tracked.ts
|
|
5657
|
+
// src/commands.push.recovery.actions.ts
|
|
5658
|
+
init_push_gitleaks_scan();
|
|
5464
5659
|
init_utils();
|
|
5465
|
-
import { basename as basename3 } from "node:path";
|
|
5466
|
-
function trackedRootSkillsAt(ref, repo) {
|
|
5467
|
-
try {
|
|
5468
|
-
const raw = gitCaptureRaw(["ls-tree", "--name-only", "-z", ref, "--", "shared/skills/"], repo);
|
|
5469
|
-
const entries = raw.split("\0").filter((entry) => entry !== "");
|
|
5470
|
-
return new Set(entries.map((entry) => basename3(entry)));
|
|
5471
|
-
} catch {
|
|
5472
|
-
return /* @__PURE__ */ new Set();
|
|
5473
|
-
}
|
|
5474
|
-
}
|
|
5475
5660
|
|
|
5476
|
-
// src/
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5492
|
-
|
|
5493
|
-
|
|
5661
|
+
// src/commands.push.recovery.secret-shape.ts
|
|
5662
|
+
var VALUE_HEAD = 4;
|
|
5663
|
+
var VALUE_TAIL = 4;
|
|
5664
|
+
var MIN_ELIDE_LEN = 16;
|
|
5665
|
+
var FRAGMENT_SEP = "...";
|
|
5666
|
+
var NEAR_WINDOW = 40;
|
|
5667
|
+
var VALUE_PAD = 16;
|
|
5668
|
+
var REDACTED_TOKEN = "REDACTED";
|
|
5669
|
+
var CONTROL_CHARS = /[\x00-\x1f\x7f]/g;
|
|
5670
|
+
function classifyCharset(value) {
|
|
5671
|
+
if (/^[0-9a-fA-F]+$/.test(value)) return "hex";
|
|
5672
|
+
if (/^[0-9a-zA-Z]+$/.test(value)) return "alphanumeric";
|
|
5673
|
+
if (/^[0-9a-zA-Z_-]+$/.test(value)) return "base64url";
|
|
5674
|
+
return "mixed";
|
|
5675
|
+
}
|
|
5676
|
+
function describeSecret(secret, entropy) {
|
|
5677
|
+
const parts = [`${secret.length} chars`, classifyCharset(secret)];
|
|
5678
|
+
if (typeof entropy === "number" && Number.isFinite(entropy) && entropy > 0) {
|
|
5679
|
+
parts.push(`entropy ${entropy.toFixed(2)}`);
|
|
5680
|
+
}
|
|
5681
|
+
const summary = parts.join(", ");
|
|
5682
|
+
if (secret.length < MIN_ELIDE_LEN) return summary;
|
|
5683
|
+
const fragment = secret.slice(0, VALUE_HEAD) + FRAGMENT_SEP + secret.slice(-VALUE_TAIL);
|
|
5684
|
+
return `${fragment.padEnd(VALUE_PAD)} ${summary}`;
|
|
5685
|
+
}
|
|
5686
|
+
function emptyToNull(text) {
|
|
5687
|
+
return text.length > 0 ? text : null;
|
|
5688
|
+
}
|
|
5689
|
+
function withoutValue(near, value) {
|
|
5690
|
+
if (near === null || value === null || value.length === 0) return near;
|
|
5691
|
+
const idx = near.indexOf(value);
|
|
5692
|
+
return idx < 0 ? near : emptyToNull(near.slice(0, idx));
|
|
5693
|
+
}
|
|
5694
|
+
function alignRedactedMatch(finding, span, lead) {
|
|
5695
|
+
const idx = finding.Match.indexOf(REDACTED_TOKEN);
|
|
5696
|
+
if (idx < 0) return null;
|
|
5697
|
+
const pre = finding.Match.slice(0, idx);
|
|
5698
|
+
const post = finding.Match.slice(idx + REDACTED_TOKEN.length);
|
|
5699
|
+
if (span !== finding.Match && span.startsWith(pre) && span.endsWith(post) && span.length > pre.length + post.length) {
|
|
5700
|
+
return {
|
|
5701
|
+
value: span.slice(pre.length, span.length - post.length),
|
|
5702
|
+
near: emptyToNull(lead + pre),
|
|
5703
|
+
exact: true
|
|
5704
|
+
};
|
|
5705
|
+
}
|
|
5706
|
+
return { value: null, near: emptyToNull(lead + pre), exact: false };
|
|
5707
|
+
}
|
|
5708
|
+
function maskSiblingSpans(buf, siblings, ownStart, ownEnd) {
|
|
5709
|
+
if (siblings.length === 0) return buf;
|
|
5710
|
+
const copy = Buffer.from(buf);
|
|
5711
|
+
for (const sibling of siblings) {
|
|
5712
|
+
const start = Math.max(1, Math.min(sibling.StartColumn, copy.length + 1)) - 1;
|
|
5713
|
+
const end = Math.max(start, Math.min(sibling.EndColumn, copy.length));
|
|
5714
|
+
const headEnd = Math.min(end, ownStart);
|
|
5715
|
+
if (start < headEnd) copy.fill(0, start, headEnd);
|
|
5716
|
+
const tailStart = Math.max(start, ownEnd);
|
|
5717
|
+
if (tailStart < end) copy.fill(0, tailStart, end);
|
|
5718
|
+
}
|
|
5719
|
+
return copy;
|
|
5720
|
+
}
|
|
5721
|
+
function resolveSecretContext(finding, readLine, siblings = []) {
|
|
5722
|
+
const resolved = resolveSpanContext(finding, readLine, siblings);
|
|
5723
|
+
const ownStripped = withoutValue(resolved.near, resolved.value);
|
|
5724
|
+
const multiLine = typeof finding.EndLine === "number" && finding.EndLine > finding.StartLine;
|
|
5725
|
+
return {
|
|
5726
|
+
value: resolved.value,
|
|
5727
|
+
near: ownStripped,
|
|
5728
|
+
exact: resolved.exact && !multiLine
|
|
5729
|
+
};
|
|
5494
5730
|
}
|
|
5495
|
-
function
|
|
5496
|
-
const
|
|
5497
|
-
|
|
5498
|
-
|
|
5499
|
-
|
|
5500
|
-
|
|
5501
|
-
|
|
5502
|
-
|
|
5503
|
-
|
|
5731
|
+
function resolveSpanContext(finding, readLine, siblings = []) {
|
|
5732
|
+
const raw = readLine(finding.File, finding.StartLine);
|
|
5733
|
+
let span;
|
|
5734
|
+
let lead;
|
|
5735
|
+
if (raw !== null) {
|
|
5736
|
+
const buf = Buffer.from(raw, "utf8");
|
|
5737
|
+
const len = buf.length;
|
|
5738
|
+
const startCol = Math.max(1, Math.min(finding.StartColumn, len + 1));
|
|
5739
|
+
const endCol = Math.max(startCol, Math.min(finding.EndColumn, len));
|
|
5740
|
+
span = buf.subarray(startCol - 1, endCol).toString("utf8");
|
|
5741
|
+
lead = maskSiblingSpans(buf, siblings, startCol - 1, endCol).subarray(0, startCol - 1).toString("utf8");
|
|
5742
|
+
} else {
|
|
5743
|
+
span = finding.Match;
|
|
5744
|
+
lead = "";
|
|
5504
5745
|
}
|
|
5505
|
-
const
|
|
5506
|
-
|
|
5746
|
+
const aligned = alignRedactedMatch(finding, span, lead);
|
|
5747
|
+
if (aligned !== null) return aligned;
|
|
5748
|
+
if (typeof finding.Secret === "string" && finding.Secret.length > 0 && span.includes(finding.Secret)) {
|
|
5749
|
+
return {
|
|
5750
|
+
value: finding.Secret,
|
|
5751
|
+
near: emptyToNull(lead + span.slice(0, span.indexOf(finding.Secret))),
|
|
5752
|
+
exact: true
|
|
5753
|
+
};
|
|
5754
|
+
}
|
|
5755
|
+
return {
|
|
5756
|
+
value: span.length > 0 ? span : null,
|
|
5757
|
+
near: emptyToNull(lead),
|
|
5758
|
+
exact: false
|
|
5759
|
+
};
|
|
5507
5760
|
}
|
|
5508
|
-
function
|
|
5509
|
-
const
|
|
5510
|
-
const
|
|
5511
|
-
|
|
5512
|
-
if (
|
|
5513
|
-
|
|
5514
|
-
|
|
5761
|
+
function formatNear(text) {
|
|
5762
|
+
const stripped = elideTokenRuns(text.replace(CONTROL_CHARS, ""));
|
|
5763
|
+
const truncated = stripped.length > NEAR_WINDOW;
|
|
5764
|
+
const window = stripped.slice(-NEAR_WINDOW).trim();
|
|
5765
|
+
if (window.length === 0) return null;
|
|
5766
|
+
return (truncated ? FRAGMENT_SEP : "") + window;
|
|
5767
|
+
}
|
|
5768
|
+
function elideTokenRuns(text) {
|
|
5769
|
+
return text.replace(/[A-Za-z0-9_+=-]+/g, (atom) => {
|
|
5770
|
+
if (atom.length < MIN_ELIDE_LEN) return atom;
|
|
5771
|
+
if (/^[A-Za-z_-]+$/.test(atom)) return atom;
|
|
5772
|
+
return FRAGMENT_SEP;
|
|
5773
|
+
});
|
|
5515
5774
|
}
|
|
5516
5775
|
|
|
5517
|
-
// src/commands.push.recovery.
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
5527
|
-
|
|
5528
|
-
}
|
|
5529
|
-
|
|
5530
|
-
|
|
5776
|
+
// src/commands.push.recovery.display.ts
|
|
5777
|
+
var RULE_PAD = 24;
|
|
5778
|
+
var GROUP_SEP = "\0";
|
|
5779
|
+
function sessionSuffix(finding) {
|
|
5780
|
+
const sid = sessionIdFromFinding(finding);
|
|
5781
|
+
if (sid === null) return "";
|
|
5782
|
+
const rawBasename = finding.File.replace(/^.*\//, "");
|
|
5783
|
+
const basename4 = rawBasename.endsWith(".jsonl") ? rawBasename.slice(0, -".jsonl".length) : rawBasename;
|
|
5784
|
+
return basename4 === sid ? "" : ` (session: ${sid})`;
|
|
5785
|
+
}
|
|
5786
|
+
function renderFindingBlock(finding, readLine, siblings = []) {
|
|
5787
|
+
const lines = [` file: ${finding.File}:${finding.StartLine}${sessionSuffix(finding)}`];
|
|
5788
|
+
const { value, near, exact } = resolveSecretContext(finding, readLine, siblings);
|
|
5789
|
+
if (value !== null) {
|
|
5790
|
+
lines.push(` value: ${describeSecret(value, exact ? finding.Entropy : void 0)}`);
|
|
5791
|
+
}
|
|
5792
|
+
const formattedNear = near !== null ? formatNear(near) : null;
|
|
5793
|
+
if (formattedNear !== null) lines.push(` near: ${formattedNear}`);
|
|
5794
|
+
return lines;
|
|
5531
5795
|
}
|
|
5532
|
-
function
|
|
5533
|
-
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
const
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
|
|
5796
|
+
function groupFindingsForPrompt(findings, readLine) {
|
|
5797
|
+
const groups = [];
|
|
5798
|
+
const indexByKey = /* @__PURE__ */ new Map();
|
|
5799
|
+
for (const f of findings) {
|
|
5800
|
+
const { value } = resolveSecretContext(f, readLine);
|
|
5801
|
+
const key = f.Fingerprint.length > 0 && value !== null ? `${f.Fingerprint}${GROUP_SEP}${value}` : (
|
|
5802
|
+
// findingKey omits the fingerprint, so carry it here too, or two
|
|
5803
|
+
// findings differing ONLY by fingerprint would merge.
|
|
5804
|
+
`${f.Fingerprint}${GROUP_SEP}${findingKey(f)}`
|
|
5805
|
+
);
|
|
5806
|
+
const idx = indexByKey.get(key);
|
|
5807
|
+
if (idx === void 0) {
|
|
5808
|
+
indexByKey.set(key, groups.length);
|
|
5809
|
+
groups.push([f]);
|
|
5810
|
+
} else {
|
|
5811
|
+
groups[idx].push(f);
|
|
5812
|
+
}
|
|
5548
5813
|
}
|
|
5549
|
-
return
|
|
5814
|
+
return groups;
|
|
5550
5815
|
}
|
|
5551
|
-
function
|
|
5552
|
-
const
|
|
5553
|
-
if (
|
|
5554
|
-
|
|
5555
|
-
if (localPath === null) {
|
|
5556
|
-
return `skill file ${parsed.name}/${parsed.relPath}: local file not found or unresolvable`;
|
|
5557
|
-
}
|
|
5558
|
-
return null;
|
|
5816
|
+
function sharedFingerprintPeers(groups, index) {
|
|
5817
|
+
const target = groups[index][0].Fingerprint;
|
|
5818
|
+
if (target.length === 0) return 0;
|
|
5819
|
+
return groups.filter((g, i) => i !== index && g[0].Fingerprint === target).length;
|
|
5559
5820
|
}
|
|
5560
|
-
function
|
|
5561
|
-
const
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
return refuse("could not parse this finding as a skill file; choose Skip.");
|
|
5568
|
-
}
|
|
5569
|
-
const { name, relPath } = parsed;
|
|
5570
|
-
const localPath = resolveSkillLocalPath(name, relPath);
|
|
5571
|
-
if (localPath === null) {
|
|
5572
|
-
return refuse(`could not locate the local skill file for ${name}/${relPath}; choose Skip.`);
|
|
5573
|
-
}
|
|
5574
|
-
const findings = scan(localPath);
|
|
5575
|
-
if (findings === null) {
|
|
5576
|
-
return refuse(`re-scan of ${name}/${relPath} failed; choose Skip.`);
|
|
5577
|
-
}
|
|
5578
|
-
if (findings.length === 0) {
|
|
5579
|
-
return refuse(`nothing to redact in ${name}/${relPath}; choose Skip.`);
|
|
5821
|
+
function buildPromptHeader(group, index, total, readLine, peers = 0, siblings = []) {
|
|
5822
|
+
const first = group[0];
|
|
5823
|
+
const headerLine = group.length === 1 ? `Finding ${index}/${total}: ${first.RuleID}` : `Finding ${index}/${total}: ${first.RuleID.padEnd(RULE_PAD)} ${group.length} occurrences, 1 ignore entry`;
|
|
5824
|
+
const body = renderFindingBlock(first, readLine, siblings);
|
|
5825
|
+
if (peers > 0) {
|
|
5826
|
+
const other = peers === 1 ? "secret" : "secrets";
|
|
5827
|
+
body.push(` warn: Allow also suppresses ${peers} other distinct ${other} on this line`);
|
|
5580
5828
|
}
|
|
5581
|
-
|
|
5582
|
-
const
|
|
5583
|
-
|
|
5584
|
-
if (after === before) {
|
|
5585
|
-
warn(
|
|
5586
|
-
`no redaction applied to ${name}/${relPath}: finding match values were not located in the file. Inspect it manually; the push re-scan still blocks a real leak.`
|
|
5587
|
-
);
|
|
5588
|
-
}
|
|
5589
|
-
writeFileSync8(localPath, after, "utf8");
|
|
5590
|
-
const dest = join42(repoHome(), "shared", "skills", name, ...relPath.split("/"));
|
|
5591
|
-
mkdirSync12(dirname12(dest), { recursive: true });
|
|
5592
|
-
cpSync9(localPath, dest, { force: true });
|
|
5593
|
-
return true;
|
|
5829
|
+
const sid = sessionIdFromFinding(first);
|
|
5830
|
+
const menu = sid === null ? " [R]edact [A]llow [S]kip (default)" : " [R]edact [A]llow [D]rop session [S]kip (default)";
|
|
5831
|
+
return ["", headerLine, ...body, menu, ""].join("\n");
|
|
5594
5832
|
}
|
|
5595
5833
|
|
|
5596
5834
|
// src/commands.push.recovery.actions.ts
|
|
5597
|
-
init_push_gitleaks_scan();
|
|
5598
|
-
init_utils();
|
|
5599
|
-
function applyAllow(f, repo) {
|
|
5600
|
-
appendGitleaksIgnore(f.Fingerprint, repo);
|
|
5601
|
-
}
|
|
5602
5835
|
function allowAllFindings(findings, repo) {
|
|
5603
5836
|
for (const f of findings) {
|
|
5604
5837
|
appendGitleaksIgnore(f.Fingerprint, repo);
|
|
@@ -5615,6 +5848,7 @@ function allowFindingsByRule(findings, ruleId, repo) {
|
|
|
5615
5848
|
return count;
|
|
5616
5849
|
}
|
|
5617
5850
|
function makeDefaultReadLine(repo) {
|
|
5851
|
+
const cache = /* @__PURE__ */ new Map();
|
|
5618
5852
|
return (file, line) => {
|
|
5619
5853
|
try {
|
|
5620
5854
|
const repoRoot = resolve3(repo);
|
|
@@ -5622,8 +5856,11 @@ function makeDefaultReadLine(repo) {
|
|
|
5622
5856
|
if (isAbsolute2(file) || target !== repoRoot && !target.startsWith(repoRoot + sep8)) {
|
|
5623
5857
|
return null;
|
|
5624
5858
|
}
|
|
5625
|
-
|
|
5626
|
-
|
|
5859
|
+
let lines = cache.get(target);
|
|
5860
|
+
if (lines === void 0) {
|
|
5861
|
+
lines = readFileSync17(target, "utf8").split(/\r?\n/);
|
|
5862
|
+
cache.set(target, lines);
|
|
5863
|
+
}
|
|
5627
5864
|
const idx = line - 1;
|
|
5628
5865
|
if (idx < 0 || idx >= lines.length) return null;
|
|
5629
5866
|
return lines[idx] ?? null;
|
|
@@ -5634,16 +5871,19 @@ function makeDefaultReadLine(repo) {
|
|
|
5634
5871
|
}
|
|
5635
5872
|
async function collectActions(findings, prompt, readLine) {
|
|
5636
5873
|
const reader = readLine ?? makeDefaultReadLine(repoHome());
|
|
5874
|
+
const groups = groupFindingsForPrompt(findings, reader);
|
|
5637
5875
|
const actions = /* @__PURE__ */ new Map();
|
|
5638
|
-
for (
|
|
5639
|
-
const
|
|
5640
|
-
const
|
|
5641
|
-
const
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5646
|
-
|
|
5876
|
+
for (let i = 0; i < groups.length; i++) {
|
|
5877
|
+
const group = groups[i];
|
|
5878
|
+
const peers = sharedFingerprintPeers(groups, i);
|
|
5879
|
+
const siblings = findings.filter(
|
|
5880
|
+
(o) => o.File === group[0].File && o.StartLine === group[0].StartLine && !group.includes(o)
|
|
5881
|
+
);
|
|
5882
|
+
const header = buildPromptHeader(group, i + 1, groups.length, reader, peers, siblings);
|
|
5883
|
+
const action = parseAction(await prompt(header + "> "));
|
|
5884
|
+
for (const f of group) {
|
|
5885
|
+
actions.set(findingKey(f), action);
|
|
5886
|
+
}
|
|
5647
5887
|
}
|
|
5648
5888
|
return actions;
|
|
5649
5889
|
}
|
|
@@ -5683,13 +5923,9 @@ function dispatchNonSession(f, action, ctx) {
|
|
|
5683
5923
|
}
|
|
5684
5924
|
function dispatchOne(f, ctx) {
|
|
5685
5925
|
const action = ctx.actions.get(findingKey(f)) ?? "skip";
|
|
5686
|
-
if (action === "skip") return;
|
|
5926
|
+
if (action === "skip" || action === "allow") return;
|
|
5687
5927
|
const sid = sessionIdFromFinding(f);
|
|
5688
5928
|
if (sid !== null && ctx.droppedSids.has(sid)) return;
|
|
5689
|
-
if (action === "allow") {
|
|
5690
|
-
applyAllow(f, ctx.repo);
|
|
5691
|
-
return;
|
|
5692
|
-
}
|
|
5693
5929
|
if (sid === null) {
|
|
5694
5930
|
dispatchNonSession(f, action, ctx);
|
|
5695
5931
|
return;
|
|
@@ -5725,6 +5961,7 @@ function dispatchActions(findings, actions, opts) {
|
|
|
5725
5961
|
for (const f of findings) {
|
|
5726
5962
|
dispatchOne(f, ctx);
|
|
5727
5963
|
}
|
|
5964
|
+
applyDeferredAllows(findings, ctx, repo);
|
|
5728
5965
|
}
|
|
5729
5966
|
|
|
5730
5967
|
// src/commands.push.recovery.redact-all.ts
|
|
@@ -6228,11 +6465,19 @@ function isProblem(item2) {
|
|
|
6228
6465
|
function isRepoStateLine(item2) {
|
|
6229
6466
|
return item2.includes("repo state:");
|
|
6230
6467
|
}
|
|
6468
|
+
function isCopySyncModalityLine(item2) {
|
|
6469
|
+
return item2.includes("sync modality: copy-sync");
|
|
6470
|
+
}
|
|
6231
6471
|
function compactSections(sections) {
|
|
6232
6472
|
return sections.map((s) => {
|
|
6233
6473
|
if (ALWAYS_FULL.has(s.header)) return s;
|
|
6234
6474
|
if (s.header === "Environment") {
|
|
6235
|
-
return {
|
|
6475
|
+
return {
|
|
6476
|
+
...s,
|
|
6477
|
+
items: s.items.filter(
|
|
6478
|
+
(it) => isRepoStateLine(it) || isCopySyncModalityLine(it) || isProblem(it)
|
|
6479
|
+
)
|
|
6480
|
+
};
|
|
6236
6481
|
}
|
|
6237
6482
|
return { ...s, items: s.items.filter(isProblem) };
|
|
6238
6483
|
});
|
|
@@ -7547,6 +7792,22 @@ function handleWedge(repo, forceRemote) {
|
|
|
7547
7792
|
const state = wedge === "rebase" ? "mid-rebase" : "mid-merge";
|
|
7548
7793
|
die(wedgeMarkerRunbookText(state), { code: EXIT.CONFLICT });
|
|
7549
7794
|
}
|
|
7795
|
+
function readMapForMirror(mapPath) {
|
|
7796
|
+
if (!existsSync42(mapPath)) return { projects: {} };
|
|
7797
|
+
try {
|
|
7798
|
+
return readPathMap(mapPath);
|
|
7799
|
+
} catch {
|
|
7800
|
+
return null;
|
|
7801
|
+
}
|
|
7802
|
+
}
|
|
7803
|
+
function mirrorSharedLinksBeforePull(repo, ts) {
|
|
7804
|
+
if (process.platform !== "win32") return;
|
|
7805
|
+
try {
|
|
7806
|
+
stageLocalSharedEdits(readMapForMirror(join53(repo, "path-map.json")), ts);
|
|
7807
|
+
} catch (err) {
|
|
7808
|
+
warn(`could not stage local shared edits before the pull: ${err.message}`);
|
|
7809
|
+
}
|
|
7810
|
+
}
|
|
7550
7811
|
function runPullCore(opts = {}) {
|
|
7551
7812
|
const dryRun = opts.dryRun === true;
|
|
7552
7813
|
const forceRemote = opts.forceRemote === true;
|
|
@@ -7568,6 +7829,7 @@ function runPullCore(opts = {}) {
|
|
|
7568
7829
|
dryRun ? `pulling on host=${HOST} (backup=${ts}; dry-run)` : `pull on host=${HOST} (backup=${ts})`
|
|
7569
7830
|
);
|
|
7570
7831
|
}
|
|
7832
|
+
if (!dryRun && !forceRemote) mirrorSharedLinksBeforePull(repo, ts);
|
|
7571
7833
|
const prePostHeads = capturePrePostHeads(repo, () => {
|
|
7572
7834
|
gitOrFatal(["pull", "--rebase", "--autostash"], "git pull --rebase", repo);
|
|
7573
7835
|
});
|
|
@@ -9103,7 +9365,7 @@ function parsePullArgs(argv) {
|
|
|
9103
9365
|
|
|
9104
9366
|
// src/nomad.dispatch.push.ts
|
|
9105
9367
|
var RULE_ID_RE = /^\w[\w-]*$/;
|
|
9106
|
-
function
|
|
9368
|
+
function applyAllow(argv, i, st) {
|
|
9107
9369
|
if (st.allowRule !== void 0) return REJECT;
|
|
9108
9370
|
const val = extractFlagValue(argv, i);
|
|
9109
9371
|
if (val === null || !RULE_ID_RE.test(val)) return REJECT;
|
|
@@ -9119,7 +9381,7 @@ function applyPushToken(argv, i, st) {
|
|
|
9119
9381
|
case "--allow-all":
|
|
9120
9382
|
return applyBool(st.allowAll, () => st.allowAll = true);
|
|
9121
9383
|
case "--allow":
|
|
9122
|
-
return
|
|
9384
|
+
return applyAllow(argv, i, st);
|
|
9123
9385
|
case "--full-scan":
|
|
9124
9386
|
return applyBool(st.fullScan, () => st.fullScan = true);
|
|
9125
9387
|
default:
|
|
@@ -9177,7 +9439,7 @@ function parseSyncArgs(argv) {
|
|
|
9177
9439
|
// package.json
|
|
9178
9440
|
var package_default = {
|
|
9179
9441
|
name: "claude-nomad",
|
|
9180
|
-
version: "0.
|
|
9442
|
+
version: "0.63.0",
|
|
9181
9443
|
type: "module",
|
|
9182
9444
|
description: "Sync Claude Code config (~/.claude/) across machines via a private Git repo, with path remapping and per-host settings overrides.",
|
|
9183
9445
|
keywords: [
|