@ankurhalder/codebase-snapshot 3.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -310
- package/dist/bin.js +2247 -709
- package/dist/{chunk-5B4UVQLR.js → chunk-RGIMMEAW.js} +1763 -425
- package/dist/cli.cjs +2212 -673
- package/dist/cli.d.cts +3 -1
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +268 -68
- package/dist/index.cjs +1762 -422
- package/dist/index.d.cts +28 -29
- package/dist/index.d.ts +28 -29
- package/dist/index.js +1 -1
- package/dist/{types-CJqkePM4.d.cts → types-LSfwsKE-.d.cts} +38 -4
- package/dist/{types-CJqkePM4.d.ts → types-LSfwsKE-.d.ts} +38 -4
- package/docs/agent.md +77 -37
- package/docs/cli.md +317 -0
- package/docs/configuration.md +254 -226
- package/docs/install.md +114 -86
- package/docs/output.md +213 -0
- package/docs/troubleshooting.md +277 -0
- package/package.json +7 -4
package/dist/cli.cjs
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
var fs9 = require('fs');
|
|
4
4
|
var path10 = require('path');
|
|
5
5
|
var util = require('util');
|
|
6
|
-
var url = require('url');
|
|
7
6
|
var childProcess = require('child_process');
|
|
7
|
+
var url = require('url');
|
|
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,100 +35,10 @@ 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
|
|
40
|
-
function side(manifest) {
|
|
41
|
-
return {
|
|
42
|
-
generatedAt: manifest.generatedAt,
|
|
43
|
-
fingerprint: manifest.fingerprint,
|
|
44
|
-
fileCount: manifest.counts.totalFiles,
|
|
45
|
-
partsCount: manifest.counts.partsCount
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
function hashMap(manifest) {
|
|
49
|
-
const map = /* @__PURE__ */ new Map();
|
|
50
|
-
for (const file of manifest.files) {
|
|
51
|
-
if (file && typeof file.relativePath === "string") {
|
|
52
|
-
map.set(file.relativePath, file.hashSha256);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
return map;
|
|
56
|
-
}
|
|
57
|
-
function compareManifests(before, after) {
|
|
58
|
-
const left = hashMap(before);
|
|
59
|
-
const right = hashMap(after);
|
|
60
|
-
const added = [];
|
|
61
|
-
const removed = [];
|
|
62
|
-
const changed = [];
|
|
63
|
-
let unchanged = 0;
|
|
64
|
-
for (const [filePath, hash] of right) {
|
|
65
|
-
const previous = left.get(filePath);
|
|
66
|
-
if (previous === void 0) added.push(filePath);
|
|
67
|
-
else if (previous !== hash) changed.push(filePath);
|
|
68
|
-
else unchanged += 1;
|
|
69
|
-
}
|
|
70
|
-
for (const filePath of left.keys()) {
|
|
71
|
-
if (!right.has(filePath)) removed.push(filePath);
|
|
72
|
-
}
|
|
73
|
-
const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
74
|
-
return {
|
|
75
|
-
before: side(before),
|
|
76
|
-
after: side(after),
|
|
77
|
-
added: sort(added),
|
|
78
|
-
removed: sort(removed),
|
|
79
|
-
changed: sort(changed),
|
|
80
|
-
unchanged
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
function loadManifest(target) {
|
|
84
|
-
let isDirectory;
|
|
85
|
-
try {
|
|
86
|
-
isDirectory = fs9__namespace.statSync(target).isDirectory();
|
|
87
|
-
} catch {
|
|
88
|
-
throw new Error(`No snapshot found at ${target}: the path does not exist.`);
|
|
89
|
-
}
|
|
90
|
-
const manifestPath = isDirectory ? path10__namespace.join(target, "manifest.json") : target;
|
|
91
|
-
let raw;
|
|
92
|
-
try {
|
|
93
|
-
raw = fs9__namespace.readFileSync(manifestPath, "utf8");
|
|
94
|
-
} catch {
|
|
95
|
-
throw new Error(`Could not read a manifest at ${manifestPath}.`);
|
|
96
|
-
}
|
|
97
|
-
let parsed;
|
|
98
|
-
try {
|
|
99
|
-
parsed = JSON.parse(raw);
|
|
100
|
-
} catch (err) {
|
|
101
|
-
throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
|
|
102
|
-
}
|
|
103
|
-
if (!parsed || !Array.isArray(parsed.files)) {
|
|
104
|
-
throw new Error(`${manifestPath} is not a snapshot manifest (no "files" array).`);
|
|
105
|
-
}
|
|
106
|
-
if (!parsed.counts || typeof parsed.counts !== "object") {
|
|
107
|
-
throw new Error(`${manifestPath} is not a snapshot manifest (no "counts" object).`);
|
|
108
|
-
}
|
|
109
|
-
return parsed;
|
|
110
|
-
}
|
|
111
|
-
function renderDiffText(diff) {
|
|
112
|
-
const lines = [];
|
|
113
|
-
lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
|
|
114
|
-
lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
|
|
115
|
-
lines.push("");
|
|
116
|
-
lines.push(` + ${diff.added.length} added`);
|
|
117
|
-
lines.push(` - ${diff.removed.length} removed`);
|
|
118
|
-
lines.push(` ~ ${diff.changed.length} changed`);
|
|
119
|
-
lines.push(` = ${diff.unchanged} unchanged`);
|
|
120
|
-
const section = (title, values) => {
|
|
121
|
-
if (values.length === 0) return;
|
|
122
|
-
lines.push("", title);
|
|
123
|
-
for (const value of values.slice(0, 200)) lines.push(` ${value}`);
|
|
124
|
-
if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
|
|
125
|
-
};
|
|
126
|
-
section("added:", diff.added);
|
|
127
|
-
section("removed:", diff.removed);
|
|
128
|
-
section("changed:", diff.changed);
|
|
129
|
-
return lines.join("\n");
|
|
130
|
-
}
|
|
131
42
|
var MANIFEST_FILES = [
|
|
132
43
|
"package.json",
|
|
133
44
|
"pyproject.toml",
|
|
@@ -966,68 +877,537 @@ function toDetected(preset) {
|
|
|
966
877
|
function detectPreset(cwd, forced) {
|
|
967
878
|
return detectPresetFromContext(buildPresetContext(cwd), forced);
|
|
968
879
|
}
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
880
|
+
|
|
881
|
+
// src/security/patterns.ts
|
|
882
|
+
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)";
|
|
883
|
+
var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
|
|
884
|
+
var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|process\\.env|Buffer|Record|object)\\b)";
|
|
885
|
+
var UNQUOTED_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{1,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
886
|
+
var UNQUOTED_AMBIGUOUS_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{8,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
887
|
+
var UNQUOTED_STRONG_NAME = `(?<!id[-_])${STRONG_CREDENTIAL_NAME}`;
|
|
888
|
+
var DEFAULT_SECRET_PATTERNS = [
|
|
889
|
+
{
|
|
890
|
+
rule: "private-key-header",
|
|
891
|
+
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
rule: "jwt-token",
|
|
895
|
+
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
rule: "aws-access-key-id",
|
|
899
|
+
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
900
|
+
},
|
|
901
|
+
{
|
|
902
|
+
// The name is no longer limited to the three snake_case spellings: an
|
|
903
|
+
// `awsSecret` / `secretAccessKey` property leaked verbatim because only the
|
|
904
|
+
// literal words matched. The value is masked alone, so the key survives.
|
|
905
|
+
rule: "aws-secret-access-key",
|
|
906
|
+
pattern: new RegExp(
|
|
907
|
+
`${STRONG_CREDENTIAL_NAME}["']?\\s{0,8}[:=]\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
|
|
908
|
+
"i"
|
|
909
|
+
),
|
|
910
|
+
valueGroup: 1
|
|
911
|
+
},
|
|
912
|
+
{
|
|
913
|
+
// A 40-character base64 value with no key name in sight. A key-independent
|
|
914
|
+
// rule is the only thing that catches `new AWS.SecretKey(…)` arguments and
|
|
915
|
+
// pasted `~/.aws/credentials` fragments, but a bare length test also matches
|
|
916
|
+
// every 40-char git SHA-1, so the value must mix case and digits and must
|
|
917
|
+
// not be pure hex.
|
|
918
|
+
rule: "aws-secret-access-key-value",
|
|
919
|
+
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}/
|
|
920
|
+
},
|
|
921
|
+
{
|
|
922
|
+
rule: "github-token",
|
|
923
|
+
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
// `xoxc-`/`xoxd-` are the user and refresh tokens, `xoxb`/`xoxp` the bot and
|
|
927
|
+
// user tokens; the whole Slack family shares one prefix shape.
|
|
928
|
+
rule: "slack-token",
|
|
929
|
+
pattern: /xox[baprsdc]-[0-9A-Za-z-]{10,250}/
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
// Prefix-only vendors. Each prefix below is documented, globally unique and
|
|
933
|
+
// cannot collide with an ordinary identifier, which is what makes a bare
|
|
934
|
+
// prefix rule cheap and safe: `glpat-` (GitLab), `hvs.`/`s.` (Vault service
|
|
935
|
+
// and batch tokens), `whsec_` (Stripe webhook secret), `dckr_pat_` (Docker
|
|
936
|
+
// Hub), `hf_` (HuggingFace), `ya29.` (Google OAuth access token).
|
|
937
|
+
rule: "known-vendor-token-prefix",
|
|
938
|
+
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})/
|
|
939
|
+
},
|
|
940
|
+
{
|
|
941
|
+
rule: "slack-webhook-url",
|
|
942
|
+
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
rule: "stripe-secret-key",
|
|
946
|
+
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
947
|
+
},
|
|
948
|
+
{
|
|
949
|
+
rule: "google-api-key",
|
|
950
|
+
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
rule: "google-oauth-client-secret",
|
|
954
|
+
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
rule: "openai-anthropic-key",
|
|
958
|
+
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
rule: "npm-token",
|
|
962
|
+
pattern: /npm_[A-Za-z0-9]{36}/
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
rule: "pypi-token",
|
|
966
|
+
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
967
|
+
},
|
|
968
|
+
{
|
|
969
|
+
rule: "sendgrid-key",
|
|
970
|
+
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
971
|
+
},
|
|
972
|
+
{
|
|
973
|
+
rule: "twilio-api-key",
|
|
974
|
+
pattern: /SK[0-9a-fA-F]{32}/
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
rule: "digitalocean-token",
|
|
978
|
+
pattern: /dop_v1_[0-9a-f]{64}/
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
rule: "azure-storage-account-key",
|
|
982
|
+
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
983
|
+
},
|
|
984
|
+
{
|
|
985
|
+
// Scheme-less userinfo: `//user:password@host`.
|
|
986
|
+
//
|
|
987
|
+
// Enumerating schemes is a losing game — `mysqlx`, `svn`, `telnet`, `imap`,
|
|
988
|
+
// `pop3`, `nntp`, `kafka`, `elasticsearch` each leaked a password while the
|
|
989
|
+
// list grew by hand. This rule matches the shape instead, so any scheme
|
|
990
|
+
// (including one invented tomorrow) is covered, provided the `//` is followed
|
|
991
|
+
// immediately by the userinfo: `// see the docs` and a bare reminder comment
|
|
992
|
+
// never match, because a space or the comment wording breaks the shape.
|
|
993
|
+
rule: "uri-credentials",
|
|
994
|
+
pattern: /\/\/[^\s:/@"'`;()<>,]{1,128}:[^\s:/@"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
|
|
995
|
+
},
|
|
996
|
+
{
|
|
997
|
+
// The host tail stops at quotes, `;`, `)`, `<`, `>` and `,`: an unbounded
|
|
998
|
+
// `[^\s/]` used to swallow the closing quote and the statement separator of
|
|
999
|
+
// the string the URL was written in.
|
|
1000
|
+
rule: "database-uri-credentials",
|
|
1001
|
+
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}/
|
|
1002
|
+
},
|
|
1003
|
+
{
|
|
1004
|
+
rule: "basic-auth-url",
|
|
1005
|
+
pattern: /(?:https?|ftp|sftp|ssh|git|smtp|smtps|ldap|ldaps):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/i
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
// Quoted credentials in any common key spelling. The value may be as short
|
|
1009
|
+
// as one character: `password: "hunter2"` is a credential whatever its
|
|
1010
|
+
// entropy, and a length floor was exactly how it survived into `parts`.
|
|
1011
|
+
//
|
|
1012
|
+
// The key is either bare (`dbPassword:`) or wholly quoted (`"dbPassword":`).
|
|
1013
|
+
// There is deliberately no optional quote *before* the separator: that let a
|
|
1014
|
+
// previous string's closing quote stand in for the key's, so
|
|
1015
|
+
// ``${pass ? " PASS" : " FAIL"}`` had `" FAIL"` masked as if `PASS` were
|
|
1016
|
+
// the key name.
|
|
1017
|
+
rule: "credential-assignment",
|
|
1018
|
+
pattern: new RegExp(
|
|
1019
|
+
`(?:["']${STRONG_CREDENTIAL_NAME}["']|${STRONG_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{1,512})\\1`,
|
|
1020
|
+
"i"
|
|
1021
|
+
),
|
|
1022
|
+
valueGroup: 2
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
// `pass`, `id-token` and `credentials` are ambiguous on their own, so they
|
|
1026
|
+
// are accepted only against a quoted value long enough to be a credential.
|
|
1027
|
+
rule: "credential-assignment-weak",
|
|
1028
|
+
pattern: new RegExp(
|
|
1029
|
+
`(?:["']${WEAK_CREDENTIAL_NAME}["']|${WEAK_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{8,512})\\1`,
|
|
1030
|
+
"i"
|
|
1031
|
+
),
|
|
1032
|
+
valueGroup: 2
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
// The unquoted twin for YAML, docker-compose and `.env`: those formats never
|
|
1036
|
+
// quote their values, so the rules above cannot see them. `valueGroup` keeps
|
|
1037
|
+
// `POSTGRES_PASSWORD:` in the output.
|
|
1038
|
+
rule: "credential-assignment-unquoted",
|
|
1039
|
+
pattern: new RegExp(
|
|
1040
|
+
`${UNQUOTED_STRONG_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
|
|
1041
|
+
"i"
|
|
1042
|
+
),
|
|
1043
|
+
valueGroup: 1
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
// `pass: hunter2hunter2` and `credentials: hunter2hunter2`: the ambiguous
|
|
1047
|
+
// names, unquoted, with the length and non-digit floor that keeps
|
|
1048
|
+
// `pass: 3` / `credentials: none` readable.
|
|
1049
|
+
rule: "credential-assignment-unquoted-weak",
|
|
1050
|
+
pattern: new RegExp(
|
|
1051
|
+
`${WEAK_CREDENTIAL_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
|
|
1052
|
+
"i"
|
|
1053
|
+
),
|
|
1054
|
+
valueGroup: 1
|
|
1055
|
+
},
|
|
1056
|
+
{
|
|
1057
|
+
// `Authorization: Bearer <token>` and `Authorization: Basic <base64>`.
|
|
1058
|
+
//
|
|
1059
|
+
// The scheme is consumed by a lookbehind so the match is the token alone and
|
|
1060
|
+
// needs no `valueGroup`. A lone `Bearer`/`Basic`, and a word after it that
|
|
1061
|
+
// cannot be a credential (`Bearer authentication`), stay readable: the value
|
|
1062
|
+
// must contain a digit, be at least 32 chars, or carry base64 `=` padding.
|
|
1063
|
+
rule: "authorization-header",
|
|
1064
|
+
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_.+/=-])/
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
// Broader names than before (`apiKey`, `clientSecret`, `privateKey` were
|
|
1068
|
+
// invisible to it) and value-only masking. Kept alongside the two rules
|
|
1069
|
+
// above because it also matches a *quoted key* followed by an unquoted
|
|
1070
|
+
// value, e.g. `"client_secret": abc123`.
|
|
1071
|
+
rule: "generic-secret-assignment",
|
|
1072
|
+
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["']([^"'\s]{8,256})["']/i,
|
|
1073
|
+
valueGroup: 1
|
|
1074
|
+
},
|
|
1075
|
+
{
|
|
1076
|
+
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
1077
|
+
// was therefore invisible to the rule above. Anchored to the line start and
|
|
1078
|
+
// limited to secret-looking names to stay at zero false positives.
|
|
1079
|
+
//
|
|
1080
|
+
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
1081
|
+
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
1082
|
+
// every offset of a long identifier, which made this rule quadratic
|
|
1083
|
+
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
1084
|
+
// The value is matched on its own — `=\s*(value)` — and masked alone.
|
|
1085
|
+
// Anchoring the value to end-of-line (`[^\s#]{6,512}$`) meant ANY trailing
|
|
1086
|
+
// comment or space disabled the rule, so `TOKEN=secret # rotate` and
|
|
1087
|
+
// `TOKEN=secret ` were emitted in clear; a CRLF file disabled it wholesale.
|
|
1088
|
+
rule: "env-secret-assignment",
|
|
1089
|
+
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,
|
|
1090
|
+
valueGroup: 1
|
|
990
1091
|
}
|
|
991
|
-
}
|
|
992
|
-
var DEFAULT_IGNORED_DIRS = [
|
|
993
|
-
"node_modules",
|
|
994
|
-
".git",
|
|
995
|
-
"dist",
|
|
996
|
-
"build",
|
|
997
|
-
".next",
|
|
998
|
-
".nuxt",
|
|
999
|
-
".turbo",
|
|
1000
|
-
".cache",
|
|
1001
|
-
"coverage",
|
|
1002
|
-
"out",
|
|
1003
|
-
"snapshot",
|
|
1004
|
-
"snapshot.tmp"
|
|
1005
|
-
];
|
|
1006
|
-
var DEFAULT_IGNORED_FILES = [
|
|
1007
|
-
"package-lock.json",
|
|
1008
|
-
"yarn.lock",
|
|
1009
|
-
"pnpm-lock.yaml",
|
|
1010
|
-
"bun.lockb",
|
|
1011
|
-
"composer.lock"
|
|
1012
1092
|
];
|
|
1013
|
-
var
|
|
1014
|
-
var
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
var
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
if (
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1093
|
+
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
1094
|
+
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
1095
|
+
|
|
1096
|
+
// src/security/redact.ts
|
|
1097
|
+
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
1098
|
+
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
1099
|
+
function describeValue(value) {
|
|
1100
|
+
if (value === null) return "null";
|
|
1101
|
+
if (Array.isArray(value)) return "an array";
|
|
1102
|
+
if (value instanceof RegExp) return value.toString();
|
|
1103
|
+
if (typeof value === "string") return `the string ${JSON.stringify(value)}`;
|
|
1104
|
+
return `a ${typeof value}`;
|
|
1105
|
+
}
|
|
1106
|
+
function compilePattern(rule, pattern, valueGroup) {
|
|
1107
|
+
if (pattern instanceof RegExp) {
|
|
1108
|
+
const flags = new Set(pattern.flags.replace(/[gy]/g, "").split(""));
|
|
1109
|
+
if (valueGroup !== void 0) flags.add("d");
|
|
1110
|
+
return {
|
|
1111
|
+
rule,
|
|
1112
|
+
pattern: new RegExp(pattern.source, [...flags].join("")),
|
|
1113
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
if (typeof pattern === "string") {
|
|
1117
|
+
try {
|
|
1118
|
+
return {
|
|
1119
|
+
rule,
|
|
1120
|
+
pattern: new RegExp(pattern, valueGroup === void 0 ? "i" : "id"),
|
|
1121
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1122
|
+
};
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
throw new Error(
|
|
1125
|
+
`Invalid regular expression for secret rule "${rule}": ${err.message}`
|
|
1126
|
+
);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
throw new Error(
|
|
1130
|
+
`Secret rule "${rule}" has a pattern that is neither a RegExp nor a string (received ${describeValue(pattern)}).`
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
function compileSecretRules(rules) {
|
|
1134
|
+
if (!Array.isArray(rules)) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
`secrets.customPatterns must be an array of secret rules (received ${describeValue(rules)}).`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
let generated = 0;
|
|
1140
|
+
return rules.map((entry, index) => {
|
|
1141
|
+
const where = `secrets.customPatterns[${index}]`;
|
|
1142
|
+
if (entry instanceof RegExp) {
|
|
1143
|
+
generated += 1;
|
|
1144
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1145
|
+
}
|
|
1146
|
+
if (typeof entry === "string") {
|
|
1147
|
+
generated += 1;
|
|
1148
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1149
|
+
}
|
|
1150
|
+
if (typeof entry === "object" && entry !== null) {
|
|
1151
|
+
const candidate = entry;
|
|
1152
|
+
if (typeof candidate.rule !== "string" || candidate.rule.trim() === "") {
|
|
1153
|
+
throw new Error(
|
|
1154
|
+
`${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.`
|
|
1155
|
+
);
|
|
1156
|
+
}
|
|
1157
|
+
const valueGroup = candidate.valueGroup;
|
|
1158
|
+
if (valueGroup !== void 0) {
|
|
1159
|
+
if (typeof valueGroup !== "number" || !Number.isInteger(valueGroup) || valueGroup < 0) {
|
|
1160
|
+
throw new Error(
|
|
1161
|
+
`${where}.valueGroup must be a non-negative integer capture-group index (received ${describeValue(valueGroup)}).`
|
|
1162
|
+
);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
if (candidate.pattern === void 0) {
|
|
1166
|
+
throw new Error(
|
|
1167
|
+
`${where} (rule "${candidate.rule}") is missing \`pattern\`. Expected { rule, pattern: RegExp | string }.`
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
return compilePattern(
|
|
1171
|
+
candidate.rule,
|
|
1172
|
+
candidate.pattern,
|
|
1173
|
+
valueGroup
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
throw new Error(
|
|
1177
|
+
`${where} must be a RegExp, a string, or { rule, pattern } (received ${describeValue(entry)}).`
|
|
1178
|
+
);
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
function toGlobal(rule) {
|
|
1182
|
+
const flags = new Set(rule.pattern.flags.replace(/[gy]/g, "").split(""));
|
|
1183
|
+
flags.add("g");
|
|
1184
|
+
if (rule.valueGroup !== void 0) flags.add("d");
|
|
1185
|
+
return new RegExp(rule.pattern.source, [...flags].join(""));
|
|
1186
|
+
}
|
|
1187
|
+
var LINE_BREAK = /(\r\n|\n|\r)/;
|
|
1188
|
+
function scanAndRedactSecrets(content, rules, filePath) {
|
|
1189
|
+
const findings = [];
|
|
1190
|
+
let redacted = content;
|
|
1191
|
+
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
1192
|
+
blockPattern.lastIndex = 0;
|
|
1193
|
+
let match;
|
|
1194
|
+
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
1195
|
+
if (match[0].length === 0) {
|
|
1196
|
+
blockPattern.lastIndex += 1;
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
const line = redacted.slice(0, match.index).split("\n").length;
|
|
1200
|
+
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
1201
|
+
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
1202
|
+
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
1203
|
+
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
const compiled = rules.map((rule) => ({
|
|
1207
|
+
rule: rule.rule,
|
|
1208
|
+
pattern: toGlobal(rule),
|
|
1209
|
+
...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
|
|
1210
|
+
}));
|
|
1211
|
+
const segments = redacted.split(LINE_BREAK);
|
|
1212
|
+
let lineNumber = 1;
|
|
1213
|
+
for (let index = 0; index < segments.length; index += 2) {
|
|
1214
|
+
const line = segments[index];
|
|
1215
|
+
if (line.length > 0) {
|
|
1216
|
+
const spans = [];
|
|
1217
|
+
const matchedRules = /* @__PURE__ */ new Set();
|
|
1218
|
+
for (const { rule, pattern, valueGroup } of compiled) {
|
|
1219
|
+
pattern.lastIndex = 0;
|
|
1220
|
+
let match;
|
|
1221
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
1222
|
+
if (match[0].length === 0) {
|
|
1223
|
+
pattern.lastIndex += 1;
|
|
1224
|
+
continue;
|
|
1225
|
+
}
|
|
1226
|
+
const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
|
|
1227
|
+
spans.push(
|
|
1228
|
+
group ? { start: group[0], end: group[1], rule } : { start: match.index, end: match.index + match[0].length, rule }
|
|
1229
|
+
);
|
|
1230
|
+
matchedRules.add(rule);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
if (spans.length > 0) {
|
|
1234
|
+
for (const rule of matchedRules) {
|
|
1235
|
+
findings.push({ rule, line: lineNumber, path: filePath });
|
|
1236
|
+
}
|
|
1237
|
+
segments[index] = applySpans(line, spans);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (index + 1 < segments.length) lineNumber += 1;
|
|
1241
|
+
}
|
|
1242
|
+
return { redacted: segments.join(""), findings };
|
|
1243
|
+
}
|
|
1244
|
+
var TOKEN_CHARS_LEFT = /[A-Za-z0-9_+/-]/;
|
|
1245
|
+
var TOKEN_CHARS_RIGHT = /[A-Za-z0-9_+/=-]/;
|
|
1246
|
+
function expandOverToken(line, start, end) {
|
|
1247
|
+
let left = start;
|
|
1248
|
+
let right = end;
|
|
1249
|
+
while (left > 0 && TOKEN_CHARS_LEFT.test(line[left - 1])) left -= 1;
|
|
1250
|
+
while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
|
|
1251
|
+
return { start: left, end: right };
|
|
1252
|
+
}
|
|
1253
|
+
function applySpans(line, spans) {
|
|
1254
|
+
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1255
|
+
const merged = [];
|
|
1256
|
+
for (const span of ordered) {
|
|
1257
|
+
const clampedStart = Math.max(0, Math.min(span.start, line.length));
|
|
1258
|
+
const clampedEnd = Math.max(clampedStart, Math.min(span.end, line.length));
|
|
1259
|
+
const { start, end } = expandOverToken(line, clampedStart, clampedEnd);
|
|
1260
|
+
const last = merged[merged.length - 1];
|
|
1261
|
+
if (last && start <= last.end) last.end = Math.max(last.end, end);
|
|
1262
|
+
else merged.push({ start, end });
|
|
1263
|
+
}
|
|
1264
|
+
let out = "";
|
|
1265
|
+
let cursor = 0;
|
|
1266
|
+
for (const span of merged) {
|
|
1267
|
+
out += line.slice(cursor, span.start);
|
|
1268
|
+
out += REDACTION_MARKER;
|
|
1269
|
+
cursor = span.end;
|
|
1270
|
+
}
|
|
1271
|
+
out += line.slice(cursor);
|
|
1272
|
+
return out;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// src/scanner/files.ts
|
|
1276
|
+
var PRESET_IGNORES = new Set(
|
|
1277
|
+
PRESETS.flatMap((preset) => preset.exclude ?? [])
|
|
1278
|
+
);
|
|
1279
|
+
function readLinkTarget(linkPath, fallback) {
|
|
1280
|
+
try {
|
|
1281
|
+
return fs9__namespace.readlinkSync(linkPath);
|
|
1282
|
+
} catch {
|
|
1283
|
+
return fallback;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
function isInside(root, candidate) {
|
|
1287
|
+
const relative5 = path10__namespace.relative(root, candidate);
|
|
1288
|
+
return relative5 === "" || !relative5.startsWith(`..${path10__namespace.sep}`) && relative5 !== ".." && !path10__namespace.isAbsolute(relative5);
|
|
1289
|
+
}
|
|
1290
|
+
function getGitInfo(cwd, fallbackBranch = "main") {
|
|
1291
|
+
try {
|
|
1292
|
+
const reported = childProcess__namespace.execSync("git rev-parse --abbrev-ref HEAD", {
|
|
1293
|
+
cwd,
|
|
1294
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1295
|
+
encoding: "utf8"
|
|
1296
|
+
}).trim();
|
|
1297
|
+
const branch = !reported || reported === "HEAD" ? fallbackBranch : reported;
|
|
1298
|
+
const commit = childProcess__namespace.execSync("git rev-parse HEAD", {
|
|
1299
|
+
cwd,
|
|
1300
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1301
|
+
encoding: "utf8"
|
|
1302
|
+
}).trim() || "unknown";
|
|
1303
|
+
const status = childProcess__namespace.execSync("git status --porcelain", {
|
|
1304
|
+
cwd,
|
|
1305
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1306
|
+
encoding: "utf8"
|
|
1307
|
+
}).trim();
|
|
1308
|
+
return { branch, commit, isDirty: status.length > 0 };
|
|
1309
|
+
} catch {
|
|
1310
|
+
return { branch: fallbackBranch, commit: "unknown", isDirty: false };
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
var DEFAULT_IGNORED_DIRS = [
|
|
1314
|
+
"node_modules",
|
|
1315
|
+
".git",
|
|
1316
|
+
"dist",
|
|
1317
|
+
"build",
|
|
1318
|
+
".next",
|
|
1319
|
+
".nuxt",
|
|
1320
|
+
".turbo",
|
|
1321
|
+
".cache",
|
|
1322
|
+
"coverage",
|
|
1323
|
+
"out",
|
|
1324
|
+
"snapshot",
|
|
1325
|
+
"snapshot.tmp"
|
|
1326
|
+
];
|
|
1327
|
+
var DEFAULT_IGNORED_FILES = [
|
|
1328
|
+
"package-lock.json",
|
|
1329
|
+
"yarn.lock",
|
|
1330
|
+
"pnpm-lock.yaml",
|
|
1331
|
+
"bun.lockb",
|
|
1332
|
+
"composer.lock"
|
|
1333
|
+
];
|
|
1334
|
+
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;
|
|
1335
|
+
var SENSITIVE_PATH = /(?:^|\/)\.(?:aws|ssh|gnupg)\/|(?:^|\/)\.docker\/config\.json$|(?:^|\/)\.kube\/config$/i;
|
|
1336
|
+
var SENSITIVE_EXT = /\.(?:pem|key|p12|pfx|jks|keystore|ppk|asc|kdbx|tfstate|tfvars)$/i;
|
|
1337
|
+
var TEMPLATE_SUFFIX = /\.(?:example|sample|template|dist)(?=$|[.])/i;
|
|
1338
|
+
var BACKUP_SUFFIX = /(?:~|\.(?:bak|backup|old|orig|save|copy|swp|swo|tmp|part))$/i;
|
|
1339
|
+
function normaliseName(name) {
|
|
1340
|
+
let normalised = name.trim().replace(/[. ]+$/, "");
|
|
1341
|
+
let previous = "";
|
|
1342
|
+
while (previous !== normalised) {
|
|
1343
|
+
previous = normalised;
|
|
1344
|
+
normalised = normalised.replace(BACKUP_SUFFIX, "");
|
|
1345
|
+
}
|
|
1346
|
+
return normalised;
|
|
1347
|
+
}
|
|
1348
|
+
var matchesSensitiveList = (posixPath, base) => SENSITIVE_BASENAME.test(base) || SENSITIVE_PATH.test(posixPath) || SENSITIVE_EXT.test(base);
|
|
1349
|
+
function isSensitivePath(relativePath) {
|
|
1350
|
+
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1351
|
+
const raw = posixPath.split("/").pop() ?? posixPath;
|
|
1352
|
+
const base = normaliseName(raw);
|
|
1353
|
+
if (TEMPLATE_SUFFIX.test(base)) {
|
|
1354
|
+
const stripped = base.replace(new RegExp(TEMPLATE_SUFFIX.source, "gi"), "");
|
|
1355
|
+
if (matchesSensitiveList(posixPath, stripped)) return false;
|
|
1356
|
+
}
|
|
1357
|
+
return matchesSensitiveList(posixPath, base);
|
|
1358
|
+
}
|
|
1359
|
+
var activeSecretRules = null;
|
|
1360
|
+
function setActiveSecretRules(rules) {
|
|
1361
|
+
activeSecretRules = rules === null ? null : [...rules];
|
|
1362
|
+
}
|
|
1363
|
+
function pathScanRules(extraRules) {
|
|
1364
|
+
const fallback = extraRules ?? activeSecretRules ?? [];
|
|
1365
|
+
return [...DEFAULT_SECRET_PATTERNS, ...fallback];
|
|
1366
|
+
}
|
|
1367
|
+
function findSecretInPath(relativePath, extraRules) {
|
|
1368
|
+
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1369
|
+
const spans = [];
|
|
1370
|
+
const rules = [];
|
|
1371
|
+
for (const { rule, pattern } of pathScanRules(extraRules)) {
|
|
1372
|
+
const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
|
|
1373
|
+
let match;
|
|
1374
|
+
while ((match = scan.exec(posixPath)) !== null) {
|
|
1375
|
+
if (match[0].length === 0) {
|
|
1376
|
+
scan.lastIndex += 1;
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
spans.push({ start: match.index, end: match.index + match[0].length });
|
|
1380
|
+
if (!rules.includes(rule)) rules.push(rule);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (spans.length === 0) return null;
|
|
1384
|
+
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1385
|
+
const merged = [];
|
|
1386
|
+
for (const span of spans) {
|
|
1387
|
+
const last = merged[merged.length - 1];
|
|
1388
|
+
if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
|
|
1389
|
+
else merged.push({ start: span.start, end: span.end });
|
|
1390
|
+
}
|
|
1391
|
+
let out = "";
|
|
1392
|
+
let cursor = 0;
|
|
1393
|
+
for (const span of merged) {
|
|
1394
|
+
out += posixPath.slice(cursor, span.start);
|
|
1395
|
+
out += REDACTION_MARKER;
|
|
1396
|
+
cursor = span.end;
|
|
1397
|
+
}
|
|
1398
|
+
out += posixPath.slice(cursor);
|
|
1399
|
+
return { rules, masked: out };
|
|
1400
|
+
}
|
|
1401
|
+
function maskSkipPaths(records, extraRules) {
|
|
1402
|
+
return records.map((record) => {
|
|
1403
|
+
const hit = findSecretInPath(record.path, extraRules);
|
|
1404
|
+
return hit ? { ...record, path: hit.masked } : record;
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
function globToRegExp(glob) {
|
|
1408
|
+
const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1409
|
+
const anchored = normalized.startsWith("/");
|
|
1410
|
+
const body = anchored ? normalized.slice(1) : normalized;
|
|
1031
1411
|
const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
|
|
1032
1412
|
return new RegExp(`${prefix}${globBodyToRegExp(body, true)}(?:/.*)?$`);
|
|
1033
1413
|
}
|
|
@@ -1144,12 +1524,120 @@ function defaultIgnoreReason(relativePath) {
|
|
|
1144
1524
|
}
|
|
1145
1525
|
return null;
|
|
1146
1526
|
}
|
|
1527
|
+
function listIgnoredPaths(cwd) {
|
|
1528
|
+
try {
|
|
1529
|
+
const stdout = childProcess__namespace.execSync(
|
|
1530
|
+
"git ls-files -z --others --ignored --exclude-standard --directory",
|
|
1531
|
+
{
|
|
1532
|
+
cwd,
|
|
1533
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1534
|
+
encoding: "utf8",
|
|
1535
|
+
maxBuffer: 32 * 1024 * 1024
|
|
1536
|
+
}
|
|
1537
|
+
);
|
|
1538
|
+
return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1539
|
+
} catch {
|
|
1540
|
+
return [];
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
|
|
1544
|
+
function isToolOwnedDirName(name) {
|
|
1545
|
+
return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
|
|
1546
|
+
}
|
|
1547
|
+
function hasSnapshotMarker(target) {
|
|
1548
|
+
try {
|
|
1549
|
+
return fs9__namespace.statSync(target).isDirectory() && fs9__namespace.existsSync(path10__namespace.join(target, ".codebase-snapshot"));
|
|
1550
|
+
} catch {
|
|
1551
|
+
return false;
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
function makeToolOwnedDecider(cwd) {
|
|
1555
|
+
const markers = /* @__PURE__ */ new Map();
|
|
1556
|
+
return (relativePath) => {
|
|
1557
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
1558
|
+
for (let depth = 1; depth < parts.length; depth += 1) {
|
|
1559
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
1560
|
+
if (depth === 1 && isToolOwnedDirName(prefix)) return true;
|
|
1561
|
+
let hasMarker = markers.get(prefix);
|
|
1562
|
+
if (hasMarker === void 0) {
|
|
1563
|
+
hasMarker = fs9__namespace.existsSync(path10__namespace.join(cwd, prefix, ".codebase-snapshot"));
|
|
1564
|
+
markers.set(prefix, hasMarker);
|
|
1565
|
+
}
|
|
1566
|
+
if (hasMarker) return true;
|
|
1567
|
+
}
|
|
1568
|
+
return false;
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
function makeLinkProbe() {
|
|
1572
|
+
const cache = /* @__PURE__ */ new Map();
|
|
1573
|
+
return (absolutePath) => {
|
|
1574
|
+
let known = cache.get(absolutePath);
|
|
1575
|
+
if (known === void 0) {
|
|
1576
|
+
try {
|
|
1577
|
+
known = fs9__namespace.lstatSync(absolutePath).isSymbolicLink();
|
|
1578
|
+
} catch {
|
|
1579
|
+
known = false;
|
|
1580
|
+
}
|
|
1581
|
+
cache.set(absolutePath, known);
|
|
1582
|
+
}
|
|
1583
|
+
return known;
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
function dropLinkedDirectories(cwd, candidates, skipped) {
|
|
1587
|
+
const isLink = makeLinkProbe();
|
|
1588
|
+
let rootReal = cwd;
|
|
1589
|
+
try {
|
|
1590
|
+
rootReal = fs9__namespace.realpathSync(cwd);
|
|
1591
|
+
} catch {
|
|
1592
|
+
}
|
|
1593
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1594
|
+
const kept = [];
|
|
1595
|
+
for (const candidate of candidates) {
|
|
1596
|
+
const parts = candidate.split("/");
|
|
1597
|
+
let linkedDir = null;
|
|
1598
|
+
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
1599
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
1600
|
+
if (isLink(path10__namespace.join(cwd, prefix))) {
|
|
1601
|
+
linkedDir = prefix;
|
|
1602
|
+
break;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
if (linkedDir === null) {
|
|
1606
|
+
kept.push(candidate);
|
|
1607
|
+
continue;
|
|
1608
|
+
}
|
|
1609
|
+
if (reported.has(linkedDir)) continue;
|
|
1610
|
+
reported.add(linkedDir);
|
|
1611
|
+
const absolute = path10__namespace.join(cwd, linkedDir);
|
|
1612
|
+
let targetReal = absolute;
|
|
1613
|
+
try {
|
|
1614
|
+
targetReal = fs9__namespace.realpathSync(absolute);
|
|
1615
|
+
} catch {
|
|
1616
|
+
}
|
|
1617
|
+
const target = readLinkTarget(absolute, targetReal);
|
|
1618
|
+
const what = linkedDir === candidate ? "symbolic link" : "directory link";
|
|
1619
|
+
skipped.push(
|
|
1620
|
+
isInside(rootReal, targetReal) ? {
|
|
1621
|
+
path: linkedDir,
|
|
1622
|
+
reason: "symlink",
|
|
1623
|
+
detail: `${what} not followed (target: ${target})`
|
|
1624
|
+
} : {
|
|
1625
|
+
path: linkedDir,
|
|
1626
|
+
reason: "outside-root",
|
|
1627
|
+
detail: `link target resolves outside the scanned root (target: ${target}); not followed`
|
|
1628
|
+
}
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
return kept;
|
|
1632
|
+
}
|
|
1147
1633
|
function collectFiles(cwd, options = {}) {
|
|
1148
1634
|
const includePatterns = options.includePatterns ?? [];
|
|
1149
1635
|
const excludePatterns = options.excludePatterns ?? [];
|
|
1150
1636
|
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
1637
|
+
const pathRules = options.secretRules;
|
|
1638
|
+
const isToolOwned = makeToolOwnedDecider(cwd);
|
|
1151
1639
|
let candidates;
|
|
1152
|
-
|
|
1640
|
+
const skipped = [];
|
|
1153
1641
|
try {
|
|
1154
1642
|
const stdout = childProcess__namespace.execSync(
|
|
1155
1643
|
"git ls-files -z --cached --others --exclude-standard",
|
|
@@ -1161,13 +1649,33 @@ function collectFiles(cwd, options = {}) {
|
|
|
1161
1649
|
}
|
|
1162
1650
|
);
|
|
1163
1651
|
candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1652
|
+
candidates = dropLinkedDirectories(cwd, candidates, skipped);
|
|
1653
|
+
const known = new Set(candidates);
|
|
1654
|
+
for (const ignoredPath of listIgnoredPaths(cwd)) {
|
|
1655
|
+
const trimmed = ignoredPath.replace(/\/+$/, "");
|
|
1656
|
+
if (known.has(trimmed) || known.has(ignoredPath)) continue;
|
|
1657
|
+
if (isToolOwned(trimmed)) continue;
|
|
1658
|
+
if (isSensitivePath(trimmed)) {
|
|
1659
|
+
skipped.push({
|
|
1660
|
+
path: trimmed,
|
|
1661
|
+
reason: "sensitive-file",
|
|
1662
|
+
detail: "credential-bearing file, ignored by .gitignore; enable includeSensitiveFiles to emit it"
|
|
1663
|
+
});
|
|
1664
|
+
} else {
|
|
1665
|
+
skipped.push({
|
|
1666
|
+
path: trimmed,
|
|
1667
|
+
reason: "gitignore",
|
|
1668
|
+
detail: ignoredPath.endsWith("/") ? "ignored by .gitignore (directory, not descended)" : "ignored by .gitignore"
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1164
1672
|
} catch {
|
|
1165
|
-
const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set());
|
|
1673
|
+
const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), true);
|
|
1166
1674
|
candidates = walked.files;
|
|
1167
|
-
|
|
1675
|
+
skipped.push(...walked.skipped);
|
|
1168
1676
|
}
|
|
1169
|
-
const skipped = [...walkSkips];
|
|
1170
1677
|
const files = candidates.filter((file) => {
|
|
1678
|
+
if (isToolOwned(file)) return false;
|
|
1171
1679
|
const ignored = defaultIgnoreReason(file);
|
|
1172
1680
|
if (ignored) {
|
|
1173
1681
|
skipped.push({ path: file, reason: "default-ignore", detail: ignored });
|
|
@@ -1183,10 +1691,11 @@ function collectFiles(cwd, options = {}) {
|
|
|
1183
1691
|
}
|
|
1184
1692
|
for (const pattern of excludePatterns) {
|
|
1185
1693
|
if (matchesExclude(file, pattern)) {
|
|
1694
|
+
const fromPreset = PRESET_IGNORES.has(pattern);
|
|
1186
1695
|
skipped.push({
|
|
1187
1696
|
path: file,
|
|
1188
|
-
reason: "excluded",
|
|
1189
|
-
detail: `matches exclude pattern "${pattern}"`
|
|
1697
|
+
reason: fromPreset ? "default-ignore" : "excluded",
|
|
1698
|
+
detail: fromPreset ? `preset ignore: "${pattern}"` : `matches exclude pattern "${pattern}"`
|
|
1190
1699
|
});
|
|
1191
1700
|
return false;
|
|
1192
1701
|
}
|
|
@@ -1199,56 +1708,119 @@ function collectFiles(cwd, options = {}) {
|
|
|
1199
1708
|
});
|
|
1200
1709
|
return false;
|
|
1201
1710
|
}
|
|
1711
|
+
const secret = findSecretInPath(file, pathRules);
|
|
1712
|
+
if (secret) {
|
|
1713
|
+
skipped.push({
|
|
1714
|
+
path: secret.masked,
|
|
1715
|
+
reason: "sensitive-path",
|
|
1716
|
+
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`
|
|
1717
|
+
});
|
|
1718
|
+
return false;
|
|
1719
|
+
}
|
|
1202
1720
|
return true;
|
|
1203
1721
|
}).sort();
|
|
1204
|
-
return { files, skipped };
|
|
1722
|
+
return { files, skipped: maskSkipPaths(skipped, pathRules) };
|
|
1205
1723
|
}
|
|
1206
|
-
function
|
|
1207
|
-
const results = { files: [], skipped: [] };
|
|
1208
|
-
let realDir;
|
|
1724
|
+
function rootRealFor(root) {
|
|
1209
1725
|
try {
|
|
1210
|
-
|
|
1726
|
+
return fs9__namespace.realpathSync(root);
|
|
1211
1727
|
} catch {
|
|
1212
|
-
|
|
1728
|
+
return root;
|
|
1213
1729
|
}
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
const
|
|
1730
|
+
}
|
|
1731
|
+
function walkDirectory(dir, baseDir, rootReal, visited, isRoot = false) {
|
|
1732
|
+
const results = { files: [], skipped: [] };
|
|
1733
|
+
if (visited.has(dir)) return results;
|
|
1734
|
+
visited.add(dir);
|
|
1735
|
+
const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
|
|
1217
1736
|
let entries;
|
|
1218
1737
|
try {
|
|
1219
1738
|
entries = fs9__namespace.readdirSync(dir, { withFileTypes: true });
|
|
1220
1739
|
} catch {
|
|
1221
|
-
results.skipped.push({
|
|
1740
|
+
results.skipped.push({
|
|
1741
|
+
path: path10__namespace.relative(baseDir, dir).replace(/\\/g, "/") || ".",
|
|
1742
|
+
reason: "read-error",
|
|
1743
|
+
detail: "directory unreadable"
|
|
1744
|
+
});
|
|
1222
1745
|
return results;
|
|
1223
1746
|
}
|
|
1224
1747
|
for (const entry of entries) {
|
|
1225
|
-
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) continue;
|
|
1226
1748
|
const fullPath = path10__namespace.join(dir, entry.name);
|
|
1227
1749
|
const relPath = path10__namespace.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
1228
|
-
if (
|
|
1750
|
+
if (isToolOwnedDirName(entry.name)) continue;
|
|
1751
|
+
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
|
|
1752
|
+
results.skipped.push({
|
|
1753
|
+
path: relPath,
|
|
1754
|
+
reason: "default-ignore",
|
|
1755
|
+
detail: `built-in ignore: directory "${entry.name}" (not descended)`
|
|
1756
|
+
});
|
|
1757
|
+
continue;
|
|
1758
|
+
}
|
|
1759
|
+
if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
|
|
1760
|
+
results.skipped.push({
|
|
1761
|
+
path: relPath,
|
|
1762
|
+
reason: "default-ignore",
|
|
1763
|
+
detail: "built-in ignore: filename"
|
|
1764
|
+
});
|
|
1765
|
+
continue;
|
|
1766
|
+
}
|
|
1767
|
+
if (ignoredByGitignore(relPath)) {
|
|
1768
|
+
if (!hasSnapshotMarker(fullPath)) {
|
|
1769
|
+
results.skipped.push({
|
|
1770
|
+
path: relPath,
|
|
1771
|
+
reason: isSensitivePath(relPath) ? "sensitive-file" : "gitignore",
|
|
1772
|
+
detail: "ignored by .gitignore"
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
continue;
|
|
1776
|
+
}
|
|
1229
1777
|
let isDirectory = entry.isDirectory();
|
|
1230
1778
|
let isFile = entry.isFile();
|
|
1231
1779
|
if (entry.isSymbolicLink()) {
|
|
1780
|
+
let targetReal;
|
|
1232
1781
|
try {
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1782
|
+
targetReal = fs9__namespace.realpathSync(fullPath);
|
|
1783
|
+
} catch {
|
|
1784
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
let stats;
|
|
1788
|
+
try {
|
|
1789
|
+
stats = fs9__namespace.statSync(fullPath);
|
|
1790
|
+
} catch {
|
|
1791
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
const linkTarget = readLinkTarget(fullPath, targetReal);
|
|
1795
|
+
if (!isInside(rootReal, targetReal)) {
|
|
1240
1796
|
results.skipped.push({
|
|
1241
1797
|
path: relPath,
|
|
1242
|
-
reason: "
|
|
1243
|
-
detail:
|
|
1798
|
+
reason: "outside-root",
|
|
1799
|
+
detail: `link target resolves outside the scanned root (target: ${linkTarget}); not followed`
|
|
1244
1800
|
});
|
|
1245
|
-
} catch {
|
|
1246
|
-
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1247
1801
|
continue;
|
|
1248
1802
|
}
|
|
1803
|
+
if (stats.isDirectory()) {
|
|
1804
|
+
results.skipped.push({
|
|
1805
|
+
path: relPath,
|
|
1806
|
+
reason: "symlink",
|
|
1807
|
+
detail: `directory link not followed (target: ${linkTarget})`
|
|
1808
|
+
});
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
if (!stats.isFile()) {
|
|
1812
|
+
results.skipped.push({
|
|
1813
|
+
path: relPath,
|
|
1814
|
+
reason: "symlink",
|
|
1815
|
+
detail: `link to neither a file nor a directory (target: ${linkTarget})`
|
|
1816
|
+
});
|
|
1817
|
+
continue;
|
|
1818
|
+
}
|
|
1819
|
+
isDirectory = false;
|
|
1820
|
+
isFile = true;
|
|
1249
1821
|
}
|
|
1250
1822
|
if (isDirectory) {
|
|
1251
|
-
const nested = walkDirectory(fullPath, baseDir, visited);
|
|
1823
|
+
const nested = walkDirectory(fullPath, baseDir, rootReal, visited);
|
|
1252
1824
|
results.files.push(...nested.files);
|
|
1253
1825
|
results.skipped.push(...nested.skipped);
|
|
1254
1826
|
} else if (isFile) {
|
|
@@ -1258,196 +1830,132 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1258
1830
|
return results;
|
|
1259
1831
|
}
|
|
1260
1832
|
|
|
1261
|
-
// src/
|
|
1262
|
-
|
|
1263
|
-
{
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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
|
|
1833
|
+
// src/diff.ts
|
|
1834
|
+
function side(manifest) {
|
|
1835
|
+
return {
|
|
1836
|
+
generatedAt: manifest.generatedAt,
|
|
1837
|
+
fingerprint: manifest.fingerprint,
|
|
1838
|
+
fileCount: manifest.counts.totalFiles,
|
|
1839
|
+
partsCount: manifest.counts.partsCount
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
function hashMap(manifest) {
|
|
1843
|
+
const map = /* @__PURE__ */ new Map();
|
|
1844
|
+
for (const file of manifest.files) {
|
|
1845
|
+
if (file && typeof file.relativePath === "string") {
|
|
1846
|
+
map.set(file.relativePath, file.hashSha256);
|
|
1847
|
+
}
|
|
1354
1848
|
}
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
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
|
-
};
|
|
1849
|
+
return map;
|
|
1850
|
+
}
|
|
1851
|
+
function manifestProblem(value) {
|
|
1852
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
|
|
1853
|
+
const candidate = value;
|
|
1854
|
+
if (!Array.isArray(candidate.files)) return 'no "files" array';
|
|
1855
|
+
for (const entry of candidate.files) {
|
|
1856
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1857
|
+
return 'a "files" entry is not an object';
|
|
1369
1858
|
}
|
|
1370
|
-
if (typeof
|
|
1371
|
-
|
|
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
|
-
}
|
|
1859
|
+
if (typeof entry.relativePath !== "string") {
|
|
1860
|
+
return 'a "files" entry has no "relativePath"';
|
|
1378
1861
|
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1862
|
+
}
|
|
1863
|
+
const counts = candidate.counts;
|
|
1864
|
+
if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
|
|
1865
|
+
return 'no "counts" object';
|
|
1866
|
+
}
|
|
1867
|
+
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
1868
|
+
if (typeof counts[key] !== "number") {
|
|
1869
|
+
return `no numeric "counts.${key}"`;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
|
|
1873
|
+
if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
|
|
1874
|
+
return null;
|
|
1383
1875
|
}
|
|
1384
|
-
function
|
|
1385
|
-
const
|
|
1386
|
-
|
|
1876
|
+
function assertUsableManifest(value, label) {
|
|
1877
|
+
const problem = manifestProblem(value);
|
|
1878
|
+
if (problem) {
|
|
1879
|
+
throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
|
|
1880
|
+
}
|
|
1387
1881
|
}
|
|
1388
|
-
function
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
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
|
-
}
|
|
1882
|
+
function compareManifests(before, after) {
|
|
1883
|
+
assertUsableManifest(before, "before");
|
|
1884
|
+
assertUsableManifest(after, "after");
|
|
1885
|
+
const left = hashMap(before);
|
|
1886
|
+
const right = hashMap(after);
|
|
1887
|
+
const added = [];
|
|
1888
|
+
const removed = [];
|
|
1889
|
+
const changed = [];
|
|
1890
|
+
let unchanged = 0;
|
|
1891
|
+
for (const [filePath, hash] of right) {
|
|
1892
|
+
const previous = left.get(filePath);
|
|
1893
|
+
if (previous === void 0) added.push(filePath);
|
|
1894
|
+
else if (previous !== hash) changed.push(filePath);
|
|
1895
|
+
else unchanged += 1;
|
|
1405
1896
|
}
|
|
1406
|
-
const
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
const
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
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 };
|
|
1897
|
+
for (const filePath of left.keys()) {
|
|
1898
|
+
if (!right.has(filePath)) removed.push(filePath);
|
|
1899
|
+
}
|
|
1900
|
+
const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
1901
|
+
const mask = (values) => values.map((value) => findSecretInPath(value)?.masked ?? value);
|
|
1902
|
+
return {
|
|
1903
|
+
before: side(before),
|
|
1904
|
+
after: side(after),
|
|
1905
|
+
added: mask(sort(added)),
|
|
1906
|
+
removed: mask(sort(removed)),
|
|
1907
|
+
changed: mask(sort(changed)),
|
|
1908
|
+
unchanged
|
|
1909
|
+
};
|
|
1434
1910
|
}
|
|
1435
|
-
function
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
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);
|
|
1911
|
+
function loadManifest(target) {
|
|
1912
|
+
let isDirectory;
|
|
1913
|
+
try {
|
|
1914
|
+
isDirectory = fs9__namespace.statSync(target).isDirectory();
|
|
1915
|
+
} catch {
|
|
1916
|
+
throw new Error(`No snapshot found at ${target}: the path does not exist.`);
|
|
1445
1917
|
}
|
|
1446
|
-
|
|
1447
|
-
|
|
1918
|
+
const manifestPath = isDirectory ? path10__namespace.join(target, "manifest.json") : target;
|
|
1919
|
+
let raw;
|
|
1920
|
+
try {
|
|
1921
|
+
raw = fs9__namespace.readFileSync(manifestPath, "utf8");
|
|
1922
|
+
} catch {
|
|
1923
|
+
throw new Error(`Could not read a manifest at ${manifestPath}.`);
|
|
1924
|
+
}
|
|
1925
|
+
let parsed;
|
|
1926
|
+
try {
|
|
1927
|
+
parsed = JSON.parse(raw);
|
|
1928
|
+
} catch (err) {
|
|
1929
|
+
throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
|
|
1930
|
+
}
|
|
1931
|
+
const problem = manifestProblem(parsed);
|
|
1932
|
+
if (problem) {
|
|
1933
|
+
throw new Error(
|
|
1934
|
+
`${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
return parsed;
|
|
1938
|
+
}
|
|
1939
|
+
function renderDiffText(diff) {
|
|
1940
|
+
const lines = [];
|
|
1941
|
+
lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
|
|
1942
|
+
lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
|
|
1943
|
+
lines.push("");
|
|
1944
|
+
lines.push(` + ${diff.added.length} added`);
|
|
1945
|
+
lines.push(` - ${diff.removed.length} removed`);
|
|
1946
|
+
lines.push(` ~ ${diff.changed.length} changed`);
|
|
1947
|
+
lines.push(` = ${diff.unchanged} unchanged`);
|
|
1948
|
+
const section = (title, values) => {
|
|
1949
|
+
if (values.length === 0) return;
|
|
1950
|
+
lines.push("", title);
|
|
1951
|
+
for (const value of values.slice(0, 200)) lines.push(` ${value}`);
|
|
1952
|
+
if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
|
|
1953
|
+
};
|
|
1954
|
+
section("added:", diff.added);
|
|
1955
|
+
section("removed:", diff.removed);
|
|
1956
|
+
section("changed:", diff.changed);
|
|
1957
|
+
return lines.join("\n");
|
|
1448
1958
|
}
|
|
1449
|
-
|
|
1450
|
-
// src/config.ts
|
|
1451
1959
|
var CONFIG_FILENAMES = [
|
|
1452
1960
|
"snapshot.config.ts",
|
|
1453
1961
|
"snapshot.config.js",
|
|
@@ -1496,7 +2004,12 @@ function normalizeResolvedConfig(config) {
|
|
|
1496
2004
|
),
|
|
1497
2005
|
include: resolveInclude(config.include),
|
|
1498
2006
|
preset: typeof config.preset === "string" ? config.preset : "generic",
|
|
1499
|
-
presetExclude:
|
|
2007
|
+
presetExclude: [
|
|
2008
|
+
.../* @__PURE__ */ new Set([
|
|
2009
|
+
...Array.isArray(config.presetExclude) ? config.presetExclude : [],
|
|
2010
|
+
...outputExcludePatterns(config.cwd, config.outDir, config.tmpDir)
|
|
2011
|
+
])
|
|
2012
|
+
],
|
|
1500
2013
|
layersMode: config.layersMode === "replace" ? "replace" : "merge",
|
|
1501
2014
|
budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
|
|
1502
2015
|
hooks: resolveHooks(config.hooks),
|
|
@@ -1533,7 +2046,68 @@ function supportsTypeScriptImports() {
|
|
|
1533
2046
|
return major > 22 || major === 22 && minor >= 6;
|
|
1534
2047
|
}
|
|
1535
2048
|
function stripJsonComments(raw) {
|
|
1536
|
-
|
|
2049
|
+
const text2 = raw.replace(/^\uFEFF/, "");
|
|
2050
|
+
let out = "";
|
|
2051
|
+
let inString = false;
|
|
2052
|
+
let escaped = false;
|
|
2053
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2054
|
+
const char = text2[index];
|
|
2055
|
+
if (inString) {
|
|
2056
|
+
out += char;
|
|
2057
|
+
if (escaped) escaped = false;
|
|
2058
|
+
else if (char === "\\") escaped = true;
|
|
2059
|
+
else if (char === '"') inString = false;
|
|
2060
|
+
continue;
|
|
2061
|
+
}
|
|
2062
|
+
if (char === '"') {
|
|
2063
|
+
inString = true;
|
|
2064
|
+
out += char;
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
if (char === "/" && text2[index + 1] === "/") {
|
|
2068
|
+
while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
|
|
2069
|
+
index -= 1;
|
|
2070
|
+
continue;
|
|
2071
|
+
}
|
|
2072
|
+
if (char === "/" && text2[index + 1] === "*") {
|
|
2073
|
+
index += 2;
|
|
2074
|
+
while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
|
|
2075
|
+
if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
|
|
2076
|
+
index += 1;
|
|
2077
|
+
}
|
|
2078
|
+
index += 1;
|
|
2079
|
+
continue;
|
|
2080
|
+
}
|
|
2081
|
+
out += char;
|
|
2082
|
+
}
|
|
2083
|
+
return stripTrailingCommas(out);
|
|
2084
|
+
}
|
|
2085
|
+
function stripTrailingCommas(text2) {
|
|
2086
|
+
let out = "";
|
|
2087
|
+
let inString = false;
|
|
2088
|
+
let escaped = false;
|
|
2089
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2090
|
+
const char = text2[index];
|
|
2091
|
+
if (inString) {
|
|
2092
|
+
out += char;
|
|
2093
|
+
if (escaped) escaped = false;
|
|
2094
|
+
else if (char === "\\") escaped = true;
|
|
2095
|
+
else if (char === '"') inString = false;
|
|
2096
|
+
continue;
|
|
2097
|
+
}
|
|
2098
|
+
if (char === '"') {
|
|
2099
|
+
inString = true;
|
|
2100
|
+
out += char;
|
|
2101
|
+
continue;
|
|
2102
|
+
}
|
|
2103
|
+
if (char === ",") {
|
|
2104
|
+
let next = index + 1;
|
|
2105
|
+
while (next < text2.length && /\s/.test(text2[next])) next += 1;
|
|
2106
|
+
if (text2[next] === "}" || text2[next] === "]") continue;
|
|
2107
|
+
}
|
|
2108
|
+
out += char;
|
|
2109
|
+
}
|
|
2110
|
+
return out;
|
|
1537
2111
|
}
|
|
1538
2112
|
function parseJsonish(raw, file) {
|
|
1539
2113
|
try {
|
|
@@ -1542,6 +2116,47 @@ function parseJsonish(raw, file) {
|
|
|
1542
2116
|
throw new Error(`Could not parse ${file}: ${err.message}`);
|
|
1543
2117
|
}
|
|
1544
2118
|
}
|
|
2119
|
+
function describeValue2(value) {
|
|
2120
|
+
if (value === null) return "null";
|
|
2121
|
+
if (Array.isArray(value)) return "an array";
|
|
2122
|
+
if (value instanceof RegExp) return value.toString();
|
|
2123
|
+
if (typeof value === "function") return "a function";
|
|
2124
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
2125
|
+
if (typeof value === "object") {
|
|
2126
|
+
const keys = Object.keys(value);
|
|
2127
|
+
return keys.length > 0 ? `an object with keys ${keys.join(", ")}` : "an empty object";
|
|
2128
|
+
}
|
|
2129
|
+
return `${typeof value} (${String(value)})`;
|
|
2130
|
+
}
|
|
2131
|
+
var isThenable = (value) => typeof value.then === "function";
|
|
2132
|
+
function readConfigExport(exported, file) {
|
|
2133
|
+
if (exported === void 0 || exported === null) {
|
|
2134
|
+
throw new Error(
|
|
2135
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2136
|
+
);
|
|
2137
|
+
}
|
|
2138
|
+
if (Array.isArray(exported)) {
|
|
2139
|
+
throw new Error(
|
|
2140
|
+
`${file} exports an array; expected a single configuration object such as \`export default { outDir: "snapshot" }\`.`
|
|
2141
|
+
);
|
|
2142
|
+
}
|
|
2143
|
+
if (typeof exported === "function") {
|
|
2144
|
+
throw new Error(
|
|
2145
|
+
`${file} exports a function; expected a configuration object. Call the function yourself and export its result.`
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
if (typeof exported !== "object") {
|
|
2149
|
+
throw new Error(
|
|
2150
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
if (isThenable(exported)) {
|
|
2154
|
+
throw new Error(
|
|
2155
|
+
`${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;\`).`
|
|
2156
|
+
);
|
|
2157
|
+
}
|
|
2158
|
+
return exported;
|
|
2159
|
+
}
|
|
1545
2160
|
async function loadConfigFileDetailed(cwd) {
|
|
1546
2161
|
const warnings = [];
|
|
1547
2162
|
const errors = [];
|
|
@@ -1571,13 +2186,7 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
1571
2186
|
}
|
|
1572
2187
|
try {
|
|
1573
2188
|
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
|
-
}
|
|
2189
|
+
const config = readConfigExport(imported.default ?? imported, fullPath);
|
|
1581
2190
|
return { config, file: fullPath, warnings, errors };
|
|
1582
2191
|
} catch (err) {
|
|
1583
2192
|
const message = `Could not load ${fullPath}: ${err.message}`;
|
|
@@ -1627,7 +2236,7 @@ var DEFAULT_ARTIFACTS = {
|
|
|
1627
2236
|
function isArtifactName(value) {
|
|
1628
2237
|
return ARTIFACT_NAMES.includes(value);
|
|
1629
2238
|
}
|
|
1630
|
-
function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
2239
|
+
function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
|
|
1631
2240
|
const artifacts = {
|
|
1632
2241
|
...DEFAULT_ARTIFACTS,
|
|
1633
2242
|
// `includeMonolith` predates this selection and stays the switch for it.
|
|
@@ -1649,6 +2258,9 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
1649
2258
|
artifacts[name] = value;
|
|
1650
2259
|
}
|
|
1651
2260
|
}
|
|
2261
|
+
if (monolithOverride !== void 0) {
|
|
2262
|
+
artifacts.monolith = monolithOverride;
|
|
2263
|
+
}
|
|
1652
2264
|
for (const name of skipped ?? []) {
|
|
1653
2265
|
if (!isArtifactName(name)) {
|
|
1654
2266
|
throw new Error(
|
|
@@ -1659,9 +2271,106 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
1659
2271
|
}
|
|
1660
2272
|
return artifacts;
|
|
1661
2273
|
}
|
|
1662
|
-
function
|
|
1663
|
-
if (
|
|
1664
|
-
|
|
2274
|
+
function strictPositiveInteger(value, fallback, label) {
|
|
2275
|
+
if (value === void 0) return fallback;
|
|
2276
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
2277
|
+
throw new Error(`${label} must be a positive integer (received ${describeValue2(value)}).`);
|
|
2278
|
+
}
|
|
2279
|
+
return value;
|
|
2280
|
+
}
|
|
2281
|
+
function strictBoolean(value, fallback, label) {
|
|
2282
|
+
if (value === void 0) return fallback;
|
|
2283
|
+
if (typeof value !== "boolean") {
|
|
2284
|
+
throw new Error(`${label} must be true or false (received ${describeValue2(value)}).`);
|
|
2285
|
+
}
|
|
2286
|
+
return value;
|
|
2287
|
+
}
|
|
2288
|
+
function strictString(value, fallback, label) {
|
|
2289
|
+
if (value === void 0) return fallback;
|
|
2290
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2291
|
+
throw new Error(`${label} must be a non-empty string (received ${describeValue2(value)}).`);
|
|
2292
|
+
}
|
|
2293
|
+
return value;
|
|
2294
|
+
}
|
|
2295
|
+
function resolveExcludePatterns(value) {
|
|
2296
|
+
if (value === void 0) return [];
|
|
2297
|
+
if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string")) {
|
|
2298
|
+
throw new Error(
|
|
2299
|
+
`\`excludePatterns\` must be an array of glob strings such as ["**/*.test.ts"] (received ${describeValue2(value)}).`
|
|
2300
|
+
);
|
|
2301
|
+
}
|
|
2302
|
+
return value.map((pattern) => pattern.trim()).filter(Boolean);
|
|
2303
|
+
}
|
|
2304
|
+
var AUTO_PRESET_ALIASES = /* @__PURE__ */ new Set(["auto", "detected"]);
|
|
2305
|
+
function resolvePresetName(value) {
|
|
2306
|
+
if (value === void 0) return void 0;
|
|
2307
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2308
|
+
throw new Error(
|
|
2309
|
+
`\`preset\` must be a preset name (received ${describeValue2(value)}). Use "detected" (or "auto") to detect one, or one of: ${listPresetNames().join(", ")}.`
|
|
2310
|
+
);
|
|
2311
|
+
}
|
|
2312
|
+
const name = value.trim();
|
|
2313
|
+
return AUTO_PRESET_ALIASES.has(name.toLowerCase()) ? void 0 : name;
|
|
2314
|
+
}
|
|
2315
|
+
function resolveRuleList(value, kind) {
|
|
2316
|
+
const collection = kind === "layer" ? "layers" : "kinds";
|
|
2317
|
+
const example = kind === "layer" ? `{ name: "api", match: "src/api/**" }` : `{ kind: "route", match: "src/routes/**" }`;
|
|
2318
|
+
if (value === void 0) return [];
|
|
2319
|
+
if (!Array.isArray(value)) {
|
|
2320
|
+
throw new Error(
|
|
2321
|
+
`\`${collection}\` must be an array of ${kind} rules (received ${describeValue2(value)}). A ${kind} rule is ${example}.`
|
|
2322
|
+
);
|
|
2323
|
+
}
|
|
2324
|
+
return value.map((entry, index) => {
|
|
2325
|
+
const where = `${collection}[${index}]`;
|
|
2326
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
2327
|
+
throw new Error(`${where} must be an object (received ${describeValue2(entry)}). A ${kind} rule is ${example}.`);
|
|
2328
|
+
}
|
|
2329
|
+
const record = entry;
|
|
2330
|
+
const keys = Object.keys(record);
|
|
2331
|
+
const canonical = kind === "layer" ? "name" : "kind";
|
|
2332
|
+
const alias = kind === "layer" ? "layer" : "name";
|
|
2333
|
+
const rawName = typeof record[canonical] === "string" ? record[canonical] : typeof record[alias] === "string" ? record[alias] : void 0;
|
|
2334
|
+
if (rawName === void 0 || rawName.trim() === "") {
|
|
2335
|
+
throw new Error(
|
|
2336
|
+
`${where} is missing \`${canonical}\` (received keys: ${keys.join(", ") || "none"}). A ${kind} rule is ${example}; \`${alias}\` is accepted as an alias of \`${canonical}\`.`
|
|
2337
|
+
);
|
|
2338
|
+
}
|
|
2339
|
+
const match = record.match;
|
|
2340
|
+
if (match === void 0) {
|
|
2341
|
+
throw new Error(
|
|
2342
|
+
`${where}.match is required (received keys: ${keys.join(", ") || "none"}). Use a glob string ("src/api/**"), a RegExp (/\\\\.tsx$/) or a function.`
|
|
2343
|
+
);
|
|
2344
|
+
}
|
|
2345
|
+
if (!(typeof match === "string" || match instanceof RegExp || typeof match === "function")) {
|
|
2346
|
+
throw new Error(`${where}.match must be a string, a RegExp or a function (received ${describeValue2(match)}).`);
|
|
2347
|
+
}
|
|
2348
|
+
if (typeof match === "string" && match.trim() === "") {
|
|
2349
|
+
throw new Error(`${where}.match must not be an empty string.`);
|
|
2350
|
+
}
|
|
2351
|
+
return {
|
|
2352
|
+
name: rawName.trim(),
|
|
2353
|
+
match
|
|
2354
|
+
};
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
function resolveLayerRules(value) {
|
|
2358
|
+
return resolveRuleList(value, "layer").map((rule) => ({ name: rule.name, match: rule.match }));
|
|
2359
|
+
}
|
|
2360
|
+
function resolveKindRules(value) {
|
|
2361
|
+
return resolveRuleList(value, "kind").map((rule) => ({ kind: rule.name, match: rule.match }));
|
|
2362
|
+
}
|
|
2363
|
+
function outputExcludePatterns(cwd, outDir, tmpDir) {
|
|
2364
|
+
const patterns = [];
|
|
2365
|
+
for (const dir of [outDir, tmpDir]) {
|
|
2366
|
+
const absolute = path10__namespace.resolve(cwd, dir);
|
|
2367
|
+
const relative5 = path10__namespace.relative(cwd, absolute);
|
|
2368
|
+
if (!relative5 || relative5 === ".." || relative5.startsWith(`..${path10__namespace.sep}`) || path10__namespace.isAbsolute(relative5)) {
|
|
2369
|
+
continue;
|
|
2370
|
+
}
|
|
2371
|
+
patterns.push(`${relative5.replace(/\\/g, "/")}/**`);
|
|
2372
|
+
}
|
|
2373
|
+
return patterns;
|
|
1665
2374
|
}
|
|
1666
2375
|
var BYTE_UNITS = {
|
|
1667
2376
|
b: 1,
|
|
@@ -1730,11 +2439,13 @@ function resolveLayersMode(mode) {
|
|
|
1730
2439
|
}
|
|
1731
2440
|
return mode;
|
|
1732
2441
|
}
|
|
1733
|
-
function resolveBudgets(budgets, fallback) {
|
|
2442
|
+
function resolveBudgets(budgets, fallback, overrideDefault) {
|
|
1734
2443
|
const byLayer = {};
|
|
1735
2444
|
if (budgets === void 0) return { default: fallback, byLayer };
|
|
1736
2445
|
if (typeof budgets !== "object" || budgets === null || Array.isArray(budgets)) {
|
|
1737
|
-
throw new Error(
|
|
2446
|
+
throw new Error(
|
|
2447
|
+
`\`budgets\` must be an object such as { default: "120k", byLayer: { api: "60k" } } (received ${describeValue2(budgets)}).`
|
|
2448
|
+
);
|
|
1738
2449
|
}
|
|
1739
2450
|
if (budgets.byLayer !== void 0) {
|
|
1740
2451
|
if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
|
|
@@ -1749,10 +2460,12 @@ function resolveBudgets(budgets, fallback) {
|
|
|
1749
2460
|
);
|
|
1750
2461
|
}
|
|
1751
2462
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
2463
|
+
const resolvedDefault = boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default");
|
|
2464
|
+
if (!overrideDefault) return { default: resolvedDefault, byLayer };
|
|
2465
|
+
for (const [layer2, value] of Object.entries(byLayer)) {
|
|
2466
|
+
byLayer[layer2] = Math.min(value, fallback);
|
|
2467
|
+
}
|
|
2468
|
+
return { default: fallback, byLayer };
|
|
1756
2469
|
}
|
|
1757
2470
|
function resolveHooks(hooks) {
|
|
1758
2471
|
if (hooks === void 0) return {};
|
|
@@ -1765,6 +2478,11 @@ function resolveHooks(hooks) {
|
|
|
1765
2478
|
return { ...hooks };
|
|
1766
2479
|
}
|
|
1767
2480
|
function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
2481
|
+
if (userConfig?.cwd !== void 0 && (typeof userConfig.cwd !== "string" || userConfig.cwd.trim() === "")) {
|
|
2482
|
+
throw new Error(
|
|
2483
|
+
`\`cwd\` must be a non-empty path string (received ${describeValue2(userConfig.cwd)}).`
|
|
2484
|
+
);
|
|
2485
|
+
}
|
|
1768
2486
|
const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
|
|
1769
2487
|
const resolved = path10__namespace.resolve(candidate);
|
|
1770
2488
|
if (!fs9__namespace.existsSync(resolved)) {
|
|
@@ -1777,55 +2495,107 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
|
1777
2495
|
}
|
|
1778
2496
|
function resolveConfig(userConfig, cliOptions, baseDir) {
|
|
1779
2497
|
const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
|
|
1780
|
-
const forcedPreset = userConfig?.preset;
|
|
2498
|
+
const forcedPreset = resolvePresetName(userConfig?.preset);
|
|
1781
2499
|
if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
|
|
1782
2500
|
throw new Error(unknownPresetMessage(forcedPreset));
|
|
1783
2501
|
}
|
|
1784
2502
|
const preset = detectPreset(cwd, forcedPreset);
|
|
1785
2503
|
const layersMode = resolveLayersMode(userConfig?.layersMode);
|
|
1786
|
-
const
|
|
1787
|
-
const
|
|
1788
|
-
const
|
|
1789
|
-
const
|
|
1790
|
-
const
|
|
1791
|
-
|
|
2504
|
+
const userLayers = resolveLayerRules(userConfig?.layers);
|
|
2505
|
+
const userKinds = resolveKindRules(userConfig?.kinds);
|
|
2506
|
+
const layers = userLayers.length > 0 ? layersMode === "replace" ? userLayers : [...userLayers, ...preset.layers] : preset.layers;
|
|
2507
|
+
const kinds = userKinds.length > 0 ? layersMode === "replace" ? userKinds : [...userKinds, ...preset.kinds] : preset.kinds;
|
|
2508
|
+
const secrets = userConfig?.secrets;
|
|
2509
|
+
if (secrets !== void 0 && (typeof secrets !== "object" || Array.isArray(secrets))) {
|
|
2510
|
+
throw new Error(
|
|
2511
|
+
`\`secrets\` must be an object of { customPatterns, disableDefaultPatterns } (received ${describeValue2(secrets)}).`
|
|
2512
|
+
);
|
|
2513
|
+
}
|
|
2514
|
+
const customRules = secrets?.customPatterns ?? [];
|
|
2515
|
+
if (!Array.isArray(customRules)) {
|
|
2516
|
+
throw new Error(
|
|
2517
|
+
`\`secrets.customPatterns\` must be an array of secret rules (received ${describeValue2(customRules)}). Each entry is a RegExp, a string, or { rule, pattern }.`
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
const disableDefaultPatterns = strictBoolean(
|
|
2521
|
+
secrets?.disableDefaultPatterns,
|
|
2522
|
+
false,
|
|
2523
|
+
"`secrets.disableDefaultPatterns`"
|
|
2524
|
+
);
|
|
2525
|
+
const aliasedRules = userConfig?.secretRules;
|
|
2526
|
+
if (aliasedRules !== void 0 && !Array.isArray(aliasedRules)) {
|
|
2527
|
+
throw new Error(
|
|
2528
|
+
`\`secretRules\` must be an array of { rule, pattern } entries (received ${describeValue2(aliasedRules)}).`
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
const compiledCustomRules = compileSecretRules([
|
|
2532
|
+
...customRules,
|
|
2533
|
+
...aliasedRules ?? []
|
|
2534
|
+
]);
|
|
2535
|
+
const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
|
|
2536
|
+
setActiveSecretRules(secretRules);
|
|
2537
|
+
const cliMonolith = cliOptions?.includeMonolith;
|
|
2538
|
+
const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
|
|
1792
2539
|
const artifacts = resolveArtifacts(
|
|
1793
2540
|
userConfig?.artifacts,
|
|
1794
2541
|
cliOptions?.skipArtifacts,
|
|
1795
|
-
includeMonolith
|
|
2542
|
+
includeMonolith,
|
|
2543
|
+
cliMonolith
|
|
1796
2544
|
);
|
|
2545
|
+
const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
|
|
1797
2546
|
const maxPartBytes = boundedBytes(
|
|
1798
|
-
|
|
2547
|
+
explicitMaxPartBytes,
|
|
1799
2548
|
300 * 1024,
|
|
1800
2549
|
MIN_MAX_PART_BYTES,
|
|
1801
2550
|
"maxPartBytes"
|
|
1802
2551
|
);
|
|
1803
|
-
const budgets = resolveBudgets(
|
|
2552
|
+
const budgets = resolveBudgets(
|
|
2553
|
+
userConfig?.budgets,
|
|
2554
|
+
maxPartBytes,
|
|
2555
|
+
explicitMaxPartBytes !== void 0
|
|
2556
|
+
);
|
|
2557
|
+
const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
|
|
2558
|
+
const outDir = cliOptions?.outDir || configuredOutDir;
|
|
2559
|
+
const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
|
|
2560
|
+
const branch = strictString(userConfig?.branch, "main", "`branch`");
|
|
2561
|
+
const configuredSensitiveFiles = strictBoolean(
|
|
2562
|
+
userConfig?.includeSensitiveFiles,
|
|
2563
|
+
false,
|
|
2564
|
+
"`includeSensitiveFiles`"
|
|
2565
|
+
);
|
|
2566
|
+
const includeSensitiveFiles = strictBoolean(
|
|
2567
|
+
cliOptions?.includeSensitiveFiles,
|
|
2568
|
+
configuredSensitiveFiles,
|
|
2569
|
+
"`includeSensitiveFiles`"
|
|
2570
|
+
);
|
|
2571
|
+
const presetExclude = [
|
|
2572
|
+
.../* @__PURE__ */ new Set([...preset.exclude, ...outputExcludePatterns(cwd, outDir, tmpDir)])
|
|
2573
|
+
];
|
|
1804
2574
|
return markResolvedConfig({
|
|
1805
2575
|
cwd,
|
|
1806
|
-
branch
|
|
1807
|
-
outDir
|
|
1808
|
-
tmpDir
|
|
2576
|
+
branch,
|
|
2577
|
+
outDir,
|
|
2578
|
+
tmpDir,
|
|
1809
2579
|
maxPartBytes,
|
|
1810
2580
|
largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
|
|
1811
|
-
previewHeadLines:
|
|
1812
|
-
previewTailLines:
|
|
2581
|
+
previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
|
|
2582
|
+
previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
|
|
1813
2583
|
includeMonolith: artifacts.monolith,
|
|
1814
2584
|
maxMonolithBytes: optionalBytes(
|
|
1815
2585
|
cliOptions?.maxMonolithBytes !== void 0 ? cliOptions.maxMonolithBytes : userConfig?.maxMonolithBytes,
|
|
1816
2586
|
"maxMonolithBytes"
|
|
1817
2587
|
),
|
|
1818
|
-
includeSensitiveFiles
|
|
2588
|
+
includeSensitiveFiles,
|
|
1819
2589
|
include: resolveInclude(userConfig?.include),
|
|
1820
2590
|
preset: preset.presetName,
|
|
1821
|
-
presetExclude
|
|
2591
|
+
presetExclude,
|
|
1822
2592
|
artifacts,
|
|
1823
2593
|
layersMode,
|
|
1824
2594
|
budgets,
|
|
1825
2595
|
hooks: resolveHooks(userConfig?.hooks),
|
|
1826
2596
|
layers,
|
|
1827
2597
|
kinds,
|
|
1828
|
-
excludePatterns: userConfig?.excludePatterns
|
|
2598
|
+
excludePatterns: resolveExcludePatterns(userConfig?.excludePatterns),
|
|
1829
2599
|
secretRules
|
|
1830
2600
|
});
|
|
1831
2601
|
}
|
|
@@ -1904,6 +2674,75 @@ function truncatePreview(content, headLines, tailLines) {
|
|
|
1904
2674
|
includedLines: headLines + tailLines + 1
|
|
1905
2675
|
};
|
|
1906
2676
|
}
|
|
2677
|
+
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2678
|
+
".png",
|
|
2679
|
+
".jpg",
|
|
2680
|
+
".jpeg",
|
|
2681
|
+
".gif",
|
|
2682
|
+
".webp",
|
|
2683
|
+
".avif",
|
|
2684
|
+
".ico",
|
|
2685
|
+
".bmp",
|
|
2686
|
+
".tiff",
|
|
2687
|
+
".pdf",
|
|
2688
|
+
".zip",
|
|
2689
|
+
".tar",
|
|
2690
|
+
".gz",
|
|
2691
|
+
".tgz",
|
|
2692
|
+
".bz2",
|
|
2693
|
+
".xz",
|
|
2694
|
+
".7z",
|
|
2695
|
+
".rar",
|
|
2696
|
+
".mp3",
|
|
2697
|
+
".mp4",
|
|
2698
|
+
".wav",
|
|
2699
|
+
".ogg",
|
|
2700
|
+
".webm",
|
|
2701
|
+
".mov",
|
|
2702
|
+
".avi",
|
|
2703
|
+
".woff",
|
|
2704
|
+
".woff2",
|
|
2705
|
+
".ttf",
|
|
2706
|
+
".otf",
|
|
2707
|
+
".eot",
|
|
2708
|
+
".wasm",
|
|
2709
|
+
".node",
|
|
2710
|
+
".exe",
|
|
2711
|
+
".dll",
|
|
2712
|
+
".dylib",
|
|
2713
|
+
".so"
|
|
2714
|
+
]);
|
|
2715
|
+
function isBinaryExtension(filePath) {
|
|
2716
|
+
const ext = path10__namespace.extname(filePath).toLowerCase();
|
|
2717
|
+
return BINARY_EXTENSIONS.has(ext);
|
|
2718
|
+
}
|
|
2719
|
+
function isBinaryBuffer(buffer) {
|
|
2720
|
+
const checkLength = Math.min(buffer.length, 8192);
|
|
2721
|
+
for (let index = 0; index < checkLength; index++) {
|
|
2722
|
+
if (buffer[index] === 0) return true;
|
|
2723
|
+
}
|
|
2724
|
+
return false;
|
|
2725
|
+
}
|
|
2726
|
+
function isBinaryFile(absolutePath) {
|
|
2727
|
+
if (isBinaryExtension(absolutePath)) return true;
|
|
2728
|
+
let fd;
|
|
2729
|
+
try {
|
|
2730
|
+
fd = fs9__namespace.openSync(absolutePath, "r");
|
|
2731
|
+
const buffer = Buffer.alloc(8192);
|
|
2732
|
+
const bytesRead = fs9__namespace.readSync(fd, buffer, 0, 8192, 0);
|
|
2733
|
+
if (bytesRead === 0) return false;
|
|
2734
|
+
return isBinaryBuffer(buffer.subarray(0, bytesRead));
|
|
2735
|
+
} catch {
|
|
2736
|
+
return false;
|
|
2737
|
+
} finally {
|
|
2738
|
+
if (fd !== void 0) {
|
|
2739
|
+
try {
|
|
2740
|
+
fs9__namespace.closeSync(fd);
|
|
2741
|
+
} catch {
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
1907
2746
|
var LANGUAGE_BY_EXTENSION = {
|
|
1908
2747
|
".ts": "typescript",
|
|
1909
2748
|
".mts": "typescript",
|
|
@@ -1970,16 +2809,15 @@ function describeMatcher(matcher) {
|
|
|
1970
2809
|
if (typeof matcher === "string") return matcher;
|
|
1971
2810
|
if (matcher instanceof RegExp) return String(matcher);
|
|
1972
2811
|
if (typeof matcher === "function") {
|
|
1973
|
-
|
|
1974
|
-
return source.length > 70 ? `${source.slice(0, 67)}...` : source;
|
|
2812
|
+
return "function (custom predicate)";
|
|
1975
2813
|
}
|
|
1976
2814
|
return String(matcher);
|
|
1977
2815
|
}
|
|
1978
2816
|
function normalizeTarget(cwd, target) {
|
|
1979
2817
|
const absolute = path10__namespace.resolve(cwd, target);
|
|
1980
|
-
const
|
|
1981
|
-
if (
|
|
1982
|
-
return
|
|
2818
|
+
const relative5 = path10__namespace.relative(cwd, absolute);
|
|
2819
|
+
if (relative5.startsWith("..") || path10__namespace.isAbsolute(relative5)) return target;
|
|
2820
|
+
return relative5.split(path10__namespace.sep).join("/");
|
|
1983
2821
|
}
|
|
1984
2822
|
function findRuleMatch(rules, relativePath) {
|
|
1985
2823
|
return rules.find(
|
|
@@ -1995,20 +2833,24 @@ function readManifest2(config) {
|
|
|
1995
2833
|
}
|
|
1996
2834
|
}
|
|
1997
2835
|
function explainPath(config, target) {
|
|
1998
|
-
const
|
|
1999
|
-
const absolute = path10__namespace.join(config.cwd,
|
|
2836
|
+
const relative5 = normalizeTarget(config.cwd, target);
|
|
2837
|
+
const absolute = path10__namespace.join(config.cwd, relative5);
|
|
2838
|
+
const maskedPath2 = findSecretInPath(relative5, config.secretRules)?.masked ?? relative5;
|
|
2000
2839
|
const result = {
|
|
2001
|
-
path:
|
|
2840
|
+
path: maskedPath2,
|
|
2002
2841
|
exists: fs9__namespace.existsSync(absolute),
|
|
2003
2842
|
included: false
|
|
2004
2843
|
};
|
|
2005
2844
|
const collection = collectFiles(config.cwd, {
|
|
2006
2845
|
includePatterns: config.include,
|
|
2007
2846
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
2008
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
2847
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
2848
|
+
secretRules: config.secretRules
|
|
2009
2849
|
});
|
|
2010
|
-
if (!collection.files.includes(
|
|
2011
|
-
const record = collection.skipped.find(
|
|
2850
|
+
if (!collection.files.includes(relative5)) {
|
|
2851
|
+
const record = collection.skipped.find(
|
|
2852
|
+
(entry2) => entry2.path === relative5 || entry2.path === maskedPath2
|
|
2853
|
+
);
|
|
2012
2854
|
if (record) {
|
|
2013
2855
|
result.reason = record.reason;
|
|
2014
2856
|
result.detail = record.detail;
|
|
@@ -2022,24 +2864,30 @@ function explainPath(config, target) {
|
|
|
2022
2864
|
return result;
|
|
2023
2865
|
}
|
|
2024
2866
|
result.included = true;
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2867
|
+
if (isBinaryFile(absolute)) {
|
|
2868
|
+
result.included = false;
|
|
2869
|
+
result.reason = "binary";
|
|
2870
|
+
result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
|
|
2871
|
+
return result;
|
|
2872
|
+
}
|
|
2873
|
+
result.layer = classifyLayer(relative5, config.layers);
|
|
2874
|
+
result.kind = classifyKind(relative5, config.kinds);
|
|
2875
|
+
result.language = detectLanguage(relative5);
|
|
2876
|
+
const layerRule = findRuleMatch(config.layers, relative5);
|
|
2029
2877
|
if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
|
|
2030
|
-
const kindRule = findRuleMatch(config.kinds,
|
|
2878
|
+
const kindRule = findRuleMatch(config.kinds, relative5);
|
|
2031
2879
|
if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
|
|
2032
2880
|
if (fs9__namespace.existsSync(absolute) && fs9__namespace.statSync(absolute).isFile()) {
|
|
2033
2881
|
const buffer = fs9__namespace.readFileSync(absolute);
|
|
2034
2882
|
const text2 = buffer.toString("utf8");
|
|
2035
2883
|
result.bytes = buffer.length;
|
|
2036
2884
|
result.lines = countLines(text2);
|
|
2037
|
-
const { findings } = scanAndRedactSecrets(text2, config.secretRules,
|
|
2885
|
+
const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative5);
|
|
2038
2886
|
result.secretCount = findings.length;
|
|
2039
2887
|
result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
|
|
2040
2888
|
}
|
|
2041
2889
|
const manifest = readManifest2(config);
|
|
2042
|
-
const entry = manifest?.files.find((file) => file.relativePath ===
|
|
2890
|
+
const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative5) : void 0;
|
|
2043
2891
|
result.part = entry?.part ?? null;
|
|
2044
2892
|
result.inclusionMode = entry?.inclusionMode;
|
|
2045
2893
|
return result;
|
|
@@ -2163,7 +3011,7 @@ function renderConfig(answers) {
|
|
|
2163
3011
|
secrets: {
|
|
2164
3012
|
// [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
|
|
2165
3013
|
customPatterns: [],
|
|
2166
|
-
// Set true to drop the
|
|
3014
|
+
// Set true to drop the built-in rules above and keep only customPatterns.
|
|
2167
3015
|
disableDefaultPatterns: false,
|
|
2168
3016
|
},
|
|
2169
3017
|
|
|
@@ -2179,6 +3027,10 @@ function renderConfig(answers) {
|
|
|
2179
3027
|
},
|
|
2180
3028
|
// Skip the monolith above this size. null = no limit.
|
|
2181
3029
|
maxMonolithBytes: null,
|
|
3030
|
+
// Top-level switch for the monolith, equivalent to artifacts.monolith.
|
|
3031
|
+
// When both are present the artifact entry wins; this one exists so the
|
|
3032
|
+
// generated file lists every setting the tool accepts.
|
|
3033
|
+
includeMonolith: ${artifacts.monolith},
|
|
2182
3034
|
|
|
2183
3035
|
// \u2500\u2500 Advanced \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2184
3036
|
// afterGenerate: (manifest) => { /* write your own artifact */ }
|
|
@@ -2500,6 +3352,14 @@ function existingConfig(cwd) {
|
|
|
2500
3352
|
}
|
|
2501
3353
|
return null;
|
|
2502
3354
|
}
|
|
3355
|
+
function packageJsonHasConfig(cwd) {
|
|
3356
|
+
try {
|
|
3357
|
+
const parsed = JSON.parse(fs9__namespace.readFileSync(path10__namespace.join(cwd, "package.json"), "utf8"));
|
|
3358
|
+
return typeof parsed.snapshot === "object" && parsed.snapshot !== null;
|
|
3359
|
+
} catch {
|
|
3360
|
+
return false;
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
2503
3363
|
function artifactAnswers(options) {
|
|
2504
3364
|
const artifacts = { ...DEFAULT_ANSWERS.artifacts };
|
|
2505
3365
|
for (const name of options.skipArtifacts ?? []) {
|
|
@@ -2712,6 +3572,11 @@ async function runInit(options) {
|
|
|
2712
3572
|
`A configuration already exists (${present}). Re-run with --force to replace it.`
|
|
2713
3573
|
);
|
|
2714
3574
|
}
|
|
3575
|
+
if (packageJsonHasConfig(options.cwd) && !options.force) {
|
|
3576
|
+
throw new Error(
|
|
3577
|
+
'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.'
|
|
3578
|
+
);
|
|
3579
|
+
}
|
|
2715
3580
|
if (fs9__namespace.existsSync(configPath) && !options.force) {
|
|
2716
3581
|
throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
|
|
2717
3582
|
}
|
|
@@ -3268,14 +4133,42 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3268
4133
|
const modifiers = ts__default.default.canHaveModifiers(node) ? ts__default.default.getModifiers(node) : void 0;
|
|
3269
4134
|
return Boolean(modifiers?.some((m) => m.kind === ts__default.default.SyntaxKind.DefaultKeyword));
|
|
3270
4135
|
};
|
|
3271
|
-
const
|
|
4136
|
+
const rendersJsx = (node) => {
|
|
4137
|
+
const pending = [node];
|
|
4138
|
+
while (pending.length > 0) {
|
|
4139
|
+
const current = pending.pop();
|
|
4140
|
+
if (ts__default.default.isJsxElement(current) || ts__default.default.isJsxSelfClosingElement(current) || ts__default.default.isJsxFragment(current)) {
|
|
4141
|
+
return true;
|
|
4142
|
+
}
|
|
4143
|
+
if (ts__default.default.isCallExpression(current)) {
|
|
4144
|
+
const callee = current.expression;
|
|
4145
|
+
const calleeName = ts__default.default.isIdentifier(callee) ? callee.text : ts__default.default.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
|
4146
|
+
if (calleeName === "createElement") return true;
|
|
4147
|
+
}
|
|
4148
|
+
ts__default.default.forEachChild(current, (child) => {
|
|
4149
|
+
pending.push(child);
|
|
4150
|
+
});
|
|
4151
|
+
}
|
|
4152
|
+
return false;
|
|
4153
|
+
};
|
|
4154
|
+
const REACT_BASE_CLASS = /(?:^|\.)(?:PureComponent|Component)$/;
|
|
4155
|
+
const extendsReactBase = (node) => {
|
|
4156
|
+
const heritage = (node.heritageClauses ?? []).find(
|
|
4157
|
+
(clause) => clause.token === ts__default.default.SyntaxKind.ExtendsKeyword
|
|
4158
|
+
);
|
|
4159
|
+
const expression = heritage?.types[0]?.expression;
|
|
4160
|
+
if (!expression) return false;
|
|
4161
|
+
const text2 = ts__default.default.isIdentifier(expression) ? expression.text : expression.getText(sourceFile);
|
|
4162
|
+
return REACT_BASE_CLASS.test(text2);
|
|
4163
|
+
};
|
|
4164
|
+
const recordFunctionLike = (name, exported, node) => {
|
|
3272
4165
|
summary.functions.push(name);
|
|
3273
4166
|
if (exported) {
|
|
3274
4167
|
summary.exports.push(name);
|
|
3275
4168
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
3276
4169
|
}
|
|
3277
4170
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
3278
|
-
if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
|
|
4171
|
+
if (/^[A-Z]/.test(name) && rendersJsx(node)) summary.reactComponents.push(name);
|
|
3279
4172
|
};
|
|
3280
4173
|
const stack = [sourceFile];
|
|
3281
4174
|
while (stack.length > 0) {
|
|
@@ -3304,7 +4197,7 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3304
4197
|
if (ts__default.default.isFunctionDeclaration(node)) {
|
|
3305
4198
|
const exported = isExported(node);
|
|
3306
4199
|
if (node.name) {
|
|
3307
|
-
recordFunctionLike(node.name.text, exported);
|
|
4200
|
+
recordFunctionLike(node.name.text, exported, node);
|
|
3308
4201
|
if (isDefault(node)) summary.exports.push("default");
|
|
3309
4202
|
} else if (isDefault(node)) {
|
|
3310
4203
|
summary.exports.push("default");
|
|
@@ -3315,7 +4208,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3315
4208
|
summary.classes.push(node.name.text);
|
|
3316
4209
|
if (isExported(node)) summary.exports.push(node.name.text);
|
|
3317
4210
|
if (isDefault(node)) summary.exports.push("default");
|
|
3318
|
-
if (/^[A-Z]/.test(node.name.text))
|
|
4211
|
+
if (/^[A-Z]/.test(node.name.text) && (extendsReactBase(node) || rendersJsx(node))) {
|
|
4212
|
+
summary.reactComponents.push(node.name.text);
|
|
4213
|
+
}
|
|
3319
4214
|
} else if (isDefault(node)) {
|
|
3320
4215
|
summary.exports.push("default");
|
|
3321
4216
|
}
|
|
@@ -3343,7 +4238,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
3343
4238
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
3344
4239
|
}
|
|
3345
4240
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
3346
|
-
if (/^[A-Z]/.test(name)
|
|
4241
|
+
if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
|
|
4242
|
+
summary.reactComponents.push(name);
|
|
4243
|
+
}
|
|
3347
4244
|
}
|
|
3348
4245
|
}
|
|
3349
4246
|
const children = [];
|
|
@@ -3561,7 +4458,8 @@ function summarizeMarkdown(content) {
|
|
|
3561
4458
|
}
|
|
3562
4459
|
|
|
3563
4460
|
// src/generator/audit.ts
|
|
3564
|
-
function generateAuditReport(manifest, secretFindings) {
|
|
4461
|
+
function generateAuditReport(manifest, secretFindings, secretRules) {
|
|
4462
|
+
const mask = (value) => findSecretInPath(value, secretRules)?.masked ?? value;
|
|
3565
4463
|
const lines = [
|
|
3566
4464
|
"# Codebase Health & Security Audit",
|
|
3567
4465
|
"",
|
|
@@ -3570,7 +4468,11 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
3570
4468
|
`**Total Files:** ${manifest.counts.totalFiles} `,
|
|
3571
4469
|
`**Total Lines:** ${manifest.counts.totalLines.toLocaleString()} `,
|
|
3572
4470
|
`**Total Size:** ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB `,
|
|
3573
|
-
|
|
4471
|
+
// This counts the bytes that were *emitted* after redaction, not the bytes
|
|
4472
|
+
// that were masked: calling it "redacted bytes" made a repository with zero
|
|
4473
|
+
// secrets report a non-zero redacted-byte figure directly above "no secrets
|
|
4474
|
+
// detected". The number of masked spans is reported in section 1 instead.
|
|
4475
|
+
`**Emitted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
|
|
3574
4476
|
"",
|
|
3575
4477
|
"## 1. Security Scan & Redaction",
|
|
3576
4478
|
"",
|
|
@@ -3592,7 +4494,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
3592
4494
|
"| :--- | :--- | :--- |"
|
|
3593
4495
|
);
|
|
3594
4496
|
for (const finding of secretFindings) {
|
|
3595
|
-
lines.push(`| \`${finding.path ?? "unknown"}\` | ${finding.line} | \`${finding.rule}\` |`);
|
|
4497
|
+
lines.push(`| \`${mask(finding.path ?? "unknown")}\` | ${finding.line} | \`${finding.rule}\` |`);
|
|
3596
4498
|
}
|
|
3597
4499
|
lines.push("", "### Matches by rule", "", "| Rule | Matches |", "| :--- | :--- |");
|
|
3598
4500
|
for (const [rule, count] of Array.from(byRule.entries()).sort((a, b) => b[1] - a[1])) {
|
|
@@ -3614,7 +4516,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
3614
4516
|
for (const file of largeFiles) {
|
|
3615
4517
|
const sizeKb = (file.sizeBytes / 1024).toFixed(1);
|
|
3616
4518
|
const status = file.isPreviewTruncated ? "Truncated Head/Tail" : "Full";
|
|
3617
|
-
lines.push(`| \`${file.relativePath}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
|
|
4519
|
+
lines.push(`| \`${mask(file.relativePath)}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
|
|
3618
4520
|
}
|
|
3619
4521
|
lines.push("");
|
|
3620
4522
|
}
|
|
@@ -3656,7 +4558,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
3656
4558
|
"| :--- | :--- | :--- |"
|
|
3657
4559
|
);
|
|
3658
4560
|
for (const skip of manifest.skipped) {
|
|
3659
|
-
lines.push(`| \`${skip.path}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
|
|
4561
|
+
lines.push(`| \`${mask(skip.path)}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
|
|
3660
4562
|
}
|
|
3661
4563
|
lines.push("");
|
|
3662
4564
|
}
|
|
@@ -3731,6 +4633,8 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
3731
4633
|
layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
|
|
3732
4634
|
kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
|
|
3733
4635
|
}
|
|
4636
|
+
const pathLines = included.map((file) => displayPath(file.relativePath));
|
|
4637
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
3734
4638
|
const header = [
|
|
3735
4639
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
3736
4640
|
"",
|
|
@@ -3743,9 +4647,9 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
3743
4647
|
"",
|
|
3744
4648
|
"## Files",
|
|
3745
4649
|
"",
|
|
3746
|
-
|
|
3747
|
-
...
|
|
3748
|
-
|
|
4650
|
+
`${listFence}text`,
|
|
4651
|
+
...pathLines,
|
|
4652
|
+
listFence,
|
|
3749
4653
|
"",
|
|
3750
4654
|
"## Contents",
|
|
3751
4655
|
""
|
|
@@ -3773,7 +4677,8 @@ function capValue(value, max) {
|
|
|
3773
4677
|
return `${value.slice(0, half)}\u2026${value.slice(value.length - half)}`;
|
|
3774
4678
|
}
|
|
3775
4679
|
var capMetadata = (value) => capValue(value, METADATA_VALUE_MAX);
|
|
3776
|
-
var
|
|
4680
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
|
|
4681
|
+
var displayPath = (value) => capValue(value.replace(CONTROL_CHARS, " "), DISPLAY_PATH_MAX);
|
|
3777
4682
|
function topEntries(counts, limit) {
|
|
3778
4683
|
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
4684
|
}
|
|
@@ -3869,6 +4774,8 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
3869
4774
|
layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
|
|
3870
4775
|
kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
|
|
3871
4776
|
}
|
|
4777
|
+
const pathLines = group.map((s) => displayPath(s.file.relativePath));
|
|
4778
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
3872
4779
|
const header = [
|
|
3873
4780
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
3874
4781
|
"",
|
|
@@ -3881,9 +4788,9 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
3881
4788
|
"",
|
|
3882
4789
|
"## Files",
|
|
3883
4790
|
"",
|
|
3884
|
-
|
|
3885
|
-
...
|
|
3886
|
-
|
|
4791
|
+
`${listFence}text`,
|
|
4792
|
+
...pathLines,
|
|
4793
|
+
listFence,
|
|
3887
4794
|
"",
|
|
3888
4795
|
"## Contents",
|
|
3889
4796
|
""
|
|
@@ -3942,9 +4849,12 @@ function renderFileSection(file) {
|
|
|
3942
4849
|
].join("\n");
|
|
3943
4850
|
}
|
|
3944
4851
|
function fenceForContent(content) {
|
|
4852
|
+
return fenceForText(content, 4);
|
|
4853
|
+
}
|
|
4854
|
+
function fenceForText(content, minimum) {
|
|
3945
4855
|
const matches = content.match(/`+/g);
|
|
3946
4856
|
const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
|
|
3947
|
-
return "`".repeat(Math.max(
|
|
4857
|
+
return "`".repeat(Math.max(minimum, maxRun + 1));
|
|
3948
4858
|
}
|
|
3949
4859
|
function renderSummary(summary) {
|
|
3950
4860
|
if (!summary) return [];
|
|
@@ -4093,6 +5003,7 @@ function generateReadme(manifest) {
|
|
|
4093
5003
|
|
|
4094
5004
|
// src/generator/tree.ts
|
|
4095
5005
|
var MAX_TREE_LINES = 2e4;
|
|
5006
|
+
var CONTROL_CHARS2 = /[\u0000-\u001f\u007f]/g;
|
|
4096
5007
|
function generateProjectTree(filePaths) {
|
|
4097
5008
|
const root = { children: /* @__PURE__ */ new Map() };
|
|
4098
5009
|
for (const filePath of filePaths) {
|
|
@@ -4107,7 +5018,7 @@ function generateProjectTree(filePaths) {
|
|
|
4107
5018
|
current = child;
|
|
4108
5019
|
}
|
|
4109
5020
|
}
|
|
4110
|
-
const lines = ["
|
|
5021
|
+
const lines = ["."];
|
|
4111
5022
|
let truncatedAt = -1;
|
|
4112
5023
|
function walk(node, prefix) {
|
|
4113
5024
|
if (truncatedAt !== -1) return;
|
|
@@ -4122,7 +5033,8 @@ function generateProjectTree(filePaths) {
|
|
|
4122
5033
|
const isLast = index === keys.length - 1;
|
|
4123
5034
|
const child = node.children.get(key);
|
|
4124
5035
|
const isDirectory = child.children.size > 0;
|
|
4125
|
-
|
|
5036
|
+
const label = key.replace(CONTROL_CHARS2, " ");
|
|
5037
|
+
lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
|
|
4126
5038
|
if (lines.length >= MAX_TREE_LINES) {
|
|
4127
5039
|
truncatedAt = lines.length;
|
|
4128
5040
|
return;
|
|
@@ -4134,77 +5046,11 @@ function generateProjectTree(filePaths) {
|
|
|
4134
5046
|
if (truncatedAt !== -1) {
|
|
4135
5047
|
lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
|
|
4136
5048
|
}
|
|
4137
|
-
lines.
|
|
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;
|
|
5049
|
+
const fence = fenceForText(lines.join("\n"), 3);
|
|
5050
|
+
return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
|
|
4188
5051
|
}
|
|
4189
|
-
function
|
|
4190
|
-
|
|
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 {
|
|
4205
|
-
}
|
|
4206
|
-
}
|
|
4207
|
-
}
|
|
5052
|
+
function maskedPath(value, secretRules) {
|
|
5053
|
+
return findSecretInPath(value, secretRules)?.masked ?? value;
|
|
4208
5054
|
}
|
|
4209
5055
|
function emptyDrift() {
|
|
4210
5056
|
return {
|
|
@@ -4219,6 +5065,15 @@ function emptyDrift() {
|
|
|
4219
5065
|
function sha256Hex(buffer) {
|
|
4220
5066
|
return crypto__namespace.createHash("sha256").update(buffer).digest("hex");
|
|
4221
5067
|
}
|
|
5068
|
+
function wouldNowBeIncluded(cwd, relativePath) {
|
|
5069
|
+
const absolutePath = path10__namespace.join(cwd, relativePath);
|
|
5070
|
+
try {
|
|
5071
|
+
if (!fs9__namespace.statSync(absolutePath).isFile()) return false;
|
|
5072
|
+
return !isBinaryFile(absolutePath);
|
|
5073
|
+
} catch {
|
|
5074
|
+
return false;
|
|
5075
|
+
}
|
|
5076
|
+
}
|
|
4222
5077
|
function validateSnapshot(configInput) {
|
|
4223
5078
|
assertUsableConfigInput(configInput);
|
|
4224
5079
|
const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
|
|
@@ -4267,15 +5122,20 @@ function validateSnapshot(configInput) {
|
|
|
4267
5122
|
const skippedSet = new Set(
|
|
4268
5123
|
manifest.skipped.filter((entry) => entry && typeof entry.path === "string").map((entry) => entry.path)
|
|
4269
5124
|
);
|
|
5125
|
+
const isSkippedOnDisk = (diskFile) => {
|
|
5126
|
+
if (skippedSet.has(diskFile)) return true;
|
|
5127
|
+
return skippedSet.has(maskedPath(diskFile, config.secretRules));
|
|
5128
|
+
};
|
|
4270
5129
|
const collection = collectFiles(config.cwd, {
|
|
4271
5130
|
includePatterns: config.include,
|
|
4272
5131
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
4273
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
5132
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
5133
|
+
secretRules: config.secretRules
|
|
4274
5134
|
});
|
|
4275
5135
|
for (const diskFile of collection.files) {
|
|
4276
|
-
if (skippedSet.has(diskFile)) continue;
|
|
4277
5136
|
const snapshotHash = manifestFileMap.get(diskFile);
|
|
4278
5137
|
if (!snapshotHash) {
|
|
5138
|
+
if (isSkippedOnDisk(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
|
|
4279
5139
|
drift.missingInSnapshot.push(diskFile);
|
|
4280
5140
|
continue;
|
|
4281
5141
|
}
|
|
@@ -4544,10 +5404,19 @@ function validateSnapshot(configInput) {
|
|
|
4544
5404
|
`${drift.configMismatches.length} configuration mismatch(es) between the manifest and this run.`
|
|
4545
5405
|
);
|
|
4546
5406
|
}
|
|
5407
|
+
const maskedDrift = {
|
|
5408
|
+
...drift,
|
|
5409
|
+
missingInSnapshot: drift.missingInSnapshot.map((value) => maskedPath(value, config.secretRules)),
|
|
5410
|
+
missingOnDisk: drift.missingOnDisk.map((value) => maskedPath(value, config.secretRules)),
|
|
5411
|
+
hashMismatches: drift.hashMismatches.map((mismatch) => ({
|
|
5412
|
+
...mismatch,
|
|
5413
|
+
path: maskedPath(mismatch.path, config.secretRules)
|
|
5414
|
+
}))
|
|
5415
|
+
};
|
|
4547
5416
|
return {
|
|
4548
5417
|
valid: errors.length === 0,
|
|
4549
5418
|
manifestPresent: true,
|
|
4550
|
-
drift,
|
|
5419
|
+
drift: maskedDrift,
|
|
4551
5420
|
errors,
|
|
4552
5421
|
warnings
|
|
4553
5422
|
};
|
|
@@ -4555,7 +5424,7 @@ function validateSnapshot(configInput) {
|
|
|
4555
5424
|
|
|
4556
5425
|
// src/index.ts
|
|
4557
5426
|
var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
|
|
4558
|
-
var GENERATOR_VERSION = "
|
|
5427
|
+
var GENERATOR_VERSION = "4.0.1";
|
|
4559
5428
|
var SCHEMA_VERSION = "3.0.0";
|
|
4560
5429
|
function sha256Hex2(input) {
|
|
4561
5430
|
return crypto__namespace.createHash("sha256").update(input).digest("hex");
|
|
@@ -4592,7 +5461,6 @@ function bump(counts, key) {
|
|
|
4592
5461
|
}
|
|
4593
5462
|
var RELATIVE_SPECIFIER = /^\.{1,2}\//;
|
|
4594
5463
|
var IMPORT_CANDIDATE_SUFFIXES = [
|
|
4595
|
-
"",
|
|
4596
5464
|
".ts",
|
|
4597
5465
|
".tsx",
|
|
4598
5466
|
".mts",
|
|
@@ -4606,23 +5474,59 @@ var IMPORT_CANDIDATE_SUFFIXES = [
|
|
|
4606
5474
|
"/index.js",
|
|
4607
5475
|
"/index.mjs"
|
|
4608
5476
|
];
|
|
4609
|
-
|
|
5477
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5478
|
+
".ts",
|
|
5479
|
+
".tsx",
|
|
5480
|
+
".mts",
|
|
5481
|
+
".cts",
|
|
5482
|
+
".js",
|
|
5483
|
+
".jsx",
|
|
5484
|
+
".mjs",
|
|
5485
|
+
".cjs"
|
|
5486
|
+
]);
|
|
5487
|
+
function buildImportResolver(files) {
|
|
5488
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
5489
|
+
const byPathFolded = /* @__PURE__ */ new Map();
|
|
5490
|
+
const byStem = /* @__PURE__ */ new Map();
|
|
5491
|
+
const byStemFolded = /* @__PURE__ */ new Map();
|
|
5492
|
+
const remember = (map, key, value) => {
|
|
5493
|
+
if (!map.has(key)) map.set(key, value);
|
|
5494
|
+
};
|
|
5495
|
+
for (const file of files) {
|
|
5496
|
+
const relativePath = file.relativePath;
|
|
5497
|
+
remember(byPath, relativePath, relativePath);
|
|
5498
|
+
remember(byPathFolded, relativePath.toLowerCase(), relativePath);
|
|
5499
|
+
const extension = path10__namespace.posix.extname(relativePath);
|
|
5500
|
+
if (!SOURCE_EXTENSIONS.has(extension)) continue;
|
|
5501
|
+
const stem = relativePath.slice(0, -extension.length);
|
|
5502
|
+
remember(byStem, stem, relativePath);
|
|
5503
|
+
remember(byStemFolded, stem.toLowerCase(), relativePath);
|
|
5504
|
+
if (path10__namespace.posix.basename(stem) === "index") {
|
|
5505
|
+
const directory = path10__namespace.posix.dirname(stem);
|
|
5506
|
+
remember(byStem, directory, relativePath);
|
|
5507
|
+
remember(byStemFolded, directory.toLowerCase(), relativePath);
|
|
5508
|
+
}
|
|
5509
|
+
}
|
|
5510
|
+
return { byPath, byPathFolded, byStem, byStemFolded };
|
|
5511
|
+
}
|
|
5512
|
+
function resolveSpecifier(fromFile, specifier, resolver) {
|
|
4610
5513
|
if (!RELATIVE_SPECIFIER.test(specifier)) return null;
|
|
4611
|
-
const
|
|
4612
|
-
|
|
4613
|
-
|
|
5514
|
+
const joined = path10__namespace.posix.normalize(
|
|
5515
|
+
path10__namespace.posix.join(path10__namespace.posix.dirname(fromFile), specifier)
|
|
5516
|
+
);
|
|
5517
|
+
const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
|
|
5518
|
+
const direct = exact(joined);
|
|
5519
|
+
if (direct) return direct;
|
|
4614
5520
|
for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
|
|
4615
|
-
const
|
|
4616
|
-
if (
|
|
5521
|
+
const hit = exact(`${joined}${suffix}`);
|
|
5522
|
+
if (hit) return hit;
|
|
4617
5523
|
}
|
|
4618
|
-
const
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
}
|
|
4622
|
-
return null;
|
|
5524
|
+
const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
|
|
5525
|
+
const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
|
|
5526
|
+
return byStem(joined) ?? byStem(stem);
|
|
4623
5527
|
}
|
|
4624
5528
|
function buildIndexes(files) {
|
|
4625
|
-
const
|
|
5529
|
+
const resolver = buildImportResolver(files);
|
|
4626
5530
|
const importerCounts = /* @__PURE__ */ new Map();
|
|
4627
5531
|
const byFile = files.filter((file) => isSourceSummary(file.summary)).map((file) => {
|
|
4628
5532
|
const summary = file.summary;
|
|
@@ -4634,7 +5538,7 @@ function buildIndexes(files) {
|
|
|
4634
5538
|
});
|
|
4635
5539
|
for (const entry of byFile) {
|
|
4636
5540
|
for (const specifier of entry.imports) {
|
|
4637
|
-
const resolved = resolveSpecifier(entry.path, specifier,
|
|
5541
|
+
const resolved = resolveSpecifier(entry.path, specifier, resolver);
|
|
4638
5542
|
if (resolved) importerCounts.set(resolved, (importerCounts.get(resolved) ?? 0) + 1);
|
|
4639
5543
|
}
|
|
4640
5544
|
}
|
|
@@ -4725,29 +5629,224 @@ function resolveSafeOutputDirs(config) {
|
|
|
4725
5629
|
return { outDir, tmpDir };
|
|
4726
5630
|
}
|
|
4727
5631
|
var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
|
|
5632
|
+
var RUN_DIR_PREFIX = "run-";
|
|
5633
|
+
var STALE_RUN_MS = 60 * 60 * 1e3;
|
|
5634
|
+
function maskPathText(value, secretRules) {
|
|
5635
|
+
return findSecretInPath(value, secretRules)?.masked ?? value;
|
|
5636
|
+
}
|
|
5637
|
+
function maskSkipRecords(records, secretRules) {
|
|
5638
|
+
return maskSkipPaths(records, secretRules);
|
|
5639
|
+
}
|
|
5640
|
+
function isProcessAlive(pid) {
|
|
5641
|
+
try {
|
|
5642
|
+
process.kill(pid, 0);
|
|
5643
|
+
return true;
|
|
5644
|
+
} catch (err) {
|
|
5645
|
+
return err.code !== "ESRCH";
|
|
5646
|
+
}
|
|
5647
|
+
}
|
|
4728
5648
|
var OWNERSHIP_MARKER = ".codebase-snapshot";
|
|
4729
|
-
|
|
5649
|
+
var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
|
|
5650
|
+
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;
|
|
5651
|
+
function listEntries(dir) {
|
|
5652
|
+
const entries = [];
|
|
5653
|
+
const walk = (absolute, prefix) => {
|
|
5654
|
+
let children;
|
|
5655
|
+
try {
|
|
5656
|
+
children = fs9__namespace.readdirSync(absolute, { withFileTypes: true });
|
|
5657
|
+
} catch {
|
|
5658
|
+
return;
|
|
5659
|
+
}
|
|
5660
|
+
for (const child of children) {
|
|
5661
|
+
const relative5 = prefix === "" ? child.name : `${prefix}/${child.name}`;
|
|
5662
|
+
entries.push(relative5);
|
|
5663
|
+
if (child.isDirectory()) walk(path10__namespace.join(absolute, child.name), relative5);
|
|
5664
|
+
}
|
|
5665
|
+
};
|
|
5666
|
+
walk(dir, "");
|
|
5667
|
+
return entries.sort();
|
|
5668
|
+
}
|
|
5669
|
+
function markerPayload(dir, state, id, createdAt) {
|
|
4730
5670
|
const marker2 = {
|
|
4731
5671
|
generator: GENERATOR_NAME,
|
|
4732
5672
|
version: GENERATOR_VERSION,
|
|
4733
|
-
schemaVersion: SCHEMA_VERSION
|
|
5673
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5674
|
+
createdAt,
|
|
5675
|
+
state,
|
|
5676
|
+
id,
|
|
5677
|
+
pid: process.pid,
|
|
5678
|
+
entries: listEntries(dir)
|
|
4734
5679
|
};
|
|
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
5680
|
const manifestPath = path10__namespace.join(dir, "manifest.json");
|
|
4741
|
-
if (
|
|
5681
|
+
if (fs9__namespace.existsSync(manifestPath)) {
|
|
5682
|
+
marker2.manifestSha256 = sha256Hex2(fs9__namespace.readFileSync(manifestPath));
|
|
5683
|
+
}
|
|
5684
|
+
return marker2;
|
|
5685
|
+
}
|
|
5686
|
+
function writeOwnershipMarker(dir, marker2) {
|
|
5687
|
+
fs9__namespace.writeFileSync(
|
|
5688
|
+
path10__namespace.join(dir, OWNERSHIP_MARKER),
|
|
5689
|
+
`${JSON.stringify(marker2, null, 2)}
|
|
5690
|
+
`,
|
|
5691
|
+
"utf8"
|
|
5692
|
+
);
|
|
5693
|
+
}
|
|
5694
|
+
function readOwnershipMarker(dir) {
|
|
5695
|
+
const markerPath = path10__namespace.join(dir, OWNERSHIP_MARKER);
|
|
5696
|
+
if (!fs9__namespace.existsSync(markerPath)) {
|
|
5697
|
+
return { marker: null, problem: "it holds no .codebase-snapshot marker" };
|
|
5698
|
+
}
|
|
5699
|
+
let raw;
|
|
4742
5700
|
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;
|
|
5701
|
+
raw = fs9__namespace.readFileSync(markerPath, "utf8");
|
|
4746
5702
|
} catch {
|
|
4747
|
-
return
|
|
5703
|
+
return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
|
|
5704
|
+
}
|
|
5705
|
+
if (raw.trim().length === 0) {
|
|
5706
|
+
return { marker: null, problem: "its .codebase-snapshot marker is empty" };
|
|
5707
|
+
}
|
|
5708
|
+
let parsed;
|
|
5709
|
+
try {
|
|
5710
|
+
parsed = JSON.parse(raw);
|
|
5711
|
+
} catch {
|
|
5712
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not valid JSON" };
|
|
5713
|
+
}
|
|
5714
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5715
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not a JSON object" };
|
|
5716
|
+
}
|
|
5717
|
+
const candidate = parsed;
|
|
5718
|
+
if (candidate.generator !== GENERATOR_NAME) {
|
|
5719
|
+
return { marker: null, problem: "its .codebase-snapshot marker names a different generator" };
|
|
5720
|
+
}
|
|
5721
|
+
if (typeof candidate.schemaVersion !== "string" || !SUPPORTED_MARKER_SCHEMAS.has(candidate.schemaVersion)) {
|
|
5722
|
+
return {
|
|
5723
|
+
marker: null,
|
|
5724
|
+
problem: `its .codebase-snapshot marker has a schema version this build does not understand (${String(candidate.schemaVersion)})`
|
|
5725
|
+
};
|
|
5726
|
+
}
|
|
5727
|
+
if (typeof candidate.id !== "string" || !SNAPSHOT_ID_PATTERN.test(candidate.id)) {
|
|
5728
|
+
return { marker: null, problem: "its .codebase-snapshot marker carries no snapshot id" };
|
|
5729
|
+
}
|
|
5730
|
+
if (candidate.state !== "staging" && candidate.state !== "complete") {
|
|
5731
|
+
return { marker: null, problem: "its .codebase-snapshot marker records no run state" };
|
|
5732
|
+
}
|
|
5733
|
+
return {
|
|
5734
|
+
marker: {
|
|
5735
|
+
generator: GENERATOR_NAME,
|
|
5736
|
+
version: typeof candidate.version === "string" ? candidate.version : "unknown",
|
|
5737
|
+
schemaVersion: candidate.schemaVersion,
|
|
5738
|
+
createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
|
|
5739
|
+
state: candidate.state,
|
|
5740
|
+
id: candidate.id,
|
|
5741
|
+
pid: typeof candidate.pid === "number" ? candidate.pid : void 0,
|
|
5742
|
+
manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
|
|
5743
|
+
entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
|
|
5744
|
+
},
|
|
5745
|
+
problem: null
|
|
5746
|
+
};
|
|
5747
|
+
}
|
|
5748
|
+
function describeManifestProblem(value) {
|
|
5749
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "is not a JSON object";
|
|
5750
|
+
const candidate = value;
|
|
5751
|
+
const generator = candidate.generator;
|
|
5752
|
+
if (typeof generator !== "object" || generator === null || generator.name !== GENERATOR_NAME) {
|
|
5753
|
+
return "was not written by this generator";
|
|
5754
|
+
}
|
|
5755
|
+
if (!Array.isArray(candidate.files)) return "has no files array";
|
|
5756
|
+
return null;
|
|
5757
|
+
}
|
|
5758
|
+
function assessSnapshotDirectory(dir) {
|
|
5759
|
+
const { marker: marker2, problem } = readOwnershipMarker(dir);
|
|
5760
|
+
if (!marker2) return { owned: false, problem, foreignEntries: [], marker: null };
|
|
5761
|
+
if (marker2.state !== "complete") {
|
|
5762
|
+
return {
|
|
5763
|
+
owned: false,
|
|
5764
|
+
problem: "its .codebase-snapshot marker records a run that never finished writing it",
|
|
5765
|
+
foreignEntries: [],
|
|
5766
|
+
marker: marker2
|
|
5767
|
+
};
|
|
5768
|
+
}
|
|
5769
|
+
const manifestPath = path10__namespace.join(dir, "manifest.json");
|
|
5770
|
+
if (fs9__namespace.existsSync(manifestPath)) {
|
|
5771
|
+
let raw;
|
|
5772
|
+
try {
|
|
5773
|
+
raw = fs9__namespace.readFileSync(manifestPath, "utf8");
|
|
5774
|
+
} catch {
|
|
5775
|
+
return {
|
|
5776
|
+
owned: false,
|
|
5777
|
+
problem: "its manifest.json could not be read",
|
|
5778
|
+
foreignEntries: [],
|
|
5779
|
+
marker: marker2
|
|
5780
|
+
};
|
|
5781
|
+
}
|
|
5782
|
+
let parsed;
|
|
5783
|
+
try {
|
|
5784
|
+
parsed = JSON.parse(raw);
|
|
5785
|
+
} catch {
|
|
5786
|
+
return {
|
|
5787
|
+
owned: false,
|
|
5788
|
+
problem: "its manifest.json is not valid JSON",
|
|
5789
|
+
foreignEntries: [],
|
|
5790
|
+
marker: marker2
|
|
5791
|
+
};
|
|
5792
|
+
}
|
|
5793
|
+
const manifestProblem2 = describeManifestProblem(parsed);
|
|
5794
|
+
if (manifestProblem2) {
|
|
5795
|
+
return {
|
|
5796
|
+
owned: false,
|
|
5797
|
+
problem: `its manifest.json ${manifestProblem2}`,
|
|
5798
|
+
foreignEntries: [],
|
|
5799
|
+
marker: marker2
|
|
5800
|
+
};
|
|
5801
|
+
}
|
|
5802
|
+
if (!marker2.manifestSha256) {
|
|
5803
|
+
return {
|
|
5804
|
+
owned: false,
|
|
5805
|
+
problem: "its .codebase-snapshot marker records no manifest digest",
|
|
5806
|
+
foreignEntries: [],
|
|
5807
|
+
marker: marker2
|
|
5808
|
+
};
|
|
5809
|
+
}
|
|
5810
|
+
if (sha256Hex2(raw) !== marker2.manifestSha256) {
|
|
5811
|
+
return {
|
|
5812
|
+
owned: false,
|
|
5813
|
+
problem: "its manifest.json does not match the digest recorded in its .codebase-snapshot marker",
|
|
5814
|
+
foreignEntries: [],
|
|
5815
|
+
marker: marker2
|
|
5816
|
+
};
|
|
5817
|
+
}
|
|
5818
|
+
} else if (marker2.manifestSha256) {
|
|
5819
|
+
return {
|
|
5820
|
+
owned: false,
|
|
5821
|
+
problem: "its manifest.json is missing although its marker records one",
|
|
5822
|
+
foreignEntries: [],
|
|
5823
|
+
marker: marker2
|
|
5824
|
+
};
|
|
5825
|
+
}
|
|
5826
|
+
const recorded = new Set(marker2.entries ?? []);
|
|
5827
|
+
if (recorded.size === 0) {
|
|
5828
|
+
return {
|
|
5829
|
+
owned: false,
|
|
5830
|
+
problem: "its .codebase-snapshot marker records no contents",
|
|
5831
|
+
foreignEntries: [],
|
|
5832
|
+
marker: marker2
|
|
5833
|
+
};
|
|
4748
5834
|
}
|
|
5835
|
+
const foreignEntries = listEntries(dir).filter(
|
|
5836
|
+
(entry) => !recorded.has(entry) && !PLACEHOLDER_ENTRIES.has(entry)
|
|
5837
|
+
);
|
|
5838
|
+
return {
|
|
5839
|
+
owned: foreignEntries.length === 0,
|
|
5840
|
+
problem: null,
|
|
5841
|
+
foreignEntries,
|
|
5842
|
+
marker: marker2
|
|
5843
|
+
};
|
|
5844
|
+
}
|
|
5845
|
+
function describeEntries(entries, limit = 5) {
|
|
5846
|
+
const shown = entries.slice(0, limit).join(", ");
|
|
5847
|
+
return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
|
|
4749
5848
|
}
|
|
4750
|
-
function assertReplaceable(target, label) {
|
|
5849
|
+
function assertReplaceable(target, label, secretRules) {
|
|
4751
5850
|
if (!fs9__namespace.existsSync(target)) return;
|
|
4752
5851
|
if (!fs9__namespace.statSync(target).isDirectory()) {
|
|
4753
5852
|
throw new Error(
|
|
@@ -4755,26 +5854,210 @@ function assertReplaceable(target, label) {
|
|
|
4755
5854
|
);
|
|
4756
5855
|
}
|
|
4757
5856
|
const entries = fs9__namespace.readdirSync(target);
|
|
4758
|
-
|
|
4759
|
-
if (
|
|
4760
|
-
|
|
4761
|
-
const
|
|
4762
|
-
|
|
5857
|
+
if (entries.length === 0) return;
|
|
5858
|
+
if (entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
|
|
5859
|
+
const { problem } = readOwnershipMarker(target);
|
|
5860
|
+
const verdict = assessSnapshotDirectory(target);
|
|
5861
|
+
if (verdict.owned) return;
|
|
5862
|
+
const shownForeign = verdict.foreignEntries.map((name) => maskPathText(name, secretRules));
|
|
5863
|
+
const shownEntries = entries.map((name) => maskPathText(name, secretRules));
|
|
5864
|
+
if (verdict.foreignEntries.length > 0) {
|
|
5865
|
+
throw new Error(
|
|
5866
|
+
`Refusing to replace ${label} ${target}: it carries this generator's ownership marker, but these entries are not part of the snapshot it recorded: ${describeEntries(shownForeign)}. Remove them yourself, or point ${label} at a new or empty directory.`
|
|
5867
|
+
);
|
|
5868
|
+
}
|
|
5869
|
+
const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
|
|
4763
5870
|
throw new Error(
|
|
4764
|
-
`Refusing to replace ${label} ${target}:
|
|
5871
|
+
`Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(shownEntries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
|
|
4765
5872
|
);
|
|
4766
5873
|
}
|
|
4767
|
-
function
|
|
5874
|
+
function assertStagingRootUsable(tmpDir, secretRules) {
|
|
5875
|
+
if (!fs9__namespace.existsSync(tmpDir)) return;
|
|
5876
|
+
if (!fs9__namespace.statSync(tmpDir).isDirectory()) {
|
|
5877
|
+
throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
|
|
5878
|
+
}
|
|
5879
|
+
const entries = fs9__namespace.readdirSync(tmpDir);
|
|
5880
|
+
if (entries.length === 0) return;
|
|
5881
|
+
const foreign = entries.filter((name) => {
|
|
5882
|
+
const full = path10__namespace.join(tmpDir, name);
|
|
5883
|
+
if (!name.startsWith(RUN_DIR_PREFIX)) return true;
|
|
5884
|
+
let stats;
|
|
5885
|
+
try {
|
|
5886
|
+
stats = fs9__namespace.statSync(full);
|
|
5887
|
+
} catch {
|
|
5888
|
+
return true;
|
|
5889
|
+
}
|
|
5890
|
+
if (!stats.isDirectory()) return true;
|
|
5891
|
+
const { marker: marker2 } = readOwnershipMarker(full);
|
|
5892
|
+
if (marker2) return false;
|
|
5893
|
+
try {
|
|
5894
|
+
return fs9__namespace.readdirSync(full).length > 0;
|
|
5895
|
+
} catch {
|
|
5896
|
+
return true;
|
|
5897
|
+
}
|
|
5898
|
+
});
|
|
5899
|
+
if (foreign.length > 0) {
|
|
5900
|
+
const shown = describeEntries(foreign.map((name) => maskPathText(name, secretRules)));
|
|
5901
|
+
throw new Error(
|
|
5902
|
+
`Refusing to replace tmpDir ${tmpDir}: it holds ${shown}, which this generator did not write. Point tmpDir at a new or empty directory, or remove those files yourself.`
|
|
5903
|
+
);
|
|
5904
|
+
}
|
|
5905
|
+
}
|
|
5906
|
+
function cleanStaleRunDirs(tmpDir) {
|
|
5907
|
+
let entries;
|
|
5908
|
+
try {
|
|
5909
|
+
entries = fs9__namespace.readdirSync(tmpDir);
|
|
5910
|
+
} catch {
|
|
5911
|
+
return;
|
|
5912
|
+
}
|
|
5913
|
+
for (const name of entries) {
|
|
5914
|
+
if (!name.startsWith(RUN_DIR_PREFIX)) continue;
|
|
5915
|
+
const full = path10__namespace.join(tmpDir, name);
|
|
5916
|
+
const { marker: marker2 } = readOwnershipMarker(full);
|
|
5917
|
+
if (marker2) {
|
|
5918
|
+
const created = Date.parse(marker2.createdAt);
|
|
5919
|
+
const aged = !Number.isFinite(created) || Date.now() - created > STALE_RUN_MS;
|
|
5920
|
+
const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
|
|
5921
|
+
if (aged || abandoned) {
|
|
5922
|
+
try {
|
|
5923
|
+
fs9__namespace.rmSync(full, { recursive: true, force: true });
|
|
5924
|
+
} catch {
|
|
5925
|
+
}
|
|
5926
|
+
}
|
|
5927
|
+
continue;
|
|
5928
|
+
}
|
|
5929
|
+
try {
|
|
5930
|
+
if (fs9__namespace.readdirSync(full).length === 0) fs9__namespace.rmSync(full, { recursive: true, force: true });
|
|
5931
|
+
} catch {
|
|
5932
|
+
}
|
|
5933
|
+
}
|
|
5934
|
+
}
|
|
5935
|
+
function removeIfEmpty(dir) {
|
|
5936
|
+
try {
|
|
5937
|
+
if (fs9__namespace.readdirSync(dir).length === 0) fs9__namespace.rmSync(dir, { recursive: true, force: true });
|
|
5938
|
+
} catch {
|
|
5939
|
+
}
|
|
5940
|
+
}
|
|
5941
|
+
function ensureParentDirectory(target, label) {
|
|
5942
|
+
const parent = path10__namespace.dirname(target);
|
|
5943
|
+
if (fs9__namespace.existsSync(parent) && !fs9__namespace.statSync(parent).isDirectory()) {
|
|
5944
|
+
throw new Error(
|
|
5945
|
+
`Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
|
|
5946
|
+
);
|
|
5947
|
+
}
|
|
5948
|
+
try {
|
|
5949
|
+
fs9__namespace.mkdirSync(parent, { recursive: true });
|
|
5950
|
+
} catch (err) {
|
|
5951
|
+
throw new Error(
|
|
5952
|
+
`Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
|
|
5953
|
+
);
|
|
5954
|
+
}
|
|
5955
|
+
}
|
|
5956
|
+
function acquireRunLock(stagingRoot) {
|
|
5957
|
+
const key = lockKeyFor(stagingRoot);
|
|
5958
|
+
const lockPath = path10__namespace.join(os__namespace.tmpdir(), `codebase-snapshot-${key}.lock`);
|
|
5959
|
+
const payload = `${JSON.stringify({
|
|
5960
|
+
pid: process.pid,
|
|
5961
|
+
stagingRoot,
|
|
5962
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5963
|
+
})}
|
|
5964
|
+
`;
|
|
5965
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
5966
|
+
try {
|
|
5967
|
+
const handle = fs9__namespace.openSync(lockPath, "wx");
|
|
5968
|
+
try {
|
|
5969
|
+
fs9__namespace.writeSync(handle, payload);
|
|
5970
|
+
} finally {
|
|
5971
|
+
fs9__namespace.closeSync(handle);
|
|
5972
|
+
}
|
|
5973
|
+
return () => {
|
|
5974
|
+
try {
|
|
5975
|
+
fs9__namespace.rmSync(lockPath, { force: true });
|
|
5976
|
+
} catch {
|
|
5977
|
+
}
|
|
5978
|
+
};
|
|
5979
|
+
} catch (err) {
|
|
5980
|
+
if (err.code !== "EEXIST") throw err;
|
|
5981
|
+
if (attempt === 0 && lockOwnerIsGone(lockPath)) {
|
|
5982
|
+
fs9__namespace.rmSync(lockPath, { force: true });
|
|
5983
|
+
continue;
|
|
5984
|
+
}
|
|
5985
|
+
throw new Error(
|
|
5986
|
+
`Another generate run is already using the staging directory ${stagingRoot}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
|
|
5987
|
+
);
|
|
5988
|
+
}
|
|
5989
|
+
}
|
|
5990
|
+
throw new Error(`Could not acquire the generate lock for ${stagingRoot}.`);
|
|
5991
|
+
}
|
|
5992
|
+
function lockKeyFor(stagingRoot) {
|
|
5993
|
+
let key = stagingRoot;
|
|
5994
|
+
try {
|
|
5995
|
+
key = fs9__namespace.realpathSync(stagingRoot);
|
|
5996
|
+
} catch {
|
|
5997
|
+
}
|
|
5998
|
+
if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
|
|
5999
|
+
return sha256Hex2(key).slice(0, 16);
|
|
6000
|
+
}
|
|
6001
|
+
function lockOwnerIsGone(lockPath) {
|
|
6002
|
+
let raw;
|
|
6003
|
+
try {
|
|
6004
|
+
raw = fs9__namespace.readFileSync(lockPath, "utf8");
|
|
6005
|
+
} catch {
|
|
6006
|
+
return false;
|
|
6007
|
+
}
|
|
6008
|
+
let pid;
|
|
6009
|
+
try {
|
|
6010
|
+
pid = JSON.parse(raw).pid;
|
|
6011
|
+
} catch {
|
|
6012
|
+
return false;
|
|
6013
|
+
}
|
|
6014
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
6015
|
+
try {
|
|
6016
|
+
process.kill(pid, 0);
|
|
6017
|
+
return false;
|
|
6018
|
+
} catch (err) {
|
|
6019
|
+
return err.code === "ESRCH";
|
|
6020
|
+
}
|
|
6021
|
+
}
|
|
6022
|
+
function writeManifestAtomically(manifestPath, manifest) {
|
|
6023
|
+
const tempPath = `${manifestPath}.${process.pid}.tmp`;
|
|
6024
|
+
fs9__namespace.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
6025
|
+
try {
|
|
6026
|
+
fs9__namespace.renameSync(tempPath, manifestPath);
|
|
6027
|
+
} catch (err) {
|
|
6028
|
+
try {
|
|
6029
|
+
fs9__namespace.rmSync(tempPath, { force: true });
|
|
6030
|
+
} catch {
|
|
6031
|
+
}
|
|
6032
|
+
throw err;
|
|
6033
|
+
}
|
|
6034
|
+
}
|
|
6035
|
+
function swapIntoPlace(stagingDir, outDir) {
|
|
6036
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6037
|
+
const deadline = Date.now() + 3e3;
|
|
6038
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
6039
|
+
try {
|
|
6040
|
+
swapOnce(stagingDir, outDir);
|
|
6041
|
+
return;
|
|
6042
|
+
} catch (err) {
|
|
6043
|
+
const code = err.code;
|
|
6044
|
+
const racy = code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM";
|
|
6045
|
+
if (!racy || attempt >= 10 || Date.now() > deadline) throw err;
|
|
6046
|
+
sleepSync(50);
|
|
6047
|
+
}
|
|
6048
|
+
}
|
|
6049
|
+
}
|
|
6050
|
+
function swapOnce(stagingDir, outDir) {
|
|
4768
6051
|
const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
|
|
4769
6052
|
const hadPrevious = fs9__namespace.existsSync(outDir);
|
|
4770
6053
|
if (hadPrevious) fs9__namespace.renameSync(outDir, backupDir);
|
|
4771
6054
|
try {
|
|
4772
6055
|
try {
|
|
4773
|
-
fs9__namespace.renameSync(
|
|
6056
|
+
fs9__namespace.renameSync(stagingDir, outDir);
|
|
4774
6057
|
} catch (err) {
|
|
4775
6058
|
if (err.code === "EXDEV") {
|
|
4776
|
-
fs9__namespace.cpSync(
|
|
4777
|
-
fs9__namespace.rmSync(
|
|
6059
|
+
fs9__namespace.cpSync(stagingDir, outDir, { recursive: true });
|
|
6060
|
+
fs9__namespace.rmSync(stagingDir, { recursive: true, force: true });
|
|
4778
6061
|
} else {
|
|
4779
6062
|
throw err;
|
|
4780
6063
|
}
|
|
@@ -4788,6 +6071,10 @@ function swapIntoPlace(tmpDir, outDir) {
|
|
|
4788
6071
|
throw err;
|
|
4789
6072
|
}
|
|
4790
6073
|
}
|
|
6074
|
+
function sleepSync(milliseconds) {
|
|
6075
|
+
const buffer = new Int32Array(new SharedArrayBuffer(4));
|
|
6076
|
+
Atomics.wait(buffer, 0, 0, milliseconds);
|
|
6077
|
+
}
|
|
4791
6078
|
function artifactWarnings(artifacts) {
|
|
4792
6079
|
const messages = [];
|
|
4793
6080
|
if (!artifacts.manifest) {
|
|
@@ -4850,10 +6137,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4850
6137
|
const collection = collectFiles(config.cwd, {
|
|
4851
6138
|
includePatterns: config.include,
|
|
4852
6139
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
4853
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
6140
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
6141
|
+
secretRules: config.secretRules
|
|
4854
6142
|
});
|
|
4855
6143
|
const files = [];
|
|
4856
|
-
const
|
|
6144
|
+
const monolithBodies = /* @__PURE__ */ new Map();
|
|
6145
|
+
const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
|
|
4857
6146
|
const secretFindings = [];
|
|
4858
6147
|
const warnings = [];
|
|
4859
6148
|
const layerCounts = {};
|
|
@@ -4916,6 +6205,9 @@ function generateSnapshot(configInput, options) {
|
|
|
4916
6205
|
isPreviewTruncated = preview.isTruncated;
|
|
4917
6206
|
includedLines = preview.includedLines;
|
|
4918
6207
|
}
|
|
6208
|
+
if (config.includeMonolith && isPreviewTruncated) {
|
|
6209
|
+
monolithBodies.set(relativePath, redacted);
|
|
6210
|
+
}
|
|
4919
6211
|
let summary;
|
|
4920
6212
|
let summaryQuality;
|
|
4921
6213
|
try {
|
|
@@ -4924,12 +6216,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4924
6216
|
summaryQuality = outcome?.quality;
|
|
4925
6217
|
if (isSourceSummary(summary) && summary.parseErrors) {
|
|
4926
6218
|
warnings.push(
|
|
4927
|
-
`Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
|
|
6219
|
+
`Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
|
|
4928
6220
|
);
|
|
4929
6221
|
}
|
|
4930
6222
|
} catch (err) {
|
|
4931
6223
|
warnings.push(
|
|
4932
|
-
`Summary skipped for ${relativePath}: ${err.message}`.replace(/\s+/g, " ")
|
|
6224
|
+
`Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(/\s+/g, " ")
|
|
4933
6225
|
);
|
|
4934
6226
|
}
|
|
4935
6227
|
files.push({
|
|
@@ -4958,7 +6250,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4958
6250
|
).join("\n")
|
|
4959
6251
|
);
|
|
4960
6252
|
const fullContents = /* @__PURE__ */ new Map();
|
|
4961
|
-
for (const file of files)
|
|
6253
|
+
for (const file of files) {
|
|
6254
|
+
fullContents.set(
|
|
6255
|
+
file.relativePath,
|
|
6256
|
+
monolithBodies.get(file.relativePath) ?? file.content ?? ""
|
|
6257
|
+
);
|
|
6258
|
+
}
|
|
4962
6259
|
const parts = partitionByBudget(files, config, fingerprint);
|
|
4963
6260
|
const maxEffectiveBudget = Math.max(
|
|
4964
6261
|
config.maxPartBytes,
|
|
@@ -5033,21 +6330,28 @@ function generateSnapshot(configInput, options) {
|
|
|
5033
6330
|
const { content: _content, ...rest } = file;
|
|
5034
6331
|
return rest;
|
|
5035
6332
|
}),
|
|
5036
|
-
skipped,
|
|
6333
|
+
skipped: maskSkipRecords(skipped, config.secretRules),
|
|
5037
6334
|
warnings
|
|
5038
6335
|
};
|
|
5039
6336
|
manifest.warnings = [
|
|
5040
6337
|
...warnings,
|
|
5041
|
-
...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
|
|
6338
|
+
...secretFindings.map((finding) => `${finding.rule} at ${maskPathText(finding.path ?? "unknown", config.secretRules)}:${finding.line}`)
|
|
5042
6339
|
];
|
|
5043
6340
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
5044
6341
|
let monolithJson = null;
|
|
5045
6342
|
let monolithMarkdown = null;
|
|
5046
6343
|
if (config.includeMonolith) {
|
|
5047
|
-
const fullFiles = files.map((file) =>
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
|
|
6344
|
+
const fullFiles = files.map((file) => {
|
|
6345
|
+
const content = fullContents.get(file.relativePath) ?? "";
|
|
6346
|
+
return {
|
|
6347
|
+
...file,
|
|
6348
|
+
content,
|
|
6349
|
+
inclusionMode: "full",
|
|
6350
|
+
isPreviewTruncated: false,
|
|
6351
|
+
includedBytes: Buffer.byteLength(content, "utf8"),
|
|
6352
|
+
includedLines: countLines(content)
|
|
6353
|
+
};
|
|
6354
|
+
});
|
|
5051
6355
|
const json = generateMonolithJson(manifest, fullFiles);
|
|
5052
6356
|
const markdown = generateMonolithMarkdown(manifest, fullFiles);
|
|
5053
6357
|
const jsonBytes = Buffer.byteLength(json, "utf8");
|
|
@@ -5075,57 +6379,202 @@ function generateSnapshot(configInput, options) {
|
|
|
5075
6379
|
}
|
|
5076
6380
|
for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
|
|
5077
6381
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
6382
|
+
const { outDir, tmpDir } = resolveSafeOutputDirs(config);
|
|
6383
|
+
const { artifacts } = config;
|
|
6384
|
+
assertReplaceable(outDir, "outDir", config.secretRules);
|
|
6385
|
+
assertStagingRootUsable(tmpDir, config.secretRules);
|
|
5078
6386
|
if (dryRun) {
|
|
5079
6387
|
runAfterGenerate(config, manifest);
|
|
5080
6388
|
return manifest;
|
|
5081
6389
|
}
|
|
5082
|
-
const
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
6390
|
+
const releaseLock = acquireRunLock(tmpDir);
|
|
6391
|
+
let stagingDir = null;
|
|
6392
|
+
try {
|
|
6393
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6394
|
+
fs9__namespace.mkdirSync(tmpDir, { recursive: true });
|
|
6395
|
+
cleanStaleRunDirs(tmpDir);
|
|
6396
|
+
const snapshotId = crypto__namespace.randomUUID();
|
|
6397
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6398
|
+
stagingDir = path10__namespace.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
|
|
6399
|
+
fs9__namespace.mkdirSync(stagingDir);
|
|
6400
|
+
writeOwnershipMarker(stagingDir, {
|
|
6401
|
+
generator: GENERATOR_NAME,
|
|
6402
|
+
version: GENERATOR_VERSION,
|
|
6403
|
+
schemaVersion: SCHEMA_VERSION,
|
|
6404
|
+
createdAt,
|
|
6405
|
+
state: "staging",
|
|
6406
|
+
id: snapshotId,
|
|
6407
|
+
pid: process.pid
|
|
6408
|
+
});
|
|
6409
|
+
const partsDir = path10__namespace.join(stagingDir, "parts");
|
|
6410
|
+
const reportsDir = path10__namespace.join(stagingDir, "reports");
|
|
6411
|
+
if (artifacts.parts) fs9__namespace.mkdirSync(partsDir, { recursive: true });
|
|
6412
|
+
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
6413
|
+
fs9__namespace.mkdirSync(reportsDir, { recursive: true });
|
|
6414
|
+
}
|
|
6415
|
+
if (artifacts.parts) {
|
|
6416
|
+
for (const part of parts) {
|
|
6417
|
+
fs9__namespace.writeFileSync(path10__namespace.join(partsDir, part.filename), part.markdown, "utf8");
|
|
6418
|
+
}
|
|
5098
6419
|
}
|
|
6420
|
+
if (artifacts.projectTree) {
|
|
6421
|
+
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
6422
|
+
fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
6423
|
+
}
|
|
6424
|
+
if (artifacts.navigation) {
|
|
6425
|
+
fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
6426
|
+
}
|
|
6427
|
+
if (artifacts.audit) {
|
|
6428
|
+
fs9__namespace.writeFileSync(
|
|
6429
|
+
path10__namespace.join(reportsDir, "audit.md"),
|
|
6430
|
+
generateAuditReport(manifest, secretFindings, config.secretRules),
|
|
6431
|
+
"utf8"
|
|
6432
|
+
);
|
|
6433
|
+
}
|
|
6434
|
+
if (artifacts.readme) {
|
|
6435
|
+
fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
|
|
6436
|
+
}
|
|
6437
|
+
if (artifacts.manifest) {
|
|
6438
|
+
fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
6439
|
+
}
|
|
6440
|
+
if (monolithJson !== null) {
|
|
6441
|
+
fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
|
|
6442
|
+
}
|
|
6443
|
+
if (monolithMarkdown !== null) {
|
|
6444
|
+
fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
6445
|
+
}
|
|
6446
|
+
writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
|
|
6447
|
+
swapIntoPlace(stagingDir, outDir);
|
|
6448
|
+
stagingDir = null;
|
|
6449
|
+
const warningsBeforeHook = manifest.warnings.length;
|
|
6450
|
+
runAfterGenerate(config, manifest);
|
|
6451
|
+
if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
|
|
6452
|
+
writeManifestAtomically(path10__namespace.join(outDir, "manifest.json"), manifest);
|
|
6453
|
+
}
|
|
6454
|
+
writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
|
|
6455
|
+
return manifest;
|
|
6456
|
+
} catch (err) {
|
|
6457
|
+
if (stagingDir !== null) {
|
|
6458
|
+
try {
|
|
6459
|
+
fs9__namespace.rmSync(stagingDir, { recursive: true, force: true });
|
|
6460
|
+
} catch {
|
|
6461
|
+
}
|
|
6462
|
+
}
|
|
6463
|
+
throw err;
|
|
6464
|
+
} finally {
|
|
6465
|
+
removeIfEmpty(tmpDir);
|
|
6466
|
+
releaseLock();
|
|
5099
6467
|
}
|
|
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
|
-
|
|
6468
|
+
}
|
|
6469
|
+
|
|
6470
|
+
// src/agent/option-scope.ts
|
|
6471
|
+
var COMMAND_OPTION_SCOPE = {
|
|
6472
|
+
generate: [
|
|
6473
|
+
"cwd",
|
|
6474
|
+
"out-dir",
|
|
6475
|
+
"max-part-bytes",
|
|
6476
|
+
"large-file-bytes",
|
|
6477
|
+
"max-monolith-bytes",
|
|
6478
|
+
"include-monolith",
|
|
6479
|
+
"include-sensitive-files",
|
|
6480
|
+
"skip-artifacts",
|
|
6481
|
+
"only-artifacts",
|
|
6482
|
+
"no-config",
|
|
6483
|
+
"format",
|
|
6484
|
+
"mode",
|
|
6485
|
+
"dry-run",
|
|
6486
|
+
"watch",
|
|
6487
|
+
"help",
|
|
6488
|
+
"version"
|
|
6489
|
+
],
|
|
6490
|
+
validate: [
|
|
6491
|
+
"cwd",
|
|
6492
|
+
"out-dir",
|
|
6493
|
+
"max-part-bytes",
|
|
6494
|
+
"large-file-bytes",
|
|
6495
|
+
"max-monolith-bytes",
|
|
6496
|
+
"include-monolith",
|
|
6497
|
+
"include-sensitive-files",
|
|
6498
|
+
"skip-artifacts",
|
|
6499
|
+
"only-artifacts",
|
|
6500
|
+
"no-config",
|
|
6501
|
+
"format",
|
|
6502
|
+
"mode",
|
|
6503
|
+
"help",
|
|
6504
|
+
"version"
|
|
6505
|
+
],
|
|
6506
|
+
report: [
|
|
6507
|
+
"cwd",
|
|
6508
|
+
"out-dir",
|
|
6509
|
+
"max-part-bytes",
|
|
6510
|
+
"large-file-bytes",
|
|
6511
|
+
"max-monolith-bytes",
|
|
6512
|
+
"include-monolith",
|
|
6513
|
+
"include-sensitive-files",
|
|
6514
|
+
"skip-artifacts",
|
|
6515
|
+
"only-artifacts",
|
|
6516
|
+
"no-config",
|
|
6517
|
+
"format",
|
|
6518
|
+
"mode",
|
|
6519
|
+
"help",
|
|
6520
|
+
"version"
|
|
6521
|
+
],
|
|
6522
|
+
explain: [
|
|
6523
|
+
"cwd",
|
|
6524
|
+
"out-dir",
|
|
6525
|
+
"max-part-bytes",
|
|
6526
|
+
"large-file-bytes",
|
|
6527
|
+
"max-monolith-bytes",
|
|
6528
|
+
"include-monolith",
|
|
6529
|
+
"include-sensitive-files",
|
|
6530
|
+
"skip-artifacts",
|
|
6531
|
+
"only-artifacts",
|
|
6532
|
+
"no-config",
|
|
6533
|
+
"format",
|
|
6534
|
+
"mode",
|
|
6535
|
+
"help",
|
|
6536
|
+
"version"
|
|
6537
|
+
],
|
|
6538
|
+
diff: ["format", "mode", "help", "version"],
|
|
6539
|
+
describe: ["format", "mode", "help", "version"],
|
|
6540
|
+
init: [
|
|
6541
|
+
"cwd",
|
|
6542
|
+
"out-dir",
|
|
6543
|
+
"max-part-bytes",
|
|
6544
|
+
"include-sensitive-files",
|
|
6545
|
+
"skip-artifacts",
|
|
6546
|
+
"only-artifacts",
|
|
6547
|
+
"preset",
|
|
6548
|
+
"include",
|
|
6549
|
+
"exclude",
|
|
6550
|
+
"yes",
|
|
6551
|
+
"minimal",
|
|
6552
|
+
"force",
|
|
6553
|
+
"dry-run",
|
|
6554
|
+
"format",
|
|
6555
|
+
"mode",
|
|
6556
|
+
"help",
|
|
6557
|
+
"version"
|
|
6558
|
+
],
|
|
6559
|
+
skill: ["cwd", "format", "mode", "install", "target", "help", "version"]
|
|
6560
|
+
};
|
|
6561
|
+
var COMMAND_ORDER = [
|
|
6562
|
+
"generate",
|
|
6563
|
+
"validate",
|
|
6564
|
+
"report",
|
|
6565
|
+
"explain",
|
|
6566
|
+
"init",
|
|
6567
|
+
"diff",
|
|
6568
|
+
"describe",
|
|
6569
|
+
"skill"
|
|
6570
|
+
];
|
|
6571
|
+
function commandsForOption(option) {
|
|
6572
|
+
return COMMAND_ORDER.filter((command) => COMMAND_OPTION_SCOPE[command].includes(option));
|
|
6573
|
+
}
|
|
6574
|
+
function optionAppliesTo(option) {
|
|
6575
|
+
const commands = commandsForOption(option);
|
|
6576
|
+
if (commands.length === COMMAND_ORDER.length) return "all commands";
|
|
6577
|
+
return commands.join("/");
|
|
5129
6578
|
}
|
|
5130
6579
|
|
|
5131
6580
|
// src/agent/contract.ts
|
|
@@ -5217,11 +6666,11 @@ function buildAgentContract() {
|
|
|
5217
6666
|
invocation: {
|
|
5218
6667
|
oneShot: `npx ${PACKAGE_NAME}@latest`,
|
|
5219
6668
|
installed: "npx codebase-snapshot",
|
|
5220
|
-
bins: ["codebase-snapshot"],
|
|
6669
|
+
bins: ["codebase-snapshot", "snap"],
|
|
5221
6670
|
notes: [
|
|
5222
6671
|
`The bare package name also works: npx ${PACKAGE_NAME} <command>.`,
|
|
5223
|
-
"Pin @latest (or @
|
|
5224
|
-
"
|
|
6672
|
+
"Pin @latest (or @4) when you need the exact same version on every machine.",
|
|
6673
|
+
"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
6674
|
"Human progress goes to stderr. With --format json the payload on stdout is pure JSON."
|
|
5226
6675
|
]
|
|
5227
6676
|
},
|
|
@@ -5230,7 +6679,7 @@ function buildAgentContract() {
|
|
|
5230
6679
|
step: 1,
|
|
5231
6680
|
id: "verify",
|
|
5232
6681
|
run: `npx ${PACKAGE_NAME}@latest --version`,
|
|
5233
|
-
expect:
|
|
6682
|
+
expect: `prints v${GENERATOR_VERSION} on stdout and exits 0`,
|
|
5234
6683
|
notes: "If it prints nothing, clear the npm cache and retry, or use `npx -p <pkg> codebase-snapshot`."
|
|
5235
6684
|
},
|
|
5236
6685
|
{
|
|
@@ -5300,9 +6749,9 @@ function buildAgentContract() {
|
|
|
5300
6749
|
{
|
|
5301
6750
|
name: "explain",
|
|
5302
6751
|
aliases: [],
|
|
5303
|
-
summary: "Explain how one path would be treated: layer, kind, part, skip reason.",
|
|
6752
|
+
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
6753
|
positionals: ["<path>"],
|
|
5305
|
-
exits: [0
|
|
6754
|
+
exits: [0]
|
|
5306
6755
|
},
|
|
5307
6756
|
{
|
|
5308
6757
|
name: "diff",
|
|
@@ -5336,140 +6785,147 @@ function buildAgentContract() {
|
|
|
5336
6785
|
flag: "--out-dir",
|
|
5337
6786
|
type: "string",
|
|
5338
6787
|
default: "snapshot",
|
|
5339
|
-
appliesTo: "
|
|
6788
|
+
appliesTo: optionAppliesTo("out-dir"),
|
|
5340
6789
|
summary: "Where the snapshot is written. Must not be the repo root or a directory the tool did not create."
|
|
5341
6790
|
},
|
|
5342
6791
|
{
|
|
5343
6792
|
flag: "--max-part-bytes",
|
|
5344
6793
|
type: "string",
|
|
5345
6794
|
default: 307200,
|
|
5346
|
-
appliesTo: "
|
|
6795
|
+
appliesTo: optionAppliesTo("max-part-bytes"),
|
|
5347
6796
|
summary: 'Byte budget per markdown part. Accepts "120k" / "1.5mb". Minimum 4096.'
|
|
5348
6797
|
},
|
|
5349
6798
|
{
|
|
5350
6799
|
flag: "--large-file-bytes",
|
|
5351
6800
|
type: "string",
|
|
5352
6801
|
default: 204800,
|
|
5353
|
-
appliesTo: "
|
|
6802
|
+
appliesTo: optionAppliesTo("large-file-bytes"),
|
|
5354
6803
|
summary: "Above this size a file gets a head/tail preview instead of the full body."
|
|
5355
6804
|
},
|
|
5356
6805
|
{
|
|
5357
6806
|
flag: "--max-monolith-bytes",
|
|
5358
6807
|
type: "string",
|
|
5359
6808
|
default: "unset",
|
|
5360
|
-
appliesTo: "
|
|
6809
|
+
appliesTo: optionAppliesTo("max-monolith-bytes"),
|
|
5361
6810
|
summary: "Skip the monolith artifacts when they would exceed this size."
|
|
5362
6811
|
},
|
|
5363
6812
|
{
|
|
5364
6813
|
flag: "--include-monolith",
|
|
5365
6814
|
type: "boolean",
|
|
5366
6815
|
default: false,
|
|
5367
|
-
appliesTo: "
|
|
6816
|
+
appliesTo: optionAppliesTo("include-monolith"),
|
|
5368
6817
|
summary: "Also write full-codebase.json and full-codebase.md."
|
|
5369
6818
|
},
|
|
5370
6819
|
{
|
|
5371
6820
|
flag: "--include-sensitive-files",
|
|
5372
6821
|
type: "boolean",
|
|
5373
6822
|
default: false,
|
|
5374
|
-
appliesTo: "
|
|
6823
|
+
appliesTo: optionAppliesTo("include-sensitive-files"),
|
|
5375
6824
|
summary: "Also emit .env / *.pem / .npmrc style files. Off by default; they are recorded as skipped."
|
|
5376
6825
|
},
|
|
5377
6826
|
{
|
|
5378
6827
|
flag: "--skip-artifacts",
|
|
5379
6828
|
type: "string",
|
|
5380
6829
|
default: "unset",
|
|
5381
|
-
appliesTo: "
|
|
6830
|
+
appliesTo: optionAppliesTo("skip-artifacts"),
|
|
5382
6831
|
summary: "Comma-separated artifacts to omit (readme, manifest, parts, navigation, projectTree, audit, monolith)."
|
|
5383
6832
|
},
|
|
5384
6833
|
{
|
|
5385
6834
|
flag: "--only-artifacts",
|
|
5386
6835
|
type: "string",
|
|
5387
6836
|
default: "unset",
|
|
5388
|
-
appliesTo: "
|
|
6837
|
+
appliesTo: optionAppliesTo("only-artifacts"),
|
|
5389
6838
|
summary: "Write only these artifacts. Cannot be combined with --skip-artifacts."
|
|
5390
6839
|
},
|
|
5391
6840
|
{
|
|
5392
6841
|
flag: "--format",
|
|
5393
6842
|
type: "string",
|
|
5394
6843
|
default: "text",
|
|
5395
|
-
appliesTo: "
|
|
6844
|
+
appliesTo: optionAppliesTo("format"),
|
|
5396
6845
|
summary: 'Output format: "text" or "json". JSON goes to stdout, progress to stderr.'
|
|
5397
6846
|
},
|
|
5398
6847
|
{
|
|
5399
6848
|
flag: "--cwd",
|
|
5400
6849
|
type: "string",
|
|
5401
6850
|
default: "process.cwd()",
|
|
5402
|
-
appliesTo: "
|
|
6851
|
+
appliesTo: optionAppliesTo("cwd"),
|
|
5403
6852
|
summary: "Repository to operate on. Must be an existing directory."
|
|
5404
6853
|
},
|
|
5405
6854
|
{
|
|
5406
6855
|
flag: "--dry-run",
|
|
5407
6856
|
type: "boolean",
|
|
5408
6857
|
default: false,
|
|
5409
|
-
appliesTo: "
|
|
6858
|
+
appliesTo: optionAppliesTo("dry-run"),
|
|
5410
6859
|
summary: "Compute everything and print the result without writing."
|
|
5411
6860
|
},
|
|
5412
6861
|
{
|
|
5413
6862
|
flag: "--watch",
|
|
5414
6863
|
type: "boolean",
|
|
5415
6864
|
default: false,
|
|
5416
|
-
appliesTo: "
|
|
6865
|
+
appliesTo: optionAppliesTo("watch"),
|
|
5417
6866
|
summary: "Regenerate on every change until interrupted."
|
|
5418
6867
|
},
|
|
6868
|
+
{
|
|
6869
|
+
flag: "--no-config",
|
|
6870
|
+
type: "boolean",
|
|
6871
|
+
default: false,
|
|
6872
|
+
appliesTo: optionAppliesTo("no-config"),
|
|
6873
|
+
summary: "Ignore the config file and run on the built-in defaults."
|
|
6874
|
+
},
|
|
5419
6875
|
{
|
|
5420
6876
|
flag: "--preset",
|
|
5421
6877
|
type: "string",
|
|
5422
6878
|
default: "detected",
|
|
5423
|
-
appliesTo: "
|
|
6879
|
+
appliesTo: optionAppliesTo("preset"),
|
|
5424
6880
|
summary: "Force a project preset instead of detecting one. Unknown names are a usage error."
|
|
5425
6881
|
},
|
|
5426
6882
|
{
|
|
5427
6883
|
flag: "--include",
|
|
5428
6884
|
type: "string",
|
|
5429
6885
|
default: "unset",
|
|
5430
|
-
appliesTo: "
|
|
6886
|
+
appliesTo: optionAppliesTo("include"),
|
|
5431
6887
|
summary: "Comma-separated allow-list of globs to record in the generated config."
|
|
5432
6888
|
},
|
|
5433
6889
|
{
|
|
5434
6890
|
flag: "--exclude",
|
|
5435
6891
|
type: "string",
|
|
5436
6892
|
default: "unset",
|
|
5437
|
-
appliesTo: "
|
|
6893
|
+
appliesTo: optionAppliesTo("exclude"),
|
|
5438
6894
|
summary: "Comma-separated globs to record in the generated config."
|
|
5439
6895
|
},
|
|
5440
6896
|
{
|
|
5441
6897
|
flag: "--yes",
|
|
5442
6898
|
type: "boolean",
|
|
5443
6899
|
default: false,
|
|
5444
|
-
appliesTo: "
|
|
6900
|
+
appliesTo: optionAppliesTo("yes"),
|
|
5445
6901
|
summary: "Accept every default without prompting. Required in a non-TTY."
|
|
5446
6902
|
},
|
|
5447
6903
|
{
|
|
5448
6904
|
flag: "--minimal",
|
|
5449
6905
|
type: "boolean",
|
|
5450
6906
|
default: false,
|
|
5451
|
-
appliesTo: "
|
|
6907
|
+
appliesTo: optionAppliesTo("minimal"),
|
|
5452
6908
|
summary: "Write only the settings that differ from the defaults."
|
|
5453
6909
|
},
|
|
5454
6910
|
{
|
|
5455
6911
|
flag: "--force",
|
|
5456
6912
|
type: "boolean",
|
|
5457
6913
|
default: false,
|
|
5458
|
-
appliesTo: "
|
|
6914
|
+
appliesTo: optionAppliesTo("force"),
|
|
5459
6915
|
summary: "Overwrite an existing config file."
|
|
5460
6916
|
},
|
|
5461
6917
|
{
|
|
5462
6918
|
flag: "--install",
|
|
5463
6919
|
type: "boolean",
|
|
5464
6920
|
default: false,
|
|
5465
|
-
appliesTo: "
|
|
6921
|
+
appliesTo: optionAppliesTo("install"),
|
|
5466
6922
|
summary: "Write the skill into the repository instead of printing it."
|
|
5467
6923
|
},
|
|
5468
6924
|
{
|
|
5469
6925
|
flag: "--target",
|
|
5470
6926
|
type: "string",
|
|
5471
6927
|
default: "all",
|
|
5472
|
-
appliesTo: "
|
|
6928
|
+
appliesTo: optionAppliesTo("target"),
|
|
5473
6929
|
summary: 'Where to install: "claude", "agents", "cursor" or "all".'
|
|
5474
6930
|
}
|
|
5475
6931
|
],
|
|
@@ -5544,8 +7000,8 @@ function buildAgentContract() {
|
|
|
5544
7000
|
troubleshooting: [
|
|
5545
7001
|
{
|
|
5546
7002
|
symptom: "Command prints nothing and exits 0",
|
|
5547
|
-
cause: "
|
|
5548
|
-
fix: `
|
|
7003
|
+
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.",
|
|
7004
|
+
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
7005
|
},
|
|
5550
7006
|
{
|
|
5551
7007
|
symptom: "Exit 1 with 'Refusing to replace outDir'",
|
|
@@ -5619,7 +7075,8 @@ not resolved \u2014 clear the npm cache and retry; do not assume the repository
|
|
|
5619
7075
|
\`--format json\` if you want to parse it.
|
|
5620
7076
|
|
|
5621
7077
|
4. **Configure (recommended).** Run \`npx ${PACKAGE_NAME2}@latest init --yes\`. It detects
|
|
5622
|
-
the project shape, then writes \`snapshot.config.ts\`
|
|
7078
|
+
the project shape, then writes \`snapshot.config.ts\` (or \`.mjs\` when the project is
|
|
7079
|
+
not an ES module) listing **every** setting at its
|
|
5623
7080
|
current value, commented. Pass \`--preset <name>\` only if detection is wrong; the
|
|
5624
7081
|
preset names are in the contract. This step is optional: without a config the tool
|
|
5625
7082
|
runs on the same defaults.
|
|
@@ -5720,14 +7177,24 @@ function mergeAgentsFile(cwd, skill) {
|
|
|
5720
7177
|
next = `# AGENTS.md
|
|
5721
7178
|
|
|
5722
7179
|
${block}`;
|
|
5723
|
-
} else
|
|
7180
|
+
} else {
|
|
7181
|
+
const starts = existing.split(AGENTS_MARKER_START).length - 1;
|
|
7182
|
+
const ends = existing.split(AGENTS_MARKER_END).length - 1;
|
|
5724
7183
|
const start = existing.indexOf(AGENTS_MARKER_START);
|
|
5725
7184
|
const end = existing.indexOf(AGENTS_MARKER_END) + AGENTS_MARKER_END.length;
|
|
5726
|
-
|
|
5727
|
-
|
|
5728
|
-
|
|
7185
|
+
if (starts === 1 && ends === 1 && start < end) {
|
|
7186
|
+
next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(end)}`;
|
|
7187
|
+
} else if (starts > 0 && ends === 0) {
|
|
7188
|
+
next = `${existing.slice(0, start)}${block.trimEnd()}${existing.slice(
|
|
7189
|
+
start + AGENTS_MARKER_START.length
|
|
7190
|
+
)}`;
|
|
7191
|
+
} else if (existing.includes(block.trimEnd())) {
|
|
7192
|
+
next = existing;
|
|
7193
|
+
} else {
|
|
7194
|
+
next = `${existing.trimEnd()}
|
|
5729
7195
|
|
|
5730
7196
|
${block}`;
|
|
7197
|
+
}
|
|
5731
7198
|
}
|
|
5732
7199
|
const action = existing === null ? "created" : existing === next ? "unchanged" : "updated";
|
|
5733
7200
|
if (action !== "unchanged") {
|
|
@@ -5790,6 +7257,7 @@ var CLI_OPTIONS = {
|
|
|
5790
7257
|
mode: { type: "string" },
|
|
5791
7258
|
"dry-run": { type: "boolean" },
|
|
5792
7259
|
watch: { type: "boolean" },
|
|
7260
|
+
"no-config": { type: "boolean" },
|
|
5793
7261
|
preset: { type: "string" },
|
|
5794
7262
|
include: { type: "string" },
|
|
5795
7263
|
exclude: { type: "string" },
|
|
@@ -5836,6 +7304,7 @@ Options:
|
|
|
5836
7304
|
--format <text|json> CLI output format (default: text)
|
|
5837
7305
|
--dry-run Compute the snapshot without writing anything
|
|
5838
7306
|
--watch Re-generate whenever the repository changes
|
|
7307
|
+
--no-config Ignore snapshot.config.* and use the built-in defaults
|
|
5839
7308
|
|
|
5840
7309
|
init only:
|
|
5841
7310
|
--preset <name> Force a project preset instead of detecting one
|
|
@@ -5997,28 +7466,32 @@ function parseCliArgs(argv) {
|
|
|
5997
7466
|
} else if (rawSkip !== void 0) {
|
|
5998
7467
|
skipArtifacts = parseArtifactList(String(rawSkip), "--skip-artifacts");
|
|
5999
7468
|
}
|
|
6000
|
-
|
|
6001
|
-
|
|
6002
|
-
|
|
6003
|
-
if (
|
|
6004
|
-
|
|
6005
|
-
if (
|
|
6006
|
-
|
|
6007
|
-
|
|
6008
|
-
|
|
6009
|
-
|
|
6010
|
-
|
|
6011
|
-
|
|
6012
|
-
|
|
7469
|
+
const scope = COMMAND_OPTION_SCOPE[command];
|
|
7470
|
+
const ignored = [];
|
|
7471
|
+
for (const name of Object.keys(CLI_OPTIONS)) {
|
|
7472
|
+
if (scope.includes(name)) continue;
|
|
7473
|
+
const value = values[name];
|
|
7474
|
+
if (value === void 0 || value === false) continue;
|
|
7475
|
+
ignored.push(`--${name}`);
|
|
7476
|
+
}
|
|
7477
|
+
if (ignored.length > 0) {
|
|
7478
|
+
const owners = Array.from(
|
|
7479
|
+
new Set(
|
|
7480
|
+
ignored.flatMap(
|
|
7481
|
+
(flag) => commandsForOption(flag.replace(/^--/, "")).map((owner) => `\`${owner}\``)
|
|
7482
|
+
)
|
|
7483
|
+
)
|
|
7484
|
+
);
|
|
7485
|
+
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)." : "";
|
|
7486
|
+
throw new UsageError(
|
|
7487
|
+
`${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}`
|
|
7488
|
+
);
|
|
6013
7489
|
}
|
|
6014
7490
|
if (command === "init" && values.preset !== void 0) {
|
|
6015
7491
|
const requested = String(values.preset);
|
|
6016
7492
|
if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
|
|
6017
7493
|
}
|
|
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))) {
|
|
7494
|
+
if (command === "skill" && values.target !== void 0 && !isSkillTarget(String(values.target))) {
|
|
6022
7495
|
throw new UsageError(
|
|
6023
7496
|
`--target must be one of: ${SKILL_TARGETS.join(", ")} (received "${String(values.target)}").`
|
|
6024
7497
|
);
|
|
@@ -6029,6 +7502,7 @@ function parseCliArgs(argv) {
|
|
|
6029
7502
|
format: rawFormat,
|
|
6030
7503
|
dryRun: values["dry-run"] === true,
|
|
6031
7504
|
watch: values.watch === true,
|
|
7505
|
+
noConfig: values["no-config"] === true,
|
|
6032
7506
|
preset: values.preset === void 0 ? void 0 : String(values.preset),
|
|
6033
7507
|
maxPartBytesRaw: values["max-part-bytes"] === void 0 ? void 0 : String(values["max-part-bytes"]),
|
|
6034
7508
|
include: values.include === void 0 ? void 0 : String(values.include),
|
|
@@ -6082,6 +7556,12 @@ function splitList(value) {
|
|
|
6082
7556
|
if (value === void 0) return void 0;
|
|
6083
7557
|
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
6084
7558
|
}
|
|
7559
|
+
function displayOutPath(cwd, outDir, ...segments) {
|
|
7560
|
+
const absolute = path10__namespace.resolve(cwd, outDir, ...segments);
|
|
7561
|
+
const relative5 = path10__namespace.relative(cwd, absolute);
|
|
7562
|
+
if (relative5 && !relative5.startsWith("..") && !path10__namespace.isAbsolute(relative5)) return relative5;
|
|
7563
|
+
return absolute;
|
|
7564
|
+
}
|
|
6085
7565
|
function describeManifestShapeProblem(manifest) {
|
|
6086
7566
|
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
6087
7567
|
return "not a JSON object";
|
|
@@ -6101,6 +7581,7 @@ async function runInitCommand(invocation) {
|
|
|
6101
7581
|
const result = await runInit({
|
|
6102
7582
|
cwd: invocation.cwd,
|
|
6103
7583
|
preset: invocation.preset,
|
|
7584
|
+
outDir: invocation.outDir,
|
|
6104
7585
|
maxPartBytes: invocation.maxPartBytesRaw ?? invocation.maxPartBytes,
|
|
6105
7586
|
include: splitList(invocation.include),
|
|
6106
7587
|
exclude: splitList(invocation.exclude),
|
|
@@ -6127,7 +7608,11 @@ async function runInitCommand(invocation) {
|
|
|
6127
7608
|
}
|
|
6128
7609
|
for (const note of result.notes) info(` \u26A0 ${note}`);
|
|
6129
7610
|
info("");
|
|
6130
|
-
|
|
7611
|
+
if (result.scripts.length > 0) {
|
|
7612
|
+
info(" Next: npm run snapshot");
|
|
7613
|
+
} else {
|
|
7614
|
+
info(` Next: ${GENERATOR_NAME} generate`);
|
|
7615
|
+
}
|
|
6131
7616
|
info(
|
|
6132
7617
|
` Tip: \`${GENERATOR_NAME} skill --install\` makes this repo discoverable by AI agents.`
|
|
6133
7618
|
);
|
|
@@ -6143,6 +7628,13 @@ async function runInitCommand(invocation) {
|
|
|
6143
7628
|
}
|
|
6144
7629
|
function runSkillCommand(invocation) {
|
|
6145
7630
|
if (!invocation.install) {
|
|
7631
|
+
if (invocation.format === "json") {
|
|
7632
|
+
process.stdout.write(
|
|
7633
|
+
`${JSON.stringify({ name: SKILL_NAME, version: GENERATOR_VERSION, skill: renderSkill() }, null, 2)}
|
|
7634
|
+
`
|
|
7635
|
+
);
|
|
7636
|
+
return 0;
|
|
7637
|
+
}
|
|
6146
7638
|
process.stdout.write(renderSkill());
|
|
6147
7639
|
return 0;
|
|
6148
7640
|
}
|
|
@@ -6219,7 +7711,8 @@ ${USAGE}
|
|
|
6219
7711
|
if (invocation.command === "skill") {
|
|
6220
7712
|
return runSkillCommand(invocation);
|
|
6221
7713
|
}
|
|
6222
|
-
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
7714
|
+
const loaded = invocation.noConfig ? { config: null, warnings: [], errors: [], file: null } : await loadConfigFileDetailed(invocation.cwd);
|
|
7715
|
+
if (invocation.noConfig) info("\u2699 --no-config: using built-in defaults, config file ignored");
|
|
6223
7716
|
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
6224
7717
|
if (loaded.errors.length > 0) {
|
|
6225
7718
|
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
@@ -6266,8 +7759,7 @@ ${USAGE}
|
|
|
6266
7759
|
warn(`\u274C ${manifestPath} is not a snapshot manifest: ${shapeError}`);
|
|
6267
7760
|
return 1;
|
|
6268
7761
|
}
|
|
6269
|
-
|
|
6270
|
-
emit(invocation.format, { manifest, validation }, () => {
|
|
7762
|
+
emit(invocation.format, { manifest, validation: null }, () => {
|
|
6271
7763
|
info(`
|
|
6272
7764
|
\u{1F4CA} Codebase Snapshot Report (${resolved.cwd})`);
|
|
6273
7765
|
info(` - Generated at: ${manifest.generatedAt}`);
|
|
@@ -6279,19 +7771,19 @@ ${USAGE}
|
|
|
6279
7771
|
` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
|
|
6280
7772
|
);
|
|
6281
7773
|
info(` - Total parts: ${manifest.counts.partsCount}`);
|
|
6282
|
-
info(
|
|
7774
|
+
info(" - Integrity: not checked (run `codebase-snapshot validate`)");
|
|
6283
7775
|
info(` - Warnings: ${manifest.warnings?.length ?? 0}`);
|
|
6284
7776
|
info("\n Layers:");
|
|
6285
7777
|
for (const [layer2, count] of Object.entries(manifest.layers)) {
|
|
6286
7778
|
info(` - ${layer2.padEnd(16)}: ${count} files`);
|
|
6287
7779
|
}
|
|
6288
7780
|
});
|
|
6289
|
-
return
|
|
7781
|
+
return 0;
|
|
6290
7782
|
}
|
|
6291
7783
|
if (invocation.command === "explain") {
|
|
6292
7784
|
const result = explainPath(resolved, invocation.args[0]);
|
|
6293
7785
|
emit(invocation.format, result, () => info(renderExplainText(result)));
|
|
6294
|
-
return
|
|
7786
|
+
return 0;
|
|
6295
7787
|
}
|
|
6296
7788
|
if (invocation.command === "diff") {
|
|
6297
7789
|
try {
|
|
@@ -6315,7 +7807,9 @@ ${USAGE}
|
|
|
6315
7807
|
if (invocation.dryRun) {
|
|
6316
7808
|
info("\n\u{1F9EA} Dry run \u2014 nothing was written:");
|
|
6317
7809
|
info(` - Would track: ${manifest.counts.totalFiles} files`);
|
|
6318
|
-
info(
|
|
7810
|
+
info(
|
|
7811
|
+
` - Would write: ${manifest.counts.partsCount} part(s) under ${displayOutPath(resolved.cwd, resolved.outDir, "parts")}`
|
|
7812
|
+
);
|
|
6319
7813
|
info(` - Skipped: ${manifest.counts.skippedCount} file(s)`);
|
|
6320
7814
|
info(` - Warnings: ${manifest.warnings.length}`);
|
|
6321
7815
|
info(` - Integrity: ${manifest.validation.status}`);
|
|
@@ -6330,7 +7824,9 @@ ${USAGE}
|
|
|
6330
7824
|
);
|
|
6331
7825
|
const { artifacts } = resolved;
|
|
6332
7826
|
if (artifacts.parts) {
|
|
6333
|
-
info(
|
|
7827
|
+
info(
|
|
7828
|
+
` - Parts written: ${manifest.counts.partsCount} under ${displayOutPath(resolved.cwd, resolved.outDir, "parts")}/`
|
|
7829
|
+
);
|
|
6334
7830
|
}
|
|
6335
7831
|
const reports = [
|
|
6336
7832
|
artifacts.projectTree && "project-tree",
|
|
@@ -6338,10 +7834,28 @@ ${USAGE}
|
|
|
6338
7834
|
artifacts.audit && "audit"
|
|
6339
7835
|
].filter(Boolean);
|
|
6340
7836
|
if (reports.length > 0) {
|
|
6341
|
-
info(
|
|
7837
|
+
info(
|
|
7838
|
+
` - Reports: ${displayOutPath(resolved.cwd, resolved.outDir, "reports")}/ (${reports.join(", ")})`
|
|
7839
|
+
);
|
|
7840
|
+
}
|
|
7841
|
+
if (artifacts.manifest) {
|
|
7842
|
+
info(` - Manifest: ${displayOutPath(resolved.cwd, resolved.outDir, "manifest.json")}`);
|
|
7843
|
+
}
|
|
7844
|
+
if (artifacts.monolith) {
|
|
7845
|
+
const monolithFiles = [
|
|
7846
|
+
manifest.artifacts?.fullJson ? "full-codebase.json" : null,
|
|
7847
|
+
manifest.artifacts?.fullMarkdown ? "full-codebase.md" : null
|
|
7848
|
+
].filter((name) => name !== null);
|
|
7849
|
+
if (monolithFiles.length === 2) {
|
|
7850
|
+
info(
|
|
7851
|
+
` - Monolith: ${displayOutPath(resolved.cwd, resolved.outDir, "full-codebase.{json,md}")}`
|
|
7852
|
+
);
|
|
7853
|
+
} else if (monolithFiles.length === 1) {
|
|
7854
|
+
info(
|
|
7855
|
+
` - Monolith: ${displayOutPath(resolved.cwd, resolved.outDir, monolithFiles[0])} (the other was skipped by --max-monolith-bytes)`
|
|
7856
|
+
);
|
|
7857
|
+
}
|
|
6342
7858
|
}
|
|
6343
|
-
if (artifacts.manifest) info(` - Manifest: ${resolved.outDir}/manifest.json`);
|
|
6344
|
-
if (artifacts.monolith) info(` - Monolith: ${resolved.outDir}/full-codebase.{json,md}`);
|
|
6345
7859
|
if (reports.length === 0 && !artifacts.parts && !artifacts.manifest && !artifacts.monolith) {
|
|
6346
7860
|
info(` - Artifacts: all disabled (only the ownership marker was written)`);
|
|
6347
7861
|
}
|
|
@@ -6376,18 +7890,43 @@ ${USAGE}
|
|
|
6376
7890
|
async function runWatchLoop(invocation, resolved) {
|
|
6377
7891
|
const outDir = path10__namespace.resolve(resolved.cwd, resolved.outDir);
|
|
6378
7892
|
const tmpDir = path10__namespace.resolve(resolved.cwd, resolved.tmpDir);
|
|
7893
|
+
const VCS_SEGMENTS = /* @__PURE__ */ new Set([".git", ".hg", ".svn", ".bzr"]);
|
|
6379
7894
|
const outName = path10__namespace.basename(outDir).toLowerCase();
|
|
6380
7895
|
const tmpName = path10__namespace.basename(tmpDir).toLowerCase();
|
|
6381
7896
|
const isGenerated = (candidate) => candidate.toLowerCase().split(/[\\/]/).some(
|
|
6382
|
-
(segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`)
|
|
7897
|
+
(segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`) || VCS_SEGMENTS.has(segment)
|
|
6383
7898
|
);
|
|
6384
7899
|
let running = false;
|
|
6385
|
-
const regenerate = () => {
|
|
7900
|
+
const regenerate = async () => {
|
|
6386
7901
|
if (running) return;
|
|
6387
7902
|
running = true;
|
|
6388
7903
|
const started = performance.now();
|
|
6389
7904
|
try {
|
|
6390
|
-
const
|
|
7905
|
+
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
7906
|
+
if (loaded.errors.length > 0) {
|
|
7907
|
+
for (const error of loaded.errors) warn(`\u26A0 Config: ${error}`);
|
|
7908
|
+
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
7909
|
+
return;
|
|
7910
|
+
}
|
|
7911
|
+
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
7912
|
+
const next = resolveConfig(
|
|
7913
|
+
loaded.config,
|
|
7914
|
+
{
|
|
7915
|
+
mode: invocation.command,
|
|
7916
|
+
format: invocation.format,
|
|
7917
|
+
dryRun: invocation.dryRun,
|
|
7918
|
+
cwd: invocation.cwd,
|
|
7919
|
+
outDir: invocation.outDir,
|
|
7920
|
+
includeMonolith: invocation.includeMonolith,
|
|
7921
|
+
includeSensitiveFiles: invocation.includeSensitiveFiles,
|
|
7922
|
+
maxPartBytes: invocation.maxPartBytes,
|
|
7923
|
+
largeFileBytes: invocation.largeFileBytes,
|
|
7924
|
+
maxMonolithBytes: invocation.maxMonolithBytes,
|
|
7925
|
+
skipArtifacts: invocation.skipArtifacts
|
|
7926
|
+
},
|
|
7927
|
+
invocation.cwd
|
|
7928
|
+
);
|
|
7929
|
+
const manifest = generateSnapshot(next, { dryRun: invocation.dryRun });
|
|
6391
7930
|
const elapsed = ((performance.now() - started) / 1e3).toFixed(2);
|
|
6392
7931
|
info(
|
|
6393
7932
|
`\u267B\uFE0F ${(/* @__PURE__ */ new Date()).toISOString()} ${manifest.counts.totalFiles} file(s), ${manifest.counts.partsCount} part(s), ${manifest.validation.status} (${elapsed}s)`
|
|
@@ -6399,7 +7938,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
6399
7938
|
}
|
|
6400
7939
|
};
|
|
6401
7940
|
info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
|
|
6402
|
-
regenerate();
|
|
7941
|
+
void regenerate();
|
|
6403
7942
|
let timer = null;
|
|
6404
7943
|
const watcher = fs9__namespace.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
|
|
6405
7944
|
if (filename) {
|
|
@@ -6407,7 +7946,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
6407
7946
|
if (isGenerated(changed)) return;
|
|
6408
7947
|
}
|
|
6409
7948
|
if (timer) clearTimeout(timer);
|
|
6410
|
-
timer = setTimeout(regenerate, 250);
|
|
7949
|
+
timer = setTimeout(() => void regenerate(), 250);
|
|
6411
7950
|
});
|
|
6412
7951
|
await new Promise((resolve6) => {
|
|
6413
7952
|
const stop = () => {
|