@ankurhalder/codebase-snapshot 3.0.0 → 4.0.1
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/README.md +161 -310
- package/dist/bin.js +2247 -709
- package/dist/{chunk-5B4UVQLR.js → chunk-RGIMMEAW.js} +1763 -425
- package/dist/cli.cjs +2212 -673
- package/dist/cli.d.cts +3 -1
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +268 -68
- package/dist/index.cjs +1762 -422
- package/dist/index.d.cts +28 -29
- package/dist/index.d.ts +28 -29
- package/dist/index.js +1 -1
- package/dist/{types-CJqkePM4.d.cts → types-LSfwsKE-.d.cts} +38 -4
- package/dist/{types-CJqkePM4.d.ts → types-LSfwsKE-.d.ts} +38 -4
- package/docs/agent.md +77 -37
- package/docs/cli.md +317 -0
- package/docs/configuration.md +254 -226
- package/docs/install.md +114 -86
- package/docs/output.md +213 -0
- package/docs/troubleshooting.md +277 -0
- package/package.json +7 -4
package/dist/bin.js
CHANGED
|
@@ -6,112 +6,17 @@ import * as path12 from "path";
|
|
|
6
6
|
import { parseArgs } from "util";
|
|
7
7
|
|
|
8
8
|
// src/diff.ts
|
|
9
|
-
import * as
|
|
10
|
-
import * as
|
|
11
|
-
function side(manifest) {
|
|
12
|
-
return {
|
|
13
|
-
generatedAt: manifest.generatedAt,
|
|
14
|
-
fingerprint: manifest.fingerprint,
|
|
15
|
-
fileCount: manifest.counts.totalFiles,
|
|
16
|
-
partsCount: manifest.counts.partsCount
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
function hashMap(manifest) {
|
|
20
|
-
const map = /* @__PURE__ */ new Map();
|
|
21
|
-
for (const file of manifest.files) {
|
|
22
|
-
if (file && typeof file.relativePath === "string") {
|
|
23
|
-
map.set(file.relativePath, file.hashSha256);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return map;
|
|
27
|
-
}
|
|
28
|
-
function compareManifests(before, after) {
|
|
29
|
-
const left = hashMap(before);
|
|
30
|
-
const right = hashMap(after);
|
|
31
|
-
const added = [];
|
|
32
|
-
const removed = [];
|
|
33
|
-
const changed = [];
|
|
34
|
-
let unchanged = 0;
|
|
35
|
-
for (const [filePath, hash] of right) {
|
|
36
|
-
const previous = left.get(filePath);
|
|
37
|
-
if (previous === void 0) added.push(filePath);
|
|
38
|
-
else if (previous !== hash) changed.push(filePath);
|
|
39
|
-
else unchanged += 1;
|
|
40
|
-
}
|
|
41
|
-
for (const filePath of left.keys()) {
|
|
42
|
-
if (!right.has(filePath)) removed.push(filePath);
|
|
43
|
-
}
|
|
44
|
-
const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
45
|
-
return {
|
|
46
|
-
before: side(before),
|
|
47
|
-
after: side(after),
|
|
48
|
-
added: sort(added),
|
|
49
|
-
removed: sort(removed),
|
|
50
|
-
changed: sort(changed),
|
|
51
|
-
unchanged
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
function loadManifest(target) {
|
|
55
|
-
let isDirectory;
|
|
56
|
-
try {
|
|
57
|
-
isDirectory = fs.statSync(target).isDirectory();
|
|
58
|
-
} catch {
|
|
59
|
-
throw new Error(`No snapshot found at ${target}: the path does not exist.`);
|
|
60
|
-
}
|
|
61
|
-
const manifestPath = isDirectory ? path.join(target, "manifest.json") : target;
|
|
62
|
-
let raw;
|
|
63
|
-
try {
|
|
64
|
-
raw = fs.readFileSync(manifestPath, "utf8");
|
|
65
|
-
} catch {
|
|
66
|
-
throw new Error(`Could not read a manifest at ${manifestPath}.`);
|
|
67
|
-
}
|
|
68
|
-
let parsed;
|
|
69
|
-
try {
|
|
70
|
-
parsed = JSON.parse(raw);
|
|
71
|
-
} catch (err) {
|
|
72
|
-
throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
|
|
73
|
-
}
|
|
74
|
-
if (!parsed || !Array.isArray(parsed.files)) {
|
|
75
|
-
throw new Error(`${manifestPath} is not a snapshot manifest (no "files" array).`);
|
|
76
|
-
}
|
|
77
|
-
if (!parsed.counts || typeof parsed.counts !== "object") {
|
|
78
|
-
throw new Error(`${manifestPath} is not a snapshot manifest (no "counts" object).`);
|
|
79
|
-
}
|
|
80
|
-
return parsed;
|
|
81
|
-
}
|
|
82
|
-
function renderDiffText(diff) {
|
|
83
|
-
const lines = [];
|
|
84
|
-
lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
|
|
85
|
-
lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
|
|
86
|
-
lines.push("");
|
|
87
|
-
lines.push(` + ${diff.added.length} added`);
|
|
88
|
-
lines.push(` - ${diff.removed.length} removed`);
|
|
89
|
-
lines.push(` ~ ${diff.changed.length} changed`);
|
|
90
|
-
lines.push(` = ${diff.unchanged} unchanged`);
|
|
91
|
-
const section = (title, values) => {
|
|
92
|
-
if (values.length === 0) return;
|
|
93
|
-
lines.push("", title);
|
|
94
|
-
for (const value of values.slice(0, 200)) lines.push(` ${value}`);
|
|
95
|
-
if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
|
|
96
|
-
};
|
|
97
|
-
section("added:", diff.added);
|
|
98
|
-
section("removed:", diff.removed);
|
|
99
|
-
section("changed:", diff.changed);
|
|
100
|
-
return lines.join("\n");
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// src/explain.ts
|
|
104
|
-
import * as fs5 from "fs";
|
|
105
|
-
import * as path6 from "path";
|
|
106
|
-
|
|
107
|
-
// src/config.ts
|
|
108
|
-
import * as fs4 from "fs";
|
|
109
|
-
import * as path4 from "path";
|
|
110
|
-
import { pathToFileURL } from "url";
|
|
9
|
+
import * as fs3 from "fs";
|
|
10
|
+
import * as path3 from "path";
|
|
111
11
|
|
|
112
|
-
// src/
|
|
12
|
+
// src/scanner/files.ts
|
|
13
|
+
import * as childProcess from "child_process";
|
|
113
14
|
import * as fs2 from "fs";
|
|
114
15
|
import * as path2 from "path";
|
|
16
|
+
|
|
17
|
+
// src/presets/context.ts
|
|
18
|
+
import * as fs from "fs";
|
|
19
|
+
import * as path from "path";
|
|
115
20
|
var MANIFEST_FILES = [
|
|
116
21
|
"package.json",
|
|
117
22
|
"pyproject.toml",
|
|
@@ -127,11 +32,11 @@ var MANIFEST_FILES = [
|
|
|
127
32
|
];
|
|
128
33
|
var MANIFEST_MAX_BYTES = 1e5;
|
|
129
34
|
function readManifest(cwd, name) {
|
|
130
|
-
const full =
|
|
35
|
+
const full = path.join(cwd, name);
|
|
131
36
|
try {
|
|
132
|
-
const stats =
|
|
37
|
+
const stats = fs.statSync(full);
|
|
133
38
|
if (!stats.isFile() || stats.size > MANIFEST_MAX_BYTES) return null;
|
|
134
|
-
return
|
|
39
|
+
return fs.readFileSync(full, "utf8");
|
|
135
40
|
} catch {
|
|
136
41
|
return null;
|
|
137
42
|
}
|
|
@@ -156,7 +61,7 @@ function buildPresetContext(cwd) {
|
|
|
156
61
|
const files = /* @__PURE__ */ new Set();
|
|
157
62
|
const dirs = /* @__PURE__ */ new Set();
|
|
158
63
|
try {
|
|
159
|
-
for (const entry of
|
|
64
|
+
for (const entry of fs.readdirSync(cwd, { withFileTypes: true })) {
|
|
160
65
|
if (entry.isDirectory()) dirs.add(entry.name);
|
|
161
66
|
else if (entry.isFile()) files.add(entry.name);
|
|
162
67
|
}
|
|
@@ -951,68 +856,532 @@ function detectPreset(cwd, forced) {
|
|
|
951
856
|
return detectPresetFromContext(buildPresetContext(cwd), forced);
|
|
952
857
|
}
|
|
953
858
|
|
|
954
|
-
// src/
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
859
|
+
// src/security/patterns.ts
|
|
860
|
+
var STRONG_CREDENTIAL_NAME = "(?:password|passwd|passphrase|pwd|api[_-]?key|api[_-]?secret|secret[_-]?access[_-]?key|secret[_-]?key|client[_-]?secret[_-]?key|client[_-]?secret|private[_-]?key|aws[_-]?secret[_-]?access[_-]?key|access[_-]?token|refresh[_-]?token|auth[_-]?token|session[_-]?token|secret|token)";
|
|
861
|
+
var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
|
|
862
|
+
var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|process\\.env|Buffer|Record|object)\\b)";
|
|
863
|
+
var UNQUOTED_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{1,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
864
|
+
var UNQUOTED_AMBIGUOUS_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{8,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
865
|
+
var UNQUOTED_STRONG_NAME = `(?<!id[-_])${STRONG_CREDENTIAL_NAME}`;
|
|
866
|
+
var DEFAULT_SECRET_PATTERNS = [
|
|
867
|
+
{
|
|
868
|
+
rule: "private-key-header",
|
|
869
|
+
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
rule: "jwt-token",
|
|
873
|
+
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
rule: "aws-access-key-id",
|
|
877
|
+
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
// The name is no longer limited to the three snake_case spellings: an
|
|
881
|
+
// `awsSecret` / `secretAccessKey` property leaked verbatim because only the
|
|
882
|
+
// literal words matched. The value is masked alone, so the key survives.
|
|
883
|
+
rule: "aws-secret-access-key",
|
|
884
|
+
pattern: new RegExp(
|
|
885
|
+
`${STRONG_CREDENTIAL_NAME}["']?\\s{0,8}[:=]\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
|
|
886
|
+
"i"
|
|
887
|
+
),
|
|
888
|
+
valueGroup: 1
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
// A 40-character base64 value with no key name in sight. A key-independent
|
|
892
|
+
// rule is the only thing that catches `new AWS.SecretKey(…)` arguments and
|
|
893
|
+
// pasted `~/.aws/credentials` fragments, but a bare length test also matches
|
|
894
|
+
// every 40-char git SHA-1, so the value must mix case and digits and must
|
|
895
|
+
// not be pure hex.
|
|
896
|
+
rule: "aws-secret-access-key-value",
|
|
897
|
+
pattern: /(?<![A-Za-z0-9/+=])(?![0-9a-fA-F]{40}(?![A-Za-z0-9/+=]))(?=[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=]))(?=[A-Za-z0-9/+=]{0,39}[A-Z])(?=[A-Za-z0-9/+=]{0,39}[a-z])(?=[A-Za-z0-9/+=]{0,39}[0-9])[A-Za-z0-9/+=]{40}/
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
rule: "github-token",
|
|
901
|
+
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
// `xoxc-`/`xoxd-` are the user and refresh tokens, `xoxb`/`xoxp` the bot and
|
|
905
|
+
// user tokens; the whole Slack family shares one prefix shape.
|
|
906
|
+
rule: "slack-token",
|
|
907
|
+
pattern: /xox[baprsdc]-[0-9A-Za-z-]{10,250}/
|
|
908
|
+
},
|
|
909
|
+
{
|
|
910
|
+
// Prefix-only vendors. Each prefix below is documented, globally unique and
|
|
911
|
+
// cannot collide with an ordinary identifier, which is what makes a bare
|
|
912
|
+
// prefix rule cheap and safe: `glpat-` (GitLab), `hvs.`/`s.` (Vault service
|
|
913
|
+
// and batch tokens), `whsec_` (Stripe webhook secret), `dckr_pat_` (Docker
|
|
914
|
+
// Hub), `hf_` (HuggingFace), `ya29.` (Google OAuth access token).
|
|
915
|
+
rule: "known-vendor-token-prefix",
|
|
916
|
+
pattern: /\b(?:glpat-[A-Za-z0-9_-]{20,64}|hvs\.[A-Za-z0-9_-]{20,128}|s\.[A-Za-z0-9]{20,64}|whsec_[A-Za-z0-9]{20,64}|dckr_pat_[A-Za-z0-9_-]{20,64}|hf_[A-Za-z0-9]{30,64}|ya29\.[A-Za-z0-9_-]{20,128})/
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
rule: "slack-webhook-url",
|
|
920
|
+
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
rule: "stripe-secret-key",
|
|
924
|
+
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
rule: "google-api-key",
|
|
928
|
+
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
929
|
+
},
|
|
930
|
+
{
|
|
931
|
+
rule: "google-oauth-client-secret",
|
|
932
|
+
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
933
|
+
},
|
|
934
|
+
{
|
|
935
|
+
rule: "openai-anthropic-key",
|
|
936
|
+
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
rule: "npm-token",
|
|
940
|
+
pattern: /npm_[A-Za-z0-9]{36}/
|
|
941
|
+
},
|
|
942
|
+
{
|
|
943
|
+
rule: "pypi-token",
|
|
944
|
+
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
rule: "sendgrid-key",
|
|
948
|
+
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
rule: "twilio-api-key",
|
|
952
|
+
pattern: /SK[0-9a-fA-F]{32}/
|
|
953
|
+
},
|
|
954
|
+
{
|
|
955
|
+
rule: "digitalocean-token",
|
|
956
|
+
pattern: /dop_v1_[0-9a-f]{64}/
|
|
957
|
+
},
|
|
958
|
+
{
|
|
959
|
+
rule: "azure-storage-account-key",
|
|
960
|
+
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
// Scheme-less userinfo: `//user:password@host`.
|
|
964
|
+
//
|
|
965
|
+
// Enumerating schemes is a losing game — `mysqlx`, `svn`, `telnet`, `imap`,
|
|
966
|
+
// `pop3`, `nntp`, `kafka`, `elasticsearch` each leaked a password while the
|
|
967
|
+
// list grew by hand. This rule matches the shape instead, so any scheme
|
|
968
|
+
// (including one invented tomorrow) is covered, provided the `//` is followed
|
|
969
|
+
// immediately by the userinfo: `// see the docs` and a bare reminder comment
|
|
970
|
+
// never match, because a space or the comment wording breaks the shape.
|
|
971
|
+
rule: "uri-credentials",
|
|
972
|
+
pattern: /\/\/[^\s:/@"'`;()<>,]{1,128}:[^\s:/@"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
// The host tail stops at quotes, `;`, `)`, `<`, `>` and `,`: an unbounded
|
|
976
|
+
// `[^\s/]` used to swallow the closing quote and the statement separator of
|
|
977
|
+
// the string the URL was written in.
|
|
978
|
+
rule: "database-uri-credentials",
|
|
979
|
+
pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse|mssql|sqlserver|oracle):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
rule: "basic-auth-url",
|
|
983
|
+
pattern: /(?:https?|ftp|sftp|ssh|git|smtp|smtps|ldap|ldaps):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/i
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
// Quoted credentials in any common key spelling. The value may be as short
|
|
987
|
+
// as one character: `password: "hunter2"` is a credential whatever its
|
|
988
|
+
// entropy, and a length floor was exactly how it survived into `parts`.
|
|
989
|
+
//
|
|
990
|
+
// The key is either bare (`dbPassword:`) or wholly quoted (`"dbPassword":`).
|
|
991
|
+
// There is deliberately no optional quote *before* the separator: that let a
|
|
992
|
+
// previous string's closing quote stand in for the key's, so
|
|
993
|
+
// ``${pass ? " PASS" : " FAIL"}`` had `" FAIL"` masked as if `PASS` were
|
|
994
|
+
// the key name.
|
|
995
|
+
rule: "credential-assignment",
|
|
996
|
+
pattern: new RegExp(
|
|
997
|
+
`(?:["']${STRONG_CREDENTIAL_NAME}["']|${STRONG_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{1,512})\\1`,
|
|
998
|
+
"i"
|
|
999
|
+
),
|
|
1000
|
+
valueGroup: 2
|
|
1001
|
+
},
|
|
1002
|
+
{
|
|
1003
|
+
// `pass`, `id-token` and `credentials` are ambiguous on their own, so they
|
|
1004
|
+
// are accepted only against a quoted value long enough to be a credential.
|
|
1005
|
+
rule: "credential-assignment-weak",
|
|
1006
|
+
pattern: new RegExp(
|
|
1007
|
+
`(?:["']${WEAK_CREDENTIAL_NAME}["']|${WEAK_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{8,512})\\1`,
|
|
1008
|
+
"i"
|
|
1009
|
+
),
|
|
1010
|
+
valueGroup: 2
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
// The unquoted twin for YAML, docker-compose and `.env`: those formats never
|
|
1014
|
+
// quote their values, so the rules above cannot see them. `valueGroup` keeps
|
|
1015
|
+
// `POSTGRES_PASSWORD:` in the output.
|
|
1016
|
+
rule: "credential-assignment-unquoted",
|
|
1017
|
+
pattern: new RegExp(
|
|
1018
|
+
`${UNQUOTED_STRONG_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
|
|
1019
|
+
"i"
|
|
1020
|
+
),
|
|
1021
|
+
valueGroup: 1
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
// `pass: hunter2hunter2` and `credentials: hunter2hunter2`: the ambiguous
|
|
1025
|
+
// names, unquoted, with the length and non-digit floor that keeps
|
|
1026
|
+
// `pass: 3` / `credentials: none` readable.
|
|
1027
|
+
rule: "credential-assignment-unquoted-weak",
|
|
1028
|
+
pattern: new RegExp(
|
|
1029
|
+
`${WEAK_CREDENTIAL_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
|
|
1030
|
+
"i"
|
|
1031
|
+
),
|
|
1032
|
+
valueGroup: 1
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
// `Authorization: Bearer <token>` and `Authorization: Basic <base64>`.
|
|
1036
|
+
//
|
|
1037
|
+
// The scheme is consumed by a lookbehind so the match is the token alone and
|
|
1038
|
+
// needs no `valueGroup`. A lone `Bearer`/`Basic`, and a word after it that
|
|
1039
|
+
// cannot be a credential (`Bearer authentication`), stay readable: the value
|
|
1040
|
+
// must contain a digit, be at least 32 chars, or carry base64 `=` padding.
|
|
1041
|
+
rule: "authorization-header",
|
|
1042
|
+
pattern: /(?<=(?:Bearer|Basic)\s{1,8})(?:[A-Za-z0-9_.+/-]{0,31}[0-9][A-Za-z0-9_.+/-]{0,1024}|[A-Za-z0-9_.+/-]{32,1024}|[A-Za-z0-9_.+/-]{8,1024}={1,2})(?![A-Za-z0-9_.+/=-])/
|
|
1043
|
+
},
|
|
1044
|
+
{
|
|
1045
|
+
// Broader names than before (`apiKey`, `clientSecret`, `privateKey` were
|
|
1046
|
+
// invisible to it) and value-only masking. Kept alongside the two rules
|
|
1047
|
+
// above because it also matches a *quoted key* followed by an unquoted
|
|
1048
|
+
// value, e.g. `"client_secret": abc123`.
|
|
1049
|
+
rule: "generic-secret-assignment",
|
|
1050
|
+
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["']([^"'\s]{8,256})["']/i,
|
|
1051
|
+
valueGroup: 1
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
1055
|
+
// was therefore invisible to the rule above. Anchored to the line start and
|
|
1056
|
+
// limited to secret-looking names to stay at zero false positives.
|
|
1057
|
+
//
|
|
1058
|
+
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
1059
|
+
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
1060
|
+
// every offset of a long identifier, which made this rule quadratic
|
|
1061
|
+
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
1062
|
+
// The value is matched on its own — `=\s*(value)` — and masked alone.
|
|
1063
|
+
// Anchoring the value to end-of-line (`[^\s#]{6,512}$`) meant ANY trailing
|
|
1064
|
+
// comment or space disabled the rule, so `TOKEN=secret # rotate` and
|
|
1065
|
+
// `TOKEN=secret ` were emitted in clear; a CRLF file disabled it wholesale.
|
|
1066
|
+
rule: "env-secret-assignment",
|
|
1067
|
+
pattern: /^\s{0,64}(?:export\s{1,8})?[A-Za-z0-9_]{0,96}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)[A-Za-z0-9_]{0,96}\s{0,8}=\s{0,8}(?!["'`$<{[\]\s])([^\s#;"'`)]{1,512})/i,
|
|
1068
|
+
valueGroup: 1
|
|
979
1069
|
}
|
|
980
|
-
}
|
|
981
|
-
var DEFAULT_IGNORED_DIRS = [
|
|
982
|
-
"node_modules",
|
|
983
|
-
".git",
|
|
984
|
-
"dist",
|
|
985
|
-
"build",
|
|
986
|
-
".next",
|
|
987
|
-
".nuxt",
|
|
988
|
-
".turbo",
|
|
989
|
-
".cache",
|
|
990
|
-
"coverage",
|
|
991
|
-
"out",
|
|
992
|
-
"snapshot",
|
|
993
|
-
"snapshot.tmp"
|
|
994
1070
|
];
|
|
995
|
-
var
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1071
|
+
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
1072
|
+
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
1073
|
+
|
|
1074
|
+
// src/security/redact.ts
|
|
1075
|
+
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
1076
|
+
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
1077
|
+
function describeValue(value) {
|
|
1078
|
+
if (value === null) return "null";
|
|
1079
|
+
if (Array.isArray(value)) return "an array";
|
|
1080
|
+
if (value instanceof RegExp) return value.toString();
|
|
1081
|
+
if (typeof value === "string") return `the string ${JSON.stringify(value)}`;
|
|
1082
|
+
return `a ${typeof value}`;
|
|
1083
|
+
}
|
|
1084
|
+
function compilePattern(rule, pattern, valueGroup) {
|
|
1085
|
+
if (pattern instanceof RegExp) {
|
|
1086
|
+
const flags = new Set(pattern.flags.replace(/[gy]/g, "").split(""));
|
|
1087
|
+
if (valueGroup !== void 0) flags.add("d");
|
|
1088
|
+
return {
|
|
1089
|
+
rule,
|
|
1090
|
+
pattern: new RegExp(pattern.source, [...flags].join("")),
|
|
1091
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
if (typeof pattern === "string") {
|
|
1095
|
+
try {
|
|
1096
|
+
return {
|
|
1097
|
+
rule,
|
|
1098
|
+
pattern: new RegExp(pattern, valueGroup === void 0 ? "i" : "id"),
|
|
1099
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1100
|
+
};
|
|
1101
|
+
} catch (err) {
|
|
1102
|
+
throw new Error(
|
|
1103
|
+
`Invalid regular expression for secret rule "${rule}": ${err.message}`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
throw new Error(
|
|
1108
|
+
`Secret rule "${rule}" has a pattern that is neither a RegExp nor a string (received ${describeValue(pattern)}).`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
function compileSecretRules(rules) {
|
|
1112
|
+
if (!Array.isArray(rules)) {
|
|
1113
|
+
throw new Error(
|
|
1114
|
+
`secrets.customPatterns must be an array of secret rules (received ${describeValue(rules)}).`
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
let generated = 0;
|
|
1118
|
+
return rules.map((entry, index) => {
|
|
1119
|
+
const where = `secrets.customPatterns[${index}]`;
|
|
1120
|
+
if (entry instanceof RegExp) {
|
|
1121
|
+
generated += 1;
|
|
1122
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1123
|
+
}
|
|
1124
|
+
if (typeof entry === "string") {
|
|
1125
|
+
generated += 1;
|
|
1126
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1127
|
+
}
|
|
1128
|
+
if (typeof entry === "object" && entry !== null) {
|
|
1129
|
+
const candidate = entry;
|
|
1130
|
+
if (typeof candidate.rule !== "string" || candidate.rule.trim() === "") {
|
|
1131
|
+
throw new Error(
|
|
1132
|
+
`${where} is missing \`rule\` (received keys: ${Object.keys(candidate).join(", ") || "none"}). A named rule is { rule: "corp-token", pattern: /CORP-[A-Z0-9]{8}/ }, or pass the RegExp itself.`
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
const valueGroup = candidate.valueGroup;
|
|
1136
|
+
if (valueGroup !== void 0) {
|
|
1137
|
+
if (typeof valueGroup !== "number" || !Number.isInteger(valueGroup) || valueGroup < 0) {
|
|
1138
|
+
throw new Error(
|
|
1139
|
+
`${where}.valueGroup must be a non-negative integer capture-group index (received ${describeValue(valueGroup)}).`
|
|
1140
|
+
);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
if (candidate.pattern === void 0) {
|
|
1144
|
+
throw new Error(
|
|
1145
|
+
`${where} (rule "${candidate.rule}") is missing \`pattern\`. Expected { rule, pattern: RegExp | string }.`
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
return compilePattern(
|
|
1149
|
+
candidate.rule,
|
|
1150
|
+
candidate.pattern,
|
|
1151
|
+
valueGroup
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
`${where} must be a RegExp, a string, or { rule, pattern } (received ${describeValue(entry)}).`
|
|
1156
|
+
);
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
function toGlobal(rule) {
|
|
1160
|
+
const flags = new Set(rule.pattern.flags.replace(/[gy]/g, "").split(""));
|
|
1161
|
+
flags.add("g");
|
|
1162
|
+
if (rule.valueGroup !== void 0) flags.add("d");
|
|
1163
|
+
return new RegExp(rule.pattern.source, [...flags].join(""));
|
|
1164
|
+
}
|
|
1165
|
+
var LINE_BREAK = /(\r\n|\n|\r)/;
|
|
1166
|
+
function scanAndRedactSecrets(content, rules, filePath) {
|
|
1167
|
+
const findings = [];
|
|
1168
|
+
let redacted = content;
|
|
1169
|
+
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
1170
|
+
blockPattern.lastIndex = 0;
|
|
1171
|
+
let match;
|
|
1172
|
+
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
1173
|
+
if (match[0].length === 0) {
|
|
1174
|
+
blockPattern.lastIndex += 1;
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
const line = redacted.slice(0, match.index).split("\n").length;
|
|
1178
|
+
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
1179
|
+
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
1180
|
+
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
1181
|
+
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const compiled = rules.map((rule) => ({
|
|
1185
|
+
rule: rule.rule,
|
|
1186
|
+
pattern: toGlobal(rule),
|
|
1187
|
+
...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
|
|
1188
|
+
}));
|
|
1189
|
+
const segments = redacted.split(LINE_BREAK);
|
|
1190
|
+
let lineNumber = 1;
|
|
1191
|
+
for (let index = 0; index < segments.length; index += 2) {
|
|
1192
|
+
const line = segments[index];
|
|
1193
|
+
if (line.length > 0) {
|
|
1194
|
+
const spans = [];
|
|
1195
|
+
const matchedRules = /* @__PURE__ */ new Set();
|
|
1196
|
+
for (const { rule, pattern, valueGroup } of compiled) {
|
|
1197
|
+
pattern.lastIndex = 0;
|
|
1198
|
+
let match;
|
|
1199
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
1200
|
+
if (match[0].length === 0) {
|
|
1201
|
+
pattern.lastIndex += 1;
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
|
|
1205
|
+
spans.push(
|
|
1206
|
+
group ? { start: group[0], end: group[1], rule } : { start: match.index, end: match.index + match[0].length, rule }
|
|
1207
|
+
);
|
|
1208
|
+
matchedRules.add(rule);
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (spans.length > 0) {
|
|
1212
|
+
for (const rule of matchedRules) {
|
|
1213
|
+
findings.push({ rule, line: lineNumber, path: filePath });
|
|
1214
|
+
}
|
|
1215
|
+
segments[index] = applySpans(line, spans);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
if (index + 1 < segments.length) lineNumber += 1;
|
|
1219
|
+
}
|
|
1220
|
+
return { redacted: segments.join(""), findings };
|
|
1221
|
+
}
|
|
1222
|
+
var TOKEN_CHARS_LEFT = /[A-Za-z0-9_+/-]/;
|
|
1223
|
+
var TOKEN_CHARS_RIGHT = /[A-Za-z0-9_+/=-]/;
|
|
1224
|
+
function expandOverToken(line, start, end) {
|
|
1225
|
+
let left = start;
|
|
1226
|
+
let right = end;
|
|
1227
|
+
while (left > 0 && TOKEN_CHARS_LEFT.test(line[left - 1])) left -= 1;
|
|
1228
|
+
while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
|
|
1229
|
+
return { start: left, end: right };
|
|
1230
|
+
}
|
|
1231
|
+
function applySpans(line, spans) {
|
|
1232
|
+
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1233
|
+
const merged = [];
|
|
1234
|
+
for (const span of ordered) {
|
|
1235
|
+
const clampedStart = Math.max(0, Math.min(span.start, line.length));
|
|
1236
|
+
const clampedEnd = Math.max(clampedStart, Math.min(span.end, line.length));
|
|
1237
|
+
const { start, end } = expandOverToken(line, clampedStart, clampedEnd);
|
|
1238
|
+
const last = merged[merged.length - 1];
|
|
1239
|
+
if (last && start <= last.end) last.end = Math.max(last.end, end);
|
|
1240
|
+
else merged.push({ start, end });
|
|
1241
|
+
}
|
|
1242
|
+
let out = "";
|
|
1243
|
+
let cursor = 0;
|
|
1244
|
+
for (const span of merged) {
|
|
1245
|
+
out += line.slice(cursor, span.start);
|
|
1246
|
+
out += REDACTION_MARKER;
|
|
1247
|
+
cursor = span.end;
|
|
1248
|
+
}
|
|
1249
|
+
out += line.slice(cursor);
|
|
1250
|
+
return out;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// src/scanner/files.ts
|
|
1254
|
+
var PRESET_IGNORES = new Set(
|
|
1255
|
+
PRESETS.flatMap((preset) => preset.exclude ?? [])
|
|
1256
|
+
);
|
|
1257
|
+
function readLinkTarget(linkPath, fallback) {
|
|
1258
|
+
try {
|
|
1259
|
+
return fs2.readlinkSync(linkPath);
|
|
1260
|
+
} catch {
|
|
1261
|
+
return fallback;
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
function isInside(root, candidate) {
|
|
1265
|
+
const relative5 = path2.relative(root, candidate);
|
|
1266
|
+
return relative5 === "" || !relative5.startsWith(`..${path2.sep}`) && relative5 !== ".." && !path2.isAbsolute(relative5);
|
|
1267
|
+
}
|
|
1268
|
+
function getGitInfo(cwd, fallbackBranch = "main") {
|
|
1269
|
+
try {
|
|
1270
|
+
const reported = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
|
|
1271
|
+
cwd,
|
|
1272
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1273
|
+
encoding: "utf8"
|
|
1274
|
+
}).trim();
|
|
1275
|
+
const branch = !reported || reported === "HEAD" ? fallbackBranch : reported;
|
|
1276
|
+
const commit = childProcess.execSync("git rev-parse HEAD", {
|
|
1277
|
+
cwd,
|
|
1278
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1279
|
+
encoding: "utf8"
|
|
1280
|
+
}).trim() || "unknown";
|
|
1281
|
+
const status = childProcess.execSync("git status --porcelain", {
|
|
1282
|
+
cwd,
|
|
1283
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1284
|
+
encoding: "utf8"
|
|
1285
|
+
}).trim();
|
|
1286
|
+
return { branch, commit, isDirty: status.length > 0 };
|
|
1287
|
+
} catch {
|
|
1288
|
+
return { branch: fallbackBranch, commit: "unknown", isDirty: false };
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
var DEFAULT_IGNORED_DIRS = [
|
|
1292
|
+
"node_modules",
|
|
1293
|
+
".git",
|
|
1294
|
+
"dist",
|
|
1295
|
+
"build",
|
|
1296
|
+
".next",
|
|
1297
|
+
".nuxt",
|
|
1298
|
+
".turbo",
|
|
1299
|
+
".cache",
|
|
1300
|
+
"coverage",
|
|
1301
|
+
"out",
|
|
1302
|
+
"snapshot",
|
|
1303
|
+
"snapshot.tmp"
|
|
1304
|
+
];
|
|
1305
|
+
var DEFAULT_IGNORED_FILES = [
|
|
1306
|
+
"package-lock.json",
|
|
1307
|
+
"yarn.lock",
|
|
1308
|
+
"pnpm-lock.yaml",
|
|
1309
|
+
"bun.lockb",
|
|
1000
1310
|
"composer.lock"
|
|
1001
1311
|
];
|
|
1002
|
-
var SENSITIVE_BASENAME = /^(?:\.env(?:[._-][A-Za-z0-9_-]*)*|\.env(?:backup|bak|old|orig|copy|save)|\.envrc|\.npmrc(?:\.[A-Za-z0-9_-]+)?|\.yarnrc(?:\.yml)?|\.netrc(?:\.[A-Za-z0-9_-]+)?|\.git-credentials|\.htpasswd|\.pgpass|id_[a-z0-9_]+(?:\.[A-Za-z0-9_-]+)?|credentials(?:\.json)?|service[-_]?account(?:\.[a-z0-9_-]+)*\.json|secrets?\.(?:json|ya?ml))$/i;
|
|
1312
|
+
var SENSITIVE_BASENAME = /^(?:\.env(?:[._-][A-Za-z0-9_-]*)*|\.env(?:backup|bak|old|orig|copy|save)|\.envrc|\.npmrc(?:\.[A-Za-z0-9_-]+)?|\.yarnrc(?:\.yml)?|\.netrc(?:\.[A-Za-z0-9_-]+)?|\.git-credentials|\.htpasswd|\.pgpass|\.vault-token|vault[-_]agent[-_]token|id_[a-z0-9_]+(?:\.[A-Za-z0-9_-]+)?|credentials(?:\.(?:json|ya?ml|toml))?|service[-_]?account(?:\.[a-z0-9_-]+)*\.json|secrets?\.(?:json|ya?ml|toml)|env|[A-Za-z0-9_-]{1,64}\.env)$/i;
|
|
1003
1313
|
var SENSITIVE_PATH = /(?:^|\/)\.(?:aws|ssh|gnupg)\/|(?:^|\/)\.docker\/config\.json$|(?:^|\/)\.kube\/config$/i;
|
|
1004
1314
|
var SENSITIVE_EXT = /\.(?:pem|key|p12|pfx|jks|keystore|ppk|asc|kdbx|tfstate|tfvars)$/i;
|
|
1005
1315
|
var TEMPLATE_SUFFIX = /\.(?:example|sample|template|dist)(?=$|[.])/i;
|
|
1316
|
+
var BACKUP_SUFFIX = /(?:~|\.(?:bak|backup|old|orig|save|copy|swp|swo|tmp|part))$/i;
|
|
1317
|
+
function normaliseName(name) {
|
|
1318
|
+
let normalised = name.trim().replace(/[. ]+$/, "");
|
|
1319
|
+
let previous = "";
|
|
1320
|
+
while (previous !== normalised) {
|
|
1321
|
+
previous = normalised;
|
|
1322
|
+
normalised = normalised.replace(BACKUP_SUFFIX, "");
|
|
1323
|
+
}
|
|
1324
|
+
return normalised;
|
|
1325
|
+
}
|
|
1006
1326
|
var matchesSensitiveList = (posixPath, base) => SENSITIVE_BASENAME.test(base) || SENSITIVE_PATH.test(posixPath) || SENSITIVE_EXT.test(base);
|
|
1007
1327
|
function isSensitivePath(relativePath) {
|
|
1008
1328
|
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1009
|
-
const
|
|
1329
|
+
const raw = posixPath.split("/").pop() ?? posixPath;
|
|
1330
|
+
const base = normaliseName(raw);
|
|
1010
1331
|
if (TEMPLATE_SUFFIX.test(base)) {
|
|
1011
1332
|
const stripped = base.replace(new RegExp(TEMPLATE_SUFFIX.source, "gi"), "");
|
|
1012
1333
|
if (matchesSensitiveList(posixPath, stripped)) return false;
|
|
1013
1334
|
}
|
|
1014
1335
|
return matchesSensitiveList(posixPath, base);
|
|
1015
1336
|
}
|
|
1337
|
+
var activeSecretRules = null;
|
|
1338
|
+
function setActiveSecretRules(rules) {
|
|
1339
|
+
activeSecretRules = rules === null ? null : [...rules];
|
|
1340
|
+
}
|
|
1341
|
+
function pathScanRules(extraRules) {
|
|
1342
|
+
const fallback = extraRules ?? activeSecretRules ?? [];
|
|
1343
|
+
return [...DEFAULT_SECRET_PATTERNS, ...fallback];
|
|
1344
|
+
}
|
|
1345
|
+
function findSecretInPath(relativePath, extraRules) {
|
|
1346
|
+
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1347
|
+
const spans = [];
|
|
1348
|
+
const rules = [];
|
|
1349
|
+
for (const { rule, pattern } of pathScanRules(extraRules)) {
|
|
1350
|
+
const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
|
|
1351
|
+
let match;
|
|
1352
|
+
while ((match = scan.exec(posixPath)) !== null) {
|
|
1353
|
+
if (match[0].length === 0) {
|
|
1354
|
+
scan.lastIndex += 1;
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
spans.push({ start: match.index, end: match.index + match[0].length });
|
|
1358
|
+
if (!rules.includes(rule)) rules.push(rule);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
if (spans.length === 0) return null;
|
|
1362
|
+
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1363
|
+
const merged = [];
|
|
1364
|
+
for (const span of spans) {
|
|
1365
|
+
const last = merged[merged.length - 1];
|
|
1366
|
+
if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
|
|
1367
|
+
else merged.push({ start: span.start, end: span.end });
|
|
1368
|
+
}
|
|
1369
|
+
let out = "";
|
|
1370
|
+
let cursor = 0;
|
|
1371
|
+
for (const span of merged) {
|
|
1372
|
+
out += posixPath.slice(cursor, span.start);
|
|
1373
|
+
out += REDACTION_MARKER;
|
|
1374
|
+
cursor = span.end;
|
|
1375
|
+
}
|
|
1376
|
+
out += posixPath.slice(cursor);
|
|
1377
|
+
return { rules, masked: out };
|
|
1378
|
+
}
|
|
1379
|
+
function maskSkipPaths(records, extraRules) {
|
|
1380
|
+
return records.map((record) => {
|
|
1381
|
+
const hit = findSecretInPath(record.path, extraRules);
|
|
1382
|
+
return hit ? { ...record, path: hit.masked } : record;
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1016
1385
|
function globToRegExp(glob) {
|
|
1017
1386
|
const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1018
1387
|
const anchored = normalized.startsWith("/");
|
|
@@ -1090,10 +1459,10 @@ function matchesExclude(relativePath, pattern) {
|
|
|
1090
1459
|
return posixPath === cleaned || posixPath.startsWith(`${cleaned}/`) || posixPath.includes(`/${cleaned}/`) || posixPath.endsWith(`/${cleaned}`);
|
|
1091
1460
|
}
|
|
1092
1461
|
function readGitignoreMatcher(dir) {
|
|
1093
|
-
const gitignorePath =
|
|
1462
|
+
const gitignorePath = path2.join(dir, ".gitignore");
|
|
1094
1463
|
let raw;
|
|
1095
1464
|
try {
|
|
1096
|
-
raw =
|
|
1465
|
+
raw = fs2.readFileSync(gitignorePath, "utf8");
|
|
1097
1466
|
} catch {
|
|
1098
1467
|
return () => false;
|
|
1099
1468
|
}
|
|
@@ -1133,12 +1502,120 @@ function defaultIgnoreReason(relativePath) {
|
|
|
1133
1502
|
}
|
|
1134
1503
|
return null;
|
|
1135
1504
|
}
|
|
1505
|
+
function listIgnoredPaths(cwd) {
|
|
1506
|
+
try {
|
|
1507
|
+
const stdout = childProcess.execSync(
|
|
1508
|
+
"git ls-files -z --others --ignored --exclude-standard --directory",
|
|
1509
|
+
{
|
|
1510
|
+
cwd,
|
|
1511
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1512
|
+
encoding: "utf8",
|
|
1513
|
+
maxBuffer: 32 * 1024 * 1024
|
|
1514
|
+
}
|
|
1515
|
+
);
|
|
1516
|
+
return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1517
|
+
} catch {
|
|
1518
|
+
return [];
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
|
|
1522
|
+
function isToolOwnedDirName(name) {
|
|
1523
|
+
return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
|
|
1524
|
+
}
|
|
1525
|
+
function hasSnapshotMarker(target) {
|
|
1526
|
+
try {
|
|
1527
|
+
return fs2.statSync(target).isDirectory() && fs2.existsSync(path2.join(target, ".codebase-snapshot"));
|
|
1528
|
+
} catch {
|
|
1529
|
+
return false;
|
|
1530
|
+
}
|
|
1531
|
+
}
|
|
1532
|
+
function makeToolOwnedDecider(cwd) {
|
|
1533
|
+
const markers = /* @__PURE__ */ new Map();
|
|
1534
|
+
return (relativePath) => {
|
|
1535
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
1536
|
+
for (let depth = 1; depth < parts.length; depth += 1) {
|
|
1537
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
1538
|
+
if (depth === 1 && isToolOwnedDirName(prefix)) return true;
|
|
1539
|
+
let hasMarker = markers.get(prefix);
|
|
1540
|
+
if (hasMarker === void 0) {
|
|
1541
|
+
hasMarker = fs2.existsSync(path2.join(cwd, prefix, ".codebase-snapshot"));
|
|
1542
|
+
markers.set(prefix, hasMarker);
|
|
1543
|
+
}
|
|
1544
|
+
if (hasMarker) return true;
|
|
1545
|
+
}
|
|
1546
|
+
return false;
|
|
1547
|
+
};
|
|
1548
|
+
}
|
|
1549
|
+
function makeLinkProbe() {
|
|
1550
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1551
|
+
return (absolutePath) => {
|
|
1552
|
+
let known = cache.get(absolutePath);
|
|
1553
|
+
if (known === void 0) {
|
|
1554
|
+
try {
|
|
1555
|
+
known = fs2.lstatSync(absolutePath).isSymbolicLink();
|
|
1556
|
+
} catch {
|
|
1557
|
+
known = false;
|
|
1558
|
+
}
|
|
1559
|
+
cache.set(absolutePath, known);
|
|
1560
|
+
}
|
|
1561
|
+
return known;
|
|
1562
|
+
};
|
|
1563
|
+
}
|
|
1564
|
+
function dropLinkedDirectories(cwd, candidates, skipped) {
|
|
1565
|
+
const isLink = makeLinkProbe();
|
|
1566
|
+
let rootReal = cwd;
|
|
1567
|
+
try {
|
|
1568
|
+
rootReal = fs2.realpathSync(cwd);
|
|
1569
|
+
} catch {
|
|
1570
|
+
}
|
|
1571
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1572
|
+
const kept = [];
|
|
1573
|
+
for (const candidate of candidates) {
|
|
1574
|
+
const parts = candidate.split("/");
|
|
1575
|
+
let linkedDir = null;
|
|
1576
|
+
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
1577
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
1578
|
+
if (isLink(path2.join(cwd, prefix))) {
|
|
1579
|
+
linkedDir = prefix;
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
if (linkedDir === null) {
|
|
1584
|
+
kept.push(candidate);
|
|
1585
|
+
continue;
|
|
1586
|
+
}
|
|
1587
|
+
if (reported.has(linkedDir)) continue;
|
|
1588
|
+
reported.add(linkedDir);
|
|
1589
|
+
const absolute = path2.join(cwd, linkedDir);
|
|
1590
|
+
let targetReal = absolute;
|
|
1591
|
+
try {
|
|
1592
|
+
targetReal = fs2.realpathSync(absolute);
|
|
1593
|
+
} catch {
|
|
1594
|
+
}
|
|
1595
|
+
const target = readLinkTarget(absolute, targetReal);
|
|
1596
|
+
const what = linkedDir === candidate ? "symbolic link" : "directory link";
|
|
1597
|
+
skipped.push(
|
|
1598
|
+
isInside(rootReal, targetReal) ? {
|
|
1599
|
+
path: linkedDir,
|
|
1600
|
+
reason: "symlink",
|
|
1601
|
+
detail: `${what} not followed (target: ${target})`
|
|
1602
|
+
} : {
|
|
1603
|
+
path: linkedDir,
|
|
1604
|
+
reason: "outside-root",
|
|
1605
|
+
detail: `link target resolves outside the scanned root (target: ${target}); not followed`
|
|
1606
|
+
}
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
return kept;
|
|
1610
|
+
}
|
|
1136
1611
|
function collectFiles(cwd, options = {}) {
|
|
1137
1612
|
const includePatterns = options.includePatterns ?? [];
|
|
1138
1613
|
const excludePatterns = options.excludePatterns ?? [];
|
|
1139
1614
|
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
1615
|
+
const pathRules = options.secretRules;
|
|
1616
|
+
const isToolOwned = makeToolOwnedDecider(cwd);
|
|
1140
1617
|
let candidates;
|
|
1141
|
-
|
|
1618
|
+
const skipped = [];
|
|
1142
1619
|
try {
|
|
1143
1620
|
const stdout = childProcess.execSync(
|
|
1144
1621
|
"git ls-files -z --cached --others --exclude-standard",
|
|
@@ -1150,13 +1627,33 @@ function collectFiles(cwd, options = {}) {
|
|
|
1150
1627
|
}
|
|
1151
1628
|
);
|
|
1152
1629
|
candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1630
|
+
candidates = dropLinkedDirectories(cwd, candidates, skipped);
|
|
1631
|
+
const known = new Set(candidates);
|
|
1632
|
+
for (const ignoredPath of listIgnoredPaths(cwd)) {
|
|
1633
|
+
const trimmed = ignoredPath.replace(/\/+$/, "");
|
|
1634
|
+
if (known.has(trimmed) || known.has(ignoredPath)) continue;
|
|
1635
|
+
if (isToolOwned(trimmed)) continue;
|
|
1636
|
+
if (isSensitivePath(trimmed)) {
|
|
1637
|
+
skipped.push({
|
|
1638
|
+
path: trimmed,
|
|
1639
|
+
reason: "sensitive-file",
|
|
1640
|
+
detail: "credential-bearing file, ignored by .gitignore; enable includeSensitiveFiles to emit it"
|
|
1641
|
+
});
|
|
1642
|
+
} else {
|
|
1643
|
+
skipped.push({
|
|
1644
|
+
path: trimmed,
|
|
1645
|
+
reason: "gitignore",
|
|
1646
|
+
detail: ignoredPath.endsWith("/") ? "ignored by .gitignore (directory, not descended)" : "ignored by .gitignore"
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1153
1650
|
} catch {
|
|
1154
|
-
const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set());
|
|
1651
|
+
const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), true);
|
|
1155
1652
|
candidates = walked.files;
|
|
1156
|
-
|
|
1653
|
+
skipped.push(...walked.skipped);
|
|
1157
1654
|
}
|
|
1158
|
-
const skipped = [...walkSkips];
|
|
1159
1655
|
const files = candidates.filter((file) => {
|
|
1656
|
+
if (isToolOwned(file)) return false;
|
|
1160
1657
|
const ignored = defaultIgnoreReason(file);
|
|
1161
1658
|
if (ignored) {
|
|
1162
1659
|
skipped.push({ path: file, reason: "default-ignore", detail: ignored });
|
|
@@ -1172,10 +1669,11 @@ function collectFiles(cwd, options = {}) {
|
|
|
1172
1669
|
}
|
|
1173
1670
|
for (const pattern of excludePatterns) {
|
|
1174
1671
|
if (matchesExclude(file, pattern)) {
|
|
1672
|
+
const fromPreset = PRESET_IGNORES.has(pattern);
|
|
1175
1673
|
skipped.push({
|
|
1176
1674
|
path: file,
|
|
1177
|
-
reason: "excluded",
|
|
1178
|
-
detail: `matches exclude pattern "${pattern}"`
|
|
1675
|
+
reason: fromPreset ? "default-ignore" : "excluded",
|
|
1676
|
+
detail: fromPreset ? `preset ignore: "${pattern}"` : `matches exclude pattern "${pattern}"`
|
|
1179
1677
|
});
|
|
1180
1678
|
return false;
|
|
1181
1679
|
}
|
|
@@ -1188,56 +1686,119 @@ function collectFiles(cwd, options = {}) {
|
|
|
1188
1686
|
});
|
|
1189
1687
|
return false;
|
|
1190
1688
|
}
|
|
1689
|
+
const secret = findSecretInPath(file, pathRules);
|
|
1690
|
+
if (secret) {
|
|
1691
|
+
skipped.push({
|
|
1692
|
+
path: secret.masked,
|
|
1693
|
+
reason: "sensitive-path",
|
|
1694
|
+
detail: `path matches secret rule "${secret.rules.join('", "')}"; the file was skipped and its path is masked here because paths are emitted into every artifact`
|
|
1695
|
+
});
|
|
1696
|
+
return false;
|
|
1697
|
+
}
|
|
1191
1698
|
return true;
|
|
1192
1699
|
}).sort();
|
|
1193
|
-
return { files, skipped };
|
|
1700
|
+
return { files, skipped: maskSkipPaths(skipped, pathRules) };
|
|
1194
1701
|
}
|
|
1195
|
-
function
|
|
1196
|
-
const results = { files: [], skipped: [] };
|
|
1197
|
-
let realDir;
|
|
1702
|
+
function rootRealFor(root) {
|
|
1198
1703
|
try {
|
|
1199
|
-
|
|
1704
|
+
return fs2.realpathSync(root);
|
|
1200
1705
|
} catch {
|
|
1201
|
-
|
|
1706
|
+
return root;
|
|
1202
1707
|
}
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
const
|
|
1708
|
+
}
|
|
1709
|
+
function walkDirectory(dir, baseDir, rootReal, visited, isRoot = false) {
|
|
1710
|
+
const results = { files: [], skipped: [] };
|
|
1711
|
+
if (visited.has(dir)) return results;
|
|
1712
|
+
visited.add(dir);
|
|
1713
|
+
const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
|
|
1206
1714
|
let entries;
|
|
1207
1715
|
try {
|
|
1208
|
-
entries =
|
|
1716
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
1209
1717
|
} catch {
|
|
1210
|
-
results.skipped.push({
|
|
1718
|
+
results.skipped.push({
|
|
1719
|
+
path: path2.relative(baseDir, dir).replace(/\\/g, "/") || ".",
|
|
1720
|
+
reason: "read-error",
|
|
1721
|
+
detail: "directory unreadable"
|
|
1722
|
+
});
|
|
1211
1723
|
return results;
|
|
1212
1724
|
}
|
|
1213
1725
|
for (const entry of entries) {
|
|
1214
|
-
|
|
1215
|
-
const
|
|
1216
|
-
|
|
1217
|
-
if (
|
|
1726
|
+
const fullPath = path2.join(dir, entry.name);
|
|
1727
|
+
const relPath = path2.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
1728
|
+
if (isToolOwnedDirName(entry.name)) continue;
|
|
1729
|
+
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
|
|
1730
|
+
results.skipped.push({
|
|
1731
|
+
path: relPath,
|
|
1732
|
+
reason: "default-ignore",
|
|
1733
|
+
detail: `built-in ignore: directory "${entry.name}" (not descended)`
|
|
1734
|
+
});
|
|
1735
|
+
continue;
|
|
1736
|
+
}
|
|
1737
|
+
if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
|
|
1738
|
+
results.skipped.push({
|
|
1739
|
+
path: relPath,
|
|
1740
|
+
reason: "default-ignore",
|
|
1741
|
+
detail: "built-in ignore: filename"
|
|
1742
|
+
});
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
if (ignoredByGitignore(relPath)) {
|
|
1746
|
+
if (!hasSnapshotMarker(fullPath)) {
|
|
1747
|
+
results.skipped.push({
|
|
1748
|
+
path: relPath,
|
|
1749
|
+
reason: isSensitivePath(relPath) ? "sensitive-file" : "gitignore",
|
|
1750
|
+
detail: "ignored by .gitignore"
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1218
1755
|
let isDirectory = entry.isDirectory();
|
|
1219
1756
|
let isFile = entry.isFile();
|
|
1220
1757
|
if (entry.isSymbolicLink()) {
|
|
1758
|
+
let targetReal;
|
|
1221
1759
|
try {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1760
|
+
targetReal = fs2.realpathSync(fullPath);
|
|
1761
|
+
} catch {
|
|
1762
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
let stats;
|
|
1766
|
+
try {
|
|
1767
|
+
stats = fs2.statSync(fullPath);
|
|
1768
|
+
} catch {
|
|
1769
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
|
|
1770
|
+
continue;
|
|
1771
|
+
}
|
|
1772
|
+
const linkTarget = readLinkTarget(fullPath, targetReal);
|
|
1773
|
+
if (!isInside(rootReal, targetReal)) {
|
|
1774
|
+
results.skipped.push({
|
|
1775
|
+
path: relPath,
|
|
1776
|
+
reason: "outside-root",
|
|
1777
|
+
detail: `link target resolves outside the scanned root (target: ${linkTarget}); not followed`
|
|
1778
|
+
});
|
|
1779
|
+
continue;
|
|
1780
|
+
}
|
|
1781
|
+
if (stats.isDirectory()) {
|
|
1229
1782
|
results.skipped.push({
|
|
1230
1783
|
path: relPath,
|
|
1231
1784
|
reason: "symlink",
|
|
1232
|
-
detail:
|
|
1785
|
+
detail: `directory link not followed (target: ${linkTarget})`
|
|
1786
|
+
});
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
if (!stats.isFile()) {
|
|
1790
|
+
results.skipped.push({
|
|
1791
|
+
path: relPath,
|
|
1792
|
+
reason: "symlink",
|
|
1793
|
+
detail: `link to neither a file nor a directory (target: ${linkTarget})`
|
|
1233
1794
|
});
|
|
1234
|
-
} catch {
|
|
1235
|
-
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1236
1795
|
continue;
|
|
1237
1796
|
}
|
|
1797
|
+
isDirectory = false;
|
|
1798
|
+
isFile = true;
|
|
1238
1799
|
}
|
|
1239
1800
|
if (isDirectory) {
|
|
1240
|
-
const nested = walkDirectory(fullPath, baseDir, visited);
|
|
1801
|
+
const nested = walkDirectory(fullPath, baseDir, rootReal, visited);
|
|
1241
1802
|
results.files.push(...nested.files);
|
|
1242
1803
|
results.skipped.push(...nested.skipped);
|
|
1243
1804
|
} else if (isFile) {
|
|
@@ -1247,196 +1808,141 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1247
1808
|
return results;
|
|
1248
1809
|
}
|
|
1249
1810
|
|
|
1250
|
-
// src/
|
|
1251
|
-
|
|
1252
|
-
{
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
rule: "aws-secret-access-key",
|
|
1266
|
-
pattern: /(?:aws_secret_access_key|aws_secret_key|secret_access_key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']?/i
|
|
1267
|
-
},
|
|
1268
|
-
{
|
|
1269
|
-
rule: "github-token",
|
|
1270
|
-
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
1271
|
-
},
|
|
1272
|
-
{
|
|
1273
|
-
rule: "slack-token",
|
|
1274
|
-
pattern: /xox[baprs]-[0-9A-Za-z-]{10,250}/
|
|
1275
|
-
},
|
|
1276
|
-
{
|
|
1277
|
-
rule: "slack-webhook-url",
|
|
1278
|
-
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
1279
|
-
},
|
|
1280
|
-
{
|
|
1281
|
-
rule: "stripe-secret-key",
|
|
1282
|
-
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
1283
|
-
},
|
|
1284
|
-
{
|
|
1285
|
-
rule: "google-api-key",
|
|
1286
|
-
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
1287
|
-
},
|
|
1288
|
-
{
|
|
1289
|
-
rule: "google-oauth-client-secret",
|
|
1290
|
-
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
1291
|
-
},
|
|
1292
|
-
{
|
|
1293
|
-
rule: "openai-anthropic-key",
|
|
1294
|
-
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
1295
|
-
},
|
|
1296
|
-
{
|
|
1297
|
-
rule: "npm-token",
|
|
1298
|
-
pattern: /npm_[A-Za-z0-9]{36}/
|
|
1299
|
-
},
|
|
1300
|
-
{
|
|
1301
|
-
rule: "pypi-token",
|
|
1302
|
-
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
1303
|
-
},
|
|
1304
|
-
{
|
|
1305
|
-
rule: "sendgrid-key",
|
|
1306
|
-
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
1307
|
-
},
|
|
1308
|
-
{
|
|
1309
|
-
rule: "twilio-api-key",
|
|
1310
|
-
pattern: /SK[0-9a-fA-F]{32}/
|
|
1311
|
-
},
|
|
1312
|
-
{
|
|
1313
|
-
rule: "digitalocean-token",
|
|
1314
|
-
pattern: /dop_v1_[0-9a-f]{64}/
|
|
1315
|
-
},
|
|
1316
|
-
{
|
|
1317
|
-
rule: "azure-storage-account-key",
|
|
1318
|
-
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
1319
|
-
},
|
|
1320
|
-
{
|
|
1321
|
-
rule: "database-uri-credentials",
|
|
1322
|
-
pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/
|
|
1323
|
-
},
|
|
1324
|
-
{
|
|
1325
|
-
rule: "basic-auth-url",
|
|
1326
|
-
pattern: /(?:https?|ftp|sftp|ssh|git):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/i
|
|
1327
|
-
},
|
|
1328
|
-
{
|
|
1329
|
-
rule: "generic-secret-assignment",
|
|
1330
|
-
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["'][^"'\s]{8,256}["']/i
|
|
1331
|
-
},
|
|
1332
|
-
{
|
|
1333
|
-
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
1334
|
-
// was therefore invisible to the rule above. Anchored to the line start and
|
|
1335
|
-
// limited to secret-looking names to stay at zero false positives.
|
|
1336
|
-
//
|
|
1337
|
-
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
1338
|
-
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
1339
|
-
// every offset of a long identifier, which made this rule quadratic
|
|
1340
|
-
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
1341
|
-
rule: "env-secret-assignment",
|
|
1342
|
-
pattern: /^\s{0,64}(?:export\s{1,8})?[A-Za-z0-9_]{0,96}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIAL)[A-Za-z0-9_]{0,96}\s{0,8}=\s{0,8}(?![\s$<{[])[^\s#]{6,512}$/i
|
|
1811
|
+
// src/diff.ts
|
|
1812
|
+
function side(manifest) {
|
|
1813
|
+
return {
|
|
1814
|
+
generatedAt: manifest.generatedAt,
|
|
1815
|
+
fingerprint: manifest.fingerprint,
|
|
1816
|
+
fileCount: manifest.counts.totalFiles,
|
|
1817
|
+
partsCount: manifest.counts.partsCount
|
|
1818
|
+
};
|
|
1819
|
+
}
|
|
1820
|
+
function hashMap(manifest) {
|
|
1821
|
+
const map = /* @__PURE__ */ new Map();
|
|
1822
|
+
for (const file of manifest.files) {
|
|
1823
|
+
if (file && typeof file.relativePath === "string") {
|
|
1824
|
+
map.set(file.relativePath, file.hashSha256);
|
|
1825
|
+
}
|
|
1343
1826
|
}
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
if (rule.pattern instanceof RegExp) {
|
|
1354
|
-
return {
|
|
1355
|
-
rule: rule.rule,
|
|
1356
|
-
pattern: new RegExp(rule.pattern.source, rule.pattern.flags.replace(/[gy]/g, ""))
|
|
1357
|
-
};
|
|
1827
|
+
return map;
|
|
1828
|
+
}
|
|
1829
|
+
function manifestProblem(value) {
|
|
1830
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
|
|
1831
|
+
const candidate = value;
|
|
1832
|
+
if (!Array.isArray(candidate.files)) return 'no "files" array';
|
|
1833
|
+
for (const entry of candidate.files) {
|
|
1834
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1835
|
+
return 'a "files" entry is not an object';
|
|
1358
1836
|
}
|
|
1359
|
-
if (typeof
|
|
1360
|
-
|
|
1361
|
-
return { rule: rule.rule, pattern: new RegExp(rule.pattern, "i") };
|
|
1362
|
-
} catch (err) {
|
|
1363
|
-
throw new Error(
|
|
1364
|
-
`Invalid regular expression for secret rule "${rule.rule}": ${err.message}`
|
|
1365
|
-
);
|
|
1366
|
-
}
|
|
1837
|
+
if (typeof entry.relativePath !== "string") {
|
|
1838
|
+
return 'a "files" entry has no "relativePath"';
|
|
1367
1839
|
}
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1840
|
+
}
|
|
1841
|
+
const counts = candidate.counts;
|
|
1842
|
+
if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
|
|
1843
|
+
return 'no "counts" object';
|
|
1844
|
+
}
|
|
1845
|
+
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
1846
|
+
if (typeof counts[key] !== "number") {
|
|
1847
|
+
return `no numeric "counts.${key}"`;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
|
|
1851
|
+
if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
|
|
1852
|
+
return null;
|
|
1372
1853
|
}
|
|
1373
|
-
function
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1854
|
+
function assertUsableManifest(value, label) {
|
|
1855
|
+
const problem = manifestProblem(value);
|
|
1856
|
+
if (problem) {
|
|
1857
|
+
throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
|
|
1858
|
+
}
|
|
1376
1859
|
}
|
|
1377
|
-
function
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
1392
|
-
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
1393
|
-
}
|
|
1860
|
+
function compareManifests(before, after) {
|
|
1861
|
+
assertUsableManifest(before, "before");
|
|
1862
|
+
assertUsableManifest(after, "after");
|
|
1863
|
+
const left = hashMap(before);
|
|
1864
|
+
const right = hashMap(after);
|
|
1865
|
+
const added = [];
|
|
1866
|
+
const removed = [];
|
|
1867
|
+
const changed = [];
|
|
1868
|
+
let unchanged = 0;
|
|
1869
|
+
for (const [filePath, hash] of right) {
|
|
1870
|
+
const previous = left.get(filePath);
|
|
1871
|
+
if (previous === void 0) added.push(filePath);
|
|
1872
|
+
else if (previous !== hash) changed.push(filePath);
|
|
1873
|
+
else unchanged += 1;
|
|
1394
1874
|
}
|
|
1395
|
-
const
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
const
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
if (match[0].length === 0) {
|
|
1409
|
-
pattern.lastIndex += 1;
|
|
1410
|
-
continue;
|
|
1411
|
-
}
|
|
1412
|
-
spans.push({ start: match.index, end: match.index + match[0].length, rule });
|
|
1413
|
-
matchedRules.add(rule);
|
|
1414
|
-
}
|
|
1415
|
-
}
|
|
1416
|
-
if (spans.length === 0) return line;
|
|
1417
|
-
for (const rule of matchedRules) {
|
|
1418
|
-
findings.push({ rule, line: index + 1, path: filePath });
|
|
1419
|
-
}
|
|
1420
|
-
return applySpans(line, spans);
|
|
1421
|
-
});
|
|
1422
|
-
return { redacted: redactedLines.join("\n"), findings };
|
|
1875
|
+
for (const filePath of left.keys()) {
|
|
1876
|
+
if (!right.has(filePath)) removed.push(filePath);
|
|
1877
|
+
}
|
|
1878
|
+
const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
1879
|
+
const mask = (values) => values.map((value) => findSecretInPath(value)?.masked ?? value);
|
|
1880
|
+
return {
|
|
1881
|
+
before: side(before),
|
|
1882
|
+
after: side(after),
|
|
1883
|
+
added: mask(sort(added)),
|
|
1884
|
+
removed: mask(sort(removed)),
|
|
1885
|
+
changed: mask(sort(changed)),
|
|
1886
|
+
unchanged
|
|
1887
|
+
};
|
|
1423
1888
|
}
|
|
1424
|
-
function
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
const start = Math.max(span.start, cursor);
|
|
1431
|
-
out += line.slice(cursor, start);
|
|
1432
|
-
out += REDACTION_MARKER;
|
|
1433
|
-
cursor = Math.min(span.end, line.length);
|
|
1889
|
+
function loadManifest(target) {
|
|
1890
|
+
let isDirectory;
|
|
1891
|
+
try {
|
|
1892
|
+
isDirectory = fs3.statSync(target).isDirectory();
|
|
1893
|
+
} catch {
|
|
1894
|
+
throw new Error(`No snapshot found at ${target}: the path does not exist.`);
|
|
1434
1895
|
}
|
|
1435
|
-
|
|
1436
|
-
|
|
1896
|
+
const manifestPath = isDirectory ? path3.join(target, "manifest.json") : target;
|
|
1897
|
+
let raw;
|
|
1898
|
+
try {
|
|
1899
|
+
raw = fs3.readFileSync(manifestPath, "utf8");
|
|
1900
|
+
} catch {
|
|
1901
|
+
throw new Error(`Could not read a manifest at ${manifestPath}.`);
|
|
1902
|
+
}
|
|
1903
|
+
let parsed;
|
|
1904
|
+
try {
|
|
1905
|
+
parsed = JSON.parse(raw);
|
|
1906
|
+
} catch (err) {
|
|
1907
|
+
throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
|
|
1908
|
+
}
|
|
1909
|
+
const problem = manifestProblem(parsed);
|
|
1910
|
+
if (problem) {
|
|
1911
|
+
throw new Error(
|
|
1912
|
+
`${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
return parsed;
|
|
1437
1916
|
}
|
|
1917
|
+
function renderDiffText(diff) {
|
|
1918
|
+
const lines = [];
|
|
1919
|
+
lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
|
|
1920
|
+
lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
|
|
1921
|
+
lines.push("");
|
|
1922
|
+
lines.push(` + ${diff.added.length} added`);
|
|
1923
|
+
lines.push(` - ${diff.removed.length} removed`);
|
|
1924
|
+
lines.push(` ~ ${diff.changed.length} changed`);
|
|
1925
|
+
lines.push(` = ${diff.unchanged} unchanged`);
|
|
1926
|
+
const section = (title, values) => {
|
|
1927
|
+
if (values.length === 0) return;
|
|
1928
|
+
lines.push("", title);
|
|
1929
|
+
for (const value of values.slice(0, 200)) lines.push(` ${value}`);
|
|
1930
|
+
if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
|
|
1931
|
+
};
|
|
1932
|
+
section("added:", diff.added);
|
|
1933
|
+
section("removed:", diff.removed);
|
|
1934
|
+
section("changed:", diff.changed);
|
|
1935
|
+
return lines.join("\n");
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
// src/explain.ts
|
|
1939
|
+
import * as fs6 from "fs";
|
|
1940
|
+
import * as path7 from "path";
|
|
1438
1941
|
|
|
1439
1942
|
// src/config.ts
|
|
1943
|
+
import * as fs4 from "fs";
|
|
1944
|
+
import * as path4 from "path";
|
|
1945
|
+
import { pathToFileURL } from "url";
|
|
1440
1946
|
var CONFIG_FILENAMES = [
|
|
1441
1947
|
"snapshot.config.ts",
|
|
1442
1948
|
"snapshot.config.js",
|
|
@@ -1485,7 +1991,12 @@ function normalizeResolvedConfig(config) {
|
|
|
1485
1991
|
),
|
|
1486
1992
|
include: resolveInclude(config.include),
|
|
1487
1993
|
preset: typeof config.preset === "string" ? config.preset : "generic",
|
|
1488
|
-
presetExclude:
|
|
1994
|
+
presetExclude: [
|
|
1995
|
+
.../* @__PURE__ */ new Set([
|
|
1996
|
+
...Array.isArray(config.presetExclude) ? config.presetExclude : [],
|
|
1997
|
+
...outputExcludePatterns(config.cwd, config.outDir, config.tmpDir)
|
|
1998
|
+
])
|
|
1999
|
+
],
|
|
1489
2000
|
layersMode: config.layersMode === "replace" ? "replace" : "merge",
|
|
1490
2001
|
budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
|
|
1491
2002
|
hooks: resolveHooks(config.hooks),
|
|
@@ -1522,7 +2033,68 @@ function supportsTypeScriptImports() {
|
|
|
1522
2033
|
return major > 22 || major === 22 && minor >= 6;
|
|
1523
2034
|
}
|
|
1524
2035
|
function stripJsonComments(raw) {
|
|
1525
|
-
|
|
2036
|
+
const text2 = raw.replace(/^\uFEFF/, "");
|
|
2037
|
+
let out = "";
|
|
2038
|
+
let inString = false;
|
|
2039
|
+
let escaped = false;
|
|
2040
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2041
|
+
const char = text2[index];
|
|
2042
|
+
if (inString) {
|
|
2043
|
+
out += char;
|
|
2044
|
+
if (escaped) escaped = false;
|
|
2045
|
+
else if (char === "\\") escaped = true;
|
|
2046
|
+
else if (char === '"') inString = false;
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
if (char === '"') {
|
|
2050
|
+
inString = true;
|
|
2051
|
+
out += char;
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
if (char === "/" && text2[index + 1] === "/") {
|
|
2055
|
+
while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
|
|
2056
|
+
index -= 1;
|
|
2057
|
+
continue;
|
|
2058
|
+
}
|
|
2059
|
+
if (char === "/" && text2[index + 1] === "*") {
|
|
2060
|
+
index += 2;
|
|
2061
|
+
while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
|
|
2062
|
+
if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
|
|
2063
|
+
index += 1;
|
|
2064
|
+
}
|
|
2065
|
+
index += 1;
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
out += char;
|
|
2069
|
+
}
|
|
2070
|
+
return stripTrailingCommas(out);
|
|
2071
|
+
}
|
|
2072
|
+
function stripTrailingCommas(text2) {
|
|
2073
|
+
let out = "";
|
|
2074
|
+
let inString = false;
|
|
2075
|
+
let escaped = false;
|
|
2076
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2077
|
+
const char = text2[index];
|
|
2078
|
+
if (inString) {
|
|
2079
|
+
out += char;
|
|
2080
|
+
if (escaped) escaped = false;
|
|
2081
|
+
else if (char === "\\") escaped = true;
|
|
2082
|
+
else if (char === '"') inString = false;
|
|
2083
|
+
continue;
|
|
2084
|
+
}
|
|
2085
|
+
if (char === '"') {
|
|
2086
|
+
inString = true;
|
|
2087
|
+
out += char;
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
if (char === ",") {
|
|
2091
|
+
let next = index + 1;
|
|
2092
|
+
while (next < text2.length && /\s/.test(text2[next])) next += 1;
|
|
2093
|
+
if (text2[next] === "}" || text2[next] === "]") continue;
|
|
2094
|
+
}
|
|
2095
|
+
out += char;
|
|
2096
|
+
}
|
|
2097
|
+
return out;
|
|
1526
2098
|
}
|
|
1527
2099
|
function parseJsonish(raw, file) {
|
|
1528
2100
|
try {
|
|
@@ -1531,6 +2103,47 @@ function parseJsonish(raw, file) {
|
|
|
1531
2103
|
throw new Error(`Could not parse ${file}: ${err.message}`);
|
|
1532
2104
|
}
|
|
1533
2105
|
}
|
|
2106
|
+
function describeValue2(value) {
|
|
2107
|
+
if (value === null) return "null";
|
|
2108
|
+
if (Array.isArray(value)) return "an array";
|
|
2109
|
+
if (value instanceof RegExp) return value.toString();
|
|
2110
|
+
if (typeof value === "function") return "a function";
|
|
2111
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
2112
|
+
if (typeof value === "object") {
|
|
2113
|
+
const keys = Object.keys(value);
|
|
2114
|
+
return keys.length > 0 ? `an object with keys ${keys.join(", ")}` : "an empty object";
|
|
2115
|
+
}
|
|
2116
|
+
return `${typeof value} (${String(value)})`;
|
|
2117
|
+
}
|
|
2118
|
+
var isThenable = (value) => typeof value.then === "function";
|
|
2119
|
+
function readConfigExport(exported, file) {
|
|
2120
|
+
if (exported === void 0 || exported === null) {
|
|
2121
|
+
throw new Error(
|
|
2122
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2123
|
+
);
|
|
2124
|
+
}
|
|
2125
|
+
if (Array.isArray(exported)) {
|
|
2126
|
+
throw new Error(
|
|
2127
|
+
`${file} exports an array; expected a single configuration object such as \`export default { outDir: "snapshot" }\`.`
|
|
2128
|
+
);
|
|
2129
|
+
}
|
|
2130
|
+
if (typeof exported === "function") {
|
|
2131
|
+
throw new Error(
|
|
2132
|
+
`${file} exports a function; expected a configuration object. Call the function yourself and export its result.`
|
|
2133
|
+
);
|
|
2134
|
+
}
|
|
2135
|
+
if (typeof exported !== "object") {
|
|
2136
|
+
throw new Error(
|
|
2137
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2138
|
+
);
|
|
2139
|
+
}
|
|
2140
|
+
if (isThenable(exported)) {
|
|
2141
|
+
throw new Error(
|
|
2142
|
+
`${file} exports a Promise/thenable, not a configuration object. An async config is read synchronously, so its settings would be ignored: export a plain object (\`const config = {\u2026}; export default config;\`).`
|
|
2143
|
+
);
|
|
2144
|
+
}
|
|
2145
|
+
return exported;
|
|
2146
|
+
}
|
|
1534
2147
|
async function loadConfigFileDetailed(cwd) {
|
|
1535
2148
|
const warnings = [];
|
|
1536
2149
|
const errors = [];
|
|
@@ -1558,15 +2171,9 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
1558
2171
|
);
|
|
1559
2172
|
continue;
|
|
1560
2173
|
}
|
|
1561
|
-
try {
|
|
1562
|
-
const imported = await import(pathToFileURL(fullPath).href);
|
|
1563
|
-
const config = imported.default ?? imported;
|
|
1564
|
-
if (!config || typeof config !== "object") {
|
|
1565
|
-
const message = `${fullPath} does not export a configuration object.`;
|
|
1566
|
-
warnings.push(message);
|
|
1567
|
-
errors.push(message);
|
|
1568
|
-
continue;
|
|
1569
|
-
}
|
|
2174
|
+
try {
|
|
2175
|
+
const imported = await import(pathToFileURL(fullPath).href);
|
|
2176
|
+
const config = readConfigExport(imported.default ?? imported, fullPath);
|
|
1570
2177
|
return { config, file: fullPath, warnings, errors };
|
|
1571
2178
|
} catch (err) {
|
|
1572
2179
|
const message = `Could not load ${fullPath}: ${err.message}`;
|
|
@@ -1616,7 +2223,7 @@ var DEFAULT_ARTIFACTS = {
|
|
|
1616
2223
|
function isArtifactName(value) {
|
|
1617
2224
|
return ARTIFACT_NAMES.includes(value);
|
|
1618
2225
|
}
|
|
1619
|
-
function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
2226
|
+
function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
|
|
1620
2227
|
const artifacts = {
|
|
1621
2228
|
...DEFAULT_ARTIFACTS,
|
|
1622
2229
|
// `includeMonolith` predates this selection and stays the switch for it.
|
|
@@ -1638,6 +2245,9 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
1638
2245
|
artifacts[name] = value;
|
|
1639
2246
|
}
|
|
1640
2247
|
}
|
|
2248
|
+
if (monolithOverride !== void 0) {
|
|
2249
|
+
artifacts.monolith = monolithOverride;
|
|
2250
|
+
}
|
|
1641
2251
|
for (const name of skipped ?? []) {
|
|
1642
2252
|
if (!isArtifactName(name)) {
|
|
1643
2253
|
throw new Error(
|
|
@@ -1648,9 +2258,106 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
1648
2258
|
}
|
|
1649
2259
|
return artifacts;
|
|
1650
2260
|
}
|
|
1651
|
-
function
|
|
1652
|
-
if (
|
|
1653
|
-
|
|
2261
|
+
function strictPositiveInteger(value, fallback, label) {
|
|
2262
|
+
if (value === void 0) return fallback;
|
|
2263
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
2264
|
+
throw new Error(`${label} must be a positive integer (received ${describeValue2(value)}).`);
|
|
2265
|
+
}
|
|
2266
|
+
return value;
|
|
2267
|
+
}
|
|
2268
|
+
function strictBoolean(value, fallback, label) {
|
|
2269
|
+
if (value === void 0) return fallback;
|
|
2270
|
+
if (typeof value !== "boolean") {
|
|
2271
|
+
throw new Error(`${label} must be true or false (received ${describeValue2(value)}).`);
|
|
2272
|
+
}
|
|
2273
|
+
return value;
|
|
2274
|
+
}
|
|
2275
|
+
function strictString(value, fallback, label) {
|
|
2276
|
+
if (value === void 0) return fallback;
|
|
2277
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2278
|
+
throw new Error(`${label} must be a non-empty string (received ${describeValue2(value)}).`);
|
|
2279
|
+
}
|
|
2280
|
+
return value;
|
|
2281
|
+
}
|
|
2282
|
+
function resolveExcludePatterns(value) {
|
|
2283
|
+
if (value === void 0) return [];
|
|
2284
|
+
if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string")) {
|
|
2285
|
+
throw new Error(
|
|
2286
|
+
`\`excludePatterns\` must be an array of glob strings such as ["**/*.test.ts"] (received ${describeValue2(value)}).`
|
|
2287
|
+
);
|
|
2288
|
+
}
|
|
2289
|
+
return value.map((pattern) => pattern.trim()).filter(Boolean);
|
|
2290
|
+
}
|
|
2291
|
+
var AUTO_PRESET_ALIASES = /* @__PURE__ */ new Set(["auto", "detected"]);
|
|
2292
|
+
function resolvePresetName(value) {
|
|
2293
|
+
if (value === void 0) return void 0;
|
|
2294
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2295
|
+
throw new Error(
|
|
2296
|
+
`\`preset\` must be a preset name (received ${describeValue2(value)}). Use "detected" (or "auto") to detect one, or one of: ${listPresetNames().join(", ")}.`
|
|
2297
|
+
);
|
|
2298
|
+
}
|
|
2299
|
+
const name = value.trim();
|
|
2300
|
+
return AUTO_PRESET_ALIASES.has(name.toLowerCase()) ? void 0 : name;
|
|
2301
|
+
}
|
|
2302
|
+
function resolveRuleList(value, kind) {
|
|
2303
|
+
const collection = kind === "layer" ? "layers" : "kinds";
|
|
2304
|
+
const example = kind === "layer" ? `{ name: "api", match: "src/api/**" }` : `{ kind: "route", match: "src/routes/**" }`;
|
|
2305
|
+
if (value === void 0) return [];
|
|
2306
|
+
if (!Array.isArray(value)) {
|
|
2307
|
+
throw new Error(
|
|
2308
|
+
`\`${collection}\` must be an array of ${kind} rules (received ${describeValue2(value)}). A ${kind} rule is ${example}.`
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
return value.map((entry, index) => {
|
|
2312
|
+
const where = `${collection}[${index}]`;
|
|
2313
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
2314
|
+
throw new Error(`${where} must be an object (received ${describeValue2(entry)}). A ${kind} rule is ${example}.`);
|
|
2315
|
+
}
|
|
2316
|
+
const record = entry;
|
|
2317
|
+
const keys = Object.keys(record);
|
|
2318
|
+
const canonical = kind === "layer" ? "name" : "kind";
|
|
2319
|
+
const alias = kind === "layer" ? "layer" : "name";
|
|
2320
|
+
const rawName = typeof record[canonical] === "string" ? record[canonical] : typeof record[alias] === "string" ? record[alias] : void 0;
|
|
2321
|
+
if (rawName === void 0 || rawName.trim() === "") {
|
|
2322
|
+
throw new Error(
|
|
2323
|
+
`${where} is missing \`${canonical}\` (received keys: ${keys.join(", ") || "none"}). A ${kind} rule is ${example}; \`${alias}\` is accepted as an alias of \`${canonical}\`.`
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
const match = record.match;
|
|
2327
|
+
if (match === void 0) {
|
|
2328
|
+
throw new Error(
|
|
2329
|
+
`${where}.match is required (received keys: ${keys.join(", ") || "none"}). Use a glob string ("src/api/**"), a RegExp (/\\\\.tsx$/) or a function.`
|
|
2330
|
+
);
|
|
2331
|
+
}
|
|
2332
|
+
if (!(typeof match === "string" || match instanceof RegExp || typeof match === "function")) {
|
|
2333
|
+
throw new Error(`${where}.match must be a string, a RegExp or a function (received ${describeValue2(match)}).`);
|
|
2334
|
+
}
|
|
2335
|
+
if (typeof match === "string" && match.trim() === "") {
|
|
2336
|
+
throw new Error(`${where}.match must not be an empty string.`);
|
|
2337
|
+
}
|
|
2338
|
+
return {
|
|
2339
|
+
name: rawName.trim(),
|
|
2340
|
+
match
|
|
2341
|
+
};
|
|
2342
|
+
});
|
|
2343
|
+
}
|
|
2344
|
+
function resolveLayerRules(value) {
|
|
2345
|
+
return resolveRuleList(value, "layer").map((rule) => ({ name: rule.name, match: rule.match }));
|
|
2346
|
+
}
|
|
2347
|
+
function resolveKindRules(value) {
|
|
2348
|
+
return resolveRuleList(value, "kind").map((rule) => ({ kind: rule.name, match: rule.match }));
|
|
2349
|
+
}
|
|
2350
|
+
function outputExcludePatterns(cwd, outDir, tmpDir) {
|
|
2351
|
+
const patterns = [];
|
|
2352
|
+
for (const dir of [outDir, tmpDir]) {
|
|
2353
|
+
const absolute = path4.resolve(cwd, dir);
|
|
2354
|
+
const relative5 = path4.relative(cwd, absolute);
|
|
2355
|
+
if (!relative5 || relative5 === ".." || relative5.startsWith(`..${path4.sep}`) || path4.isAbsolute(relative5)) {
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
patterns.push(`${relative5.replace(/\\/g, "/")}/**`);
|
|
2359
|
+
}
|
|
2360
|
+
return patterns;
|
|
1654
2361
|
}
|
|
1655
2362
|
var BYTE_UNITS = {
|
|
1656
2363
|
b: 1,
|
|
@@ -1719,11 +2426,13 @@ function resolveLayersMode(mode) {
|
|
|
1719
2426
|
}
|
|
1720
2427
|
return mode;
|
|
1721
2428
|
}
|
|
1722
|
-
function resolveBudgets(budgets, fallback) {
|
|
2429
|
+
function resolveBudgets(budgets, fallback, overrideDefault) {
|
|
1723
2430
|
const byLayer = {};
|
|
1724
2431
|
if (budgets === void 0) return { default: fallback, byLayer };
|
|
1725
2432
|
if (typeof budgets !== "object" || budgets === null || Array.isArray(budgets)) {
|
|
1726
|
-
throw new Error(
|
|
2433
|
+
throw new Error(
|
|
2434
|
+
`\`budgets\` must be an object such as { default: "120k", byLayer: { api: "60k" } } (received ${describeValue2(budgets)}).`
|
|
2435
|
+
);
|
|
1727
2436
|
}
|
|
1728
2437
|
if (budgets.byLayer !== void 0) {
|
|
1729
2438
|
if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
|
|
@@ -1738,10 +2447,12 @@ function resolveBudgets(budgets, fallback) {
|
|
|
1738
2447
|
);
|
|
1739
2448
|
}
|
|
1740
2449
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2450
|
+
const resolvedDefault = boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default");
|
|
2451
|
+
if (!overrideDefault) return { default: resolvedDefault, byLayer };
|
|
2452
|
+
for (const [layer2, value] of Object.entries(byLayer)) {
|
|
2453
|
+
byLayer[layer2] = Math.min(value, fallback);
|
|
2454
|
+
}
|
|
2455
|
+
return { default: fallback, byLayer };
|
|
1745
2456
|
}
|
|
1746
2457
|
function resolveHooks(hooks) {
|
|
1747
2458
|
if (hooks === void 0) return {};
|
|
@@ -1754,6 +2465,11 @@ function resolveHooks(hooks) {
|
|
|
1754
2465
|
return { ...hooks };
|
|
1755
2466
|
}
|
|
1756
2467
|
function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
2468
|
+
if (userConfig?.cwd !== void 0 && (typeof userConfig.cwd !== "string" || userConfig.cwd.trim() === "")) {
|
|
2469
|
+
throw new Error(
|
|
2470
|
+
`\`cwd\` must be a non-empty path string (received ${describeValue2(userConfig.cwd)}).`
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
1757
2473
|
const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
|
|
1758
2474
|
const resolved = path4.resolve(candidate);
|
|
1759
2475
|
if (!fs4.existsSync(resolved)) {
|
|
@@ -1766,55 +2482,107 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
|
1766
2482
|
}
|
|
1767
2483
|
function resolveConfig(userConfig, cliOptions, baseDir) {
|
|
1768
2484
|
const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
|
|
1769
|
-
const forcedPreset = userConfig?.preset;
|
|
2485
|
+
const forcedPreset = resolvePresetName(userConfig?.preset);
|
|
1770
2486
|
if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
|
|
1771
2487
|
throw new Error(unknownPresetMessage(forcedPreset));
|
|
1772
2488
|
}
|
|
1773
2489
|
const preset = detectPreset(cwd, forcedPreset);
|
|
1774
2490
|
const layersMode = resolveLayersMode(userConfig?.layersMode);
|
|
1775
|
-
const
|
|
1776
|
-
const
|
|
1777
|
-
const
|
|
1778
|
-
const
|
|
1779
|
-
const
|
|
1780
|
-
|
|
2491
|
+
const userLayers = resolveLayerRules(userConfig?.layers);
|
|
2492
|
+
const userKinds = resolveKindRules(userConfig?.kinds);
|
|
2493
|
+
const layers = userLayers.length > 0 ? layersMode === "replace" ? userLayers : [...userLayers, ...preset.layers] : preset.layers;
|
|
2494
|
+
const kinds = userKinds.length > 0 ? layersMode === "replace" ? userKinds : [...userKinds, ...preset.kinds] : preset.kinds;
|
|
2495
|
+
const secrets = userConfig?.secrets;
|
|
2496
|
+
if (secrets !== void 0 && (typeof secrets !== "object" || Array.isArray(secrets))) {
|
|
2497
|
+
throw new Error(
|
|
2498
|
+
`\`secrets\` must be an object of { customPatterns, disableDefaultPatterns } (received ${describeValue2(secrets)}).`
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
const customRules = secrets?.customPatterns ?? [];
|
|
2502
|
+
if (!Array.isArray(customRules)) {
|
|
2503
|
+
throw new Error(
|
|
2504
|
+
`\`secrets.customPatterns\` must be an array of secret rules (received ${describeValue2(customRules)}). Each entry is a RegExp, a string, or { rule, pattern }.`
|
|
2505
|
+
);
|
|
2506
|
+
}
|
|
2507
|
+
const disableDefaultPatterns = strictBoolean(
|
|
2508
|
+
secrets?.disableDefaultPatterns,
|
|
2509
|
+
false,
|
|
2510
|
+
"`secrets.disableDefaultPatterns`"
|
|
2511
|
+
);
|
|
2512
|
+
const aliasedRules = userConfig?.secretRules;
|
|
2513
|
+
if (aliasedRules !== void 0 && !Array.isArray(aliasedRules)) {
|
|
2514
|
+
throw new Error(
|
|
2515
|
+
`\`secretRules\` must be an array of { rule, pattern } entries (received ${describeValue2(aliasedRules)}).`
|
|
2516
|
+
);
|
|
2517
|
+
}
|
|
2518
|
+
const compiledCustomRules = compileSecretRules([
|
|
2519
|
+
...customRules,
|
|
2520
|
+
...aliasedRules ?? []
|
|
2521
|
+
]);
|
|
2522
|
+
const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
|
|
2523
|
+
setActiveSecretRules(secretRules);
|
|
2524
|
+
const cliMonolith = cliOptions?.includeMonolith;
|
|
2525
|
+
const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
|
|
1781
2526
|
const artifacts = resolveArtifacts(
|
|
1782
2527
|
userConfig?.artifacts,
|
|
1783
2528
|
cliOptions?.skipArtifacts,
|
|
1784
|
-
includeMonolith
|
|
2529
|
+
includeMonolith,
|
|
2530
|
+
cliMonolith
|
|
1785
2531
|
);
|
|
2532
|
+
const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
|
|
1786
2533
|
const maxPartBytes = boundedBytes(
|
|
1787
|
-
|
|
2534
|
+
explicitMaxPartBytes,
|
|
1788
2535
|
300 * 1024,
|
|
1789
2536
|
MIN_MAX_PART_BYTES,
|
|
1790
2537
|
"maxPartBytes"
|
|
1791
2538
|
);
|
|
1792
|
-
const budgets = resolveBudgets(
|
|
2539
|
+
const budgets = resolveBudgets(
|
|
2540
|
+
userConfig?.budgets,
|
|
2541
|
+
maxPartBytes,
|
|
2542
|
+
explicitMaxPartBytes !== void 0
|
|
2543
|
+
);
|
|
2544
|
+
const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
|
|
2545
|
+
const outDir = cliOptions?.outDir || configuredOutDir;
|
|
2546
|
+
const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
|
|
2547
|
+
const branch = strictString(userConfig?.branch, "main", "`branch`");
|
|
2548
|
+
const configuredSensitiveFiles = strictBoolean(
|
|
2549
|
+
userConfig?.includeSensitiveFiles,
|
|
2550
|
+
false,
|
|
2551
|
+
"`includeSensitiveFiles`"
|
|
2552
|
+
);
|
|
2553
|
+
const includeSensitiveFiles = strictBoolean(
|
|
2554
|
+
cliOptions?.includeSensitiveFiles,
|
|
2555
|
+
configuredSensitiveFiles,
|
|
2556
|
+
"`includeSensitiveFiles`"
|
|
2557
|
+
);
|
|
2558
|
+
const presetExclude = [
|
|
2559
|
+
.../* @__PURE__ */ new Set([...preset.exclude, ...outputExcludePatterns(cwd, outDir, tmpDir)])
|
|
2560
|
+
];
|
|
1793
2561
|
return markResolvedConfig({
|
|
1794
2562
|
cwd,
|
|
1795
|
-
branch
|
|
1796
|
-
outDir
|
|
1797
|
-
tmpDir
|
|
2563
|
+
branch,
|
|
2564
|
+
outDir,
|
|
2565
|
+
tmpDir,
|
|
1798
2566
|
maxPartBytes,
|
|
1799
2567
|
largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
|
|
1800
|
-
previewHeadLines:
|
|
1801
|
-
previewTailLines:
|
|
2568
|
+
previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
|
|
2569
|
+
previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
|
|
1802
2570
|
includeMonolith: artifacts.monolith,
|
|
1803
2571
|
maxMonolithBytes: optionalBytes(
|
|
1804
2572
|
cliOptions?.maxMonolithBytes !== void 0 ? cliOptions.maxMonolithBytes : userConfig?.maxMonolithBytes,
|
|
1805
2573
|
"maxMonolithBytes"
|
|
1806
2574
|
),
|
|
1807
|
-
includeSensitiveFiles
|
|
2575
|
+
includeSensitiveFiles,
|
|
1808
2576
|
include: resolveInclude(userConfig?.include),
|
|
1809
2577
|
preset: preset.presetName,
|
|
1810
|
-
presetExclude
|
|
2578
|
+
presetExclude,
|
|
1811
2579
|
artifacts,
|
|
1812
2580
|
layersMode,
|
|
1813
2581
|
budgets,
|
|
1814
2582
|
hooks: resolveHooks(userConfig?.hooks),
|
|
1815
2583
|
layers,
|
|
1816
2584
|
kinds,
|
|
1817
|
-
excludePatterns: userConfig?.excludePatterns
|
|
2585
|
+
excludePatterns: resolveExcludePatterns(userConfig?.excludePatterns),
|
|
1818
2586
|
secretRules
|
|
1819
2587
|
});
|
|
1820
2588
|
}
|
|
@@ -1894,8 +2662,81 @@ function truncatePreview(content, headLines, tailLines) {
|
|
|
1894
2662
|
};
|
|
1895
2663
|
}
|
|
1896
2664
|
|
|
1897
|
-
// src/scanner/
|
|
2665
|
+
// src/scanner/binary.ts
|
|
2666
|
+
import * as fs5 from "fs";
|
|
1898
2667
|
import * as path5 from "path";
|
|
2668
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2669
|
+
".png",
|
|
2670
|
+
".jpg",
|
|
2671
|
+
".jpeg",
|
|
2672
|
+
".gif",
|
|
2673
|
+
".webp",
|
|
2674
|
+
".avif",
|
|
2675
|
+
".ico",
|
|
2676
|
+
".bmp",
|
|
2677
|
+
".tiff",
|
|
2678
|
+
".pdf",
|
|
2679
|
+
".zip",
|
|
2680
|
+
".tar",
|
|
2681
|
+
".gz",
|
|
2682
|
+
".tgz",
|
|
2683
|
+
".bz2",
|
|
2684
|
+
".xz",
|
|
2685
|
+
".7z",
|
|
2686
|
+
".rar",
|
|
2687
|
+
".mp3",
|
|
2688
|
+
".mp4",
|
|
2689
|
+
".wav",
|
|
2690
|
+
".ogg",
|
|
2691
|
+
".webm",
|
|
2692
|
+
".mov",
|
|
2693
|
+
".avi",
|
|
2694
|
+
".woff",
|
|
2695
|
+
".woff2",
|
|
2696
|
+
".ttf",
|
|
2697
|
+
".otf",
|
|
2698
|
+
".eot",
|
|
2699
|
+
".wasm",
|
|
2700
|
+
".node",
|
|
2701
|
+
".exe",
|
|
2702
|
+
".dll",
|
|
2703
|
+
".dylib",
|
|
2704
|
+
".so"
|
|
2705
|
+
]);
|
|
2706
|
+
function isBinaryExtension(filePath) {
|
|
2707
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
2708
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
2709
|
+
}
|
|
2710
|
+
function isBinaryBuffer(buffer) {
|
|
2711
|
+
const checkLength = Math.min(buffer.length, 8192);
|
|
2712
|
+
for (let index = 0; index < checkLength; index++) {
|
|
2713
|
+
if (buffer[index] === 0) return true;
|
|
2714
|
+
}
|
|
2715
|
+
return false;
|
|
2716
|
+
}
|
|
2717
|
+
function isBinaryFile(absolutePath) {
|
|
2718
|
+
if (isBinaryExtension(absolutePath)) return true;
|
|
2719
|
+
let fd;
|
|
2720
|
+
try {
|
|
2721
|
+
fd = fs5.openSync(absolutePath, "r");
|
|
2722
|
+
const buffer = Buffer.alloc(8192);
|
|
2723
|
+
const bytesRead = fs5.readSync(fd, buffer, 0, 8192, 0);
|
|
2724
|
+
if (bytesRead === 0) return false;
|
|
2725
|
+
return isBinaryBuffer(buffer.subarray(0, bytesRead));
|
|
2726
|
+
} catch {
|
|
2727
|
+
return false;
|
|
2728
|
+
} finally {
|
|
2729
|
+
if (fd !== void 0) {
|
|
2730
|
+
try {
|
|
2731
|
+
fs5.closeSync(fd);
|
|
2732
|
+
} catch {
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
}
|
|
2737
|
+
|
|
2738
|
+
// src/scanner/language.ts
|
|
2739
|
+
import * as path6 from "path";
|
|
1899
2740
|
var LANGUAGE_BY_EXTENSION = {
|
|
1900
2741
|
".ts": "typescript",
|
|
1901
2742
|
".mts": "typescript",
|
|
@@ -1950,10 +2791,10 @@ var LANGUAGE_BY_BASENAME = {
|
|
|
1950
2791
|
};
|
|
1951
2792
|
function detectLanguage(filePath) {
|
|
1952
2793
|
const normalized = filePath.replace(/\\/g, "/");
|
|
1953
|
-
const base =
|
|
2794
|
+
const base = path6.posix.basename(normalized);
|
|
1954
2795
|
if (LANGUAGE_BY_BASENAME[base]) return LANGUAGE_BY_BASENAME[base];
|
|
1955
2796
|
if (base === ".env" || base.startsWith(".env.")) return "dotenv";
|
|
1956
|
-
const ext =
|
|
2797
|
+
const ext = path6.posix.extname(base).toLowerCase();
|
|
1957
2798
|
return LANGUAGE_BY_EXTENSION[ext] ?? "text";
|
|
1958
2799
|
}
|
|
1959
2800
|
|
|
@@ -1962,16 +2803,15 @@ function describeMatcher(matcher) {
|
|
|
1962
2803
|
if (typeof matcher === "string") return matcher;
|
|
1963
2804
|
if (matcher instanceof RegExp) return String(matcher);
|
|
1964
2805
|
if (typeof matcher === "function") {
|
|
1965
|
-
|
|
1966
|
-
return source.length > 70 ? `${source.slice(0, 67)}...` : source;
|
|
2806
|
+
return "function (custom predicate)";
|
|
1967
2807
|
}
|
|
1968
2808
|
return String(matcher);
|
|
1969
2809
|
}
|
|
1970
2810
|
function normalizeTarget(cwd, target) {
|
|
1971
|
-
const absolute =
|
|
1972
|
-
const
|
|
1973
|
-
if (
|
|
1974
|
-
return
|
|
2811
|
+
const absolute = path7.resolve(cwd, target);
|
|
2812
|
+
const relative5 = path7.relative(cwd, absolute);
|
|
2813
|
+
if (relative5.startsWith("..") || path7.isAbsolute(relative5)) return target;
|
|
2814
|
+
return relative5.split(path7.sep).join("/");
|
|
1975
2815
|
}
|
|
1976
2816
|
function findRuleMatch(rules, relativePath) {
|
|
1977
2817
|
return rules.find(
|
|
@@ -1979,28 +2819,32 @@ function findRuleMatch(rules, relativePath) {
|
|
|
1979
2819
|
);
|
|
1980
2820
|
}
|
|
1981
2821
|
function readManifest2(config) {
|
|
1982
|
-
const manifestPath =
|
|
2822
|
+
const manifestPath = path7.resolve(config.cwd, config.outDir, "manifest.json");
|
|
1983
2823
|
try {
|
|
1984
|
-
return JSON.parse(
|
|
2824
|
+
return JSON.parse(fs6.readFileSync(manifestPath, "utf8"));
|
|
1985
2825
|
} catch {
|
|
1986
2826
|
return null;
|
|
1987
2827
|
}
|
|
1988
2828
|
}
|
|
1989
2829
|
function explainPath(config, target) {
|
|
1990
|
-
const
|
|
1991
|
-
const absolute =
|
|
2830
|
+
const relative5 = normalizeTarget(config.cwd, target);
|
|
2831
|
+
const absolute = path7.join(config.cwd, relative5);
|
|
2832
|
+
const maskedPath2 = findSecretInPath(relative5, config.secretRules)?.masked ?? relative5;
|
|
1992
2833
|
const result = {
|
|
1993
|
-
path:
|
|
1994
|
-
exists:
|
|
2834
|
+
path: maskedPath2,
|
|
2835
|
+
exists: fs6.existsSync(absolute),
|
|
1995
2836
|
included: false
|
|
1996
2837
|
};
|
|
1997
2838
|
const collection = collectFiles(config.cwd, {
|
|
1998
2839
|
includePatterns: config.include,
|
|
1999
2840
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
2000
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
2841
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
2842
|
+
secretRules: config.secretRules
|
|
2001
2843
|
});
|
|
2002
|
-
if (!collection.files.includes(
|
|
2003
|
-
const record = collection.skipped.find(
|
|
2844
|
+
if (!collection.files.includes(relative5)) {
|
|
2845
|
+
const record = collection.skipped.find(
|
|
2846
|
+
(entry2) => entry2.path === relative5 || entry2.path === maskedPath2
|
|
2847
|
+
);
|
|
2004
2848
|
if (record) {
|
|
2005
2849
|
result.reason = record.reason;
|
|
2006
2850
|
result.detail = record.detail;
|
|
@@ -2014,24 +2858,30 @@ function explainPath(config, target) {
|
|
|
2014
2858
|
return result;
|
|
2015
2859
|
}
|
|
2016
2860
|
result.included = true;
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2861
|
+
if (isBinaryFile(absolute)) {
|
|
2862
|
+
result.included = false;
|
|
2863
|
+
result.reason = "binary";
|
|
2864
|
+
result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
|
|
2865
|
+
return result;
|
|
2866
|
+
}
|
|
2867
|
+
result.layer = classifyLayer(relative5, config.layers);
|
|
2868
|
+
result.kind = classifyKind(relative5, config.kinds);
|
|
2869
|
+
result.language = detectLanguage(relative5);
|
|
2870
|
+
const layerRule = findRuleMatch(config.layers, relative5);
|
|
2021
2871
|
if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
|
|
2022
|
-
const kindRule = findRuleMatch(config.kinds,
|
|
2872
|
+
const kindRule = findRuleMatch(config.kinds, relative5);
|
|
2023
2873
|
if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
|
|
2024
|
-
if (
|
|
2025
|
-
const buffer =
|
|
2874
|
+
if (fs6.existsSync(absolute) && fs6.statSync(absolute).isFile()) {
|
|
2875
|
+
const buffer = fs6.readFileSync(absolute);
|
|
2026
2876
|
const text2 = buffer.toString("utf8");
|
|
2027
2877
|
result.bytes = buffer.length;
|
|
2028
2878
|
result.lines = countLines(text2);
|
|
2029
|
-
const { findings } = scanAndRedactSecrets(text2, config.secretRules,
|
|
2879
|
+
const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative5);
|
|
2030
2880
|
result.secretCount = findings.length;
|
|
2031
2881
|
result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
|
|
2032
2882
|
}
|
|
2033
2883
|
const manifest = readManifest2(config);
|
|
2034
|
-
const entry = manifest?.files.find((file) => file.relativePath ===
|
|
2884
|
+
const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative5) : void 0;
|
|
2035
2885
|
result.part = entry?.part ?? null;
|
|
2036
2886
|
result.inclusionMode = entry?.inclusionMode;
|
|
2037
2887
|
return result;
|
|
@@ -2071,8 +2921,8 @@ function renderExplainText(result) {
|
|
|
2071
2921
|
}
|
|
2072
2922
|
|
|
2073
2923
|
// src/wizard/init.ts
|
|
2074
|
-
import * as
|
|
2075
|
-
import * as
|
|
2924
|
+
import * as fs7 from "fs";
|
|
2925
|
+
import * as path8 from "path";
|
|
2076
2926
|
|
|
2077
2927
|
// src/wizard/config-template.ts
|
|
2078
2928
|
var DEFAULT_ANSWERS = {
|
|
@@ -2161,7 +3011,7 @@ function renderConfig(answers) {
|
|
|
2161
3011
|
secrets: {
|
|
2162
3012
|
// [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
|
|
2163
3013
|
customPatterns: [],
|
|
2164
|
-
// Set true to drop the
|
|
3014
|
+
// Set true to drop the built-in rules above and keep only customPatterns.
|
|
2165
3015
|
disableDefaultPatterns: false,
|
|
2166
3016
|
},
|
|
2167
3017
|
|
|
@@ -2177,6 +3027,10 @@ function renderConfig(answers) {
|
|
|
2177
3027
|
},
|
|
2178
3028
|
// Skip the monolith above this size. null = no limit.
|
|
2179
3029
|
maxMonolithBytes: null,
|
|
3030
|
+
// Top-level switch for the monolith, equivalent to artifacts.monolith.
|
|
3031
|
+
// When both are present the artifact entry wins; this one exists so the
|
|
3032
|
+
// generated file lists every setting the tool accepts.
|
|
3033
|
+
includeMonolith: ${artifacts.monolith},
|
|
2180
3034
|
|
|
2181
3035
|
// \u2500\u2500 Advanced \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2182
3036
|
// afterGenerate: (manifest) => { /* write your own artifact */ }
|
|
@@ -2480,10 +3334,10 @@ var CONFIG_CANDIDATES = [
|
|
|
2480
3334
|
];
|
|
2481
3335
|
function chooseConfigFileName(cwd) {
|
|
2482
3336
|
if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
|
|
2483
|
-
const pkgPath =
|
|
2484
|
-
if (!
|
|
3337
|
+
const pkgPath = path8.join(cwd, "package.json");
|
|
3338
|
+
if (!fs7.existsSync(pkgPath)) return "snapshot.config.mjs";
|
|
2485
3339
|
try {
|
|
2486
|
-
const parsed = JSON.parse(
|
|
3340
|
+
const parsed = JSON.parse(fs7.readFileSync(pkgPath, "utf8"));
|
|
2487
3341
|
return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
|
|
2488
3342
|
} catch {
|
|
2489
3343
|
return "snapshot.config.mjs";
|
|
@@ -2497,10 +3351,18 @@ var SIZE_CHOICES = [
|
|
|
2497
3351
|
];
|
|
2498
3352
|
function existingConfig(cwd) {
|
|
2499
3353
|
for (const name of CONFIG_CANDIDATES) {
|
|
2500
|
-
if (
|
|
3354
|
+
if (fs7.existsSync(path8.join(cwd, name))) return name;
|
|
2501
3355
|
}
|
|
2502
3356
|
return null;
|
|
2503
3357
|
}
|
|
3358
|
+
function packageJsonHasConfig(cwd) {
|
|
3359
|
+
try {
|
|
3360
|
+
const parsed = JSON.parse(fs7.readFileSync(path8.join(cwd, "package.json"), "utf8"));
|
|
3361
|
+
return typeof parsed.snapshot === "object" && parsed.snapshot !== null;
|
|
3362
|
+
} catch {
|
|
3363
|
+
return false;
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
2504
3366
|
function artifactAnswers(options) {
|
|
2505
3367
|
const artifacts = { ...DEFAULT_ANSWERS.artifacts };
|
|
2506
3368
|
for (const name of options.skipArtifacts ?? []) {
|
|
@@ -2516,20 +3378,20 @@ function artifactAnswers(options) {
|
|
|
2516
3378
|
}
|
|
2517
3379
|
function topLevelDirs(cwd) {
|
|
2518
3380
|
try {
|
|
2519
|
-
return
|
|
3381
|
+
return fs7.readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort();
|
|
2520
3382
|
} catch {
|
|
2521
3383
|
return [];
|
|
2522
3384
|
}
|
|
2523
3385
|
}
|
|
2524
3386
|
function addScripts(cwd, notes) {
|
|
2525
|
-
const packagePath =
|
|
2526
|
-
if (!
|
|
3387
|
+
const packagePath = path8.join(cwd, "package.json");
|
|
3388
|
+
if (!fs7.existsSync(packagePath)) {
|
|
2527
3389
|
notes.push("No package.json found, so npm scripts were not added.");
|
|
2528
3390
|
return [];
|
|
2529
3391
|
}
|
|
2530
3392
|
let parsed;
|
|
2531
3393
|
try {
|
|
2532
|
-
parsed = JSON.parse(
|
|
3394
|
+
parsed = JSON.parse(fs7.readFileSync(packagePath, "utf8"));
|
|
2533
3395
|
} catch {
|
|
2534
3396
|
notes.push("Could not parse package.json; npm scripts were not added.");
|
|
2535
3397
|
return [];
|
|
@@ -2546,18 +3408,18 @@ function addScripts(cwd, notes) {
|
|
|
2546
3408
|
}
|
|
2547
3409
|
if (added.length === 0) return [];
|
|
2548
3410
|
parsed.scripts = scripts;
|
|
2549
|
-
|
|
3411
|
+
fs7.writeFileSync(packagePath, `${JSON.stringify(parsed, null, 2)}
|
|
2550
3412
|
`, "utf8");
|
|
2551
3413
|
return added;
|
|
2552
3414
|
}
|
|
2553
3415
|
function addGitignore(cwd) {
|
|
2554
|
-
const ignorePath =
|
|
2555
|
-
const existing =
|
|
3416
|
+
const ignorePath = path8.join(cwd, ".gitignore");
|
|
3417
|
+
const existing = fs7.existsSync(ignorePath) ? fs7.readFileSync(ignorePath, "utf8") : "";
|
|
2556
3418
|
const wanted = ["snapshot/", "snapshot.tmp/"];
|
|
2557
3419
|
const added = wanted.filter((entry) => !existing.split(/\r?\n/).includes(entry));
|
|
2558
3420
|
if (added.length === 0) return [];
|
|
2559
3421
|
const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
2560
|
-
|
|
3422
|
+
fs7.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
|
|
2561
3423
|
`, "utf8");
|
|
2562
3424
|
return added;
|
|
2563
3425
|
}
|
|
@@ -2695,7 +3557,7 @@ async function runInit(options) {
|
|
|
2695
3557
|
}
|
|
2696
3558
|
const configText = options.minimal ? renderMinimalConfig(answers) : renderConfig(answers);
|
|
2697
3559
|
const configName = chooseConfigFileName(options.cwd);
|
|
2698
|
-
const configPath =
|
|
3560
|
+
const configPath = path8.join(options.cwd, configName);
|
|
2699
3561
|
if (options.dryRun) {
|
|
2700
3562
|
return {
|
|
2701
3563
|
configPath,
|
|
@@ -2713,10 +3575,15 @@ async function runInit(options) {
|
|
|
2713
3575
|
`A configuration already exists (${present}). Re-run with --force to replace it.`
|
|
2714
3576
|
);
|
|
2715
3577
|
}
|
|
2716
|
-
if (
|
|
3578
|
+
if (packageJsonHasConfig(options.cwd) && !options.force) {
|
|
3579
|
+
throw new Error(
|
|
3580
|
+
'package.json already defines a "snapshot" config. Remove that key, or re-run with --force to write a config file that takes precedence over it.'
|
|
3581
|
+
);
|
|
3582
|
+
}
|
|
3583
|
+
if (fs7.existsSync(configPath) && !options.force) {
|
|
2717
3584
|
throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
|
|
2718
3585
|
}
|
|
2719
|
-
|
|
3586
|
+
fs7.writeFileSync(configPath, configText, "utf8");
|
|
2720
3587
|
const scripts = addScripts(options.cwd, notes);
|
|
2721
3588
|
const gitignoreEntries = addGitignore(options.cwd);
|
|
2722
3589
|
return {
|
|
@@ -2733,6 +3600,7 @@ async function runInit(options) {
|
|
|
2733
3600
|
// src/index.ts
|
|
2734
3601
|
import * as crypto3 from "crypto";
|
|
2735
3602
|
import * as fs9 from "fs";
|
|
3603
|
+
import * as os from "os";
|
|
2736
3604
|
import * as path10 from "path";
|
|
2737
3605
|
|
|
2738
3606
|
// src/ast/profiles/shared.ts
|
|
@@ -3277,14 +4145,42 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3277
4145
|
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
3278
4146
|
return Boolean(modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword));
|
|
3279
4147
|
};
|
|
3280
|
-
const
|
|
4148
|
+
const rendersJsx = (node) => {
|
|
4149
|
+
const pending = [node];
|
|
4150
|
+
while (pending.length > 0) {
|
|
4151
|
+
const current = pending.pop();
|
|
4152
|
+
if (ts.isJsxElement(current) || ts.isJsxSelfClosingElement(current) || ts.isJsxFragment(current)) {
|
|
4153
|
+
return true;
|
|
4154
|
+
}
|
|
4155
|
+
if (ts.isCallExpression(current)) {
|
|
4156
|
+
const callee = current.expression;
|
|
4157
|
+
const calleeName = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
|
4158
|
+
if (calleeName === "createElement") return true;
|
|
4159
|
+
}
|
|
4160
|
+
ts.forEachChild(current, (child) => {
|
|
4161
|
+
pending.push(child);
|
|
4162
|
+
});
|
|
4163
|
+
}
|
|
4164
|
+
return false;
|
|
4165
|
+
};
|
|
4166
|
+
const REACT_BASE_CLASS = /(?:^|\.)(?:PureComponent|Component)$/;
|
|
4167
|
+
const extendsReactBase = (node) => {
|
|
4168
|
+
const heritage = (node.heritageClauses ?? []).find(
|
|
4169
|
+
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword
|
|
4170
|
+
);
|
|
4171
|
+
const expression = heritage?.types[0]?.expression;
|
|
4172
|
+
if (!expression) return false;
|
|
4173
|
+
const text2 = ts.isIdentifier(expression) ? expression.text : expression.getText(sourceFile);
|
|
4174
|
+
return REACT_BASE_CLASS.test(text2);
|
|
4175
|
+
};
|
|
4176
|
+
const recordFunctionLike = (name, exported, node) => {
|
|
3281
4177
|
summary.functions.push(name);
|
|
3282
4178
|
if (exported) {
|
|
3283
4179
|
summary.exports.push(name);
|
|
3284
4180
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
3285
4181
|
}
|
|
3286
4182
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
3287
|
-
if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
|
|
4183
|
+
if (/^[A-Z]/.test(name) && rendersJsx(node)) summary.reactComponents.push(name);
|
|
3288
4184
|
};
|
|
3289
4185
|
const stack = [sourceFile];
|
|
3290
4186
|
while (stack.length > 0) {
|
|
@@ -3313,7 +4209,7 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3313
4209
|
if (ts.isFunctionDeclaration(node)) {
|
|
3314
4210
|
const exported = isExported(node);
|
|
3315
4211
|
if (node.name) {
|
|
3316
|
-
recordFunctionLike(node.name.text, exported);
|
|
4212
|
+
recordFunctionLike(node.name.text, exported, node);
|
|
3317
4213
|
if (isDefault(node)) summary.exports.push("default");
|
|
3318
4214
|
} else if (isDefault(node)) {
|
|
3319
4215
|
summary.exports.push("default");
|
|
@@ -3324,7 +4220,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3324
4220
|
summary.classes.push(node.name.text);
|
|
3325
4221
|
if (isExported(node)) summary.exports.push(node.name.text);
|
|
3326
4222
|
if (isDefault(node)) summary.exports.push("default");
|
|
3327
|
-
if (/^[A-Z]/.test(node.name.text))
|
|
4223
|
+
if (/^[A-Z]/.test(node.name.text) && (extendsReactBase(node) || rendersJsx(node))) {
|
|
4224
|
+
summary.reactComponents.push(node.name.text);
|
|
4225
|
+
}
|
|
3328
4226
|
} else if (isDefault(node)) {
|
|
3329
4227
|
summary.exports.push("default");
|
|
3330
4228
|
}
|
|
@@ -3352,7 +4250,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3352
4250
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
3353
4251
|
}
|
|
3354
4252
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
3355
|
-
if (/^[A-Z]/.test(name)
|
|
4253
|
+
if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
|
|
4254
|
+
summary.reactComponents.push(name);
|
|
4255
|
+
}
|
|
3356
4256
|
}
|
|
3357
4257
|
}
|
|
3358
4258
|
const children = [];
|
|
@@ -3570,7 +4470,8 @@ function summarizeMarkdown(content) {
|
|
|
3570
4470
|
}
|
|
3571
4471
|
|
|
3572
4472
|
// src/generator/audit.ts
|
|
3573
|
-
function generateAuditReport(manifest, secretFindings) {
|
|
4473
|
+
function generateAuditReport(manifest, secretFindings, secretRules) {
|
|
4474
|
+
const mask = (value) => findSecretInPath(value, secretRules)?.masked ?? value;
|
|
3574
4475
|
const lines = [
|
|
3575
4476
|
"# Codebase Health & Security Audit",
|
|
3576
4477
|
"",
|
|
@@ -3579,7 +4480,11 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
3579
4480
|
`**Total Files:** ${manifest.counts.totalFiles} `,
|
|
3580
4481
|
`**Total Lines:** ${manifest.counts.totalLines.toLocaleString()} `,
|
|
3581
4482
|
`**Total Size:** ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB `,
|
|
3582
|
-
|
|
4483
|
+
// This counts the bytes that were *emitted* after redaction, not the bytes
|
|
4484
|
+
// that were masked: calling it "redacted bytes" made a repository with zero
|
|
4485
|
+
// secrets report a non-zero redacted-byte figure directly above "no secrets
|
|
4486
|
+
// detected". The number of masked spans is reported in section 1 instead.
|
|
4487
|
+
`**Emitted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
|
|
3583
4488
|
"",
|
|
3584
4489
|
"## 1. Security Scan & Redaction",
|
|
3585
4490
|
"",
|
|
@@ -3601,7 +4506,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
3601
4506
|
"| :--- | :--- | :--- |"
|
|
3602
4507
|
);
|
|
3603
4508
|
for (const finding of secretFindings) {
|
|
3604
|
-
lines.push(`| \`${finding.path ?? "unknown"}\` | ${finding.line} | \`${finding.rule}\` |`);
|
|
4509
|
+
lines.push(`| \`${mask(finding.path ?? "unknown")}\` | ${finding.line} | \`${finding.rule}\` |`);
|
|
3605
4510
|
}
|
|
3606
4511
|
lines.push("", "### Matches by rule", "", "| Rule | Matches |", "| :--- | :--- |");
|
|
3607
4512
|
for (const [rule, count] of Array.from(byRule.entries()).sort((a, b) => b[1] - a[1])) {
|
|
@@ -3623,7 +4528,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
3623
4528
|
for (const file of largeFiles) {
|
|
3624
4529
|
const sizeKb = (file.sizeBytes / 1024).toFixed(1);
|
|
3625
4530
|
const status = file.isPreviewTruncated ? "Truncated Head/Tail" : "Full";
|
|
3626
|
-
lines.push(`| \`${file.relativePath}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
|
|
4531
|
+
lines.push(`| \`${mask(file.relativePath)}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
|
|
3627
4532
|
}
|
|
3628
4533
|
lines.push("");
|
|
3629
4534
|
}
|
|
@@ -3665,7 +4570,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
3665
4570
|
"| :--- | :--- | :--- |"
|
|
3666
4571
|
);
|
|
3667
4572
|
for (const skip of manifest.skipped) {
|
|
3668
|
-
lines.push(`| \`${skip.path}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
|
|
4573
|
+
lines.push(`| \`${mask(skip.path)}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
|
|
3669
4574
|
}
|
|
3670
4575
|
lines.push("");
|
|
3671
4576
|
}
|
|
@@ -3743,6 +4648,8 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
3743
4648
|
layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
|
|
3744
4649
|
kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
|
|
3745
4650
|
}
|
|
4651
|
+
const pathLines = included.map((file) => displayPath(file.relativePath));
|
|
4652
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
3746
4653
|
const header = [
|
|
3747
4654
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
3748
4655
|
"",
|
|
@@ -3755,9 +4662,9 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
3755
4662
|
"",
|
|
3756
4663
|
"## Files",
|
|
3757
4664
|
"",
|
|
3758
|
-
|
|
3759
|
-
...
|
|
3760
|
-
|
|
4665
|
+
`${listFence}text`,
|
|
4666
|
+
...pathLines,
|
|
4667
|
+
listFence,
|
|
3761
4668
|
"",
|
|
3762
4669
|
"## Contents",
|
|
3763
4670
|
""
|
|
@@ -3785,7 +4692,8 @@ function capValue(value, max) {
|
|
|
3785
4692
|
return `${value.slice(0, half)}\u2026${value.slice(value.length - half)}`;
|
|
3786
4693
|
}
|
|
3787
4694
|
var capMetadata = (value) => capValue(value, METADATA_VALUE_MAX);
|
|
3788
|
-
var
|
|
4695
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
|
|
4696
|
+
var displayPath = (value) => capValue(value.replace(CONTROL_CHARS, " "), DISPLAY_PATH_MAX);
|
|
3789
4697
|
function topEntries(counts, limit) {
|
|
3790
4698
|
return Object.entries(counts).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)).slice(0, limit).map(([key, value]) => `${capMetadata(key)} (${value})`).join(", ");
|
|
3791
4699
|
}
|
|
@@ -3881,6 +4789,8 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
3881
4789
|
layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
|
|
3882
4790
|
kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
|
|
3883
4791
|
}
|
|
4792
|
+
const pathLines = group.map((s) => displayPath(s.file.relativePath));
|
|
4793
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
3884
4794
|
const header = [
|
|
3885
4795
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
3886
4796
|
"",
|
|
@@ -3893,9 +4803,9 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
3893
4803
|
"",
|
|
3894
4804
|
"## Files",
|
|
3895
4805
|
"",
|
|
3896
|
-
|
|
3897
|
-
...
|
|
3898
|
-
|
|
4806
|
+
`${listFence}text`,
|
|
4807
|
+
...pathLines,
|
|
4808
|
+
listFence,
|
|
3899
4809
|
"",
|
|
3900
4810
|
"## Contents",
|
|
3901
4811
|
""
|
|
@@ -3954,9 +4864,12 @@ function renderFileSection(file) {
|
|
|
3954
4864
|
].join("\n");
|
|
3955
4865
|
}
|
|
3956
4866
|
function fenceForContent(content) {
|
|
4867
|
+
return fenceForText(content, 4);
|
|
4868
|
+
}
|
|
4869
|
+
function fenceForText(content, minimum) {
|
|
3957
4870
|
const matches = content.match(/`+/g);
|
|
3958
4871
|
const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
|
|
3959
|
-
return "`".repeat(Math.max(
|
|
4872
|
+
return "`".repeat(Math.max(minimum, maxRun + 1));
|
|
3960
4873
|
}
|
|
3961
4874
|
function renderSummary(summary) {
|
|
3962
4875
|
if (!summary) return [];
|
|
@@ -4105,6 +5018,7 @@ function generateReadme(manifest) {
|
|
|
4105
5018
|
|
|
4106
5019
|
// src/generator/tree.ts
|
|
4107
5020
|
var MAX_TREE_LINES = 2e4;
|
|
5021
|
+
var CONTROL_CHARS2 = /[\u0000-\u001f\u007f]/g;
|
|
4108
5022
|
function generateProjectTree(filePaths) {
|
|
4109
5023
|
const root = { children: /* @__PURE__ */ new Map() };
|
|
4110
5024
|
for (const filePath of filePaths) {
|
|
@@ -4119,7 +5033,7 @@ function generateProjectTree(filePaths) {
|
|
|
4119
5033
|
current = child;
|
|
4120
5034
|
}
|
|
4121
5035
|
}
|
|
4122
|
-
const lines = ["
|
|
5036
|
+
const lines = ["."];
|
|
4123
5037
|
let truncatedAt = -1;
|
|
4124
5038
|
function walk(node, prefix) {
|
|
4125
5039
|
if (truncatedAt !== -1) return;
|
|
@@ -4134,7 +5048,8 @@ function generateProjectTree(filePaths) {
|
|
|
4134
5048
|
const isLast = index === keys.length - 1;
|
|
4135
5049
|
const child = node.children.get(key);
|
|
4136
5050
|
const isDirectory = child.children.size > 0;
|
|
4137
|
-
|
|
5051
|
+
const label = key.replace(CONTROL_CHARS2, " ");
|
|
5052
|
+
lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
|
|
4138
5053
|
if (lines.length >= MAX_TREE_LINES) {
|
|
4139
5054
|
truncatedAt = lines.length;
|
|
4140
5055
|
return;
|
|
@@ -4146,87 +5061,17 @@ function generateProjectTree(filePaths) {
|
|
|
4146
5061
|
if (truncatedAt !== -1) {
|
|
4147
5062
|
lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
|
|
4148
5063
|
}
|
|
4149
|
-
lines.
|
|
4150
|
-
return lines.join("\n");
|
|
4151
|
-
}
|
|
4152
|
-
|
|
4153
|
-
// src/scanner/binary.ts
|
|
4154
|
-
import * as fs7 from "fs";
|
|
4155
|
-
import * as path8 from "path";
|
|
4156
|
-
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4157
|
-
".png",
|
|
4158
|
-
".jpg",
|
|
4159
|
-
".jpeg",
|
|
4160
|
-
".gif",
|
|
4161
|
-
".webp",
|
|
4162
|
-
".avif",
|
|
4163
|
-
".ico",
|
|
4164
|
-
".bmp",
|
|
4165
|
-
".tiff",
|
|
4166
|
-
".pdf",
|
|
4167
|
-
".zip",
|
|
4168
|
-
".tar",
|
|
4169
|
-
".gz",
|
|
4170
|
-
".tgz",
|
|
4171
|
-
".bz2",
|
|
4172
|
-
".xz",
|
|
4173
|
-
".7z",
|
|
4174
|
-
".rar",
|
|
4175
|
-
".mp3",
|
|
4176
|
-
".mp4",
|
|
4177
|
-
".wav",
|
|
4178
|
-
".ogg",
|
|
4179
|
-
".webm",
|
|
4180
|
-
".mov",
|
|
4181
|
-
".avi",
|
|
4182
|
-
".woff",
|
|
4183
|
-
".woff2",
|
|
4184
|
-
".ttf",
|
|
4185
|
-
".otf",
|
|
4186
|
-
".eot",
|
|
4187
|
-
".wasm",
|
|
4188
|
-
".node",
|
|
4189
|
-
".exe",
|
|
4190
|
-
".dll",
|
|
4191
|
-
".dylib",
|
|
4192
|
-
".so"
|
|
4193
|
-
]);
|
|
4194
|
-
function isBinaryExtension(filePath) {
|
|
4195
|
-
const ext = path8.extname(filePath).toLowerCase();
|
|
4196
|
-
return BINARY_EXTENSIONS.has(ext);
|
|
4197
|
-
}
|
|
4198
|
-
function isBinaryBuffer(buffer) {
|
|
4199
|
-
const checkLength = Math.min(buffer.length, 8192);
|
|
4200
|
-
for (let index = 0; index < checkLength; index++) {
|
|
4201
|
-
if (buffer[index] === 0) return true;
|
|
4202
|
-
}
|
|
4203
|
-
return false;
|
|
4204
|
-
}
|
|
4205
|
-
function isBinaryFile(absolutePath) {
|
|
4206
|
-
if (isBinaryExtension(absolutePath)) return true;
|
|
4207
|
-
let fd;
|
|
4208
|
-
try {
|
|
4209
|
-
fd = fs7.openSync(absolutePath, "r");
|
|
4210
|
-
const buffer = Buffer.alloc(8192);
|
|
4211
|
-
const bytesRead = fs7.readSync(fd, buffer, 0, 8192, 0);
|
|
4212
|
-
if (bytesRead === 0) return false;
|
|
4213
|
-
return isBinaryBuffer(buffer.subarray(0, bytesRead));
|
|
4214
|
-
} catch {
|
|
4215
|
-
return false;
|
|
4216
|
-
} finally {
|
|
4217
|
-
if (fd !== void 0) {
|
|
4218
|
-
try {
|
|
4219
|
-
fs7.closeSync(fd);
|
|
4220
|
-
} catch {
|
|
4221
|
-
}
|
|
4222
|
-
}
|
|
4223
|
-
}
|
|
5064
|
+
const fence = fenceForText(lines.join("\n"), 3);
|
|
5065
|
+
return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
|
|
4224
5066
|
}
|
|
4225
5067
|
|
|
4226
5068
|
// src/validator/validate.ts
|
|
4227
5069
|
import * as crypto2 from "crypto";
|
|
4228
5070
|
import * as fs8 from "fs";
|
|
4229
5071
|
import * as path9 from "path";
|
|
5072
|
+
function maskedPath(value, secretRules) {
|
|
5073
|
+
return findSecretInPath(value, secretRules)?.masked ?? value;
|
|
5074
|
+
}
|
|
4230
5075
|
function emptyDrift() {
|
|
4231
5076
|
return {
|
|
4232
5077
|
missingInSnapshot: [],
|
|
@@ -4240,6 +5085,15 @@ function emptyDrift() {
|
|
|
4240
5085
|
function sha256Hex(buffer) {
|
|
4241
5086
|
return crypto2.createHash("sha256").update(buffer).digest("hex");
|
|
4242
5087
|
}
|
|
5088
|
+
function wouldNowBeIncluded(cwd, relativePath) {
|
|
5089
|
+
const absolutePath = path9.join(cwd, relativePath);
|
|
5090
|
+
try {
|
|
5091
|
+
if (!fs8.statSync(absolutePath).isFile()) return false;
|
|
5092
|
+
return !isBinaryFile(absolutePath);
|
|
5093
|
+
} catch {
|
|
5094
|
+
return false;
|
|
5095
|
+
}
|
|
5096
|
+
}
|
|
4243
5097
|
function validateSnapshot(configInput) {
|
|
4244
5098
|
assertUsableConfigInput(configInput);
|
|
4245
5099
|
const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
|
|
@@ -4288,15 +5142,20 @@ function validateSnapshot(configInput) {
|
|
|
4288
5142
|
const skippedSet = new Set(
|
|
4289
5143
|
manifest.skipped.filter((entry) => entry && typeof entry.path === "string").map((entry) => entry.path)
|
|
4290
5144
|
);
|
|
5145
|
+
const isSkippedOnDisk = (diskFile) => {
|
|
5146
|
+
if (skippedSet.has(diskFile)) return true;
|
|
5147
|
+
return skippedSet.has(maskedPath(diskFile, config.secretRules));
|
|
5148
|
+
};
|
|
4291
5149
|
const collection = collectFiles(config.cwd, {
|
|
4292
5150
|
includePatterns: config.include,
|
|
4293
5151
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
4294
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
5152
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
5153
|
+
secretRules: config.secretRules
|
|
4295
5154
|
});
|
|
4296
5155
|
for (const diskFile of collection.files) {
|
|
4297
|
-
if (skippedSet.has(diskFile)) continue;
|
|
4298
5156
|
const snapshotHash = manifestFileMap.get(diskFile);
|
|
4299
5157
|
if (!snapshotHash) {
|
|
5158
|
+
if (isSkippedOnDisk(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
|
|
4300
5159
|
drift.missingInSnapshot.push(diskFile);
|
|
4301
5160
|
continue;
|
|
4302
5161
|
}
|
|
@@ -4565,10 +5424,19 @@ function validateSnapshot(configInput) {
|
|
|
4565
5424
|
`${drift.configMismatches.length} configuration mismatch(es) between the manifest and this run.`
|
|
4566
5425
|
);
|
|
4567
5426
|
}
|
|
5427
|
+
const maskedDrift = {
|
|
5428
|
+
...drift,
|
|
5429
|
+
missingInSnapshot: drift.missingInSnapshot.map((value) => maskedPath(value, config.secretRules)),
|
|
5430
|
+
missingOnDisk: drift.missingOnDisk.map((value) => maskedPath(value, config.secretRules)),
|
|
5431
|
+
hashMismatches: drift.hashMismatches.map((mismatch) => ({
|
|
5432
|
+
...mismatch,
|
|
5433
|
+
path: maskedPath(mismatch.path, config.secretRules)
|
|
5434
|
+
}))
|
|
5435
|
+
};
|
|
4568
5436
|
return {
|
|
4569
5437
|
valid: errors.length === 0,
|
|
4570
5438
|
manifestPresent: true,
|
|
4571
|
-
drift,
|
|
5439
|
+
drift: maskedDrift,
|
|
4572
5440
|
errors,
|
|
4573
5441
|
warnings
|
|
4574
5442
|
};
|
|
@@ -4576,7 +5444,7 @@ function validateSnapshot(configInput) {
|
|
|
4576
5444
|
|
|
4577
5445
|
// src/index.ts
|
|
4578
5446
|
var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
|
|
4579
|
-
var GENERATOR_VERSION = "
|
|
5447
|
+
var GENERATOR_VERSION = "4.0.1";
|
|
4580
5448
|
var SCHEMA_VERSION = "3.0.0";
|
|
4581
5449
|
function sha256Hex2(input) {
|
|
4582
5450
|
return crypto3.createHash("sha256").update(input).digest("hex");
|
|
@@ -4613,7 +5481,6 @@ function bump(counts, key) {
|
|
|
4613
5481
|
}
|
|
4614
5482
|
var RELATIVE_SPECIFIER = /^\.{1,2}\//;
|
|
4615
5483
|
var IMPORT_CANDIDATE_SUFFIXES = [
|
|
4616
|
-
"",
|
|
4617
5484
|
".ts",
|
|
4618
5485
|
".tsx",
|
|
4619
5486
|
".mts",
|
|
@@ -4627,23 +5494,59 @@ var IMPORT_CANDIDATE_SUFFIXES = [
|
|
|
4627
5494
|
"/index.js",
|
|
4628
5495
|
"/index.mjs"
|
|
4629
5496
|
];
|
|
4630
|
-
|
|
5497
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5498
|
+
".ts",
|
|
5499
|
+
".tsx",
|
|
5500
|
+
".mts",
|
|
5501
|
+
".cts",
|
|
5502
|
+
".js",
|
|
5503
|
+
".jsx",
|
|
5504
|
+
".mjs",
|
|
5505
|
+
".cjs"
|
|
5506
|
+
]);
|
|
5507
|
+
function buildImportResolver(files) {
|
|
5508
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
5509
|
+
const byPathFolded = /* @__PURE__ */ new Map();
|
|
5510
|
+
const byStem = /* @__PURE__ */ new Map();
|
|
5511
|
+
const byStemFolded = /* @__PURE__ */ new Map();
|
|
5512
|
+
const remember = (map, key, value) => {
|
|
5513
|
+
if (!map.has(key)) map.set(key, value);
|
|
5514
|
+
};
|
|
5515
|
+
for (const file of files) {
|
|
5516
|
+
const relativePath = file.relativePath;
|
|
5517
|
+
remember(byPath, relativePath, relativePath);
|
|
5518
|
+
remember(byPathFolded, relativePath.toLowerCase(), relativePath);
|
|
5519
|
+
const extension = path10.posix.extname(relativePath);
|
|
5520
|
+
if (!SOURCE_EXTENSIONS.has(extension)) continue;
|
|
5521
|
+
const stem = relativePath.slice(0, -extension.length);
|
|
5522
|
+
remember(byStem, stem, relativePath);
|
|
5523
|
+
remember(byStemFolded, stem.toLowerCase(), relativePath);
|
|
5524
|
+
if (path10.posix.basename(stem) === "index") {
|
|
5525
|
+
const directory = path10.posix.dirname(stem);
|
|
5526
|
+
remember(byStem, directory, relativePath);
|
|
5527
|
+
remember(byStemFolded, directory.toLowerCase(), relativePath);
|
|
5528
|
+
}
|
|
5529
|
+
}
|
|
5530
|
+
return { byPath, byPathFolded, byStem, byStemFolded };
|
|
5531
|
+
}
|
|
5532
|
+
function resolveSpecifier(fromFile, specifier, resolver) {
|
|
4631
5533
|
if (!RELATIVE_SPECIFIER.test(specifier)) return null;
|
|
4632
|
-
const
|
|
4633
|
-
|
|
4634
|
-
|
|
5534
|
+
const joined = path10.posix.normalize(
|
|
5535
|
+
path10.posix.join(path10.posix.dirname(fromFile), specifier)
|
|
5536
|
+
);
|
|
5537
|
+
const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
|
|
5538
|
+
const direct = exact(joined);
|
|
5539
|
+
if (direct) return direct;
|
|
4635
5540
|
for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
|
|
4636
|
-
const
|
|
4637
|
-
if (
|
|
5541
|
+
const hit = exact(`${joined}${suffix}`);
|
|
5542
|
+
if (hit) return hit;
|
|
4638
5543
|
}
|
|
4639
|
-
const
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
}
|
|
4643
|
-
return null;
|
|
5544
|
+
const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
|
|
5545
|
+
const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
|
|
5546
|
+
return byStem(joined) ?? byStem(stem);
|
|
4644
5547
|
}
|
|
4645
5548
|
function buildIndexes(files) {
|
|
4646
|
-
const
|
|
5549
|
+
const resolver = buildImportResolver(files);
|
|
4647
5550
|
const importerCounts = /* @__PURE__ */ new Map();
|
|
4648
5551
|
const byFile = files.filter((file) => isSourceSummary(file.summary)).map((file) => {
|
|
4649
5552
|
const summary = file.summary;
|
|
@@ -4655,7 +5558,7 @@ function buildIndexes(files) {
|
|
|
4655
5558
|
});
|
|
4656
5559
|
for (const entry of byFile) {
|
|
4657
5560
|
for (const specifier of entry.imports) {
|
|
4658
|
-
const resolved = resolveSpecifier(entry.path, specifier,
|
|
5561
|
+
const resolved = resolveSpecifier(entry.path, specifier, resolver);
|
|
4659
5562
|
if (resolved) importerCounts.set(resolved, (importerCounts.get(resolved) ?? 0) + 1);
|
|
4660
5563
|
}
|
|
4661
5564
|
}
|
|
@@ -4746,29 +5649,224 @@ function resolveSafeOutputDirs(config) {
|
|
|
4746
5649
|
return { outDir, tmpDir };
|
|
4747
5650
|
}
|
|
4748
5651
|
var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
|
|
5652
|
+
var RUN_DIR_PREFIX = "run-";
|
|
5653
|
+
var STALE_RUN_MS = 60 * 60 * 1e3;
|
|
5654
|
+
function maskPathText(value, secretRules) {
|
|
5655
|
+
return findSecretInPath(value, secretRules)?.masked ?? value;
|
|
5656
|
+
}
|
|
5657
|
+
function maskSkipRecords(records, secretRules) {
|
|
5658
|
+
return maskSkipPaths(records, secretRules);
|
|
5659
|
+
}
|
|
5660
|
+
function isProcessAlive(pid) {
|
|
5661
|
+
try {
|
|
5662
|
+
process.kill(pid, 0);
|
|
5663
|
+
return true;
|
|
5664
|
+
} catch (err) {
|
|
5665
|
+
return err.code !== "ESRCH";
|
|
5666
|
+
}
|
|
5667
|
+
}
|
|
4749
5668
|
var OWNERSHIP_MARKER = ".codebase-snapshot";
|
|
4750
|
-
|
|
5669
|
+
var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
|
|
5670
|
+
var SNAPSHOT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5671
|
+
function listEntries(dir) {
|
|
5672
|
+
const entries = [];
|
|
5673
|
+
const walk = (absolute, prefix) => {
|
|
5674
|
+
let children;
|
|
5675
|
+
try {
|
|
5676
|
+
children = fs9.readdirSync(absolute, { withFileTypes: true });
|
|
5677
|
+
} catch {
|
|
5678
|
+
return;
|
|
5679
|
+
}
|
|
5680
|
+
for (const child of children) {
|
|
5681
|
+
const relative5 = prefix === "" ? child.name : `${prefix}/${child.name}`;
|
|
5682
|
+
entries.push(relative5);
|
|
5683
|
+
if (child.isDirectory()) walk(path10.join(absolute, child.name), relative5);
|
|
5684
|
+
}
|
|
5685
|
+
};
|
|
5686
|
+
walk(dir, "");
|
|
5687
|
+
return entries.sort();
|
|
5688
|
+
}
|
|
5689
|
+
function markerPayload(dir, state, id, createdAt) {
|
|
4751
5690
|
const marker2 = {
|
|
4752
5691
|
generator: GENERATOR_NAME,
|
|
4753
5692
|
version: GENERATOR_VERSION,
|
|
4754
|
-
schemaVersion: SCHEMA_VERSION
|
|
5693
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5694
|
+
createdAt,
|
|
5695
|
+
state,
|
|
5696
|
+
id,
|
|
5697
|
+
pid: process.pid,
|
|
5698
|
+
entries: listEntries(dir)
|
|
4755
5699
|
};
|
|
4756
|
-
fs9.writeFileSync(path10.join(dir, OWNERSHIP_MARKER), `${JSON.stringify(marker2, null, 2)}
|
|
4757
|
-
`, "utf8");
|
|
4758
|
-
}
|
|
4759
|
-
function isOwnOutput(dir) {
|
|
4760
|
-
if (fs9.existsSync(path10.join(dir, OWNERSHIP_MARKER))) return true;
|
|
4761
5700
|
const manifestPath = path10.join(dir, "manifest.json");
|
|
4762
|
-
if (
|
|
5701
|
+
if (fs9.existsSync(manifestPath)) {
|
|
5702
|
+
marker2.manifestSha256 = sha256Hex2(fs9.readFileSync(manifestPath));
|
|
5703
|
+
}
|
|
5704
|
+
return marker2;
|
|
5705
|
+
}
|
|
5706
|
+
function writeOwnershipMarker(dir, marker2) {
|
|
5707
|
+
fs9.writeFileSync(
|
|
5708
|
+
path10.join(dir, OWNERSHIP_MARKER),
|
|
5709
|
+
`${JSON.stringify(marker2, null, 2)}
|
|
5710
|
+
`,
|
|
5711
|
+
"utf8"
|
|
5712
|
+
);
|
|
5713
|
+
}
|
|
5714
|
+
function readOwnershipMarker(dir) {
|
|
5715
|
+
const markerPath = path10.join(dir, OWNERSHIP_MARKER);
|
|
5716
|
+
if (!fs9.existsSync(markerPath)) {
|
|
5717
|
+
return { marker: null, problem: "it holds no .codebase-snapshot marker" };
|
|
5718
|
+
}
|
|
5719
|
+
let raw;
|
|
4763
5720
|
try {
|
|
4764
|
-
|
|
4765
|
-
const generator = parsed.generator;
|
|
4766
|
-
return typeof parsed.schemaVersion === "string" && Array.isArray(parsed.files) && typeof generator === "object" && generator !== null && generator.name === GENERATOR_NAME;
|
|
5721
|
+
raw = fs9.readFileSync(markerPath, "utf8");
|
|
4767
5722
|
} catch {
|
|
4768
|
-
return
|
|
5723
|
+
return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
|
|
5724
|
+
}
|
|
5725
|
+
if (raw.trim().length === 0) {
|
|
5726
|
+
return { marker: null, problem: "its .codebase-snapshot marker is empty" };
|
|
5727
|
+
}
|
|
5728
|
+
let parsed;
|
|
5729
|
+
try {
|
|
5730
|
+
parsed = JSON.parse(raw);
|
|
5731
|
+
} catch {
|
|
5732
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not valid JSON" };
|
|
5733
|
+
}
|
|
5734
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5735
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not a JSON object" };
|
|
5736
|
+
}
|
|
5737
|
+
const candidate = parsed;
|
|
5738
|
+
if (candidate.generator !== GENERATOR_NAME) {
|
|
5739
|
+
return { marker: null, problem: "its .codebase-snapshot marker names a different generator" };
|
|
5740
|
+
}
|
|
5741
|
+
if (typeof candidate.schemaVersion !== "string" || !SUPPORTED_MARKER_SCHEMAS.has(candidate.schemaVersion)) {
|
|
5742
|
+
return {
|
|
5743
|
+
marker: null,
|
|
5744
|
+
problem: `its .codebase-snapshot marker has a schema version this build does not understand (${String(candidate.schemaVersion)})`
|
|
5745
|
+
};
|
|
5746
|
+
}
|
|
5747
|
+
if (typeof candidate.id !== "string" || !SNAPSHOT_ID_PATTERN.test(candidate.id)) {
|
|
5748
|
+
return { marker: null, problem: "its .codebase-snapshot marker carries no snapshot id" };
|
|
5749
|
+
}
|
|
5750
|
+
if (candidate.state !== "staging" && candidate.state !== "complete") {
|
|
5751
|
+
return { marker: null, problem: "its .codebase-snapshot marker records no run state" };
|
|
5752
|
+
}
|
|
5753
|
+
return {
|
|
5754
|
+
marker: {
|
|
5755
|
+
generator: GENERATOR_NAME,
|
|
5756
|
+
version: typeof candidate.version === "string" ? candidate.version : "unknown",
|
|
5757
|
+
schemaVersion: candidate.schemaVersion,
|
|
5758
|
+
createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
|
|
5759
|
+
state: candidate.state,
|
|
5760
|
+
id: candidate.id,
|
|
5761
|
+
pid: typeof candidate.pid === "number" ? candidate.pid : void 0,
|
|
5762
|
+
manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
|
|
5763
|
+
entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
|
|
5764
|
+
},
|
|
5765
|
+
problem: null
|
|
5766
|
+
};
|
|
5767
|
+
}
|
|
5768
|
+
function describeManifestProblem(value) {
|
|
5769
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "is not a JSON object";
|
|
5770
|
+
const candidate = value;
|
|
5771
|
+
const generator = candidate.generator;
|
|
5772
|
+
if (typeof generator !== "object" || generator === null || generator.name !== GENERATOR_NAME) {
|
|
5773
|
+
return "was not written by this generator";
|
|
5774
|
+
}
|
|
5775
|
+
if (!Array.isArray(candidate.files)) return "has no files array";
|
|
5776
|
+
return null;
|
|
5777
|
+
}
|
|
5778
|
+
function assessSnapshotDirectory(dir) {
|
|
5779
|
+
const { marker: marker2, problem } = readOwnershipMarker(dir);
|
|
5780
|
+
if (!marker2) return { owned: false, problem, foreignEntries: [], marker: null };
|
|
5781
|
+
if (marker2.state !== "complete") {
|
|
5782
|
+
return {
|
|
5783
|
+
owned: false,
|
|
5784
|
+
problem: "its .codebase-snapshot marker records a run that never finished writing it",
|
|
5785
|
+
foreignEntries: [],
|
|
5786
|
+
marker: marker2
|
|
5787
|
+
};
|
|
5788
|
+
}
|
|
5789
|
+
const manifestPath = path10.join(dir, "manifest.json");
|
|
5790
|
+
if (fs9.existsSync(manifestPath)) {
|
|
5791
|
+
let raw;
|
|
5792
|
+
try {
|
|
5793
|
+
raw = fs9.readFileSync(manifestPath, "utf8");
|
|
5794
|
+
} catch {
|
|
5795
|
+
return {
|
|
5796
|
+
owned: false,
|
|
5797
|
+
problem: "its manifest.json could not be read",
|
|
5798
|
+
foreignEntries: [],
|
|
5799
|
+
marker: marker2
|
|
5800
|
+
};
|
|
5801
|
+
}
|
|
5802
|
+
let parsed;
|
|
5803
|
+
try {
|
|
5804
|
+
parsed = JSON.parse(raw);
|
|
5805
|
+
} catch {
|
|
5806
|
+
return {
|
|
5807
|
+
owned: false,
|
|
5808
|
+
problem: "its manifest.json is not valid JSON",
|
|
5809
|
+
foreignEntries: [],
|
|
5810
|
+
marker: marker2
|
|
5811
|
+
};
|
|
5812
|
+
}
|
|
5813
|
+
const manifestProblem2 = describeManifestProblem(parsed);
|
|
5814
|
+
if (manifestProblem2) {
|
|
5815
|
+
return {
|
|
5816
|
+
owned: false,
|
|
5817
|
+
problem: `its manifest.json ${manifestProblem2}`,
|
|
5818
|
+
foreignEntries: [],
|
|
5819
|
+
marker: marker2
|
|
5820
|
+
};
|
|
5821
|
+
}
|
|
5822
|
+
if (!marker2.manifestSha256) {
|
|
5823
|
+
return {
|
|
5824
|
+
owned: false,
|
|
5825
|
+
problem: "its .codebase-snapshot marker records no manifest digest",
|
|
5826
|
+
foreignEntries: [],
|
|
5827
|
+
marker: marker2
|
|
5828
|
+
};
|
|
5829
|
+
}
|
|
5830
|
+
if (sha256Hex2(raw) !== marker2.manifestSha256) {
|
|
5831
|
+
return {
|
|
5832
|
+
owned: false,
|
|
5833
|
+
problem: "its manifest.json does not match the digest recorded in its .codebase-snapshot marker",
|
|
5834
|
+
foreignEntries: [],
|
|
5835
|
+
marker: marker2
|
|
5836
|
+
};
|
|
5837
|
+
}
|
|
5838
|
+
} else if (marker2.manifestSha256) {
|
|
5839
|
+
return {
|
|
5840
|
+
owned: false,
|
|
5841
|
+
problem: "its manifest.json is missing although its marker records one",
|
|
5842
|
+
foreignEntries: [],
|
|
5843
|
+
marker: marker2
|
|
5844
|
+
};
|
|
5845
|
+
}
|
|
5846
|
+
const recorded = new Set(marker2.entries ?? []);
|
|
5847
|
+
if (recorded.size === 0) {
|
|
5848
|
+
return {
|
|
5849
|
+
owned: false,
|
|
5850
|
+
problem: "its .codebase-snapshot marker records no contents",
|
|
5851
|
+
foreignEntries: [],
|
|
5852
|
+
marker: marker2
|
|
5853
|
+
};
|
|
4769
5854
|
}
|
|
5855
|
+
const foreignEntries = listEntries(dir).filter(
|
|
5856
|
+
(entry) => !recorded.has(entry) && !PLACEHOLDER_ENTRIES.has(entry)
|
|
5857
|
+
);
|
|
5858
|
+
return {
|
|
5859
|
+
owned: foreignEntries.length === 0,
|
|
5860
|
+
problem: null,
|
|
5861
|
+
foreignEntries,
|
|
5862
|
+
marker: marker2
|
|
5863
|
+
};
|
|
5864
|
+
}
|
|
5865
|
+
function describeEntries(entries, limit = 5) {
|
|
5866
|
+
const shown = entries.slice(0, limit).join(", ");
|
|
5867
|
+
return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
|
|
4770
5868
|
}
|
|
4771
|
-
function assertReplaceable(target, label) {
|
|
5869
|
+
function assertReplaceable(target, label, secretRules) {
|
|
4772
5870
|
if (!fs9.existsSync(target)) return;
|
|
4773
5871
|
if (!fs9.statSync(target).isDirectory()) {
|
|
4774
5872
|
throw new Error(
|
|
@@ -4776,26 +5874,210 @@ function assertReplaceable(target, label) {
|
|
|
4776
5874
|
);
|
|
4777
5875
|
}
|
|
4778
5876
|
const entries = fs9.readdirSync(target);
|
|
4779
|
-
|
|
4780
|
-
if (
|
|
4781
|
-
|
|
4782
|
-
const
|
|
4783
|
-
|
|
5877
|
+
if (entries.length === 0) return;
|
|
5878
|
+
if (entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
|
|
5879
|
+
const { problem } = readOwnershipMarker(target);
|
|
5880
|
+
const verdict = assessSnapshotDirectory(target);
|
|
5881
|
+
if (verdict.owned) return;
|
|
5882
|
+
const shownForeign = verdict.foreignEntries.map((name) => maskPathText(name, secretRules));
|
|
5883
|
+
const shownEntries = entries.map((name) => maskPathText(name, secretRules));
|
|
5884
|
+
if (verdict.foreignEntries.length > 0) {
|
|
5885
|
+
throw new Error(
|
|
5886
|
+
`Refusing to replace ${label} ${target}: it carries this generator's ownership marker, but these entries are not part of the snapshot it recorded: ${describeEntries(shownForeign)}. Remove them yourself, or point ${label} at a new or empty directory.`
|
|
5887
|
+
);
|
|
5888
|
+
}
|
|
5889
|
+
const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
|
|
4784
5890
|
throw new Error(
|
|
4785
|
-
`Refusing to replace ${label} ${target}:
|
|
5891
|
+
`Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(shownEntries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
|
|
4786
5892
|
);
|
|
4787
5893
|
}
|
|
4788
|
-
function
|
|
5894
|
+
function assertStagingRootUsable(tmpDir, secretRules) {
|
|
5895
|
+
if (!fs9.existsSync(tmpDir)) return;
|
|
5896
|
+
if (!fs9.statSync(tmpDir).isDirectory()) {
|
|
5897
|
+
throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
|
|
5898
|
+
}
|
|
5899
|
+
const entries = fs9.readdirSync(tmpDir);
|
|
5900
|
+
if (entries.length === 0) return;
|
|
5901
|
+
const foreign = entries.filter((name) => {
|
|
5902
|
+
const full = path10.join(tmpDir, name);
|
|
5903
|
+
if (!name.startsWith(RUN_DIR_PREFIX)) return true;
|
|
5904
|
+
let stats;
|
|
5905
|
+
try {
|
|
5906
|
+
stats = fs9.statSync(full);
|
|
5907
|
+
} catch {
|
|
5908
|
+
return true;
|
|
5909
|
+
}
|
|
5910
|
+
if (!stats.isDirectory()) return true;
|
|
5911
|
+
const { marker: marker2 } = readOwnershipMarker(full);
|
|
5912
|
+
if (marker2) return false;
|
|
5913
|
+
try {
|
|
5914
|
+
return fs9.readdirSync(full).length > 0;
|
|
5915
|
+
} catch {
|
|
5916
|
+
return true;
|
|
5917
|
+
}
|
|
5918
|
+
});
|
|
5919
|
+
if (foreign.length > 0) {
|
|
5920
|
+
const shown = describeEntries(foreign.map((name) => maskPathText(name, secretRules)));
|
|
5921
|
+
throw new Error(
|
|
5922
|
+
`Refusing to replace tmpDir ${tmpDir}: it holds ${shown}, which this generator did not write. Point tmpDir at a new or empty directory, or remove those files yourself.`
|
|
5923
|
+
);
|
|
5924
|
+
}
|
|
5925
|
+
}
|
|
5926
|
+
function cleanStaleRunDirs(tmpDir) {
|
|
5927
|
+
let entries;
|
|
5928
|
+
try {
|
|
5929
|
+
entries = fs9.readdirSync(tmpDir);
|
|
5930
|
+
} catch {
|
|
5931
|
+
return;
|
|
5932
|
+
}
|
|
5933
|
+
for (const name of entries) {
|
|
5934
|
+
if (!name.startsWith(RUN_DIR_PREFIX)) continue;
|
|
5935
|
+
const full = path10.join(tmpDir, name);
|
|
5936
|
+
const { marker: marker2 } = readOwnershipMarker(full);
|
|
5937
|
+
if (marker2) {
|
|
5938
|
+
const created = Date.parse(marker2.createdAt);
|
|
5939
|
+
const aged = !Number.isFinite(created) || Date.now() - created > STALE_RUN_MS;
|
|
5940
|
+
const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
|
|
5941
|
+
if (aged || abandoned) {
|
|
5942
|
+
try {
|
|
5943
|
+
fs9.rmSync(full, { recursive: true, force: true });
|
|
5944
|
+
} catch {
|
|
5945
|
+
}
|
|
5946
|
+
}
|
|
5947
|
+
continue;
|
|
5948
|
+
}
|
|
5949
|
+
try {
|
|
5950
|
+
if (fs9.readdirSync(full).length === 0) fs9.rmSync(full, { recursive: true, force: true });
|
|
5951
|
+
} catch {
|
|
5952
|
+
}
|
|
5953
|
+
}
|
|
5954
|
+
}
|
|
5955
|
+
function removeIfEmpty(dir) {
|
|
5956
|
+
try {
|
|
5957
|
+
if (fs9.readdirSync(dir).length === 0) fs9.rmSync(dir, { recursive: true, force: true });
|
|
5958
|
+
} catch {
|
|
5959
|
+
}
|
|
5960
|
+
}
|
|
5961
|
+
function ensureParentDirectory(target, label) {
|
|
5962
|
+
const parent = path10.dirname(target);
|
|
5963
|
+
if (fs9.existsSync(parent) && !fs9.statSync(parent).isDirectory()) {
|
|
5964
|
+
throw new Error(
|
|
5965
|
+
`Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
|
|
5966
|
+
);
|
|
5967
|
+
}
|
|
5968
|
+
try {
|
|
5969
|
+
fs9.mkdirSync(parent, { recursive: true });
|
|
5970
|
+
} catch (err) {
|
|
5971
|
+
throw new Error(
|
|
5972
|
+
`Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
|
|
5973
|
+
);
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5976
|
+
function acquireRunLock(stagingRoot) {
|
|
5977
|
+
const key = lockKeyFor(stagingRoot);
|
|
5978
|
+
const lockPath = path10.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
|
|
5979
|
+
const payload = `${JSON.stringify({
|
|
5980
|
+
pid: process.pid,
|
|
5981
|
+
stagingRoot,
|
|
5982
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5983
|
+
})}
|
|
5984
|
+
`;
|
|
5985
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
5986
|
+
try {
|
|
5987
|
+
const handle = fs9.openSync(lockPath, "wx");
|
|
5988
|
+
try {
|
|
5989
|
+
fs9.writeSync(handle, payload);
|
|
5990
|
+
} finally {
|
|
5991
|
+
fs9.closeSync(handle);
|
|
5992
|
+
}
|
|
5993
|
+
return () => {
|
|
5994
|
+
try {
|
|
5995
|
+
fs9.rmSync(lockPath, { force: true });
|
|
5996
|
+
} catch {
|
|
5997
|
+
}
|
|
5998
|
+
};
|
|
5999
|
+
} catch (err) {
|
|
6000
|
+
if (err.code !== "EEXIST") throw err;
|
|
6001
|
+
if (attempt === 0 && lockOwnerIsGone(lockPath)) {
|
|
6002
|
+
fs9.rmSync(lockPath, { force: true });
|
|
6003
|
+
continue;
|
|
6004
|
+
}
|
|
6005
|
+
throw new Error(
|
|
6006
|
+
`Another generate run is already using the staging directory ${stagingRoot}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
|
|
6007
|
+
);
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
throw new Error(`Could not acquire the generate lock for ${stagingRoot}.`);
|
|
6011
|
+
}
|
|
6012
|
+
function lockKeyFor(stagingRoot) {
|
|
6013
|
+
let key = stagingRoot;
|
|
6014
|
+
try {
|
|
6015
|
+
key = fs9.realpathSync(stagingRoot);
|
|
6016
|
+
} catch {
|
|
6017
|
+
}
|
|
6018
|
+
if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
|
|
6019
|
+
return sha256Hex2(key).slice(0, 16);
|
|
6020
|
+
}
|
|
6021
|
+
function lockOwnerIsGone(lockPath) {
|
|
6022
|
+
let raw;
|
|
6023
|
+
try {
|
|
6024
|
+
raw = fs9.readFileSync(lockPath, "utf8");
|
|
6025
|
+
} catch {
|
|
6026
|
+
return false;
|
|
6027
|
+
}
|
|
6028
|
+
let pid;
|
|
6029
|
+
try {
|
|
6030
|
+
pid = JSON.parse(raw).pid;
|
|
6031
|
+
} catch {
|
|
6032
|
+
return false;
|
|
6033
|
+
}
|
|
6034
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
6035
|
+
try {
|
|
6036
|
+
process.kill(pid, 0);
|
|
6037
|
+
return false;
|
|
6038
|
+
} catch (err) {
|
|
6039
|
+
return err.code === "ESRCH";
|
|
6040
|
+
}
|
|
6041
|
+
}
|
|
6042
|
+
function writeManifestAtomically(manifestPath, manifest) {
|
|
6043
|
+
const tempPath = `${manifestPath}.${process.pid}.tmp`;
|
|
6044
|
+
fs9.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
6045
|
+
try {
|
|
6046
|
+
fs9.renameSync(tempPath, manifestPath);
|
|
6047
|
+
} catch (err) {
|
|
6048
|
+
try {
|
|
6049
|
+
fs9.rmSync(tempPath, { force: true });
|
|
6050
|
+
} catch {
|
|
6051
|
+
}
|
|
6052
|
+
throw err;
|
|
6053
|
+
}
|
|
6054
|
+
}
|
|
6055
|
+
function swapIntoPlace(stagingDir, outDir) {
|
|
6056
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6057
|
+
const deadline = Date.now() + 3e3;
|
|
6058
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
6059
|
+
try {
|
|
6060
|
+
swapOnce(stagingDir, outDir);
|
|
6061
|
+
return;
|
|
6062
|
+
} catch (err) {
|
|
6063
|
+
const code = err.code;
|
|
6064
|
+
const racy = code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM";
|
|
6065
|
+
if (!racy || attempt >= 10 || Date.now() > deadline) throw err;
|
|
6066
|
+
sleepSync(50);
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
6069
|
+
}
|
|
6070
|
+
function swapOnce(stagingDir, outDir) {
|
|
4789
6071
|
const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
|
|
4790
6072
|
const hadPrevious = fs9.existsSync(outDir);
|
|
4791
6073
|
if (hadPrevious) fs9.renameSync(outDir, backupDir);
|
|
4792
6074
|
try {
|
|
4793
6075
|
try {
|
|
4794
|
-
fs9.renameSync(
|
|
6076
|
+
fs9.renameSync(stagingDir, outDir);
|
|
4795
6077
|
} catch (err) {
|
|
4796
6078
|
if (err.code === "EXDEV") {
|
|
4797
|
-
fs9.cpSync(
|
|
4798
|
-
fs9.rmSync(
|
|
6079
|
+
fs9.cpSync(stagingDir, outDir, { recursive: true });
|
|
6080
|
+
fs9.rmSync(stagingDir, { recursive: true, force: true });
|
|
4799
6081
|
} else {
|
|
4800
6082
|
throw err;
|
|
4801
6083
|
}
|
|
@@ -4809,6 +6091,10 @@ function swapIntoPlace(tmpDir, outDir) {
|
|
|
4809
6091
|
throw err;
|
|
4810
6092
|
}
|
|
4811
6093
|
}
|
|
6094
|
+
function sleepSync(milliseconds) {
|
|
6095
|
+
const buffer = new Int32Array(new SharedArrayBuffer(4));
|
|
6096
|
+
Atomics.wait(buffer, 0, 0, milliseconds);
|
|
6097
|
+
}
|
|
4812
6098
|
function artifactWarnings(artifacts) {
|
|
4813
6099
|
const messages = [];
|
|
4814
6100
|
if (!artifacts.manifest) {
|
|
@@ -4871,10 +6157,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4871
6157
|
const collection = collectFiles(config.cwd, {
|
|
4872
6158
|
includePatterns: config.include,
|
|
4873
6159
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
4874
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
6160
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
6161
|
+
secretRules: config.secretRules
|
|
4875
6162
|
});
|
|
4876
6163
|
const files = [];
|
|
4877
|
-
const
|
|
6164
|
+
const monolithBodies = /* @__PURE__ */ new Map();
|
|
6165
|
+
const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
|
|
4878
6166
|
const secretFindings = [];
|
|
4879
6167
|
const warnings = [];
|
|
4880
6168
|
const layerCounts = {};
|
|
@@ -4937,6 +6225,9 @@ function generateSnapshot(configInput, options) {
|
|
|
4937
6225
|
isPreviewTruncated = preview.isTruncated;
|
|
4938
6226
|
includedLines = preview.includedLines;
|
|
4939
6227
|
}
|
|
6228
|
+
if (config.includeMonolith && isPreviewTruncated) {
|
|
6229
|
+
monolithBodies.set(relativePath, redacted);
|
|
6230
|
+
}
|
|
4940
6231
|
let summary;
|
|
4941
6232
|
let summaryQuality;
|
|
4942
6233
|
try {
|
|
@@ -4945,12 +6236,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4945
6236
|
summaryQuality = outcome?.quality;
|
|
4946
6237
|
if (isSourceSummary(summary) && summary.parseErrors) {
|
|
4947
6238
|
warnings.push(
|
|
4948
|
-
`Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
|
|
6239
|
+
`Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
|
|
4949
6240
|
);
|
|
4950
6241
|
}
|
|
4951
6242
|
} catch (err) {
|
|
4952
6243
|
warnings.push(
|
|
4953
|
-
`Summary skipped for ${relativePath}: ${err.message}`.replace(/\s+/g, " ")
|
|
6244
|
+
`Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(/\s+/g, " ")
|
|
4954
6245
|
);
|
|
4955
6246
|
}
|
|
4956
6247
|
files.push({
|
|
@@ -4979,7 +6270,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4979
6270
|
).join("\n")
|
|
4980
6271
|
);
|
|
4981
6272
|
const fullContents = /* @__PURE__ */ new Map();
|
|
4982
|
-
for (const file of files)
|
|
6273
|
+
for (const file of files) {
|
|
6274
|
+
fullContents.set(
|
|
6275
|
+
file.relativePath,
|
|
6276
|
+
monolithBodies.get(file.relativePath) ?? file.content ?? ""
|
|
6277
|
+
);
|
|
6278
|
+
}
|
|
4983
6279
|
const parts = partitionByBudget(files, config, fingerprint);
|
|
4984
6280
|
const maxEffectiveBudget = Math.max(
|
|
4985
6281
|
config.maxPartBytes,
|
|
@@ -5054,21 +6350,28 @@ function generateSnapshot(configInput, options) {
|
|
|
5054
6350
|
const { content: _content, ...rest } = file;
|
|
5055
6351
|
return rest;
|
|
5056
6352
|
}),
|
|
5057
|
-
skipped,
|
|
6353
|
+
skipped: maskSkipRecords(skipped, config.secretRules),
|
|
5058
6354
|
warnings
|
|
5059
6355
|
};
|
|
5060
6356
|
manifest.warnings = [
|
|
5061
6357
|
...warnings,
|
|
5062
|
-
...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
|
|
6358
|
+
...secretFindings.map((finding) => `${finding.rule} at ${maskPathText(finding.path ?? "unknown", config.secretRules)}:${finding.line}`)
|
|
5063
6359
|
];
|
|
5064
6360
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
5065
6361
|
let monolithJson = null;
|
|
5066
6362
|
let monolithMarkdown = null;
|
|
5067
6363
|
if (config.includeMonolith) {
|
|
5068
|
-
const fullFiles = files.map((file) =>
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
6364
|
+
const fullFiles = files.map((file) => {
|
|
6365
|
+
const content = fullContents.get(file.relativePath) ?? "";
|
|
6366
|
+
return {
|
|
6367
|
+
...file,
|
|
6368
|
+
content,
|
|
6369
|
+
inclusionMode: "full",
|
|
6370
|
+
isPreviewTruncated: false,
|
|
6371
|
+
includedBytes: Buffer.byteLength(content, "utf8"),
|
|
6372
|
+
includedLines: countLines(content)
|
|
6373
|
+
};
|
|
6374
|
+
});
|
|
5072
6375
|
const json = generateMonolithJson(manifest, fullFiles);
|
|
5073
6376
|
const markdown = generateMonolithMarkdown(manifest, fullFiles);
|
|
5074
6377
|
const jsonBytes = Buffer.byteLength(json, "utf8");
|
|
@@ -5096,57 +6399,202 @@ function generateSnapshot(configInput, options) {
|
|
|
5096
6399
|
}
|
|
5097
6400
|
for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
|
|
5098
6401
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
6402
|
+
const { outDir, tmpDir } = resolveSafeOutputDirs(config);
|
|
6403
|
+
const { artifacts } = config;
|
|
6404
|
+
assertReplaceable(outDir, "outDir", config.secretRules);
|
|
6405
|
+
assertStagingRootUsable(tmpDir, config.secretRules);
|
|
5099
6406
|
if (dryRun) {
|
|
5100
6407
|
runAfterGenerate(config, manifest);
|
|
5101
6408
|
return manifest;
|
|
5102
6409
|
}
|
|
5103
|
-
const
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
6410
|
+
const releaseLock = acquireRunLock(tmpDir);
|
|
6411
|
+
let stagingDir = null;
|
|
6412
|
+
try {
|
|
6413
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6414
|
+
fs9.mkdirSync(tmpDir, { recursive: true });
|
|
6415
|
+
cleanStaleRunDirs(tmpDir);
|
|
6416
|
+
const snapshotId = crypto3.randomUUID();
|
|
6417
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6418
|
+
stagingDir = path10.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
|
|
6419
|
+
fs9.mkdirSync(stagingDir);
|
|
6420
|
+
writeOwnershipMarker(stagingDir, {
|
|
6421
|
+
generator: GENERATOR_NAME,
|
|
6422
|
+
version: GENERATOR_VERSION,
|
|
6423
|
+
schemaVersion: SCHEMA_VERSION,
|
|
6424
|
+
createdAt,
|
|
6425
|
+
state: "staging",
|
|
6426
|
+
id: snapshotId,
|
|
6427
|
+
pid: process.pid
|
|
6428
|
+
});
|
|
6429
|
+
const partsDir = path10.join(stagingDir, "parts");
|
|
6430
|
+
const reportsDir = path10.join(stagingDir, "reports");
|
|
6431
|
+
if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
|
|
6432
|
+
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
6433
|
+
fs9.mkdirSync(reportsDir, { recursive: true });
|
|
6434
|
+
}
|
|
6435
|
+
if (artifacts.parts) {
|
|
6436
|
+
for (const part of parts) {
|
|
6437
|
+
fs9.writeFileSync(path10.join(partsDir, part.filename), part.markdown, "utf8");
|
|
6438
|
+
}
|
|
5119
6439
|
}
|
|
6440
|
+
if (artifacts.projectTree) {
|
|
6441
|
+
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
6442
|
+
fs9.writeFileSync(path10.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
6443
|
+
}
|
|
6444
|
+
if (artifacts.navigation) {
|
|
6445
|
+
fs9.writeFileSync(path10.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
6446
|
+
}
|
|
6447
|
+
if (artifacts.audit) {
|
|
6448
|
+
fs9.writeFileSync(
|
|
6449
|
+
path10.join(reportsDir, "audit.md"),
|
|
6450
|
+
generateAuditReport(manifest, secretFindings, config.secretRules),
|
|
6451
|
+
"utf8"
|
|
6452
|
+
);
|
|
6453
|
+
}
|
|
6454
|
+
if (artifacts.readme) {
|
|
6455
|
+
fs9.writeFileSync(path10.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
|
|
6456
|
+
}
|
|
6457
|
+
if (artifacts.manifest) {
|
|
6458
|
+
fs9.writeFileSync(path10.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
6459
|
+
}
|
|
6460
|
+
if (monolithJson !== null) {
|
|
6461
|
+
fs9.writeFileSync(path10.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
|
|
6462
|
+
}
|
|
6463
|
+
if (monolithMarkdown !== null) {
|
|
6464
|
+
fs9.writeFileSync(path10.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
6465
|
+
}
|
|
6466
|
+
writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
|
|
6467
|
+
swapIntoPlace(stagingDir, outDir);
|
|
6468
|
+
stagingDir = null;
|
|
6469
|
+
const warningsBeforeHook = manifest.warnings.length;
|
|
6470
|
+
runAfterGenerate(config, manifest);
|
|
6471
|
+
if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
|
|
6472
|
+
writeManifestAtomically(path10.join(outDir, "manifest.json"), manifest);
|
|
6473
|
+
}
|
|
6474
|
+
writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
|
|
6475
|
+
return manifest;
|
|
6476
|
+
} catch (err) {
|
|
6477
|
+
if (stagingDir !== null) {
|
|
6478
|
+
try {
|
|
6479
|
+
fs9.rmSync(stagingDir, { recursive: true, force: true });
|
|
6480
|
+
} catch {
|
|
6481
|
+
}
|
|
6482
|
+
}
|
|
6483
|
+
throw err;
|
|
6484
|
+
} finally {
|
|
6485
|
+
removeIfEmpty(tmpDir);
|
|
6486
|
+
releaseLock();
|
|
5120
6487
|
}
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5149
|
-
|
|
6488
|
+
}
|
|
6489
|
+
|
|
6490
|
+
// src/agent/option-scope.ts
|
|
6491
|
+
var COMMAND_OPTION_SCOPE = {
|
|
6492
|
+
generate: [
|
|
6493
|
+
"cwd",
|
|
6494
|
+
"out-dir",
|
|
6495
|
+
"max-part-bytes",
|
|
6496
|
+
"large-file-bytes",
|
|
6497
|
+
"max-monolith-bytes",
|
|
6498
|
+
"include-monolith",
|
|
6499
|
+
"include-sensitive-files",
|
|
6500
|
+
"skip-artifacts",
|
|
6501
|
+
"only-artifacts",
|
|
6502
|
+
"no-config",
|
|
6503
|
+
"format",
|
|
6504
|
+
"mode",
|
|
6505
|
+
"dry-run",
|
|
6506
|
+
"watch",
|
|
6507
|
+
"help",
|
|
6508
|
+
"version"
|
|
6509
|
+
],
|
|
6510
|
+
validate: [
|
|
6511
|
+
"cwd",
|
|
6512
|
+
"out-dir",
|
|
6513
|
+
"max-part-bytes",
|
|
6514
|
+
"large-file-bytes",
|
|
6515
|
+
"max-monolith-bytes",
|
|
6516
|
+
"include-monolith",
|
|
6517
|
+
"include-sensitive-files",
|
|
6518
|
+
"skip-artifacts",
|
|
6519
|
+
"only-artifacts",
|
|
6520
|
+
"no-config",
|
|
6521
|
+
"format",
|
|
6522
|
+
"mode",
|
|
6523
|
+
"help",
|
|
6524
|
+
"version"
|
|
6525
|
+
],
|
|
6526
|
+
report: [
|
|
6527
|
+
"cwd",
|
|
6528
|
+
"out-dir",
|
|
6529
|
+
"max-part-bytes",
|
|
6530
|
+
"large-file-bytes",
|
|
6531
|
+
"max-monolith-bytes",
|
|
6532
|
+
"include-monolith",
|
|
6533
|
+
"include-sensitive-files",
|
|
6534
|
+
"skip-artifacts",
|
|
6535
|
+
"only-artifacts",
|
|
6536
|
+
"no-config",
|
|
6537
|
+
"format",
|
|
6538
|
+
"mode",
|
|
6539
|
+
"help",
|
|
6540
|
+
"version"
|
|
6541
|
+
],
|
|
6542
|
+
explain: [
|
|
6543
|
+
"cwd",
|
|
6544
|
+
"out-dir",
|
|
6545
|
+
"max-part-bytes",
|
|
6546
|
+
"large-file-bytes",
|
|
6547
|
+
"max-monolith-bytes",
|
|
6548
|
+
"include-monolith",
|
|
6549
|
+
"include-sensitive-files",
|
|
6550
|
+
"skip-artifacts",
|
|
6551
|
+
"only-artifacts",
|
|
6552
|
+
"no-config",
|
|
6553
|
+
"format",
|
|
6554
|
+
"mode",
|
|
6555
|
+
"help",
|
|
6556
|
+
"version"
|
|
6557
|
+
],
|
|
6558
|
+
diff: ["format", "mode", "help", "version"],
|
|
6559
|
+
describe: ["format", "mode", "help", "version"],
|
|
6560
|
+
init: [
|
|
6561
|
+
"cwd",
|
|
6562
|
+
"out-dir",
|
|
6563
|
+
"max-part-bytes",
|
|
6564
|
+
"include-sensitive-files",
|
|
6565
|
+
"skip-artifacts",
|
|
6566
|
+
"only-artifacts",
|
|
6567
|
+
"preset",
|
|
6568
|
+
"include",
|
|
6569
|
+
"exclude",
|
|
6570
|
+
"yes",
|
|
6571
|
+
"minimal",
|
|
6572
|
+
"force",
|
|
6573
|
+
"dry-run",
|
|
6574
|
+
"format",
|
|
6575
|
+
"mode",
|
|
6576
|
+
"help",
|
|
6577
|
+
"version"
|
|
6578
|
+
],
|
|
6579
|
+
skill: ["cwd", "format", "mode", "install", "target", "help", "version"]
|
|
6580
|
+
};
|
|
6581
|
+
var COMMAND_ORDER = [
|
|
6582
|
+
"generate",
|
|
6583
|
+
"validate",
|
|
6584
|
+
"report",
|
|
6585
|
+
"explain",
|
|
6586
|
+
"init",
|
|
6587
|
+
"diff",
|
|
6588
|
+
"describe",
|
|
6589
|
+
"skill"
|
|
6590
|
+
];
|
|
6591
|
+
function commandsForOption(option) {
|
|
6592
|
+
return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
|
|
6593
|
+
}
|
|
6594
|
+
function optionAppliesTo(option) {
|
|
6595
|
+
const commands = commandsForOption(option);
|
|
6596
|
+
if (commands.length === COMMAND_ORDER.length) return "all commands";
|
|
6597
|
+
return commands.join("/");
|
|
5150
6598
|
}
|
|
5151
6599
|
|
|
5152
6600
|
// src/agent/contract.ts
|
|
@@ -5238,11 +6686,11 @@ function buildAgentContract() {
|
|
|
5238
6686
|
invocation: {
|
|
5239
6687
|
oneShot: `npx ${PACKAGE_NAME}@latest`,
|
|
5240
6688
|
installed: "npx codebase-snapshot",
|
|
5241
|
-
bins: ["codebase-snapshot"],
|
|
6689
|
+
bins: ["codebase-snapshot", "snap"],
|
|
5242
6690
|
notes: [
|
|
5243
6691
|
`The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
|
|
5244
|
-
"Pin @latest (or @
|
|
5245
|
-
"
|
|
6692
|
+
"Pin @latest (or @4) when you need the exact same version on every machine.",
|
|
6693
|
+
"Both bin names point at the same file. npm resolves a package by its bin entries, so they must stay aliases of one executable.",
|
|
5246
6694
|
"Human progress goes to stderr. With --format json the payload on stdout is pure JSON."
|
|
5247
6695
|
]
|
|
5248
6696
|
},
|
|
@@ -5251,7 +6699,7 @@ function buildAgentContract() {
|
|
|
5251
6699
|
step: 1,
|
|
5252
6700
|
id: "verify",
|
|
5253
6701
|
run: `npx ${PACKAGE_NAME}@latest --version`,
|
|
5254
|
-
expect:
|
|
6702
|
+
expect: `prints v${GENERATOR_VERSION} on stdout and exits 0`,
|
|
5255
6703
|
notes: "If it prints nothing, clear the npm cache and retry, or use `npx -p <pkg> codebase-snapshot`."
|
|
5256
6704
|
},
|
|
5257
6705
|
{
|
|
@@ -5321,9 +6769,9 @@ function buildAgentContract() {
|
|
|
5321
6769
|
{
|
|
5322
6770
|
name: "explain",
|
|
5323
6771
|
aliases: [],
|
|
5324
|
-
summary: "Explain how one path would be treated: layer, kind, part, skip reason.",
|
|
6772
|
+
summary: "Explain how one path would be treated: layer, kind, part, skip reason. Always exits 0 when it can report; read `included` and `reason` instead of the exit code.",
|
|
5325
6773
|
positionals: ["<path>"],
|
|
5326
|
-
exits: [0
|
|
6774
|
+
exits: [0]
|
|
5327
6775
|
},
|
|
5328
6776
|
{
|
|
5329
6777
|
name: "diff",
|
|
@@ -5357,140 +6805,147 @@ function buildAgentContract() {
|
|
|
5357
6805
|
flag: "--out-dir",
|
|
5358
6806
|
type: "string",
|
|
5359
6807
|
default: "snapshot",
|
|
5360
|
-
appliesTo: "
|
|
6808
|
+
appliesTo: optionAppliesTo("out-dir"),
|
|
5361
6809
|
summary: "Where the snapshot is written. Must not be the repo root or a directory the tool did not create."
|
|
5362
6810
|
},
|
|
5363
6811
|
{
|
|
5364
6812
|
flag: "--max-part-bytes",
|
|
5365
6813
|
type: "string",
|
|
5366
6814
|
default: 307200,
|
|
5367
|
-
appliesTo: "
|
|
6815
|
+
appliesTo: optionAppliesTo("max-part-bytes"),
|
|
5368
6816
|
summary: 'Byte budget per markdown part. Accepts "120k" / "1.5mb". Minimum 4096.'
|
|
5369
6817
|
},
|
|
5370
6818
|
{
|
|
5371
6819
|
flag: "--large-file-bytes",
|
|
5372
6820
|
type: "string",
|
|
5373
6821
|
default: 204800,
|
|
5374
|
-
appliesTo: "
|
|
6822
|
+
appliesTo: optionAppliesTo("large-file-bytes"),
|
|
5375
6823
|
summary: "Above this size a file gets a head/tail preview instead of the full body."
|
|
5376
6824
|
},
|
|
5377
6825
|
{
|
|
5378
6826
|
flag: "--max-monolith-bytes",
|
|
5379
6827
|
type: "string",
|
|
5380
6828
|
default: "unset",
|
|
5381
|
-
appliesTo: "
|
|
6829
|
+
appliesTo: optionAppliesTo("max-monolith-bytes"),
|
|
5382
6830
|
summary: "Skip the monolith artifacts when they would exceed this size."
|
|
5383
6831
|
},
|
|
5384
6832
|
{
|
|
5385
6833
|
flag: "--include-monolith",
|
|
5386
6834
|
type: "boolean",
|
|
5387
6835
|
default: false,
|
|
5388
|
-
appliesTo: "
|
|
6836
|
+
appliesTo: optionAppliesTo("include-monolith"),
|
|
5389
6837
|
summary: "Also write full-codebase.json and full-codebase.md."
|
|
5390
6838
|
},
|
|
5391
6839
|
{
|
|
5392
6840
|
flag: "--include-sensitive-files",
|
|
5393
6841
|
type: "boolean",
|
|
5394
6842
|
default: false,
|
|
5395
|
-
appliesTo: "
|
|
6843
|
+
appliesTo: optionAppliesTo("include-sensitive-files"),
|
|
5396
6844
|
summary: "Also emit .env / *.pem / .npmrc style files. Off by default; they are recorded as skipped."
|
|
5397
6845
|
},
|
|
5398
6846
|
{
|
|
5399
6847
|
flag: "--skip-artifacts",
|
|
5400
6848
|
type: "string",
|
|
5401
6849
|
default: "unset",
|
|
5402
|
-
appliesTo: "
|
|
6850
|
+
appliesTo: optionAppliesTo("skip-artifacts"),
|
|
5403
6851
|
summary: "Comma-separated artifacts to omit (readme, manifest, parts, navigation, projectTree, audit, monolith)."
|
|
5404
6852
|
},
|
|
5405
6853
|
{
|
|
5406
6854
|
flag: "--only-artifacts",
|
|
5407
6855
|
type: "string",
|
|
5408
6856
|
default: "unset",
|
|
5409
|
-
appliesTo: "
|
|
6857
|
+
appliesTo: optionAppliesTo("only-artifacts"),
|
|
5410
6858
|
summary: "Write only these artifacts. Cannot be combined with --skip-artifacts."
|
|
5411
6859
|
},
|
|
5412
6860
|
{
|
|
5413
6861
|
flag: "--format",
|
|
5414
6862
|
type: "string",
|
|
5415
6863
|
default: "text",
|
|
5416
|
-
appliesTo: "
|
|
6864
|
+
appliesTo: optionAppliesTo("format"),
|
|
5417
6865
|
summary: 'Output format: "text" or "json". JSON goes to stdout, progress to stderr.'
|
|
5418
6866
|
},
|
|
5419
6867
|
{
|
|
5420
6868
|
flag: "--cwd",
|
|
5421
6869
|
type: "string",
|
|
5422
6870
|
default: "process.cwd()",
|
|
5423
|
-
appliesTo: "
|
|
6871
|
+
appliesTo: optionAppliesTo("cwd"),
|
|
5424
6872
|
summary: "Repository to operate on. Must be an existing directory."
|
|
5425
6873
|
},
|
|
5426
6874
|
{
|
|
5427
6875
|
flag: "--dry-run",
|
|
5428
6876
|
type: "boolean",
|
|
5429
6877
|
default: false,
|
|
5430
|
-
appliesTo: "
|
|
6878
|
+
appliesTo: optionAppliesTo("dry-run"),
|
|
5431
6879
|
summary: "Compute everything and print the result without writing."
|
|
5432
6880
|
},
|
|
5433
6881
|
{
|
|
5434
6882
|
flag: "--watch",
|
|
5435
6883
|
type: "boolean",
|
|
5436
6884
|
default: false,
|
|
5437
|
-
appliesTo: "
|
|
6885
|
+
appliesTo: optionAppliesTo("watch"),
|
|
5438
6886
|
summary: "Regenerate on every change until interrupted."
|
|
5439
6887
|
},
|
|
6888
|
+
{
|
|
6889
|
+
flag: "--no-config",
|
|
6890
|
+
type: "boolean",
|
|
6891
|
+
default: false,
|
|
6892
|
+
appliesTo: optionAppliesTo("no-config"),
|
|
6893
|
+
summary: "Ignore the config file and run on the built-in defaults."
|
|
6894
|
+
},
|
|
5440
6895
|
{
|
|
5441
6896
|
flag: "--preset",
|
|
5442
6897
|
type: "string",
|
|
5443
6898
|
default: "detected",
|
|
5444
|
-
appliesTo: "
|
|
6899
|
+
appliesTo: optionAppliesTo("preset"),
|
|
5445
6900
|
summary: "Force a project preset instead of detecting one. Unknown names are a usage error."
|
|
5446
6901
|
},
|
|
5447
6902
|
{
|
|
5448
6903
|
flag: "--include",
|
|
5449
6904
|
type: "string",
|
|
5450
6905
|
default: "unset",
|
|
5451
|
-
appliesTo: "
|
|
6906
|
+
appliesTo: optionAppliesTo("include"),
|
|
5452
6907
|
summary: "Comma-separated allow-list of globs to record in the generated config."
|
|
5453
6908
|
},
|
|
5454
6909
|
{
|
|
5455
6910
|
flag: "--exclude",
|
|
5456
6911
|
type: "string",
|
|
5457
6912
|
default: "unset",
|
|
5458
|
-
appliesTo: "
|
|
6913
|
+
appliesTo: optionAppliesTo("exclude"),
|
|
5459
6914
|
summary: "Comma-separated globs to record in the generated config."
|
|
5460
6915
|
},
|
|
5461
6916
|
{
|
|
5462
6917
|
flag: "--yes",
|
|
5463
6918
|
type: "boolean",
|
|
5464
6919
|
default: false,
|
|
5465
|
-
appliesTo: "
|
|
6920
|
+
appliesTo: optionAppliesTo("yes"),
|
|
5466
6921
|
summary: "Accept every default without prompting. Required in a non-TTY."
|
|
5467
6922
|
},
|
|
5468
6923
|
{
|
|
5469
6924
|
flag: "--minimal",
|
|
5470
6925
|
type: "boolean",
|
|
5471
6926
|
default: false,
|
|
5472
|
-
appliesTo: "
|
|
6927
|
+
appliesTo: optionAppliesTo("minimal"),
|
|
5473
6928
|
summary: "Write only the settings that differ from the defaults."
|
|
5474
6929
|
},
|
|
5475
6930
|
{
|
|
5476
6931
|
flag: "--force",
|
|
5477
6932
|
type: "boolean",
|
|
5478
6933
|
default: false,
|
|
5479
|
-
appliesTo: "
|
|
6934
|
+
appliesTo: optionAppliesTo("force"),
|
|
5480
6935
|
summary: "Overwrite an existing config file."
|
|
5481
6936
|
},
|
|
5482
6937
|
{
|
|
5483
6938
|
flag: "--install",
|
|
5484
6939
|
type: "boolean",
|
|
5485
6940
|
default: false,
|
|
5486
|
-
appliesTo: "
|
|
6941
|
+
appliesTo: optionAppliesTo("install"),
|
|
5487
6942
|
summary: "Write the skill into the repository instead of printing it."
|
|
5488
6943
|
},
|
|
5489
6944
|
{
|
|
5490
6945
|
flag: "--target",
|
|
5491
6946
|
type: "string",
|
|
5492
6947
|
default: "all",
|
|
5493
|
-
appliesTo: "
|
|
6948
|
+
appliesTo: optionAppliesTo("target"),
|
|
5494
6949
|
summary: 'Where to install: "claude", "agents", "cursor" or "all".'
|
|
5495
6950
|
}
|
|
5496
6951
|
],
|
|
@@ -5565,8 +7020,8 @@ function buildAgentContract() {
|
|
|
5565
7020
|
troubleshooting: [
|
|
5566
7021
|
{
|
|
5567
7022
|
symptom: "Command prints nothing and exits 0",
|
|
5568
|
-
cause: "
|
|
5569
|
-
fix: `
|
|
7023
|
+
cause: "A stale global install of this package (often a leftover `npm link`) is shadowing npx: npm runs the global binary instead of installing the published one.",
|
|
7024
|
+
fix: "Check `npm ls -g --depth=0` for @ankurhalder/codebase-snapshot and remove it with `npm rm -g @ankurhalder/codebase-snapshot`. If it is not installed globally, clear the npx cache with `npm cache clean --force` and retry."
|
|
5570
7025
|
},
|
|
5571
7026
|
{
|
|
5572
7027
|
symptom: "Exit 1 with 'Refusing to replace outDir'",
|
|
@@ -5644,7 +7099,8 @@ not resolved \u2014 clear the npm cache and retry; do not assume the repository
|
|
|
5644
7099
|
\`--format json\` if you want to parse it.
|
|
5645
7100
|
|
|
5646
7101
|
4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
|
|
5647
|
-
the project shape, then writes \`snapshot.config.ts\`
|
|
7102
|
+
the project shape, then writes \`snapshot.config.ts\` (or \`.mjs\` when the project is
|
|
7103
|
+
not an ES module) listing **every** setting at its
|
|
5648
7104
|
current value, commented. Pass \`--preset <name>\` only if detection is wrong; the
|
|
5649
7105
|
preset names are in the contract. This step is optional: without a config the tool
|
|
5650
7106
|
runs on the same defaults.
|
|
@@ -5745,14 +7201,24 @@ function mergeAgentsFile(cwd, skill) {
|
|
|
5745
7201
|
next = `# AGENTS.md
|
|
5746
7202
|
|
|
5747
7203
|
${block}`;
|
|
5748
|
-
} else
|
|
7204
|
+
} else {
|
|
7205
|
+
const starts = existing.split(AGENTS_MARKER_START).length - 1;
|
|
7206
|
+
const ends = existing.split(AGENTS_MARKER_END).length - 1;
|
|
5749
7207
|
const start = existing.indexOf(AGENTS_MARKER_START);
|
|
5750
7208
|
const end = existing.indexOf(AGENTS_MARKER_END) + AGENTS_MARKER_END.length;
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
7209
|
+
if (starts === 1 && ends === 1 && start < end) {
|
|
7210
|
+
next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(end)}`;
|
|
7211
|
+
} else if (starts > 0 && ends === 0) {
|
|
7212
|
+
next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(
|
|
7213
|
+
start + AGENTS_MARKER_START.length
|
|
7214
|
+
)}`;
|
|
7215
|
+
} else if (existing.includes(block.trimEnd())) {
|
|
7216
|
+
next = existing;
|
|
7217
|
+
} else {
|
|
7218
|
+
next = `${existing.trimEnd()}
|
|
5754
7219
|
|
|
5755
7220
|
${block}`;
|
|
7221
|
+
}
|
|
5756
7222
|
}
|
|
5757
7223
|
const action = existing === null ? "created" : existing === next ? "unchanged" : "updated";
|
|
5758
7224
|
if (action !== "unchanged") {
|
|
@@ -5815,6 +7281,7 @@ var CLI_OPTIONS = {
|
|
|
5815
7281
|
mode: { type: "string" },
|
|
5816
7282
|
"dry-run": { type: "boolean" },
|
|
5817
7283
|
watch: { type: "boolean" },
|
|
7284
|
+
"no-config": { type: "boolean" },
|
|
5818
7285
|
preset: { type: "string" },
|
|
5819
7286
|
include: { type: "string" },
|
|
5820
7287
|
exclude: { type: "string" },
|
|
@@ -5861,6 +7328,7 @@ Options:
|
|
|
5861
7328
|
--format <text|json> CLI output format (default: text)
|
|
5862
7329
|
--dry-run Compute the snapshot without writing anything
|
|
5863
7330
|
--watch Re-generate whenever the repository changes
|
|
7331
|
+
--no-config Ignore snapshot.config.* and use the built-in defaults
|
|
5864
7332
|
|
|
5865
7333
|
init only:
|
|
5866
7334
|
--preset <name> Force a project preset instead of detecting one
|
|
@@ -6010,28 +7478,32 @@ function parseCliArgs(argv) {
|
|
|
6010
7478
|
} else if (rawSkip !== void 0) {
|
|
6011
7479
|
skipArtifacts = parseArtifactList(String(rawSkip), "--skip-artifacts");
|
|
6012
7480
|
}
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
if (
|
|
6017
|
-
|
|
6018
|
-
if (
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
7481
|
+
const scope = COMMAND_OPTION_SCOPE[command];
|
|
7482
|
+
const ignored = [];
|
|
7483
|
+
for (const name of Object.keys(CLI_OPTIONS)) {
|
|
7484
|
+
if (scope.includes(name)) continue;
|
|
7485
|
+
const value = values[name];
|
|
7486
|
+
if (value === void 0 || value === false) continue;
|
|
7487
|
+
ignored.push(`--${name}`);
|
|
7488
|
+
}
|
|
7489
|
+
if (ignored.length > 0) {
|
|
7490
|
+
const owners = Array.from(
|
|
7491
|
+
new Set(
|
|
7492
|
+
ignored.flatMap(
|
|
7493
|
+
(flag) => commandsForOption(flag.replace(/^--/, "")).map((owner) => `\`${owner}\``)
|
|
7494
|
+
)
|
|
7495
|
+
)
|
|
7496
|
+
);
|
|
7497
|
+
const hint = ignored.some((flag) => ["--preset", "--include", "--exclude"].includes(flag)) ? " To scope a run, set include/excludePatterns/preset in snapshot.config.ts (or run `codebase-snapshot init` to generate one)." : "";
|
|
7498
|
+
throw new UsageError(
|
|
7499
|
+
`${ignored.join(", ")} ${ignored.length === 1 ? "does" : "do"} not apply to \`${command}\` and ${ignored.length === 1 ? "was" : "were"} not applied. ${ignored.length === 1 ? "It is" : "They are"} accepted by: ${owners.join(", ")}.${hint}`
|
|
7500
|
+
);
|
|
6026
7501
|
}
|
|
6027
7502
|
if (command === "init" && values.preset !== void 0) {
|
|
6028
7503
|
const requested = String(values.preset);
|
|
6029
7504
|
if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
|
|
6030
7505
|
}
|
|
6031
|
-
if (command
|
|
6032
|
-
throw new UsageError("--install and --target are only valid with `skill`.");
|
|
6033
|
-
}
|
|
6034
|
-
if (values.target !== void 0 && !isSkillTarget(String(values.target))) {
|
|
7506
|
+
if (command === "skill" && values.target !== void 0 && !isSkillTarget(String(values.target))) {
|
|
6035
7507
|
throw new UsageError(
|
|
6036
7508
|
`--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
|
|
6037
7509
|
);
|
|
@@ -6042,6 +7514,7 @@ function parseCliArgs(argv) {
|
|
|
6042
7514
|
format: rawFormat,
|
|
6043
7515
|
dryRun: values["dry-run"] === true,
|
|
6044
7516
|
watch: values.watch === true,
|
|
7517
|
+
noConfig: values["no-config"] === true,
|
|
6045
7518
|
preset: values.preset === void 0 ? void 0 : String(values.preset),
|
|
6046
7519
|
maxPartBytesRaw: values["max-part-bytes"] === void 0 ? void 0 : String(values["max-part-bytes"]),
|
|
6047
7520
|
include: values.include === void 0 ? void 0 : String(values.include),
|
|
@@ -6095,6 +7568,12 @@ function splitList(value) {
|
|
|
6095
7568
|
if (value === void 0) return void 0;
|
|
6096
7569
|
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
6097
7570
|
}
|
|
7571
|
+
function displayOutPath(cwd, outDir, ...segments) {
|
|
7572
|
+
const absolute = path12.resolve(cwd, outDir, ...segments);
|
|
7573
|
+
const relative5 = path12.relative(cwd, absolute);
|
|
7574
|
+
if (relative5 && !relative5.startsWith("..") && !path12.isAbsolute(relative5)) return relative5;
|
|
7575
|
+
return absolute;
|
|
7576
|
+
}
|
|
6098
7577
|
function describeManifestShapeProblem(manifest) {
|
|
6099
7578
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
6100
7579
|
return "not a JSON object";
|
|
@@ -6114,6 +7593,7 @@ async function runInitCommand(invocation) {
|
|
|
6114
7593
|
const result = await runInit({
|
|
6115
7594
|
cwd: invocation.cwd,
|
|
6116
7595
|
preset: invocation.preset,
|
|
7596
|
+
outDir: invocation.outDir,
|
|
6117
7597
|
maxPartBytes: invocation.maxPartBytesRaw ?? invocation.maxPartBytes,
|
|
6118
7598
|
include: splitList(invocation.include),
|
|
6119
7599
|
exclude: splitList(invocation.exclude),
|
|
@@ -6140,7 +7620,11 @@ async function runInitCommand(invocation) {
|
|
|
6140
7620
|
}
|
|
6141
7621
|
for (const note of result.notes) info(` \u26A0 ${note}`);
|
|
6142
7622
|
info("");
|
|
6143
|
-
|
|
7623
|
+
if (result.scripts.length > 0) {
|
|
7624
|
+
info(" Next: npm run snapshot");
|
|
7625
|
+
} else {
|
|
7626
|
+
info(` Next: ${GENERATOR_NAME} generate`);
|
|
7627
|
+
}
|
|
6144
7628
|
info(
|
|
6145
7629
|
` Tip: \`${GENERATOR_NAME} skill --install\` makes this repo discoverable by AI agents.`
|
|
6146
7630
|
);
|
|
@@ -6156,6 +7640,13 @@ async function runInitCommand(invocation) {
|
|
|
6156
7640
|
}
|
|
6157
7641
|
function runSkillCommand(invocation) {
|
|
6158
7642
|
if (!invocation.install) {
|
|
7643
|
+
if (invocation.format === "json") {
|
|
7644
|
+
process.stdout.write(
|
|
7645
|
+
`${JSON.stringify({ name: SKILL_NAME, version: GENERATOR_VERSION, skill: renderSkill() }, null, 2)}
|
|
7646
|
+
`
|
|
7647
|
+
);
|
|
7648
|
+
return 0;
|
|
7649
|
+
}
|
|
6159
7650
|
process.stdout.write(renderSkill());
|
|
6160
7651
|
return 0;
|
|
6161
7652
|
}
|
|
@@ -6232,7 +7723,8 @@ ${USAGE}
|
|
|
6232
7723
|
if (invocation.command === "skill") {
|
|
6233
7724
|
return runSkillCommand(invocation);
|
|
6234
7725
|
}
|
|
6235
|
-
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
7726
|
+
const loaded = invocation.noConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
|
|
7727
|
+
if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
|
|
6236
7728
|
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
6237
7729
|
if (loaded.errors.length > 0) {
|
|
6238
7730
|
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
@@ -6279,8 +7771,7 @@ ${USAGE}
|
|
|
6279
7771
|
warn(`\u274C ${manifestPath} is not a snapshot manifest: ${shapeError}`);
|
|
6280
7772
|
return 1;
|
|
6281
7773
|
}
|
|
6282
|
-
|
|
6283
|
-
emit(invocation.format, { manifest, validation }, () => {
|
|
7774
|
+
emit(invocation.format, { manifest, validation: null }, () => {
|
|
6284
7775
|
info(`
|
|
6285
7776
|
\u{1F4CA} Codebase Snapshot Report (${resolved.cwd})`);
|
|
6286
7777
|
info(` - Generated at: ${manifest.generatedAt}`);
|
|
@@ -6292,19 +7783,19 @@ ${USAGE}
|
|
|
6292
7783
|
` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
|
|
6293
7784
|
);
|
|
6294
7785
|
info(` - Total parts: ${manifest.counts.partsCount}`);
|
|
6295
|
-
info(
|
|
7786
|
+
info(" - Integrity: not checked (run `codebase-snapshot validate`)");
|
|
6296
7787
|
info(` - Warnings: ${manifest.warnings?.length ?? 0}`);
|
|
6297
7788
|
info("\n Layers:");
|
|
6298
7789
|
for (const [layer2, count] of Object.entries(manifest.layers)) {
|
|
6299
7790
|
info(` - ${layer2.padEnd(16)}: ${count} files`);
|
|
6300
7791
|
}
|
|
6301
7792
|
});
|
|
6302
|
-
return
|
|
7793
|
+
return 0;
|
|
6303
7794
|
}
|
|
6304
7795
|
if (invocation.command === "explain") {
|
|
6305
7796
|
const result = explainPath(resolved, invocation.args[0]);
|
|
6306
7797
|
emit(invocation.format, result, () => info(renderExplainText(result)));
|
|
6307
|
-
return
|
|
7798
|
+
return 0;
|
|
6308
7799
|
}
|
|
6309
7800
|
if (invocation.command === "diff") {
|
|
6310
7801
|
try {
|
|
@@ -6328,7 +7819,9 @@ ${USAGE}
|
|
|
6328
7819
|
if (invocation.dryRun) {
|
|
6329
7820
|
info("\n\u{1F9EA} Dry run \u2014 nothing was written:");
|
|
6330
7821
|
info(` - Would track: ${manifest.counts.totalFiles} files`);
|
|
6331
|
-
info(
|
|
7822
|
+
info(
|
|
7823
|
+
` - Would write: ${manifest.counts.partsCount} part(s) under ${displayOutPath(resolved.cwd, resolved.outDir, "parts")}`
|
|
7824
|
+
);
|
|
6332
7825
|
info(` - Skipped: ${manifest.counts.skippedCount} file(s)`);
|
|
6333
7826
|
info(` - Warnings: ${manifest.warnings.length}`);
|
|
6334
7827
|
info(` - Integrity: ${manifest.validation.status}`);
|
|
@@ -6343,7 +7836,9 @@ ${USAGE}
|
|
|
6343
7836
|
);
|
|
6344
7837
|
const { artifacts } = resolved;
|
|
6345
7838
|
if (artifacts.parts) {
|
|
6346
|
-
info(
|
|
7839
|
+
info(
|
|
7840
|
+
` - Parts written: ${manifest.counts.partsCount} under ${displayOutPath(resolved.cwd, resolved.outDir, "parts")}/`
|
|
7841
|
+
);
|
|
6347
7842
|
}
|
|
6348
7843
|
const reports = [
|
|
6349
7844
|
artifacts.projectTree && "project-tree",
|
|
@@ -6351,10 +7846,28 @@ ${USAGE}
|
|
|
6351
7846
|
artifacts.audit && "audit"
|
|
6352
7847
|
].filter(Boolean);
|
|
6353
7848
|
if (reports.length > 0) {
|
|
6354
|
-
info(
|
|
7849
|
+
info(
|
|
7850
|
+
` - Reports: ${displayOutPath(resolved.cwd, resolved.outDir, "reports")}/ (${reports.join(", ")})`
|
|
7851
|
+
);
|
|
7852
|
+
}
|
|
7853
|
+
if (artifacts.manifest) {
|
|
7854
|
+
info(` - Manifest: ${displayOutPath(resolved.cwd, resolved.outDir, "manifest.json")}`);
|
|
7855
|
+
}
|
|
7856
|
+
if (artifacts.monolith) {
|
|
7857
|
+
const monolithFiles = [
|
|
7858
|
+
manifest.artifacts?.fullJson ? "full-codebase.json" : null,
|
|
7859
|
+
manifest.artifacts?.fullMarkdown ? "full-codebase.md" : null
|
|
7860
|
+
].filter((name) => name !== null);
|
|
7861
|
+
if (monolithFiles.length === 2) {
|
|
7862
|
+
info(
|
|
7863
|
+
` - Monolith: ${displayOutPath(resolved.cwd, resolved.outDir, "full-codebase.{json,md}")}`
|
|
7864
|
+
);
|
|
7865
|
+
} else if (monolithFiles.length === 1) {
|
|
7866
|
+
info(
|
|
7867
|
+
` - Monolith: ${displayOutPath(resolved.cwd, resolved.outDir, monolithFiles[0])} (the other was skipped by --max-monolith-bytes)`
|
|
7868
|
+
);
|
|
7869
|
+
}
|
|
6355
7870
|
}
|
|
6356
|
-
if (artifacts.manifest) info(` - Manifest: ${resolved.outDir}/manifest.json`);
|
|
6357
|
-
if (artifacts.monolith) info(` - Monolith: ${resolved.outDir}/full-codebase.{json,md}`);
|
|
6358
7871
|
if (reports.length === 0 && !artifacts.parts && !artifacts.manifest && !artifacts.monolith) {
|
|
6359
7872
|
info(` - Artifacts: all disabled (only the ownership marker was written)`);
|
|
6360
7873
|
}
|
|
@@ -6389,18 +7902,43 @@ ${USAGE}
|
|
|
6389
7902
|
async function runWatchLoop(invocation, resolved) {
|
|
6390
7903
|
const outDir = path12.resolve(resolved.cwd, resolved.outDir);
|
|
6391
7904
|
const tmpDir = path12.resolve(resolved.cwd, resolved.tmpDir);
|
|
7905
|
+
const VCS_SEGMENTS = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr"]);
|
|
6392
7906
|
const outName = path12.basename(outDir).toLowerCase();
|
|
6393
7907
|
const tmpName = path12.basename(tmpDir).toLowerCase();
|
|
6394
7908
|
const isGenerated = (candidate) => candidate.toLowerCase().split(/[\\/]/).some(
|
|
6395
|
-
(segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`)
|
|
7909
|
+
(segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`) || VCS_SEGMENTS.has(segment)
|
|
6396
7910
|
);
|
|
6397
7911
|
let running = false;
|
|
6398
|
-
const regenerate = () => {
|
|
7912
|
+
const regenerate = async () => {
|
|
6399
7913
|
if (running) return;
|
|
6400
7914
|
running = true;
|
|
6401
7915
|
const started = performance.now();
|
|
6402
7916
|
try {
|
|
6403
|
-
const
|
|
7917
|
+
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
7918
|
+
if (loaded.errors.length > 0) {
|
|
7919
|
+
for (const error of loaded.errors) warn(`\u26A0 Config: ${error}`);
|
|
7920
|
+
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
7921
|
+
return;
|
|
7922
|
+
}
|
|
7923
|
+
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
7924
|
+
const next = resolveConfig(
|
|
7925
|
+
loaded.config,
|
|
7926
|
+
{
|
|
7927
|
+
mode: invocation.command,
|
|
7928
|
+
format: invocation.format,
|
|
7929
|
+
dryRun: invocation.dryRun,
|
|
7930
|
+
cwd: invocation.cwd,
|
|
7931
|
+
outDir: invocation.outDir,
|
|
7932
|
+
includeMonolith: invocation.includeMonolith,
|
|
7933
|
+
includeSensitiveFiles: invocation.includeSensitiveFiles,
|
|
7934
|
+
maxPartBytes: invocation.maxPartBytes,
|
|
7935
|
+
largeFileBytes: invocation.largeFileBytes,
|
|
7936
|
+
maxMonolithBytes: invocation.maxMonolithBytes,
|
|
7937
|
+
skipArtifacts: invocation.skipArtifacts
|
|
7938
|
+
},
|
|
7939
|
+
invocation.cwd
|
|
7940
|
+
);
|
|
7941
|
+
const manifest = generateSnapshot(next, { dryRun: invocation.dryRun });
|
|
6404
7942
|
const elapsed = ((performance.now() - started) / 1e3).toFixed(2);
|
|
6405
7943
|
info(
|
|
6406
7944
|
`\u267B\uFE0F ${(/* @__PURE__ */ new Date()).toISOString()} ${manifest.counts.totalFiles} file(s), ${manifest.counts.partsCount} part(s), ${manifest.validation.status} (${elapsed}s)`
|
|
@@ -6412,7 +7950,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
6412
7950
|
}
|
|
6413
7951
|
};
|
|
6414
7952
|
info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
|
|
6415
|
-
regenerate();
|
|
7953
|
+
void regenerate();
|
|
6416
7954
|
let timer = null;
|
|
6417
7955
|
const watcher = fs11.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
|
|
6418
7956
|
if (filename) {
|
|
@@ -6420,7 +7958,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
6420
7958
|
if (isGenerated(changed)) return;
|
|
6421
7959
|
}
|
|
6422
7960
|
if (timer) clearTimeout(timer);
|
|
6423
|
-
timer = setTimeout(regenerate, 250);
|
|
7961
|
+
timer = setTimeout(() => void regenerate(), 250);
|
|
6424
7962
|
});
|
|
6425
7963
|
await new Promise((resolve6) => {
|
|
6426
7964
|
const stop = () => {
|