@ankurhalder/codebase-snapshot 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -310
- package/dist/bin.js +1939 -570
- package/dist/{chunk-5B4UVQLR.js → chunk-FM4OOQPF.js} +1558 -390
- package/dist/cli.cjs +1907 -537
- package/dist/cli.d.cts +3 -1
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +265 -64
- package/dist/index.cjs +1557 -387
- package/dist/index.d.cts +22 -28
- package/dist/index.d.ts +22 -28
- package/dist/index.js +1 -1
- package/dist/{types-CJqkePM4.d.cts → types-BdpT_4b8.d.cts} +37 -4
- package/dist/{types-CJqkePM4.d.ts → types-BdpT_4b8.d.ts} +37 -4
- package/docs/agent.md +77 -37
- package/docs/cli.md +315 -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
|
@@ -25,7 +25,40 @@ function hashMap(manifest) {
|
|
|
25
25
|
}
|
|
26
26
|
return map;
|
|
27
27
|
}
|
|
28
|
+
function manifestProblem(value) {
|
|
29
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
|
|
30
|
+
const candidate = value;
|
|
31
|
+
if (!Array.isArray(candidate.files)) return 'no "files" array';
|
|
32
|
+
for (const entry of candidate.files) {
|
|
33
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
34
|
+
return 'a "files" entry is not an object';
|
|
35
|
+
}
|
|
36
|
+
if (typeof entry.relativePath !== "string") {
|
|
37
|
+
return 'a "files" entry has no "relativePath"';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const counts = candidate.counts;
|
|
41
|
+
if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
|
|
42
|
+
return 'no "counts" object';
|
|
43
|
+
}
|
|
44
|
+
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
45
|
+
if (typeof counts[key] !== "number") {
|
|
46
|
+
return `no numeric "counts.${key}"`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
|
|
50
|
+
if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function assertUsableManifest(value, label) {
|
|
54
|
+
const problem = manifestProblem(value);
|
|
55
|
+
if (problem) {
|
|
56
|
+
throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
28
59
|
function compareManifests(before, after) {
|
|
60
|
+
assertUsableManifest(before, "before");
|
|
61
|
+
assertUsableManifest(after, "after");
|
|
29
62
|
const left = hashMap(before);
|
|
30
63
|
const right = hashMap(after);
|
|
31
64
|
const added = [];
|
|
@@ -71,11 +104,11 @@ function loadManifest(target) {
|
|
|
71
104
|
} catch (err) {
|
|
72
105
|
throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
|
|
73
106
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
107
|
+
const problem = manifestProblem(parsed);
|
|
108
|
+
if (problem) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
|
|
111
|
+
);
|
|
79
112
|
}
|
|
80
113
|
return parsed;
|
|
81
114
|
}
|
|
@@ -101,8 +134,8 @@ function renderDiffText(diff) {
|
|
|
101
134
|
}
|
|
102
135
|
|
|
103
136
|
// src/explain.ts
|
|
104
|
-
import * as
|
|
105
|
-
import * as
|
|
137
|
+
import * as fs6 from "fs";
|
|
138
|
+
import * as path7 from "path";
|
|
106
139
|
|
|
107
140
|
// src/config.ts
|
|
108
141
|
import * as fs4 from "fs";
|
|
@@ -955,6 +988,416 @@ function detectPreset(cwd, forced) {
|
|
|
955
988
|
import * as childProcess from "child_process";
|
|
956
989
|
import * as fs3 from "fs";
|
|
957
990
|
import * as path3 from "path";
|
|
991
|
+
|
|
992
|
+
// src/security/patterns.ts
|
|
993
|
+
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)";
|
|
994
|
+
var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
|
|
995
|
+
var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|process\\.env|Buffer|Record|object)\\b)";
|
|
996
|
+
var UNQUOTED_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{1,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
997
|
+
var UNQUOTED_AMBIGUOUS_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{8,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
998
|
+
var UNQUOTED_STRONG_NAME = `(?<!id[-_])${STRONG_CREDENTIAL_NAME}`;
|
|
999
|
+
var DEFAULT_SECRET_PATTERNS = [
|
|
1000
|
+
{
|
|
1001
|
+
rule: "private-key-header",
|
|
1002
|
+
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
rule: "jwt-token",
|
|
1006
|
+
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
rule: "aws-access-key-id",
|
|
1010
|
+
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
1011
|
+
},
|
|
1012
|
+
{
|
|
1013
|
+
// The name is no longer limited to the three snake_case spellings: an
|
|
1014
|
+
// `awsSecret` / `secretAccessKey` property leaked verbatim because only the
|
|
1015
|
+
// literal words matched. The value is masked alone, so the key survives.
|
|
1016
|
+
rule: "aws-secret-access-key",
|
|
1017
|
+
pattern: new RegExp(
|
|
1018
|
+
`${STRONG_CREDENTIAL_NAME}["']?\\s{0,8}[:=]\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
|
|
1019
|
+
"i"
|
|
1020
|
+
),
|
|
1021
|
+
valueGroup: 1
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
// A 40-character base64 value with no key name in sight. A key-independent
|
|
1025
|
+
// rule is the only thing that catches `new AWS.SecretKey(…)` arguments and
|
|
1026
|
+
// pasted `~/.aws/credentials` fragments, but a bare length test also matches
|
|
1027
|
+
// every 40-char git SHA-1, so the value must mix case and digits and must
|
|
1028
|
+
// not be pure hex.
|
|
1029
|
+
rule: "aws-secret-access-key-value",
|
|
1030
|
+
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}/
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
rule: "github-token",
|
|
1034
|
+
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
// `xoxc-`/`xoxd-` are the user and refresh tokens, `xoxb`/`xoxp` the bot and
|
|
1038
|
+
// user tokens; the whole Slack family shares one prefix shape.
|
|
1039
|
+
rule: "slack-token",
|
|
1040
|
+
pattern: /xox[baprsdc]-[0-9A-Za-z-]{10,250}/
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
// Prefix-only vendors. Each prefix below is documented, globally unique and
|
|
1044
|
+
// cannot collide with an ordinary identifier, which is what makes a bare
|
|
1045
|
+
// prefix rule cheap and safe: `glpat-` (GitLab), `hvs.`/`s.` (Vault service
|
|
1046
|
+
// and batch tokens), `whsec_` (Stripe webhook secret), `dckr_pat_` (Docker
|
|
1047
|
+
// Hub), `hf_` (HuggingFace), `ya29.` (Google OAuth access token).
|
|
1048
|
+
rule: "known-vendor-token-prefix",
|
|
1049
|
+
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})/
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
rule: "slack-webhook-url",
|
|
1053
|
+
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
rule: "stripe-secret-key",
|
|
1057
|
+
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
1058
|
+
},
|
|
1059
|
+
{
|
|
1060
|
+
rule: "google-api-key",
|
|
1061
|
+
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
1062
|
+
},
|
|
1063
|
+
{
|
|
1064
|
+
rule: "google-oauth-client-secret",
|
|
1065
|
+
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
rule: "openai-anthropic-key",
|
|
1069
|
+
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
rule: "npm-token",
|
|
1073
|
+
pattern: /npm_[A-Za-z0-9]{36}/
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
rule: "pypi-token",
|
|
1077
|
+
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
rule: "sendgrid-key",
|
|
1081
|
+
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
rule: "twilio-api-key",
|
|
1085
|
+
pattern: /SK[0-9a-fA-F]{32}/
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
rule: "digitalocean-token",
|
|
1089
|
+
pattern: /dop_v1_[0-9a-f]{64}/
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
rule: "azure-storage-account-key",
|
|
1093
|
+
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
// Scheme-less userinfo: `//user:password@host`.
|
|
1097
|
+
//
|
|
1098
|
+
// Enumerating schemes is a losing game — `mysqlx`, `svn`, `telnet`, `imap`,
|
|
1099
|
+
// `pop3`, `nntp`, `kafka`, `elasticsearch` each leaked a password while the
|
|
1100
|
+
// list grew by hand. This rule matches the shape instead, so any scheme
|
|
1101
|
+
// (including one invented tomorrow) is covered, provided the `//` is followed
|
|
1102
|
+
// immediately by the userinfo: `// see the docs` and a bare reminder comment
|
|
1103
|
+
// never match, because a space or the comment wording breaks the shape.
|
|
1104
|
+
rule: "uri-credentials",
|
|
1105
|
+
pattern: /\/\/[^\s:/@"'`;()<>,]{1,128}:[^\s:/@"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
// The host tail stops at quotes, `;`, `)`, `<`, `>` and `,`: an unbounded
|
|
1109
|
+
// `[^\s/]` used to swallow the closing quote and the statement separator of
|
|
1110
|
+
// the string the URL was written in.
|
|
1111
|
+
rule: "database-uri-credentials",
|
|
1112
|
+
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}/
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
rule: "basic-auth-url",
|
|
1116
|
+
pattern: /(?:https?|ftp|sftp|ssh|git|smtp|smtps|ldap|ldaps):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/i
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
// Quoted credentials in any common key spelling. The value may be as short
|
|
1120
|
+
// as one character: `password: "hunter2"` is a credential whatever its
|
|
1121
|
+
// entropy, and a length floor was exactly how it survived into `parts`.
|
|
1122
|
+
//
|
|
1123
|
+
// The key is either bare (`dbPassword:`) or wholly quoted (`"dbPassword":`).
|
|
1124
|
+
// There is deliberately no optional quote *before* the separator: that let a
|
|
1125
|
+
// previous string's closing quote stand in for the key's, so
|
|
1126
|
+
// ``${pass ? " PASS" : " FAIL"}`` had `" FAIL"` masked as if `PASS` were
|
|
1127
|
+
// the key name.
|
|
1128
|
+
rule: "credential-assignment",
|
|
1129
|
+
pattern: new RegExp(
|
|
1130
|
+
`(?:["']${STRONG_CREDENTIAL_NAME}["']|${STRONG_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{1,512})\\1`,
|
|
1131
|
+
"i"
|
|
1132
|
+
),
|
|
1133
|
+
valueGroup: 2
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
// `pass`, `id-token` and `credentials` are ambiguous on their own, so they
|
|
1137
|
+
// are accepted only against a quoted value long enough to be a credential.
|
|
1138
|
+
rule: "credential-assignment-weak",
|
|
1139
|
+
pattern: new RegExp(
|
|
1140
|
+
`(?:["']${WEAK_CREDENTIAL_NAME}["']|${WEAK_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{8,512})\\1`,
|
|
1141
|
+
"i"
|
|
1142
|
+
),
|
|
1143
|
+
valueGroup: 2
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
// The unquoted twin for YAML, docker-compose and `.env`: those formats never
|
|
1147
|
+
// quote their values, so the rules above cannot see them. `valueGroup` keeps
|
|
1148
|
+
// `POSTGRES_PASSWORD:` in the output.
|
|
1149
|
+
rule: "credential-assignment-unquoted",
|
|
1150
|
+
pattern: new RegExp(
|
|
1151
|
+
`${UNQUOTED_STRONG_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
|
|
1152
|
+
"i"
|
|
1153
|
+
),
|
|
1154
|
+
valueGroup: 1
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
// `pass: hunter2hunter2` and `credentials: hunter2hunter2`: the ambiguous
|
|
1158
|
+
// names, unquoted, with the length and non-digit floor that keeps
|
|
1159
|
+
// `pass: 3` / `credentials: none` readable.
|
|
1160
|
+
rule: "credential-assignment-unquoted-weak",
|
|
1161
|
+
pattern: new RegExp(
|
|
1162
|
+
`${WEAK_CREDENTIAL_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
|
|
1163
|
+
"i"
|
|
1164
|
+
),
|
|
1165
|
+
valueGroup: 1
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
// `Authorization: Bearer <token>` and `Authorization: Basic <base64>`.
|
|
1169
|
+
//
|
|
1170
|
+
// The scheme is consumed by a lookbehind so the match is the token alone and
|
|
1171
|
+
// needs no `valueGroup`. A lone `Bearer`/`Basic`, and a word after it that
|
|
1172
|
+
// cannot be a credential (`Bearer authentication`), stay readable: the value
|
|
1173
|
+
// must contain a digit, be at least 32 chars, or carry base64 `=` padding.
|
|
1174
|
+
rule: "authorization-header",
|
|
1175
|
+
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_.+/=-])/
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
// Broader names than before (`apiKey`, `clientSecret`, `privateKey` were
|
|
1179
|
+
// invisible to it) and value-only masking. Kept alongside the two rules
|
|
1180
|
+
// above because it also matches a *quoted key* followed by an unquoted
|
|
1181
|
+
// value, e.g. `"client_secret": abc123`.
|
|
1182
|
+
rule: "generic-secret-assignment",
|
|
1183
|
+
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["']([^"'\s]{8,256})["']/i,
|
|
1184
|
+
valueGroup: 1
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
1188
|
+
// was therefore invisible to the rule above. Anchored to the line start and
|
|
1189
|
+
// limited to secret-looking names to stay at zero false positives.
|
|
1190
|
+
//
|
|
1191
|
+
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
1192
|
+
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
1193
|
+
// every offset of a long identifier, which made this rule quadratic
|
|
1194
|
+
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
1195
|
+
// The value is matched on its own — `=\s*(value)` — and masked alone.
|
|
1196
|
+
// Anchoring the value to end-of-line (`[^\s#]{6,512}$`) meant ANY trailing
|
|
1197
|
+
// comment or space disabled the rule, so `TOKEN=secret # rotate` and
|
|
1198
|
+
// `TOKEN=secret ` were emitted in clear; a CRLF file disabled it wholesale.
|
|
1199
|
+
rule: "env-secret-assignment",
|
|
1200
|
+
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,
|
|
1201
|
+
valueGroup: 1
|
|
1202
|
+
}
|
|
1203
|
+
];
|
|
1204
|
+
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
1205
|
+
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
1206
|
+
|
|
1207
|
+
// src/security/redact.ts
|
|
1208
|
+
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
1209
|
+
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
1210
|
+
function describeValue(value) {
|
|
1211
|
+
if (value === null) return "null";
|
|
1212
|
+
if (Array.isArray(value)) return "an array";
|
|
1213
|
+
if (value instanceof RegExp) return value.toString();
|
|
1214
|
+
if (typeof value === "string") return `the string ${JSON.stringify(value)}`;
|
|
1215
|
+
return `a ${typeof value}`;
|
|
1216
|
+
}
|
|
1217
|
+
function compilePattern(rule, pattern, valueGroup) {
|
|
1218
|
+
if (pattern instanceof RegExp) {
|
|
1219
|
+
const flags = new Set(pattern.flags.replace(/[gy]/g, "").split(""));
|
|
1220
|
+
if (valueGroup !== void 0) flags.add("d");
|
|
1221
|
+
return {
|
|
1222
|
+
rule,
|
|
1223
|
+
pattern: new RegExp(pattern.source, [...flags].join("")),
|
|
1224
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1225
|
+
};
|
|
1226
|
+
}
|
|
1227
|
+
if (typeof pattern === "string") {
|
|
1228
|
+
try {
|
|
1229
|
+
return {
|
|
1230
|
+
rule,
|
|
1231
|
+
pattern: new RegExp(pattern, valueGroup === void 0 ? "i" : "id"),
|
|
1232
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1233
|
+
};
|
|
1234
|
+
} catch (err) {
|
|
1235
|
+
throw new Error(
|
|
1236
|
+
`Invalid regular expression for secret rule "${rule}": ${err.message}`
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
throw new Error(
|
|
1241
|
+
`Secret rule "${rule}" has a pattern that is neither a RegExp nor a string (received ${describeValue(pattern)}).`
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
function compileSecretRules(rules) {
|
|
1245
|
+
if (!Array.isArray(rules)) {
|
|
1246
|
+
throw new Error(
|
|
1247
|
+
`secrets.customPatterns must be an array of secret rules (received ${describeValue(rules)}).`
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
let generated = 0;
|
|
1251
|
+
return rules.map((entry, index) => {
|
|
1252
|
+
const where = `secrets.customPatterns[${index}]`;
|
|
1253
|
+
if (entry instanceof RegExp) {
|
|
1254
|
+
generated += 1;
|
|
1255
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1256
|
+
}
|
|
1257
|
+
if (typeof entry === "string") {
|
|
1258
|
+
generated += 1;
|
|
1259
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1260
|
+
}
|
|
1261
|
+
if (typeof entry === "object" && entry !== null) {
|
|
1262
|
+
const candidate = entry;
|
|
1263
|
+
if (typeof candidate.rule !== "string" || candidate.rule.trim() === "") {
|
|
1264
|
+
throw new Error(
|
|
1265
|
+
`${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.`
|
|
1266
|
+
);
|
|
1267
|
+
}
|
|
1268
|
+
const valueGroup = candidate.valueGroup;
|
|
1269
|
+
if (valueGroup !== void 0) {
|
|
1270
|
+
if (typeof valueGroup !== "number" || !Number.isInteger(valueGroup) || valueGroup < 0) {
|
|
1271
|
+
throw new Error(
|
|
1272
|
+
`${where}.valueGroup must be a non-negative integer capture-group index (received ${describeValue(valueGroup)}).`
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (candidate.pattern === void 0) {
|
|
1277
|
+
throw new Error(
|
|
1278
|
+
`${where} (rule "${candidate.rule}") is missing \`pattern\`. Expected { rule, pattern: RegExp | string }.`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
return compilePattern(
|
|
1282
|
+
candidate.rule,
|
|
1283
|
+
candidate.pattern,
|
|
1284
|
+
valueGroup
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
throw new Error(
|
|
1288
|
+
`${where} must be a RegExp, a string, or { rule, pattern } (received ${describeValue(entry)}).`
|
|
1289
|
+
);
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
function toGlobal(rule) {
|
|
1293
|
+
const flags = new Set(rule.pattern.flags.replace(/[gy]/g, "").split(""));
|
|
1294
|
+
flags.add("g");
|
|
1295
|
+
if (rule.valueGroup !== void 0) flags.add("d");
|
|
1296
|
+
return new RegExp(rule.pattern.source, [...flags].join(""));
|
|
1297
|
+
}
|
|
1298
|
+
var LINE_BREAK = /(\r\n|\n|\r)/;
|
|
1299
|
+
function scanAndRedactSecrets(content, rules, filePath) {
|
|
1300
|
+
const findings = [];
|
|
1301
|
+
let redacted = content;
|
|
1302
|
+
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
1303
|
+
blockPattern.lastIndex = 0;
|
|
1304
|
+
let match;
|
|
1305
|
+
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
1306
|
+
if (match[0].length === 0) {
|
|
1307
|
+
blockPattern.lastIndex += 1;
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
const line = redacted.slice(0, match.index).split("\n").length;
|
|
1311
|
+
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
1312
|
+
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
1313
|
+
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
1314
|
+
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
const compiled = rules.map((rule) => ({
|
|
1318
|
+
rule: rule.rule,
|
|
1319
|
+
pattern: toGlobal(rule),
|
|
1320
|
+
...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
|
|
1321
|
+
}));
|
|
1322
|
+
const segments = redacted.split(LINE_BREAK);
|
|
1323
|
+
let lineNumber = 1;
|
|
1324
|
+
for (let index = 0; index < segments.length; index += 2) {
|
|
1325
|
+
const line = segments[index];
|
|
1326
|
+
if (line.length > 0) {
|
|
1327
|
+
const spans = [];
|
|
1328
|
+
const matchedRules = /* @__PURE__ */ new Set();
|
|
1329
|
+
for (const { rule, pattern, valueGroup } of compiled) {
|
|
1330
|
+
pattern.lastIndex = 0;
|
|
1331
|
+
let match;
|
|
1332
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
1333
|
+
if (match[0].length === 0) {
|
|
1334
|
+
pattern.lastIndex += 1;
|
|
1335
|
+
continue;
|
|
1336
|
+
}
|
|
1337
|
+
const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
|
|
1338
|
+
spans.push(
|
|
1339
|
+
group ? { start: group[0], end: group[1], rule } : { start: match.index, end: match.index + match[0].length, rule }
|
|
1340
|
+
);
|
|
1341
|
+
matchedRules.add(rule);
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
if (spans.length > 0) {
|
|
1345
|
+
for (const rule of matchedRules) {
|
|
1346
|
+
findings.push({ rule, line: lineNumber, path: filePath });
|
|
1347
|
+
}
|
|
1348
|
+
segments[index] = applySpans(line, spans);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
if (index + 1 < segments.length) lineNumber += 1;
|
|
1352
|
+
}
|
|
1353
|
+
return { redacted: segments.join(""), findings };
|
|
1354
|
+
}
|
|
1355
|
+
var TOKEN_CHARS_LEFT = /[A-Za-z0-9_+/-]/;
|
|
1356
|
+
var TOKEN_CHARS_RIGHT = /[A-Za-z0-9_+/=-]/;
|
|
1357
|
+
function expandOverToken(line, start, end) {
|
|
1358
|
+
let left = start;
|
|
1359
|
+
let right = end;
|
|
1360
|
+
while (left > 0 && TOKEN_CHARS_LEFT.test(line[left - 1])) left -= 1;
|
|
1361
|
+
while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
|
|
1362
|
+
return { start: left, end: right };
|
|
1363
|
+
}
|
|
1364
|
+
function applySpans(line, spans) {
|
|
1365
|
+
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1366
|
+
const merged = [];
|
|
1367
|
+
for (const span of ordered) {
|
|
1368
|
+
const clampedStart = Math.max(0, Math.min(span.start, line.length));
|
|
1369
|
+
const clampedEnd = Math.max(clampedStart, Math.min(span.end, line.length));
|
|
1370
|
+
const { start, end } = expandOverToken(line, clampedStart, clampedEnd);
|
|
1371
|
+
const last = merged[merged.length - 1];
|
|
1372
|
+
if (last && start <= last.end) last.end = Math.max(last.end, end);
|
|
1373
|
+
else merged.push({ start, end });
|
|
1374
|
+
}
|
|
1375
|
+
let out = "";
|
|
1376
|
+
let cursor = 0;
|
|
1377
|
+
for (const span of merged) {
|
|
1378
|
+
out += line.slice(cursor, span.start);
|
|
1379
|
+
out += REDACTION_MARKER;
|
|
1380
|
+
cursor = span.end;
|
|
1381
|
+
}
|
|
1382
|
+
out += line.slice(cursor);
|
|
1383
|
+
return out;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// src/scanner/files.ts
|
|
1387
|
+
var PRESET_IGNORES = new Set(
|
|
1388
|
+
PRESETS.flatMap((preset) => preset.exclude ?? [])
|
|
1389
|
+
);
|
|
1390
|
+
function readLinkTarget(linkPath, fallback) {
|
|
1391
|
+
try {
|
|
1392
|
+
return fs3.readlinkSync(linkPath);
|
|
1393
|
+
} catch {
|
|
1394
|
+
return fallback;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function isInside(root, candidate) {
|
|
1398
|
+
const relative5 = path3.relative(root, candidate);
|
|
1399
|
+
return relative5 === "" || !relative5.startsWith(`..${path3.sep}`) && relative5 !== ".." && !path3.isAbsolute(relative5);
|
|
1400
|
+
}
|
|
958
1401
|
function getGitInfo(cwd, fallbackBranch = "main") {
|
|
959
1402
|
try {
|
|
960
1403
|
const reported = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
|
|
@@ -999,20 +1442,61 @@ var DEFAULT_IGNORED_FILES = [
|
|
|
999
1442
|
"bun.lockb",
|
|
1000
1443
|
"composer.lock"
|
|
1001
1444
|
];
|
|
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;
|
|
1445
|
+
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
1446
|
var SENSITIVE_PATH = /(?:^|\/)\.(?:aws|ssh|gnupg)\/|(?:^|\/)\.docker\/config\.json$|(?:^|\/)\.kube\/config$/i;
|
|
1004
1447
|
var SENSITIVE_EXT = /\.(?:pem|key|p12|pfx|jks|keystore|ppk|asc|kdbx|tfstate|tfvars)$/i;
|
|
1005
1448
|
var TEMPLATE_SUFFIX = /\.(?:example|sample|template|dist)(?=$|[.])/i;
|
|
1449
|
+
var BACKUP_SUFFIX = /(?:~|\.(?:bak|backup|old|orig|save|copy|swp|swo|tmp|part))$/i;
|
|
1450
|
+
function normaliseName(name) {
|
|
1451
|
+
let normalised = name.trim().replace(/[. ]+$/, "");
|
|
1452
|
+
let previous = "";
|
|
1453
|
+
while (previous !== normalised) {
|
|
1454
|
+
previous = normalised;
|
|
1455
|
+
normalised = normalised.replace(BACKUP_SUFFIX, "");
|
|
1456
|
+
}
|
|
1457
|
+
return normalised;
|
|
1458
|
+
}
|
|
1006
1459
|
var matchesSensitiveList = (posixPath, base) => SENSITIVE_BASENAME.test(base) || SENSITIVE_PATH.test(posixPath) || SENSITIVE_EXT.test(base);
|
|
1007
1460
|
function isSensitivePath(relativePath) {
|
|
1008
1461
|
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1009
|
-
const
|
|
1462
|
+
const raw = posixPath.split("/").pop() ?? posixPath;
|
|
1463
|
+
const base = normaliseName(raw);
|
|
1010
1464
|
if (TEMPLATE_SUFFIX.test(base)) {
|
|
1011
1465
|
const stripped = base.replace(new RegExp(TEMPLATE_SUFFIX.source, "gi"), "");
|
|
1012
1466
|
if (matchesSensitiveList(posixPath, stripped)) return false;
|
|
1013
1467
|
}
|
|
1014
1468
|
return matchesSensitiveList(posixPath, base);
|
|
1015
1469
|
}
|
|
1470
|
+
function findSecretInPath(relativePath) {
|
|
1471
|
+
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1472
|
+
const spans = [];
|
|
1473
|
+
const rules = [];
|
|
1474
|
+
for (const { rule, pattern } of DEFAULT_SECRET_PATTERNS) {
|
|
1475
|
+
const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
|
|
1476
|
+
let match;
|
|
1477
|
+
while ((match = scan.exec(posixPath)) !== null) {
|
|
1478
|
+
if (match[0].length === 0) {
|
|
1479
|
+
scan.lastIndex += 1;
|
|
1480
|
+
continue;
|
|
1481
|
+
}
|
|
1482
|
+
spans.push({ start: match.index, end: match.index + match[0].length, rule });
|
|
1483
|
+
if (!rules.includes(rule)) rules.push(rule);
|
|
1484
|
+
}
|
|
1485
|
+
}
|
|
1486
|
+
if (spans.length === 0) return null;
|
|
1487
|
+
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1488
|
+
let out = "";
|
|
1489
|
+
let cursor = 0;
|
|
1490
|
+
for (const span of spans) {
|
|
1491
|
+
if (span.end <= cursor) continue;
|
|
1492
|
+
const start = Math.max(span.start, cursor);
|
|
1493
|
+
out += posixPath.slice(cursor, start);
|
|
1494
|
+
out += REDACTION_MARKER;
|
|
1495
|
+
cursor = Math.min(span.end, posixPath.length);
|
|
1496
|
+
}
|
|
1497
|
+
out += posixPath.slice(cursor);
|
|
1498
|
+
return { rules, masked: out };
|
|
1499
|
+
}
|
|
1016
1500
|
function globToRegExp(glob) {
|
|
1017
1501
|
const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1018
1502
|
const anchored = normalized.startsWith("/");
|
|
@@ -1133,31 +1617,158 @@ function defaultIgnoreReason(relativePath) {
|
|
|
1133
1617
|
}
|
|
1134
1618
|
return null;
|
|
1135
1619
|
}
|
|
1136
|
-
function
|
|
1137
|
-
const includePatterns = options.includePatterns ?? [];
|
|
1138
|
-
const excludePatterns = options.excludePatterns ?? [];
|
|
1139
|
-
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
1140
|
-
let candidates;
|
|
1141
|
-
let walkSkips = [];
|
|
1620
|
+
function listIgnoredPaths(cwd) {
|
|
1142
1621
|
try {
|
|
1143
1622
|
const stdout = childProcess.execSync(
|
|
1144
|
-
"git ls-files -z --
|
|
1623
|
+
"git ls-files -z --others --ignored --exclude-standard --directory",
|
|
1145
1624
|
{
|
|
1146
1625
|
cwd,
|
|
1147
1626
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1148
1627
|
encoding: "utf8",
|
|
1149
|
-
maxBuffer:
|
|
1628
|
+
maxBuffer: 32 * 1024 * 1024
|
|
1150
1629
|
}
|
|
1151
1630
|
);
|
|
1152
|
-
|
|
1631
|
+
return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1153
1632
|
} catch {
|
|
1154
|
-
|
|
1155
|
-
candidates = walked.files;
|
|
1156
|
-
walkSkips = walked.skipped;
|
|
1633
|
+
return [];
|
|
1157
1634
|
}
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1635
|
+
}
|
|
1636
|
+
var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
|
|
1637
|
+
function isToolOwnedDirName(name) {
|
|
1638
|
+
return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
|
|
1639
|
+
}
|
|
1640
|
+
function hasSnapshotMarker(target) {
|
|
1641
|
+
try {
|
|
1642
|
+
return fs3.statSync(target).isDirectory() && fs3.existsSync(path3.join(target, ".codebase-snapshot"));
|
|
1643
|
+
} catch {
|
|
1644
|
+
return false;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
function makeToolOwnedDecider(cwd) {
|
|
1648
|
+
const markers = /* @__PURE__ */ new Map();
|
|
1649
|
+
return (relativePath) => {
|
|
1650
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
1651
|
+
for (let depth = 1; depth < parts.length; depth += 1) {
|
|
1652
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
1653
|
+
if (depth === 1 && isToolOwnedDirName(prefix)) return true;
|
|
1654
|
+
let hasMarker = markers.get(prefix);
|
|
1655
|
+
if (hasMarker === void 0) {
|
|
1656
|
+
hasMarker = fs3.existsSync(path3.join(cwd, prefix, ".codebase-snapshot"));
|
|
1657
|
+
markers.set(prefix, hasMarker);
|
|
1658
|
+
}
|
|
1659
|
+
if (hasMarker) return true;
|
|
1660
|
+
}
|
|
1661
|
+
return false;
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
function makeLinkProbe() {
|
|
1665
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1666
|
+
return (absolutePath) => {
|
|
1667
|
+
let known = cache.get(absolutePath);
|
|
1668
|
+
if (known === void 0) {
|
|
1669
|
+
try {
|
|
1670
|
+
known = fs3.lstatSync(absolutePath).isSymbolicLink();
|
|
1671
|
+
} catch {
|
|
1672
|
+
known = false;
|
|
1673
|
+
}
|
|
1674
|
+
cache.set(absolutePath, known);
|
|
1675
|
+
}
|
|
1676
|
+
return known;
|
|
1677
|
+
};
|
|
1678
|
+
}
|
|
1679
|
+
function dropLinkedDirectories(cwd, candidates, skipped) {
|
|
1680
|
+
const isLink = makeLinkProbe();
|
|
1681
|
+
let rootReal = cwd;
|
|
1682
|
+
try {
|
|
1683
|
+
rootReal = fs3.realpathSync(cwd);
|
|
1684
|
+
} catch {
|
|
1685
|
+
}
|
|
1686
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1687
|
+
const kept = [];
|
|
1688
|
+
for (const candidate of candidates) {
|
|
1689
|
+
const parts = candidate.split("/");
|
|
1690
|
+
let linkedDir = null;
|
|
1691
|
+
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
1692
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
1693
|
+
if (isLink(path3.join(cwd, prefix))) {
|
|
1694
|
+
linkedDir = prefix;
|
|
1695
|
+
break;
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
if (linkedDir === null) {
|
|
1699
|
+
kept.push(candidate);
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
if (reported.has(linkedDir)) continue;
|
|
1703
|
+
reported.add(linkedDir);
|
|
1704
|
+
const absolute = path3.join(cwd, linkedDir);
|
|
1705
|
+
let targetReal = absolute;
|
|
1706
|
+
try {
|
|
1707
|
+
targetReal = fs3.realpathSync(absolute);
|
|
1708
|
+
} catch {
|
|
1709
|
+
}
|
|
1710
|
+
const target = readLinkTarget(absolute, targetReal);
|
|
1711
|
+
const what = linkedDir === candidate ? "symbolic link" : "directory link";
|
|
1712
|
+
skipped.push(
|
|
1713
|
+
isInside(rootReal, targetReal) ? {
|
|
1714
|
+
path: linkedDir,
|
|
1715
|
+
reason: "symlink",
|
|
1716
|
+
detail: `${what} not followed (target: ${target})`
|
|
1717
|
+
} : {
|
|
1718
|
+
path: linkedDir,
|
|
1719
|
+
reason: "outside-root",
|
|
1720
|
+
detail: `link target resolves outside the scanned root (target: ${target}); not followed`
|
|
1721
|
+
}
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
return kept;
|
|
1725
|
+
}
|
|
1726
|
+
function collectFiles(cwd, options = {}) {
|
|
1727
|
+
const includePatterns = options.includePatterns ?? [];
|
|
1728
|
+
const excludePatterns = options.excludePatterns ?? [];
|
|
1729
|
+
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
1730
|
+
const isToolOwned = makeToolOwnedDecider(cwd);
|
|
1731
|
+
let candidates;
|
|
1732
|
+
const skipped = [];
|
|
1733
|
+
try {
|
|
1734
|
+
const stdout = childProcess.execSync(
|
|
1735
|
+
"git ls-files -z --cached --others --exclude-standard",
|
|
1736
|
+
{
|
|
1737
|
+
cwd,
|
|
1738
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1739
|
+
encoding: "utf8",
|
|
1740
|
+
maxBuffer: 50 * 1024 * 1024
|
|
1741
|
+
}
|
|
1742
|
+
);
|
|
1743
|
+
candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1744
|
+
candidates = dropLinkedDirectories(cwd, candidates, skipped);
|
|
1745
|
+
const known = new Set(candidates);
|
|
1746
|
+
for (const ignoredPath of listIgnoredPaths(cwd)) {
|
|
1747
|
+
const trimmed = ignoredPath.replace(/\/+$/, "");
|
|
1748
|
+
if (known.has(trimmed) || known.has(ignoredPath)) continue;
|
|
1749
|
+
if (isToolOwned(trimmed)) continue;
|
|
1750
|
+
if (isSensitivePath(trimmed)) {
|
|
1751
|
+
skipped.push({
|
|
1752
|
+
path: trimmed,
|
|
1753
|
+
reason: "sensitive-file",
|
|
1754
|
+
detail: "credential-bearing file, ignored by .gitignore; enable includeSensitiveFiles to emit it"
|
|
1755
|
+
});
|
|
1756
|
+
} else {
|
|
1757
|
+
skipped.push({
|
|
1758
|
+
path: trimmed,
|
|
1759
|
+
reason: "gitignore",
|
|
1760
|
+
detail: ignoredPath.endsWith("/") ? "ignored by .gitignore (directory, not descended)" : "ignored by .gitignore"
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
} catch {
|
|
1765
|
+
const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set(), true);
|
|
1766
|
+
candidates = walked.files;
|
|
1767
|
+
skipped.push(...walked.skipped);
|
|
1768
|
+
}
|
|
1769
|
+
const files = candidates.filter((file) => {
|
|
1770
|
+
if (isToolOwned(file)) return false;
|
|
1771
|
+
const ignored = defaultIgnoreReason(file);
|
|
1161
1772
|
if (ignored) {
|
|
1162
1773
|
skipped.push({ path: file, reason: "default-ignore", detail: ignored });
|
|
1163
1774
|
return false;
|
|
@@ -1172,10 +1783,11 @@ function collectFiles(cwd, options = {}) {
|
|
|
1172
1783
|
}
|
|
1173
1784
|
for (const pattern of excludePatterns) {
|
|
1174
1785
|
if (matchesExclude(file, pattern)) {
|
|
1786
|
+
const fromPreset = PRESET_IGNORES.has(pattern);
|
|
1175
1787
|
skipped.push({
|
|
1176
1788
|
path: file,
|
|
1177
|
-
reason: "excluded",
|
|
1178
|
-
detail: `matches exclude pattern "${pattern}"`
|
|
1789
|
+
reason: fromPreset ? "default-ignore" : "excluded",
|
|
1790
|
+
detail: fromPreset ? `preset ignore: "${pattern}"` : `matches exclude pattern "${pattern}"`
|
|
1179
1791
|
});
|
|
1180
1792
|
return false;
|
|
1181
1793
|
}
|
|
@@ -1188,11 +1800,20 @@ function collectFiles(cwd, options = {}) {
|
|
|
1188
1800
|
});
|
|
1189
1801
|
return false;
|
|
1190
1802
|
}
|
|
1803
|
+
const secret = findSecretInPath(file);
|
|
1804
|
+
if (secret) {
|
|
1805
|
+
skipped.push({
|
|
1806
|
+
path: secret.masked,
|
|
1807
|
+
reason: "sensitive-path",
|
|
1808
|
+
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`
|
|
1809
|
+
});
|
|
1810
|
+
return false;
|
|
1811
|
+
}
|
|
1191
1812
|
return true;
|
|
1192
1813
|
}).sort();
|
|
1193
1814
|
return { files, skipped };
|
|
1194
1815
|
}
|
|
1195
|
-
function walkDirectory(dir, baseDir, visited) {
|
|
1816
|
+
function walkDirectory(dir, baseDir, visited, isRoot = false) {
|
|
1196
1817
|
const results = { files: [], skipped: [] };
|
|
1197
1818
|
let realDir;
|
|
1198
1819
|
try {
|
|
@@ -1202,7 +1823,13 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1202
1823
|
}
|
|
1203
1824
|
if (visited.has(realDir)) return results;
|
|
1204
1825
|
visited.add(realDir);
|
|
1205
|
-
|
|
1826
|
+
let rootReal;
|
|
1827
|
+
try {
|
|
1828
|
+
rootReal = fs3.realpathSync(baseDir);
|
|
1829
|
+
} catch {
|
|
1830
|
+
rootReal = baseDir;
|
|
1831
|
+
}
|
|
1832
|
+
const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
|
|
1206
1833
|
let entries;
|
|
1207
1834
|
try {
|
|
1208
1835
|
entries = fs3.readdirSync(dir, { withFileTypes: true });
|
|
@@ -1211,30 +1838,79 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1211
1838
|
return results;
|
|
1212
1839
|
}
|
|
1213
1840
|
for (const entry of entries) {
|
|
1214
|
-
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) continue;
|
|
1215
1841
|
const fullPath = path3.join(dir, entry.name);
|
|
1216
1842
|
const relPath = path3.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
1217
|
-
if (
|
|
1843
|
+
if (isToolOwnedDirName(entry.name)) continue;
|
|
1844
|
+
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
|
|
1845
|
+
results.skipped.push({
|
|
1846
|
+
path: relPath,
|
|
1847
|
+
reason: "default-ignore",
|
|
1848
|
+
detail: `built-in ignore: directory "${entry.name}" (not descended)`
|
|
1849
|
+
});
|
|
1850
|
+
continue;
|
|
1851
|
+
}
|
|
1852
|
+
if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
|
|
1853
|
+
results.skipped.push({
|
|
1854
|
+
path: relPath,
|
|
1855
|
+
reason: "default-ignore",
|
|
1856
|
+
detail: "built-in ignore: filename"
|
|
1857
|
+
});
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
if (ignoredByGitignore(relPath)) {
|
|
1861
|
+
if (!hasSnapshotMarker(fullPath)) {
|
|
1862
|
+
results.skipped.push({
|
|
1863
|
+
path: relPath,
|
|
1864
|
+
reason: isSensitivePath(relPath) ? "sensitive-file" : "gitignore",
|
|
1865
|
+
detail: "ignored by .gitignore"
|
|
1866
|
+
});
|
|
1867
|
+
}
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1218
1870
|
let isDirectory = entry.isDirectory();
|
|
1219
1871
|
let isFile = entry.isFile();
|
|
1220
1872
|
if (entry.isSymbolicLink()) {
|
|
1873
|
+
let targetReal;
|
|
1221
1874
|
try {
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1875
|
+
targetReal = fs3.realpathSync(fullPath);
|
|
1876
|
+
} catch {
|
|
1877
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1878
|
+
continue;
|
|
1879
|
+
}
|
|
1880
|
+
let stats;
|
|
1881
|
+
try {
|
|
1882
|
+
stats = fs3.statSync(fullPath);
|
|
1883
|
+
} catch {
|
|
1884
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
|
|
1885
|
+
continue;
|
|
1886
|
+
}
|
|
1887
|
+
const linkTarget = readLinkTarget(fullPath, targetReal);
|
|
1888
|
+
if (!isInside(rootReal, targetReal)) {
|
|
1889
|
+
results.skipped.push({
|
|
1890
|
+
path: relPath,
|
|
1891
|
+
reason: "outside-root",
|
|
1892
|
+
detail: `link target resolves outside the scanned root (target: ${linkTarget}); not followed`
|
|
1893
|
+
});
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
if (stats.isDirectory()) {
|
|
1229
1897
|
results.skipped.push({
|
|
1230
1898
|
path: relPath,
|
|
1231
1899
|
reason: "symlink",
|
|
1232
|
-
detail:
|
|
1900
|
+
detail: `directory link not followed (target: ${linkTarget})`
|
|
1901
|
+
});
|
|
1902
|
+
continue;
|
|
1903
|
+
}
|
|
1904
|
+
if (!stats.isFile()) {
|
|
1905
|
+
results.skipped.push({
|
|
1906
|
+
path: relPath,
|
|
1907
|
+
reason: "symlink",
|
|
1908
|
+
detail: `link to neither a file nor a directory (target: ${linkTarget})`
|
|
1233
1909
|
});
|
|
1234
|
-
} catch {
|
|
1235
|
-
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1236
1910
|
continue;
|
|
1237
1911
|
}
|
|
1912
|
+
isDirectory = false;
|
|
1913
|
+
isFile = true;
|
|
1238
1914
|
}
|
|
1239
1915
|
if (isDirectory) {
|
|
1240
1916
|
const nested = walkDirectory(fullPath, baseDir, visited);
|
|
@@ -1247,195 +1923,6 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1247
1923
|
return results;
|
|
1248
1924
|
}
|
|
1249
1925
|
|
|
1250
|
-
// src/security/patterns.ts
|
|
1251
|
-
var DEFAULT_SECRET_PATTERNS = [
|
|
1252
|
-
{
|
|
1253
|
-
rule: "private-key-header",
|
|
1254
|
-
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
1255
|
-
},
|
|
1256
|
-
{
|
|
1257
|
-
rule: "jwt-token",
|
|
1258
|
-
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
1259
|
-
},
|
|
1260
|
-
{
|
|
1261
|
-
rule: "aws-access-key-id",
|
|
1262
|
-
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
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
|
|
1343
|
-
}
|
|
1344
|
-
];
|
|
1345
|
-
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
1346
|
-
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
1347
|
-
|
|
1348
|
-
// src/security/redact.ts
|
|
1349
|
-
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
1350
|
-
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
1351
|
-
function compileSecretRules(rules) {
|
|
1352
|
-
return rules.map((rule) => {
|
|
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
|
-
};
|
|
1358
|
-
}
|
|
1359
|
-
if (typeof rule.pattern === "string") {
|
|
1360
|
-
try {
|
|
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
|
-
}
|
|
1367
|
-
}
|
|
1368
|
-
throw new Error(
|
|
1369
|
-
`Secret rule "${String(rule.rule)}" has a pattern that is neither a RegExp nor a string.`
|
|
1370
|
-
);
|
|
1371
|
-
});
|
|
1372
|
-
}
|
|
1373
|
-
function toGlobal(pattern) {
|
|
1374
|
-
const flags = pattern.flags.replace(/[gy]/g, "");
|
|
1375
|
-
return new RegExp(pattern.source, `${flags}g`);
|
|
1376
|
-
}
|
|
1377
|
-
function scanAndRedactSecrets(content, rules, filePath) {
|
|
1378
|
-
const findings = [];
|
|
1379
|
-
let redacted = content;
|
|
1380
|
-
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
1381
|
-
blockPattern.lastIndex = 0;
|
|
1382
|
-
let match;
|
|
1383
|
-
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
1384
|
-
if (match[0].length === 0) {
|
|
1385
|
-
blockPattern.lastIndex += 1;
|
|
1386
|
-
continue;
|
|
1387
|
-
}
|
|
1388
|
-
const line = redacted.slice(0, match.index).split("\n").length;
|
|
1389
|
-
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
1390
|
-
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
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
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
const compiled = rules.map((rule) => ({
|
|
1396
|
-
rule: rule.rule,
|
|
1397
|
-
pattern: toGlobal(rule.pattern)
|
|
1398
|
-
}));
|
|
1399
|
-
const lines = redacted.split("\n");
|
|
1400
|
-
const redactedLines = lines.map((line, index) => {
|
|
1401
|
-
if (line.length === 0) return line;
|
|
1402
|
-
const spans = [];
|
|
1403
|
-
const matchedRules = /* @__PURE__ */ new Set();
|
|
1404
|
-
for (const { rule, pattern } of compiled) {
|
|
1405
|
-
pattern.lastIndex = 0;
|
|
1406
|
-
let match;
|
|
1407
|
-
while ((match = pattern.exec(line)) !== null) {
|
|
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 };
|
|
1423
|
-
}
|
|
1424
|
-
function applySpans(line, spans) {
|
|
1425
|
-
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1426
|
-
let out = "";
|
|
1427
|
-
let cursor = 0;
|
|
1428
|
-
for (const span of ordered) {
|
|
1429
|
-
if (span.end <= cursor) continue;
|
|
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);
|
|
1434
|
-
}
|
|
1435
|
-
out += line.slice(cursor);
|
|
1436
|
-
return out;
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
1926
|
// src/config.ts
|
|
1440
1927
|
var CONFIG_FILENAMES = [
|
|
1441
1928
|
"snapshot.config.ts",
|
|
@@ -1485,7 +1972,12 @@ function normalizeResolvedConfig(config) {
|
|
|
1485
1972
|
),
|
|
1486
1973
|
include: resolveInclude(config.include),
|
|
1487
1974
|
preset: typeof config.preset === "string" ? config.preset : "generic",
|
|
1488
|
-
presetExclude:
|
|
1975
|
+
presetExclude: [
|
|
1976
|
+
.../* @__PURE__ */ new Set([
|
|
1977
|
+
...Array.isArray(config.presetExclude) ? config.presetExclude : [],
|
|
1978
|
+
...outputExcludePatterns(config.cwd, config.outDir, config.tmpDir)
|
|
1979
|
+
])
|
|
1980
|
+
],
|
|
1489
1981
|
layersMode: config.layersMode === "replace" ? "replace" : "merge",
|
|
1490
1982
|
budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
|
|
1491
1983
|
hooks: resolveHooks(config.hooks),
|
|
@@ -1522,7 +2014,68 @@ function supportsTypeScriptImports() {
|
|
|
1522
2014
|
return major > 22 || major === 22 && minor >= 6;
|
|
1523
2015
|
}
|
|
1524
2016
|
function stripJsonComments(raw) {
|
|
1525
|
-
|
|
2017
|
+
const text2 = raw.replace(/^\uFEFF/, "");
|
|
2018
|
+
let out = "";
|
|
2019
|
+
let inString = false;
|
|
2020
|
+
let escaped = false;
|
|
2021
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2022
|
+
const char = text2[index];
|
|
2023
|
+
if (inString) {
|
|
2024
|
+
out += char;
|
|
2025
|
+
if (escaped) escaped = false;
|
|
2026
|
+
else if (char === "\\") escaped = true;
|
|
2027
|
+
else if (char === '"') inString = false;
|
|
2028
|
+
continue;
|
|
2029
|
+
}
|
|
2030
|
+
if (char === '"') {
|
|
2031
|
+
inString = true;
|
|
2032
|
+
out += char;
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
if (char === "/" && text2[index + 1] === "/") {
|
|
2036
|
+
while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
|
|
2037
|
+
index -= 1;
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
if (char === "/" && text2[index + 1] === "*") {
|
|
2041
|
+
index += 2;
|
|
2042
|
+
while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
|
|
2043
|
+
if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
|
|
2044
|
+
index += 1;
|
|
2045
|
+
}
|
|
2046
|
+
index += 1;
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
out += char;
|
|
2050
|
+
}
|
|
2051
|
+
return stripTrailingCommas(out);
|
|
2052
|
+
}
|
|
2053
|
+
function stripTrailingCommas(text2) {
|
|
2054
|
+
let out = "";
|
|
2055
|
+
let inString = false;
|
|
2056
|
+
let escaped = false;
|
|
2057
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2058
|
+
const char = text2[index];
|
|
2059
|
+
if (inString) {
|
|
2060
|
+
out += char;
|
|
2061
|
+
if (escaped) escaped = false;
|
|
2062
|
+
else if (char === "\\") escaped = true;
|
|
2063
|
+
else if (char === '"') inString = false;
|
|
2064
|
+
continue;
|
|
2065
|
+
}
|
|
2066
|
+
if (char === '"') {
|
|
2067
|
+
inString = true;
|
|
2068
|
+
out += char;
|
|
2069
|
+
continue;
|
|
2070
|
+
}
|
|
2071
|
+
if (char === ",") {
|
|
2072
|
+
let next = index + 1;
|
|
2073
|
+
while (next < text2.length && /\s/.test(text2[next])) next += 1;
|
|
2074
|
+
if (text2[next] === "}" || text2[next] === "]") continue;
|
|
2075
|
+
}
|
|
2076
|
+
out += char;
|
|
2077
|
+
}
|
|
2078
|
+
return out;
|
|
1526
2079
|
}
|
|
1527
2080
|
function parseJsonish(raw, file) {
|
|
1528
2081
|
try {
|
|
@@ -1531,6 +2084,47 @@ function parseJsonish(raw, file) {
|
|
|
1531
2084
|
throw new Error(`Could not parse ${file}: ${err.message}`);
|
|
1532
2085
|
}
|
|
1533
2086
|
}
|
|
2087
|
+
function describeValue2(value) {
|
|
2088
|
+
if (value === null) return "null";
|
|
2089
|
+
if (Array.isArray(value)) return "an array";
|
|
2090
|
+
if (value instanceof RegExp) return value.toString();
|
|
2091
|
+
if (typeof value === "function") return "a function";
|
|
2092
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
2093
|
+
if (typeof value === "object") {
|
|
2094
|
+
const keys = Object.keys(value);
|
|
2095
|
+
return keys.length > 0 ? `an object with keys ${keys.join(", ")}` : "an empty object";
|
|
2096
|
+
}
|
|
2097
|
+
return `${typeof value} (${String(value)})`;
|
|
2098
|
+
}
|
|
2099
|
+
var isThenable = (value) => typeof value.then === "function";
|
|
2100
|
+
function readConfigExport(exported, file) {
|
|
2101
|
+
if (exported === void 0 || exported === null) {
|
|
2102
|
+
throw new Error(
|
|
2103
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2104
|
+
);
|
|
2105
|
+
}
|
|
2106
|
+
if (Array.isArray(exported)) {
|
|
2107
|
+
throw new Error(
|
|
2108
|
+
`${file} exports an array; expected a single configuration object such as \`export default { outDir: "snapshot" }\`.`
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
if (typeof exported === "function") {
|
|
2112
|
+
throw new Error(
|
|
2113
|
+
`${file} exports a function; expected a configuration object. Call the function yourself and export its result.`
|
|
2114
|
+
);
|
|
2115
|
+
}
|
|
2116
|
+
if (typeof exported !== "object") {
|
|
2117
|
+
throw new Error(
|
|
2118
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2119
|
+
);
|
|
2120
|
+
}
|
|
2121
|
+
if (isThenable(exported)) {
|
|
2122
|
+
throw new Error(
|
|
2123
|
+
`${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;\`).`
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
return exported;
|
|
2127
|
+
}
|
|
1534
2128
|
async function loadConfigFileDetailed(cwd) {
|
|
1535
2129
|
const warnings = [];
|
|
1536
2130
|
const errors = [];
|
|
@@ -1560,13 +2154,7 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
1560
2154
|
}
|
|
1561
2155
|
try {
|
|
1562
2156
|
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
|
-
}
|
|
2157
|
+
const config = readConfigExport(imported.default ?? imported, fullPath);
|
|
1570
2158
|
return { config, file: fullPath, warnings, errors };
|
|
1571
2159
|
} catch (err) {
|
|
1572
2160
|
const message = `Could not load ${fullPath}: ${err.message}`;
|
|
@@ -1616,7 +2204,7 @@ var DEFAULT_ARTIFACTS = {
|
|
|
1616
2204
|
function isArtifactName(value) {
|
|
1617
2205
|
return ARTIFACT_NAMES.includes(value);
|
|
1618
2206
|
}
|
|
1619
|
-
function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
2207
|
+
function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
|
|
1620
2208
|
const artifacts = {
|
|
1621
2209
|
...DEFAULT_ARTIFACTS,
|
|
1622
2210
|
// `includeMonolith` predates this selection and stays the switch for it.
|
|
@@ -1638,19 +2226,119 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
1638
2226
|
artifacts[name] = value;
|
|
1639
2227
|
}
|
|
1640
2228
|
}
|
|
1641
|
-
|
|
1642
|
-
|
|
2229
|
+
if (monolithOverride !== void 0) {
|
|
2230
|
+
artifacts.monolith = monolithOverride;
|
|
2231
|
+
}
|
|
2232
|
+
for (const name of skipped ?? []) {
|
|
2233
|
+
if (!isArtifactName(name)) {
|
|
2234
|
+
throw new Error(
|
|
2235
|
+
`Unknown artifact "${name}". Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
|
|
2236
|
+
);
|
|
2237
|
+
}
|
|
2238
|
+
artifacts[name] = false;
|
|
2239
|
+
}
|
|
2240
|
+
return artifacts;
|
|
2241
|
+
}
|
|
2242
|
+
function strictPositiveInteger(value, fallback, label) {
|
|
2243
|
+
if (value === void 0) return fallback;
|
|
2244
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
2245
|
+
throw new Error(`${label} must be a positive integer (received ${describeValue2(value)}).`);
|
|
2246
|
+
}
|
|
2247
|
+
return value;
|
|
2248
|
+
}
|
|
2249
|
+
function strictBoolean(value, fallback, label) {
|
|
2250
|
+
if (value === void 0) return fallback;
|
|
2251
|
+
if (typeof value !== "boolean") {
|
|
2252
|
+
throw new Error(`${label} must be true or false (received ${describeValue2(value)}).`);
|
|
2253
|
+
}
|
|
2254
|
+
return value;
|
|
2255
|
+
}
|
|
2256
|
+
function strictString(value, fallback, label) {
|
|
2257
|
+
if (value === void 0) return fallback;
|
|
2258
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2259
|
+
throw new Error(`${label} must be a non-empty string (received ${describeValue2(value)}).`);
|
|
2260
|
+
}
|
|
2261
|
+
return value;
|
|
2262
|
+
}
|
|
2263
|
+
function resolveExcludePatterns(value) {
|
|
2264
|
+
if (value === void 0) return [];
|
|
2265
|
+
if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string")) {
|
|
2266
|
+
throw new Error(
|
|
2267
|
+
`\`excludePatterns\` must be an array of glob strings such as ["**/*.test.ts"] (received ${describeValue2(value)}).`
|
|
2268
|
+
);
|
|
2269
|
+
}
|
|
2270
|
+
return value.map((pattern) => pattern.trim()).filter(Boolean);
|
|
2271
|
+
}
|
|
2272
|
+
var AUTO_PRESET_ALIASES = /* @__PURE__ */ new Set(["auto", "detected"]);
|
|
2273
|
+
function resolvePresetName(value) {
|
|
2274
|
+
if (value === void 0) return void 0;
|
|
2275
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2276
|
+
throw new Error(
|
|
2277
|
+
`\`preset\` must be a preset name (received ${describeValue2(value)}). Use "detected" (or "auto") to detect one, or one of: ${listPresetNames().join(", ")}.`
|
|
2278
|
+
);
|
|
2279
|
+
}
|
|
2280
|
+
const name = value.trim();
|
|
2281
|
+
return AUTO_PRESET_ALIASES.has(name.toLowerCase()) ? void 0 : name;
|
|
2282
|
+
}
|
|
2283
|
+
function resolveRuleList(value, kind) {
|
|
2284
|
+
const collection = kind === "layer" ? "layers" : "kinds";
|
|
2285
|
+
const example = kind === "layer" ? `{ name: "api", match: "src/api/**" }` : `{ kind: "route", match: "src/routes/**" }`;
|
|
2286
|
+
if (value === void 0) return [];
|
|
2287
|
+
if (!Array.isArray(value)) {
|
|
2288
|
+
throw new Error(
|
|
2289
|
+
`\`${collection}\` must be an array of ${kind} rules (received ${describeValue2(value)}). A ${kind} rule is ${example}.`
|
|
2290
|
+
);
|
|
2291
|
+
}
|
|
2292
|
+
return value.map((entry, index) => {
|
|
2293
|
+
const where = `${collection}[${index}]`;
|
|
2294
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
2295
|
+
throw new Error(`${where} must be an object (received ${describeValue2(entry)}). A ${kind} rule is ${example}.`);
|
|
2296
|
+
}
|
|
2297
|
+
const record = entry;
|
|
2298
|
+
const keys = Object.keys(record);
|
|
2299
|
+
const canonical = kind === "layer" ? "name" : "kind";
|
|
2300
|
+
const alias = kind === "layer" ? "layer" : "name";
|
|
2301
|
+
const rawName = typeof record[canonical] === "string" ? record[canonical] : typeof record[alias] === "string" ? record[alias] : void 0;
|
|
2302
|
+
if (rawName === void 0 || rawName.trim() === "") {
|
|
1643
2303
|
throw new Error(
|
|
1644
|
-
|
|
2304
|
+
`${where} is missing \`${canonical}\` (received keys: ${keys.join(", ") || "none"}). A ${kind} rule is ${example}; \`${alias}\` is accepted as an alias of \`${canonical}\`.`
|
|
1645
2305
|
);
|
|
1646
2306
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
2307
|
+
const match = record.match;
|
|
2308
|
+
if (match === void 0) {
|
|
2309
|
+
throw new Error(
|
|
2310
|
+
`${where}.match is required (received keys: ${keys.join(", ") || "none"}). Use a glob string ("src/api/**"), a RegExp (/\\\\.tsx$/) or a function.`
|
|
2311
|
+
);
|
|
2312
|
+
}
|
|
2313
|
+
if (!(typeof match === "string" || match instanceof RegExp || typeof match === "function")) {
|
|
2314
|
+
throw new Error(`${where}.match must be a string, a RegExp or a function (received ${describeValue2(match)}).`);
|
|
2315
|
+
}
|
|
2316
|
+
if (typeof match === "string" && match.trim() === "") {
|
|
2317
|
+
throw new Error(`${where}.match must not be an empty string.`);
|
|
2318
|
+
}
|
|
2319
|
+
return {
|
|
2320
|
+
name: rawName.trim(),
|
|
2321
|
+
match
|
|
2322
|
+
};
|
|
2323
|
+
});
|
|
2324
|
+
}
|
|
2325
|
+
function resolveLayerRules(value) {
|
|
2326
|
+
return resolveRuleList(value, "layer").map((rule) => ({ name: rule.name, match: rule.match }));
|
|
2327
|
+
}
|
|
2328
|
+
function resolveKindRules(value) {
|
|
2329
|
+
return resolveRuleList(value, "kind").map((rule) => ({ kind: rule.name, match: rule.match }));
|
|
1650
2330
|
}
|
|
1651
|
-
function
|
|
1652
|
-
|
|
1653
|
-
|
|
2331
|
+
function outputExcludePatterns(cwd, outDir, tmpDir) {
|
|
2332
|
+
const patterns = [];
|
|
2333
|
+
for (const dir of [outDir, tmpDir]) {
|
|
2334
|
+
const absolute = path4.resolve(cwd, dir);
|
|
2335
|
+
const relative5 = path4.relative(cwd, absolute);
|
|
2336
|
+
if (!relative5 || relative5 === ".." || relative5.startsWith(`..${path4.sep}`) || path4.isAbsolute(relative5)) {
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
patterns.push(`${relative5.replace(/\\/g, "/")}/**`);
|
|
2340
|
+
}
|
|
2341
|
+
return patterns;
|
|
1654
2342
|
}
|
|
1655
2343
|
var BYTE_UNITS = {
|
|
1656
2344
|
b: 1,
|
|
@@ -1719,11 +2407,13 @@ function resolveLayersMode(mode) {
|
|
|
1719
2407
|
}
|
|
1720
2408
|
return mode;
|
|
1721
2409
|
}
|
|
1722
|
-
function resolveBudgets(budgets, fallback) {
|
|
2410
|
+
function resolveBudgets(budgets, fallback, overrideDefault) {
|
|
1723
2411
|
const byLayer = {};
|
|
1724
2412
|
if (budgets === void 0) return { default: fallback, byLayer };
|
|
1725
2413
|
if (typeof budgets !== "object" || budgets === null || Array.isArray(budgets)) {
|
|
1726
|
-
throw new Error(
|
|
2414
|
+
throw new Error(
|
|
2415
|
+
`\`budgets\` must be an object such as { default: "120k", byLayer: { api: "60k" } } (received ${describeValue2(budgets)}).`
|
|
2416
|
+
);
|
|
1727
2417
|
}
|
|
1728
2418
|
if (budgets.byLayer !== void 0) {
|
|
1729
2419
|
if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
|
|
@@ -1738,10 +2428,12 @@ function resolveBudgets(budgets, fallback) {
|
|
|
1738
2428
|
);
|
|
1739
2429
|
}
|
|
1740
2430
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
2431
|
+
const resolvedDefault = boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default");
|
|
2432
|
+
if (!overrideDefault) return { default: resolvedDefault, byLayer };
|
|
2433
|
+
for (const [layer2, value] of Object.entries(byLayer)) {
|
|
2434
|
+
byLayer[layer2] = Math.min(value, fallback);
|
|
2435
|
+
}
|
|
2436
|
+
return { default: fallback, byLayer };
|
|
1745
2437
|
}
|
|
1746
2438
|
function resolveHooks(hooks) {
|
|
1747
2439
|
if (hooks === void 0) return {};
|
|
@@ -1754,6 +2446,11 @@ function resolveHooks(hooks) {
|
|
|
1754
2446
|
return { ...hooks };
|
|
1755
2447
|
}
|
|
1756
2448
|
function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
2449
|
+
if (userConfig?.cwd !== void 0 && (typeof userConfig.cwd !== "string" || userConfig.cwd.trim() === "")) {
|
|
2450
|
+
throw new Error(
|
|
2451
|
+
`\`cwd\` must be a non-empty path string (received ${describeValue2(userConfig.cwd)}).`
|
|
2452
|
+
);
|
|
2453
|
+
}
|
|
1757
2454
|
const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
|
|
1758
2455
|
const resolved = path4.resolve(candidate);
|
|
1759
2456
|
if (!fs4.existsSync(resolved)) {
|
|
@@ -1766,55 +2463,106 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
|
1766
2463
|
}
|
|
1767
2464
|
function resolveConfig(userConfig, cliOptions, baseDir) {
|
|
1768
2465
|
const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
|
|
1769
|
-
const forcedPreset = userConfig?.preset;
|
|
2466
|
+
const forcedPreset = resolvePresetName(userConfig?.preset);
|
|
1770
2467
|
if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
|
|
1771
2468
|
throw new Error(unknownPresetMessage(forcedPreset));
|
|
1772
2469
|
}
|
|
1773
2470
|
const preset = detectPreset(cwd, forcedPreset);
|
|
1774
2471
|
const layersMode = resolveLayersMode(userConfig?.layersMode);
|
|
1775
|
-
const
|
|
1776
|
-
const
|
|
1777
|
-
const
|
|
1778
|
-
const
|
|
1779
|
-
const
|
|
1780
|
-
|
|
2472
|
+
const userLayers = resolveLayerRules(userConfig?.layers);
|
|
2473
|
+
const userKinds = resolveKindRules(userConfig?.kinds);
|
|
2474
|
+
const layers = userLayers.length > 0 ? layersMode === "replace" ? userLayers : [...userLayers, ...preset.layers] : preset.layers;
|
|
2475
|
+
const kinds = userKinds.length > 0 ? layersMode === "replace" ? userKinds : [...userKinds, ...preset.kinds] : preset.kinds;
|
|
2476
|
+
const secrets = userConfig?.secrets;
|
|
2477
|
+
if (secrets !== void 0 && (typeof secrets !== "object" || Array.isArray(secrets))) {
|
|
2478
|
+
throw new Error(
|
|
2479
|
+
`\`secrets\` must be an object of { customPatterns, disableDefaultPatterns } (received ${describeValue2(secrets)}).`
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
const customRules = secrets?.customPatterns ?? [];
|
|
2483
|
+
if (!Array.isArray(customRules)) {
|
|
2484
|
+
throw new Error(
|
|
2485
|
+
`\`secrets.customPatterns\` must be an array of secret rules (received ${describeValue2(customRules)}). Each entry is a RegExp, a string, or { rule, pattern }.`
|
|
2486
|
+
);
|
|
2487
|
+
}
|
|
2488
|
+
const disableDefaultPatterns = strictBoolean(
|
|
2489
|
+
secrets?.disableDefaultPatterns,
|
|
2490
|
+
false,
|
|
2491
|
+
"`secrets.disableDefaultPatterns`"
|
|
2492
|
+
);
|
|
2493
|
+
const aliasedRules = userConfig?.secretRules;
|
|
2494
|
+
if (aliasedRules !== void 0 && !Array.isArray(aliasedRules)) {
|
|
2495
|
+
throw new Error(
|
|
2496
|
+
`\`secretRules\` must be an array of { rule, pattern } entries (received ${describeValue2(aliasedRules)}).`
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
const compiledCustomRules = compileSecretRules([
|
|
2500
|
+
...customRules,
|
|
2501
|
+
...aliasedRules ?? []
|
|
2502
|
+
]);
|
|
2503
|
+
const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
|
|
2504
|
+
const cliMonolith = cliOptions?.includeMonolith;
|
|
2505
|
+
const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
|
|
1781
2506
|
const artifacts = resolveArtifacts(
|
|
1782
2507
|
userConfig?.artifacts,
|
|
1783
2508
|
cliOptions?.skipArtifacts,
|
|
1784
|
-
includeMonolith
|
|
2509
|
+
includeMonolith,
|
|
2510
|
+
cliMonolith
|
|
1785
2511
|
);
|
|
2512
|
+
const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
|
|
1786
2513
|
const maxPartBytes = boundedBytes(
|
|
1787
|
-
|
|
2514
|
+
explicitMaxPartBytes,
|
|
1788
2515
|
300 * 1024,
|
|
1789
2516
|
MIN_MAX_PART_BYTES,
|
|
1790
2517
|
"maxPartBytes"
|
|
1791
2518
|
);
|
|
1792
|
-
const budgets = resolveBudgets(
|
|
2519
|
+
const budgets = resolveBudgets(
|
|
2520
|
+
userConfig?.budgets,
|
|
2521
|
+
maxPartBytes,
|
|
2522
|
+
explicitMaxPartBytes !== void 0
|
|
2523
|
+
);
|
|
2524
|
+
const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
|
|
2525
|
+
const outDir = cliOptions?.outDir || configuredOutDir;
|
|
2526
|
+
const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
|
|
2527
|
+
const branch = strictString(userConfig?.branch, "main", "`branch`");
|
|
2528
|
+
const configuredSensitiveFiles = strictBoolean(
|
|
2529
|
+
userConfig?.includeSensitiveFiles,
|
|
2530
|
+
false,
|
|
2531
|
+
"`includeSensitiveFiles`"
|
|
2532
|
+
);
|
|
2533
|
+
const includeSensitiveFiles = strictBoolean(
|
|
2534
|
+
cliOptions?.includeSensitiveFiles,
|
|
2535
|
+
configuredSensitiveFiles,
|
|
2536
|
+
"`includeSensitiveFiles`"
|
|
2537
|
+
);
|
|
2538
|
+
const presetExclude = [
|
|
2539
|
+
.../* @__PURE__ */ new Set([...preset.exclude, ...outputExcludePatterns(cwd, outDir, tmpDir)])
|
|
2540
|
+
];
|
|
1793
2541
|
return markResolvedConfig({
|
|
1794
2542
|
cwd,
|
|
1795
|
-
branch
|
|
1796
|
-
outDir
|
|
1797
|
-
tmpDir
|
|
2543
|
+
branch,
|
|
2544
|
+
outDir,
|
|
2545
|
+
tmpDir,
|
|
1798
2546
|
maxPartBytes,
|
|
1799
2547
|
largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
|
|
1800
|
-
previewHeadLines:
|
|
1801
|
-
previewTailLines:
|
|
2548
|
+
previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
|
|
2549
|
+
previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
|
|
1802
2550
|
includeMonolith: artifacts.monolith,
|
|
1803
2551
|
maxMonolithBytes: optionalBytes(
|
|
1804
2552
|
cliOptions?.maxMonolithBytes !== void 0 ? cliOptions.maxMonolithBytes : userConfig?.maxMonolithBytes,
|
|
1805
2553
|
"maxMonolithBytes"
|
|
1806
2554
|
),
|
|
1807
|
-
includeSensitiveFiles
|
|
2555
|
+
includeSensitiveFiles,
|
|
1808
2556
|
include: resolveInclude(userConfig?.include),
|
|
1809
2557
|
preset: preset.presetName,
|
|
1810
|
-
presetExclude
|
|
2558
|
+
presetExclude,
|
|
1811
2559
|
artifacts,
|
|
1812
2560
|
layersMode,
|
|
1813
2561
|
budgets,
|
|
1814
2562
|
hooks: resolveHooks(userConfig?.hooks),
|
|
1815
2563
|
layers,
|
|
1816
2564
|
kinds,
|
|
1817
|
-
excludePatterns: userConfig?.excludePatterns
|
|
2565
|
+
excludePatterns: resolveExcludePatterns(userConfig?.excludePatterns),
|
|
1818
2566
|
secretRules
|
|
1819
2567
|
});
|
|
1820
2568
|
}
|
|
@@ -1894,8 +2642,81 @@ function truncatePreview(content, headLines, tailLines) {
|
|
|
1894
2642
|
};
|
|
1895
2643
|
}
|
|
1896
2644
|
|
|
1897
|
-
// src/scanner/
|
|
2645
|
+
// src/scanner/binary.ts
|
|
2646
|
+
import * as fs5 from "fs";
|
|
1898
2647
|
import * as path5 from "path";
|
|
2648
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2649
|
+
".png",
|
|
2650
|
+
".jpg",
|
|
2651
|
+
".jpeg",
|
|
2652
|
+
".gif",
|
|
2653
|
+
".webp",
|
|
2654
|
+
".avif",
|
|
2655
|
+
".ico",
|
|
2656
|
+
".bmp",
|
|
2657
|
+
".tiff",
|
|
2658
|
+
".pdf",
|
|
2659
|
+
".zip",
|
|
2660
|
+
".tar",
|
|
2661
|
+
".gz",
|
|
2662
|
+
".tgz",
|
|
2663
|
+
".bz2",
|
|
2664
|
+
".xz",
|
|
2665
|
+
".7z",
|
|
2666
|
+
".rar",
|
|
2667
|
+
".mp3",
|
|
2668
|
+
".mp4",
|
|
2669
|
+
".wav",
|
|
2670
|
+
".ogg",
|
|
2671
|
+
".webm",
|
|
2672
|
+
".mov",
|
|
2673
|
+
".avi",
|
|
2674
|
+
".woff",
|
|
2675
|
+
".woff2",
|
|
2676
|
+
".ttf",
|
|
2677
|
+
".otf",
|
|
2678
|
+
".eot",
|
|
2679
|
+
".wasm",
|
|
2680
|
+
".node",
|
|
2681
|
+
".exe",
|
|
2682
|
+
".dll",
|
|
2683
|
+
".dylib",
|
|
2684
|
+
".so"
|
|
2685
|
+
]);
|
|
2686
|
+
function isBinaryExtension(filePath) {
|
|
2687
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
2688
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
2689
|
+
}
|
|
2690
|
+
function isBinaryBuffer(buffer) {
|
|
2691
|
+
const checkLength = Math.min(buffer.length, 8192);
|
|
2692
|
+
for (let index = 0; index < checkLength; index++) {
|
|
2693
|
+
if (buffer[index] === 0) return true;
|
|
2694
|
+
}
|
|
2695
|
+
return false;
|
|
2696
|
+
}
|
|
2697
|
+
function isBinaryFile(absolutePath) {
|
|
2698
|
+
if (isBinaryExtension(absolutePath)) return true;
|
|
2699
|
+
let fd;
|
|
2700
|
+
try {
|
|
2701
|
+
fd = fs5.openSync(absolutePath, "r");
|
|
2702
|
+
const buffer = Buffer.alloc(8192);
|
|
2703
|
+
const bytesRead = fs5.readSync(fd, buffer, 0, 8192, 0);
|
|
2704
|
+
if (bytesRead === 0) return false;
|
|
2705
|
+
return isBinaryBuffer(buffer.subarray(0, bytesRead));
|
|
2706
|
+
} catch {
|
|
2707
|
+
return false;
|
|
2708
|
+
} finally {
|
|
2709
|
+
if (fd !== void 0) {
|
|
2710
|
+
try {
|
|
2711
|
+
fs5.closeSync(fd);
|
|
2712
|
+
} catch {
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
|
|
2718
|
+
// src/scanner/language.ts
|
|
2719
|
+
import * as path6 from "path";
|
|
1899
2720
|
var LANGUAGE_BY_EXTENSION = {
|
|
1900
2721
|
".ts": "typescript",
|
|
1901
2722
|
".mts": "typescript",
|
|
@@ -1950,10 +2771,10 @@ var LANGUAGE_BY_BASENAME = {
|
|
|
1950
2771
|
};
|
|
1951
2772
|
function detectLanguage(filePath) {
|
|
1952
2773
|
const normalized = filePath.replace(/\\/g, "/");
|
|
1953
|
-
const base =
|
|
2774
|
+
const base = path6.posix.basename(normalized);
|
|
1954
2775
|
if (LANGUAGE_BY_BASENAME[base]) return LANGUAGE_BY_BASENAME[base];
|
|
1955
2776
|
if (base === ".env" || base.startsWith(".env.")) return "dotenv";
|
|
1956
|
-
const ext =
|
|
2777
|
+
const ext = path6.posix.extname(base).toLowerCase();
|
|
1957
2778
|
return LANGUAGE_BY_EXTENSION[ext] ?? "text";
|
|
1958
2779
|
}
|
|
1959
2780
|
|
|
@@ -1962,16 +2783,15 @@ function describeMatcher(matcher) {
|
|
|
1962
2783
|
if (typeof matcher === "string") return matcher;
|
|
1963
2784
|
if (matcher instanceof RegExp) return String(matcher);
|
|
1964
2785
|
if (typeof matcher === "function") {
|
|
1965
|
-
|
|
1966
|
-
return source.length > 70 ? `${source.slice(0, 67)}...` : source;
|
|
2786
|
+
return "function (custom predicate)";
|
|
1967
2787
|
}
|
|
1968
2788
|
return String(matcher);
|
|
1969
2789
|
}
|
|
1970
2790
|
function normalizeTarget(cwd, target) {
|
|
1971
|
-
const absolute =
|
|
1972
|
-
const
|
|
1973
|
-
if (
|
|
1974
|
-
return
|
|
2791
|
+
const absolute = path7.resolve(cwd, target);
|
|
2792
|
+
const relative5 = path7.relative(cwd, absolute);
|
|
2793
|
+
if (relative5.startsWith("..") || path7.isAbsolute(relative5)) return target;
|
|
2794
|
+
return relative5.split(path7.sep).join("/");
|
|
1975
2795
|
}
|
|
1976
2796
|
function findRuleMatch(rules, relativePath) {
|
|
1977
2797
|
return rules.find(
|
|
@@ -1979,19 +2799,19 @@ function findRuleMatch(rules, relativePath) {
|
|
|
1979
2799
|
);
|
|
1980
2800
|
}
|
|
1981
2801
|
function readManifest2(config) {
|
|
1982
|
-
const manifestPath =
|
|
2802
|
+
const manifestPath = path7.resolve(config.cwd, config.outDir, "manifest.json");
|
|
1983
2803
|
try {
|
|
1984
|
-
return JSON.parse(
|
|
2804
|
+
return JSON.parse(fs6.readFileSync(manifestPath, "utf8"));
|
|
1985
2805
|
} catch {
|
|
1986
2806
|
return null;
|
|
1987
2807
|
}
|
|
1988
2808
|
}
|
|
1989
2809
|
function explainPath(config, target) {
|
|
1990
|
-
const
|
|
1991
|
-
const absolute =
|
|
2810
|
+
const relative5 = normalizeTarget(config.cwd, target);
|
|
2811
|
+
const absolute = path7.join(config.cwd, relative5);
|
|
1992
2812
|
const result = {
|
|
1993
|
-
path:
|
|
1994
|
-
exists:
|
|
2813
|
+
path: relative5,
|
|
2814
|
+
exists: fs6.existsSync(absolute),
|
|
1995
2815
|
included: false
|
|
1996
2816
|
};
|
|
1997
2817
|
const collection = collectFiles(config.cwd, {
|
|
@@ -1999,8 +2819,8 @@ function explainPath(config, target) {
|
|
|
1999
2819
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
2000
2820
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
2001
2821
|
});
|
|
2002
|
-
if (!collection.files.includes(
|
|
2003
|
-
const record = collection.skipped.find((entry2) => entry2.path ===
|
|
2822
|
+
if (!collection.files.includes(relative5)) {
|
|
2823
|
+
const record = collection.skipped.find((entry2) => entry2.path === relative5);
|
|
2004
2824
|
if (record) {
|
|
2005
2825
|
result.reason = record.reason;
|
|
2006
2826
|
result.detail = record.detail;
|
|
@@ -2014,24 +2834,30 @@ function explainPath(config, target) {
|
|
|
2014
2834
|
return result;
|
|
2015
2835
|
}
|
|
2016
2836
|
result.included = true;
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2837
|
+
if (isBinaryFile(absolute)) {
|
|
2838
|
+
result.included = false;
|
|
2839
|
+
result.reason = "binary";
|
|
2840
|
+
result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
|
|
2841
|
+
return result;
|
|
2842
|
+
}
|
|
2843
|
+
result.layer = classifyLayer(relative5, config.layers);
|
|
2844
|
+
result.kind = classifyKind(relative5, config.kinds);
|
|
2845
|
+
result.language = detectLanguage(relative5);
|
|
2846
|
+
const layerRule = findRuleMatch(config.layers, relative5);
|
|
2021
2847
|
if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
|
|
2022
|
-
const kindRule = findRuleMatch(config.kinds,
|
|
2848
|
+
const kindRule = findRuleMatch(config.kinds, relative5);
|
|
2023
2849
|
if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
|
|
2024
|
-
if (
|
|
2025
|
-
const buffer =
|
|
2850
|
+
if (fs6.existsSync(absolute) && fs6.statSync(absolute).isFile()) {
|
|
2851
|
+
const buffer = fs6.readFileSync(absolute);
|
|
2026
2852
|
const text2 = buffer.toString("utf8");
|
|
2027
2853
|
result.bytes = buffer.length;
|
|
2028
2854
|
result.lines = countLines(text2);
|
|
2029
|
-
const { findings } = scanAndRedactSecrets(text2, config.secretRules,
|
|
2855
|
+
const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative5);
|
|
2030
2856
|
result.secretCount = findings.length;
|
|
2031
2857
|
result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
|
|
2032
2858
|
}
|
|
2033
2859
|
const manifest = readManifest2(config);
|
|
2034
|
-
const entry = manifest?.files.find((file) => file.relativePath ===
|
|
2860
|
+
const entry = manifest?.files.find((file) => file.relativePath === relative5);
|
|
2035
2861
|
result.part = entry?.part ?? null;
|
|
2036
2862
|
result.inclusionMode = entry?.inclusionMode;
|
|
2037
2863
|
return result;
|
|
@@ -2071,8 +2897,8 @@ function renderExplainText(result) {
|
|
|
2071
2897
|
}
|
|
2072
2898
|
|
|
2073
2899
|
// src/wizard/init.ts
|
|
2074
|
-
import * as
|
|
2075
|
-
import * as
|
|
2900
|
+
import * as fs7 from "fs";
|
|
2901
|
+
import * as path8 from "path";
|
|
2076
2902
|
|
|
2077
2903
|
// src/wizard/config-template.ts
|
|
2078
2904
|
var DEFAULT_ANSWERS = {
|
|
@@ -2161,7 +2987,7 @@ function renderConfig(answers) {
|
|
|
2161
2987
|
secrets: {
|
|
2162
2988
|
// [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
|
|
2163
2989
|
customPatterns: [],
|
|
2164
|
-
// Set true to drop the
|
|
2990
|
+
// Set true to drop the built-in rules above and keep only customPatterns.
|
|
2165
2991
|
disableDefaultPatterns: false,
|
|
2166
2992
|
},
|
|
2167
2993
|
|
|
@@ -2177,6 +3003,10 @@ function renderConfig(answers) {
|
|
|
2177
3003
|
},
|
|
2178
3004
|
// Skip the monolith above this size. null = no limit.
|
|
2179
3005
|
maxMonolithBytes: null,
|
|
3006
|
+
// Top-level switch for the monolith, equivalent to artifacts.monolith.
|
|
3007
|
+
// When both are present the artifact entry wins; this one exists so the
|
|
3008
|
+
// generated file lists every setting the tool accepts.
|
|
3009
|
+
includeMonolith: ${artifacts.monolith},
|
|
2180
3010
|
|
|
2181
3011
|
// \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
3012
|
// afterGenerate: (manifest) => { /* write your own artifact */ }
|
|
@@ -2480,10 +3310,10 @@ var CONFIG_CANDIDATES = [
|
|
|
2480
3310
|
];
|
|
2481
3311
|
function chooseConfigFileName(cwd) {
|
|
2482
3312
|
if (!supportsTypeScriptImports()) return "snapshot.config.mjs";
|
|
2483
|
-
const pkgPath =
|
|
2484
|
-
if (!
|
|
3313
|
+
const pkgPath = path8.join(cwd, "package.json");
|
|
3314
|
+
if (!fs7.existsSync(pkgPath)) return "snapshot.config.mjs";
|
|
2485
3315
|
try {
|
|
2486
|
-
const parsed = JSON.parse(
|
|
3316
|
+
const parsed = JSON.parse(fs7.readFileSync(pkgPath, "utf8"));
|
|
2487
3317
|
return parsed.type === "module" ? "snapshot.config.ts" : "snapshot.config.mjs";
|
|
2488
3318
|
} catch {
|
|
2489
3319
|
return "snapshot.config.mjs";
|
|
@@ -2497,10 +3327,18 @@ var SIZE_CHOICES = [
|
|
|
2497
3327
|
];
|
|
2498
3328
|
function existingConfig(cwd) {
|
|
2499
3329
|
for (const name of CONFIG_CANDIDATES) {
|
|
2500
|
-
if (
|
|
3330
|
+
if (fs7.existsSync(path8.join(cwd, name))) return name;
|
|
2501
3331
|
}
|
|
2502
3332
|
return null;
|
|
2503
3333
|
}
|
|
3334
|
+
function packageJsonHasConfig(cwd) {
|
|
3335
|
+
try {
|
|
3336
|
+
const parsed = JSON.parse(fs7.readFileSync(path8.join(cwd, "package.json"), "utf8"));
|
|
3337
|
+
return typeof parsed.snapshot === "object" && parsed.snapshot !== null;
|
|
3338
|
+
} catch {
|
|
3339
|
+
return false;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
2504
3342
|
function artifactAnswers(options) {
|
|
2505
3343
|
const artifacts = { ...DEFAULT_ANSWERS.artifacts };
|
|
2506
3344
|
for (const name of options.skipArtifacts ?? []) {
|
|
@@ -2516,20 +3354,20 @@ function artifactAnswers(options) {
|
|
|
2516
3354
|
}
|
|
2517
3355
|
function topLevelDirs(cwd) {
|
|
2518
3356
|
try {
|
|
2519
|
-
return
|
|
3357
|
+
return fs7.readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort();
|
|
2520
3358
|
} catch {
|
|
2521
3359
|
return [];
|
|
2522
3360
|
}
|
|
2523
3361
|
}
|
|
2524
3362
|
function addScripts(cwd, notes) {
|
|
2525
|
-
const packagePath =
|
|
2526
|
-
if (!
|
|
3363
|
+
const packagePath = path8.join(cwd, "package.json");
|
|
3364
|
+
if (!fs7.existsSync(packagePath)) {
|
|
2527
3365
|
notes.push("No package.json found, so npm scripts were not added.");
|
|
2528
3366
|
return [];
|
|
2529
3367
|
}
|
|
2530
3368
|
let parsed;
|
|
2531
3369
|
try {
|
|
2532
|
-
parsed = JSON.parse(
|
|
3370
|
+
parsed = JSON.parse(fs7.readFileSync(packagePath, "utf8"));
|
|
2533
3371
|
} catch {
|
|
2534
3372
|
notes.push("Could not parse package.json; npm scripts were not added.");
|
|
2535
3373
|
return [];
|
|
@@ -2546,18 +3384,18 @@ function addScripts(cwd, notes) {
|
|
|
2546
3384
|
}
|
|
2547
3385
|
if (added.length === 0) return [];
|
|
2548
3386
|
parsed.scripts = scripts;
|
|
2549
|
-
|
|
3387
|
+
fs7.writeFileSync(packagePath, `${JSON.stringify(parsed, null, 2)}
|
|
2550
3388
|
`, "utf8");
|
|
2551
3389
|
return added;
|
|
2552
3390
|
}
|
|
2553
3391
|
function addGitignore(cwd) {
|
|
2554
|
-
const ignorePath =
|
|
2555
|
-
const existing =
|
|
3392
|
+
const ignorePath = path8.join(cwd, ".gitignore");
|
|
3393
|
+
const existing = fs7.existsSync(ignorePath) ? fs7.readFileSync(ignorePath, "utf8") : "";
|
|
2556
3394
|
const wanted = ["snapshot/", "snapshot.tmp/"];
|
|
2557
3395
|
const added = wanted.filter((entry) => !existing.split(/\r?\n/).includes(entry));
|
|
2558
3396
|
if (added.length === 0) return [];
|
|
2559
3397
|
const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
2560
|
-
|
|
3398
|
+
fs7.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
|
|
2561
3399
|
`, "utf8");
|
|
2562
3400
|
return added;
|
|
2563
3401
|
}
|
|
@@ -2695,7 +3533,7 @@ async function runInit(options) {
|
|
|
2695
3533
|
}
|
|
2696
3534
|
const configText = options.minimal ? renderMinimalConfig(answers) : renderConfig(answers);
|
|
2697
3535
|
const configName = chooseConfigFileName(options.cwd);
|
|
2698
|
-
const configPath =
|
|
3536
|
+
const configPath = path8.join(options.cwd, configName);
|
|
2699
3537
|
if (options.dryRun) {
|
|
2700
3538
|
return {
|
|
2701
3539
|
configPath,
|
|
@@ -2713,10 +3551,15 @@ async function runInit(options) {
|
|
|
2713
3551
|
`A configuration already exists (${present}). Re-run with --force to replace it.`
|
|
2714
3552
|
);
|
|
2715
3553
|
}
|
|
2716
|
-
if (
|
|
3554
|
+
if (packageJsonHasConfig(options.cwd) && !options.force) {
|
|
3555
|
+
throw new Error(
|
|
3556
|
+
'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.'
|
|
3557
|
+
);
|
|
3558
|
+
}
|
|
3559
|
+
if (fs7.existsSync(configPath) && !options.force) {
|
|
2717
3560
|
throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
|
|
2718
3561
|
}
|
|
2719
|
-
|
|
3562
|
+
fs7.writeFileSync(configPath, configText, "utf8");
|
|
2720
3563
|
const scripts = addScripts(options.cwd, notes);
|
|
2721
3564
|
const gitignoreEntries = addGitignore(options.cwd);
|
|
2722
3565
|
return {
|
|
@@ -2733,6 +3576,7 @@ async function runInit(options) {
|
|
|
2733
3576
|
// src/index.ts
|
|
2734
3577
|
import * as crypto3 from "crypto";
|
|
2735
3578
|
import * as fs9 from "fs";
|
|
3579
|
+
import * as os from "os";
|
|
2736
3580
|
import * as path10 from "path";
|
|
2737
3581
|
|
|
2738
3582
|
// src/ast/profiles/shared.ts
|
|
@@ -3277,14 +4121,42 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3277
4121
|
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
3278
4122
|
return Boolean(modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword));
|
|
3279
4123
|
};
|
|
3280
|
-
const
|
|
4124
|
+
const rendersJsx = (node) => {
|
|
4125
|
+
const pending = [node];
|
|
4126
|
+
while (pending.length > 0) {
|
|
4127
|
+
const current = pending.pop();
|
|
4128
|
+
if (ts.isJsxElement(current) || ts.isJsxSelfClosingElement(current) || ts.isJsxFragment(current)) {
|
|
4129
|
+
return true;
|
|
4130
|
+
}
|
|
4131
|
+
if (ts.isCallExpression(current)) {
|
|
4132
|
+
const callee = current.expression;
|
|
4133
|
+
const calleeName = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
|
4134
|
+
if (calleeName === "createElement") return true;
|
|
4135
|
+
}
|
|
4136
|
+
ts.forEachChild(current, (child) => {
|
|
4137
|
+
pending.push(child);
|
|
4138
|
+
});
|
|
4139
|
+
}
|
|
4140
|
+
return false;
|
|
4141
|
+
};
|
|
4142
|
+
const REACT_BASE_CLASS = /(?:^|\.)(?:PureComponent|Component)$/;
|
|
4143
|
+
const extendsReactBase = (node) => {
|
|
4144
|
+
const heritage = (node.heritageClauses ?? []).find(
|
|
4145
|
+
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword
|
|
4146
|
+
);
|
|
4147
|
+
const expression = heritage?.types[0]?.expression;
|
|
4148
|
+
if (!expression) return false;
|
|
4149
|
+
const text2 = ts.isIdentifier(expression) ? expression.text : expression.getText(sourceFile);
|
|
4150
|
+
return REACT_BASE_CLASS.test(text2);
|
|
4151
|
+
};
|
|
4152
|
+
const recordFunctionLike = (name, exported, node) => {
|
|
3281
4153
|
summary.functions.push(name);
|
|
3282
4154
|
if (exported) {
|
|
3283
4155
|
summary.exports.push(name);
|
|
3284
4156
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
3285
4157
|
}
|
|
3286
4158
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
3287
|
-
if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
|
|
4159
|
+
if (/^[A-Z]/.test(name) && rendersJsx(node)) summary.reactComponents.push(name);
|
|
3288
4160
|
};
|
|
3289
4161
|
const stack = [sourceFile];
|
|
3290
4162
|
while (stack.length > 0) {
|
|
@@ -3313,7 +4185,7 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3313
4185
|
if (ts.isFunctionDeclaration(node)) {
|
|
3314
4186
|
const exported = isExported(node);
|
|
3315
4187
|
if (node.name) {
|
|
3316
|
-
recordFunctionLike(node.name.text, exported);
|
|
4188
|
+
recordFunctionLike(node.name.text, exported, node);
|
|
3317
4189
|
if (isDefault(node)) summary.exports.push("default");
|
|
3318
4190
|
} else if (isDefault(node)) {
|
|
3319
4191
|
summary.exports.push("default");
|
|
@@ -3324,7 +4196,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3324
4196
|
summary.classes.push(node.name.text);
|
|
3325
4197
|
if (isExported(node)) summary.exports.push(node.name.text);
|
|
3326
4198
|
if (isDefault(node)) summary.exports.push("default");
|
|
3327
|
-
if (/^[A-Z]/.test(node.name.text))
|
|
4199
|
+
if (/^[A-Z]/.test(node.name.text) && (extendsReactBase(node) || rendersJsx(node))) {
|
|
4200
|
+
summary.reactComponents.push(node.name.text);
|
|
4201
|
+
}
|
|
3328
4202
|
} else if (isDefault(node)) {
|
|
3329
4203
|
summary.exports.push("default");
|
|
3330
4204
|
}
|
|
@@ -3352,7 +4226,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3352
4226
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
3353
4227
|
}
|
|
3354
4228
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
3355
|
-
if (/^[A-Z]/.test(name)
|
|
4229
|
+
if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
|
|
4230
|
+
summary.reactComponents.push(name);
|
|
4231
|
+
}
|
|
3356
4232
|
}
|
|
3357
4233
|
}
|
|
3358
4234
|
const children = [];
|
|
@@ -3579,7 +4455,11 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
3579
4455
|
`**Total Files:** ${manifest.counts.totalFiles} `,
|
|
3580
4456
|
`**Total Lines:** ${manifest.counts.totalLines.toLocaleString()} `,
|
|
3581
4457
|
`**Total Size:** ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB `,
|
|
3582
|
-
|
|
4458
|
+
// This counts the bytes that were *emitted* after redaction, not the bytes
|
|
4459
|
+
// that were masked: calling it "redacted bytes" made a repository with zero
|
|
4460
|
+
// secrets report a non-zero redacted-byte figure directly above "no secrets
|
|
4461
|
+
// detected". The number of masked spans is reported in section 1 instead.
|
|
4462
|
+
`**Emitted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
|
|
3583
4463
|
"",
|
|
3584
4464
|
"## 1. Security Scan & Redaction",
|
|
3585
4465
|
"",
|
|
@@ -3743,6 +4623,8 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
3743
4623
|
layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
|
|
3744
4624
|
kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
|
|
3745
4625
|
}
|
|
4626
|
+
const pathLines = included.map((file) => displayPath(file.relativePath));
|
|
4627
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
3746
4628
|
const header = [
|
|
3747
4629
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
3748
4630
|
"",
|
|
@@ -3755,9 +4637,9 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
3755
4637
|
"",
|
|
3756
4638
|
"## Files",
|
|
3757
4639
|
"",
|
|
3758
|
-
|
|
3759
|
-
...
|
|
3760
|
-
|
|
4640
|
+
`${listFence}text`,
|
|
4641
|
+
...pathLines,
|
|
4642
|
+
listFence,
|
|
3761
4643
|
"",
|
|
3762
4644
|
"## Contents",
|
|
3763
4645
|
""
|
|
@@ -3785,7 +4667,8 @@ function capValue(value, max) {
|
|
|
3785
4667
|
return `${value.slice(0, half)}\u2026${value.slice(value.length - half)}`;
|
|
3786
4668
|
}
|
|
3787
4669
|
var capMetadata = (value) => capValue(value, METADATA_VALUE_MAX);
|
|
3788
|
-
var
|
|
4670
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
|
|
4671
|
+
var displayPath = (value) => capValue(value.replace(CONTROL_CHARS, " "), DISPLAY_PATH_MAX);
|
|
3789
4672
|
function topEntries(counts, limit) {
|
|
3790
4673
|
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
4674
|
}
|
|
@@ -3881,6 +4764,8 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
3881
4764
|
layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
|
|
3882
4765
|
kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
|
|
3883
4766
|
}
|
|
4767
|
+
const pathLines = group.map((s) => displayPath(s.file.relativePath));
|
|
4768
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
3884
4769
|
const header = [
|
|
3885
4770
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
3886
4771
|
"",
|
|
@@ -3893,9 +4778,9 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
3893
4778
|
"",
|
|
3894
4779
|
"## Files",
|
|
3895
4780
|
"",
|
|
3896
|
-
|
|
3897
|
-
...
|
|
3898
|
-
|
|
4781
|
+
`${listFence}text`,
|
|
4782
|
+
...pathLines,
|
|
4783
|
+
listFence,
|
|
3899
4784
|
"",
|
|
3900
4785
|
"## Contents",
|
|
3901
4786
|
""
|
|
@@ -3954,9 +4839,12 @@ function renderFileSection(file) {
|
|
|
3954
4839
|
].join("\n");
|
|
3955
4840
|
}
|
|
3956
4841
|
function fenceForContent(content) {
|
|
4842
|
+
return fenceForText(content, 4);
|
|
4843
|
+
}
|
|
4844
|
+
function fenceForText(content, minimum) {
|
|
3957
4845
|
const matches = content.match(/`+/g);
|
|
3958
4846
|
const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
|
|
3959
|
-
return "`".repeat(Math.max(
|
|
4847
|
+
return "`".repeat(Math.max(minimum, maxRun + 1));
|
|
3960
4848
|
}
|
|
3961
4849
|
function renderSummary(summary) {
|
|
3962
4850
|
if (!summary) return [];
|
|
@@ -4105,122 +4993,51 @@ function generateReadme(manifest) {
|
|
|
4105
4993
|
|
|
4106
4994
|
// src/generator/tree.ts
|
|
4107
4995
|
var MAX_TREE_LINES = 2e4;
|
|
4996
|
+
var CONTROL_CHARS2 = /[\u0000-\u001f\u007f]/g;
|
|
4108
4997
|
function generateProjectTree(filePaths) {
|
|
4109
4998
|
const root = { children: /* @__PURE__ */ new Map() };
|
|
4110
4999
|
for (const filePath of filePaths) {
|
|
4111
5000
|
const parts = filePath.split("/").filter((part) => part.length > 0);
|
|
4112
5001
|
let current = root;
|
|
4113
5002
|
for (const part of parts) {
|
|
4114
|
-
let child = current.children.get(part);
|
|
4115
|
-
if (!child) {
|
|
4116
|
-
child = { children: /* @__PURE__ */ new Map() };
|
|
4117
|
-
current.children.set(part, child);
|
|
4118
|
-
}
|
|
4119
|
-
current = child;
|
|
4120
|
-
}
|
|
4121
|
-
}
|
|
4122
|
-
const lines = ["# Project Tree", "", "```text", "."];
|
|
4123
|
-
let truncatedAt = -1;
|
|
4124
|
-
function walk(node, prefix) {
|
|
4125
|
-
if (truncatedAt !== -1) return;
|
|
4126
|
-
const keys = Array.from(node.children.keys()).sort((a, b) => {
|
|
4127
|
-
const aIsDir = node.children.get(a).children.size > 0;
|
|
4128
|
-
const bIsDir = node.children.get(b).children.size > 0;
|
|
4129
|
-
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
|
|
4130
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
4131
|
-
});
|
|
4132
|
-
keys.forEach((key, index) => {
|
|
4133
|
-
if (truncatedAt !== -1) return;
|
|
4134
|
-
const isLast = index === keys.length - 1;
|
|
4135
|
-
const child = node.children.get(key);
|
|
4136
|
-
const isDirectory = child.children.size > 0;
|
|
4137
|
-
lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${key}${isDirectory ? "/" : ""}`);
|
|
4138
|
-
if (lines.length >= MAX_TREE_LINES) {
|
|
4139
|
-
truncatedAt = lines.length;
|
|
4140
|
-
return;
|
|
4141
|
-
}
|
|
4142
|
-
walk(child, prefix + (isLast ? " " : "\u2502 "));
|
|
4143
|
-
});
|
|
4144
|
-
}
|
|
4145
|
-
walk(root, "");
|
|
4146
|
-
if (truncatedAt !== -1) {
|
|
4147
|
-
lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
|
|
4148
|
-
}
|
|
4149
|
-
lines.push("```", "");
|
|
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 {
|
|
5003
|
+
let child = current.children.get(part);
|
|
5004
|
+
if (!child) {
|
|
5005
|
+
child = { children: /* @__PURE__ */ new Map() };
|
|
5006
|
+
current.children.set(part, child);
|
|
4221
5007
|
}
|
|
5008
|
+
current = child;
|
|
4222
5009
|
}
|
|
4223
5010
|
}
|
|
5011
|
+
const lines = ["."];
|
|
5012
|
+
let truncatedAt = -1;
|
|
5013
|
+
function walk(node, prefix) {
|
|
5014
|
+
if (truncatedAt !== -1) return;
|
|
5015
|
+
const keys = Array.from(node.children.keys()).sort((a, b) => {
|
|
5016
|
+
const aIsDir = node.children.get(a).children.size > 0;
|
|
5017
|
+
const bIsDir = node.children.get(b).children.size > 0;
|
|
5018
|
+
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
|
|
5019
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
5020
|
+
});
|
|
5021
|
+
keys.forEach((key, index) => {
|
|
5022
|
+
if (truncatedAt !== -1) return;
|
|
5023
|
+
const isLast = index === keys.length - 1;
|
|
5024
|
+
const child = node.children.get(key);
|
|
5025
|
+
const isDirectory = child.children.size > 0;
|
|
5026
|
+
const label = key.replace(CONTROL_CHARS2, " ");
|
|
5027
|
+
lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
|
|
5028
|
+
if (lines.length >= MAX_TREE_LINES) {
|
|
5029
|
+
truncatedAt = lines.length;
|
|
5030
|
+
return;
|
|
5031
|
+
}
|
|
5032
|
+
walk(child, prefix + (isLast ? " " : "\u2502 "));
|
|
5033
|
+
});
|
|
5034
|
+
}
|
|
5035
|
+
walk(root, "");
|
|
5036
|
+
if (truncatedAt !== -1) {
|
|
5037
|
+
lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
|
|
5038
|
+
}
|
|
5039
|
+
const fence = fenceForText(lines.join("\n"), 3);
|
|
5040
|
+
return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
|
|
4224
5041
|
}
|
|
4225
5042
|
|
|
4226
5043
|
// src/validator/validate.ts
|
|
@@ -4240,6 +5057,15 @@ function emptyDrift() {
|
|
|
4240
5057
|
function sha256Hex(buffer) {
|
|
4241
5058
|
return crypto2.createHash("sha256").update(buffer).digest("hex");
|
|
4242
5059
|
}
|
|
5060
|
+
function wouldNowBeIncluded(cwd, relativePath) {
|
|
5061
|
+
const absolutePath = path9.join(cwd, relativePath);
|
|
5062
|
+
try {
|
|
5063
|
+
if (!fs8.statSync(absolutePath).isFile()) return false;
|
|
5064
|
+
return !isBinaryFile(absolutePath);
|
|
5065
|
+
} catch {
|
|
5066
|
+
return false;
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
4243
5069
|
function validateSnapshot(configInput) {
|
|
4244
5070
|
assertUsableConfigInput(configInput);
|
|
4245
5071
|
const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
|
|
@@ -4294,9 +5120,9 @@ function validateSnapshot(configInput) {
|
|
|
4294
5120
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
4295
5121
|
});
|
|
4296
5122
|
for (const diskFile of collection.files) {
|
|
4297
|
-
if (skippedSet.has(diskFile)) continue;
|
|
4298
5123
|
const snapshotHash = manifestFileMap.get(diskFile);
|
|
4299
5124
|
if (!snapshotHash) {
|
|
5125
|
+
if (skippedSet.has(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
|
|
4300
5126
|
drift.missingInSnapshot.push(diskFile);
|
|
4301
5127
|
continue;
|
|
4302
5128
|
}
|
|
@@ -4576,7 +5402,7 @@ function validateSnapshot(configInput) {
|
|
|
4576
5402
|
|
|
4577
5403
|
// src/index.ts
|
|
4578
5404
|
var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
|
|
4579
|
-
var GENERATOR_VERSION = "
|
|
5405
|
+
var GENERATOR_VERSION = "4.0.0";
|
|
4580
5406
|
var SCHEMA_VERSION = "3.0.0";
|
|
4581
5407
|
function sha256Hex2(input) {
|
|
4582
5408
|
return crypto3.createHash("sha256").update(input).digest("hex");
|
|
@@ -4746,27 +5572,213 @@ function resolveSafeOutputDirs(config) {
|
|
|
4746
5572
|
return { outDir, tmpDir };
|
|
4747
5573
|
}
|
|
4748
5574
|
var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
|
|
5575
|
+
var STAGING_ENTRIES = /* @__PURE__ */ new Set([
|
|
5576
|
+
".codebase-snapshot",
|
|
5577
|
+
"manifest.json",
|
|
5578
|
+
"README.md",
|
|
5579
|
+
"parts",
|
|
5580
|
+
"reports",
|
|
5581
|
+
"full-codebase.json",
|
|
5582
|
+
"full-codebase.md"
|
|
5583
|
+
]);
|
|
4749
5584
|
var OWNERSHIP_MARKER = ".codebase-snapshot";
|
|
4750
|
-
|
|
5585
|
+
var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
|
|
5586
|
+
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;
|
|
5587
|
+
function listEntries(dir) {
|
|
5588
|
+
const entries = [];
|
|
5589
|
+
const walk = (absolute, prefix) => {
|
|
5590
|
+
let children;
|
|
5591
|
+
try {
|
|
5592
|
+
children = fs9.readdirSync(absolute, { withFileTypes: true });
|
|
5593
|
+
} catch {
|
|
5594
|
+
return;
|
|
5595
|
+
}
|
|
5596
|
+
for (const child of children) {
|
|
5597
|
+
const relative5 = prefix === "" ? child.name : `${prefix}/${child.name}`;
|
|
5598
|
+
entries.push(relative5);
|
|
5599
|
+
if (child.isDirectory()) walk(path10.join(absolute, child.name), relative5);
|
|
5600
|
+
}
|
|
5601
|
+
};
|
|
5602
|
+
walk(dir, "");
|
|
5603
|
+
return entries.sort();
|
|
5604
|
+
}
|
|
5605
|
+
function markerPayload(dir, state, id, createdAt) {
|
|
4751
5606
|
const marker2 = {
|
|
4752
5607
|
generator: GENERATOR_NAME,
|
|
4753
5608
|
version: GENERATOR_VERSION,
|
|
4754
|
-
schemaVersion: SCHEMA_VERSION
|
|
5609
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5610
|
+
createdAt,
|
|
5611
|
+
state,
|
|
5612
|
+
id,
|
|
5613
|
+
entries: listEntries(dir)
|
|
4755
5614
|
};
|
|
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
5615
|
const manifestPath = path10.join(dir, "manifest.json");
|
|
4762
|
-
if (
|
|
5616
|
+
if (fs9.existsSync(manifestPath)) {
|
|
5617
|
+
marker2.manifestSha256 = sha256Hex2(fs9.readFileSync(manifestPath));
|
|
5618
|
+
}
|
|
5619
|
+
return marker2;
|
|
5620
|
+
}
|
|
5621
|
+
function writeOwnershipMarker(dir, marker2) {
|
|
5622
|
+
fs9.writeFileSync(
|
|
5623
|
+
path10.join(dir, OWNERSHIP_MARKER),
|
|
5624
|
+
`${JSON.stringify(marker2, null, 2)}
|
|
5625
|
+
`,
|
|
5626
|
+
"utf8"
|
|
5627
|
+
);
|
|
5628
|
+
}
|
|
5629
|
+
function readOwnershipMarker(dir) {
|
|
5630
|
+
const markerPath = path10.join(dir, OWNERSHIP_MARKER);
|
|
5631
|
+
if (!fs9.existsSync(markerPath)) {
|
|
5632
|
+
return { marker: null, problem: "it holds no .codebase-snapshot marker" };
|
|
5633
|
+
}
|
|
5634
|
+
let raw;
|
|
4763
5635
|
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;
|
|
5636
|
+
raw = fs9.readFileSync(markerPath, "utf8");
|
|
4767
5637
|
} catch {
|
|
4768
|
-
return
|
|
5638
|
+
return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
|
|
5639
|
+
}
|
|
5640
|
+
if (raw.trim().length === 0) {
|
|
5641
|
+
return { marker: null, problem: "its .codebase-snapshot marker is empty" };
|
|
5642
|
+
}
|
|
5643
|
+
let parsed;
|
|
5644
|
+
try {
|
|
5645
|
+
parsed = JSON.parse(raw);
|
|
5646
|
+
} catch {
|
|
5647
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not valid JSON" };
|
|
5648
|
+
}
|
|
5649
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5650
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not a JSON object" };
|
|
5651
|
+
}
|
|
5652
|
+
const candidate = parsed;
|
|
5653
|
+
if (candidate.generator !== GENERATOR_NAME) {
|
|
5654
|
+
return { marker: null, problem: "its .codebase-snapshot marker names a different generator" };
|
|
5655
|
+
}
|
|
5656
|
+
if (typeof candidate.schemaVersion !== "string" || !SUPPORTED_MARKER_SCHEMAS.has(candidate.schemaVersion)) {
|
|
5657
|
+
return {
|
|
5658
|
+
marker: null,
|
|
5659
|
+
problem: `its .codebase-snapshot marker has a schema version this build does not understand (${String(candidate.schemaVersion)})`
|
|
5660
|
+
};
|
|
5661
|
+
}
|
|
5662
|
+
if (typeof candidate.id !== "string" || !SNAPSHOT_ID_PATTERN.test(candidate.id)) {
|
|
5663
|
+
return { marker: null, problem: "its .codebase-snapshot marker carries no snapshot id" };
|
|
5664
|
+
}
|
|
5665
|
+
if (candidate.state !== "staging" && candidate.state !== "complete") {
|
|
5666
|
+
return { marker: null, problem: "its .codebase-snapshot marker records no run state" };
|
|
5667
|
+
}
|
|
5668
|
+
return {
|
|
5669
|
+
marker: {
|
|
5670
|
+
generator: GENERATOR_NAME,
|
|
5671
|
+
version: typeof candidate.version === "string" ? candidate.version : "unknown",
|
|
5672
|
+
schemaVersion: candidate.schemaVersion,
|
|
5673
|
+
createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
|
|
5674
|
+
state: candidate.state,
|
|
5675
|
+
id: candidate.id,
|
|
5676
|
+
manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
|
|
5677
|
+
entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
|
|
5678
|
+
},
|
|
5679
|
+
problem: null
|
|
5680
|
+
};
|
|
5681
|
+
}
|
|
5682
|
+
function describeManifestProblem(value) {
|
|
5683
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "is not a JSON object";
|
|
5684
|
+
const candidate = value;
|
|
5685
|
+
const generator = candidate.generator;
|
|
5686
|
+
if (typeof generator !== "object" || generator === null || generator.name !== GENERATOR_NAME) {
|
|
5687
|
+
return "was not written by this generator";
|
|
5688
|
+
}
|
|
5689
|
+
if (!Array.isArray(candidate.files)) return "has no files array";
|
|
5690
|
+
return null;
|
|
5691
|
+
}
|
|
5692
|
+
function assessSnapshotDirectory(dir) {
|
|
5693
|
+
const { marker: marker2, problem } = readOwnershipMarker(dir);
|
|
5694
|
+
if (!marker2) return { owned: false, problem, foreignEntries: [], marker: null };
|
|
5695
|
+
if (marker2.state !== "complete") {
|
|
5696
|
+
return {
|
|
5697
|
+
owned: false,
|
|
5698
|
+
problem: "its .codebase-snapshot marker records a run that never finished writing it",
|
|
5699
|
+
foreignEntries: [],
|
|
5700
|
+
marker: marker2
|
|
5701
|
+
};
|
|
5702
|
+
}
|
|
5703
|
+
const manifestPath = path10.join(dir, "manifest.json");
|
|
5704
|
+
if (fs9.existsSync(manifestPath)) {
|
|
5705
|
+
let raw;
|
|
5706
|
+
try {
|
|
5707
|
+
raw = fs9.readFileSync(manifestPath, "utf8");
|
|
5708
|
+
} catch {
|
|
5709
|
+
return {
|
|
5710
|
+
owned: false,
|
|
5711
|
+
problem: "its manifest.json could not be read",
|
|
5712
|
+
foreignEntries: [],
|
|
5713
|
+
marker: marker2
|
|
5714
|
+
};
|
|
5715
|
+
}
|
|
5716
|
+
let parsed;
|
|
5717
|
+
try {
|
|
5718
|
+
parsed = JSON.parse(raw);
|
|
5719
|
+
} catch {
|
|
5720
|
+
return {
|
|
5721
|
+
owned: false,
|
|
5722
|
+
problem: "its manifest.json is not valid JSON",
|
|
5723
|
+
foreignEntries: [],
|
|
5724
|
+
marker: marker2
|
|
5725
|
+
};
|
|
5726
|
+
}
|
|
5727
|
+
const manifestProblem2 = describeManifestProblem(parsed);
|
|
5728
|
+
if (manifestProblem2) {
|
|
5729
|
+
return {
|
|
5730
|
+
owned: false,
|
|
5731
|
+
problem: `its manifest.json ${manifestProblem2}`,
|
|
5732
|
+
foreignEntries: [],
|
|
5733
|
+
marker: marker2
|
|
5734
|
+
};
|
|
5735
|
+
}
|
|
5736
|
+
if (!marker2.manifestSha256) {
|
|
5737
|
+
return {
|
|
5738
|
+
owned: false,
|
|
5739
|
+
problem: "its .codebase-snapshot marker records no manifest digest",
|
|
5740
|
+
foreignEntries: [],
|
|
5741
|
+
marker: marker2
|
|
5742
|
+
};
|
|
5743
|
+
}
|
|
5744
|
+
if (sha256Hex2(raw) !== marker2.manifestSha256) {
|
|
5745
|
+
return {
|
|
5746
|
+
owned: false,
|
|
5747
|
+
problem: "its manifest.json does not match the digest recorded in its .codebase-snapshot marker",
|
|
5748
|
+
foreignEntries: [],
|
|
5749
|
+
marker: marker2
|
|
5750
|
+
};
|
|
5751
|
+
}
|
|
5752
|
+
} else if (marker2.manifestSha256) {
|
|
5753
|
+
return {
|
|
5754
|
+
owned: false,
|
|
5755
|
+
problem: "its manifest.json is missing although its marker records one",
|
|
5756
|
+
foreignEntries: [],
|
|
5757
|
+
marker: marker2
|
|
5758
|
+
};
|
|
5759
|
+
}
|
|
5760
|
+
const recorded = new Set(marker2.entries ?? []);
|
|
5761
|
+
if (recorded.size === 0) {
|
|
5762
|
+
return {
|
|
5763
|
+
owned: false,
|
|
5764
|
+
problem: "its .codebase-snapshot marker records no contents",
|
|
5765
|
+
foreignEntries: [],
|
|
5766
|
+
marker: marker2
|
|
5767
|
+
};
|
|
4769
5768
|
}
|
|
5769
|
+
const foreignEntries = listEntries(dir).filter(
|
|
5770
|
+
(entry) => !recorded.has(entry) && !PLACEHOLDER_ENTRIES.has(entry)
|
|
5771
|
+
);
|
|
5772
|
+
return {
|
|
5773
|
+
owned: foreignEntries.length === 0,
|
|
5774
|
+
problem: null,
|
|
5775
|
+
foreignEntries,
|
|
5776
|
+
marker: marker2
|
|
5777
|
+
};
|
|
5778
|
+
}
|
|
5779
|
+
function describeEntries(entries, limit = 5) {
|
|
5780
|
+
const shown = entries.slice(0, limit).join(", ");
|
|
5781
|
+
return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
|
|
4770
5782
|
}
|
|
4771
5783
|
function assertReplaceable(target, label) {
|
|
4772
5784
|
if (!fs9.existsSync(target)) return;
|
|
@@ -4776,16 +5788,124 @@ function assertReplaceable(target, label) {
|
|
|
4776
5788
|
);
|
|
4777
5789
|
}
|
|
4778
5790
|
const entries = fs9.readdirSync(target);
|
|
4779
|
-
|
|
4780
|
-
if (
|
|
4781
|
-
|
|
4782
|
-
|
|
4783
|
-
|
|
5791
|
+
if (entries.length === 0) return;
|
|
5792
|
+
if (label === "outDir" && entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
|
|
5793
|
+
const { marker: marker2, problem } = readOwnershipMarker(target);
|
|
5794
|
+
if (label === "tmpDir") {
|
|
5795
|
+
if (!marker2) {
|
|
5796
|
+
throw new Error(
|
|
5797
|
+
`Refusing to replace tmpDir ${target}: ${problem ?? "it does not carry this generator's marker"}. This generator only clears a staging directory it wrote. Point tmpDir at a new or empty directory, or remove ${target} yourself.`
|
|
5798
|
+
);
|
|
5799
|
+
}
|
|
5800
|
+
const unexpected = entries.filter((name) => !STAGING_ENTRIES.has(name));
|
|
5801
|
+
if (unexpected.length > 0) {
|
|
5802
|
+
throw new Error(
|
|
5803
|
+
`Refusing to replace tmpDir ${target}: it holds ${describeEntries(unexpected)}, which this generator did not write. Point tmpDir at a new or empty directory, or remove those files yourself.`
|
|
5804
|
+
);
|
|
5805
|
+
}
|
|
5806
|
+
return;
|
|
5807
|
+
}
|
|
5808
|
+
const verdict = assessSnapshotDirectory(target);
|
|
5809
|
+
if (verdict.owned) return;
|
|
5810
|
+
if (verdict.foreignEntries.length > 0) {
|
|
5811
|
+
throw new Error(
|
|
5812
|
+
`Refusing to replace ${label} ${target}: it carries this generator's ownership marker, but these entries are not part of the snapshot it recorded: ${describeEntries(verdict.foreignEntries)}. Remove them yourself, or point ${label} at a new or empty directory.`
|
|
5813
|
+
);
|
|
5814
|
+
}
|
|
5815
|
+
const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
|
|
4784
5816
|
throw new Error(
|
|
4785
|
-
`Refusing to replace ${label} ${target}:
|
|
5817
|
+
`Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(entries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
|
|
5818
|
+
);
|
|
5819
|
+
}
|
|
5820
|
+
function ensureParentDirectory(target, label) {
|
|
5821
|
+
const parent = path10.dirname(target);
|
|
5822
|
+
if (fs9.existsSync(parent) && !fs9.statSync(parent).isDirectory()) {
|
|
5823
|
+
throw new Error(
|
|
5824
|
+
`Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
|
|
5825
|
+
);
|
|
5826
|
+
}
|
|
5827
|
+
try {
|
|
5828
|
+
fs9.mkdirSync(parent, { recursive: true });
|
|
5829
|
+
} catch (err) {
|
|
5830
|
+
throw new Error(
|
|
5831
|
+
`Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
|
|
5832
|
+
);
|
|
5833
|
+
}
|
|
5834
|
+
}
|
|
5835
|
+
function acquireRunLock(outDir) {
|
|
5836
|
+
const lockPath = path10.join(
|
|
5837
|
+
os.tmpdir(),
|
|
5838
|
+
`codebase-snapshot-${sha256Hex2(outDir).slice(0, 16)}.lock`
|
|
4786
5839
|
);
|
|
5840
|
+
const payload = `${JSON.stringify({
|
|
5841
|
+
pid: process.pid,
|
|
5842
|
+
outDir,
|
|
5843
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5844
|
+
})}
|
|
5845
|
+
`;
|
|
5846
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
5847
|
+
try {
|
|
5848
|
+
const handle = fs9.openSync(lockPath, "wx");
|
|
5849
|
+
try {
|
|
5850
|
+
fs9.writeSync(handle, payload);
|
|
5851
|
+
} finally {
|
|
5852
|
+
fs9.closeSync(handle);
|
|
5853
|
+
}
|
|
5854
|
+
return () => {
|
|
5855
|
+
try {
|
|
5856
|
+
fs9.rmSync(lockPath, { force: true });
|
|
5857
|
+
} catch {
|
|
5858
|
+
}
|
|
5859
|
+
};
|
|
5860
|
+
} catch (err) {
|
|
5861
|
+
if (err.code !== "EEXIST") throw err;
|
|
5862
|
+
if (attempt === 0 && lockOwnerIsGone(lockPath)) {
|
|
5863
|
+
fs9.rmSync(lockPath, { force: true });
|
|
5864
|
+
continue;
|
|
5865
|
+
}
|
|
5866
|
+
throw new Error(
|
|
5867
|
+
`Another generate run is already writing ${outDir}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
|
|
5868
|
+
);
|
|
5869
|
+
}
|
|
5870
|
+
}
|
|
5871
|
+
throw new Error(`Could not acquire the generate lock for ${outDir}.`);
|
|
5872
|
+
}
|
|
5873
|
+
function lockOwnerIsGone(lockPath) {
|
|
5874
|
+
let raw;
|
|
5875
|
+
try {
|
|
5876
|
+
raw = fs9.readFileSync(lockPath, "utf8");
|
|
5877
|
+
} catch {
|
|
5878
|
+
return false;
|
|
5879
|
+
}
|
|
5880
|
+
let pid;
|
|
5881
|
+
try {
|
|
5882
|
+
pid = JSON.parse(raw).pid;
|
|
5883
|
+
} catch {
|
|
5884
|
+
return false;
|
|
5885
|
+
}
|
|
5886
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
5887
|
+
try {
|
|
5888
|
+
process.kill(pid, 0);
|
|
5889
|
+
return false;
|
|
5890
|
+
} catch (err) {
|
|
5891
|
+
return err.code === "ESRCH";
|
|
5892
|
+
}
|
|
5893
|
+
}
|
|
5894
|
+
function writeManifestAtomically(manifestPath, manifest) {
|
|
5895
|
+
const tempPath = `${manifestPath}.${process.pid}.tmp`;
|
|
5896
|
+
fs9.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
5897
|
+
try {
|
|
5898
|
+
fs9.renameSync(tempPath, manifestPath);
|
|
5899
|
+
} catch (err) {
|
|
5900
|
+
try {
|
|
5901
|
+
fs9.rmSync(tempPath, { force: true });
|
|
5902
|
+
} catch {
|
|
5903
|
+
}
|
|
5904
|
+
throw err;
|
|
5905
|
+
}
|
|
4787
5906
|
}
|
|
4788
5907
|
function swapIntoPlace(tmpDir, outDir) {
|
|
5908
|
+
ensureParentDirectory(outDir, "outDir");
|
|
4789
5909
|
const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
|
|
4790
5910
|
const hadPrevious = fs9.existsSync(outDir);
|
|
4791
5911
|
if (hadPrevious) fs9.renameSync(outDir, backupDir);
|
|
@@ -4874,6 +5994,7 @@ function generateSnapshot(configInput, options) {
|
|
|
4874
5994
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
4875
5995
|
});
|
|
4876
5996
|
const files = [];
|
|
5997
|
+
const monolithBodies = /* @__PURE__ */ new Map();
|
|
4877
5998
|
const skipped = [...collection.skipped];
|
|
4878
5999
|
const secretFindings = [];
|
|
4879
6000
|
const warnings = [];
|
|
@@ -4937,6 +6058,9 @@ function generateSnapshot(configInput, options) {
|
|
|
4937
6058
|
isPreviewTruncated = preview.isTruncated;
|
|
4938
6059
|
includedLines = preview.includedLines;
|
|
4939
6060
|
}
|
|
6061
|
+
if (config.includeMonolith && isPreviewTruncated) {
|
|
6062
|
+
monolithBodies.set(relativePath, redacted);
|
|
6063
|
+
}
|
|
4940
6064
|
let summary;
|
|
4941
6065
|
let summaryQuality;
|
|
4942
6066
|
try {
|
|
@@ -4979,7 +6103,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4979
6103
|
).join("\n")
|
|
4980
6104
|
);
|
|
4981
6105
|
const fullContents = /* @__PURE__ */ new Map();
|
|
4982
|
-
for (const file of files)
|
|
6106
|
+
for (const file of files) {
|
|
6107
|
+
fullContents.set(
|
|
6108
|
+
file.relativePath,
|
|
6109
|
+
monolithBodies.get(file.relativePath) ?? file.content ?? ""
|
|
6110
|
+
);
|
|
6111
|
+
}
|
|
4983
6112
|
const parts = partitionByBudget(files, config, fingerprint);
|
|
4984
6113
|
const maxEffectiveBudget = Math.max(
|
|
4985
6114
|
config.maxPartBytes,
|
|
@@ -5065,10 +6194,17 @@ function generateSnapshot(configInput, options) {
|
|
|
5065
6194
|
let monolithJson = null;
|
|
5066
6195
|
let monolithMarkdown = null;
|
|
5067
6196
|
if (config.includeMonolith) {
|
|
5068
|
-
const fullFiles = files.map((file) =>
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
6197
|
+
const fullFiles = files.map((file) => {
|
|
6198
|
+
const content = fullContents.get(file.relativePath) ?? "";
|
|
6199
|
+
return {
|
|
6200
|
+
...file,
|
|
6201
|
+
content,
|
|
6202
|
+
inclusionMode: "full",
|
|
6203
|
+
isPreviewTruncated: false,
|
|
6204
|
+
includedBytes: Buffer.byteLength(content, "utf8"),
|
|
6205
|
+
includedLines: countLines(content)
|
|
6206
|
+
};
|
|
6207
|
+
});
|
|
5072
6208
|
const json = generateMonolithJson(manifest, fullFiles);
|
|
5073
6209
|
const markdown = generateMonolithMarkdown(manifest, fullFiles);
|
|
5074
6210
|
const jsonBytes = Buffer.byteLength(json, "utf8");
|
|
@@ -5096,57 +6232,199 @@ function generateSnapshot(configInput, options) {
|
|
|
5096
6232
|
}
|
|
5097
6233
|
for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
|
|
5098
6234
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
6235
|
+
const { outDir, tmpDir } = resolveSafeOutputDirs(config);
|
|
6236
|
+
const { artifacts } = config;
|
|
6237
|
+
assertReplaceable(tmpDir, "tmpDir");
|
|
6238
|
+
assertReplaceable(outDir, "outDir");
|
|
5099
6239
|
if (dryRun) {
|
|
5100
6240
|
runAfterGenerate(config, manifest);
|
|
5101
6241
|
return manifest;
|
|
5102
6242
|
}
|
|
5103
|
-
const
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
6243
|
+
const releaseLock = acquireRunLock(outDir);
|
|
6244
|
+
let stagingStarted = false;
|
|
6245
|
+
try {
|
|
6246
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6247
|
+
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
6248
|
+
fs9.mkdirSync(tmpDir, { recursive: true });
|
|
6249
|
+
stagingStarted = true;
|
|
6250
|
+
const snapshotId = crypto3.randomUUID();
|
|
6251
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6252
|
+
writeOwnershipMarker(tmpDir, {
|
|
6253
|
+
generator: GENERATOR_NAME,
|
|
6254
|
+
version: GENERATOR_VERSION,
|
|
6255
|
+
schemaVersion: SCHEMA_VERSION,
|
|
6256
|
+
createdAt,
|
|
6257
|
+
state: "staging",
|
|
6258
|
+
id: snapshotId
|
|
6259
|
+
});
|
|
6260
|
+
const partsDir = path10.join(tmpDir, "parts");
|
|
6261
|
+
const reportsDir = path10.join(tmpDir, "reports");
|
|
6262
|
+
if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
|
|
6263
|
+
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
6264
|
+
fs9.mkdirSync(reportsDir, { recursive: true });
|
|
6265
|
+
}
|
|
6266
|
+
if (artifacts.parts) {
|
|
6267
|
+
for (const part of parts) {
|
|
6268
|
+
fs9.writeFileSync(path10.join(partsDir, part.filename), part.markdown, "utf8");
|
|
6269
|
+
}
|
|
5119
6270
|
}
|
|
6271
|
+
if (artifacts.projectTree) {
|
|
6272
|
+
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
6273
|
+
fs9.writeFileSync(path10.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
6274
|
+
}
|
|
6275
|
+
if (artifacts.navigation) {
|
|
6276
|
+
fs9.writeFileSync(path10.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
6277
|
+
}
|
|
6278
|
+
if (artifacts.audit) {
|
|
6279
|
+
fs9.writeFileSync(
|
|
6280
|
+
path10.join(reportsDir, "audit.md"),
|
|
6281
|
+
generateAuditReport(manifest, secretFindings),
|
|
6282
|
+
"utf8"
|
|
6283
|
+
);
|
|
6284
|
+
}
|
|
6285
|
+
if (artifacts.readme) {
|
|
6286
|
+
fs9.writeFileSync(path10.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
|
|
6287
|
+
}
|
|
6288
|
+
if (artifacts.manifest) {
|
|
6289
|
+
fs9.writeFileSync(path10.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
6290
|
+
}
|
|
6291
|
+
if (monolithJson !== null) {
|
|
6292
|
+
fs9.writeFileSync(path10.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
|
|
6293
|
+
}
|
|
6294
|
+
if (monolithMarkdown !== null) {
|
|
6295
|
+
fs9.writeFileSync(path10.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
6296
|
+
}
|
|
6297
|
+
writeOwnershipMarker(tmpDir, markerPayload(tmpDir, "complete", snapshotId, createdAt));
|
|
6298
|
+
swapIntoPlace(tmpDir, outDir);
|
|
6299
|
+
stagingStarted = false;
|
|
6300
|
+
const warningsBeforeHook = manifest.warnings.length;
|
|
6301
|
+
runAfterGenerate(config, manifest);
|
|
6302
|
+
if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
|
|
6303
|
+
writeManifestAtomically(path10.join(outDir, "manifest.json"), manifest);
|
|
6304
|
+
}
|
|
6305
|
+
writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
|
|
6306
|
+
return manifest;
|
|
6307
|
+
} catch (err) {
|
|
6308
|
+
if (stagingStarted) {
|
|
6309
|
+
try {
|
|
6310
|
+
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
6311
|
+
} catch {
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
throw err;
|
|
6315
|
+
} finally {
|
|
6316
|
+
releaseLock();
|
|
5120
6317
|
}
|
|
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
|
-
|
|
6318
|
+
}
|
|
6319
|
+
|
|
6320
|
+
// src/agent/option-scope.ts
|
|
6321
|
+
var COMMAND_OPTION_SCOPE = {
|
|
6322
|
+
generate: [
|
|
6323
|
+
"cwd",
|
|
6324
|
+
"out-dir",
|
|
6325
|
+
"max-part-bytes",
|
|
6326
|
+
"large-file-bytes",
|
|
6327
|
+
"max-monolith-bytes",
|
|
6328
|
+
"include-monolith",
|
|
6329
|
+
"include-sensitive-files",
|
|
6330
|
+
"skip-artifacts",
|
|
6331
|
+
"only-artifacts",
|
|
6332
|
+
"no-config",
|
|
6333
|
+
"format",
|
|
6334
|
+
"mode",
|
|
6335
|
+
"dry-run",
|
|
6336
|
+
"watch",
|
|
6337
|
+
"help",
|
|
6338
|
+
"version"
|
|
6339
|
+
],
|
|
6340
|
+
validate: [
|
|
6341
|
+
"cwd",
|
|
6342
|
+
"out-dir",
|
|
6343
|
+
"max-part-bytes",
|
|
6344
|
+
"large-file-bytes",
|
|
6345
|
+
"max-monolith-bytes",
|
|
6346
|
+
"include-monolith",
|
|
6347
|
+
"include-sensitive-files",
|
|
6348
|
+
"skip-artifacts",
|
|
6349
|
+
"only-artifacts",
|
|
6350
|
+
"no-config",
|
|
6351
|
+
"format",
|
|
6352
|
+
"mode",
|
|
6353
|
+
"help",
|
|
6354
|
+
"version"
|
|
6355
|
+
],
|
|
6356
|
+
report: [
|
|
6357
|
+
"cwd",
|
|
6358
|
+
"out-dir",
|
|
6359
|
+
"max-part-bytes",
|
|
6360
|
+
"large-file-bytes",
|
|
6361
|
+
"max-monolith-bytes",
|
|
6362
|
+
"include-monolith",
|
|
6363
|
+
"include-sensitive-files",
|
|
6364
|
+
"skip-artifacts",
|
|
6365
|
+
"only-artifacts",
|
|
6366
|
+
"no-config",
|
|
6367
|
+
"format",
|
|
6368
|
+
"mode",
|
|
6369
|
+
"help",
|
|
6370
|
+
"version"
|
|
6371
|
+
],
|
|
6372
|
+
explain: [
|
|
6373
|
+
"cwd",
|
|
6374
|
+
"out-dir",
|
|
6375
|
+
"max-part-bytes",
|
|
6376
|
+
"large-file-bytes",
|
|
6377
|
+
"max-monolith-bytes",
|
|
6378
|
+
"include-monolith",
|
|
6379
|
+
"include-sensitive-files",
|
|
6380
|
+
"skip-artifacts",
|
|
6381
|
+
"only-artifacts",
|
|
6382
|
+
"no-config",
|
|
6383
|
+
"format",
|
|
6384
|
+
"mode",
|
|
6385
|
+
"help",
|
|
6386
|
+
"version"
|
|
6387
|
+
],
|
|
6388
|
+
diff: ["format", "mode", "help", "version"],
|
|
6389
|
+
describe: ["format", "mode", "help", "version"],
|
|
6390
|
+
init: [
|
|
6391
|
+
"cwd",
|
|
6392
|
+
"out-dir",
|
|
6393
|
+
"max-part-bytes",
|
|
6394
|
+
"include-sensitive-files",
|
|
6395
|
+
"skip-artifacts",
|
|
6396
|
+
"only-artifacts",
|
|
6397
|
+
"preset",
|
|
6398
|
+
"include",
|
|
6399
|
+
"exclude",
|
|
6400
|
+
"yes",
|
|
6401
|
+
"minimal",
|
|
6402
|
+
"force",
|
|
6403
|
+
"dry-run",
|
|
6404
|
+
"format",
|
|
6405
|
+
"mode",
|
|
6406
|
+
"help",
|
|
6407
|
+
"version"
|
|
6408
|
+
],
|
|
6409
|
+
skill: ["cwd", "format", "mode", "install", "target", "help", "version"]
|
|
6410
|
+
};
|
|
6411
|
+
var COMMAND_ORDER = [
|
|
6412
|
+
"generate",
|
|
6413
|
+
"validate",
|
|
6414
|
+
"report",
|
|
6415
|
+
"explain",
|
|
6416
|
+
"init",
|
|
6417
|
+
"diff",
|
|
6418
|
+
"describe",
|
|
6419
|
+
"skill"
|
|
6420
|
+
];
|
|
6421
|
+
function commandsForOption(option) {
|
|
6422
|
+
return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
|
|
6423
|
+
}
|
|
6424
|
+
function optionAppliesTo(option) {
|
|
6425
|
+
const commands = commandsForOption(option);
|
|
6426
|
+
if (commands.length === COMMAND_ORDER.length) return "all commands";
|
|
6427
|
+
return commands.join("/");
|
|
5150
6428
|
}
|
|
5151
6429
|
|
|
5152
6430
|
// src/agent/contract.ts
|
|
@@ -5238,11 +6516,11 @@ function buildAgentContract() {
|
|
|
5238
6516
|
invocation: {
|
|
5239
6517
|
oneShot: `npx ${PACKAGE_NAME}@latest`,
|
|
5240
6518
|
installed: "npx codebase-snapshot",
|
|
5241
|
-
bins: ["codebase-snapshot"],
|
|
6519
|
+
bins: ["codebase-snapshot", "snap"],
|
|
5242
6520
|
notes: [
|
|
5243
6521
|
`The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
|
|
5244
|
-
"Pin @latest (or @
|
|
5245
|
-
"
|
|
6522
|
+
"Pin @latest (or @4) when you need the exact same version on every machine.",
|
|
6523
|
+
"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
6524
|
"Human progress goes to stderr. With --format json the payload on stdout is pure JSON."
|
|
5247
6525
|
]
|
|
5248
6526
|
},
|
|
@@ -5251,7 +6529,7 @@ function buildAgentContract() {
|
|
|
5251
6529
|
step: 1,
|
|
5252
6530
|
id: "verify",
|
|
5253
6531
|
run: `npx ${PACKAGE_NAME}@latest --version`,
|
|
5254
|
-
expect:
|
|
6532
|
+
expect: `prints v${GENERATOR_VERSION} on stdout and exits 0`,
|
|
5255
6533
|
notes: "If it prints nothing, clear the npm cache and retry, or use `npx -p <pkg> codebase-snapshot`."
|
|
5256
6534
|
},
|
|
5257
6535
|
{
|
|
@@ -5321,9 +6599,9 @@ function buildAgentContract() {
|
|
|
5321
6599
|
{
|
|
5322
6600
|
name: "explain",
|
|
5323
6601
|
aliases: [],
|
|
5324
|
-
summary: "Explain how one path would be treated: layer, kind, part, skip reason.",
|
|
6602
|
+
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
6603
|
positionals: ["<path>"],
|
|
5326
|
-
exits: [0
|
|
6604
|
+
exits: [0]
|
|
5327
6605
|
},
|
|
5328
6606
|
{
|
|
5329
6607
|
name: "diff",
|
|
@@ -5357,140 +6635,147 @@ function buildAgentContract() {
|
|
|
5357
6635
|
flag: "--out-dir",
|
|
5358
6636
|
type: "string",
|
|
5359
6637
|
default: "snapshot",
|
|
5360
|
-
appliesTo: "
|
|
6638
|
+
appliesTo: optionAppliesTo("out-dir"),
|
|
5361
6639
|
summary: "Where the snapshot is written. Must not be the repo root or a directory the tool did not create."
|
|
5362
6640
|
},
|
|
5363
6641
|
{
|
|
5364
6642
|
flag: "--max-part-bytes",
|
|
5365
6643
|
type: "string",
|
|
5366
6644
|
default: 307200,
|
|
5367
|
-
appliesTo: "
|
|
6645
|
+
appliesTo: optionAppliesTo("max-part-bytes"),
|
|
5368
6646
|
summary: 'Byte budget per markdown part. Accepts "120k" / "1.5mb". Minimum 4096.'
|
|
5369
6647
|
},
|
|
5370
6648
|
{
|
|
5371
6649
|
flag: "--large-file-bytes",
|
|
5372
6650
|
type: "string",
|
|
5373
6651
|
default: 204800,
|
|
5374
|
-
appliesTo: "
|
|
6652
|
+
appliesTo: optionAppliesTo("large-file-bytes"),
|
|
5375
6653
|
summary: "Above this size a file gets a head/tail preview instead of the full body."
|
|
5376
6654
|
},
|
|
5377
6655
|
{
|
|
5378
6656
|
flag: "--max-monolith-bytes",
|
|
5379
6657
|
type: "string",
|
|
5380
6658
|
default: "unset",
|
|
5381
|
-
appliesTo: "
|
|
6659
|
+
appliesTo: optionAppliesTo("max-monolith-bytes"),
|
|
5382
6660
|
summary: "Skip the monolith artifacts when they would exceed this size."
|
|
5383
6661
|
},
|
|
5384
6662
|
{
|
|
5385
6663
|
flag: "--include-monolith",
|
|
5386
6664
|
type: "boolean",
|
|
5387
6665
|
default: false,
|
|
5388
|
-
appliesTo: "
|
|
6666
|
+
appliesTo: optionAppliesTo("include-monolith"),
|
|
5389
6667
|
summary: "Also write full-codebase.json and full-codebase.md."
|
|
5390
6668
|
},
|
|
5391
6669
|
{
|
|
5392
6670
|
flag: "--include-sensitive-files",
|
|
5393
6671
|
type: "boolean",
|
|
5394
6672
|
default: false,
|
|
5395
|
-
appliesTo: "
|
|
6673
|
+
appliesTo: optionAppliesTo("include-sensitive-files"),
|
|
5396
6674
|
summary: "Also emit .env / *.pem / .npmrc style files. Off by default; they are recorded as skipped."
|
|
5397
6675
|
},
|
|
5398
6676
|
{
|
|
5399
6677
|
flag: "--skip-artifacts",
|
|
5400
6678
|
type: "string",
|
|
5401
6679
|
default: "unset",
|
|
5402
|
-
appliesTo: "
|
|
6680
|
+
appliesTo: optionAppliesTo("skip-artifacts"),
|
|
5403
6681
|
summary: "Comma-separated artifacts to omit (readme, manifest, parts, navigation, projectTree, audit, monolith)."
|
|
5404
6682
|
},
|
|
5405
6683
|
{
|
|
5406
6684
|
flag: "--only-artifacts",
|
|
5407
6685
|
type: "string",
|
|
5408
6686
|
default: "unset",
|
|
5409
|
-
appliesTo: "
|
|
6687
|
+
appliesTo: optionAppliesTo("only-artifacts"),
|
|
5410
6688
|
summary: "Write only these artifacts. Cannot be combined with --skip-artifacts."
|
|
5411
6689
|
},
|
|
5412
6690
|
{
|
|
5413
6691
|
flag: "--format",
|
|
5414
6692
|
type: "string",
|
|
5415
6693
|
default: "text",
|
|
5416
|
-
appliesTo: "
|
|
6694
|
+
appliesTo: optionAppliesTo("format"),
|
|
5417
6695
|
summary: 'Output format: "text" or "json". JSON goes to stdout, progress to stderr.'
|
|
5418
6696
|
},
|
|
5419
6697
|
{
|
|
5420
6698
|
flag: "--cwd",
|
|
5421
6699
|
type: "string",
|
|
5422
6700
|
default: "process.cwd()",
|
|
5423
|
-
appliesTo: "
|
|
6701
|
+
appliesTo: optionAppliesTo("cwd"),
|
|
5424
6702
|
summary: "Repository to operate on. Must be an existing directory."
|
|
5425
6703
|
},
|
|
5426
6704
|
{
|
|
5427
6705
|
flag: "--dry-run",
|
|
5428
6706
|
type: "boolean",
|
|
5429
6707
|
default: false,
|
|
5430
|
-
appliesTo: "
|
|
6708
|
+
appliesTo: optionAppliesTo("dry-run"),
|
|
5431
6709
|
summary: "Compute everything and print the result without writing."
|
|
5432
6710
|
},
|
|
5433
6711
|
{
|
|
5434
6712
|
flag: "--watch",
|
|
5435
6713
|
type: "boolean",
|
|
5436
6714
|
default: false,
|
|
5437
|
-
appliesTo: "
|
|
6715
|
+
appliesTo: optionAppliesTo("watch"),
|
|
5438
6716
|
summary: "Regenerate on every change until interrupted."
|
|
5439
6717
|
},
|
|
6718
|
+
{
|
|
6719
|
+
flag: "--no-config",
|
|
6720
|
+
type: "boolean",
|
|
6721
|
+
default: false,
|
|
6722
|
+
appliesTo: optionAppliesTo("no-config"),
|
|
6723
|
+
summary: "Ignore the config file and run on the built-in defaults."
|
|
6724
|
+
},
|
|
5440
6725
|
{
|
|
5441
6726
|
flag: "--preset",
|
|
5442
6727
|
type: "string",
|
|
5443
6728
|
default: "detected",
|
|
5444
|
-
appliesTo: "
|
|
6729
|
+
appliesTo: optionAppliesTo("preset"),
|
|
5445
6730
|
summary: "Force a project preset instead of detecting one. Unknown names are a usage error."
|
|
5446
6731
|
},
|
|
5447
6732
|
{
|
|
5448
6733
|
flag: "--include",
|
|
5449
6734
|
type: "string",
|
|
5450
6735
|
default: "unset",
|
|
5451
|
-
appliesTo: "
|
|
6736
|
+
appliesTo: optionAppliesTo("include"),
|
|
5452
6737
|
summary: "Comma-separated allow-list of globs to record in the generated config."
|
|
5453
6738
|
},
|
|
5454
6739
|
{
|
|
5455
6740
|
flag: "--exclude",
|
|
5456
6741
|
type: "string",
|
|
5457
6742
|
default: "unset",
|
|
5458
|
-
appliesTo: "
|
|
6743
|
+
appliesTo: optionAppliesTo("exclude"),
|
|
5459
6744
|
summary: "Comma-separated globs to record in the generated config."
|
|
5460
6745
|
},
|
|
5461
6746
|
{
|
|
5462
6747
|
flag: "--yes",
|
|
5463
6748
|
type: "boolean",
|
|
5464
6749
|
default: false,
|
|
5465
|
-
appliesTo: "
|
|
6750
|
+
appliesTo: optionAppliesTo("yes"),
|
|
5466
6751
|
summary: "Accept every default without prompting. Required in a non-TTY."
|
|
5467
6752
|
},
|
|
5468
6753
|
{
|
|
5469
6754
|
flag: "--minimal",
|
|
5470
6755
|
type: "boolean",
|
|
5471
6756
|
default: false,
|
|
5472
|
-
appliesTo: "
|
|
6757
|
+
appliesTo: optionAppliesTo("minimal"),
|
|
5473
6758
|
summary: "Write only the settings that differ from the defaults."
|
|
5474
6759
|
},
|
|
5475
6760
|
{
|
|
5476
6761
|
flag: "--force",
|
|
5477
6762
|
type: "boolean",
|
|
5478
6763
|
default: false,
|
|
5479
|
-
appliesTo: "
|
|
6764
|
+
appliesTo: optionAppliesTo("force"),
|
|
5480
6765
|
summary: "Overwrite an existing config file."
|
|
5481
6766
|
},
|
|
5482
6767
|
{
|
|
5483
6768
|
flag: "--install",
|
|
5484
6769
|
type: "boolean",
|
|
5485
6770
|
default: false,
|
|
5486
|
-
appliesTo: "
|
|
6771
|
+
appliesTo: optionAppliesTo("install"),
|
|
5487
6772
|
summary: "Write the skill into the repository instead of printing it."
|
|
5488
6773
|
},
|
|
5489
6774
|
{
|
|
5490
6775
|
flag: "--target",
|
|
5491
6776
|
type: "string",
|
|
5492
6777
|
default: "all",
|
|
5493
|
-
appliesTo: "
|
|
6778
|
+
appliesTo: optionAppliesTo("target"),
|
|
5494
6779
|
summary: 'Where to install: "claude", "agents", "cursor" or "all".'
|
|
5495
6780
|
}
|
|
5496
6781
|
],
|
|
@@ -5565,8 +6850,8 @@ function buildAgentContract() {
|
|
|
5565
6850
|
troubleshooting: [
|
|
5566
6851
|
{
|
|
5567
6852
|
symptom: "Command prints nothing and exits 0",
|
|
5568
|
-
cause: "
|
|
5569
|
-
fix: `
|
|
6853
|
+
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.",
|
|
6854
|
+
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
6855
|
},
|
|
5571
6856
|
{
|
|
5572
6857
|
symptom: "Exit 1 with 'Refusing to replace outDir'",
|
|
@@ -5644,7 +6929,8 @@ not resolved \u2014 clear the npm cache and retry; do not assume the repository
|
|
|
5644
6929
|
\`--format json\` if you want to parse it.
|
|
5645
6930
|
|
|
5646
6931
|
4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
|
|
5647
|
-
the project shape, then writes \`snapshot.config.ts\`
|
|
6932
|
+
the project shape, then writes \`snapshot.config.ts\` (or \`.mjs\` when the project is
|
|
6933
|
+
not an ES module) listing **every** setting at its
|
|
5648
6934
|
current value, commented. Pass \`--preset <name>\` only if detection is wrong; the
|
|
5649
6935
|
preset names are in the contract. This step is optional: without a config the tool
|
|
5650
6936
|
runs on the same defaults.
|
|
@@ -5745,14 +7031,24 @@ function mergeAgentsFile(cwd, skill) {
|
|
|
5745
7031
|
next = `# AGENTS.md
|
|
5746
7032
|
|
|
5747
7033
|
${block}`;
|
|
5748
|
-
} else
|
|
7034
|
+
} else {
|
|
7035
|
+
const starts = existing.split(AGENTS_MARKER_START).length - 1;
|
|
7036
|
+
const ends = existing.split(AGENTS_MARKER_END).length - 1;
|
|
5749
7037
|
const start = existing.indexOf(AGENTS_MARKER_START);
|
|
5750
7038
|
const end = existing.indexOf(AGENTS_MARKER_END) + AGENTS_MARKER_END.length;
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
7039
|
+
if (starts === 1 && ends === 1 && start < end) {
|
|
7040
|
+
next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(end)}`;
|
|
7041
|
+
} else if (starts > 0 && ends === 0) {
|
|
7042
|
+
next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(
|
|
7043
|
+
start + AGENTS_MARKER_START.length
|
|
7044
|
+
)}`;
|
|
7045
|
+
} else if (existing.includes(block.trimEnd())) {
|
|
7046
|
+
next = existing;
|
|
7047
|
+
} else {
|
|
7048
|
+
next = `${existing.trimEnd()}
|
|
5754
7049
|
|
|
5755
7050
|
${block}`;
|
|
7051
|
+
}
|
|
5756
7052
|
}
|
|
5757
7053
|
const action = existing === null ? "created" : existing === next ? "unchanged" : "updated";
|
|
5758
7054
|
if (action !== "unchanged") {
|
|
@@ -5815,6 +7111,7 @@ var CLI_OPTIONS = {
|
|
|
5815
7111
|
mode: { type: "string" },
|
|
5816
7112
|
"dry-run": { type: "boolean" },
|
|
5817
7113
|
watch: { type: "boolean" },
|
|
7114
|
+
"no-config": { type: "boolean" },
|
|
5818
7115
|
preset: { type: "string" },
|
|
5819
7116
|
include: { type: "string" },
|
|
5820
7117
|
exclude: { type: "string" },
|
|
@@ -5861,6 +7158,7 @@ Options:
|
|
|
5861
7158
|
--format <text|json> CLI output format (default: text)
|
|
5862
7159
|
--dry-run Compute the snapshot without writing anything
|
|
5863
7160
|
--watch Re-generate whenever the repository changes
|
|
7161
|
+
--no-config Ignore snapshot.config.* and use the built-in defaults
|
|
5864
7162
|
|
|
5865
7163
|
init only:
|
|
5866
7164
|
--preset <name> Force a project preset instead of detecting one
|
|
@@ -6010,28 +7308,32 @@ function parseCliArgs(argv) {
|
|
|
6010
7308
|
} else if (rawSkip !== void 0) {
|
|
6011
7309
|
skipArtifacts = parseArtifactList(String(rawSkip), "--skip-artifacts");
|
|
6012
7310
|
}
|
|
6013
|
-
|
|
6014
|
-
|
|
6015
|
-
|
|
6016
|
-
if (
|
|
6017
|
-
|
|
6018
|
-
if (
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
|
|
6022
|
-
|
|
6023
|
-
|
|
6024
|
-
|
|
6025
|
-
|
|
7311
|
+
const scope = COMMAND_OPTION_SCOPE[command];
|
|
7312
|
+
const ignored = [];
|
|
7313
|
+
for (const name of Object.keys(CLI_OPTIONS)) {
|
|
7314
|
+
if (scope.includes(name)) continue;
|
|
7315
|
+
const value = values[name];
|
|
7316
|
+
if (value === void 0 || value === false) continue;
|
|
7317
|
+
ignored.push(`--${name}`);
|
|
7318
|
+
}
|
|
7319
|
+
if (ignored.length > 0) {
|
|
7320
|
+
const owners = Array.from(
|
|
7321
|
+
new Set(
|
|
7322
|
+
ignored.flatMap(
|
|
7323
|
+
(flag) => commandsForOption(flag.replace(/^--/, "")).map((owner) => `\`${owner}\``)
|
|
7324
|
+
)
|
|
7325
|
+
)
|
|
7326
|
+
);
|
|
7327
|
+
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)." : "";
|
|
7328
|
+
throw new UsageError(
|
|
7329
|
+
`${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}`
|
|
7330
|
+
);
|
|
6026
7331
|
}
|
|
6027
7332
|
if (command === "init" && values.preset !== void 0) {
|
|
6028
7333
|
const requested = String(values.preset);
|
|
6029
7334
|
if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
|
|
6030
7335
|
}
|
|
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))) {
|
|
7336
|
+
if (command === "skill" && values.target !== void 0 && !isSkillTarget(String(values.target))) {
|
|
6035
7337
|
throw new UsageError(
|
|
6036
7338
|
`--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
|
|
6037
7339
|
);
|
|
@@ -6042,6 +7344,7 @@ function parseCliArgs(argv) {
|
|
|
6042
7344
|
format: rawFormat,
|
|
6043
7345
|
dryRun: values["dry-run"] === true,
|
|
6044
7346
|
watch: values.watch === true,
|
|
7347
|
+
noConfig: values["no-config"] === true,
|
|
6045
7348
|
preset: values.preset === void 0 ? void 0 : String(values.preset),
|
|
6046
7349
|
maxPartBytesRaw: values["max-part-bytes"] === void 0 ? void 0 : String(values["max-part-bytes"]),
|
|
6047
7350
|
include: values.include === void 0 ? void 0 : String(values.include),
|
|
@@ -6095,6 +7398,12 @@ function splitList(value) {
|
|
|
6095
7398
|
if (value === void 0) return void 0;
|
|
6096
7399
|
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
6097
7400
|
}
|
|
7401
|
+
function displayOutPath(cwd, outDir, ...segments) {
|
|
7402
|
+
const absolute = path12.resolve(cwd, outDir, ...segments);
|
|
7403
|
+
const relative5 = path12.relative(cwd, absolute);
|
|
7404
|
+
if (relative5 && !relative5.startsWith("..") && !path12.isAbsolute(relative5)) return relative5;
|
|
7405
|
+
return absolute;
|
|
7406
|
+
}
|
|
6098
7407
|
function describeManifestShapeProblem(manifest) {
|
|
6099
7408
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
6100
7409
|
return "not a JSON object";
|
|
@@ -6114,6 +7423,7 @@ async function runInitCommand(invocation) {
|
|
|
6114
7423
|
const result = await runInit({
|
|
6115
7424
|
cwd: invocation.cwd,
|
|
6116
7425
|
preset: invocation.preset,
|
|
7426
|
+
outDir: invocation.outDir,
|
|
6117
7427
|
maxPartBytes: invocation.maxPartBytesRaw ?? invocation.maxPartBytes,
|
|
6118
7428
|
include: splitList(invocation.include),
|
|
6119
7429
|
exclude: splitList(invocation.exclude),
|
|
@@ -6140,7 +7450,11 @@ async function runInitCommand(invocation) {
|
|
|
6140
7450
|
}
|
|
6141
7451
|
for (const note of result.notes) info(` \u26A0 ${note}`);
|
|
6142
7452
|
info("");
|
|
6143
|
-
|
|
7453
|
+
if (result.scripts.length > 0) {
|
|
7454
|
+
info(" Next: npm run snapshot");
|
|
7455
|
+
} else {
|
|
7456
|
+
info(` Next: ${GENERATOR_NAME} generate`);
|
|
7457
|
+
}
|
|
6144
7458
|
info(
|
|
6145
7459
|
` Tip: \`${GENERATOR_NAME} skill --install\` makes this repo discoverable by AI agents.`
|
|
6146
7460
|
);
|
|
@@ -6156,6 +7470,13 @@ async function runInitCommand(invocation) {
|
|
|
6156
7470
|
}
|
|
6157
7471
|
function runSkillCommand(invocation) {
|
|
6158
7472
|
if (!invocation.install) {
|
|
7473
|
+
if (invocation.format === "json") {
|
|
7474
|
+
process.stdout.write(
|
|
7475
|
+
`${JSON.stringify({ name: SKILL_NAME, version: GENERATOR_VERSION, skill: renderSkill() }, null, 2)}
|
|
7476
|
+
`
|
|
7477
|
+
);
|
|
7478
|
+
return 0;
|
|
7479
|
+
}
|
|
6159
7480
|
process.stdout.write(renderSkill());
|
|
6160
7481
|
return 0;
|
|
6161
7482
|
}
|
|
@@ -6232,7 +7553,8 @@ ${USAGE}
|
|
|
6232
7553
|
if (invocation.command === "skill") {
|
|
6233
7554
|
return runSkillCommand(invocation);
|
|
6234
7555
|
}
|
|
6235
|
-
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
7556
|
+
const loaded = invocation.noConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
|
|
7557
|
+
if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
|
|
6236
7558
|
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
6237
7559
|
if (loaded.errors.length > 0) {
|
|
6238
7560
|
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
@@ -6304,7 +7626,7 @@ ${USAGE}
|
|
|
6304
7626
|
if (invocation.command === "explain") {
|
|
6305
7627
|
const result = explainPath(resolved, invocation.args[0]);
|
|
6306
7628
|
emit(invocation.format, result, () => info(renderExplainText(result)));
|
|
6307
|
-
return
|
|
7629
|
+
return 0;
|
|
6308
7630
|
}
|
|
6309
7631
|
if (invocation.command === "diff") {
|
|
6310
7632
|
try {
|
|
@@ -6328,7 +7650,9 @@ ${USAGE}
|
|
|
6328
7650
|
if (invocation.dryRun) {
|
|
6329
7651
|
info("\n\u{1F9EA} Dry run \u2014 nothing was written:");
|
|
6330
7652
|
info(` - Would track: ${manifest.counts.totalFiles} files`);
|
|
6331
|
-
info(
|
|
7653
|
+
info(
|
|
7654
|
+
` - Would write: ${manifest.counts.partsCount} part(s) under ${displayOutPath(resolved.cwd, resolved.outDir, "parts")}`
|
|
7655
|
+
);
|
|
6332
7656
|
info(` - Skipped: ${manifest.counts.skippedCount} file(s)`);
|
|
6333
7657
|
info(` - Warnings: ${manifest.warnings.length}`);
|
|
6334
7658
|
info(` - Integrity: ${manifest.validation.status}`);
|
|
@@ -6343,7 +7667,9 @@ ${USAGE}
|
|
|
6343
7667
|
);
|
|
6344
7668
|
const { artifacts } = resolved;
|
|
6345
7669
|
if (artifacts.parts) {
|
|
6346
|
-
info(
|
|
7670
|
+
info(
|
|
7671
|
+
` - Parts written: ${manifest.counts.partsCount} under ${displayOutPath(resolved.cwd, resolved.outDir, "parts")}/`
|
|
7672
|
+
);
|
|
6347
7673
|
}
|
|
6348
7674
|
const reports = [
|
|
6349
7675
|
artifacts.projectTree && "project-tree",
|
|
@@ -6351,10 +7677,28 @@ ${USAGE}
|
|
|
6351
7677
|
artifacts.audit && "audit"
|
|
6352
7678
|
].filter(Boolean);
|
|
6353
7679
|
if (reports.length > 0) {
|
|
6354
|
-
info(
|
|
7680
|
+
info(
|
|
7681
|
+
` - Reports: ${displayOutPath(resolved.cwd, resolved.outDir, "reports")}/ (${reports.join(", ")})`
|
|
7682
|
+
);
|
|
7683
|
+
}
|
|
7684
|
+
if (artifacts.manifest) {
|
|
7685
|
+
info(` - Manifest: ${displayOutPath(resolved.cwd, resolved.outDir, "manifest.json")}`);
|
|
7686
|
+
}
|
|
7687
|
+
if (artifacts.monolith) {
|
|
7688
|
+
const monolithFiles = [
|
|
7689
|
+
manifest.artifacts?.fullJson ? "full-codebase.json" : null,
|
|
7690
|
+
manifest.artifacts?.fullMarkdown ? "full-codebase.md" : null
|
|
7691
|
+
].filter((name) => name !== null);
|
|
7692
|
+
if (monolithFiles.length === 2) {
|
|
7693
|
+
info(
|
|
7694
|
+
` - Monolith: ${displayOutPath(resolved.cwd, resolved.outDir, "full-codebase.{json,md}")}`
|
|
7695
|
+
);
|
|
7696
|
+
} else if (monolithFiles.length === 1) {
|
|
7697
|
+
info(
|
|
7698
|
+
` - Monolith: ${displayOutPath(resolved.cwd, resolved.outDir, monolithFiles[0])} (the other was skipped by --max-monolith-bytes)`
|
|
7699
|
+
);
|
|
7700
|
+
}
|
|
6355
7701
|
}
|
|
6356
|
-
if (artifacts.manifest) info(` - Manifest: ${resolved.outDir}/manifest.json`);
|
|
6357
|
-
if (artifacts.monolith) info(` - Monolith: ${resolved.outDir}/full-codebase.{json,md}`);
|
|
6358
7702
|
if (reports.length === 0 && !artifacts.parts && !artifacts.manifest && !artifacts.monolith) {
|
|
6359
7703
|
info(` - Artifacts: all disabled (only the ownership marker was written)`);
|
|
6360
7704
|
}
|
|
@@ -6389,18 +7733,43 @@ ${USAGE}
|
|
|
6389
7733
|
async function runWatchLoop(invocation, resolved) {
|
|
6390
7734
|
const outDir = path12.resolve(resolved.cwd, resolved.outDir);
|
|
6391
7735
|
const tmpDir = path12.resolve(resolved.cwd, resolved.tmpDir);
|
|
7736
|
+
const VCS_SEGMENTS = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr"]);
|
|
6392
7737
|
const outName = path12.basename(outDir).toLowerCase();
|
|
6393
7738
|
const tmpName = path12.basename(tmpDir).toLowerCase();
|
|
6394
7739
|
const isGenerated = (candidate) => candidate.toLowerCase().split(/[\\/]/).some(
|
|
6395
|
-
(segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`)
|
|
7740
|
+
(segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`) || VCS_SEGMENTS.has(segment)
|
|
6396
7741
|
);
|
|
6397
7742
|
let running = false;
|
|
6398
|
-
const regenerate = () => {
|
|
7743
|
+
const regenerate = async () => {
|
|
6399
7744
|
if (running) return;
|
|
6400
7745
|
running = true;
|
|
6401
7746
|
const started = performance.now();
|
|
6402
7747
|
try {
|
|
6403
|
-
const
|
|
7748
|
+
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
7749
|
+
if (loaded.errors.length > 0) {
|
|
7750
|
+
for (const error of loaded.errors) warn(`\u26A0 Config: ${error}`);
|
|
7751
|
+
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
7752
|
+
return;
|
|
7753
|
+
}
|
|
7754
|
+
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
7755
|
+
const next = resolveConfig(
|
|
7756
|
+
loaded.config,
|
|
7757
|
+
{
|
|
7758
|
+
mode: invocation.command,
|
|
7759
|
+
format: invocation.format,
|
|
7760
|
+
dryRun: invocation.dryRun,
|
|
7761
|
+
cwd: invocation.cwd,
|
|
7762
|
+
outDir: invocation.outDir,
|
|
7763
|
+
includeMonolith: invocation.includeMonolith,
|
|
7764
|
+
includeSensitiveFiles: invocation.includeSensitiveFiles,
|
|
7765
|
+
maxPartBytes: invocation.maxPartBytes,
|
|
7766
|
+
largeFileBytes: invocation.largeFileBytes,
|
|
7767
|
+
maxMonolithBytes: invocation.maxMonolithBytes,
|
|
7768
|
+
skipArtifacts: invocation.skipArtifacts
|
|
7769
|
+
},
|
|
7770
|
+
invocation.cwd
|
|
7771
|
+
);
|
|
7772
|
+
const manifest = generateSnapshot(next, { dryRun: invocation.dryRun });
|
|
6404
7773
|
const elapsed = ((performance.now() - started) / 1e3).toFixed(2);
|
|
6405
7774
|
info(
|
|
6406
7775
|
`\u267B\uFE0F ${(/* @__PURE__ */ new Date()).toISOString()} ${manifest.counts.totalFiles} file(s), ${manifest.counts.partsCount} part(s), ${manifest.validation.status} (${elapsed}s)`
|
|
@@ -6412,7 +7781,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
6412
7781
|
}
|
|
6413
7782
|
};
|
|
6414
7783
|
info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
|
|
6415
|
-
regenerate();
|
|
7784
|
+
void regenerate();
|
|
6416
7785
|
let timer = null;
|
|
6417
7786
|
const watcher = fs11.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
|
|
6418
7787
|
if (filename) {
|
|
@@ -6420,7 +7789,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
6420
7789
|
if (isGenerated(changed)) return;
|
|
6421
7790
|
}
|
|
6422
7791
|
if (timer) clearTimeout(timer);
|
|
6423
|
-
timer = setTimeout(regenerate, 250);
|
|
7792
|
+
timer = setTimeout(() => void regenerate(), 250);
|
|
6424
7793
|
});
|
|
6425
7794
|
await new Promise((resolve6) => {
|
|
6426
7795
|
const stop = () => {
|