@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/cli.cjs
CHANGED
|
@@ -7,6 +7,7 @@ var url = require('url');
|
|
|
7
7
|
var childProcess = require('child_process');
|
|
8
8
|
var readline = require('readline');
|
|
9
9
|
var crypto = require('crypto');
|
|
10
|
+
var os = require('os');
|
|
10
11
|
var ts = require('typescript');
|
|
11
12
|
|
|
12
13
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
@@ -34,6 +35,7 @@ var path10__namespace = /*#__PURE__*/_interopNamespace(path10);
|
|
|
34
35
|
var childProcess__namespace = /*#__PURE__*/_interopNamespace(childProcess);
|
|
35
36
|
var readline__namespace = /*#__PURE__*/_interopNamespace(readline);
|
|
36
37
|
var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
|
|
38
|
+
var os__namespace = /*#__PURE__*/_interopNamespace(os);
|
|
37
39
|
var ts__default = /*#__PURE__*/_interopDefault(ts);
|
|
38
40
|
|
|
39
41
|
// src/cli.ts
|
|
@@ -54,7 +56,40 @@ function hashMap(manifest) {
|
|
|
54
56
|
}
|
|
55
57
|
return map;
|
|
56
58
|
}
|
|
59
|
+
function manifestProblem(value) {
|
|
60
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
|
|
61
|
+
const candidate = value;
|
|
62
|
+
if (!Array.isArray(candidate.files)) return 'no "files" array';
|
|
63
|
+
for (const entry of candidate.files) {
|
|
64
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
65
|
+
return 'a "files" entry is not an object';
|
|
66
|
+
}
|
|
67
|
+
if (typeof entry.relativePath !== "string") {
|
|
68
|
+
return 'a "files" entry has no "relativePath"';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const counts = candidate.counts;
|
|
72
|
+
if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
|
|
73
|
+
return 'no "counts" object';
|
|
74
|
+
}
|
|
75
|
+
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
76
|
+
if (typeof counts[key] !== "number") {
|
|
77
|
+
return `no numeric "counts.${key}"`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
|
|
81
|
+
if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
function assertUsableManifest(value, label) {
|
|
85
|
+
const problem = manifestProblem(value);
|
|
86
|
+
if (problem) {
|
|
87
|
+
throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
57
90
|
function compareManifests(before, after) {
|
|
91
|
+
assertUsableManifest(before, "before");
|
|
92
|
+
assertUsableManifest(after, "after");
|
|
58
93
|
const left = hashMap(before);
|
|
59
94
|
const right = hashMap(after);
|
|
60
95
|
const added = [];
|
|
@@ -100,11 +135,11 @@ function loadManifest(target) {
|
|
|
100
135
|
} catch (err) {
|
|
101
136
|
throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
|
|
102
137
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
138
|
+
const problem = manifestProblem(parsed);
|
|
139
|
+
if (problem) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
|
|
142
|
+
);
|
|
108
143
|
}
|
|
109
144
|
return parsed;
|
|
110
145
|
}
|
|
@@ -966,6 +1001,416 @@ function toDetected(preset) {
|
|
|
966
1001
|
function detectPreset(cwd, forced) {
|
|
967
1002
|
return detectPresetFromContext(buildPresetContext(cwd), forced);
|
|
968
1003
|
}
|
|
1004
|
+
|
|
1005
|
+
// src/security/patterns.ts
|
|
1006
|
+
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)";
|
|
1007
|
+
var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
|
|
1008
|
+
var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|process\\.env|Buffer|Record|object)\\b)";
|
|
1009
|
+
var UNQUOTED_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{1,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
1010
|
+
var UNQUOTED_AMBIGUOUS_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{8,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
1011
|
+
var UNQUOTED_STRONG_NAME = `(?<!id[-_])${STRONG_CREDENTIAL_NAME}`;
|
|
1012
|
+
var DEFAULT_SECRET_PATTERNS = [
|
|
1013
|
+
{
|
|
1014
|
+
rule: "private-key-header",
|
|
1015
|
+
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
rule: "jwt-token",
|
|
1019
|
+
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
rule: "aws-access-key-id",
|
|
1023
|
+
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
// The name is no longer limited to the three snake_case spellings: an
|
|
1027
|
+
// `awsSecret` / `secretAccessKey` property leaked verbatim because only the
|
|
1028
|
+
// literal words matched. The value is masked alone, so the key survives.
|
|
1029
|
+
rule: "aws-secret-access-key",
|
|
1030
|
+
pattern: new RegExp(
|
|
1031
|
+
`${STRONG_CREDENTIAL_NAME}["']?\\s{0,8}[:=]\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
|
|
1032
|
+
"i"
|
|
1033
|
+
),
|
|
1034
|
+
valueGroup: 1
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
// A 40-character base64 value with no key name in sight. A key-independent
|
|
1038
|
+
// rule is the only thing that catches `new AWS.SecretKey(…)` arguments and
|
|
1039
|
+
// pasted `~/.aws/credentials` fragments, but a bare length test also matches
|
|
1040
|
+
// every 40-char git SHA-1, so the value must mix case and digits and must
|
|
1041
|
+
// not be pure hex.
|
|
1042
|
+
rule: "aws-secret-access-key-value",
|
|
1043
|
+
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}/
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
rule: "github-token",
|
|
1047
|
+
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
1048
|
+
},
|
|
1049
|
+
{
|
|
1050
|
+
// `xoxc-`/`xoxd-` are the user and refresh tokens, `xoxb`/`xoxp` the bot and
|
|
1051
|
+
// user tokens; the whole Slack family shares one prefix shape.
|
|
1052
|
+
rule: "slack-token",
|
|
1053
|
+
pattern: /xox[baprsdc]-[0-9A-Za-z-]{10,250}/
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
// Prefix-only vendors. Each prefix below is documented, globally unique and
|
|
1057
|
+
// cannot collide with an ordinary identifier, which is what makes a bare
|
|
1058
|
+
// prefix rule cheap and safe: `glpat-` (GitLab), `hvs.`/`s.` (Vault service
|
|
1059
|
+
// and batch tokens), `whsec_` (Stripe webhook secret), `dckr_pat_` (Docker
|
|
1060
|
+
// Hub), `hf_` (HuggingFace), `ya29.` (Google OAuth access token).
|
|
1061
|
+
rule: "known-vendor-token-prefix",
|
|
1062
|
+
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})/
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
rule: "slack-webhook-url",
|
|
1066
|
+
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
rule: "stripe-secret-key",
|
|
1070
|
+
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
rule: "google-api-key",
|
|
1074
|
+
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
1075
|
+
},
|
|
1076
|
+
{
|
|
1077
|
+
rule: "google-oauth-client-secret",
|
|
1078
|
+
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
1079
|
+
},
|
|
1080
|
+
{
|
|
1081
|
+
rule: "openai-anthropic-key",
|
|
1082
|
+
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
rule: "npm-token",
|
|
1086
|
+
pattern: /npm_[A-Za-z0-9]{36}/
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
rule: "pypi-token",
|
|
1090
|
+
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
rule: "sendgrid-key",
|
|
1094
|
+
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
1095
|
+
},
|
|
1096
|
+
{
|
|
1097
|
+
rule: "twilio-api-key",
|
|
1098
|
+
pattern: /SK[0-9a-fA-F]{32}/
|
|
1099
|
+
},
|
|
1100
|
+
{
|
|
1101
|
+
rule: "digitalocean-token",
|
|
1102
|
+
pattern: /dop_v1_[0-9a-f]{64}/
|
|
1103
|
+
},
|
|
1104
|
+
{
|
|
1105
|
+
rule: "azure-storage-account-key",
|
|
1106
|
+
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
// Scheme-less userinfo: `//user:password@host`.
|
|
1110
|
+
//
|
|
1111
|
+
// Enumerating schemes is a losing game — `mysqlx`, `svn`, `telnet`, `imap`,
|
|
1112
|
+
// `pop3`, `nntp`, `kafka`, `elasticsearch` each leaked a password while the
|
|
1113
|
+
// list grew by hand. This rule matches the shape instead, so any scheme
|
|
1114
|
+
// (including one invented tomorrow) is covered, provided the `//` is followed
|
|
1115
|
+
// immediately by the userinfo: `// see the docs` and a bare reminder comment
|
|
1116
|
+
// never match, because a space or the comment wording breaks the shape.
|
|
1117
|
+
rule: "uri-credentials",
|
|
1118
|
+
pattern: /\/\/[^\s:/@"'`;()<>,]{1,128}:[^\s:/@"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
// The host tail stops at quotes, `;`, `)`, `<`, `>` and `,`: an unbounded
|
|
1122
|
+
// `[^\s/]` used to swallow the closing quote and the statement separator of
|
|
1123
|
+
// the string the URL was written in.
|
|
1124
|
+
rule: "database-uri-credentials",
|
|
1125
|
+
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}/
|
|
1126
|
+
},
|
|
1127
|
+
{
|
|
1128
|
+
rule: "basic-auth-url",
|
|
1129
|
+
pattern: /(?:https?|ftp|sftp|ssh|git|smtp|smtps|ldap|ldaps):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/i
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
// Quoted credentials in any common key spelling. The value may be as short
|
|
1133
|
+
// as one character: `password: "hunter2"` is a credential whatever its
|
|
1134
|
+
// entropy, and a length floor was exactly how it survived into `parts`.
|
|
1135
|
+
//
|
|
1136
|
+
// The key is either bare (`dbPassword:`) or wholly quoted (`"dbPassword":`).
|
|
1137
|
+
// There is deliberately no optional quote *before* the separator: that let a
|
|
1138
|
+
// previous string's closing quote stand in for the key's, so
|
|
1139
|
+
// ``${pass ? " PASS" : " FAIL"}`` had `" FAIL"` masked as if `PASS` were
|
|
1140
|
+
// the key name.
|
|
1141
|
+
rule: "credential-assignment",
|
|
1142
|
+
pattern: new RegExp(
|
|
1143
|
+
`(?:["']${STRONG_CREDENTIAL_NAME}["']|${STRONG_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{1,512})\\1`,
|
|
1144
|
+
"i"
|
|
1145
|
+
),
|
|
1146
|
+
valueGroup: 2
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
// `pass`, `id-token` and `credentials` are ambiguous on their own, so they
|
|
1150
|
+
// are accepted only against a quoted value long enough to be a credential.
|
|
1151
|
+
rule: "credential-assignment-weak",
|
|
1152
|
+
pattern: new RegExp(
|
|
1153
|
+
`(?:["']${WEAK_CREDENTIAL_NAME}["']|${WEAK_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{8,512})\\1`,
|
|
1154
|
+
"i"
|
|
1155
|
+
),
|
|
1156
|
+
valueGroup: 2
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
// The unquoted twin for YAML, docker-compose and `.env`: those formats never
|
|
1160
|
+
// quote their values, so the rules above cannot see them. `valueGroup` keeps
|
|
1161
|
+
// `POSTGRES_PASSWORD:` in the output.
|
|
1162
|
+
rule: "credential-assignment-unquoted",
|
|
1163
|
+
pattern: new RegExp(
|
|
1164
|
+
`${UNQUOTED_STRONG_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
|
|
1165
|
+
"i"
|
|
1166
|
+
),
|
|
1167
|
+
valueGroup: 1
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
// `pass: hunter2hunter2` and `credentials: hunter2hunter2`: the ambiguous
|
|
1171
|
+
// names, unquoted, with the length and non-digit floor that keeps
|
|
1172
|
+
// `pass: 3` / `credentials: none` readable.
|
|
1173
|
+
rule: "credential-assignment-unquoted-weak",
|
|
1174
|
+
pattern: new RegExp(
|
|
1175
|
+
`${WEAK_CREDENTIAL_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
|
|
1176
|
+
"i"
|
|
1177
|
+
),
|
|
1178
|
+
valueGroup: 1
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
// `Authorization: Bearer <token>` and `Authorization: Basic <base64>`.
|
|
1182
|
+
//
|
|
1183
|
+
// The scheme is consumed by a lookbehind so the match is the token alone and
|
|
1184
|
+
// needs no `valueGroup`. A lone `Bearer`/`Basic`, and a word after it that
|
|
1185
|
+
// cannot be a credential (`Bearer authentication`), stay readable: the value
|
|
1186
|
+
// must contain a digit, be at least 32 chars, or carry base64 `=` padding.
|
|
1187
|
+
rule: "authorization-header",
|
|
1188
|
+
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_.+/=-])/
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
// Broader names than before (`apiKey`, `clientSecret`, `privateKey` were
|
|
1192
|
+
// invisible to it) and value-only masking. Kept alongside the two rules
|
|
1193
|
+
// above because it also matches a *quoted key* followed by an unquoted
|
|
1194
|
+
// value, e.g. `"client_secret": abc123`.
|
|
1195
|
+
rule: "generic-secret-assignment",
|
|
1196
|
+
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["']([^"'\s]{8,256})["']/i,
|
|
1197
|
+
valueGroup: 1
|
|
1198
|
+
},
|
|
1199
|
+
{
|
|
1200
|
+
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
1201
|
+
// was therefore invisible to the rule above. Anchored to the line start and
|
|
1202
|
+
// limited to secret-looking names to stay at zero false positives.
|
|
1203
|
+
//
|
|
1204
|
+
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
1205
|
+
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
1206
|
+
// every offset of a long identifier, which made this rule quadratic
|
|
1207
|
+
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
1208
|
+
// The value is matched on its own — `=\s*(value)` — and masked alone.
|
|
1209
|
+
// Anchoring the value to end-of-line (`[^\s#]{6,512}$`) meant ANY trailing
|
|
1210
|
+
// comment or space disabled the rule, so `TOKEN=secret # rotate` and
|
|
1211
|
+
// `TOKEN=secret ` were emitted in clear; a CRLF file disabled it wholesale.
|
|
1212
|
+
rule: "env-secret-assignment",
|
|
1213
|
+
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,
|
|
1214
|
+
valueGroup: 1
|
|
1215
|
+
}
|
|
1216
|
+
];
|
|
1217
|
+
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
1218
|
+
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
1219
|
+
|
|
1220
|
+
// src/security/redact.ts
|
|
1221
|
+
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
1222
|
+
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
1223
|
+
function describeValue(value) {
|
|
1224
|
+
if (value === null) return "null";
|
|
1225
|
+
if (Array.isArray(value)) return "an array";
|
|
1226
|
+
if (value instanceof RegExp) return value.toString();
|
|
1227
|
+
if (typeof value === "string") return `the string ${JSON.stringify(value)}`;
|
|
1228
|
+
return `a ${typeof value}`;
|
|
1229
|
+
}
|
|
1230
|
+
function compilePattern(rule, pattern, valueGroup) {
|
|
1231
|
+
if (pattern instanceof RegExp) {
|
|
1232
|
+
const flags = new Set(pattern.flags.replace(/[gy]/g, "").split(""));
|
|
1233
|
+
if (valueGroup !== void 0) flags.add("d");
|
|
1234
|
+
return {
|
|
1235
|
+
rule,
|
|
1236
|
+
pattern: new RegExp(pattern.source, [...flags].join("")),
|
|
1237
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
if (typeof pattern === "string") {
|
|
1241
|
+
try {
|
|
1242
|
+
return {
|
|
1243
|
+
rule,
|
|
1244
|
+
pattern: new RegExp(pattern, valueGroup === void 0 ? "i" : "id"),
|
|
1245
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1246
|
+
};
|
|
1247
|
+
} catch (err) {
|
|
1248
|
+
throw new Error(
|
|
1249
|
+
`Invalid regular expression for secret rule "${rule}": ${err.message}`
|
|
1250
|
+
);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
throw new Error(
|
|
1254
|
+
`Secret rule "${rule}" has a pattern that is neither a RegExp nor a string (received ${describeValue(pattern)}).`
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
function compileSecretRules(rules) {
|
|
1258
|
+
if (!Array.isArray(rules)) {
|
|
1259
|
+
throw new Error(
|
|
1260
|
+
`secrets.customPatterns must be an array of secret rules (received ${describeValue(rules)}).`
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
let generated = 0;
|
|
1264
|
+
return rules.map((entry, index) => {
|
|
1265
|
+
const where = `secrets.customPatterns[${index}]`;
|
|
1266
|
+
if (entry instanceof RegExp) {
|
|
1267
|
+
generated += 1;
|
|
1268
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1269
|
+
}
|
|
1270
|
+
if (typeof entry === "string") {
|
|
1271
|
+
generated += 1;
|
|
1272
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1273
|
+
}
|
|
1274
|
+
if (typeof entry === "object" && entry !== null) {
|
|
1275
|
+
const candidate = entry;
|
|
1276
|
+
if (typeof candidate.rule !== "string" || candidate.rule.trim() === "") {
|
|
1277
|
+
throw new Error(
|
|
1278
|
+
`${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.`
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
const valueGroup = candidate.valueGroup;
|
|
1282
|
+
if (valueGroup !== void 0) {
|
|
1283
|
+
if (typeof valueGroup !== "number" || !Number.isInteger(valueGroup) || valueGroup < 0) {
|
|
1284
|
+
throw new Error(
|
|
1285
|
+
`${where}.valueGroup must be a non-negative integer capture-group index (received ${describeValue(valueGroup)}).`
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
if (candidate.pattern === void 0) {
|
|
1290
|
+
throw new Error(
|
|
1291
|
+
`${where} (rule "${candidate.rule}") is missing \`pattern\`. Expected { rule, pattern: RegExp | string }.`
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
return compilePattern(
|
|
1295
|
+
candidate.rule,
|
|
1296
|
+
candidate.pattern,
|
|
1297
|
+
valueGroup
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
throw new Error(
|
|
1301
|
+
`${where} must be a RegExp, a string, or { rule, pattern } (received ${describeValue(entry)}).`
|
|
1302
|
+
);
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
function toGlobal(rule) {
|
|
1306
|
+
const flags = new Set(rule.pattern.flags.replace(/[gy]/g, "").split(""));
|
|
1307
|
+
flags.add("g");
|
|
1308
|
+
if (rule.valueGroup !== void 0) flags.add("d");
|
|
1309
|
+
return new RegExp(rule.pattern.source, [...flags].join(""));
|
|
1310
|
+
}
|
|
1311
|
+
var LINE_BREAK = /(\r\n|\n|\r)/;
|
|
1312
|
+
function scanAndRedactSecrets(content, rules, filePath) {
|
|
1313
|
+
const findings = [];
|
|
1314
|
+
let redacted = content;
|
|
1315
|
+
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
1316
|
+
blockPattern.lastIndex = 0;
|
|
1317
|
+
let match;
|
|
1318
|
+
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
1319
|
+
if (match[0].length === 0) {
|
|
1320
|
+
blockPattern.lastIndex += 1;
|
|
1321
|
+
continue;
|
|
1322
|
+
}
|
|
1323
|
+
const line = redacted.slice(0, match.index).split("\n").length;
|
|
1324
|
+
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
1325
|
+
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
1326
|
+
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
1327
|
+
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
const compiled = rules.map((rule) => ({
|
|
1331
|
+
rule: rule.rule,
|
|
1332
|
+
pattern: toGlobal(rule),
|
|
1333
|
+
...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
|
|
1334
|
+
}));
|
|
1335
|
+
const segments = redacted.split(LINE_BREAK);
|
|
1336
|
+
let lineNumber = 1;
|
|
1337
|
+
for (let index = 0; index < segments.length; index += 2) {
|
|
1338
|
+
const line = segments[index];
|
|
1339
|
+
if (line.length > 0) {
|
|
1340
|
+
const spans = [];
|
|
1341
|
+
const matchedRules = /* @__PURE__ */ new Set();
|
|
1342
|
+
for (const { rule, pattern, valueGroup } of compiled) {
|
|
1343
|
+
pattern.lastIndex = 0;
|
|
1344
|
+
let match;
|
|
1345
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
1346
|
+
if (match[0].length === 0) {
|
|
1347
|
+
pattern.lastIndex += 1;
|
|
1348
|
+
continue;
|
|
1349
|
+
}
|
|
1350
|
+
const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
|
|
1351
|
+
spans.push(
|
|
1352
|
+
group ? { start: group[0], end: group[1], rule } : { start: match.index, end: match.index + match[0].length, rule }
|
|
1353
|
+
);
|
|
1354
|
+
matchedRules.add(rule);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
if (spans.length > 0) {
|
|
1358
|
+
for (const rule of matchedRules) {
|
|
1359
|
+
findings.push({ rule, line: lineNumber, path: filePath });
|
|
1360
|
+
}
|
|
1361
|
+
segments[index] = applySpans(line, spans);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
if (index + 1 < segments.length) lineNumber += 1;
|
|
1365
|
+
}
|
|
1366
|
+
return { redacted: segments.join(""), findings };
|
|
1367
|
+
}
|
|
1368
|
+
var TOKEN_CHARS_LEFT = /[A-Za-z0-9_+/-]/;
|
|
1369
|
+
var TOKEN_CHARS_RIGHT = /[A-Za-z0-9_+/=-]/;
|
|
1370
|
+
function expandOverToken(line, start, end) {
|
|
1371
|
+
let left = start;
|
|
1372
|
+
let right = end;
|
|
1373
|
+
while (left > 0 && TOKEN_CHARS_LEFT.test(line[left - 1])) left -= 1;
|
|
1374
|
+
while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
|
|
1375
|
+
return { start: left, end: right };
|
|
1376
|
+
}
|
|
1377
|
+
function applySpans(line, spans) {
|
|
1378
|
+
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1379
|
+
const merged = [];
|
|
1380
|
+
for (const span of ordered) {
|
|
1381
|
+
const clampedStart = Math.max(0, Math.min(span.start, line.length));
|
|
1382
|
+
const clampedEnd = Math.max(clampedStart, Math.min(span.end, line.length));
|
|
1383
|
+
const { start, end } = expandOverToken(line, clampedStart, clampedEnd);
|
|
1384
|
+
const last = merged[merged.length - 1];
|
|
1385
|
+
if (last && start <= last.end) last.end = Math.max(last.end, end);
|
|
1386
|
+
else merged.push({ start, end });
|
|
1387
|
+
}
|
|
1388
|
+
let out = "";
|
|
1389
|
+
let cursor = 0;
|
|
1390
|
+
for (const span of merged) {
|
|
1391
|
+
out += line.slice(cursor, span.start);
|
|
1392
|
+
out += REDACTION_MARKER;
|
|
1393
|
+
cursor = span.end;
|
|
1394
|
+
}
|
|
1395
|
+
out += line.slice(cursor);
|
|
1396
|
+
return out;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// src/scanner/files.ts
|
|
1400
|
+
var PRESET_IGNORES = new Set(
|
|
1401
|
+
PRESETS.flatMap((preset) => preset.exclude ?? [])
|
|
1402
|
+
);
|
|
1403
|
+
function readLinkTarget(linkPath, fallback) {
|
|
1404
|
+
try {
|
|
1405
|
+
return fs9__namespace.readlinkSync(linkPath);
|
|
1406
|
+
} catch {
|
|
1407
|
+
return fallback;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
function isInside(root, candidate) {
|
|
1411
|
+
const relative5 = path10__namespace.relative(root, candidate);
|
|
1412
|
+
return relative5 === "" || !relative5.startsWith(`..${path10__namespace.sep}`) && relative5 !== ".." && !path10__namespace.isAbsolute(relative5);
|
|
1413
|
+
}
|
|
969
1414
|
function getGitInfo(cwd, fallbackBranch = "main") {
|
|
970
1415
|
try {
|
|
971
1416
|
const reported = childProcess__namespace.execSync("git rev-parse --abbrev-ref HEAD", {
|
|
@@ -1010,20 +1455,61 @@ var DEFAULT_IGNORED_FILES = [
|
|
|
1010
1455
|
"bun.lockb",
|
|
1011
1456
|
"composer.lock"
|
|
1012
1457
|
];
|
|
1013
|
-
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;
|
|
1458
|
+
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;
|
|
1014
1459
|
var SENSITIVE_PATH = /(?:^|\/)\.(?:aws|ssh|gnupg)\/|(?:^|\/)\.docker\/config\.json$|(?:^|\/)\.kube\/config$/i;
|
|
1015
1460
|
var SENSITIVE_EXT = /\.(?:pem|key|p12|pfx|jks|keystore|ppk|asc|kdbx|tfstate|tfvars)$/i;
|
|
1016
1461
|
var TEMPLATE_SUFFIX = /\.(?:example|sample|template|dist)(?=$|[.])/i;
|
|
1462
|
+
var BACKUP_SUFFIX = /(?:~|\.(?:bak|backup|old|orig|save|copy|swp|swo|tmp|part))$/i;
|
|
1463
|
+
function normaliseName(name) {
|
|
1464
|
+
let normalised = name.trim().replace(/[. ]+$/, "");
|
|
1465
|
+
let previous = "";
|
|
1466
|
+
while (previous !== normalised) {
|
|
1467
|
+
previous = normalised;
|
|
1468
|
+
normalised = normalised.replace(BACKUP_SUFFIX, "");
|
|
1469
|
+
}
|
|
1470
|
+
return normalised;
|
|
1471
|
+
}
|
|
1017
1472
|
var matchesSensitiveList = (posixPath, base) => SENSITIVE_BASENAME.test(base) || SENSITIVE_PATH.test(posixPath) || SENSITIVE_EXT.test(base);
|
|
1018
1473
|
function isSensitivePath(relativePath) {
|
|
1019
1474
|
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1020
|
-
const
|
|
1475
|
+
const raw = posixPath.split("/").pop() ?? posixPath;
|
|
1476
|
+
const base = normaliseName(raw);
|
|
1021
1477
|
if (TEMPLATE_SUFFIX.test(base)) {
|
|
1022
1478
|
const stripped = base.replace(new RegExp(TEMPLATE_SUFFIX.source, "gi"), "");
|
|
1023
1479
|
if (matchesSensitiveList(posixPath, stripped)) return false;
|
|
1024
1480
|
}
|
|
1025
1481
|
return matchesSensitiveList(posixPath, base);
|
|
1026
1482
|
}
|
|
1483
|
+
function findSecretInPath(relativePath) {
|
|
1484
|
+
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1485
|
+
const spans = [];
|
|
1486
|
+
const rules = [];
|
|
1487
|
+
for (const { rule, pattern } of DEFAULT_SECRET_PATTERNS) {
|
|
1488
|
+
const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
|
|
1489
|
+
let match;
|
|
1490
|
+
while ((match = scan.exec(posixPath)) !== null) {
|
|
1491
|
+
if (match[0].length === 0) {
|
|
1492
|
+
scan.lastIndex += 1;
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
spans.push({ start: match.index, end: match.index + match[0].length, rule });
|
|
1496
|
+
if (!rules.includes(rule)) rules.push(rule);
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
if (spans.length === 0) return null;
|
|
1500
|
+
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1501
|
+
let out = "";
|
|
1502
|
+
let cursor = 0;
|
|
1503
|
+
for (const span of spans) {
|
|
1504
|
+
if (span.end <= cursor) continue;
|
|
1505
|
+
const start = Math.max(span.start, cursor);
|
|
1506
|
+
out += posixPath.slice(cursor, start);
|
|
1507
|
+
out += REDACTION_MARKER;
|
|
1508
|
+
cursor = Math.min(span.end, posixPath.length);
|
|
1509
|
+
}
|
|
1510
|
+
out += posixPath.slice(cursor);
|
|
1511
|
+
return { rules, masked: out };
|
|
1512
|
+
}
|
|
1027
1513
|
function globToRegExp(glob) {
|
|
1028
1514
|
const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1029
1515
|
const anchored = normalized.startsWith("/");
|
|
@@ -1144,36 +1630,163 @@ function defaultIgnoreReason(relativePath) {
|
|
|
1144
1630
|
}
|
|
1145
1631
|
return null;
|
|
1146
1632
|
}
|
|
1147
|
-
function
|
|
1148
|
-
const includePatterns = options.includePatterns ?? [];
|
|
1149
|
-
const excludePatterns = options.excludePatterns ?? [];
|
|
1150
|
-
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
1151
|
-
let candidates;
|
|
1152
|
-
let walkSkips = [];
|
|
1633
|
+
function listIgnoredPaths(cwd) {
|
|
1153
1634
|
try {
|
|
1154
1635
|
const stdout = childProcess__namespace.execSync(
|
|
1155
|
-
"git ls-files -z --
|
|
1636
|
+
"git ls-files -z --others --ignored --exclude-standard --directory",
|
|
1156
1637
|
{
|
|
1157
1638
|
cwd,
|
|
1158
1639
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1159
1640
|
encoding: "utf8",
|
|
1160
|
-
maxBuffer:
|
|
1641
|
+
maxBuffer: 32 * 1024 * 1024
|
|
1161
1642
|
}
|
|
1162
1643
|
);
|
|
1163
|
-
|
|
1644
|
+
return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1164
1645
|
} catch {
|
|
1165
|
-
|
|
1166
|
-
candidates = walked.files;
|
|
1167
|
-
walkSkips = walked.skipped;
|
|
1646
|
+
return [];
|
|
1168
1647
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1648
|
+
}
|
|
1649
|
+
var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
|
|
1650
|
+
function isToolOwnedDirName(name) {
|
|
1651
|
+
return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
|
|
1652
|
+
}
|
|
1653
|
+
function hasSnapshotMarker(target) {
|
|
1654
|
+
try {
|
|
1655
|
+
return fs9__namespace.statSync(target).isDirectory() && fs9__namespace.existsSync(path10__namespace.join(target, ".codebase-snapshot"));
|
|
1656
|
+
} catch {
|
|
1657
|
+
return false;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
function makeToolOwnedDecider(cwd) {
|
|
1661
|
+
const markers = /* @__PURE__ */ new Map();
|
|
1662
|
+
return (relativePath) => {
|
|
1663
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
1664
|
+
for (let depth = 1; depth < parts.length; depth += 1) {
|
|
1665
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
1666
|
+
if (depth === 1 && isToolOwnedDirName(prefix)) return true;
|
|
1667
|
+
let hasMarker = markers.get(prefix);
|
|
1668
|
+
if (hasMarker === void 0) {
|
|
1669
|
+
hasMarker = fs9__namespace.existsSync(path10__namespace.join(cwd, prefix, ".codebase-snapshot"));
|
|
1670
|
+
markers.set(prefix, hasMarker);
|
|
1671
|
+
}
|
|
1672
|
+
if (hasMarker) return true;
|
|
1673
|
+
}
|
|
1674
|
+
return false;
|
|
1675
|
+
};
|
|
1676
|
+
}
|
|
1677
|
+
function makeLinkProbe() {
|
|
1678
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1679
|
+
return (absolutePath) => {
|
|
1680
|
+
let known = cache.get(absolutePath);
|
|
1681
|
+
if (known === void 0) {
|
|
1682
|
+
try {
|
|
1683
|
+
known = fs9__namespace.lstatSync(absolutePath).isSymbolicLink();
|
|
1684
|
+
} catch {
|
|
1685
|
+
known = false;
|
|
1686
|
+
}
|
|
1687
|
+
cache.set(absolutePath, known);
|
|
1688
|
+
}
|
|
1689
|
+
return known;
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
function dropLinkedDirectories(cwd, candidates, skipped) {
|
|
1693
|
+
const isLink = makeLinkProbe();
|
|
1694
|
+
let rootReal = cwd;
|
|
1695
|
+
try {
|
|
1696
|
+
rootReal = fs9__namespace.realpathSync(cwd);
|
|
1697
|
+
} catch {
|
|
1698
|
+
}
|
|
1699
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1700
|
+
const kept = [];
|
|
1701
|
+
for (const candidate of candidates) {
|
|
1702
|
+
const parts = candidate.split("/");
|
|
1703
|
+
let linkedDir = null;
|
|
1704
|
+
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
1705
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
1706
|
+
if (isLink(path10__namespace.join(cwd, prefix))) {
|
|
1707
|
+
linkedDir = prefix;
|
|
1708
|
+
break;
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
if (linkedDir === null) {
|
|
1712
|
+
kept.push(candidate);
|
|
1713
|
+
continue;
|
|
1714
|
+
}
|
|
1715
|
+
if (reported.has(linkedDir)) continue;
|
|
1716
|
+
reported.add(linkedDir);
|
|
1717
|
+
const absolute = path10__namespace.join(cwd, linkedDir);
|
|
1718
|
+
let targetReal = absolute;
|
|
1719
|
+
try {
|
|
1720
|
+
targetReal = fs9__namespace.realpathSync(absolute);
|
|
1721
|
+
} catch {
|
|
1722
|
+
}
|
|
1723
|
+
const target = readLinkTarget(absolute, targetReal);
|
|
1724
|
+
const what = linkedDir === candidate ? "symbolic link" : "directory link";
|
|
1725
|
+
skipped.push(
|
|
1726
|
+
isInside(rootReal, targetReal) ? {
|
|
1727
|
+
path: linkedDir,
|
|
1728
|
+
reason: "symlink",
|
|
1729
|
+
detail: `${what} not followed (target: ${target})`
|
|
1730
|
+
} : {
|
|
1731
|
+
path: linkedDir,
|
|
1732
|
+
reason: "outside-root",
|
|
1733
|
+
detail: `link target resolves outside the scanned root (target: ${target}); not followed`
|
|
1734
|
+
}
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
return kept;
|
|
1738
|
+
}
|
|
1739
|
+
function collectFiles(cwd, options = {}) {
|
|
1740
|
+
const includePatterns = options.includePatterns ?? [];
|
|
1741
|
+
const excludePatterns = options.excludePatterns ?? [];
|
|
1742
|
+
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
1743
|
+
const isToolOwned = makeToolOwnedDecider(cwd);
|
|
1744
|
+
let candidates;
|
|
1745
|
+
const skipped = [];
|
|
1746
|
+
try {
|
|
1747
|
+
const stdout = childProcess__namespace.execSync(
|
|
1748
|
+
"git ls-files -z --cached --others --exclude-standard",
|
|
1749
|
+
{
|
|
1750
|
+
cwd,
|
|
1751
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1752
|
+
encoding: "utf8",
|
|
1753
|
+
maxBuffer: 50 * 1024 * 1024
|
|
1754
|
+
}
|
|
1755
|
+
);
|
|
1756
|
+
candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1757
|
+
candidates = dropLinkedDirectories(cwd, candidates, skipped);
|
|
1758
|
+
const known = new Set(candidates);
|
|
1759
|
+
for (const ignoredPath of listIgnoredPaths(cwd)) {
|
|
1760
|
+
const trimmed = ignoredPath.replace(/\/+$/, "");
|
|
1761
|
+
if (known.has(trimmed) || known.has(ignoredPath)) continue;
|
|
1762
|
+
if (isToolOwned(trimmed)) continue;
|
|
1763
|
+
if (isSensitivePath(trimmed)) {
|
|
1764
|
+
skipped.push({
|
|
1765
|
+
path: trimmed,
|
|
1766
|
+
reason: "sensitive-file",
|
|
1767
|
+
detail: "credential-bearing file, ignored by .gitignore; enable includeSensitiveFiles to emit it"
|
|
1768
|
+
});
|
|
1769
|
+
} else {
|
|
1770
|
+
skipped.push({
|
|
1771
|
+
path: trimmed,
|
|
1772
|
+
reason: "gitignore",
|
|
1773
|
+
detail: ignoredPath.endsWith("/") ? "ignored by .gitignore (directory, not descended)" : "ignored by .gitignore"
|
|
1774
|
+
});
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
} catch {
|
|
1778
|
+
const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set(), true);
|
|
1779
|
+
candidates = walked.files;
|
|
1780
|
+
skipped.push(...walked.skipped);
|
|
1781
|
+
}
|
|
1782
|
+
const files = candidates.filter((file) => {
|
|
1783
|
+
if (isToolOwned(file)) return false;
|
|
1784
|
+
const ignored = defaultIgnoreReason(file);
|
|
1785
|
+
if (ignored) {
|
|
1786
|
+
skipped.push({ path: file, reason: "default-ignore", detail: ignored });
|
|
1787
|
+
return false;
|
|
1788
|
+
}
|
|
1789
|
+
if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesExclude(file, pattern))) {
|
|
1177
1790
|
skipped.push({
|
|
1178
1791
|
path: file,
|
|
1179
1792
|
reason: "excluded",
|
|
@@ -1183,10 +1796,11 @@ function collectFiles(cwd, options = {}) {
|
|
|
1183
1796
|
}
|
|
1184
1797
|
for (const pattern of excludePatterns) {
|
|
1185
1798
|
if (matchesExclude(file, pattern)) {
|
|
1799
|
+
const fromPreset = PRESET_IGNORES.has(pattern);
|
|
1186
1800
|
skipped.push({
|
|
1187
1801
|
path: file,
|
|
1188
|
-
reason: "excluded",
|
|
1189
|
-
detail: `matches exclude pattern "${pattern}"`
|
|
1802
|
+
reason: fromPreset ? "default-ignore" : "excluded",
|
|
1803
|
+
detail: fromPreset ? `preset ignore: "${pattern}"` : `matches exclude pattern "${pattern}"`
|
|
1190
1804
|
});
|
|
1191
1805
|
return false;
|
|
1192
1806
|
}
|
|
@@ -1199,11 +1813,20 @@ function collectFiles(cwd, options = {}) {
|
|
|
1199
1813
|
});
|
|
1200
1814
|
return false;
|
|
1201
1815
|
}
|
|
1816
|
+
const secret = findSecretInPath(file);
|
|
1817
|
+
if (secret) {
|
|
1818
|
+
skipped.push({
|
|
1819
|
+
path: secret.masked,
|
|
1820
|
+
reason: "sensitive-path",
|
|
1821
|
+
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`
|
|
1822
|
+
});
|
|
1823
|
+
return false;
|
|
1824
|
+
}
|
|
1202
1825
|
return true;
|
|
1203
1826
|
}).sort();
|
|
1204
1827
|
return { files, skipped };
|
|
1205
1828
|
}
|
|
1206
|
-
function walkDirectory(dir, baseDir, visited) {
|
|
1829
|
+
function walkDirectory(dir, baseDir, visited, isRoot = false) {
|
|
1207
1830
|
const results = { files: [], skipped: [] };
|
|
1208
1831
|
let realDir;
|
|
1209
1832
|
try {
|
|
@@ -1213,7 +1836,13 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1213
1836
|
}
|
|
1214
1837
|
if (visited.has(realDir)) return results;
|
|
1215
1838
|
visited.add(realDir);
|
|
1216
|
-
|
|
1839
|
+
let rootReal;
|
|
1840
|
+
try {
|
|
1841
|
+
rootReal = fs9__namespace.realpathSync(baseDir);
|
|
1842
|
+
} catch {
|
|
1843
|
+
rootReal = baseDir;
|
|
1844
|
+
}
|
|
1845
|
+
const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
|
|
1217
1846
|
let entries;
|
|
1218
1847
|
try {
|
|
1219
1848
|
entries = fs9__namespace.readdirSync(dir, { withFileTypes: true });
|
|
@@ -1222,30 +1851,79 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1222
1851
|
return results;
|
|
1223
1852
|
}
|
|
1224
1853
|
for (const entry of entries) {
|
|
1225
|
-
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) continue;
|
|
1226
1854
|
const fullPath = path10__namespace.join(dir, entry.name);
|
|
1227
1855
|
const relPath = path10__namespace.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
1228
|
-
if (
|
|
1856
|
+
if (isToolOwnedDirName(entry.name)) continue;
|
|
1857
|
+
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
|
|
1858
|
+
results.skipped.push({
|
|
1859
|
+
path: relPath,
|
|
1860
|
+
reason: "default-ignore",
|
|
1861
|
+
detail: `built-in ignore: directory "${entry.name}" (not descended)`
|
|
1862
|
+
});
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
|
|
1866
|
+
results.skipped.push({
|
|
1867
|
+
path: relPath,
|
|
1868
|
+
reason: "default-ignore",
|
|
1869
|
+
detail: "built-in ignore: filename"
|
|
1870
|
+
});
|
|
1871
|
+
continue;
|
|
1872
|
+
}
|
|
1873
|
+
if (ignoredByGitignore(relPath)) {
|
|
1874
|
+
if (!hasSnapshotMarker(fullPath)) {
|
|
1875
|
+
results.skipped.push({
|
|
1876
|
+
path: relPath,
|
|
1877
|
+
reason: isSensitivePath(relPath) ? "sensitive-file" : "gitignore",
|
|
1878
|
+
detail: "ignored by .gitignore"
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
continue;
|
|
1882
|
+
}
|
|
1229
1883
|
let isDirectory = entry.isDirectory();
|
|
1230
1884
|
let isFile = entry.isFile();
|
|
1231
1885
|
if (entry.isSymbolicLink()) {
|
|
1886
|
+
let targetReal;
|
|
1232
1887
|
try {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1888
|
+
targetReal = fs9__namespace.realpathSync(fullPath);
|
|
1889
|
+
} catch {
|
|
1890
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1891
|
+
continue;
|
|
1892
|
+
}
|
|
1893
|
+
let stats;
|
|
1894
|
+
try {
|
|
1895
|
+
stats = fs9__namespace.statSync(fullPath);
|
|
1896
|
+
} catch {
|
|
1897
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
|
|
1898
|
+
continue;
|
|
1899
|
+
}
|
|
1900
|
+
const linkTarget = readLinkTarget(fullPath, targetReal);
|
|
1901
|
+
if (!isInside(rootReal, targetReal)) {
|
|
1902
|
+
results.skipped.push({
|
|
1903
|
+
path: relPath,
|
|
1904
|
+
reason: "outside-root",
|
|
1905
|
+
detail: `link target resolves outside the scanned root (target: ${linkTarget}); not followed`
|
|
1906
|
+
});
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
if (stats.isDirectory()) {
|
|
1240
1910
|
results.skipped.push({
|
|
1241
1911
|
path: relPath,
|
|
1242
1912
|
reason: "symlink",
|
|
1243
|
-
detail:
|
|
1913
|
+
detail: `directory link not followed (target: ${linkTarget})`
|
|
1914
|
+
});
|
|
1915
|
+
continue;
|
|
1916
|
+
}
|
|
1917
|
+
if (!stats.isFile()) {
|
|
1918
|
+
results.skipped.push({
|
|
1919
|
+
path: relPath,
|
|
1920
|
+
reason: "symlink",
|
|
1921
|
+
detail: `link to neither a file nor a directory (target: ${linkTarget})`
|
|
1244
1922
|
});
|
|
1245
|
-
} catch {
|
|
1246
|
-
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1247
1923
|
continue;
|
|
1248
1924
|
}
|
|
1925
|
+
isDirectory = false;
|
|
1926
|
+
isFile = true;
|
|
1249
1927
|
}
|
|
1250
1928
|
if (isDirectory) {
|
|
1251
1929
|
const nested = walkDirectory(fullPath, baseDir, visited);
|
|
@@ -1258,195 +1936,6 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1258
1936
|
return results;
|
|
1259
1937
|
}
|
|
1260
1938
|
|
|
1261
|
-
// src/security/patterns.ts
|
|
1262
|
-
var DEFAULT_SECRET_PATTERNS = [
|
|
1263
|
-
{
|
|
1264
|
-
rule: "private-key-header",
|
|
1265
|
-
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
1266
|
-
},
|
|
1267
|
-
{
|
|
1268
|
-
rule: "jwt-token",
|
|
1269
|
-
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
1270
|
-
},
|
|
1271
|
-
{
|
|
1272
|
-
rule: "aws-access-key-id",
|
|
1273
|
-
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
1274
|
-
},
|
|
1275
|
-
{
|
|
1276
|
-
rule: "aws-secret-access-key",
|
|
1277
|
-
pattern: /(?:aws_secret_access_key|aws_secret_key|secret_access_key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']?/i
|
|
1278
|
-
},
|
|
1279
|
-
{
|
|
1280
|
-
rule: "github-token",
|
|
1281
|
-
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
1282
|
-
},
|
|
1283
|
-
{
|
|
1284
|
-
rule: "slack-token",
|
|
1285
|
-
pattern: /xox[baprs]-[0-9A-Za-z-]{10,250}/
|
|
1286
|
-
},
|
|
1287
|
-
{
|
|
1288
|
-
rule: "slack-webhook-url",
|
|
1289
|
-
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
1290
|
-
},
|
|
1291
|
-
{
|
|
1292
|
-
rule: "stripe-secret-key",
|
|
1293
|
-
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
1294
|
-
},
|
|
1295
|
-
{
|
|
1296
|
-
rule: "google-api-key",
|
|
1297
|
-
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
1298
|
-
},
|
|
1299
|
-
{
|
|
1300
|
-
rule: "google-oauth-client-secret",
|
|
1301
|
-
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
1302
|
-
},
|
|
1303
|
-
{
|
|
1304
|
-
rule: "openai-anthropic-key",
|
|
1305
|
-
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
1306
|
-
},
|
|
1307
|
-
{
|
|
1308
|
-
rule: "npm-token",
|
|
1309
|
-
pattern: /npm_[A-Za-z0-9]{36}/
|
|
1310
|
-
},
|
|
1311
|
-
{
|
|
1312
|
-
rule: "pypi-token",
|
|
1313
|
-
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
1314
|
-
},
|
|
1315
|
-
{
|
|
1316
|
-
rule: "sendgrid-key",
|
|
1317
|
-
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
1318
|
-
},
|
|
1319
|
-
{
|
|
1320
|
-
rule: "twilio-api-key",
|
|
1321
|
-
pattern: /SK[0-9a-fA-F]{32}/
|
|
1322
|
-
},
|
|
1323
|
-
{
|
|
1324
|
-
rule: "digitalocean-token",
|
|
1325
|
-
pattern: /dop_v1_[0-9a-f]{64}/
|
|
1326
|
-
},
|
|
1327
|
-
{
|
|
1328
|
-
rule: "azure-storage-account-key",
|
|
1329
|
-
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
1330
|
-
},
|
|
1331
|
-
{
|
|
1332
|
-
rule: "database-uri-credentials",
|
|
1333
|
-
pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/
|
|
1334
|
-
},
|
|
1335
|
-
{
|
|
1336
|
-
rule: "basic-auth-url",
|
|
1337
|
-
pattern: /(?:https?|ftp|sftp|ssh|git):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/i
|
|
1338
|
-
},
|
|
1339
|
-
{
|
|
1340
|
-
rule: "generic-secret-assignment",
|
|
1341
|
-
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["'][^"'\s]{8,256}["']/i
|
|
1342
|
-
},
|
|
1343
|
-
{
|
|
1344
|
-
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
1345
|
-
// was therefore invisible to the rule above. Anchored to the line start and
|
|
1346
|
-
// limited to secret-looking names to stay at zero false positives.
|
|
1347
|
-
//
|
|
1348
|
-
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
1349
|
-
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
1350
|
-
// every offset of a long identifier, which made this rule quadratic
|
|
1351
|
-
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
1352
|
-
rule: "env-secret-assignment",
|
|
1353
|
-
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
|
|
1354
|
-
}
|
|
1355
|
-
];
|
|
1356
|
-
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
1357
|
-
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
1358
|
-
|
|
1359
|
-
// src/security/redact.ts
|
|
1360
|
-
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
1361
|
-
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
1362
|
-
function compileSecretRules(rules) {
|
|
1363
|
-
return rules.map((rule) => {
|
|
1364
|
-
if (rule.pattern instanceof RegExp) {
|
|
1365
|
-
return {
|
|
1366
|
-
rule: rule.rule,
|
|
1367
|
-
pattern: new RegExp(rule.pattern.source, rule.pattern.flags.replace(/[gy]/g, ""))
|
|
1368
|
-
};
|
|
1369
|
-
}
|
|
1370
|
-
if (typeof rule.pattern === "string") {
|
|
1371
|
-
try {
|
|
1372
|
-
return { rule: rule.rule, pattern: new RegExp(rule.pattern, "i") };
|
|
1373
|
-
} catch (err) {
|
|
1374
|
-
throw new Error(
|
|
1375
|
-
`Invalid regular expression for secret rule "${rule.rule}": ${err.message}`
|
|
1376
|
-
);
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
throw new Error(
|
|
1380
|
-
`Secret rule "${String(rule.rule)}" has a pattern that is neither a RegExp nor a string.`
|
|
1381
|
-
);
|
|
1382
|
-
});
|
|
1383
|
-
}
|
|
1384
|
-
function toGlobal(pattern) {
|
|
1385
|
-
const flags = pattern.flags.replace(/[gy]/g, "");
|
|
1386
|
-
return new RegExp(pattern.source, `${flags}g`);
|
|
1387
|
-
}
|
|
1388
|
-
function scanAndRedactSecrets(content, rules, filePath) {
|
|
1389
|
-
const findings = [];
|
|
1390
|
-
let redacted = content;
|
|
1391
|
-
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
1392
|
-
blockPattern.lastIndex = 0;
|
|
1393
|
-
let match;
|
|
1394
|
-
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
1395
|
-
if (match[0].length === 0) {
|
|
1396
|
-
blockPattern.lastIndex += 1;
|
|
1397
|
-
continue;
|
|
1398
|
-
}
|
|
1399
|
-
const line = redacted.slice(0, match.index).split("\n").length;
|
|
1400
|
-
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
1401
|
-
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
1402
|
-
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
1403
|
-
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
1404
|
-
}
|
|
1405
|
-
}
|
|
1406
|
-
const compiled = rules.map((rule) => ({
|
|
1407
|
-
rule: rule.rule,
|
|
1408
|
-
pattern: toGlobal(rule.pattern)
|
|
1409
|
-
}));
|
|
1410
|
-
const lines = redacted.split("\n");
|
|
1411
|
-
const redactedLines = lines.map((line, index) => {
|
|
1412
|
-
if (line.length === 0) return line;
|
|
1413
|
-
const spans = [];
|
|
1414
|
-
const matchedRules = /* @__PURE__ */ new Set();
|
|
1415
|
-
for (const { rule, pattern } of compiled) {
|
|
1416
|
-
pattern.lastIndex = 0;
|
|
1417
|
-
let match;
|
|
1418
|
-
while ((match = pattern.exec(line)) !== null) {
|
|
1419
|
-
if (match[0].length === 0) {
|
|
1420
|
-
pattern.lastIndex += 1;
|
|
1421
|
-
continue;
|
|
1422
|
-
}
|
|
1423
|
-
spans.push({ start: match.index, end: match.index + match[0].length, rule });
|
|
1424
|
-
matchedRules.add(rule);
|
|
1425
|
-
}
|
|
1426
|
-
}
|
|
1427
|
-
if (spans.length === 0) return line;
|
|
1428
|
-
for (const rule of matchedRules) {
|
|
1429
|
-
findings.push({ rule, line: index + 1, path: filePath });
|
|
1430
|
-
}
|
|
1431
|
-
return applySpans(line, spans);
|
|
1432
|
-
});
|
|
1433
|
-
return { redacted: redactedLines.join("\n"), findings };
|
|
1434
|
-
}
|
|
1435
|
-
function applySpans(line, spans) {
|
|
1436
|
-
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1437
|
-
let out = "";
|
|
1438
|
-
let cursor = 0;
|
|
1439
|
-
for (const span of ordered) {
|
|
1440
|
-
if (span.end <= cursor) continue;
|
|
1441
|
-
const start = Math.max(span.start, cursor);
|
|
1442
|
-
out += line.slice(cursor, start);
|
|
1443
|
-
out += REDACTION_MARKER;
|
|
1444
|
-
cursor = Math.min(span.end, line.length);
|
|
1445
|
-
}
|
|
1446
|
-
out += line.slice(cursor);
|
|
1447
|
-
return out;
|
|
1448
|
-
}
|
|
1449
|
-
|
|
1450
1939
|
// src/config.ts
|
|
1451
1940
|
var CONFIG_FILENAMES = [
|
|
1452
1941
|
"snapshot.config.ts",
|
|
@@ -1496,7 +1985,12 @@ function normalizeResolvedConfig(config) {
|
|
|
1496
1985
|
),
|
|
1497
1986
|
include: resolveInclude(config.include),
|
|
1498
1987
|
preset: typeof config.preset === "string" ? config.preset : "generic",
|
|
1499
|
-
presetExclude:
|
|
1988
|
+
presetExclude: [
|
|
1989
|
+
.../* @__PURE__ */ new Set([
|
|
1990
|
+
...Array.isArray(config.presetExclude) ? config.presetExclude : [],
|
|
1991
|
+
...outputExcludePatterns(config.cwd, config.outDir, config.tmpDir)
|
|
1992
|
+
])
|
|
1993
|
+
],
|
|
1500
1994
|
layersMode: config.layersMode === "replace" ? "replace" : "merge",
|
|
1501
1995
|
budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
|
|
1502
1996
|
hooks: resolveHooks(config.hooks),
|
|
@@ -1533,7 +2027,68 @@ function supportsTypeScriptImports() {
|
|
|
1533
2027
|
return major > 22 || major === 22 && minor >= 6;
|
|
1534
2028
|
}
|
|
1535
2029
|
function stripJsonComments(raw) {
|
|
1536
|
-
|
|
2030
|
+
const text2 = raw.replace(/^\uFEFF/, "");
|
|
2031
|
+
let out = "";
|
|
2032
|
+
let inString = false;
|
|
2033
|
+
let escaped = false;
|
|
2034
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2035
|
+
const char = text2[index];
|
|
2036
|
+
if (inString) {
|
|
2037
|
+
out += char;
|
|
2038
|
+
if (escaped) escaped = false;
|
|
2039
|
+
else if (char === "\\") escaped = true;
|
|
2040
|
+
else if (char === '"') inString = false;
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
if (char === '"') {
|
|
2044
|
+
inString = true;
|
|
2045
|
+
out += char;
|
|
2046
|
+
continue;
|
|
2047
|
+
}
|
|
2048
|
+
if (char === "/" && text2[index + 1] === "/") {
|
|
2049
|
+
while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
|
|
2050
|
+
index -= 1;
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
if (char === "/" && text2[index + 1] === "*") {
|
|
2054
|
+
index += 2;
|
|
2055
|
+
while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
|
|
2056
|
+
if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
|
|
2057
|
+
index += 1;
|
|
2058
|
+
}
|
|
2059
|
+
index += 1;
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
out += char;
|
|
2063
|
+
}
|
|
2064
|
+
return stripTrailingCommas(out);
|
|
2065
|
+
}
|
|
2066
|
+
function stripTrailingCommas(text2) {
|
|
2067
|
+
let out = "";
|
|
2068
|
+
let inString = false;
|
|
2069
|
+
let escaped = false;
|
|
2070
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2071
|
+
const char = text2[index];
|
|
2072
|
+
if (inString) {
|
|
2073
|
+
out += char;
|
|
2074
|
+
if (escaped) escaped = false;
|
|
2075
|
+
else if (char === "\\") escaped = true;
|
|
2076
|
+
else if (char === '"') inString = false;
|
|
2077
|
+
continue;
|
|
2078
|
+
}
|
|
2079
|
+
if (char === '"') {
|
|
2080
|
+
inString = true;
|
|
2081
|
+
out += char;
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
if (char === ",") {
|
|
2085
|
+
let next = index + 1;
|
|
2086
|
+
while (next < text2.length && /\s/.test(text2[next])) next += 1;
|
|
2087
|
+
if (text2[next] === "}" || text2[next] === "]") continue;
|
|
2088
|
+
}
|
|
2089
|
+
out += char;
|
|
2090
|
+
}
|
|
2091
|
+
return out;
|
|
1537
2092
|
}
|
|
1538
2093
|
function parseJsonish(raw, file) {
|
|
1539
2094
|
try {
|
|
@@ -1542,6 +2097,47 @@ function parseJsonish(raw, file) {
|
|
|
1542
2097
|
throw new Error(`Could not parse ${file}: ${err.message}`);
|
|
1543
2098
|
}
|
|
1544
2099
|
}
|
|
2100
|
+
function describeValue2(value) {
|
|
2101
|
+
if (value === null) return "null";
|
|
2102
|
+
if (Array.isArray(value)) return "an array";
|
|
2103
|
+
if (value instanceof RegExp) return value.toString();
|
|
2104
|
+
if (typeof value === "function") return "a function";
|
|
2105
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
2106
|
+
if (typeof value === "object") {
|
|
2107
|
+
const keys = Object.keys(value);
|
|
2108
|
+
return keys.length > 0 ? `an object with keys ${keys.join(", ")}` : "an empty object";
|
|
2109
|
+
}
|
|
2110
|
+
return `${typeof value} (${String(value)})`;
|
|
2111
|
+
}
|
|
2112
|
+
var isThenable = (value) => typeof value.then === "function";
|
|
2113
|
+
function readConfigExport(exported, file) {
|
|
2114
|
+
if (exported === void 0 || exported === null) {
|
|
2115
|
+
throw new Error(
|
|
2116
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2117
|
+
);
|
|
2118
|
+
}
|
|
2119
|
+
if (Array.isArray(exported)) {
|
|
2120
|
+
throw new Error(
|
|
2121
|
+
`${file} exports an array; expected a single configuration object such as \`export default { outDir: "snapshot" }\`.`
|
|
2122
|
+
);
|
|
2123
|
+
}
|
|
2124
|
+
if (typeof exported === "function") {
|
|
2125
|
+
throw new Error(
|
|
2126
|
+
`${file} exports a function; expected a configuration object. Call the function yourself and export its result.`
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
if (typeof exported !== "object") {
|
|
2130
|
+
throw new Error(
|
|
2131
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2132
|
+
);
|
|
2133
|
+
}
|
|
2134
|
+
if (isThenable(exported)) {
|
|
2135
|
+
throw new Error(
|
|
2136
|
+
`${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;\`).`
|
|
2137
|
+
);
|
|
2138
|
+
}
|
|
2139
|
+
return exported;
|
|
2140
|
+
}
|
|
1545
2141
|
async function loadConfigFileDetailed(cwd) {
|
|
1546
2142
|
const warnings = [];
|
|
1547
2143
|
const errors = [];
|
|
@@ -1571,13 +2167,7 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
1571
2167
|
}
|
|
1572
2168
|
try {
|
|
1573
2169
|
const imported = await import(url.pathToFileURL(fullPath).href);
|
|
1574
|
-
const config = imported.default ?? imported;
|
|
1575
|
-
if (!config || typeof config !== "object") {
|
|
1576
|
-
const message = `${fullPath} does not export a configuration object.`;
|
|
1577
|
-
warnings.push(message);
|
|
1578
|
-
errors.push(message);
|
|
1579
|
-
continue;
|
|
1580
|
-
}
|
|
2170
|
+
const config = readConfigExport(imported.default ?? imported, fullPath);
|
|
1581
2171
|
return { config, file: fullPath, warnings, errors };
|
|
1582
2172
|
} catch (err) {
|
|
1583
2173
|
const message = `Could not load ${fullPath}: ${err.message}`;
|
|
@@ -1627,7 +2217,7 @@ var DEFAULT_ARTIFACTS = {
|
|
|
1627
2217
|
function isArtifactName(value) {
|
|
1628
2218
|
return ARTIFACT_NAMES.includes(value);
|
|
1629
2219
|
}
|
|
1630
|
-
function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
2220
|
+
function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
|
|
1631
2221
|
const artifacts = {
|
|
1632
2222
|
...DEFAULT_ARTIFACTS,
|
|
1633
2223
|
// `includeMonolith` predates this selection and stays the switch for it.
|
|
@@ -1649,6 +2239,9 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
1649
2239
|
artifacts[name] = value;
|
|
1650
2240
|
}
|
|
1651
2241
|
}
|
|
2242
|
+
if (monolithOverride !== void 0) {
|
|
2243
|
+
artifacts.monolith = monolithOverride;
|
|
2244
|
+
}
|
|
1652
2245
|
for (const name of skipped ?? []) {
|
|
1653
2246
|
if (!isArtifactName(name)) {
|
|
1654
2247
|
throw new Error(
|
|
@@ -1659,9 +2252,106 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
1659
2252
|
}
|
|
1660
2253
|
return artifacts;
|
|
1661
2254
|
}
|
|
1662
|
-
function
|
|
1663
|
-
if (
|
|
1664
|
-
|
|
2255
|
+
function strictPositiveInteger(value, fallback, label) {
|
|
2256
|
+
if (value === void 0) return fallback;
|
|
2257
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
2258
|
+
throw new Error(`${label} must be a positive integer (received ${describeValue2(value)}).`);
|
|
2259
|
+
}
|
|
2260
|
+
return value;
|
|
2261
|
+
}
|
|
2262
|
+
function strictBoolean(value, fallback, label) {
|
|
2263
|
+
if (value === void 0) return fallback;
|
|
2264
|
+
if (typeof value !== "boolean") {
|
|
2265
|
+
throw new Error(`${label} must be true or false (received ${describeValue2(value)}).`);
|
|
2266
|
+
}
|
|
2267
|
+
return value;
|
|
2268
|
+
}
|
|
2269
|
+
function strictString(value, fallback, label) {
|
|
2270
|
+
if (value === void 0) return fallback;
|
|
2271
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2272
|
+
throw new Error(`${label} must be a non-empty string (received ${describeValue2(value)}).`);
|
|
2273
|
+
}
|
|
2274
|
+
return value;
|
|
2275
|
+
}
|
|
2276
|
+
function resolveExcludePatterns(value) {
|
|
2277
|
+
if (value === void 0) return [];
|
|
2278
|
+
if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string")) {
|
|
2279
|
+
throw new Error(
|
|
2280
|
+
`\`excludePatterns\` must be an array of glob strings such as ["**/*.test.ts"] (received ${describeValue2(value)}).`
|
|
2281
|
+
);
|
|
2282
|
+
}
|
|
2283
|
+
return value.map((pattern) => pattern.trim()).filter(Boolean);
|
|
2284
|
+
}
|
|
2285
|
+
var AUTO_PRESET_ALIASES = /* @__PURE__ */ new Set(["auto", "detected"]);
|
|
2286
|
+
function resolvePresetName(value) {
|
|
2287
|
+
if (value === void 0) return void 0;
|
|
2288
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2289
|
+
throw new Error(
|
|
2290
|
+
`\`preset\` must be a preset name (received ${describeValue2(value)}). Use "detected" (or "auto") to detect one, or one of: ${listPresetNames().join(", ")}.`
|
|
2291
|
+
);
|
|
2292
|
+
}
|
|
2293
|
+
const name = value.trim();
|
|
2294
|
+
return AUTO_PRESET_ALIASES.has(name.toLowerCase()) ? void 0 : name;
|
|
2295
|
+
}
|
|
2296
|
+
function resolveRuleList(value, kind) {
|
|
2297
|
+
const collection = kind === "layer" ? "layers" : "kinds";
|
|
2298
|
+
const example = kind === "layer" ? `{ name: "api", match: "src/api/**" }` : `{ kind: "route", match: "src/routes/**" }`;
|
|
2299
|
+
if (value === void 0) return [];
|
|
2300
|
+
if (!Array.isArray(value)) {
|
|
2301
|
+
throw new Error(
|
|
2302
|
+
`\`${collection}\` must be an array of ${kind} rules (received ${describeValue2(value)}). A ${kind} rule is ${example}.`
|
|
2303
|
+
);
|
|
2304
|
+
}
|
|
2305
|
+
return value.map((entry, index) => {
|
|
2306
|
+
const where = `${collection}[${index}]`;
|
|
2307
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
2308
|
+
throw new Error(`${where} must be an object (received ${describeValue2(entry)}). A ${kind} rule is ${example}.`);
|
|
2309
|
+
}
|
|
2310
|
+
const record = entry;
|
|
2311
|
+
const keys = Object.keys(record);
|
|
2312
|
+
const canonical = kind === "layer" ? "name" : "kind";
|
|
2313
|
+
const alias = kind === "layer" ? "layer" : "name";
|
|
2314
|
+
const rawName = typeof record[canonical] === "string" ? record[canonical] : typeof record[alias] === "string" ? record[alias] : void 0;
|
|
2315
|
+
if (rawName === void 0 || rawName.trim() === "") {
|
|
2316
|
+
throw new Error(
|
|
2317
|
+
`${where} is missing \`${canonical}\` (received keys: ${keys.join(", ") || "none"}). A ${kind} rule is ${example}; \`${alias}\` is accepted as an alias of \`${canonical}\`.`
|
|
2318
|
+
);
|
|
2319
|
+
}
|
|
2320
|
+
const match = record.match;
|
|
2321
|
+
if (match === void 0) {
|
|
2322
|
+
throw new Error(
|
|
2323
|
+
`${where}.match is required (received keys: ${keys.join(", ") || "none"}). Use a glob string ("src/api/**"), a RegExp (/\\\\.tsx$/) or a function.`
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
if (!(typeof match === "string" || match instanceof RegExp || typeof match === "function")) {
|
|
2327
|
+
throw new Error(`${where}.match must be a string, a RegExp or a function (received ${describeValue2(match)}).`);
|
|
2328
|
+
}
|
|
2329
|
+
if (typeof match === "string" && match.trim() === "") {
|
|
2330
|
+
throw new Error(`${where}.match must not be an empty string.`);
|
|
2331
|
+
}
|
|
2332
|
+
return {
|
|
2333
|
+
name: rawName.trim(),
|
|
2334
|
+
match
|
|
2335
|
+
};
|
|
2336
|
+
});
|
|
2337
|
+
}
|
|
2338
|
+
function resolveLayerRules(value) {
|
|
2339
|
+
return resolveRuleList(value, "layer").map((rule) => ({ name: rule.name, match: rule.match }));
|
|
2340
|
+
}
|
|
2341
|
+
function resolveKindRules(value) {
|
|
2342
|
+
return resolveRuleList(value, "kind").map((rule) => ({ kind: rule.name, match: rule.match }));
|
|
2343
|
+
}
|
|
2344
|
+
function outputExcludePatterns(cwd, outDir, tmpDir) {
|
|
2345
|
+
const patterns = [];
|
|
2346
|
+
for (const dir of [outDir, tmpDir]) {
|
|
2347
|
+
const absolute = path10__namespace.resolve(cwd, dir);
|
|
2348
|
+
const relative5 = path10__namespace.relative(cwd, absolute);
|
|
2349
|
+
if (!relative5 || relative5 === ".." || relative5.startsWith(`..${path10__namespace.sep}`) || path10__namespace.isAbsolute(relative5)) {
|
|
2350
|
+
continue;
|
|
2351
|
+
}
|
|
2352
|
+
patterns.push(`${relative5.replace(/\\/g, "/")}/**`);
|
|
2353
|
+
}
|
|
2354
|
+
return patterns;
|
|
1665
2355
|
}
|
|
1666
2356
|
var BYTE_UNITS = {
|
|
1667
2357
|
b: 1,
|
|
@@ -1730,11 +2420,13 @@ function resolveLayersMode(mode) {
|
|
|
1730
2420
|
}
|
|
1731
2421
|
return mode;
|
|
1732
2422
|
}
|
|
1733
|
-
function resolveBudgets(budgets, fallback) {
|
|
2423
|
+
function resolveBudgets(budgets, fallback, overrideDefault) {
|
|
1734
2424
|
const byLayer = {};
|
|
1735
2425
|
if (budgets === void 0) return { default: fallback, byLayer };
|
|
1736
2426
|
if (typeof budgets !== "object" || budgets === null || Array.isArray(budgets)) {
|
|
1737
|
-
throw new Error(
|
|
2427
|
+
throw new Error(
|
|
2428
|
+
`\`budgets\` must be an object such as { default: "120k", byLayer: { api: "60k" } } (received ${describeValue2(budgets)}).`
|
|
2429
|
+
);
|
|
1738
2430
|
}
|
|
1739
2431
|
if (budgets.byLayer !== void 0) {
|
|
1740
2432
|
if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
|
|
@@ -1749,10 +2441,12 @@ function resolveBudgets(budgets, fallback) {
|
|
|
1749
2441
|
);
|
|
1750
2442
|
}
|
|
1751
2443
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
2444
|
+
const resolvedDefault = boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default");
|
|
2445
|
+
if (!overrideDefault) return { default: resolvedDefault, byLayer };
|
|
2446
|
+
for (const [layer2, value] of Object.entries(byLayer)) {
|
|
2447
|
+
byLayer[layer2] = Math.min(value, fallback);
|
|
2448
|
+
}
|
|
2449
|
+
return { default: fallback, byLayer };
|
|
1756
2450
|
}
|
|
1757
2451
|
function resolveHooks(hooks) {
|
|
1758
2452
|
if (hooks === void 0) return {};
|
|
@@ -1765,6 +2459,11 @@ function resolveHooks(hooks) {
|
|
|
1765
2459
|
return { ...hooks };
|
|
1766
2460
|
}
|
|
1767
2461
|
function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
2462
|
+
if (userConfig?.cwd !== void 0 && (typeof userConfig.cwd !== "string" || userConfig.cwd.trim() === "")) {
|
|
2463
|
+
throw new Error(
|
|
2464
|
+
`\`cwd\` must be a non-empty path string (received ${describeValue2(userConfig.cwd)}).`
|
|
2465
|
+
);
|
|
2466
|
+
}
|
|
1768
2467
|
const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
|
|
1769
2468
|
const resolved = path10__namespace.resolve(candidate);
|
|
1770
2469
|
if (!fs9__namespace.existsSync(resolved)) {
|
|
@@ -1777,55 +2476,106 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
|
1777
2476
|
}
|
|
1778
2477
|
function resolveConfig(userConfig, cliOptions, baseDir) {
|
|
1779
2478
|
const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
|
|
1780
|
-
const forcedPreset = userConfig?.preset;
|
|
2479
|
+
const forcedPreset = resolvePresetName(userConfig?.preset);
|
|
1781
2480
|
if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
|
|
1782
2481
|
throw new Error(unknownPresetMessage(forcedPreset));
|
|
1783
2482
|
}
|
|
1784
2483
|
const preset = detectPreset(cwd, forcedPreset);
|
|
1785
2484
|
const layersMode = resolveLayersMode(userConfig?.layersMode);
|
|
1786
|
-
const
|
|
1787
|
-
const
|
|
1788
|
-
const
|
|
1789
|
-
const
|
|
1790
|
-
const
|
|
1791
|
-
|
|
2485
|
+
const userLayers = resolveLayerRules(userConfig?.layers);
|
|
2486
|
+
const userKinds = resolveKindRules(userConfig?.kinds);
|
|
2487
|
+
const layers = userLayers.length > 0 ? layersMode === "replace" ? userLayers : [...userLayers, ...preset.layers] : preset.layers;
|
|
2488
|
+
const kinds = userKinds.length > 0 ? layersMode === "replace" ? userKinds : [...userKinds, ...preset.kinds] : preset.kinds;
|
|
2489
|
+
const secrets = userConfig?.secrets;
|
|
2490
|
+
if (secrets !== void 0 && (typeof secrets !== "object" || Array.isArray(secrets))) {
|
|
2491
|
+
throw new Error(
|
|
2492
|
+
`\`secrets\` must be an object of { customPatterns, disableDefaultPatterns } (received ${describeValue2(secrets)}).`
|
|
2493
|
+
);
|
|
2494
|
+
}
|
|
2495
|
+
const customRules = secrets?.customPatterns ?? [];
|
|
2496
|
+
if (!Array.isArray(customRules)) {
|
|
2497
|
+
throw new Error(
|
|
2498
|
+
`\`secrets.customPatterns\` must be an array of secret rules (received ${describeValue2(customRules)}). Each entry is a RegExp, a string, or { rule, pattern }.`
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
const disableDefaultPatterns = strictBoolean(
|
|
2502
|
+
secrets?.disableDefaultPatterns,
|
|
2503
|
+
false,
|
|
2504
|
+
"`secrets.disableDefaultPatterns`"
|
|
2505
|
+
);
|
|
2506
|
+
const aliasedRules = userConfig?.secretRules;
|
|
2507
|
+
if (aliasedRules !== void 0 && !Array.isArray(aliasedRules)) {
|
|
2508
|
+
throw new Error(
|
|
2509
|
+
`\`secretRules\` must be an array of { rule, pattern } entries (received ${describeValue2(aliasedRules)}).`
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
const compiledCustomRules = compileSecretRules([
|
|
2513
|
+
...customRules,
|
|
2514
|
+
...aliasedRules ?? []
|
|
2515
|
+
]);
|
|
2516
|
+
const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
|
|
2517
|
+
const cliMonolith = cliOptions?.includeMonolith;
|
|
2518
|
+
const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
|
|
1792
2519
|
const artifacts = resolveArtifacts(
|
|
1793
2520
|
userConfig?.artifacts,
|
|
1794
2521
|
cliOptions?.skipArtifacts,
|
|
1795
|
-
includeMonolith
|
|
2522
|
+
includeMonolith,
|
|
2523
|
+
cliMonolith
|
|
1796
2524
|
);
|
|
2525
|
+
const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
|
|
1797
2526
|
const maxPartBytes = boundedBytes(
|
|
1798
|
-
|
|
2527
|
+
explicitMaxPartBytes,
|
|
1799
2528
|
300 * 1024,
|
|
1800
2529
|
MIN_MAX_PART_BYTES,
|
|
1801
2530
|
"maxPartBytes"
|
|
1802
2531
|
);
|
|
1803
|
-
const budgets = resolveBudgets(
|
|
2532
|
+
const budgets = resolveBudgets(
|
|
2533
|
+
userConfig?.budgets,
|
|
2534
|
+
maxPartBytes,
|
|
2535
|
+
explicitMaxPartBytes !== void 0
|
|
2536
|
+
);
|
|
2537
|
+
const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
|
|
2538
|
+
const outDir = cliOptions?.outDir || configuredOutDir;
|
|
2539
|
+
const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
|
|
2540
|
+
const branch = strictString(userConfig?.branch, "main", "`branch`");
|
|
2541
|
+
const configuredSensitiveFiles = strictBoolean(
|
|
2542
|
+
userConfig?.includeSensitiveFiles,
|
|
2543
|
+
false,
|
|
2544
|
+
"`includeSensitiveFiles`"
|
|
2545
|
+
);
|
|
2546
|
+
const includeSensitiveFiles = strictBoolean(
|
|
2547
|
+
cliOptions?.includeSensitiveFiles,
|
|
2548
|
+
configuredSensitiveFiles,
|
|
2549
|
+
"`includeSensitiveFiles`"
|
|
2550
|
+
);
|
|
2551
|
+
const presetExclude = [
|
|
2552
|
+
.../* @__PURE__ */ new Set([...preset.exclude, ...outputExcludePatterns(cwd, outDir, tmpDir)])
|
|
2553
|
+
];
|
|
1804
2554
|
return markResolvedConfig({
|
|
1805
2555
|
cwd,
|
|
1806
|
-
branch
|
|
1807
|
-
outDir
|
|
1808
|
-
tmpDir
|
|
2556
|
+
branch,
|
|
2557
|
+
outDir,
|
|
2558
|
+
tmpDir,
|
|
1809
2559
|
maxPartBytes,
|
|
1810
2560
|
largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
|
|
1811
|
-
previewHeadLines:
|
|
1812
|
-
previewTailLines:
|
|
2561
|
+
previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
|
|
2562
|
+
previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
|
|
1813
2563
|
includeMonolith: artifacts.monolith,
|
|
1814
2564
|
maxMonolithBytes: optionalBytes(
|
|
1815
2565
|
cliOptions?.maxMonolithBytes !== void 0 ? cliOptions.maxMonolithBytes : userConfig?.maxMonolithBytes,
|
|
1816
2566
|
"maxMonolithBytes"
|
|
1817
2567
|
),
|
|
1818
|
-
includeSensitiveFiles
|
|
2568
|
+
includeSensitiveFiles,
|
|
1819
2569
|
include: resolveInclude(userConfig?.include),
|
|
1820
2570
|
preset: preset.presetName,
|
|
1821
|
-
presetExclude
|
|
2571
|
+
presetExclude,
|
|
1822
2572
|
artifacts,
|
|
1823
2573
|
layersMode,
|
|
1824
2574
|
budgets,
|
|
1825
2575
|
hooks: resolveHooks(userConfig?.hooks),
|
|
1826
2576
|
layers,
|
|
1827
2577
|
kinds,
|
|
1828
|
-
excludePatterns: userConfig?.excludePatterns
|
|
2578
|
+
excludePatterns: resolveExcludePatterns(userConfig?.excludePatterns),
|
|
1829
2579
|
secretRules
|
|
1830
2580
|
});
|
|
1831
2581
|
}
|
|
@@ -1904,6 +2654,75 @@ function truncatePreview(content, headLines, tailLines) {
|
|
|
1904
2654
|
includedLines: headLines + tailLines + 1
|
|
1905
2655
|
};
|
|
1906
2656
|
}
|
|
2657
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2658
|
+
".png",
|
|
2659
|
+
".jpg",
|
|
2660
|
+
".jpeg",
|
|
2661
|
+
".gif",
|
|
2662
|
+
".webp",
|
|
2663
|
+
".avif",
|
|
2664
|
+
".ico",
|
|
2665
|
+
".bmp",
|
|
2666
|
+
".tiff",
|
|
2667
|
+
".pdf",
|
|
2668
|
+
".zip",
|
|
2669
|
+
".tar",
|
|
2670
|
+
".gz",
|
|
2671
|
+
".tgz",
|
|
2672
|
+
".bz2",
|
|
2673
|
+
".xz",
|
|
2674
|
+
".7z",
|
|
2675
|
+
".rar",
|
|
2676
|
+
".mp3",
|
|
2677
|
+
".mp4",
|
|
2678
|
+
".wav",
|
|
2679
|
+
".ogg",
|
|
2680
|
+
".webm",
|
|
2681
|
+
".mov",
|
|
2682
|
+
".avi",
|
|
2683
|
+
".woff",
|
|
2684
|
+
".woff2",
|
|
2685
|
+
".ttf",
|
|
2686
|
+
".otf",
|
|
2687
|
+
".eot",
|
|
2688
|
+
".wasm",
|
|
2689
|
+
".node",
|
|
2690
|
+
".exe",
|
|
2691
|
+
".dll",
|
|
2692
|
+
".dylib",
|
|
2693
|
+
".so"
|
|
2694
|
+
]);
|
|
2695
|
+
function isBinaryExtension(filePath) {
|
|
2696
|
+
const ext = path10__namespace.extname(filePath).toLowerCase();
|
|
2697
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
2698
|
+
}
|
|
2699
|
+
function isBinaryBuffer(buffer) {
|
|
2700
|
+
const checkLength = Math.min(buffer.length, 8192);
|
|
2701
|
+
for (let index = 0; index < checkLength; index++) {
|
|
2702
|
+
if (buffer[index] === 0) return true;
|
|
2703
|
+
}
|
|
2704
|
+
return false;
|
|
2705
|
+
}
|
|
2706
|
+
function isBinaryFile(absolutePath) {
|
|
2707
|
+
if (isBinaryExtension(absolutePath)) return true;
|
|
2708
|
+
let fd;
|
|
2709
|
+
try {
|
|
2710
|
+
fd = fs9__namespace.openSync(absolutePath, "r");
|
|
2711
|
+
const buffer = Buffer.alloc(8192);
|
|
2712
|
+
const bytesRead = fs9__namespace.readSync(fd, buffer, 0, 8192, 0);
|
|
2713
|
+
if (bytesRead === 0) return false;
|
|
2714
|
+
return isBinaryBuffer(buffer.subarray(0, bytesRead));
|
|
2715
|
+
} catch {
|
|
2716
|
+
return false;
|
|
2717
|
+
} finally {
|
|
2718
|
+
if (fd !== void 0) {
|
|
2719
|
+
try {
|
|
2720
|
+
fs9__namespace.closeSync(fd);
|
|
2721
|
+
} catch {
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
1907
2726
|
var LANGUAGE_BY_EXTENSION = {
|
|
1908
2727
|
".ts": "typescript",
|
|
1909
2728
|
".mts": "typescript",
|
|
@@ -1970,16 +2789,15 @@ function describeMatcher(matcher) {
|
|
|
1970
2789
|
if (typeof matcher === "string") return matcher;
|
|
1971
2790
|
if (matcher instanceof RegExp) return String(matcher);
|
|
1972
2791
|
if (typeof matcher === "function") {
|
|
1973
|
-
|
|
1974
|
-
return source.length > 70 ? `${source.slice(0, 67)}...` : source;
|
|
2792
|
+
return "function (custom predicate)";
|
|
1975
2793
|
}
|
|
1976
2794
|
return String(matcher);
|
|
1977
2795
|
}
|
|
1978
2796
|
function normalizeTarget(cwd, target) {
|
|
1979
2797
|
const absolute = path10__namespace.resolve(cwd, target);
|
|
1980
|
-
const
|
|
1981
|
-
if (
|
|
1982
|
-
return
|
|
2798
|
+
const relative5 = path10__namespace.relative(cwd, absolute);
|
|
2799
|
+
if (relative5.startsWith("..") || path10__namespace.isAbsolute(relative5)) return target;
|
|
2800
|
+
return relative5.split(path10__namespace.sep).join("/");
|
|
1983
2801
|
}
|
|
1984
2802
|
function findRuleMatch(rules, relativePath) {
|
|
1985
2803
|
return rules.find(
|
|
@@ -1995,10 +2813,10 @@ function readManifest2(config) {
|
|
|
1995
2813
|
}
|
|
1996
2814
|
}
|
|
1997
2815
|
function explainPath(config, target) {
|
|
1998
|
-
const
|
|
1999
|
-
const absolute = path10__namespace.join(config.cwd,
|
|
2816
|
+
const relative5 = normalizeTarget(config.cwd, target);
|
|
2817
|
+
const absolute = path10__namespace.join(config.cwd, relative5);
|
|
2000
2818
|
const result = {
|
|
2001
|
-
path:
|
|
2819
|
+
path: relative5,
|
|
2002
2820
|
exists: fs9__namespace.existsSync(absolute),
|
|
2003
2821
|
included: false
|
|
2004
2822
|
};
|
|
@@ -2007,8 +2825,8 @@ function explainPath(config, target) {
|
|
|
2007
2825
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
2008
2826
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
2009
2827
|
});
|
|
2010
|
-
if (!collection.files.includes(
|
|
2011
|
-
const record = collection.skipped.find((entry2) => entry2.path ===
|
|
2828
|
+
if (!collection.files.includes(relative5)) {
|
|
2829
|
+
const record = collection.skipped.find((entry2) => entry2.path === relative5);
|
|
2012
2830
|
if (record) {
|
|
2013
2831
|
result.reason = record.reason;
|
|
2014
2832
|
result.detail = record.detail;
|
|
@@ -2022,24 +2840,30 @@ function explainPath(config, target) {
|
|
|
2022
2840
|
return result;
|
|
2023
2841
|
}
|
|
2024
2842
|
result.included = true;
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2843
|
+
if (isBinaryFile(absolute)) {
|
|
2844
|
+
result.included = false;
|
|
2845
|
+
result.reason = "binary";
|
|
2846
|
+
result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
|
|
2847
|
+
return result;
|
|
2848
|
+
}
|
|
2849
|
+
result.layer = classifyLayer(relative5, config.layers);
|
|
2850
|
+
result.kind = classifyKind(relative5, config.kinds);
|
|
2851
|
+
result.language = detectLanguage(relative5);
|
|
2852
|
+
const layerRule = findRuleMatch(config.layers, relative5);
|
|
2029
2853
|
if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
|
|
2030
|
-
const kindRule = findRuleMatch(config.kinds,
|
|
2854
|
+
const kindRule = findRuleMatch(config.kinds, relative5);
|
|
2031
2855
|
if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
|
|
2032
2856
|
if (fs9__namespace.existsSync(absolute) && fs9__namespace.statSync(absolute).isFile()) {
|
|
2033
2857
|
const buffer = fs9__namespace.readFileSync(absolute);
|
|
2034
2858
|
const text2 = buffer.toString("utf8");
|
|
2035
2859
|
result.bytes = buffer.length;
|
|
2036
2860
|
result.lines = countLines(text2);
|
|
2037
|
-
const { findings } = scanAndRedactSecrets(text2, config.secretRules,
|
|
2861
|
+
const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative5);
|
|
2038
2862
|
result.secretCount = findings.length;
|
|
2039
2863
|
result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
|
|
2040
2864
|
}
|
|
2041
2865
|
const manifest = readManifest2(config);
|
|
2042
|
-
const entry = manifest?.files.find((file) => file.relativePath ===
|
|
2866
|
+
const entry = manifest?.files.find((file) => file.relativePath === relative5);
|
|
2043
2867
|
result.part = entry?.part ?? null;
|
|
2044
2868
|
result.inclusionMode = entry?.inclusionMode;
|
|
2045
2869
|
return result;
|
|
@@ -2163,7 +2987,7 @@ function renderConfig(answers) {
|
|
|
2163
2987
|
secrets: {
|
|
2164
2988
|
// [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
|
|
2165
2989
|
customPatterns: [],
|
|
2166
|
-
// Set true to drop the
|
|
2990
|
+
// Set true to drop the built-in rules above and keep only customPatterns.
|
|
2167
2991
|
disableDefaultPatterns: false,
|
|
2168
2992
|
},
|
|
2169
2993
|
|
|
@@ -2179,6 +3003,10 @@ function renderConfig(answers) {
|
|
|
2179
3003
|
},
|
|
2180
3004
|
// Skip the monolith above this size. null = no limit.
|
|
2181
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},
|
|
2182
3010
|
|
|
2183
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
|
|
2184
3012
|
// afterGenerate: (manifest) => { /* write your own artifact */ }
|
|
@@ -2500,6 +3328,14 @@ function existingConfig(cwd) {
|
|
|
2500
3328
|
}
|
|
2501
3329
|
return null;
|
|
2502
3330
|
}
|
|
3331
|
+
function packageJsonHasConfig(cwd) {
|
|
3332
|
+
try {
|
|
3333
|
+
const parsed = JSON.parse(fs9__namespace.readFileSync(path10__namespace.join(cwd, "package.json"), "utf8"));
|
|
3334
|
+
return typeof parsed.snapshot === "object" && parsed.snapshot !== null;
|
|
3335
|
+
} catch {
|
|
3336
|
+
return false;
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
2503
3339
|
function artifactAnswers(options) {
|
|
2504
3340
|
const artifacts = { ...DEFAULT_ANSWERS.artifacts };
|
|
2505
3341
|
for (const name of options.skipArtifacts ?? []) {
|
|
@@ -2712,6 +3548,11 @@ async function runInit(options) {
|
|
|
2712
3548
|
`A configuration already exists (${present}). Re-run with --force to replace it.`
|
|
2713
3549
|
);
|
|
2714
3550
|
}
|
|
3551
|
+
if (packageJsonHasConfig(options.cwd) && !options.force) {
|
|
3552
|
+
throw new Error(
|
|
3553
|
+
'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.'
|
|
3554
|
+
);
|
|
3555
|
+
}
|
|
2715
3556
|
if (fs9__namespace.existsSync(configPath) && !options.force) {
|
|
2716
3557
|
throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
|
|
2717
3558
|
}
|
|
@@ -3268,14 +4109,42 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3268
4109
|
const modifiers = ts__default.default.canHaveModifiers(node) ? ts__default.default.getModifiers(node) : void 0;
|
|
3269
4110
|
return Boolean(modifiers?.some((m) => m.kind === ts__default.default.SyntaxKind.DefaultKeyword));
|
|
3270
4111
|
};
|
|
3271
|
-
const
|
|
4112
|
+
const rendersJsx = (node) => {
|
|
4113
|
+
const pending = [node];
|
|
4114
|
+
while (pending.length > 0) {
|
|
4115
|
+
const current = pending.pop();
|
|
4116
|
+
if (ts__default.default.isJsxElement(current) || ts__default.default.isJsxSelfClosingElement(current) || ts__default.default.isJsxFragment(current)) {
|
|
4117
|
+
return true;
|
|
4118
|
+
}
|
|
4119
|
+
if (ts__default.default.isCallExpression(current)) {
|
|
4120
|
+
const callee = current.expression;
|
|
4121
|
+
const calleeName = ts__default.default.isIdentifier(callee) ? callee.text : ts__default.default.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
|
4122
|
+
if (calleeName === "createElement") return true;
|
|
4123
|
+
}
|
|
4124
|
+
ts__default.default.forEachChild(current, (child) => {
|
|
4125
|
+
pending.push(child);
|
|
4126
|
+
});
|
|
4127
|
+
}
|
|
4128
|
+
return false;
|
|
4129
|
+
};
|
|
4130
|
+
const REACT_BASE_CLASS = /(?:^|\.)(?:PureComponent|Component)$/;
|
|
4131
|
+
const extendsReactBase = (node) => {
|
|
4132
|
+
const heritage = (node.heritageClauses ?? []).find(
|
|
4133
|
+
(clause) => clause.token === ts__default.default.SyntaxKind.ExtendsKeyword
|
|
4134
|
+
);
|
|
4135
|
+
const expression = heritage?.types[0]?.expression;
|
|
4136
|
+
if (!expression) return false;
|
|
4137
|
+
const text2 = ts__default.default.isIdentifier(expression) ? expression.text : expression.getText(sourceFile);
|
|
4138
|
+
return REACT_BASE_CLASS.test(text2);
|
|
4139
|
+
};
|
|
4140
|
+
const recordFunctionLike = (name, exported, node) => {
|
|
3272
4141
|
summary.functions.push(name);
|
|
3273
4142
|
if (exported) {
|
|
3274
4143
|
summary.exports.push(name);
|
|
3275
4144
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
3276
4145
|
}
|
|
3277
4146
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
3278
|
-
if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
|
|
4147
|
+
if (/^[A-Z]/.test(name) && rendersJsx(node)) summary.reactComponents.push(name);
|
|
3279
4148
|
};
|
|
3280
4149
|
const stack = [sourceFile];
|
|
3281
4150
|
while (stack.length > 0) {
|
|
@@ -3304,7 +4173,7 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3304
4173
|
if (ts__default.default.isFunctionDeclaration(node)) {
|
|
3305
4174
|
const exported = isExported(node);
|
|
3306
4175
|
if (node.name) {
|
|
3307
|
-
recordFunctionLike(node.name.text, exported);
|
|
4176
|
+
recordFunctionLike(node.name.text, exported, node);
|
|
3308
4177
|
if (isDefault(node)) summary.exports.push("default");
|
|
3309
4178
|
} else if (isDefault(node)) {
|
|
3310
4179
|
summary.exports.push("default");
|
|
@@ -3315,7 +4184,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3315
4184
|
summary.classes.push(node.name.text);
|
|
3316
4185
|
if (isExported(node)) summary.exports.push(node.name.text);
|
|
3317
4186
|
if (isDefault(node)) summary.exports.push("default");
|
|
3318
|
-
if (/^[A-Z]/.test(node.name.text))
|
|
4187
|
+
if (/^[A-Z]/.test(node.name.text) && (extendsReactBase(node) || rendersJsx(node))) {
|
|
4188
|
+
summary.reactComponents.push(node.name.text);
|
|
4189
|
+
}
|
|
3319
4190
|
} else if (isDefault(node)) {
|
|
3320
4191
|
summary.exports.push("default");
|
|
3321
4192
|
}
|
|
@@ -3343,7 +4214,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3343
4214
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
3344
4215
|
}
|
|
3345
4216
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
3346
|
-
if (/^[A-Z]/.test(name)
|
|
4217
|
+
if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
|
|
4218
|
+
summary.reactComponents.push(name);
|
|
4219
|
+
}
|
|
3347
4220
|
}
|
|
3348
4221
|
}
|
|
3349
4222
|
const children = [];
|
|
@@ -3570,7 +4443,11 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
3570
4443
|
`**Total Files:** ${manifest.counts.totalFiles} `,
|
|
3571
4444
|
`**Total Lines:** ${manifest.counts.totalLines.toLocaleString()} `,
|
|
3572
4445
|
`**Total Size:** ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB `,
|
|
3573
|
-
|
|
4446
|
+
// This counts the bytes that were *emitted* after redaction, not the bytes
|
|
4447
|
+
// that were masked: calling it "redacted bytes" made a repository with zero
|
|
4448
|
+
// secrets report a non-zero redacted-byte figure directly above "no secrets
|
|
4449
|
+
// detected". The number of masked spans is reported in section 1 instead.
|
|
4450
|
+
`**Emitted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
|
|
3574
4451
|
"",
|
|
3575
4452
|
"## 1. Security Scan & Redaction",
|
|
3576
4453
|
"",
|
|
@@ -3731,6 +4608,8 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
3731
4608
|
layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
|
|
3732
4609
|
kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
|
|
3733
4610
|
}
|
|
4611
|
+
const pathLines = included.map((file) => displayPath(file.relativePath));
|
|
4612
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
3734
4613
|
const header = [
|
|
3735
4614
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
3736
4615
|
"",
|
|
@@ -3743,9 +4622,9 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
3743
4622
|
"",
|
|
3744
4623
|
"## Files",
|
|
3745
4624
|
"",
|
|
3746
|
-
|
|
3747
|
-
...
|
|
3748
|
-
|
|
4625
|
+
`${listFence}text`,
|
|
4626
|
+
...pathLines,
|
|
4627
|
+
listFence,
|
|
3749
4628
|
"",
|
|
3750
4629
|
"## Contents",
|
|
3751
4630
|
""
|
|
@@ -3773,7 +4652,8 @@ function capValue(value, max) {
|
|
|
3773
4652
|
return `${value.slice(0, half)}\u2026${value.slice(value.length - half)}`;
|
|
3774
4653
|
}
|
|
3775
4654
|
var capMetadata = (value) => capValue(value, METADATA_VALUE_MAX);
|
|
3776
|
-
var
|
|
4655
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
|
|
4656
|
+
var displayPath = (value) => capValue(value.replace(CONTROL_CHARS, " "), DISPLAY_PATH_MAX);
|
|
3777
4657
|
function topEntries(counts, limit) {
|
|
3778
4658
|
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(", ");
|
|
3779
4659
|
}
|
|
@@ -3869,6 +4749,8 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
3869
4749
|
layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
|
|
3870
4750
|
kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
|
|
3871
4751
|
}
|
|
4752
|
+
const pathLines = group.map((s) => displayPath(s.file.relativePath));
|
|
4753
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
3872
4754
|
const header = [
|
|
3873
4755
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
3874
4756
|
"",
|
|
@@ -3881,9 +4763,9 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
3881
4763
|
"",
|
|
3882
4764
|
"## Files",
|
|
3883
4765
|
"",
|
|
3884
|
-
|
|
3885
|
-
...
|
|
3886
|
-
|
|
4766
|
+
`${listFence}text`,
|
|
4767
|
+
...pathLines,
|
|
4768
|
+
listFence,
|
|
3887
4769
|
"",
|
|
3888
4770
|
"## Contents",
|
|
3889
4771
|
""
|
|
@@ -3942,9 +4824,12 @@ function renderFileSection(file) {
|
|
|
3942
4824
|
].join("\n");
|
|
3943
4825
|
}
|
|
3944
4826
|
function fenceForContent(content) {
|
|
4827
|
+
return fenceForText(content, 4);
|
|
4828
|
+
}
|
|
4829
|
+
function fenceForText(content, minimum) {
|
|
3945
4830
|
const matches = content.match(/`+/g);
|
|
3946
4831
|
const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
|
|
3947
|
-
return "`".repeat(Math.max(
|
|
4832
|
+
return "`".repeat(Math.max(minimum, maxRun + 1));
|
|
3948
4833
|
}
|
|
3949
4834
|
function renderSummary(summary) {
|
|
3950
4835
|
if (!summary) return [];
|
|
@@ -4093,118 +4978,51 @@ function generateReadme(manifest) {
|
|
|
4093
4978
|
|
|
4094
4979
|
// src/generator/tree.ts
|
|
4095
4980
|
var MAX_TREE_LINES = 2e4;
|
|
4981
|
+
var CONTROL_CHARS2 = /[\u0000-\u001f\u007f]/g;
|
|
4096
4982
|
function generateProjectTree(filePaths) {
|
|
4097
4983
|
const root = { children: /* @__PURE__ */ new Map() };
|
|
4098
4984
|
for (const filePath of filePaths) {
|
|
4099
4985
|
const parts = filePath.split("/").filter((part) => part.length > 0);
|
|
4100
4986
|
let current = root;
|
|
4101
4987
|
for (const part of parts) {
|
|
4102
|
-
let child = current.children.get(part);
|
|
4103
|
-
if (!child) {
|
|
4104
|
-
child = { children: /* @__PURE__ */ new Map() };
|
|
4105
|
-
current.children.set(part, child);
|
|
4106
|
-
}
|
|
4107
|
-
current = child;
|
|
4108
|
-
}
|
|
4109
|
-
}
|
|
4110
|
-
const lines = ["# Project Tree", "", "```text", "."];
|
|
4111
|
-
let truncatedAt = -1;
|
|
4112
|
-
function walk(node, prefix) {
|
|
4113
|
-
if (truncatedAt !== -1) return;
|
|
4114
|
-
const keys = Array.from(node.children.keys()).sort((a, b) => {
|
|
4115
|
-
const aIsDir = node.children.get(a).children.size > 0;
|
|
4116
|
-
const bIsDir = node.children.get(b).children.size > 0;
|
|
4117
|
-
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
|
|
4118
|
-
return a < b ? -1 : a > b ? 1 : 0;
|
|
4119
|
-
});
|
|
4120
|
-
keys.forEach((key, index) => {
|
|
4121
|
-
if (truncatedAt !== -1) return;
|
|
4122
|
-
const isLast = index === keys.length - 1;
|
|
4123
|
-
const child = node.children.get(key);
|
|
4124
|
-
const isDirectory = child.children.size > 0;
|
|
4125
|
-
lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${key}${isDirectory ? "/" : ""}`);
|
|
4126
|
-
if (lines.length >= MAX_TREE_LINES) {
|
|
4127
|
-
truncatedAt = lines.length;
|
|
4128
|
-
return;
|
|
4129
|
-
}
|
|
4130
|
-
walk(child, prefix + (isLast ? " " : "\u2502 "));
|
|
4131
|
-
});
|
|
4132
|
-
}
|
|
4133
|
-
walk(root, "");
|
|
4134
|
-
if (truncatedAt !== -1) {
|
|
4135
|
-
lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
|
|
4136
|
-
}
|
|
4137
|
-
lines.push("```", "");
|
|
4138
|
-
return lines.join("\n");
|
|
4139
|
-
}
|
|
4140
|
-
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4141
|
-
".png",
|
|
4142
|
-
".jpg",
|
|
4143
|
-
".jpeg",
|
|
4144
|
-
".gif",
|
|
4145
|
-
".webp",
|
|
4146
|
-
".avif",
|
|
4147
|
-
".ico",
|
|
4148
|
-
".bmp",
|
|
4149
|
-
".tiff",
|
|
4150
|
-
".pdf",
|
|
4151
|
-
".zip",
|
|
4152
|
-
".tar",
|
|
4153
|
-
".gz",
|
|
4154
|
-
".tgz",
|
|
4155
|
-
".bz2",
|
|
4156
|
-
".xz",
|
|
4157
|
-
".7z",
|
|
4158
|
-
".rar",
|
|
4159
|
-
".mp3",
|
|
4160
|
-
".mp4",
|
|
4161
|
-
".wav",
|
|
4162
|
-
".ogg",
|
|
4163
|
-
".webm",
|
|
4164
|
-
".mov",
|
|
4165
|
-
".avi",
|
|
4166
|
-
".woff",
|
|
4167
|
-
".woff2",
|
|
4168
|
-
".ttf",
|
|
4169
|
-
".otf",
|
|
4170
|
-
".eot",
|
|
4171
|
-
".wasm",
|
|
4172
|
-
".node",
|
|
4173
|
-
".exe",
|
|
4174
|
-
".dll",
|
|
4175
|
-
".dylib",
|
|
4176
|
-
".so"
|
|
4177
|
-
]);
|
|
4178
|
-
function isBinaryExtension(filePath) {
|
|
4179
|
-
const ext = path10__namespace.extname(filePath).toLowerCase();
|
|
4180
|
-
return BINARY_EXTENSIONS.has(ext);
|
|
4181
|
-
}
|
|
4182
|
-
function isBinaryBuffer(buffer) {
|
|
4183
|
-
const checkLength = Math.min(buffer.length, 8192);
|
|
4184
|
-
for (let index = 0; index < checkLength; index++) {
|
|
4185
|
-
if (buffer[index] === 0) return true;
|
|
4186
|
-
}
|
|
4187
|
-
return false;
|
|
4188
|
-
}
|
|
4189
|
-
function isBinaryFile(absolutePath) {
|
|
4190
|
-
if (isBinaryExtension(absolutePath)) return true;
|
|
4191
|
-
let fd;
|
|
4192
|
-
try {
|
|
4193
|
-
fd = fs9__namespace.openSync(absolutePath, "r");
|
|
4194
|
-
const buffer = Buffer.alloc(8192);
|
|
4195
|
-
const bytesRead = fs9__namespace.readSync(fd, buffer, 0, 8192, 0);
|
|
4196
|
-
if (bytesRead === 0) return false;
|
|
4197
|
-
return isBinaryBuffer(buffer.subarray(0, bytesRead));
|
|
4198
|
-
} catch {
|
|
4199
|
-
return false;
|
|
4200
|
-
} finally {
|
|
4201
|
-
if (fd !== void 0) {
|
|
4202
|
-
try {
|
|
4203
|
-
fs9__namespace.closeSync(fd);
|
|
4204
|
-
} catch {
|
|
4988
|
+
let child = current.children.get(part);
|
|
4989
|
+
if (!child) {
|
|
4990
|
+
child = { children: /* @__PURE__ */ new Map() };
|
|
4991
|
+
current.children.set(part, child);
|
|
4205
4992
|
}
|
|
4993
|
+
current = child;
|
|
4206
4994
|
}
|
|
4207
4995
|
}
|
|
4996
|
+
const lines = ["."];
|
|
4997
|
+
let truncatedAt = -1;
|
|
4998
|
+
function walk(node, prefix) {
|
|
4999
|
+
if (truncatedAt !== -1) return;
|
|
5000
|
+
const keys = Array.from(node.children.keys()).sort((a, b) => {
|
|
5001
|
+
const aIsDir = node.children.get(a).children.size > 0;
|
|
5002
|
+
const bIsDir = node.children.get(b).children.size > 0;
|
|
5003
|
+
if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
|
|
5004
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
5005
|
+
});
|
|
5006
|
+
keys.forEach((key, index) => {
|
|
5007
|
+
if (truncatedAt !== -1) return;
|
|
5008
|
+
const isLast = index === keys.length - 1;
|
|
5009
|
+
const child = node.children.get(key);
|
|
5010
|
+
const isDirectory = child.children.size > 0;
|
|
5011
|
+
const label = key.replace(CONTROL_CHARS2, " ");
|
|
5012
|
+
lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
|
|
5013
|
+
if (lines.length >= MAX_TREE_LINES) {
|
|
5014
|
+
truncatedAt = lines.length;
|
|
5015
|
+
return;
|
|
5016
|
+
}
|
|
5017
|
+
walk(child, prefix + (isLast ? " " : "\u2502 "));
|
|
5018
|
+
});
|
|
5019
|
+
}
|
|
5020
|
+
walk(root, "");
|
|
5021
|
+
if (truncatedAt !== -1) {
|
|
5022
|
+
lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
|
|
5023
|
+
}
|
|
5024
|
+
const fence = fenceForText(lines.join("\n"), 3);
|
|
5025
|
+
return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
|
|
4208
5026
|
}
|
|
4209
5027
|
function emptyDrift() {
|
|
4210
5028
|
return {
|
|
@@ -4219,6 +5037,15 @@ function emptyDrift() {
|
|
|
4219
5037
|
function sha256Hex(buffer) {
|
|
4220
5038
|
return crypto__namespace.createHash("sha256").update(buffer).digest("hex");
|
|
4221
5039
|
}
|
|
5040
|
+
function wouldNowBeIncluded(cwd, relativePath) {
|
|
5041
|
+
const absolutePath = path10__namespace.join(cwd, relativePath);
|
|
5042
|
+
try {
|
|
5043
|
+
if (!fs9__namespace.statSync(absolutePath).isFile()) return false;
|
|
5044
|
+
return !isBinaryFile(absolutePath);
|
|
5045
|
+
} catch {
|
|
5046
|
+
return false;
|
|
5047
|
+
}
|
|
5048
|
+
}
|
|
4222
5049
|
function validateSnapshot(configInput) {
|
|
4223
5050
|
assertUsableConfigInput(configInput);
|
|
4224
5051
|
const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
|
|
@@ -4273,9 +5100,9 @@ function validateSnapshot(configInput) {
|
|
|
4273
5100
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
4274
5101
|
});
|
|
4275
5102
|
for (const diskFile of collection.files) {
|
|
4276
|
-
if (skippedSet.has(diskFile)) continue;
|
|
4277
5103
|
const snapshotHash = manifestFileMap.get(diskFile);
|
|
4278
5104
|
if (!snapshotHash) {
|
|
5105
|
+
if (skippedSet.has(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
|
|
4279
5106
|
drift.missingInSnapshot.push(diskFile);
|
|
4280
5107
|
continue;
|
|
4281
5108
|
}
|
|
@@ -4555,7 +5382,7 @@ function validateSnapshot(configInput) {
|
|
|
4555
5382
|
|
|
4556
5383
|
// src/index.ts
|
|
4557
5384
|
var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
|
|
4558
|
-
var GENERATOR_VERSION = "
|
|
5385
|
+
var GENERATOR_VERSION = "4.0.0";
|
|
4559
5386
|
var SCHEMA_VERSION = "3.0.0";
|
|
4560
5387
|
function sha256Hex2(input) {
|
|
4561
5388
|
return crypto__namespace.createHash("sha256").update(input).digest("hex");
|
|
@@ -4725,27 +5552,213 @@ function resolveSafeOutputDirs(config) {
|
|
|
4725
5552
|
return { outDir, tmpDir };
|
|
4726
5553
|
}
|
|
4727
5554
|
var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
|
|
5555
|
+
var STAGING_ENTRIES = /* @__PURE__ */ new Set([
|
|
5556
|
+
".codebase-snapshot",
|
|
5557
|
+
"manifest.json",
|
|
5558
|
+
"README.md",
|
|
5559
|
+
"parts",
|
|
5560
|
+
"reports",
|
|
5561
|
+
"full-codebase.json",
|
|
5562
|
+
"full-codebase.md"
|
|
5563
|
+
]);
|
|
4728
5564
|
var OWNERSHIP_MARKER = ".codebase-snapshot";
|
|
4729
|
-
|
|
5565
|
+
var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
|
|
5566
|
+
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;
|
|
5567
|
+
function listEntries(dir) {
|
|
5568
|
+
const entries = [];
|
|
5569
|
+
const walk = (absolute, prefix) => {
|
|
5570
|
+
let children;
|
|
5571
|
+
try {
|
|
5572
|
+
children = fs9__namespace.readdirSync(absolute, { withFileTypes: true });
|
|
5573
|
+
} catch {
|
|
5574
|
+
return;
|
|
5575
|
+
}
|
|
5576
|
+
for (const child of children) {
|
|
5577
|
+
const relative5 = prefix === "" ? child.name : `${prefix}/${child.name}`;
|
|
5578
|
+
entries.push(relative5);
|
|
5579
|
+
if (child.isDirectory()) walk(path10__namespace.join(absolute, child.name), relative5);
|
|
5580
|
+
}
|
|
5581
|
+
};
|
|
5582
|
+
walk(dir, "");
|
|
5583
|
+
return entries.sort();
|
|
5584
|
+
}
|
|
5585
|
+
function markerPayload(dir, state, id, createdAt) {
|
|
4730
5586
|
const marker2 = {
|
|
4731
5587
|
generator: GENERATOR_NAME,
|
|
4732
5588
|
version: GENERATOR_VERSION,
|
|
4733
|
-
schemaVersion: SCHEMA_VERSION
|
|
5589
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5590
|
+
createdAt,
|
|
5591
|
+
state,
|
|
5592
|
+
id,
|
|
5593
|
+
entries: listEntries(dir)
|
|
4734
5594
|
};
|
|
4735
|
-
fs9__namespace.writeFileSync(path10__namespace.join(dir, OWNERSHIP_MARKER), `${JSON.stringify(marker2, null, 2)}
|
|
4736
|
-
`, "utf8");
|
|
4737
|
-
}
|
|
4738
|
-
function isOwnOutput(dir) {
|
|
4739
|
-
if (fs9__namespace.existsSync(path10__namespace.join(dir, OWNERSHIP_MARKER))) return true;
|
|
4740
5595
|
const manifestPath = path10__namespace.join(dir, "manifest.json");
|
|
4741
|
-
if (
|
|
5596
|
+
if (fs9__namespace.existsSync(manifestPath)) {
|
|
5597
|
+
marker2.manifestSha256 = sha256Hex2(fs9__namespace.readFileSync(manifestPath));
|
|
5598
|
+
}
|
|
5599
|
+
return marker2;
|
|
5600
|
+
}
|
|
5601
|
+
function writeOwnershipMarker(dir, marker2) {
|
|
5602
|
+
fs9__namespace.writeFileSync(
|
|
5603
|
+
path10__namespace.join(dir, OWNERSHIP_MARKER),
|
|
5604
|
+
`${JSON.stringify(marker2, null, 2)}
|
|
5605
|
+
`,
|
|
5606
|
+
"utf8"
|
|
5607
|
+
);
|
|
5608
|
+
}
|
|
5609
|
+
function readOwnershipMarker(dir) {
|
|
5610
|
+
const markerPath = path10__namespace.join(dir, OWNERSHIP_MARKER);
|
|
5611
|
+
if (!fs9__namespace.existsSync(markerPath)) {
|
|
5612
|
+
return { marker: null, problem: "it holds no .codebase-snapshot marker" };
|
|
5613
|
+
}
|
|
5614
|
+
let raw;
|
|
4742
5615
|
try {
|
|
4743
|
-
|
|
4744
|
-
const generator = parsed.generator;
|
|
4745
|
-
return typeof parsed.schemaVersion === "string" && Array.isArray(parsed.files) && typeof generator === "object" && generator !== null && generator.name === GENERATOR_NAME;
|
|
5616
|
+
raw = fs9__namespace.readFileSync(markerPath, "utf8");
|
|
4746
5617
|
} catch {
|
|
4747
|
-
return
|
|
5618
|
+
return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
|
|
5619
|
+
}
|
|
5620
|
+
if (raw.trim().length === 0) {
|
|
5621
|
+
return { marker: null, problem: "its .codebase-snapshot marker is empty" };
|
|
5622
|
+
}
|
|
5623
|
+
let parsed;
|
|
5624
|
+
try {
|
|
5625
|
+
parsed = JSON.parse(raw);
|
|
5626
|
+
} catch {
|
|
5627
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not valid JSON" };
|
|
5628
|
+
}
|
|
5629
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5630
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not a JSON object" };
|
|
5631
|
+
}
|
|
5632
|
+
const candidate = parsed;
|
|
5633
|
+
if (candidate.generator !== GENERATOR_NAME) {
|
|
5634
|
+
return { marker: null, problem: "its .codebase-snapshot marker names a different generator" };
|
|
5635
|
+
}
|
|
5636
|
+
if (typeof candidate.schemaVersion !== "string" || !SUPPORTED_MARKER_SCHEMAS.has(candidate.schemaVersion)) {
|
|
5637
|
+
return {
|
|
5638
|
+
marker: null,
|
|
5639
|
+
problem: `its .codebase-snapshot marker has a schema version this build does not understand (${String(candidate.schemaVersion)})`
|
|
5640
|
+
};
|
|
5641
|
+
}
|
|
5642
|
+
if (typeof candidate.id !== "string" || !SNAPSHOT_ID_PATTERN.test(candidate.id)) {
|
|
5643
|
+
return { marker: null, problem: "its .codebase-snapshot marker carries no snapshot id" };
|
|
5644
|
+
}
|
|
5645
|
+
if (candidate.state !== "staging" && candidate.state !== "complete") {
|
|
5646
|
+
return { marker: null, problem: "its .codebase-snapshot marker records no run state" };
|
|
5647
|
+
}
|
|
5648
|
+
return {
|
|
5649
|
+
marker: {
|
|
5650
|
+
generator: GENERATOR_NAME,
|
|
5651
|
+
version: typeof candidate.version === "string" ? candidate.version : "unknown",
|
|
5652
|
+
schemaVersion: candidate.schemaVersion,
|
|
5653
|
+
createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
|
|
5654
|
+
state: candidate.state,
|
|
5655
|
+
id: candidate.id,
|
|
5656
|
+
manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
|
|
5657
|
+
entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
|
|
5658
|
+
},
|
|
5659
|
+
problem: null
|
|
5660
|
+
};
|
|
5661
|
+
}
|
|
5662
|
+
function describeManifestProblem(value) {
|
|
5663
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "is not a JSON object";
|
|
5664
|
+
const candidate = value;
|
|
5665
|
+
const generator = candidate.generator;
|
|
5666
|
+
if (typeof generator !== "object" || generator === null || generator.name !== GENERATOR_NAME) {
|
|
5667
|
+
return "was not written by this generator";
|
|
5668
|
+
}
|
|
5669
|
+
if (!Array.isArray(candidate.files)) return "has no files array";
|
|
5670
|
+
return null;
|
|
5671
|
+
}
|
|
5672
|
+
function assessSnapshotDirectory(dir) {
|
|
5673
|
+
const { marker: marker2, problem } = readOwnershipMarker(dir);
|
|
5674
|
+
if (!marker2) return { owned: false, problem, foreignEntries: [], marker: null };
|
|
5675
|
+
if (marker2.state !== "complete") {
|
|
5676
|
+
return {
|
|
5677
|
+
owned: false,
|
|
5678
|
+
problem: "its .codebase-snapshot marker records a run that never finished writing it",
|
|
5679
|
+
foreignEntries: [],
|
|
5680
|
+
marker: marker2
|
|
5681
|
+
};
|
|
5682
|
+
}
|
|
5683
|
+
const manifestPath = path10__namespace.join(dir, "manifest.json");
|
|
5684
|
+
if (fs9__namespace.existsSync(manifestPath)) {
|
|
5685
|
+
let raw;
|
|
5686
|
+
try {
|
|
5687
|
+
raw = fs9__namespace.readFileSync(manifestPath, "utf8");
|
|
5688
|
+
} catch {
|
|
5689
|
+
return {
|
|
5690
|
+
owned: false,
|
|
5691
|
+
problem: "its manifest.json could not be read",
|
|
5692
|
+
foreignEntries: [],
|
|
5693
|
+
marker: marker2
|
|
5694
|
+
};
|
|
5695
|
+
}
|
|
5696
|
+
let parsed;
|
|
5697
|
+
try {
|
|
5698
|
+
parsed = JSON.parse(raw);
|
|
5699
|
+
} catch {
|
|
5700
|
+
return {
|
|
5701
|
+
owned: false,
|
|
5702
|
+
problem: "its manifest.json is not valid JSON",
|
|
5703
|
+
foreignEntries: [],
|
|
5704
|
+
marker: marker2
|
|
5705
|
+
};
|
|
5706
|
+
}
|
|
5707
|
+
const manifestProblem2 = describeManifestProblem(parsed);
|
|
5708
|
+
if (manifestProblem2) {
|
|
5709
|
+
return {
|
|
5710
|
+
owned: false,
|
|
5711
|
+
problem: `its manifest.json ${manifestProblem2}`,
|
|
5712
|
+
foreignEntries: [],
|
|
5713
|
+
marker: marker2
|
|
5714
|
+
};
|
|
5715
|
+
}
|
|
5716
|
+
if (!marker2.manifestSha256) {
|
|
5717
|
+
return {
|
|
5718
|
+
owned: false,
|
|
5719
|
+
problem: "its .codebase-snapshot marker records no manifest digest",
|
|
5720
|
+
foreignEntries: [],
|
|
5721
|
+
marker: marker2
|
|
5722
|
+
};
|
|
5723
|
+
}
|
|
5724
|
+
if (sha256Hex2(raw) !== marker2.manifestSha256) {
|
|
5725
|
+
return {
|
|
5726
|
+
owned: false,
|
|
5727
|
+
problem: "its manifest.json does not match the digest recorded in its .codebase-snapshot marker",
|
|
5728
|
+
foreignEntries: [],
|
|
5729
|
+
marker: marker2
|
|
5730
|
+
};
|
|
5731
|
+
}
|
|
5732
|
+
} else if (marker2.manifestSha256) {
|
|
5733
|
+
return {
|
|
5734
|
+
owned: false,
|
|
5735
|
+
problem: "its manifest.json is missing although its marker records one",
|
|
5736
|
+
foreignEntries: [],
|
|
5737
|
+
marker: marker2
|
|
5738
|
+
};
|
|
5739
|
+
}
|
|
5740
|
+
const recorded = new Set(marker2.entries ?? []);
|
|
5741
|
+
if (recorded.size === 0) {
|
|
5742
|
+
return {
|
|
5743
|
+
owned: false,
|
|
5744
|
+
problem: "its .codebase-snapshot marker records no contents",
|
|
5745
|
+
foreignEntries: [],
|
|
5746
|
+
marker: marker2
|
|
5747
|
+
};
|
|
4748
5748
|
}
|
|
5749
|
+
const foreignEntries = listEntries(dir).filter(
|
|
5750
|
+
(entry) => !recorded.has(entry) && !PLACEHOLDER_ENTRIES.has(entry)
|
|
5751
|
+
);
|
|
5752
|
+
return {
|
|
5753
|
+
owned: foreignEntries.length === 0,
|
|
5754
|
+
problem: null,
|
|
5755
|
+
foreignEntries,
|
|
5756
|
+
marker: marker2
|
|
5757
|
+
};
|
|
5758
|
+
}
|
|
5759
|
+
function describeEntries(entries, limit = 5) {
|
|
5760
|
+
const shown = entries.slice(0, limit).join(", ");
|
|
5761
|
+
return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
|
|
4749
5762
|
}
|
|
4750
5763
|
function assertReplaceable(target, label) {
|
|
4751
5764
|
if (!fs9__namespace.existsSync(target)) return;
|
|
@@ -4755,16 +5768,124 @@ function assertReplaceable(target, label) {
|
|
|
4755
5768
|
);
|
|
4756
5769
|
}
|
|
4757
5770
|
const entries = fs9__namespace.readdirSync(target);
|
|
4758
|
-
|
|
4759
|
-
if (
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
5771
|
+
if (entries.length === 0) return;
|
|
5772
|
+
if (label === "outDir" && entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
|
|
5773
|
+
const { marker: marker2, problem } = readOwnershipMarker(target);
|
|
5774
|
+
if (label === "tmpDir") {
|
|
5775
|
+
if (!marker2) {
|
|
5776
|
+
throw new Error(
|
|
5777
|
+
`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.`
|
|
5778
|
+
);
|
|
5779
|
+
}
|
|
5780
|
+
const unexpected = entries.filter((name) => !STAGING_ENTRIES.has(name));
|
|
5781
|
+
if (unexpected.length > 0) {
|
|
5782
|
+
throw new Error(
|
|
5783
|
+
`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.`
|
|
5784
|
+
);
|
|
5785
|
+
}
|
|
5786
|
+
return;
|
|
5787
|
+
}
|
|
5788
|
+
const verdict = assessSnapshotDirectory(target);
|
|
5789
|
+
if (verdict.owned) return;
|
|
5790
|
+
if (verdict.foreignEntries.length > 0) {
|
|
5791
|
+
throw new Error(
|
|
5792
|
+
`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.`
|
|
5793
|
+
);
|
|
5794
|
+
}
|
|
5795
|
+
const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
|
|
4763
5796
|
throw new Error(
|
|
4764
|
-
`Refusing to replace ${label} ${target}:
|
|
5797
|
+
`Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(entries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
|
|
5798
|
+
);
|
|
5799
|
+
}
|
|
5800
|
+
function ensureParentDirectory(target, label) {
|
|
5801
|
+
const parent = path10__namespace.dirname(target);
|
|
5802
|
+
if (fs9__namespace.existsSync(parent) && !fs9__namespace.statSync(parent).isDirectory()) {
|
|
5803
|
+
throw new Error(
|
|
5804
|
+
`Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
|
|
5805
|
+
);
|
|
5806
|
+
}
|
|
5807
|
+
try {
|
|
5808
|
+
fs9__namespace.mkdirSync(parent, { recursive: true });
|
|
5809
|
+
} catch (err) {
|
|
5810
|
+
throw new Error(
|
|
5811
|
+
`Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
|
|
5812
|
+
);
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
5815
|
+
function acquireRunLock(outDir) {
|
|
5816
|
+
const lockPath = path10__namespace.join(
|
|
5817
|
+
os__namespace.tmpdir(),
|
|
5818
|
+
`codebase-snapshot-${sha256Hex2(outDir).slice(0, 16)}.lock`
|
|
4765
5819
|
);
|
|
5820
|
+
const payload = `${JSON.stringify({
|
|
5821
|
+
pid: process.pid,
|
|
5822
|
+
outDir,
|
|
5823
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5824
|
+
})}
|
|
5825
|
+
`;
|
|
5826
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
5827
|
+
try {
|
|
5828
|
+
const handle = fs9__namespace.openSync(lockPath, "wx");
|
|
5829
|
+
try {
|
|
5830
|
+
fs9__namespace.writeSync(handle, payload);
|
|
5831
|
+
} finally {
|
|
5832
|
+
fs9__namespace.closeSync(handle);
|
|
5833
|
+
}
|
|
5834
|
+
return () => {
|
|
5835
|
+
try {
|
|
5836
|
+
fs9__namespace.rmSync(lockPath, { force: true });
|
|
5837
|
+
} catch {
|
|
5838
|
+
}
|
|
5839
|
+
};
|
|
5840
|
+
} catch (err) {
|
|
5841
|
+
if (err.code !== "EEXIST") throw err;
|
|
5842
|
+
if (attempt === 0 && lockOwnerIsGone(lockPath)) {
|
|
5843
|
+
fs9__namespace.rmSync(lockPath, { force: true });
|
|
5844
|
+
continue;
|
|
5845
|
+
}
|
|
5846
|
+
throw new Error(
|
|
5847
|
+
`Another generate run is already writing ${outDir}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
|
|
5848
|
+
);
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
throw new Error(`Could not acquire the generate lock for ${outDir}.`);
|
|
5852
|
+
}
|
|
5853
|
+
function lockOwnerIsGone(lockPath) {
|
|
5854
|
+
let raw;
|
|
5855
|
+
try {
|
|
5856
|
+
raw = fs9__namespace.readFileSync(lockPath, "utf8");
|
|
5857
|
+
} catch {
|
|
5858
|
+
return false;
|
|
5859
|
+
}
|
|
5860
|
+
let pid;
|
|
5861
|
+
try {
|
|
5862
|
+
pid = JSON.parse(raw).pid;
|
|
5863
|
+
} catch {
|
|
5864
|
+
return false;
|
|
5865
|
+
}
|
|
5866
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
5867
|
+
try {
|
|
5868
|
+
process.kill(pid, 0);
|
|
5869
|
+
return false;
|
|
5870
|
+
} catch (err) {
|
|
5871
|
+
return err.code === "ESRCH";
|
|
5872
|
+
}
|
|
5873
|
+
}
|
|
5874
|
+
function writeManifestAtomically(manifestPath, manifest) {
|
|
5875
|
+
const tempPath = `${manifestPath}.${process.pid}.tmp`;
|
|
5876
|
+
fs9__namespace.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
5877
|
+
try {
|
|
5878
|
+
fs9__namespace.renameSync(tempPath, manifestPath);
|
|
5879
|
+
} catch (err) {
|
|
5880
|
+
try {
|
|
5881
|
+
fs9__namespace.rmSync(tempPath, { force: true });
|
|
5882
|
+
} catch {
|
|
5883
|
+
}
|
|
5884
|
+
throw err;
|
|
5885
|
+
}
|
|
4766
5886
|
}
|
|
4767
5887
|
function swapIntoPlace(tmpDir, outDir) {
|
|
5888
|
+
ensureParentDirectory(outDir, "outDir");
|
|
4768
5889
|
const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
|
|
4769
5890
|
const hadPrevious = fs9__namespace.existsSync(outDir);
|
|
4770
5891
|
if (hadPrevious) fs9__namespace.renameSync(outDir, backupDir);
|
|
@@ -4853,6 +5974,7 @@ function generateSnapshot(configInput, options) {
|
|
|
4853
5974
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
4854
5975
|
});
|
|
4855
5976
|
const files = [];
|
|
5977
|
+
const monolithBodies = /* @__PURE__ */ new Map();
|
|
4856
5978
|
const skipped = [...collection.skipped];
|
|
4857
5979
|
const secretFindings = [];
|
|
4858
5980
|
const warnings = [];
|
|
@@ -4916,6 +6038,9 @@ function generateSnapshot(configInput, options) {
|
|
|
4916
6038
|
isPreviewTruncated = preview.isTruncated;
|
|
4917
6039
|
includedLines = preview.includedLines;
|
|
4918
6040
|
}
|
|
6041
|
+
if (config.includeMonolith && isPreviewTruncated) {
|
|
6042
|
+
monolithBodies.set(relativePath, redacted);
|
|
6043
|
+
}
|
|
4919
6044
|
let summary;
|
|
4920
6045
|
let summaryQuality;
|
|
4921
6046
|
try {
|
|
@@ -4958,7 +6083,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4958
6083
|
).join("\n")
|
|
4959
6084
|
);
|
|
4960
6085
|
const fullContents = /* @__PURE__ */ new Map();
|
|
4961
|
-
for (const file of files)
|
|
6086
|
+
for (const file of files) {
|
|
6087
|
+
fullContents.set(
|
|
6088
|
+
file.relativePath,
|
|
6089
|
+
monolithBodies.get(file.relativePath) ?? file.content ?? ""
|
|
6090
|
+
);
|
|
6091
|
+
}
|
|
4962
6092
|
const parts = partitionByBudget(files, config, fingerprint);
|
|
4963
6093
|
const maxEffectiveBudget = Math.max(
|
|
4964
6094
|
config.maxPartBytes,
|
|
@@ -5044,10 +6174,17 @@ function generateSnapshot(configInput, options) {
|
|
|
5044
6174
|
let monolithJson = null;
|
|
5045
6175
|
let monolithMarkdown = null;
|
|
5046
6176
|
if (config.includeMonolith) {
|
|
5047
|
-
const fullFiles = files.map((file) =>
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
6177
|
+
const fullFiles = files.map((file) => {
|
|
6178
|
+
const content = fullContents.get(file.relativePath) ?? "";
|
|
6179
|
+
return {
|
|
6180
|
+
...file,
|
|
6181
|
+
content,
|
|
6182
|
+
inclusionMode: "full",
|
|
6183
|
+
isPreviewTruncated: false,
|
|
6184
|
+
includedBytes: Buffer.byteLength(content, "utf8"),
|
|
6185
|
+
includedLines: countLines(content)
|
|
6186
|
+
};
|
|
6187
|
+
});
|
|
5051
6188
|
const json = generateMonolithJson(manifest, fullFiles);
|
|
5052
6189
|
const markdown = generateMonolithMarkdown(manifest, fullFiles);
|
|
5053
6190
|
const jsonBytes = Buffer.byteLength(json, "utf8");
|
|
@@ -5075,57 +6212,199 @@ function generateSnapshot(configInput, options) {
|
|
|
5075
6212
|
}
|
|
5076
6213
|
for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
|
|
5077
6214
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
6215
|
+
const { outDir, tmpDir } = resolveSafeOutputDirs(config);
|
|
6216
|
+
const { artifacts } = config;
|
|
6217
|
+
assertReplaceable(tmpDir, "tmpDir");
|
|
6218
|
+
assertReplaceable(outDir, "outDir");
|
|
5078
6219
|
if (dryRun) {
|
|
5079
6220
|
runAfterGenerate(config, manifest);
|
|
5080
6221
|
return manifest;
|
|
5081
6222
|
}
|
|
5082
|
-
const
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
6223
|
+
const releaseLock = acquireRunLock(outDir);
|
|
6224
|
+
let stagingStarted = false;
|
|
6225
|
+
try {
|
|
6226
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6227
|
+
fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
|
|
6228
|
+
fs9__namespace.mkdirSync(tmpDir, { recursive: true });
|
|
6229
|
+
stagingStarted = true;
|
|
6230
|
+
const snapshotId = crypto__namespace.randomUUID();
|
|
6231
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6232
|
+
writeOwnershipMarker(tmpDir, {
|
|
6233
|
+
generator: GENERATOR_NAME,
|
|
6234
|
+
version: GENERATOR_VERSION,
|
|
6235
|
+
schemaVersion: SCHEMA_VERSION,
|
|
6236
|
+
createdAt,
|
|
6237
|
+
state: "staging",
|
|
6238
|
+
id: snapshotId
|
|
6239
|
+
});
|
|
6240
|
+
const partsDir = path10__namespace.join(tmpDir, "parts");
|
|
6241
|
+
const reportsDir = path10__namespace.join(tmpDir, "reports");
|
|
6242
|
+
if (artifacts.parts) fs9__namespace.mkdirSync(partsDir, { recursive: true });
|
|
6243
|
+
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
6244
|
+
fs9__namespace.mkdirSync(reportsDir, { recursive: true });
|
|
6245
|
+
}
|
|
6246
|
+
if (artifacts.parts) {
|
|
6247
|
+
for (const part of parts) {
|
|
6248
|
+
fs9__namespace.writeFileSync(path10__namespace.join(partsDir, part.filename), part.markdown, "utf8");
|
|
6249
|
+
}
|
|
5098
6250
|
}
|
|
6251
|
+
if (artifacts.projectTree) {
|
|
6252
|
+
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
6253
|
+
fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
6254
|
+
}
|
|
6255
|
+
if (artifacts.navigation) {
|
|
6256
|
+
fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
6257
|
+
}
|
|
6258
|
+
if (artifacts.audit) {
|
|
6259
|
+
fs9__namespace.writeFileSync(
|
|
6260
|
+
path10__namespace.join(reportsDir, "audit.md"),
|
|
6261
|
+
generateAuditReport(manifest, secretFindings),
|
|
6262
|
+
"utf8"
|
|
6263
|
+
);
|
|
6264
|
+
}
|
|
6265
|
+
if (artifacts.readme) {
|
|
6266
|
+
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
|
|
6267
|
+
}
|
|
6268
|
+
if (artifacts.manifest) {
|
|
6269
|
+
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
6270
|
+
}
|
|
6271
|
+
if (monolithJson !== null) {
|
|
6272
|
+
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
|
|
6273
|
+
}
|
|
6274
|
+
if (monolithMarkdown !== null) {
|
|
6275
|
+
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
6276
|
+
}
|
|
6277
|
+
writeOwnershipMarker(tmpDir, markerPayload(tmpDir, "complete", snapshotId, createdAt));
|
|
6278
|
+
swapIntoPlace(tmpDir, outDir);
|
|
6279
|
+
stagingStarted = false;
|
|
6280
|
+
const warningsBeforeHook = manifest.warnings.length;
|
|
6281
|
+
runAfterGenerate(config, manifest);
|
|
6282
|
+
if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
|
|
6283
|
+
writeManifestAtomically(path10__namespace.join(outDir, "manifest.json"), manifest);
|
|
6284
|
+
}
|
|
6285
|
+
writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
|
|
6286
|
+
return manifest;
|
|
6287
|
+
} catch (err) {
|
|
6288
|
+
if (stagingStarted) {
|
|
6289
|
+
try {
|
|
6290
|
+
fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
|
|
6291
|
+
} catch {
|
|
6292
|
+
}
|
|
6293
|
+
}
|
|
6294
|
+
throw err;
|
|
6295
|
+
} finally {
|
|
6296
|
+
releaseLock();
|
|
5099
6297
|
}
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
|
|
5110
|
-
|
|
5111
|
-
|
|
5112
|
-
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
|
|
5127
|
-
|
|
5128
|
-
|
|
6298
|
+
}
|
|
6299
|
+
|
|
6300
|
+
// src/agent/option-scope.ts
|
|
6301
|
+
var COMMAND_OPTION_SCOPE = {
|
|
6302
|
+
generate: [
|
|
6303
|
+
"cwd",
|
|
6304
|
+
"out-dir",
|
|
6305
|
+
"max-part-bytes",
|
|
6306
|
+
"large-file-bytes",
|
|
6307
|
+
"max-monolith-bytes",
|
|
6308
|
+
"include-monolith",
|
|
6309
|
+
"include-sensitive-files",
|
|
6310
|
+
"skip-artifacts",
|
|
6311
|
+
"only-artifacts",
|
|
6312
|
+
"no-config",
|
|
6313
|
+
"format",
|
|
6314
|
+
"mode",
|
|
6315
|
+
"dry-run",
|
|
6316
|
+
"watch",
|
|
6317
|
+
"help",
|
|
6318
|
+
"version"
|
|
6319
|
+
],
|
|
6320
|
+
validate: [
|
|
6321
|
+
"cwd",
|
|
6322
|
+
"out-dir",
|
|
6323
|
+
"max-part-bytes",
|
|
6324
|
+
"large-file-bytes",
|
|
6325
|
+
"max-monolith-bytes",
|
|
6326
|
+
"include-monolith",
|
|
6327
|
+
"include-sensitive-files",
|
|
6328
|
+
"skip-artifacts",
|
|
6329
|
+
"only-artifacts",
|
|
6330
|
+
"no-config",
|
|
6331
|
+
"format",
|
|
6332
|
+
"mode",
|
|
6333
|
+
"help",
|
|
6334
|
+
"version"
|
|
6335
|
+
],
|
|
6336
|
+
report: [
|
|
6337
|
+
"cwd",
|
|
6338
|
+
"out-dir",
|
|
6339
|
+
"max-part-bytes",
|
|
6340
|
+
"large-file-bytes",
|
|
6341
|
+
"max-monolith-bytes",
|
|
6342
|
+
"include-monolith",
|
|
6343
|
+
"include-sensitive-files",
|
|
6344
|
+
"skip-artifacts",
|
|
6345
|
+
"only-artifacts",
|
|
6346
|
+
"no-config",
|
|
6347
|
+
"format",
|
|
6348
|
+
"mode",
|
|
6349
|
+
"help",
|
|
6350
|
+
"version"
|
|
6351
|
+
],
|
|
6352
|
+
explain: [
|
|
6353
|
+
"cwd",
|
|
6354
|
+
"out-dir",
|
|
6355
|
+
"max-part-bytes",
|
|
6356
|
+
"large-file-bytes",
|
|
6357
|
+
"max-monolith-bytes",
|
|
6358
|
+
"include-monolith",
|
|
6359
|
+
"include-sensitive-files",
|
|
6360
|
+
"skip-artifacts",
|
|
6361
|
+
"only-artifacts",
|
|
6362
|
+
"no-config",
|
|
6363
|
+
"format",
|
|
6364
|
+
"mode",
|
|
6365
|
+
"help",
|
|
6366
|
+
"version"
|
|
6367
|
+
],
|
|
6368
|
+
diff: ["format", "mode", "help", "version"],
|
|
6369
|
+
describe: ["format", "mode", "help", "version"],
|
|
6370
|
+
init: [
|
|
6371
|
+
"cwd",
|
|
6372
|
+
"out-dir",
|
|
6373
|
+
"max-part-bytes",
|
|
6374
|
+
"include-sensitive-files",
|
|
6375
|
+
"skip-artifacts",
|
|
6376
|
+
"only-artifacts",
|
|
6377
|
+
"preset",
|
|
6378
|
+
"include",
|
|
6379
|
+
"exclude",
|
|
6380
|
+
"yes",
|
|
6381
|
+
"minimal",
|
|
6382
|
+
"force",
|
|
6383
|
+
"dry-run",
|
|
6384
|
+
"format",
|
|
6385
|
+
"mode",
|
|
6386
|
+
"help",
|
|
6387
|
+
"version"
|
|
6388
|
+
],
|
|
6389
|
+
skill: ["cwd", "format", "mode", "install", "target", "help", "version"]
|
|
6390
|
+
};
|
|
6391
|
+
var COMMAND_ORDER = [
|
|
6392
|
+
"generate",
|
|
6393
|
+
"validate",
|
|
6394
|
+
"report",
|
|
6395
|
+
"explain",
|
|
6396
|
+
"init",
|
|
6397
|
+
"diff",
|
|
6398
|
+
"describe",
|
|
6399
|
+
"skill"
|
|
6400
|
+
];
|
|
6401
|
+
function commandsForOption(option) {
|
|
6402
|
+
return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
|
|
6403
|
+
}
|
|
6404
|
+
function optionAppliesTo(option) {
|
|
6405
|
+
const commands = commandsForOption(option);
|
|
6406
|
+
if (commands.length === COMMAND_ORDER.length) return "all commands";
|
|
6407
|
+
return commands.join("/");
|
|
5129
6408
|
}
|
|
5130
6409
|
|
|
5131
6410
|
// src/agent/contract.ts
|
|
@@ -5217,11 +6496,11 @@ function buildAgentContract() {
|
|
|
5217
6496
|
invocation: {
|
|
5218
6497
|
oneShot: `npx ${PACKAGE_NAME}@latest`,
|
|
5219
6498
|
installed: "npx codebase-snapshot",
|
|
5220
|
-
bins: ["codebase-snapshot"],
|
|
6499
|
+
bins: ["codebase-snapshot", "snap"],
|
|
5221
6500
|
notes: [
|
|
5222
6501
|
`The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
|
|
5223
|
-
"Pin @latest (or @
|
|
5224
|
-
"
|
|
6502
|
+
"Pin @latest (or @4) when you need the exact same version on every machine.",
|
|
6503
|
+
"Both bin names point at the same file. npm resolves a package by its bin entries, so they must stay aliases of one executable.",
|
|
5225
6504
|
"Human progress goes to stderr. With --format json the payload on stdout is pure JSON."
|
|
5226
6505
|
]
|
|
5227
6506
|
},
|
|
@@ -5230,7 +6509,7 @@ function buildAgentContract() {
|
|
|
5230
6509
|
step: 1,
|
|
5231
6510
|
id: "verify",
|
|
5232
6511
|
run: `npx ${PACKAGE_NAME}@latest --version`,
|
|
5233
|
-
expect:
|
|
6512
|
+
expect: `prints v${GENERATOR_VERSION} on stdout and exits 0`,
|
|
5234
6513
|
notes: "If it prints nothing, clear the npm cache and retry, or use `npx -p <pkg> codebase-snapshot`."
|
|
5235
6514
|
},
|
|
5236
6515
|
{
|
|
@@ -5300,9 +6579,9 @@ function buildAgentContract() {
|
|
|
5300
6579
|
{
|
|
5301
6580
|
name: "explain",
|
|
5302
6581
|
aliases: [],
|
|
5303
|
-
summary: "Explain how one path would be treated: layer, kind, part, skip reason.",
|
|
6582
|
+
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.",
|
|
5304
6583
|
positionals: ["<path>"],
|
|
5305
|
-
exits: [0
|
|
6584
|
+
exits: [0]
|
|
5306
6585
|
},
|
|
5307
6586
|
{
|
|
5308
6587
|
name: "diff",
|
|
@@ -5336,140 +6615,147 @@ function buildAgentContract() {
|
|
|
5336
6615
|
flag: "--out-dir",
|
|
5337
6616
|
type: "string",
|
|
5338
6617
|
default: "snapshot",
|
|
5339
|
-
appliesTo: "
|
|
6618
|
+
appliesTo: optionAppliesTo("out-dir"),
|
|
5340
6619
|
summary: "Where the snapshot is written. Must not be the repo root or a directory the tool did not create."
|
|
5341
6620
|
},
|
|
5342
6621
|
{
|
|
5343
6622
|
flag: "--max-part-bytes",
|
|
5344
6623
|
type: "string",
|
|
5345
6624
|
default: 307200,
|
|
5346
|
-
appliesTo: "
|
|
6625
|
+
appliesTo: optionAppliesTo("max-part-bytes"),
|
|
5347
6626
|
summary: 'Byte budget per markdown part. Accepts "120k" / "1.5mb". Minimum 4096.'
|
|
5348
6627
|
},
|
|
5349
6628
|
{
|
|
5350
6629
|
flag: "--large-file-bytes",
|
|
5351
6630
|
type: "string",
|
|
5352
6631
|
default: 204800,
|
|
5353
|
-
appliesTo: "
|
|
6632
|
+
appliesTo: optionAppliesTo("large-file-bytes"),
|
|
5354
6633
|
summary: "Above this size a file gets a head/tail preview instead of the full body."
|
|
5355
6634
|
},
|
|
5356
6635
|
{
|
|
5357
6636
|
flag: "--max-monolith-bytes",
|
|
5358
6637
|
type: "string",
|
|
5359
6638
|
default: "unset",
|
|
5360
|
-
appliesTo: "
|
|
6639
|
+
appliesTo: optionAppliesTo("max-monolith-bytes"),
|
|
5361
6640
|
summary: "Skip the monolith artifacts when they would exceed this size."
|
|
5362
6641
|
},
|
|
5363
6642
|
{
|
|
5364
6643
|
flag: "--include-monolith",
|
|
5365
6644
|
type: "boolean",
|
|
5366
6645
|
default: false,
|
|
5367
|
-
appliesTo: "
|
|
6646
|
+
appliesTo: optionAppliesTo("include-monolith"),
|
|
5368
6647
|
summary: "Also write full-codebase.json and full-codebase.md."
|
|
5369
6648
|
},
|
|
5370
6649
|
{
|
|
5371
6650
|
flag: "--include-sensitive-files",
|
|
5372
6651
|
type: "boolean",
|
|
5373
6652
|
default: false,
|
|
5374
|
-
appliesTo: "
|
|
6653
|
+
appliesTo: optionAppliesTo("include-sensitive-files"),
|
|
5375
6654
|
summary: "Also emit .env / *.pem / .npmrc style files. Off by default; they are recorded as skipped."
|
|
5376
6655
|
},
|
|
5377
6656
|
{
|
|
5378
6657
|
flag: "--skip-artifacts",
|
|
5379
6658
|
type: "string",
|
|
5380
6659
|
default: "unset",
|
|
5381
|
-
appliesTo: "
|
|
6660
|
+
appliesTo: optionAppliesTo("skip-artifacts"),
|
|
5382
6661
|
summary: "Comma-separated artifacts to omit (readme, manifest, parts, navigation, projectTree, audit, monolith)."
|
|
5383
6662
|
},
|
|
5384
6663
|
{
|
|
5385
6664
|
flag: "--only-artifacts",
|
|
5386
6665
|
type: "string",
|
|
5387
6666
|
default: "unset",
|
|
5388
|
-
appliesTo: "
|
|
6667
|
+
appliesTo: optionAppliesTo("only-artifacts"),
|
|
5389
6668
|
summary: "Write only these artifacts. Cannot be combined with --skip-artifacts."
|
|
5390
6669
|
},
|
|
5391
6670
|
{
|
|
5392
6671
|
flag: "--format",
|
|
5393
6672
|
type: "string",
|
|
5394
6673
|
default: "text",
|
|
5395
|
-
appliesTo: "
|
|
6674
|
+
appliesTo: optionAppliesTo("format"),
|
|
5396
6675
|
summary: 'Output format: "text" or "json". JSON goes to stdout, progress to stderr.'
|
|
5397
6676
|
},
|
|
5398
6677
|
{
|
|
5399
6678
|
flag: "--cwd",
|
|
5400
6679
|
type: "string",
|
|
5401
6680
|
default: "process.cwd()",
|
|
5402
|
-
appliesTo: "
|
|
6681
|
+
appliesTo: optionAppliesTo("cwd"),
|
|
5403
6682
|
summary: "Repository to operate on. Must be an existing directory."
|
|
5404
6683
|
},
|
|
5405
6684
|
{
|
|
5406
6685
|
flag: "--dry-run",
|
|
5407
6686
|
type: "boolean",
|
|
5408
6687
|
default: false,
|
|
5409
|
-
appliesTo: "
|
|
6688
|
+
appliesTo: optionAppliesTo("dry-run"),
|
|
5410
6689
|
summary: "Compute everything and print the result without writing."
|
|
5411
6690
|
},
|
|
5412
6691
|
{
|
|
5413
6692
|
flag: "--watch",
|
|
5414
6693
|
type: "boolean",
|
|
5415
6694
|
default: false,
|
|
5416
|
-
appliesTo: "
|
|
6695
|
+
appliesTo: optionAppliesTo("watch"),
|
|
5417
6696
|
summary: "Regenerate on every change until interrupted."
|
|
5418
6697
|
},
|
|
6698
|
+
{
|
|
6699
|
+
flag: "--no-config",
|
|
6700
|
+
type: "boolean",
|
|
6701
|
+
default: false,
|
|
6702
|
+
appliesTo: optionAppliesTo("no-config"),
|
|
6703
|
+
summary: "Ignore the config file and run on the built-in defaults."
|
|
6704
|
+
},
|
|
5419
6705
|
{
|
|
5420
6706
|
flag: "--preset",
|
|
5421
6707
|
type: "string",
|
|
5422
6708
|
default: "detected",
|
|
5423
|
-
appliesTo: "
|
|
6709
|
+
appliesTo: optionAppliesTo("preset"),
|
|
5424
6710
|
summary: "Force a project preset instead of detecting one. Unknown names are a usage error."
|
|
5425
6711
|
},
|
|
5426
6712
|
{
|
|
5427
6713
|
flag: "--include",
|
|
5428
6714
|
type: "string",
|
|
5429
6715
|
default: "unset",
|
|
5430
|
-
appliesTo: "
|
|
6716
|
+
appliesTo: optionAppliesTo("include"),
|
|
5431
6717
|
summary: "Comma-separated allow-list of globs to record in the generated config."
|
|
5432
6718
|
},
|
|
5433
6719
|
{
|
|
5434
6720
|
flag: "--exclude",
|
|
5435
6721
|
type: "string",
|
|
5436
6722
|
default: "unset",
|
|
5437
|
-
appliesTo: "
|
|
6723
|
+
appliesTo: optionAppliesTo("exclude"),
|
|
5438
6724
|
summary: "Comma-separated globs to record in the generated config."
|
|
5439
6725
|
},
|
|
5440
6726
|
{
|
|
5441
6727
|
flag: "--yes",
|
|
5442
6728
|
type: "boolean",
|
|
5443
6729
|
default: false,
|
|
5444
|
-
appliesTo: "
|
|
6730
|
+
appliesTo: optionAppliesTo("yes"),
|
|
5445
6731
|
summary: "Accept every default without prompting. Required in a non-TTY."
|
|
5446
6732
|
},
|
|
5447
6733
|
{
|
|
5448
6734
|
flag: "--minimal",
|
|
5449
6735
|
type: "boolean",
|
|
5450
6736
|
default: false,
|
|
5451
|
-
appliesTo: "
|
|
6737
|
+
appliesTo: optionAppliesTo("minimal"),
|
|
5452
6738
|
summary: "Write only the settings that differ from the defaults."
|
|
5453
6739
|
},
|
|
5454
6740
|
{
|
|
5455
6741
|
flag: "--force",
|
|
5456
6742
|
type: "boolean",
|
|
5457
6743
|
default: false,
|
|
5458
|
-
appliesTo: "
|
|
6744
|
+
appliesTo: optionAppliesTo("force"),
|
|
5459
6745
|
summary: "Overwrite an existing config file."
|
|
5460
6746
|
},
|
|
5461
6747
|
{
|
|
5462
6748
|
flag: "--install",
|
|
5463
6749
|
type: "boolean",
|
|
5464
6750
|
default: false,
|
|
5465
|
-
appliesTo: "
|
|
6751
|
+
appliesTo: optionAppliesTo("install"),
|
|
5466
6752
|
summary: "Write the skill into the repository instead of printing it."
|
|
5467
6753
|
},
|
|
5468
6754
|
{
|
|
5469
6755
|
flag: "--target",
|
|
5470
6756
|
type: "string",
|
|
5471
6757
|
default: "all",
|
|
5472
|
-
appliesTo: "
|
|
6758
|
+
appliesTo: optionAppliesTo("target"),
|
|
5473
6759
|
summary: 'Where to install: "claude", "agents", "cursor" or "all".'
|
|
5474
6760
|
}
|
|
5475
6761
|
],
|
|
@@ -5544,8 +6830,8 @@ function buildAgentContract() {
|
|
|
5544
6830
|
troubleshooting: [
|
|
5545
6831
|
{
|
|
5546
6832
|
symptom: "Command prints nothing and exits 0",
|
|
5547
|
-
cause: "
|
|
5548
|
-
fix: `
|
|
6833
|
+
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.",
|
|
6834
|
+
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."
|
|
5549
6835
|
},
|
|
5550
6836
|
{
|
|
5551
6837
|
symptom: "Exit 1 with 'Refusing to replace outDir'",
|
|
@@ -5619,7 +6905,8 @@ not resolved \u2014 clear the npm cache and retry; do not assume the repository
|
|
|
5619
6905
|
\`--format json\` if you want to parse it.
|
|
5620
6906
|
|
|
5621
6907
|
4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
|
|
5622
|
-
the project shape, then writes \`snapshot.config.ts\`
|
|
6908
|
+
the project shape, then writes \`snapshot.config.ts\` (or \`.mjs\` when the project is
|
|
6909
|
+
not an ES module) listing **every** setting at its
|
|
5623
6910
|
current value, commented. Pass \`--preset <name>\` only if detection is wrong; the
|
|
5624
6911
|
preset names are in the contract. This step is optional: without a config the tool
|
|
5625
6912
|
runs on the same defaults.
|
|
@@ -5720,14 +7007,24 @@ function mergeAgentsFile(cwd, skill) {
|
|
|
5720
7007
|
next = `# AGENTS.md
|
|
5721
7008
|
|
|
5722
7009
|
${block}`;
|
|
5723
|
-
} else
|
|
7010
|
+
} else {
|
|
7011
|
+
const starts = existing.split(AGENTS_MARKER_START).length - 1;
|
|
7012
|
+
const ends = existing.split(AGENTS_MARKER_END).length - 1;
|
|
5724
7013
|
const start = existing.indexOf(AGENTS_MARKER_START);
|
|
5725
7014
|
const end = existing.indexOf(AGENTS_MARKER_END) + AGENTS_MARKER_END.length;
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
7015
|
+
if (starts === 1 && ends === 1 && start < end) {
|
|
7016
|
+
next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(end)}`;
|
|
7017
|
+
} else if (starts > 0 && ends === 0) {
|
|
7018
|
+
next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(
|
|
7019
|
+
start + AGENTS_MARKER_START.length
|
|
7020
|
+
)}`;
|
|
7021
|
+
} else if (existing.includes(block.trimEnd())) {
|
|
7022
|
+
next = existing;
|
|
7023
|
+
} else {
|
|
7024
|
+
next = `${existing.trimEnd()}
|
|
5729
7025
|
|
|
5730
7026
|
${block}`;
|
|
7027
|
+
}
|
|
5731
7028
|
}
|
|
5732
7029
|
const action = existing === null ? "created" : existing === next ? "unchanged" : "updated";
|
|
5733
7030
|
if (action !== "unchanged") {
|
|
@@ -5790,6 +7087,7 @@ var CLI_OPTIONS = {
|
|
|
5790
7087
|
mode: { type: "string" },
|
|
5791
7088
|
"dry-run": { type: "boolean" },
|
|
5792
7089
|
watch: { type: "boolean" },
|
|
7090
|
+
"no-config": { type: "boolean" },
|
|
5793
7091
|
preset: { type: "string" },
|
|
5794
7092
|
include: { type: "string" },
|
|
5795
7093
|
exclude: { type: "string" },
|
|
@@ -5836,6 +7134,7 @@ Options:
|
|
|
5836
7134
|
--format <text|json> CLI output format (default: text)
|
|
5837
7135
|
--dry-run Compute the snapshot without writing anything
|
|
5838
7136
|
--watch Re-generate whenever the repository changes
|
|
7137
|
+
--no-config Ignore snapshot.config.* and use the built-in defaults
|
|
5839
7138
|
|
|
5840
7139
|
init only:
|
|
5841
7140
|
--preset <name> Force a project preset instead of detecting one
|
|
@@ -5997,28 +7296,32 @@ function parseCliArgs(argv) {
|
|
|
5997
7296
|
} else if (rawSkip !== void 0) {
|
|
5998
7297
|
skipArtifacts = parseArtifactList(String(rawSkip), "--skip-artifacts");
|
|
5999
7298
|
}
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
if (
|
|
6004
|
-
|
|
6005
|
-
if (
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
7299
|
+
const scope = COMMAND_OPTION_SCOPE[command];
|
|
7300
|
+
const ignored = [];
|
|
7301
|
+
for (const name of Object.keys(CLI_OPTIONS)) {
|
|
7302
|
+
if (scope.includes(name)) continue;
|
|
7303
|
+
const value = values[name];
|
|
7304
|
+
if (value === void 0 || value === false) continue;
|
|
7305
|
+
ignored.push(`--${name}`);
|
|
7306
|
+
}
|
|
7307
|
+
if (ignored.length > 0) {
|
|
7308
|
+
const owners = Array.from(
|
|
7309
|
+
new Set(
|
|
7310
|
+
ignored.flatMap(
|
|
7311
|
+
(flag) => commandsForOption(flag.replace(/^--/, "")).map((owner) => `\`${owner}\``)
|
|
7312
|
+
)
|
|
7313
|
+
)
|
|
7314
|
+
);
|
|
7315
|
+
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)." : "";
|
|
7316
|
+
throw new UsageError(
|
|
7317
|
+
`${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}`
|
|
7318
|
+
);
|
|
6013
7319
|
}
|
|
6014
7320
|
if (command === "init" && values.preset !== void 0) {
|
|
6015
7321
|
const requested = String(values.preset);
|
|
6016
7322
|
if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
|
|
6017
7323
|
}
|
|
6018
|
-
if (command
|
|
6019
|
-
throw new UsageError("--install and --target are only valid with `skill`.");
|
|
6020
|
-
}
|
|
6021
|
-
if (values.target !== void 0 && !isSkillTarget(String(values.target))) {
|
|
7324
|
+
if (command === "skill" && values.target !== void 0 && !isSkillTarget(String(values.target))) {
|
|
6022
7325
|
throw new UsageError(
|
|
6023
7326
|
`--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
|
|
6024
7327
|
);
|
|
@@ -6029,6 +7332,7 @@ function parseCliArgs(argv) {
|
|
|
6029
7332
|
format: rawFormat,
|
|
6030
7333
|
dryRun: values["dry-run"] === true,
|
|
6031
7334
|
watch: values.watch === true,
|
|
7335
|
+
noConfig: values["no-config"] === true,
|
|
6032
7336
|
preset: values.preset === void 0 ? void 0 : String(values.preset),
|
|
6033
7337
|
maxPartBytesRaw: values["max-part-bytes"] === void 0 ? void 0 : String(values["max-part-bytes"]),
|
|
6034
7338
|
include: values.include === void 0 ? void 0 : String(values.include),
|
|
@@ -6082,6 +7386,12 @@ function splitList(value) {
|
|
|
6082
7386
|
if (value === void 0) return void 0;
|
|
6083
7387
|
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
6084
7388
|
}
|
|
7389
|
+
function displayOutPath(cwd, outDir, ...segments) {
|
|
7390
|
+
const absolute = path10__namespace.resolve(cwd, outDir, ...segments);
|
|
7391
|
+
const relative5 = path10__namespace.relative(cwd, absolute);
|
|
7392
|
+
if (relative5 && !relative5.startsWith("..") && !path10__namespace.isAbsolute(relative5)) return relative5;
|
|
7393
|
+
return absolute;
|
|
7394
|
+
}
|
|
6085
7395
|
function describeManifestShapeProblem(manifest) {
|
|
6086
7396
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
6087
7397
|
return "not a JSON object";
|
|
@@ -6101,6 +7411,7 @@ async function runInitCommand(invocation) {
|
|
|
6101
7411
|
const result = await runInit({
|
|
6102
7412
|
cwd: invocation.cwd,
|
|
6103
7413
|
preset: invocation.preset,
|
|
7414
|
+
outDir: invocation.outDir,
|
|
6104
7415
|
maxPartBytes: invocation.maxPartBytesRaw ?? invocation.maxPartBytes,
|
|
6105
7416
|
include: splitList(invocation.include),
|
|
6106
7417
|
exclude: splitList(invocation.exclude),
|
|
@@ -6127,7 +7438,11 @@ async function runInitCommand(invocation) {
|
|
|
6127
7438
|
}
|
|
6128
7439
|
for (const note of result.notes) info(` \u26A0 ${note}`);
|
|
6129
7440
|
info("");
|
|
6130
|
-
|
|
7441
|
+
if (result.scripts.length > 0) {
|
|
7442
|
+
info(" Next: npm run snapshot");
|
|
7443
|
+
} else {
|
|
7444
|
+
info(` Next: ${GENERATOR_NAME} generate`);
|
|
7445
|
+
}
|
|
6131
7446
|
info(
|
|
6132
7447
|
` Tip: \`${GENERATOR_NAME} skill --install\` makes this repo discoverable by AI agents.`
|
|
6133
7448
|
);
|
|
@@ -6143,6 +7458,13 @@ async function runInitCommand(invocation) {
|
|
|
6143
7458
|
}
|
|
6144
7459
|
function runSkillCommand(invocation) {
|
|
6145
7460
|
if (!invocation.install) {
|
|
7461
|
+
if (invocation.format === "json") {
|
|
7462
|
+
process.stdout.write(
|
|
7463
|
+
`${JSON.stringify({ name: SKILL_NAME, version: GENERATOR_VERSION, skill: renderSkill() }, null, 2)}
|
|
7464
|
+
`
|
|
7465
|
+
);
|
|
7466
|
+
return 0;
|
|
7467
|
+
}
|
|
6146
7468
|
process.stdout.write(renderSkill());
|
|
6147
7469
|
return 0;
|
|
6148
7470
|
}
|
|
@@ -6219,7 +7541,8 @@ ${USAGE}
|
|
|
6219
7541
|
if (invocation.command === "skill") {
|
|
6220
7542
|
return runSkillCommand(invocation);
|
|
6221
7543
|
}
|
|
6222
|
-
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
7544
|
+
const loaded = invocation.noConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
|
|
7545
|
+
if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
|
|
6223
7546
|
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
6224
7547
|
if (loaded.errors.length > 0) {
|
|
6225
7548
|
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
@@ -6291,7 +7614,7 @@ ${USAGE}
|
|
|
6291
7614
|
if (invocation.command === "explain") {
|
|
6292
7615
|
const result = explainPath(resolved, invocation.args[0]);
|
|
6293
7616
|
emit(invocation.format, result, () => info(renderExplainText(result)));
|
|
6294
|
-
return
|
|
7617
|
+
return 0;
|
|
6295
7618
|
}
|
|
6296
7619
|
if (invocation.command === "diff") {
|
|
6297
7620
|
try {
|
|
@@ -6315,7 +7638,9 @@ ${USAGE}
|
|
|
6315
7638
|
if (invocation.dryRun) {
|
|
6316
7639
|
info("\n\u{1F9EA} Dry run \u2014 nothing was written:");
|
|
6317
7640
|
info(` - Would track: ${manifest.counts.totalFiles} files`);
|
|
6318
|
-
info(
|
|
7641
|
+
info(
|
|
7642
|
+
` - Would write: ${manifest.counts.partsCount} part(s) under ${displayOutPath(resolved.cwd, resolved.outDir, "parts")}`
|
|
7643
|
+
);
|
|
6319
7644
|
info(` - Skipped: ${manifest.counts.skippedCount} file(s)`);
|
|
6320
7645
|
info(` - Warnings: ${manifest.warnings.length}`);
|
|
6321
7646
|
info(` - Integrity: ${manifest.validation.status}`);
|
|
@@ -6330,7 +7655,9 @@ ${USAGE}
|
|
|
6330
7655
|
);
|
|
6331
7656
|
const { artifacts } = resolved;
|
|
6332
7657
|
if (artifacts.parts) {
|
|
6333
|
-
info(
|
|
7658
|
+
info(
|
|
7659
|
+
` - Parts written: ${manifest.counts.partsCount} under ${displayOutPath(resolved.cwd, resolved.outDir, "parts")}/`
|
|
7660
|
+
);
|
|
6334
7661
|
}
|
|
6335
7662
|
const reports = [
|
|
6336
7663
|
artifacts.projectTree && "project-tree",
|
|
@@ -6338,10 +7665,28 @@ ${USAGE}
|
|
|
6338
7665
|
artifacts.audit && "audit"
|
|
6339
7666
|
].filter(Boolean);
|
|
6340
7667
|
if (reports.length > 0) {
|
|
6341
|
-
info(
|
|
7668
|
+
info(
|
|
7669
|
+
` - Reports: ${displayOutPath(resolved.cwd, resolved.outDir, "reports")}/ (${reports.join(", ")})`
|
|
7670
|
+
);
|
|
7671
|
+
}
|
|
7672
|
+
if (artifacts.manifest) {
|
|
7673
|
+
info(` - Manifest: ${displayOutPath(resolved.cwd, resolved.outDir, "manifest.json")}`);
|
|
7674
|
+
}
|
|
7675
|
+
if (artifacts.monolith) {
|
|
7676
|
+
const monolithFiles = [
|
|
7677
|
+
manifest.artifacts?.fullJson ? "full-codebase.json" : null,
|
|
7678
|
+
manifest.artifacts?.fullMarkdown ? "full-codebase.md" : null
|
|
7679
|
+
].filter((name) => name !== null);
|
|
7680
|
+
if (monolithFiles.length === 2) {
|
|
7681
|
+
info(
|
|
7682
|
+
` - Monolith: ${displayOutPath(resolved.cwd, resolved.outDir, "full-codebase.{json,md}")}`
|
|
7683
|
+
);
|
|
7684
|
+
} else if (monolithFiles.length === 1) {
|
|
7685
|
+
info(
|
|
7686
|
+
` - Monolith: ${displayOutPath(resolved.cwd, resolved.outDir, monolithFiles[0])} (the other was skipped by --max-monolith-bytes)`
|
|
7687
|
+
);
|
|
7688
|
+
}
|
|
6342
7689
|
}
|
|
6343
|
-
if (artifacts.manifest) info(` - Manifest: ${resolved.outDir}/manifest.json`);
|
|
6344
|
-
if (artifacts.monolith) info(` - Monolith: ${resolved.outDir}/full-codebase.{json,md}`);
|
|
6345
7690
|
if (reports.length === 0 && !artifacts.parts && !artifacts.manifest && !artifacts.monolith) {
|
|
6346
7691
|
info(` - Artifacts: all disabled (only the ownership marker was written)`);
|
|
6347
7692
|
}
|
|
@@ -6376,18 +7721,43 @@ ${USAGE}
|
|
|
6376
7721
|
async function runWatchLoop(invocation, resolved) {
|
|
6377
7722
|
const outDir = path10__namespace.resolve(resolved.cwd, resolved.outDir);
|
|
6378
7723
|
const tmpDir = path10__namespace.resolve(resolved.cwd, resolved.tmpDir);
|
|
7724
|
+
const VCS_SEGMENTS = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr"]);
|
|
6379
7725
|
const outName = path10__namespace.basename(outDir).toLowerCase();
|
|
6380
7726
|
const tmpName = path10__namespace.basename(tmpDir).toLowerCase();
|
|
6381
7727
|
const isGenerated = (candidate) => candidate.toLowerCase().split(/[\\/]/).some(
|
|
6382
|
-
(segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`)
|
|
7728
|
+
(segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`) || VCS_SEGMENTS.has(segment)
|
|
6383
7729
|
);
|
|
6384
7730
|
let running = false;
|
|
6385
|
-
const regenerate = () => {
|
|
7731
|
+
const regenerate = async () => {
|
|
6386
7732
|
if (running) return;
|
|
6387
7733
|
running = true;
|
|
6388
7734
|
const started = performance.now();
|
|
6389
7735
|
try {
|
|
6390
|
-
const
|
|
7736
|
+
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
7737
|
+
if (loaded.errors.length > 0) {
|
|
7738
|
+
for (const error of loaded.errors) warn(`\u26A0 Config: ${error}`);
|
|
7739
|
+
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
7740
|
+
return;
|
|
7741
|
+
}
|
|
7742
|
+
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
7743
|
+
const next = resolveConfig(
|
|
7744
|
+
loaded.config,
|
|
7745
|
+
{
|
|
7746
|
+
mode: invocation.command,
|
|
7747
|
+
format: invocation.format,
|
|
7748
|
+
dryRun: invocation.dryRun,
|
|
7749
|
+
cwd: invocation.cwd,
|
|
7750
|
+
outDir: invocation.outDir,
|
|
7751
|
+
includeMonolith: invocation.includeMonolith,
|
|
7752
|
+
includeSensitiveFiles: invocation.includeSensitiveFiles,
|
|
7753
|
+
maxPartBytes: invocation.maxPartBytes,
|
|
7754
|
+
largeFileBytes: invocation.largeFileBytes,
|
|
7755
|
+
maxMonolithBytes: invocation.maxMonolithBytes,
|
|
7756
|
+
skipArtifacts: invocation.skipArtifacts
|
|
7757
|
+
},
|
|
7758
|
+
invocation.cwd
|
|
7759
|
+
);
|
|
7760
|
+
const manifest = generateSnapshot(next, { dryRun: invocation.dryRun });
|
|
6391
7761
|
const elapsed = ((performance.now() - started) / 1e3).toFixed(2);
|
|
6392
7762
|
info(
|
|
6393
7763
|
`\u267B\uFE0F ${(/* @__PURE__ */ new Date()).toISOString()} ${manifest.counts.totalFiles} file(s), ${manifest.counts.partsCount} part(s), ${manifest.validation.status} (${elapsed}s)`
|
|
@@ -6399,7 +7769,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
6399
7769
|
}
|
|
6400
7770
|
};
|
|
6401
7771
|
info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
|
|
6402
|
-
regenerate();
|
|
7772
|
+
void regenerate();
|
|
6403
7773
|
let timer = null;
|
|
6404
7774
|
const watcher = fs9__namespace.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
|
|
6405
7775
|
if (filename) {
|
|
@@ -6407,7 +7777,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
6407
7777
|
if (isGenerated(changed)) return;
|
|
6408
7778
|
}
|
|
6409
7779
|
if (timer) clearTimeout(timer);
|
|
6410
|
-
timer = setTimeout(regenerate, 250);
|
|
7780
|
+
timer = setTimeout(() => void regenerate(), 250);
|
|
6411
7781
|
});
|
|
6412
7782
|
await new Promise((resolve6) => {
|
|
6413
7783
|
const stop = () => {
|