@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/index.cjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
var crypto = require('crypto');
|
|
4
4
|
var fs9 = require('fs');
|
|
5
|
+
var os = require('os');
|
|
5
6
|
var path10 = require('path');
|
|
6
7
|
var ts = require('typescript');
|
|
7
8
|
var url = require('url');
|
|
@@ -30,6 +31,7 @@ function _interopNamespace(e) {
|
|
|
30
31
|
|
|
31
32
|
var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
|
|
32
33
|
var fs9__namespace = /*#__PURE__*/_interopNamespace(fs9);
|
|
34
|
+
var os__namespace = /*#__PURE__*/_interopNamespace(os);
|
|
33
35
|
var path10__namespace = /*#__PURE__*/_interopNamespace(path10);
|
|
34
36
|
var ts__default = /*#__PURE__*/_interopDefault(ts);
|
|
35
37
|
var childProcess__namespace = /*#__PURE__*/_interopNamespace(childProcess);
|
|
@@ -576,14 +578,42 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
576
578
|
const modifiers = ts__default.default.canHaveModifiers(node) ? ts__default.default.getModifiers(node) : void 0;
|
|
577
579
|
return Boolean(modifiers?.some((m) => m.kind === ts__default.default.SyntaxKind.DefaultKeyword));
|
|
578
580
|
};
|
|
579
|
-
const
|
|
581
|
+
const rendersJsx = (node) => {
|
|
582
|
+
const pending = [node];
|
|
583
|
+
while (pending.length > 0) {
|
|
584
|
+
const current = pending.pop();
|
|
585
|
+
if (ts__default.default.isJsxElement(current) || ts__default.default.isJsxSelfClosingElement(current) || ts__default.default.isJsxFragment(current)) {
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
if (ts__default.default.isCallExpression(current)) {
|
|
589
|
+
const callee = current.expression;
|
|
590
|
+
const calleeName = ts__default.default.isIdentifier(callee) ? callee.text : ts__default.default.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
|
591
|
+
if (calleeName === "createElement") return true;
|
|
592
|
+
}
|
|
593
|
+
ts__default.default.forEachChild(current, (child) => {
|
|
594
|
+
pending.push(child);
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
return false;
|
|
598
|
+
};
|
|
599
|
+
const REACT_BASE_CLASS = /(?:^|\.)(?:PureComponent|Component)$/;
|
|
600
|
+
const extendsReactBase = (node) => {
|
|
601
|
+
const heritage = (node.heritageClauses ?? []).find(
|
|
602
|
+
(clause) => clause.token === ts__default.default.SyntaxKind.ExtendsKeyword
|
|
603
|
+
);
|
|
604
|
+
const expression = heritage?.types[0]?.expression;
|
|
605
|
+
if (!expression) return false;
|
|
606
|
+
const text2 = ts__default.default.isIdentifier(expression) ? expression.text : expression.getText(sourceFile);
|
|
607
|
+
return REACT_BASE_CLASS.test(text2);
|
|
608
|
+
};
|
|
609
|
+
const recordFunctionLike = (name, exported, node) => {
|
|
580
610
|
summary.functions.push(name);
|
|
581
611
|
if (exported) {
|
|
582
612
|
summary.exports.push(name);
|
|
583
613
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
584
614
|
}
|
|
585
615
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
586
|
-
if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
|
|
616
|
+
if (/^[A-Z]/.test(name) && rendersJsx(node)) summary.reactComponents.push(name);
|
|
587
617
|
};
|
|
588
618
|
const stack = [sourceFile];
|
|
589
619
|
while (stack.length > 0) {
|
|
@@ -612,7 +642,7 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
612
642
|
if (ts__default.default.isFunctionDeclaration(node)) {
|
|
613
643
|
const exported = isExported(node);
|
|
614
644
|
if (node.name) {
|
|
615
|
-
recordFunctionLike(node.name.text, exported);
|
|
645
|
+
recordFunctionLike(node.name.text, exported, node);
|
|
616
646
|
if (isDefault(node)) summary.exports.push("default");
|
|
617
647
|
} else if (isDefault(node)) {
|
|
618
648
|
summary.exports.push("default");
|
|
@@ -623,7 +653,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
623
653
|
summary.classes.push(node.name.text);
|
|
624
654
|
if (isExported(node)) summary.exports.push(node.name.text);
|
|
625
655
|
if (isDefault(node)) summary.exports.push("default");
|
|
626
|
-
if (/^[A-Z]/.test(node.name.text))
|
|
656
|
+
if (/^[A-Z]/.test(node.name.text) && (extendsReactBase(node) || rendersJsx(node))) {
|
|
657
|
+
summary.reactComponents.push(node.name.text);
|
|
658
|
+
}
|
|
627
659
|
} else if (isDefault(node)) {
|
|
628
660
|
summary.exports.push("default");
|
|
629
661
|
}
|
|
@@ -651,7 +683,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
651
683
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
652
684
|
}
|
|
653
685
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
654
|
-
if (/^[A-Z]/.test(name)
|
|
686
|
+
if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
|
|
687
|
+
summary.reactComponents.push(name);
|
|
688
|
+
}
|
|
655
689
|
}
|
|
656
690
|
}
|
|
657
691
|
const children = [];
|
|
@@ -1705,6 +1739,416 @@ function toDetected(preset) {
|
|
|
1705
1739
|
function detectPreset(cwd, forced) {
|
|
1706
1740
|
return detectPresetFromContext(buildPresetContext(cwd), forced);
|
|
1707
1741
|
}
|
|
1742
|
+
|
|
1743
|
+
// src/security/patterns.ts
|
|
1744
|
+
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)";
|
|
1745
|
+
var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
|
|
1746
|
+
var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|process\\.env|Buffer|Record|object)\\b)";
|
|
1747
|
+
var UNQUOTED_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{1,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
1748
|
+
var UNQUOTED_AMBIGUOUS_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{8,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
1749
|
+
var UNQUOTED_STRONG_NAME = `(?<!id[-_])${STRONG_CREDENTIAL_NAME}`;
|
|
1750
|
+
var DEFAULT_SECRET_PATTERNS = [
|
|
1751
|
+
{
|
|
1752
|
+
rule: "private-key-header",
|
|
1753
|
+
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
rule: "jwt-token",
|
|
1757
|
+
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
1758
|
+
},
|
|
1759
|
+
{
|
|
1760
|
+
rule: "aws-access-key-id",
|
|
1761
|
+
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
// The name is no longer limited to the three snake_case spellings: an
|
|
1765
|
+
// `awsSecret` / `secretAccessKey` property leaked verbatim because only the
|
|
1766
|
+
// literal words matched. The value is masked alone, so the key survives.
|
|
1767
|
+
rule: "aws-secret-access-key",
|
|
1768
|
+
pattern: new RegExp(
|
|
1769
|
+
`${STRONG_CREDENTIAL_NAME}["']?\\s{0,8}[:=]\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
|
|
1770
|
+
"i"
|
|
1771
|
+
),
|
|
1772
|
+
valueGroup: 1
|
|
1773
|
+
},
|
|
1774
|
+
{
|
|
1775
|
+
// A 40-character base64 value with no key name in sight. A key-independent
|
|
1776
|
+
// rule is the only thing that catches `new AWS.SecretKey(…)` arguments and
|
|
1777
|
+
// pasted `~/.aws/credentials` fragments, but a bare length test also matches
|
|
1778
|
+
// every 40-char git SHA-1, so the value must mix case and digits and must
|
|
1779
|
+
// not be pure hex.
|
|
1780
|
+
rule: "aws-secret-access-key-value",
|
|
1781
|
+
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}/
|
|
1782
|
+
},
|
|
1783
|
+
{
|
|
1784
|
+
rule: "github-token",
|
|
1785
|
+
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
1786
|
+
},
|
|
1787
|
+
{
|
|
1788
|
+
// `xoxc-`/`xoxd-` are the user and refresh tokens, `xoxb`/`xoxp` the bot and
|
|
1789
|
+
// user tokens; the whole Slack family shares one prefix shape.
|
|
1790
|
+
rule: "slack-token",
|
|
1791
|
+
pattern: /xox[baprsdc]-[0-9A-Za-z-]{10,250}/
|
|
1792
|
+
},
|
|
1793
|
+
{
|
|
1794
|
+
// Prefix-only vendors. Each prefix below is documented, globally unique and
|
|
1795
|
+
// cannot collide with an ordinary identifier, which is what makes a bare
|
|
1796
|
+
// prefix rule cheap and safe: `glpat-` (GitLab), `hvs.`/`s.` (Vault service
|
|
1797
|
+
// and batch tokens), `whsec_` (Stripe webhook secret), `dckr_pat_` (Docker
|
|
1798
|
+
// Hub), `hf_` (HuggingFace), `ya29.` (Google OAuth access token).
|
|
1799
|
+
rule: "known-vendor-token-prefix",
|
|
1800
|
+
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})/
|
|
1801
|
+
},
|
|
1802
|
+
{
|
|
1803
|
+
rule: "slack-webhook-url",
|
|
1804
|
+
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
1805
|
+
},
|
|
1806
|
+
{
|
|
1807
|
+
rule: "stripe-secret-key",
|
|
1808
|
+
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
1809
|
+
},
|
|
1810
|
+
{
|
|
1811
|
+
rule: "google-api-key",
|
|
1812
|
+
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
rule: "google-oauth-client-secret",
|
|
1816
|
+
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
1817
|
+
},
|
|
1818
|
+
{
|
|
1819
|
+
rule: "openai-anthropic-key",
|
|
1820
|
+
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
1821
|
+
},
|
|
1822
|
+
{
|
|
1823
|
+
rule: "npm-token",
|
|
1824
|
+
pattern: /npm_[A-Za-z0-9]{36}/
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
rule: "pypi-token",
|
|
1828
|
+
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
1829
|
+
},
|
|
1830
|
+
{
|
|
1831
|
+
rule: "sendgrid-key",
|
|
1832
|
+
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
1833
|
+
},
|
|
1834
|
+
{
|
|
1835
|
+
rule: "twilio-api-key",
|
|
1836
|
+
pattern: /SK[0-9a-fA-F]{32}/
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
rule: "digitalocean-token",
|
|
1840
|
+
pattern: /dop_v1_[0-9a-f]{64}/
|
|
1841
|
+
},
|
|
1842
|
+
{
|
|
1843
|
+
rule: "azure-storage-account-key",
|
|
1844
|
+
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
// Scheme-less userinfo: `//user:password@host`.
|
|
1848
|
+
//
|
|
1849
|
+
// Enumerating schemes is a losing game — `mysqlx`, `svn`, `telnet`, `imap`,
|
|
1850
|
+
// `pop3`, `nntp`, `kafka`, `elasticsearch` each leaked a password while the
|
|
1851
|
+
// list grew by hand. This rule matches the shape instead, so any scheme
|
|
1852
|
+
// (including one invented tomorrow) is covered, provided the `//` is followed
|
|
1853
|
+
// immediately by the userinfo: `// see the docs` and a bare reminder comment
|
|
1854
|
+
// never match, because a space or the comment wording breaks the shape.
|
|
1855
|
+
rule: "uri-credentials",
|
|
1856
|
+
pattern: /\/\/[^\s:/@"'`;()<>,]{1,128}:[^\s:/@"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
|
|
1857
|
+
},
|
|
1858
|
+
{
|
|
1859
|
+
// The host tail stops at quotes, `;`, `)`, `<`, `>` and `,`: an unbounded
|
|
1860
|
+
// `[^\s/]` used to swallow the closing quote and the statement separator of
|
|
1861
|
+
// the string the URL was written in.
|
|
1862
|
+
rule: "database-uri-credentials",
|
|
1863
|
+
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}/
|
|
1864
|
+
},
|
|
1865
|
+
{
|
|
1866
|
+
rule: "basic-auth-url",
|
|
1867
|
+
pattern: /(?:https?|ftp|sftp|ssh|git|smtp|smtps|ldap|ldaps):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/i
|
|
1868
|
+
},
|
|
1869
|
+
{
|
|
1870
|
+
// Quoted credentials in any common key spelling. The value may be as short
|
|
1871
|
+
// as one character: `password: "hunter2"` is a credential whatever its
|
|
1872
|
+
// entropy, and a length floor was exactly how it survived into `parts`.
|
|
1873
|
+
//
|
|
1874
|
+
// The key is either bare (`dbPassword:`) or wholly quoted (`"dbPassword":`).
|
|
1875
|
+
// There is deliberately no optional quote *before* the separator: that let a
|
|
1876
|
+
// previous string's closing quote stand in for the key's, so
|
|
1877
|
+
// ``${pass ? " PASS" : " FAIL"}`` had `" FAIL"` masked as if `PASS` were
|
|
1878
|
+
// the key name.
|
|
1879
|
+
rule: "credential-assignment",
|
|
1880
|
+
pattern: new RegExp(
|
|
1881
|
+
`(?:["']${STRONG_CREDENTIAL_NAME}["']|${STRONG_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{1,512})\\1`,
|
|
1882
|
+
"i"
|
|
1883
|
+
),
|
|
1884
|
+
valueGroup: 2
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
// `pass`, `id-token` and `credentials` are ambiguous on their own, so they
|
|
1888
|
+
// are accepted only against a quoted value long enough to be a credential.
|
|
1889
|
+
rule: "credential-assignment-weak",
|
|
1890
|
+
pattern: new RegExp(
|
|
1891
|
+
`(?:["']${WEAK_CREDENTIAL_NAME}["']|${WEAK_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{8,512})\\1`,
|
|
1892
|
+
"i"
|
|
1893
|
+
),
|
|
1894
|
+
valueGroup: 2
|
|
1895
|
+
},
|
|
1896
|
+
{
|
|
1897
|
+
// The unquoted twin for YAML, docker-compose and `.env`: those formats never
|
|
1898
|
+
// quote their values, so the rules above cannot see them. `valueGroup` keeps
|
|
1899
|
+
// `POSTGRES_PASSWORD:` in the output.
|
|
1900
|
+
rule: "credential-assignment-unquoted",
|
|
1901
|
+
pattern: new RegExp(
|
|
1902
|
+
`${UNQUOTED_STRONG_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
|
|
1903
|
+
"i"
|
|
1904
|
+
),
|
|
1905
|
+
valueGroup: 1
|
|
1906
|
+
},
|
|
1907
|
+
{
|
|
1908
|
+
// `pass: hunter2hunter2` and `credentials: hunter2hunter2`: the ambiguous
|
|
1909
|
+
// names, unquoted, with the length and non-digit floor that keeps
|
|
1910
|
+
// `pass: 3` / `credentials: none` readable.
|
|
1911
|
+
rule: "credential-assignment-unquoted-weak",
|
|
1912
|
+
pattern: new RegExp(
|
|
1913
|
+
`${WEAK_CREDENTIAL_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
|
|
1914
|
+
"i"
|
|
1915
|
+
),
|
|
1916
|
+
valueGroup: 1
|
|
1917
|
+
},
|
|
1918
|
+
{
|
|
1919
|
+
// `Authorization: Bearer <token>` and `Authorization: Basic <base64>`.
|
|
1920
|
+
//
|
|
1921
|
+
// The scheme is consumed by a lookbehind so the match is the token alone and
|
|
1922
|
+
// needs no `valueGroup`. A lone `Bearer`/`Basic`, and a word after it that
|
|
1923
|
+
// cannot be a credential (`Bearer authentication`), stay readable: the value
|
|
1924
|
+
// must contain a digit, be at least 32 chars, or carry base64 `=` padding.
|
|
1925
|
+
rule: "authorization-header",
|
|
1926
|
+
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_.+/=-])/
|
|
1927
|
+
},
|
|
1928
|
+
{
|
|
1929
|
+
// Broader names than before (`apiKey`, `clientSecret`, `privateKey` were
|
|
1930
|
+
// invisible to it) and value-only masking. Kept alongside the two rules
|
|
1931
|
+
// above because it also matches a *quoted key* followed by an unquoted
|
|
1932
|
+
// value, e.g. `"client_secret": abc123`.
|
|
1933
|
+
rule: "generic-secret-assignment",
|
|
1934
|
+
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["']([^"'\s]{8,256})["']/i,
|
|
1935
|
+
valueGroup: 1
|
|
1936
|
+
},
|
|
1937
|
+
{
|
|
1938
|
+
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
1939
|
+
// was therefore invisible to the rule above. Anchored to the line start and
|
|
1940
|
+
// limited to secret-looking names to stay at zero false positives.
|
|
1941
|
+
//
|
|
1942
|
+
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
1943
|
+
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
1944
|
+
// every offset of a long identifier, which made this rule quadratic
|
|
1945
|
+
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
1946
|
+
// The value is matched on its own — `=\s*(value)` — and masked alone.
|
|
1947
|
+
// Anchoring the value to end-of-line (`[^\s#]{6,512}$`) meant ANY trailing
|
|
1948
|
+
// comment or space disabled the rule, so `TOKEN=secret # rotate` and
|
|
1949
|
+
// `TOKEN=secret ` were emitted in clear; a CRLF file disabled it wholesale.
|
|
1950
|
+
rule: "env-secret-assignment",
|
|
1951
|
+
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,
|
|
1952
|
+
valueGroup: 1
|
|
1953
|
+
}
|
|
1954
|
+
];
|
|
1955
|
+
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
1956
|
+
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
1957
|
+
|
|
1958
|
+
// src/security/redact.ts
|
|
1959
|
+
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
1960
|
+
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
1961
|
+
function describeValue(value) {
|
|
1962
|
+
if (value === null) return "null";
|
|
1963
|
+
if (Array.isArray(value)) return "an array";
|
|
1964
|
+
if (value instanceof RegExp) return value.toString();
|
|
1965
|
+
if (typeof value === "string") return `the string ${JSON.stringify(value)}`;
|
|
1966
|
+
return `a ${typeof value}`;
|
|
1967
|
+
}
|
|
1968
|
+
function compilePattern(rule, pattern, valueGroup) {
|
|
1969
|
+
if (pattern instanceof RegExp) {
|
|
1970
|
+
const flags = new Set(pattern.flags.replace(/[gy]/g, "").split(""));
|
|
1971
|
+
if (valueGroup !== void 0) flags.add("d");
|
|
1972
|
+
return {
|
|
1973
|
+
rule,
|
|
1974
|
+
pattern: new RegExp(pattern.source, [...flags].join("")),
|
|
1975
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
if (typeof pattern === "string") {
|
|
1979
|
+
try {
|
|
1980
|
+
return {
|
|
1981
|
+
rule,
|
|
1982
|
+
pattern: new RegExp(pattern, valueGroup === void 0 ? "i" : "id"),
|
|
1983
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1984
|
+
};
|
|
1985
|
+
} catch (err) {
|
|
1986
|
+
throw new Error(
|
|
1987
|
+
`Invalid regular expression for secret rule "${rule}": ${err.message}`
|
|
1988
|
+
);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
throw new Error(
|
|
1992
|
+
`Secret rule "${rule}" has a pattern that is neither a RegExp nor a string (received ${describeValue(pattern)}).`
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1995
|
+
function compileSecretRules(rules) {
|
|
1996
|
+
if (!Array.isArray(rules)) {
|
|
1997
|
+
throw new Error(
|
|
1998
|
+
`secrets.customPatterns must be an array of secret rules (received ${describeValue(rules)}).`
|
|
1999
|
+
);
|
|
2000
|
+
}
|
|
2001
|
+
let generated = 0;
|
|
2002
|
+
return rules.map((entry, index) => {
|
|
2003
|
+
const where = `secrets.customPatterns[${index}]`;
|
|
2004
|
+
if (entry instanceof RegExp) {
|
|
2005
|
+
generated += 1;
|
|
2006
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
2007
|
+
}
|
|
2008
|
+
if (typeof entry === "string") {
|
|
2009
|
+
generated += 1;
|
|
2010
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
2011
|
+
}
|
|
2012
|
+
if (typeof entry === "object" && entry !== null) {
|
|
2013
|
+
const candidate = entry;
|
|
2014
|
+
if (typeof candidate.rule !== "string" || candidate.rule.trim() === "") {
|
|
2015
|
+
throw new Error(
|
|
2016
|
+
`${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.`
|
|
2017
|
+
);
|
|
2018
|
+
}
|
|
2019
|
+
const valueGroup = candidate.valueGroup;
|
|
2020
|
+
if (valueGroup !== void 0) {
|
|
2021
|
+
if (typeof valueGroup !== "number" || !Number.isInteger(valueGroup) || valueGroup < 0) {
|
|
2022
|
+
throw new Error(
|
|
2023
|
+
`${where}.valueGroup must be a non-negative integer capture-group index (received ${describeValue(valueGroup)}).`
|
|
2024
|
+
);
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
if (candidate.pattern === void 0) {
|
|
2028
|
+
throw new Error(
|
|
2029
|
+
`${where} (rule "${candidate.rule}") is missing \`pattern\`. Expected { rule, pattern: RegExp | string }.`
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
return compilePattern(
|
|
2033
|
+
candidate.rule,
|
|
2034
|
+
candidate.pattern,
|
|
2035
|
+
valueGroup
|
|
2036
|
+
);
|
|
2037
|
+
}
|
|
2038
|
+
throw new Error(
|
|
2039
|
+
`${where} must be a RegExp, a string, or { rule, pattern } (received ${describeValue(entry)}).`
|
|
2040
|
+
);
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
function toGlobal(rule) {
|
|
2044
|
+
const flags = new Set(rule.pattern.flags.replace(/[gy]/g, "").split(""));
|
|
2045
|
+
flags.add("g");
|
|
2046
|
+
if (rule.valueGroup !== void 0) flags.add("d");
|
|
2047
|
+
return new RegExp(rule.pattern.source, [...flags].join(""));
|
|
2048
|
+
}
|
|
2049
|
+
var LINE_BREAK = /(\r\n|\n|\r)/;
|
|
2050
|
+
function scanAndRedactSecrets(content, rules, filePath) {
|
|
2051
|
+
const findings = [];
|
|
2052
|
+
let redacted = content;
|
|
2053
|
+
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
2054
|
+
blockPattern.lastIndex = 0;
|
|
2055
|
+
let match;
|
|
2056
|
+
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
2057
|
+
if (match[0].length === 0) {
|
|
2058
|
+
blockPattern.lastIndex += 1;
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
const line = redacted.slice(0, match.index).split("\n").length;
|
|
2062
|
+
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
2063
|
+
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
2064
|
+
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
2065
|
+
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
2066
|
+
}
|
|
2067
|
+
}
|
|
2068
|
+
const compiled = rules.map((rule) => ({
|
|
2069
|
+
rule: rule.rule,
|
|
2070
|
+
pattern: toGlobal(rule),
|
|
2071
|
+
...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
|
|
2072
|
+
}));
|
|
2073
|
+
const segments = redacted.split(LINE_BREAK);
|
|
2074
|
+
let lineNumber = 1;
|
|
2075
|
+
for (let index = 0; index < segments.length; index += 2) {
|
|
2076
|
+
const line = segments[index];
|
|
2077
|
+
if (line.length > 0) {
|
|
2078
|
+
const spans = [];
|
|
2079
|
+
const matchedRules = /* @__PURE__ */ new Set();
|
|
2080
|
+
for (const { rule, pattern, valueGroup } of compiled) {
|
|
2081
|
+
pattern.lastIndex = 0;
|
|
2082
|
+
let match;
|
|
2083
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
2084
|
+
if (match[0].length === 0) {
|
|
2085
|
+
pattern.lastIndex += 1;
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
|
|
2089
|
+
spans.push(
|
|
2090
|
+
group ? { start: group[0], end: group[1], rule } : { start: match.index, end: match.index + match[0].length, rule }
|
|
2091
|
+
);
|
|
2092
|
+
matchedRules.add(rule);
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
if (spans.length > 0) {
|
|
2096
|
+
for (const rule of matchedRules) {
|
|
2097
|
+
findings.push({ rule, line: lineNumber, path: filePath });
|
|
2098
|
+
}
|
|
2099
|
+
segments[index] = applySpans(line, spans);
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
if (index + 1 < segments.length) lineNumber += 1;
|
|
2103
|
+
}
|
|
2104
|
+
return { redacted: segments.join(""), findings };
|
|
2105
|
+
}
|
|
2106
|
+
var TOKEN_CHARS_LEFT = /[A-Za-z0-9_+/-]/;
|
|
2107
|
+
var TOKEN_CHARS_RIGHT = /[A-Za-z0-9_+/=-]/;
|
|
2108
|
+
function expandOverToken(line, start, end) {
|
|
2109
|
+
let left = start;
|
|
2110
|
+
let right = end;
|
|
2111
|
+
while (left > 0 && TOKEN_CHARS_LEFT.test(line[left - 1])) left -= 1;
|
|
2112
|
+
while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
|
|
2113
|
+
return { start: left, end: right };
|
|
2114
|
+
}
|
|
2115
|
+
function applySpans(line, spans) {
|
|
2116
|
+
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
2117
|
+
const merged = [];
|
|
2118
|
+
for (const span of ordered) {
|
|
2119
|
+
const clampedStart = Math.max(0, Math.min(span.start, line.length));
|
|
2120
|
+
const clampedEnd = Math.max(clampedStart, Math.min(span.end, line.length));
|
|
2121
|
+
const { start, end } = expandOverToken(line, clampedStart, clampedEnd);
|
|
2122
|
+
const last = merged[merged.length - 1];
|
|
2123
|
+
if (last && start <= last.end) last.end = Math.max(last.end, end);
|
|
2124
|
+
else merged.push({ start, end });
|
|
2125
|
+
}
|
|
2126
|
+
let out = "";
|
|
2127
|
+
let cursor = 0;
|
|
2128
|
+
for (const span of merged) {
|
|
2129
|
+
out += line.slice(cursor, span.start);
|
|
2130
|
+
out += REDACTION_MARKER;
|
|
2131
|
+
cursor = span.end;
|
|
2132
|
+
}
|
|
2133
|
+
out += line.slice(cursor);
|
|
2134
|
+
return out;
|
|
2135
|
+
}
|
|
2136
|
+
|
|
2137
|
+
// src/scanner/files.ts
|
|
2138
|
+
var PRESET_IGNORES = new Set(
|
|
2139
|
+
PRESETS.flatMap((preset) => preset.exclude ?? [])
|
|
2140
|
+
);
|
|
2141
|
+
function readLinkTarget(linkPath, fallback) {
|
|
2142
|
+
try {
|
|
2143
|
+
return fs9__namespace.readlinkSync(linkPath);
|
|
2144
|
+
} catch {
|
|
2145
|
+
return fallback;
|
|
2146
|
+
}
|
|
2147
|
+
}
|
|
2148
|
+
function isInside(root, candidate) {
|
|
2149
|
+
const relative4 = path10__namespace.relative(root, candidate);
|
|
2150
|
+
return relative4 === "" || !relative4.startsWith(`..${path10__namespace.sep}`) && relative4 !== ".." && !path10__namespace.isAbsolute(relative4);
|
|
2151
|
+
}
|
|
1708
2152
|
function getGitInfo(cwd, fallbackBranch = "main") {
|
|
1709
2153
|
try {
|
|
1710
2154
|
const reported = childProcess__namespace.execSync("git rev-parse --abbrev-ref HEAD", {
|
|
@@ -1749,20 +2193,79 @@ var DEFAULT_IGNORED_FILES = [
|
|
|
1749
2193
|
"bun.lockb",
|
|
1750
2194
|
"composer.lock"
|
|
1751
2195
|
];
|
|
1752
|
-
var SENSITIVE_BASENAME = /^(?:\.env(?:[._-][A-Za-z0-9_-]*)*|\.env(?:backup|bak|old|orig|copy|save)|\.envrc|\.npmrc(?:\.[A-Za-z0-9_-]+)?|\.yarnrc(?:\.yml)?|\.netrc(?:\.[A-Za-z0-9_-]+)?|\.git-credentials|\.htpasswd|\.pgpass|id_[a-z0-9_]+(?:\.[A-Za-z0-9_-]+)?|credentials(?:\.json)?|service[-_]?account(?:\.[a-z0-9_-]+)*\.json|secrets?\.(?:json|ya?ml))$/i;
|
|
2196
|
+
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;
|
|
1753
2197
|
var SENSITIVE_PATH = /(?:^|\/)\.(?:aws|ssh|gnupg)\/|(?:^|\/)\.docker\/config\.json$|(?:^|\/)\.kube\/config$/i;
|
|
1754
2198
|
var SENSITIVE_EXT = /\.(?:pem|key|p12|pfx|jks|keystore|ppk|asc|kdbx|tfstate|tfvars)$/i;
|
|
1755
2199
|
var TEMPLATE_SUFFIX = /\.(?:example|sample|template|dist)(?=$|[.])/i;
|
|
2200
|
+
var BACKUP_SUFFIX = /(?:~|\.(?:bak|backup|old|orig|save|copy|swp|swo|tmp|part))$/i;
|
|
2201
|
+
function normaliseName(name) {
|
|
2202
|
+
let normalised = name.trim().replace(/[. ]+$/, "");
|
|
2203
|
+
let previous = "";
|
|
2204
|
+
while (previous !== normalised) {
|
|
2205
|
+
previous = normalised;
|
|
2206
|
+
normalised = normalised.replace(BACKUP_SUFFIX, "");
|
|
2207
|
+
}
|
|
2208
|
+
return normalised;
|
|
2209
|
+
}
|
|
1756
2210
|
var matchesSensitiveList = (posixPath, base) => SENSITIVE_BASENAME.test(base) || SENSITIVE_PATH.test(posixPath) || SENSITIVE_EXT.test(base);
|
|
1757
2211
|
function isSensitivePath(relativePath) {
|
|
1758
2212
|
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1759
|
-
const
|
|
2213
|
+
const raw = posixPath.split("/").pop() ?? posixPath;
|
|
2214
|
+
const base = normaliseName(raw);
|
|
1760
2215
|
if (TEMPLATE_SUFFIX.test(base)) {
|
|
1761
2216
|
const stripped = base.replace(new RegExp(TEMPLATE_SUFFIX.source, "gi"), "");
|
|
1762
2217
|
if (matchesSensitiveList(posixPath, stripped)) return false;
|
|
1763
2218
|
}
|
|
1764
2219
|
return matchesSensitiveList(posixPath, base);
|
|
1765
2220
|
}
|
|
2221
|
+
var activeSecretRules = null;
|
|
2222
|
+
function setActiveSecretRules(rules) {
|
|
2223
|
+
activeSecretRules = rules === null ? null : [...rules];
|
|
2224
|
+
}
|
|
2225
|
+
function pathScanRules(extraRules) {
|
|
2226
|
+
const fallback = extraRules ?? activeSecretRules ?? [];
|
|
2227
|
+
return [...DEFAULT_SECRET_PATTERNS, ...fallback];
|
|
2228
|
+
}
|
|
2229
|
+
function findSecretInPath(relativePath, extraRules) {
|
|
2230
|
+
const posixPath = relativePath.replace(/\\/g, "/");
|
|
2231
|
+
const spans = [];
|
|
2232
|
+
const rules = [];
|
|
2233
|
+
for (const { rule, pattern } of pathScanRules(extraRules)) {
|
|
2234
|
+
const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
|
|
2235
|
+
let match;
|
|
2236
|
+
while ((match = scan.exec(posixPath)) !== null) {
|
|
2237
|
+
if (match[0].length === 0) {
|
|
2238
|
+
scan.lastIndex += 1;
|
|
2239
|
+
continue;
|
|
2240
|
+
}
|
|
2241
|
+
spans.push({ start: match.index, end: match.index + match[0].length });
|
|
2242
|
+
if (!rules.includes(rule)) rules.push(rule);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
if (spans.length === 0) return null;
|
|
2246
|
+
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
2247
|
+
const merged = [];
|
|
2248
|
+
for (const span of spans) {
|
|
2249
|
+
const last = merged[merged.length - 1];
|
|
2250
|
+
if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
|
|
2251
|
+
else merged.push({ start: span.start, end: span.end });
|
|
2252
|
+
}
|
|
2253
|
+
let out = "";
|
|
2254
|
+
let cursor = 0;
|
|
2255
|
+
for (const span of merged) {
|
|
2256
|
+
out += posixPath.slice(cursor, span.start);
|
|
2257
|
+
out += REDACTION_MARKER;
|
|
2258
|
+
cursor = span.end;
|
|
2259
|
+
}
|
|
2260
|
+
out += posixPath.slice(cursor);
|
|
2261
|
+
return { rules, masked: out };
|
|
2262
|
+
}
|
|
2263
|
+
function maskSkipPaths(records, extraRules) {
|
|
2264
|
+
return records.map((record) => {
|
|
2265
|
+
const hit = findSecretInPath(record.path, extraRules);
|
|
2266
|
+
return hit ? { ...record, path: hit.masked } : record;
|
|
2267
|
+
});
|
|
2268
|
+
}
|
|
1766
2269
|
function globToRegExp(glob) {
|
|
1767
2270
|
const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1768
2271
|
const anchored = normalized.startsWith("/");
|
|
@@ -1883,49 +2386,178 @@ function defaultIgnoreReason(relativePath) {
|
|
|
1883
2386
|
}
|
|
1884
2387
|
return null;
|
|
1885
2388
|
}
|
|
1886
|
-
function
|
|
1887
|
-
const includePatterns = options.includePatterns ?? [];
|
|
1888
|
-
const excludePatterns = options.excludePatterns ?? [];
|
|
1889
|
-
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
1890
|
-
let candidates;
|
|
1891
|
-
let walkSkips = [];
|
|
2389
|
+
function listIgnoredPaths(cwd) {
|
|
1892
2390
|
try {
|
|
1893
2391
|
const stdout = childProcess__namespace.execSync(
|
|
1894
|
-
"git ls-files -z --
|
|
2392
|
+
"git ls-files -z --others --ignored --exclude-standard --directory",
|
|
1895
2393
|
{
|
|
1896
2394
|
cwd,
|
|
1897
2395
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1898
2396
|
encoding: "utf8",
|
|
1899
|
-
maxBuffer:
|
|
2397
|
+
maxBuffer: 32 * 1024 * 1024
|
|
1900
2398
|
}
|
|
1901
2399
|
);
|
|
1902
|
-
|
|
2400
|
+
return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1903
2401
|
} catch {
|
|
1904
|
-
|
|
1905
|
-
candidates = walked.files;
|
|
1906
|
-
walkSkips = walked.skipped;
|
|
2402
|
+
return [];
|
|
1907
2403
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
2404
|
+
}
|
|
2405
|
+
var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
|
|
2406
|
+
function isToolOwnedDirName(name) {
|
|
2407
|
+
return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
|
|
2408
|
+
}
|
|
2409
|
+
function hasSnapshotMarker(target) {
|
|
2410
|
+
try {
|
|
2411
|
+
return fs9__namespace.statSync(target).isDirectory() && fs9__namespace.existsSync(path10__namespace.join(target, ".codebase-snapshot"));
|
|
2412
|
+
} catch {
|
|
2413
|
+
return false;
|
|
2414
|
+
}
|
|
2415
|
+
}
|
|
2416
|
+
function makeToolOwnedDecider(cwd) {
|
|
2417
|
+
const markers = /* @__PURE__ */ new Map();
|
|
2418
|
+
return (relativePath) => {
|
|
2419
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
2420
|
+
for (let depth = 1; depth < parts.length; depth += 1) {
|
|
2421
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
2422
|
+
if (depth === 1 && isToolOwnedDirName(prefix)) return true;
|
|
2423
|
+
let hasMarker = markers.get(prefix);
|
|
2424
|
+
if (hasMarker === void 0) {
|
|
2425
|
+
hasMarker = fs9__namespace.existsSync(path10__namespace.join(cwd, prefix, ".codebase-snapshot"));
|
|
2426
|
+
markers.set(prefix, hasMarker);
|
|
2427
|
+
}
|
|
2428
|
+
if (hasMarker) return true;
|
|
2429
|
+
}
|
|
2430
|
+
return false;
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
function makeLinkProbe() {
|
|
2434
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2435
|
+
return (absolutePath) => {
|
|
2436
|
+
let known = cache.get(absolutePath);
|
|
2437
|
+
if (known === void 0) {
|
|
2438
|
+
try {
|
|
2439
|
+
known = fs9__namespace.lstatSync(absolutePath).isSymbolicLink();
|
|
2440
|
+
} catch {
|
|
2441
|
+
known = false;
|
|
2442
|
+
}
|
|
2443
|
+
cache.set(absolutePath, known);
|
|
2444
|
+
}
|
|
2445
|
+
return known;
|
|
2446
|
+
};
|
|
2447
|
+
}
|
|
2448
|
+
function dropLinkedDirectories(cwd, candidates, skipped) {
|
|
2449
|
+
const isLink = makeLinkProbe();
|
|
2450
|
+
let rootReal = cwd;
|
|
2451
|
+
try {
|
|
2452
|
+
rootReal = fs9__namespace.realpathSync(cwd);
|
|
2453
|
+
} catch {
|
|
2454
|
+
}
|
|
2455
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2456
|
+
const kept = [];
|
|
2457
|
+
for (const candidate of candidates) {
|
|
2458
|
+
const parts = candidate.split("/");
|
|
2459
|
+
let linkedDir = null;
|
|
2460
|
+
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
2461
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
2462
|
+
if (isLink(path10__namespace.join(cwd, prefix))) {
|
|
2463
|
+
linkedDir = prefix;
|
|
2464
|
+
break;
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
if (linkedDir === null) {
|
|
2468
|
+
kept.push(candidate);
|
|
2469
|
+
continue;
|
|
2470
|
+
}
|
|
2471
|
+
if (reported.has(linkedDir)) continue;
|
|
2472
|
+
reported.add(linkedDir);
|
|
2473
|
+
const absolute = path10__namespace.join(cwd, linkedDir);
|
|
2474
|
+
let targetReal = absolute;
|
|
2475
|
+
try {
|
|
2476
|
+
targetReal = fs9__namespace.realpathSync(absolute);
|
|
2477
|
+
} catch {
|
|
2478
|
+
}
|
|
2479
|
+
const target = readLinkTarget(absolute, targetReal);
|
|
2480
|
+
const what = linkedDir === candidate ? "symbolic link" : "directory link";
|
|
2481
|
+
skipped.push(
|
|
2482
|
+
isInside(rootReal, targetReal) ? {
|
|
2483
|
+
path: linkedDir,
|
|
2484
|
+
reason: "symlink",
|
|
2485
|
+
detail: `${what} not followed (target: ${target})`
|
|
2486
|
+
} : {
|
|
2487
|
+
path: linkedDir,
|
|
2488
|
+
reason: "outside-root",
|
|
2489
|
+
detail: `link target resolves outside the scanned root (target: ${target}); not followed`
|
|
2490
|
+
}
|
|
2491
|
+
);
|
|
2492
|
+
}
|
|
2493
|
+
return kept;
|
|
2494
|
+
}
|
|
2495
|
+
function collectFiles(cwd, options = {}) {
|
|
2496
|
+
const includePatterns = options.includePatterns ?? [];
|
|
2497
|
+
const excludePatterns = options.excludePatterns ?? [];
|
|
2498
|
+
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
2499
|
+
const pathRules = options.secretRules;
|
|
2500
|
+
const isToolOwned = makeToolOwnedDecider(cwd);
|
|
2501
|
+
let candidates;
|
|
2502
|
+
const skipped = [];
|
|
2503
|
+
try {
|
|
2504
|
+
const stdout = childProcess__namespace.execSync(
|
|
2505
|
+
"git ls-files -z --cached --others --exclude-standard",
|
|
2506
|
+
{
|
|
2507
|
+
cwd,
|
|
2508
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2509
|
+
encoding: "utf8",
|
|
2510
|
+
maxBuffer: 50 * 1024 * 1024
|
|
2511
|
+
}
|
|
2512
|
+
);
|
|
2513
|
+
candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
2514
|
+
candidates = dropLinkedDirectories(cwd, candidates, skipped);
|
|
2515
|
+
const known = new Set(candidates);
|
|
2516
|
+
for (const ignoredPath of listIgnoredPaths(cwd)) {
|
|
2517
|
+
const trimmed = ignoredPath.replace(/\/+$/, "");
|
|
2518
|
+
if (known.has(trimmed) || known.has(ignoredPath)) continue;
|
|
2519
|
+
if (isToolOwned(trimmed)) continue;
|
|
2520
|
+
if (isSensitivePath(trimmed)) {
|
|
2521
|
+
skipped.push({
|
|
2522
|
+
path: trimmed,
|
|
2523
|
+
reason: "sensitive-file",
|
|
2524
|
+
detail: "credential-bearing file, ignored by .gitignore; enable includeSensitiveFiles to emit it"
|
|
2525
|
+
});
|
|
2526
|
+
} else {
|
|
2527
|
+
skipped.push({
|
|
2528
|
+
path: trimmed,
|
|
2529
|
+
reason: "gitignore",
|
|
2530
|
+
detail: ignoredPath.endsWith("/") ? "ignored by .gitignore (directory, not descended)" : "ignored by .gitignore"
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
} catch {
|
|
2535
|
+
const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), true);
|
|
2536
|
+
candidates = walked.files;
|
|
2537
|
+
skipped.push(...walked.skipped);
|
|
2538
|
+
}
|
|
2539
|
+
const files = candidates.filter((file) => {
|
|
2540
|
+
if (isToolOwned(file)) return false;
|
|
2541
|
+
const ignored = defaultIgnoreReason(file);
|
|
2542
|
+
if (ignored) {
|
|
2543
|
+
skipped.push({ path: file, reason: "default-ignore", detail: ignored });
|
|
2544
|
+
return false;
|
|
2545
|
+
}
|
|
2546
|
+
if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesExclude(file, pattern))) {
|
|
2547
|
+
skipped.push({
|
|
2548
|
+
path: file,
|
|
2549
|
+
reason: "excluded",
|
|
1919
2550
|
detail: `does not match any include pattern (${includePatterns.join(", ")})`
|
|
1920
2551
|
});
|
|
1921
2552
|
return false;
|
|
1922
2553
|
}
|
|
1923
2554
|
for (const pattern of excludePatterns) {
|
|
1924
2555
|
if (matchesExclude(file, pattern)) {
|
|
2556
|
+
const fromPreset = PRESET_IGNORES.has(pattern);
|
|
1925
2557
|
skipped.push({
|
|
1926
2558
|
path: file,
|
|
1927
|
-
reason: "excluded",
|
|
1928
|
-
detail: `matches exclude pattern "${pattern}"`
|
|
2559
|
+
reason: fromPreset ? "default-ignore" : "excluded",
|
|
2560
|
+
detail: fromPreset ? `preset ignore: "${pattern}"` : `matches exclude pattern "${pattern}"`
|
|
1929
2561
|
});
|
|
1930
2562
|
return false;
|
|
1931
2563
|
}
|
|
@@ -1938,56 +2570,119 @@ function collectFiles(cwd, options = {}) {
|
|
|
1938
2570
|
});
|
|
1939
2571
|
return false;
|
|
1940
2572
|
}
|
|
2573
|
+
const secret = findSecretInPath(file, pathRules);
|
|
2574
|
+
if (secret) {
|
|
2575
|
+
skipped.push({
|
|
2576
|
+
path: secret.masked,
|
|
2577
|
+
reason: "sensitive-path",
|
|
2578
|
+
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`
|
|
2579
|
+
});
|
|
2580
|
+
return false;
|
|
2581
|
+
}
|
|
1941
2582
|
return true;
|
|
1942
2583
|
}).sort();
|
|
1943
|
-
return { files, skipped };
|
|
2584
|
+
return { files, skipped: maskSkipPaths(skipped, pathRules) };
|
|
1944
2585
|
}
|
|
1945
|
-
function
|
|
1946
|
-
const results = { files: [], skipped: [] };
|
|
1947
|
-
let realDir;
|
|
2586
|
+
function rootRealFor(root) {
|
|
1948
2587
|
try {
|
|
1949
|
-
|
|
2588
|
+
return fs9__namespace.realpathSync(root);
|
|
1950
2589
|
} catch {
|
|
1951
|
-
|
|
2590
|
+
return root;
|
|
1952
2591
|
}
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
const
|
|
2592
|
+
}
|
|
2593
|
+
function walkDirectory(dir, baseDir, rootReal, visited, isRoot = false) {
|
|
2594
|
+
const results = { files: [], skipped: [] };
|
|
2595
|
+
if (visited.has(dir)) return results;
|
|
2596
|
+
visited.add(dir);
|
|
2597
|
+
const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
|
|
1956
2598
|
let entries;
|
|
1957
2599
|
try {
|
|
1958
2600
|
entries = fs9__namespace.readdirSync(dir, { withFileTypes: true });
|
|
1959
2601
|
} catch {
|
|
1960
|
-
results.skipped.push({
|
|
2602
|
+
results.skipped.push({
|
|
2603
|
+
path: path10__namespace.relative(baseDir, dir).replace(/\\/g, "/") || ".",
|
|
2604
|
+
reason: "read-error",
|
|
2605
|
+
detail: "directory unreadable"
|
|
2606
|
+
});
|
|
1961
2607
|
return results;
|
|
1962
2608
|
}
|
|
1963
2609
|
for (const entry of entries) {
|
|
1964
|
-
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) continue;
|
|
1965
2610
|
const fullPath = path10__namespace.join(dir, entry.name);
|
|
1966
2611
|
const relPath = path10__namespace.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
1967
|
-
if (
|
|
2612
|
+
if (isToolOwnedDirName(entry.name)) continue;
|
|
2613
|
+
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
|
|
2614
|
+
results.skipped.push({
|
|
2615
|
+
path: relPath,
|
|
2616
|
+
reason: "default-ignore",
|
|
2617
|
+
detail: `built-in ignore: directory "${entry.name}" (not descended)`
|
|
2618
|
+
});
|
|
2619
|
+
continue;
|
|
2620
|
+
}
|
|
2621
|
+
if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
|
|
2622
|
+
results.skipped.push({
|
|
2623
|
+
path: relPath,
|
|
2624
|
+
reason: "default-ignore",
|
|
2625
|
+
detail: "built-in ignore: filename"
|
|
2626
|
+
});
|
|
2627
|
+
continue;
|
|
2628
|
+
}
|
|
2629
|
+
if (ignoredByGitignore(relPath)) {
|
|
2630
|
+
if (!hasSnapshotMarker(fullPath)) {
|
|
2631
|
+
results.skipped.push({
|
|
2632
|
+
path: relPath,
|
|
2633
|
+
reason: isSensitivePath(relPath) ? "sensitive-file" : "gitignore",
|
|
2634
|
+
detail: "ignored by .gitignore"
|
|
2635
|
+
});
|
|
2636
|
+
}
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
1968
2639
|
let isDirectory = entry.isDirectory();
|
|
1969
2640
|
let isFile = entry.isFile();
|
|
1970
2641
|
if (entry.isSymbolicLink()) {
|
|
2642
|
+
let targetReal;
|
|
1971
2643
|
try {
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2644
|
+
targetReal = fs9__namespace.realpathSync(fullPath);
|
|
2645
|
+
} catch {
|
|
2646
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
2647
|
+
continue;
|
|
2648
|
+
}
|
|
2649
|
+
let stats;
|
|
2650
|
+
try {
|
|
2651
|
+
stats = fs9__namespace.statSync(fullPath);
|
|
2652
|
+
} catch {
|
|
2653
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
|
|
2654
|
+
continue;
|
|
2655
|
+
}
|
|
2656
|
+
const linkTarget = readLinkTarget(fullPath, targetReal);
|
|
2657
|
+
if (!isInside(rootReal, targetReal)) {
|
|
2658
|
+
results.skipped.push({
|
|
2659
|
+
path: relPath,
|
|
2660
|
+
reason: "outside-root",
|
|
2661
|
+
detail: `link target resolves outside the scanned root (target: ${linkTarget}); not followed`
|
|
2662
|
+
});
|
|
2663
|
+
continue;
|
|
2664
|
+
}
|
|
2665
|
+
if (stats.isDirectory()) {
|
|
1979
2666
|
results.skipped.push({
|
|
1980
2667
|
path: relPath,
|
|
1981
2668
|
reason: "symlink",
|
|
1982
|
-
detail:
|
|
2669
|
+
detail: `directory link not followed (target: ${linkTarget})`
|
|
2670
|
+
});
|
|
2671
|
+
continue;
|
|
2672
|
+
}
|
|
2673
|
+
if (!stats.isFile()) {
|
|
2674
|
+
results.skipped.push({
|
|
2675
|
+
path: relPath,
|
|
2676
|
+
reason: "symlink",
|
|
2677
|
+
detail: `link to neither a file nor a directory (target: ${linkTarget})`
|
|
1983
2678
|
});
|
|
1984
|
-
} catch {
|
|
1985
|
-
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1986
2679
|
continue;
|
|
1987
2680
|
}
|
|
2681
|
+
isDirectory = false;
|
|
2682
|
+
isFile = true;
|
|
1988
2683
|
}
|
|
1989
2684
|
if (isDirectory) {
|
|
1990
|
-
const nested = walkDirectory(fullPath, baseDir, visited);
|
|
2685
|
+
const nested = walkDirectory(fullPath, baseDir, rootReal, visited);
|
|
1991
2686
|
results.files.push(...nested.files);
|
|
1992
2687
|
results.skipped.push(...nested.skipped);
|
|
1993
2688
|
} else if (isFile) {
|
|
@@ -1997,195 +2692,6 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1997
2692
|
return results;
|
|
1998
2693
|
}
|
|
1999
2694
|
|
|
2000
|
-
// src/security/patterns.ts
|
|
2001
|
-
var DEFAULT_SECRET_PATTERNS = [
|
|
2002
|
-
{
|
|
2003
|
-
rule: "private-key-header",
|
|
2004
|
-
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
2005
|
-
},
|
|
2006
|
-
{
|
|
2007
|
-
rule: "jwt-token",
|
|
2008
|
-
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
2009
|
-
},
|
|
2010
|
-
{
|
|
2011
|
-
rule: "aws-access-key-id",
|
|
2012
|
-
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
2013
|
-
},
|
|
2014
|
-
{
|
|
2015
|
-
rule: "aws-secret-access-key",
|
|
2016
|
-
pattern: /(?:aws_secret_access_key|aws_secret_key|secret_access_key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']?/i
|
|
2017
|
-
},
|
|
2018
|
-
{
|
|
2019
|
-
rule: "github-token",
|
|
2020
|
-
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
2021
|
-
},
|
|
2022
|
-
{
|
|
2023
|
-
rule: "slack-token",
|
|
2024
|
-
pattern: /xox[baprs]-[0-9A-Za-z-]{10,250}/
|
|
2025
|
-
},
|
|
2026
|
-
{
|
|
2027
|
-
rule: "slack-webhook-url",
|
|
2028
|
-
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
2029
|
-
},
|
|
2030
|
-
{
|
|
2031
|
-
rule: "stripe-secret-key",
|
|
2032
|
-
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
2033
|
-
},
|
|
2034
|
-
{
|
|
2035
|
-
rule: "google-api-key",
|
|
2036
|
-
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
2037
|
-
},
|
|
2038
|
-
{
|
|
2039
|
-
rule: "google-oauth-client-secret",
|
|
2040
|
-
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
2041
|
-
},
|
|
2042
|
-
{
|
|
2043
|
-
rule: "openai-anthropic-key",
|
|
2044
|
-
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
2045
|
-
},
|
|
2046
|
-
{
|
|
2047
|
-
rule: "npm-token",
|
|
2048
|
-
pattern: /npm_[A-Za-z0-9]{36}/
|
|
2049
|
-
},
|
|
2050
|
-
{
|
|
2051
|
-
rule: "pypi-token",
|
|
2052
|
-
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
2053
|
-
},
|
|
2054
|
-
{
|
|
2055
|
-
rule: "sendgrid-key",
|
|
2056
|
-
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
2057
|
-
},
|
|
2058
|
-
{
|
|
2059
|
-
rule: "twilio-api-key",
|
|
2060
|
-
pattern: /SK[0-9a-fA-F]{32}/
|
|
2061
|
-
},
|
|
2062
|
-
{
|
|
2063
|
-
rule: "digitalocean-token",
|
|
2064
|
-
pattern: /dop_v1_[0-9a-f]{64}/
|
|
2065
|
-
},
|
|
2066
|
-
{
|
|
2067
|
-
rule: "azure-storage-account-key",
|
|
2068
|
-
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
2069
|
-
},
|
|
2070
|
-
{
|
|
2071
|
-
rule: "database-uri-credentials",
|
|
2072
|
-
pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/
|
|
2073
|
-
},
|
|
2074
|
-
{
|
|
2075
|
-
rule: "basic-auth-url",
|
|
2076
|
-
pattern: /(?:https?|ftp|sftp|ssh|git):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/i
|
|
2077
|
-
},
|
|
2078
|
-
{
|
|
2079
|
-
rule: "generic-secret-assignment",
|
|
2080
|
-
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["'][^"'\s]{8,256}["']/i
|
|
2081
|
-
},
|
|
2082
|
-
{
|
|
2083
|
-
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
2084
|
-
// was therefore invisible to the rule above. Anchored to the line start and
|
|
2085
|
-
// limited to secret-looking names to stay at zero false positives.
|
|
2086
|
-
//
|
|
2087
|
-
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
2088
|
-
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
2089
|
-
// every offset of a long identifier, which made this rule quadratic
|
|
2090
|
-
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
2091
|
-
rule: "env-secret-assignment",
|
|
2092
|
-
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
|
|
2093
|
-
}
|
|
2094
|
-
];
|
|
2095
|
-
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
2096
|
-
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
2097
|
-
|
|
2098
|
-
// src/security/redact.ts
|
|
2099
|
-
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
2100
|
-
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
2101
|
-
function compileSecretRules(rules) {
|
|
2102
|
-
return rules.map((rule) => {
|
|
2103
|
-
if (rule.pattern instanceof RegExp) {
|
|
2104
|
-
return {
|
|
2105
|
-
rule: rule.rule,
|
|
2106
|
-
pattern: new RegExp(rule.pattern.source, rule.pattern.flags.replace(/[gy]/g, ""))
|
|
2107
|
-
};
|
|
2108
|
-
}
|
|
2109
|
-
if (typeof rule.pattern === "string") {
|
|
2110
|
-
try {
|
|
2111
|
-
return { rule: rule.rule, pattern: new RegExp(rule.pattern, "i") };
|
|
2112
|
-
} catch (err) {
|
|
2113
|
-
throw new Error(
|
|
2114
|
-
`Invalid regular expression for secret rule "${rule.rule}": ${err.message}`
|
|
2115
|
-
);
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
throw new Error(
|
|
2119
|
-
`Secret rule "${String(rule.rule)}" has a pattern that is neither a RegExp nor a string.`
|
|
2120
|
-
);
|
|
2121
|
-
});
|
|
2122
|
-
}
|
|
2123
|
-
function toGlobal(pattern) {
|
|
2124
|
-
const flags = pattern.flags.replace(/[gy]/g, "");
|
|
2125
|
-
return new RegExp(pattern.source, `${flags}g`);
|
|
2126
|
-
}
|
|
2127
|
-
function scanAndRedactSecrets(content, rules, filePath) {
|
|
2128
|
-
const findings = [];
|
|
2129
|
-
let redacted = content;
|
|
2130
|
-
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
2131
|
-
blockPattern.lastIndex = 0;
|
|
2132
|
-
let match;
|
|
2133
|
-
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
2134
|
-
if (match[0].length === 0) {
|
|
2135
|
-
blockPattern.lastIndex += 1;
|
|
2136
|
-
continue;
|
|
2137
|
-
}
|
|
2138
|
-
const line = redacted.slice(0, match.index).split("\n").length;
|
|
2139
|
-
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
2140
|
-
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
2141
|
-
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
2142
|
-
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
2143
|
-
}
|
|
2144
|
-
}
|
|
2145
|
-
const compiled = rules.map((rule) => ({
|
|
2146
|
-
rule: rule.rule,
|
|
2147
|
-
pattern: toGlobal(rule.pattern)
|
|
2148
|
-
}));
|
|
2149
|
-
const lines = redacted.split("\n");
|
|
2150
|
-
const redactedLines = lines.map((line, index) => {
|
|
2151
|
-
if (line.length === 0) return line;
|
|
2152
|
-
const spans = [];
|
|
2153
|
-
const matchedRules = /* @__PURE__ */ new Set();
|
|
2154
|
-
for (const { rule, pattern } of compiled) {
|
|
2155
|
-
pattern.lastIndex = 0;
|
|
2156
|
-
let match;
|
|
2157
|
-
while ((match = pattern.exec(line)) !== null) {
|
|
2158
|
-
if (match[0].length === 0) {
|
|
2159
|
-
pattern.lastIndex += 1;
|
|
2160
|
-
continue;
|
|
2161
|
-
}
|
|
2162
|
-
spans.push({ start: match.index, end: match.index + match[0].length, rule });
|
|
2163
|
-
matchedRules.add(rule);
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
if (spans.length === 0) return line;
|
|
2167
|
-
for (const rule of matchedRules) {
|
|
2168
|
-
findings.push({ rule, line: index + 1, path: filePath });
|
|
2169
|
-
}
|
|
2170
|
-
return applySpans(line, spans);
|
|
2171
|
-
});
|
|
2172
|
-
return { redacted: redactedLines.join("\n"), findings };
|
|
2173
|
-
}
|
|
2174
|
-
function applySpans(line, spans) {
|
|
2175
|
-
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
2176
|
-
let out = "";
|
|
2177
|
-
let cursor = 0;
|
|
2178
|
-
for (const span of ordered) {
|
|
2179
|
-
if (span.end <= cursor) continue;
|
|
2180
|
-
const start = Math.max(span.start, cursor);
|
|
2181
|
-
out += line.slice(cursor, start);
|
|
2182
|
-
out += REDACTION_MARKER;
|
|
2183
|
-
cursor = Math.min(span.end, line.length);
|
|
2184
|
-
}
|
|
2185
|
-
out += line.slice(cursor);
|
|
2186
|
-
return out;
|
|
2187
|
-
}
|
|
2188
|
-
|
|
2189
2695
|
// src/config.ts
|
|
2190
2696
|
var CONFIG_FILENAMES = [
|
|
2191
2697
|
"snapshot.config.ts",
|
|
@@ -2235,7 +2741,12 @@ function normalizeResolvedConfig(config) {
|
|
|
2235
2741
|
),
|
|
2236
2742
|
include: resolveInclude(config.include),
|
|
2237
2743
|
preset: typeof config.preset === "string" ? config.preset : "generic",
|
|
2238
|
-
presetExclude:
|
|
2744
|
+
presetExclude: [
|
|
2745
|
+
.../* @__PURE__ */ new Set([
|
|
2746
|
+
...Array.isArray(config.presetExclude) ? config.presetExclude : [],
|
|
2747
|
+
...outputExcludePatterns(config.cwd, config.outDir, config.tmpDir)
|
|
2748
|
+
])
|
|
2749
|
+
],
|
|
2239
2750
|
layersMode: config.layersMode === "replace" ? "replace" : "merge",
|
|
2240
2751
|
budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
|
|
2241
2752
|
hooks: resolveHooks(config.hooks),
|
|
@@ -2272,7 +2783,68 @@ function supportsTypeScriptImports() {
|
|
|
2272
2783
|
return major > 22 || major === 22 && minor >= 6;
|
|
2273
2784
|
}
|
|
2274
2785
|
function stripJsonComments2(raw) {
|
|
2275
|
-
|
|
2786
|
+
const text2 = raw.replace(/^\uFEFF/, "");
|
|
2787
|
+
let out = "";
|
|
2788
|
+
let inString = false;
|
|
2789
|
+
let escaped = false;
|
|
2790
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2791
|
+
const char = text2[index];
|
|
2792
|
+
if (inString) {
|
|
2793
|
+
out += char;
|
|
2794
|
+
if (escaped) escaped = false;
|
|
2795
|
+
else if (char === "\\") escaped = true;
|
|
2796
|
+
else if (char === '"') inString = false;
|
|
2797
|
+
continue;
|
|
2798
|
+
}
|
|
2799
|
+
if (char === '"') {
|
|
2800
|
+
inString = true;
|
|
2801
|
+
out += char;
|
|
2802
|
+
continue;
|
|
2803
|
+
}
|
|
2804
|
+
if (char === "/" && text2[index + 1] === "/") {
|
|
2805
|
+
while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
|
|
2806
|
+
index -= 1;
|
|
2807
|
+
continue;
|
|
2808
|
+
}
|
|
2809
|
+
if (char === "/" && text2[index + 1] === "*") {
|
|
2810
|
+
index += 2;
|
|
2811
|
+
while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
|
|
2812
|
+
if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
|
|
2813
|
+
index += 1;
|
|
2814
|
+
}
|
|
2815
|
+
index += 1;
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2818
|
+
out += char;
|
|
2819
|
+
}
|
|
2820
|
+
return stripTrailingCommas(out);
|
|
2821
|
+
}
|
|
2822
|
+
function stripTrailingCommas(text2) {
|
|
2823
|
+
let out = "";
|
|
2824
|
+
let inString = false;
|
|
2825
|
+
let escaped = false;
|
|
2826
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2827
|
+
const char = text2[index];
|
|
2828
|
+
if (inString) {
|
|
2829
|
+
out += char;
|
|
2830
|
+
if (escaped) escaped = false;
|
|
2831
|
+
else if (char === "\\") escaped = true;
|
|
2832
|
+
else if (char === '"') inString = false;
|
|
2833
|
+
continue;
|
|
2834
|
+
}
|
|
2835
|
+
if (char === '"') {
|
|
2836
|
+
inString = true;
|
|
2837
|
+
out += char;
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
if (char === ",") {
|
|
2841
|
+
let next = index + 1;
|
|
2842
|
+
while (next < text2.length && /\s/.test(text2[next])) next += 1;
|
|
2843
|
+
if (text2[next] === "}" || text2[next] === "]") continue;
|
|
2844
|
+
}
|
|
2845
|
+
out += char;
|
|
2846
|
+
}
|
|
2847
|
+
return out;
|
|
2276
2848
|
}
|
|
2277
2849
|
function parseJsonish(raw, file) {
|
|
2278
2850
|
try {
|
|
@@ -2281,6 +2853,47 @@ function parseJsonish(raw, file) {
|
|
|
2281
2853
|
throw new Error(`Could not parse ${file}: ${err.message}`);
|
|
2282
2854
|
}
|
|
2283
2855
|
}
|
|
2856
|
+
function describeValue2(value) {
|
|
2857
|
+
if (value === null) return "null";
|
|
2858
|
+
if (Array.isArray(value)) return "an array";
|
|
2859
|
+
if (value instanceof RegExp) return value.toString();
|
|
2860
|
+
if (typeof value === "function") return "a function";
|
|
2861
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
2862
|
+
if (typeof value === "object") {
|
|
2863
|
+
const keys = Object.keys(value);
|
|
2864
|
+
return keys.length > 0 ? `an object with keys ${keys.join(", ")}` : "an empty object";
|
|
2865
|
+
}
|
|
2866
|
+
return `${typeof value} (${String(value)})`;
|
|
2867
|
+
}
|
|
2868
|
+
var isThenable = (value) => typeof value.then === "function";
|
|
2869
|
+
function readConfigExport(exported, file) {
|
|
2870
|
+
if (exported === void 0 || exported === null) {
|
|
2871
|
+
throw new Error(
|
|
2872
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2873
|
+
);
|
|
2874
|
+
}
|
|
2875
|
+
if (Array.isArray(exported)) {
|
|
2876
|
+
throw new Error(
|
|
2877
|
+
`${file} exports an array; expected a single configuration object such as \`export default { outDir: "snapshot" }\`.`
|
|
2878
|
+
);
|
|
2879
|
+
}
|
|
2880
|
+
if (typeof exported === "function") {
|
|
2881
|
+
throw new Error(
|
|
2882
|
+
`${file} exports a function; expected a configuration object. Call the function yourself and export its result.`
|
|
2883
|
+
);
|
|
2884
|
+
}
|
|
2885
|
+
if (typeof exported !== "object") {
|
|
2886
|
+
throw new Error(
|
|
2887
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2888
|
+
);
|
|
2889
|
+
}
|
|
2890
|
+
if (isThenable(exported)) {
|
|
2891
|
+
throw new Error(
|
|
2892
|
+
`${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;\`).`
|
|
2893
|
+
);
|
|
2894
|
+
}
|
|
2895
|
+
return exported;
|
|
2896
|
+
}
|
|
2284
2897
|
async function loadConfigFileDetailed(cwd) {
|
|
2285
2898
|
const warnings = [];
|
|
2286
2899
|
const errors = [];
|
|
@@ -2310,13 +2923,7 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
2310
2923
|
}
|
|
2311
2924
|
try {
|
|
2312
2925
|
const imported = await import(url.pathToFileURL(fullPath).href);
|
|
2313
|
-
const config = imported.default ?? imported;
|
|
2314
|
-
if (!config || typeof config !== "object") {
|
|
2315
|
-
const message = `${fullPath} does not export a configuration object.`;
|
|
2316
|
-
warnings.push(message);
|
|
2317
|
-
errors.push(message);
|
|
2318
|
-
continue;
|
|
2319
|
-
}
|
|
2926
|
+
const config = readConfigExport(imported.default ?? imported, fullPath);
|
|
2320
2927
|
return { config, file: fullPath, warnings, errors };
|
|
2321
2928
|
} catch (err) {
|
|
2322
2929
|
const message = `Could not load ${fullPath}: ${err.message}`;
|
|
@@ -2370,7 +2977,7 @@ var DEFAULT_ARTIFACTS = {
|
|
|
2370
2977
|
function isArtifactName(value) {
|
|
2371
2978
|
return ARTIFACT_NAMES.includes(value);
|
|
2372
2979
|
}
|
|
2373
|
-
function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
2980
|
+
function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
|
|
2374
2981
|
const artifacts = {
|
|
2375
2982
|
...DEFAULT_ARTIFACTS,
|
|
2376
2983
|
// `includeMonolith` predates this selection and stays the switch for it.
|
|
@@ -2392,6 +2999,9 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
2392
2999
|
artifacts[name] = value;
|
|
2393
3000
|
}
|
|
2394
3001
|
}
|
|
3002
|
+
if (monolithOverride !== void 0) {
|
|
3003
|
+
artifacts.monolith = monolithOverride;
|
|
3004
|
+
}
|
|
2395
3005
|
for (const name of skipped ?? []) {
|
|
2396
3006
|
if (!isArtifactName(name)) {
|
|
2397
3007
|
throw new Error(
|
|
@@ -2402,9 +3012,106 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
2402
3012
|
}
|
|
2403
3013
|
return artifacts;
|
|
2404
3014
|
}
|
|
2405
|
-
function
|
|
2406
|
-
if (
|
|
2407
|
-
|
|
3015
|
+
function strictPositiveInteger(value, fallback, label) {
|
|
3016
|
+
if (value === void 0) return fallback;
|
|
3017
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
3018
|
+
throw new Error(`${label} must be a positive integer (received ${describeValue2(value)}).`);
|
|
3019
|
+
}
|
|
3020
|
+
return value;
|
|
3021
|
+
}
|
|
3022
|
+
function strictBoolean(value, fallback, label) {
|
|
3023
|
+
if (value === void 0) return fallback;
|
|
3024
|
+
if (typeof value !== "boolean") {
|
|
3025
|
+
throw new Error(`${label} must be true or false (received ${describeValue2(value)}).`);
|
|
3026
|
+
}
|
|
3027
|
+
return value;
|
|
3028
|
+
}
|
|
3029
|
+
function strictString(value, fallback, label) {
|
|
3030
|
+
if (value === void 0) return fallback;
|
|
3031
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
3032
|
+
throw new Error(`${label} must be a non-empty string (received ${describeValue2(value)}).`);
|
|
3033
|
+
}
|
|
3034
|
+
return value;
|
|
3035
|
+
}
|
|
3036
|
+
function resolveExcludePatterns(value) {
|
|
3037
|
+
if (value === void 0) return [];
|
|
3038
|
+
if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string")) {
|
|
3039
|
+
throw new Error(
|
|
3040
|
+
`\`excludePatterns\` must be an array of glob strings such as ["**/*.test.ts"] (received ${describeValue2(value)}).`
|
|
3041
|
+
);
|
|
3042
|
+
}
|
|
3043
|
+
return value.map((pattern) => pattern.trim()).filter(Boolean);
|
|
3044
|
+
}
|
|
3045
|
+
var AUTO_PRESET_ALIASES = /* @__PURE__ */ new Set(["auto", "detected"]);
|
|
3046
|
+
function resolvePresetName(value) {
|
|
3047
|
+
if (value === void 0) return void 0;
|
|
3048
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
3049
|
+
throw new Error(
|
|
3050
|
+
`\`preset\` must be a preset name (received ${describeValue2(value)}). Use "detected" (or "auto") to detect one, or one of: ${listPresetNames().join(", ")}.`
|
|
3051
|
+
);
|
|
3052
|
+
}
|
|
3053
|
+
const name = value.trim();
|
|
3054
|
+
return AUTO_PRESET_ALIASES.has(name.toLowerCase()) ? void 0 : name;
|
|
3055
|
+
}
|
|
3056
|
+
function resolveRuleList(value, kind) {
|
|
3057
|
+
const collection = kind === "layer" ? "layers" : "kinds";
|
|
3058
|
+
const example = kind === "layer" ? `{ name: "api", match: "src/api/**" }` : `{ kind: "route", match: "src/routes/**" }`;
|
|
3059
|
+
if (value === void 0) return [];
|
|
3060
|
+
if (!Array.isArray(value)) {
|
|
3061
|
+
throw new Error(
|
|
3062
|
+
`\`${collection}\` must be an array of ${kind} rules (received ${describeValue2(value)}). A ${kind} rule is ${example}.`
|
|
3063
|
+
);
|
|
3064
|
+
}
|
|
3065
|
+
return value.map((entry, index) => {
|
|
3066
|
+
const where = `${collection}[${index}]`;
|
|
3067
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
3068
|
+
throw new Error(`${where} must be an object (received ${describeValue2(entry)}). A ${kind} rule is ${example}.`);
|
|
3069
|
+
}
|
|
3070
|
+
const record = entry;
|
|
3071
|
+
const keys = Object.keys(record);
|
|
3072
|
+
const canonical = kind === "layer" ? "name" : "kind";
|
|
3073
|
+
const alias = kind === "layer" ? "layer" : "name";
|
|
3074
|
+
const rawName = typeof record[canonical] === "string" ? record[canonical] : typeof record[alias] === "string" ? record[alias] : void 0;
|
|
3075
|
+
if (rawName === void 0 || rawName.trim() === "") {
|
|
3076
|
+
throw new Error(
|
|
3077
|
+
`${where} is missing \`${canonical}\` (received keys: ${keys.join(", ") || "none"}). A ${kind} rule is ${example}; \`${alias}\` is accepted as an alias of \`${canonical}\`.`
|
|
3078
|
+
);
|
|
3079
|
+
}
|
|
3080
|
+
const match = record.match;
|
|
3081
|
+
if (match === void 0) {
|
|
3082
|
+
throw new Error(
|
|
3083
|
+
`${where}.match is required (received keys: ${keys.join(", ") || "none"}). Use a glob string ("src/api/**"), a RegExp (/\\\\.tsx$/) or a function.`
|
|
3084
|
+
);
|
|
3085
|
+
}
|
|
3086
|
+
if (!(typeof match === "string" || match instanceof RegExp || typeof match === "function")) {
|
|
3087
|
+
throw new Error(`${where}.match must be a string, a RegExp or a function (received ${describeValue2(match)}).`);
|
|
3088
|
+
}
|
|
3089
|
+
if (typeof match === "string" && match.trim() === "") {
|
|
3090
|
+
throw new Error(`${where}.match must not be an empty string.`);
|
|
3091
|
+
}
|
|
3092
|
+
return {
|
|
3093
|
+
name: rawName.trim(),
|
|
3094
|
+
match
|
|
3095
|
+
};
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
function resolveLayerRules(value) {
|
|
3099
|
+
return resolveRuleList(value, "layer").map((rule) => ({ name: rule.name, match: rule.match }));
|
|
3100
|
+
}
|
|
3101
|
+
function resolveKindRules(value) {
|
|
3102
|
+
return resolveRuleList(value, "kind").map((rule) => ({ kind: rule.name, match: rule.match }));
|
|
3103
|
+
}
|
|
3104
|
+
function outputExcludePatterns(cwd, outDir, tmpDir) {
|
|
3105
|
+
const patterns = [];
|
|
3106
|
+
for (const dir of [outDir, tmpDir]) {
|
|
3107
|
+
const absolute = path10__namespace.resolve(cwd, dir);
|
|
3108
|
+
const relative4 = path10__namespace.relative(cwd, absolute);
|
|
3109
|
+
if (!relative4 || relative4 === ".." || relative4.startsWith(`..${path10__namespace.sep}`) || path10__namespace.isAbsolute(relative4)) {
|
|
3110
|
+
continue;
|
|
3111
|
+
}
|
|
3112
|
+
patterns.push(`${relative4.replace(/\\/g, "/")}/**`);
|
|
3113
|
+
}
|
|
3114
|
+
return patterns;
|
|
2408
3115
|
}
|
|
2409
3116
|
var BYTE_UNITS = {
|
|
2410
3117
|
b: 1,
|
|
@@ -2473,11 +3180,13 @@ function resolveLayersMode(mode) {
|
|
|
2473
3180
|
}
|
|
2474
3181
|
return mode;
|
|
2475
3182
|
}
|
|
2476
|
-
function resolveBudgets(budgets, fallback) {
|
|
3183
|
+
function resolveBudgets(budgets, fallback, overrideDefault) {
|
|
2477
3184
|
const byLayer = {};
|
|
2478
3185
|
if (budgets === void 0) return { default: fallback, byLayer };
|
|
2479
3186
|
if (typeof budgets !== "object" || budgets === null || Array.isArray(budgets)) {
|
|
2480
|
-
throw new Error(
|
|
3187
|
+
throw new Error(
|
|
3188
|
+
`\`budgets\` must be an object such as { default: "120k", byLayer: { api: "60k" } } (received ${describeValue2(budgets)}).`
|
|
3189
|
+
);
|
|
2481
3190
|
}
|
|
2482
3191
|
if (budgets.byLayer !== void 0) {
|
|
2483
3192
|
if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
|
|
@@ -2492,10 +3201,12 @@ function resolveBudgets(budgets, fallback) {
|
|
|
2492
3201
|
);
|
|
2493
3202
|
}
|
|
2494
3203
|
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
3204
|
+
const resolvedDefault = boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default");
|
|
3205
|
+
if (!overrideDefault) return { default: resolvedDefault, byLayer };
|
|
3206
|
+
for (const [layer2, value] of Object.entries(byLayer)) {
|
|
3207
|
+
byLayer[layer2] = Math.min(value, fallback);
|
|
3208
|
+
}
|
|
3209
|
+
return { default: fallback, byLayer };
|
|
2499
3210
|
}
|
|
2500
3211
|
function resolveHooks(hooks) {
|
|
2501
3212
|
if (hooks === void 0) return {};
|
|
@@ -2508,6 +3219,11 @@ function resolveHooks(hooks) {
|
|
|
2508
3219
|
return { ...hooks };
|
|
2509
3220
|
}
|
|
2510
3221
|
function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
3222
|
+
if (userConfig?.cwd !== void 0 && (typeof userConfig.cwd !== "string" || userConfig.cwd.trim() === "")) {
|
|
3223
|
+
throw new Error(
|
|
3224
|
+
`\`cwd\` must be a non-empty path string (received ${describeValue2(userConfig.cwd)}).`
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
2511
3227
|
const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
|
|
2512
3228
|
const resolved = path10__namespace.resolve(candidate);
|
|
2513
3229
|
if (!fs9__namespace.existsSync(resolved)) {
|
|
@@ -2520,55 +3236,107 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
|
2520
3236
|
}
|
|
2521
3237
|
function resolveConfig(userConfig, cliOptions, baseDir) {
|
|
2522
3238
|
const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
|
|
2523
|
-
const forcedPreset = userConfig?.preset;
|
|
3239
|
+
const forcedPreset = resolvePresetName(userConfig?.preset);
|
|
2524
3240
|
if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
|
|
2525
3241
|
throw new Error(unknownPresetMessage(forcedPreset));
|
|
2526
3242
|
}
|
|
2527
3243
|
const preset = detectPreset(cwd, forcedPreset);
|
|
2528
3244
|
const layersMode = resolveLayersMode(userConfig?.layersMode);
|
|
2529
|
-
const
|
|
2530
|
-
const
|
|
2531
|
-
const
|
|
2532
|
-
const
|
|
2533
|
-
const
|
|
2534
|
-
|
|
3245
|
+
const userLayers = resolveLayerRules(userConfig?.layers);
|
|
3246
|
+
const userKinds = resolveKindRules(userConfig?.kinds);
|
|
3247
|
+
const layers = userLayers.length > 0 ? layersMode === "replace" ? userLayers : [...userLayers, ...preset.layers] : preset.layers;
|
|
3248
|
+
const kinds = userKinds.length > 0 ? layersMode === "replace" ? userKinds : [...userKinds, ...preset.kinds] : preset.kinds;
|
|
3249
|
+
const secrets = userConfig?.secrets;
|
|
3250
|
+
if (secrets !== void 0 && (typeof secrets !== "object" || Array.isArray(secrets))) {
|
|
3251
|
+
throw new Error(
|
|
3252
|
+
`\`secrets\` must be an object of { customPatterns, disableDefaultPatterns } (received ${describeValue2(secrets)}).`
|
|
3253
|
+
);
|
|
3254
|
+
}
|
|
3255
|
+
const customRules = secrets?.customPatterns ?? [];
|
|
3256
|
+
if (!Array.isArray(customRules)) {
|
|
3257
|
+
throw new Error(
|
|
3258
|
+
`\`secrets.customPatterns\` must be an array of secret rules (received ${describeValue2(customRules)}). Each entry is a RegExp, a string, or { rule, pattern }.`
|
|
3259
|
+
);
|
|
3260
|
+
}
|
|
3261
|
+
const disableDefaultPatterns = strictBoolean(
|
|
3262
|
+
secrets?.disableDefaultPatterns,
|
|
3263
|
+
false,
|
|
3264
|
+
"`secrets.disableDefaultPatterns`"
|
|
3265
|
+
);
|
|
3266
|
+
const aliasedRules = userConfig?.secretRules;
|
|
3267
|
+
if (aliasedRules !== void 0 && !Array.isArray(aliasedRules)) {
|
|
3268
|
+
throw new Error(
|
|
3269
|
+
`\`secretRules\` must be an array of { rule, pattern } entries (received ${describeValue2(aliasedRules)}).`
|
|
3270
|
+
);
|
|
3271
|
+
}
|
|
3272
|
+
const compiledCustomRules = compileSecretRules([
|
|
3273
|
+
...customRules,
|
|
3274
|
+
...aliasedRules ?? []
|
|
3275
|
+
]);
|
|
3276
|
+
const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
|
|
3277
|
+
setActiveSecretRules(secretRules);
|
|
3278
|
+
const cliMonolith = cliOptions?.includeMonolith;
|
|
3279
|
+
const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
|
|
2535
3280
|
const artifacts = resolveArtifacts(
|
|
2536
3281
|
userConfig?.artifacts,
|
|
2537
3282
|
cliOptions?.skipArtifacts,
|
|
2538
|
-
includeMonolith
|
|
3283
|
+
includeMonolith,
|
|
3284
|
+
cliMonolith
|
|
2539
3285
|
);
|
|
3286
|
+
const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
|
|
2540
3287
|
const maxPartBytes = boundedBytes(
|
|
2541
|
-
|
|
3288
|
+
explicitMaxPartBytes,
|
|
2542
3289
|
300 * 1024,
|
|
2543
3290
|
MIN_MAX_PART_BYTES,
|
|
2544
3291
|
"maxPartBytes"
|
|
2545
3292
|
);
|
|
2546
|
-
const budgets = resolveBudgets(
|
|
3293
|
+
const budgets = resolveBudgets(
|
|
3294
|
+
userConfig?.budgets,
|
|
3295
|
+
maxPartBytes,
|
|
3296
|
+
explicitMaxPartBytes !== void 0
|
|
3297
|
+
);
|
|
3298
|
+
const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
|
|
3299
|
+
const outDir = cliOptions?.outDir || configuredOutDir;
|
|
3300
|
+
const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
|
|
3301
|
+
const branch = strictString(userConfig?.branch, "main", "`branch`");
|
|
3302
|
+
const configuredSensitiveFiles = strictBoolean(
|
|
3303
|
+
userConfig?.includeSensitiveFiles,
|
|
3304
|
+
false,
|
|
3305
|
+
"`includeSensitiveFiles`"
|
|
3306
|
+
);
|
|
3307
|
+
const includeSensitiveFiles = strictBoolean(
|
|
3308
|
+
cliOptions?.includeSensitiveFiles,
|
|
3309
|
+
configuredSensitiveFiles,
|
|
3310
|
+
"`includeSensitiveFiles`"
|
|
3311
|
+
);
|
|
3312
|
+
const presetExclude = [
|
|
3313
|
+
.../* @__PURE__ */ new Set([...preset.exclude, ...outputExcludePatterns(cwd, outDir, tmpDir)])
|
|
3314
|
+
];
|
|
2547
3315
|
return markResolvedConfig({
|
|
2548
3316
|
cwd,
|
|
2549
|
-
branch
|
|
2550
|
-
outDir
|
|
2551
|
-
tmpDir
|
|
3317
|
+
branch,
|
|
3318
|
+
outDir,
|
|
3319
|
+
tmpDir,
|
|
2552
3320
|
maxPartBytes,
|
|
2553
3321
|
largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
|
|
2554
|
-
previewHeadLines:
|
|
2555
|
-
previewTailLines:
|
|
3322
|
+
previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
|
|
3323
|
+
previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
|
|
2556
3324
|
includeMonolith: artifacts.monolith,
|
|
2557
3325
|
maxMonolithBytes: optionalBytes(
|
|
2558
3326
|
cliOptions?.maxMonolithBytes !== void 0 ? cliOptions.maxMonolithBytes : userConfig?.maxMonolithBytes,
|
|
2559
3327
|
"maxMonolithBytes"
|
|
2560
3328
|
),
|
|
2561
|
-
includeSensitiveFiles
|
|
3329
|
+
includeSensitiveFiles,
|
|
2562
3330
|
include: resolveInclude(userConfig?.include),
|
|
2563
3331
|
preset: preset.presetName,
|
|
2564
|
-
presetExclude
|
|
3332
|
+
presetExclude,
|
|
2565
3333
|
artifacts,
|
|
2566
3334
|
layersMode,
|
|
2567
3335
|
budgets,
|
|
2568
3336
|
hooks: resolveHooks(userConfig?.hooks),
|
|
2569
3337
|
layers,
|
|
2570
3338
|
kinds,
|
|
2571
|
-
excludePatterns: userConfig?.excludePatterns
|
|
3339
|
+
excludePatterns: resolveExcludePatterns(userConfig?.excludePatterns),
|
|
2572
3340
|
secretRules
|
|
2573
3341
|
});
|
|
2574
3342
|
}
|
|
@@ -2614,7 +3382,8 @@ function defineConfig(config) {
|
|
|
2614
3382
|
}
|
|
2615
3383
|
|
|
2616
3384
|
// src/generator/audit.ts
|
|
2617
|
-
function generateAuditReport(manifest, secretFindings) {
|
|
3385
|
+
function generateAuditReport(manifest, secretFindings, secretRules) {
|
|
3386
|
+
const mask = (value) => findSecretInPath(value, secretRules)?.masked ?? value;
|
|
2618
3387
|
const lines = [
|
|
2619
3388
|
"# Codebase Health & Security Audit",
|
|
2620
3389
|
"",
|
|
@@ -2623,7 +3392,11 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
2623
3392
|
`**Total Files:** ${manifest.counts.totalFiles} `,
|
|
2624
3393
|
`**Total Lines:** ${manifest.counts.totalLines.toLocaleString()} `,
|
|
2625
3394
|
`**Total Size:** ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB `,
|
|
2626
|
-
|
|
3395
|
+
// This counts the bytes that were *emitted* after redaction, not the bytes
|
|
3396
|
+
// that were masked: calling it "redacted bytes" made a repository with zero
|
|
3397
|
+
// secrets report a non-zero redacted-byte figure directly above "no secrets
|
|
3398
|
+
// detected". The number of masked spans is reported in section 1 instead.
|
|
3399
|
+
`**Emitted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
|
|
2627
3400
|
"",
|
|
2628
3401
|
"## 1. Security Scan & Redaction",
|
|
2629
3402
|
"",
|
|
@@ -2645,7 +3418,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
2645
3418
|
"| :--- | :--- | :--- |"
|
|
2646
3419
|
);
|
|
2647
3420
|
for (const finding of secretFindings) {
|
|
2648
|
-
lines.push(`| \`${finding.path ?? "unknown"}\` | ${finding.line} | \`${finding.rule}\` |`);
|
|
3421
|
+
lines.push(`| \`${mask(finding.path ?? "unknown")}\` | ${finding.line} | \`${finding.rule}\` |`);
|
|
2649
3422
|
}
|
|
2650
3423
|
lines.push("", "### Matches by rule", "", "| Rule | Matches |", "| :--- | :--- |");
|
|
2651
3424
|
for (const [rule, count] of Array.from(byRule.entries()).sort((a, b) => b[1] - a[1])) {
|
|
@@ -2667,7 +3440,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
2667
3440
|
for (const file of largeFiles) {
|
|
2668
3441
|
const sizeKb = (file.sizeBytes / 1024).toFixed(1);
|
|
2669
3442
|
const status = file.isPreviewTruncated ? "Truncated Head/Tail" : "Full";
|
|
2670
|
-
lines.push(`| \`${file.relativePath}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
|
|
3443
|
+
lines.push(`| \`${mask(file.relativePath)}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
|
|
2671
3444
|
}
|
|
2672
3445
|
lines.push("");
|
|
2673
3446
|
}
|
|
@@ -2709,7 +3482,7 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
2709
3482
|
"| :--- | :--- | :--- |"
|
|
2710
3483
|
);
|
|
2711
3484
|
for (const skip of manifest.skipped) {
|
|
2712
|
-
lines.push(`| \`${skip.path}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
|
|
3485
|
+
lines.push(`| \`${mask(skip.path)}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
|
|
2713
3486
|
}
|
|
2714
3487
|
lines.push("");
|
|
2715
3488
|
}
|
|
@@ -2826,6 +3599,8 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
2826
3599
|
layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
|
|
2827
3600
|
kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
|
|
2828
3601
|
}
|
|
3602
|
+
const pathLines = included.map((file) => displayPath(file.relativePath));
|
|
3603
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
2829
3604
|
const header = [
|
|
2830
3605
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
2831
3606
|
"",
|
|
@@ -2838,9 +3613,9 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
2838
3613
|
"",
|
|
2839
3614
|
"## Files",
|
|
2840
3615
|
"",
|
|
2841
|
-
|
|
2842
|
-
...
|
|
2843
|
-
|
|
3616
|
+
`${listFence}text`,
|
|
3617
|
+
...pathLines,
|
|
3618
|
+
listFence,
|
|
2844
3619
|
"",
|
|
2845
3620
|
"## Contents",
|
|
2846
3621
|
""
|
|
@@ -2868,7 +3643,8 @@ function capValue(value, max) {
|
|
|
2868
3643
|
return `${value.slice(0, half)}\u2026${value.slice(value.length - half)}`;
|
|
2869
3644
|
}
|
|
2870
3645
|
var capMetadata = (value) => capValue(value, METADATA_VALUE_MAX);
|
|
2871
|
-
var
|
|
3646
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
|
|
3647
|
+
var displayPath = (value) => capValue(value.replace(CONTROL_CHARS, " "), DISPLAY_PATH_MAX);
|
|
2872
3648
|
function topEntries(counts, limit) {
|
|
2873
3649
|
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(", ");
|
|
2874
3650
|
}
|
|
@@ -2964,6 +3740,8 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
2964
3740
|
layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
|
|
2965
3741
|
kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
|
|
2966
3742
|
}
|
|
3743
|
+
const pathLines = group.map((s) => displayPath(s.file.relativePath));
|
|
3744
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
2967
3745
|
const header = [
|
|
2968
3746
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
2969
3747
|
"",
|
|
@@ -2976,9 +3754,9 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
2976
3754
|
"",
|
|
2977
3755
|
"## Files",
|
|
2978
3756
|
"",
|
|
2979
|
-
|
|
2980
|
-
...
|
|
2981
|
-
|
|
3757
|
+
`${listFence}text`,
|
|
3758
|
+
...pathLines,
|
|
3759
|
+
listFence,
|
|
2982
3760
|
"",
|
|
2983
3761
|
"## Contents",
|
|
2984
3762
|
""
|
|
@@ -3037,9 +3815,12 @@ function renderFileSection(file) {
|
|
|
3037
3815
|
].join("\n");
|
|
3038
3816
|
}
|
|
3039
3817
|
function fenceForContent(content) {
|
|
3818
|
+
return fenceForText(content, 4);
|
|
3819
|
+
}
|
|
3820
|
+
function fenceForText(content, minimum) {
|
|
3040
3821
|
const matches = content.match(/`+/g);
|
|
3041
3822
|
const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
|
|
3042
|
-
return "`".repeat(Math.max(
|
|
3823
|
+
return "`".repeat(Math.max(minimum, maxRun + 1));
|
|
3043
3824
|
}
|
|
3044
3825
|
function renderSummary(summary) {
|
|
3045
3826
|
if (!summary) return [];
|
|
@@ -3188,6 +3969,7 @@ function generateReadme(manifest) {
|
|
|
3188
3969
|
|
|
3189
3970
|
// src/generator/tree.ts
|
|
3190
3971
|
var MAX_TREE_LINES = 2e4;
|
|
3972
|
+
var CONTROL_CHARS2 = /[\u0000-\u001f\u007f]/g;
|
|
3191
3973
|
function generateProjectTree(filePaths) {
|
|
3192
3974
|
const root = { children: /* @__PURE__ */ new Map() };
|
|
3193
3975
|
for (const filePath of filePaths) {
|
|
@@ -3202,7 +3984,7 @@ function generateProjectTree(filePaths) {
|
|
|
3202
3984
|
current = child;
|
|
3203
3985
|
}
|
|
3204
3986
|
}
|
|
3205
|
-
const lines = ["
|
|
3987
|
+
const lines = ["."];
|
|
3206
3988
|
let truncatedAt = -1;
|
|
3207
3989
|
function walk(node, prefix) {
|
|
3208
3990
|
if (truncatedAt !== -1) return;
|
|
@@ -3217,7 +3999,8 @@ function generateProjectTree(filePaths) {
|
|
|
3217
3999
|
const isLast = index === keys.length - 1;
|
|
3218
4000
|
const child = node.children.get(key);
|
|
3219
4001
|
const isDirectory = child.children.size > 0;
|
|
3220
|
-
|
|
4002
|
+
const label = key.replace(CONTROL_CHARS2, " ");
|
|
4003
|
+
lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
|
|
3221
4004
|
if (lines.length >= MAX_TREE_LINES) {
|
|
3222
4005
|
truncatedAt = lines.length;
|
|
3223
4006
|
return;
|
|
@@ -3229,8 +4012,8 @@ function generateProjectTree(filePaths) {
|
|
|
3229
4012
|
if (truncatedAt !== -1) {
|
|
3230
4013
|
lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
|
|
3231
4014
|
}
|
|
3232
|
-
lines.
|
|
3233
|
-
return lines.join("\n");
|
|
4015
|
+
const fence = fenceForText(lines.join("\n"), 3);
|
|
4016
|
+
return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
|
|
3234
4017
|
}
|
|
3235
4018
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3236
4019
|
".png",
|
|
@@ -3361,6 +4144,9 @@ function detectLanguage(filePath) {
|
|
|
3361
4144
|
const ext = path10__namespace.posix.extname(base).toLowerCase();
|
|
3362
4145
|
return LANGUAGE_BY_EXTENSION[ext] ?? "text";
|
|
3363
4146
|
}
|
|
4147
|
+
function maskedPath(value, secretRules) {
|
|
4148
|
+
return findSecretInPath(value, secretRules)?.masked ?? value;
|
|
4149
|
+
}
|
|
3364
4150
|
function emptyDrift() {
|
|
3365
4151
|
return {
|
|
3366
4152
|
missingInSnapshot: [],
|
|
@@ -3374,6 +4160,15 @@ function emptyDrift() {
|
|
|
3374
4160
|
function sha256Hex(buffer) {
|
|
3375
4161
|
return crypto__namespace.createHash("sha256").update(buffer).digest("hex");
|
|
3376
4162
|
}
|
|
4163
|
+
function wouldNowBeIncluded(cwd, relativePath) {
|
|
4164
|
+
const absolutePath = path10__namespace.join(cwd, relativePath);
|
|
4165
|
+
try {
|
|
4166
|
+
if (!fs9__namespace.statSync(absolutePath).isFile()) return false;
|
|
4167
|
+
return !isBinaryFile(absolutePath);
|
|
4168
|
+
} catch {
|
|
4169
|
+
return false;
|
|
4170
|
+
}
|
|
4171
|
+
}
|
|
3377
4172
|
function validateSnapshot(configInput) {
|
|
3378
4173
|
assertUsableConfigInput(configInput);
|
|
3379
4174
|
const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
|
|
@@ -3422,15 +4217,20 @@ function validateSnapshot(configInput) {
|
|
|
3422
4217
|
const skippedSet = new Set(
|
|
3423
4218
|
manifest.skipped.filter((entry) => entry && typeof entry.path === "string").map((entry) => entry.path)
|
|
3424
4219
|
);
|
|
4220
|
+
const isSkippedOnDisk = (diskFile) => {
|
|
4221
|
+
if (skippedSet.has(diskFile)) return true;
|
|
4222
|
+
return skippedSet.has(maskedPath(diskFile, config.secretRules));
|
|
4223
|
+
};
|
|
3425
4224
|
const collection = collectFiles(config.cwd, {
|
|
3426
4225
|
includePatterns: config.include,
|
|
3427
4226
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
3428
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
4227
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
4228
|
+
secretRules: config.secretRules
|
|
3429
4229
|
});
|
|
3430
4230
|
for (const diskFile of collection.files) {
|
|
3431
|
-
if (skippedSet.has(diskFile)) continue;
|
|
3432
4231
|
const snapshotHash = manifestFileMap.get(diskFile);
|
|
3433
4232
|
if (!snapshotHash) {
|
|
4233
|
+
if (isSkippedOnDisk(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
|
|
3434
4234
|
drift.missingInSnapshot.push(diskFile);
|
|
3435
4235
|
continue;
|
|
3436
4236
|
}
|
|
@@ -3699,10 +4499,19 @@ function validateSnapshot(configInput) {
|
|
|
3699
4499
|
`${drift.configMismatches.length} configuration mismatch(es) between the manifest and this run.`
|
|
3700
4500
|
);
|
|
3701
4501
|
}
|
|
4502
|
+
const maskedDrift = {
|
|
4503
|
+
...drift,
|
|
4504
|
+
missingInSnapshot: drift.missingInSnapshot.map((value) => maskedPath(value, config.secretRules)),
|
|
4505
|
+
missingOnDisk: drift.missingOnDisk.map((value) => maskedPath(value, config.secretRules)),
|
|
4506
|
+
hashMismatches: drift.hashMismatches.map((mismatch) => ({
|
|
4507
|
+
...mismatch,
|
|
4508
|
+
path: maskedPath(mismatch.path, config.secretRules)
|
|
4509
|
+
}))
|
|
4510
|
+
};
|
|
3702
4511
|
return {
|
|
3703
4512
|
valid: errors.length === 0,
|
|
3704
4513
|
manifestPresent: true,
|
|
3705
|
-
drift,
|
|
4514
|
+
drift: maskedDrift,
|
|
3706
4515
|
errors,
|
|
3707
4516
|
warnings
|
|
3708
4517
|
};
|
|
@@ -3711,16 +4520,15 @@ function describeMatcher(matcher) {
|
|
|
3711
4520
|
if (typeof matcher === "string") return matcher;
|
|
3712
4521
|
if (matcher instanceof RegExp) return String(matcher);
|
|
3713
4522
|
if (typeof matcher === "function") {
|
|
3714
|
-
|
|
3715
|
-
return source.length > 70 ? `${source.slice(0, 67)}...` : source;
|
|
4523
|
+
return "function (custom predicate)";
|
|
3716
4524
|
}
|
|
3717
4525
|
return String(matcher);
|
|
3718
4526
|
}
|
|
3719
4527
|
function normalizeTarget(cwd, target) {
|
|
3720
4528
|
const absolute = path10__namespace.resolve(cwd, target);
|
|
3721
|
-
const
|
|
3722
|
-
if (
|
|
3723
|
-
return
|
|
4529
|
+
const relative4 = path10__namespace.relative(cwd, absolute);
|
|
4530
|
+
if (relative4.startsWith("..") || path10__namespace.isAbsolute(relative4)) return target;
|
|
4531
|
+
return relative4.split(path10__namespace.sep).join("/");
|
|
3724
4532
|
}
|
|
3725
4533
|
function findRuleMatch(rules, relativePath) {
|
|
3726
4534
|
return rules.find(
|
|
@@ -3736,20 +4544,24 @@ function readManifest2(config) {
|
|
|
3736
4544
|
}
|
|
3737
4545
|
}
|
|
3738
4546
|
function explainPath(config, target) {
|
|
3739
|
-
const
|
|
3740
|
-
const absolute = path10__namespace.join(config.cwd,
|
|
4547
|
+
const relative4 = normalizeTarget(config.cwd, target);
|
|
4548
|
+
const absolute = path10__namespace.join(config.cwd, relative4);
|
|
4549
|
+
const maskedPath2 = findSecretInPath(relative4, config.secretRules)?.masked ?? relative4;
|
|
3741
4550
|
const result = {
|
|
3742
|
-
path:
|
|
4551
|
+
path: maskedPath2,
|
|
3743
4552
|
exists: fs9__namespace.existsSync(absolute),
|
|
3744
4553
|
included: false
|
|
3745
4554
|
};
|
|
3746
4555
|
const collection = collectFiles(config.cwd, {
|
|
3747
4556
|
includePatterns: config.include,
|
|
3748
4557
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
3749
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
4558
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
4559
|
+
secretRules: config.secretRules
|
|
3750
4560
|
});
|
|
3751
|
-
if (!collection.files.includes(
|
|
3752
|
-
const record = collection.skipped.find(
|
|
4561
|
+
if (!collection.files.includes(relative4)) {
|
|
4562
|
+
const record = collection.skipped.find(
|
|
4563
|
+
(entry2) => entry2.path === relative4 || entry2.path === maskedPath2
|
|
4564
|
+
);
|
|
3753
4565
|
if (record) {
|
|
3754
4566
|
result.reason = record.reason;
|
|
3755
4567
|
result.detail = record.detail;
|
|
@@ -3763,24 +4575,30 @@ function explainPath(config, target) {
|
|
|
3763
4575
|
return result;
|
|
3764
4576
|
}
|
|
3765
4577
|
result.included = true;
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
4578
|
+
if (isBinaryFile(absolute)) {
|
|
4579
|
+
result.included = false;
|
|
4580
|
+
result.reason = "binary";
|
|
4581
|
+
result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
|
|
4582
|
+
return result;
|
|
4583
|
+
}
|
|
4584
|
+
result.layer = classifyLayer(relative4, config.layers);
|
|
4585
|
+
result.kind = classifyKind(relative4, config.kinds);
|
|
4586
|
+
result.language = detectLanguage(relative4);
|
|
4587
|
+
const layerRule = findRuleMatch(config.layers, relative4);
|
|
3770
4588
|
if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
|
|
3771
|
-
const kindRule = findRuleMatch(config.kinds,
|
|
4589
|
+
const kindRule = findRuleMatch(config.kinds, relative4);
|
|
3772
4590
|
if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
|
|
3773
4591
|
if (fs9__namespace.existsSync(absolute) && fs9__namespace.statSync(absolute).isFile()) {
|
|
3774
4592
|
const buffer = fs9__namespace.readFileSync(absolute);
|
|
3775
4593
|
const text2 = buffer.toString("utf8");
|
|
3776
4594
|
result.bytes = buffer.length;
|
|
3777
4595
|
result.lines = countLines(text2);
|
|
3778
|
-
const { findings } = scanAndRedactSecrets(text2, config.secretRules,
|
|
4596
|
+
const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative4);
|
|
3779
4597
|
result.secretCount = findings.length;
|
|
3780
4598
|
result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
|
|
3781
4599
|
}
|
|
3782
4600
|
const manifest = readManifest2(config);
|
|
3783
|
-
const entry = manifest?.files.find((file) => file.relativePath ===
|
|
4601
|
+
const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative4) : void 0;
|
|
3784
4602
|
result.part = entry?.part ?? null;
|
|
3785
4603
|
result.inclusionMode = entry?.inclusionMode;
|
|
3786
4604
|
return result;
|
|
@@ -3835,7 +4653,40 @@ function hashMap(manifest) {
|
|
|
3835
4653
|
}
|
|
3836
4654
|
return map;
|
|
3837
4655
|
}
|
|
4656
|
+
function manifestProblem(value) {
|
|
4657
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
|
|
4658
|
+
const candidate = value;
|
|
4659
|
+
if (!Array.isArray(candidate.files)) return 'no "files" array';
|
|
4660
|
+
for (const entry of candidate.files) {
|
|
4661
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
4662
|
+
return 'a "files" entry is not an object';
|
|
4663
|
+
}
|
|
4664
|
+
if (typeof entry.relativePath !== "string") {
|
|
4665
|
+
return 'a "files" entry has no "relativePath"';
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
const counts = candidate.counts;
|
|
4669
|
+
if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
|
|
4670
|
+
return 'no "counts" object';
|
|
4671
|
+
}
|
|
4672
|
+
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
4673
|
+
if (typeof counts[key] !== "number") {
|
|
4674
|
+
return `no numeric "counts.${key}"`;
|
|
4675
|
+
}
|
|
4676
|
+
}
|
|
4677
|
+
if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
|
|
4678
|
+
if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
|
|
4679
|
+
return null;
|
|
4680
|
+
}
|
|
4681
|
+
function assertUsableManifest(value, label) {
|
|
4682
|
+
const problem = manifestProblem(value);
|
|
4683
|
+
if (problem) {
|
|
4684
|
+
throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
|
|
4685
|
+
}
|
|
4686
|
+
}
|
|
3838
4687
|
function compareManifests(before, after) {
|
|
4688
|
+
assertUsableManifest(before, "before");
|
|
4689
|
+
assertUsableManifest(after, "after");
|
|
3839
4690
|
const left = hashMap(before);
|
|
3840
4691
|
const right = hashMap(after);
|
|
3841
4692
|
const added = [];
|
|
@@ -3852,12 +4703,13 @@ function compareManifests(before, after) {
|
|
|
3852
4703
|
if (!right.has(filePath)) removed.push(filePath);
|
|
3853
4704
|
}
|
|
3854
4705
|
const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
4706
|
+
const mask = (values) => values.map((value) => findSecretInPath(value)?.masked ?? value);
|
|
3855
4707
|
return {
|
|
3856
4708
|
before: side(before),
|
|
3857
4709
|
after: side(after),
|
|
3858
|
-
added: sort(added),
|
|
3859
|
-
removed: sort(removed),
|
|
3860
|
-
changed: sort(changed),
|
|
4710
|
+
added: mask(sort(added)),
|
|
4711
|
+
removed: mask(sort(removed)),
|
|
4712
|
+
changed: mask(sort(changed)),
|
|
3861
4713
|
unchanged
|
|
3862
4714
|
};
|
|
3863
4715
|
}
|
|
@@ -3881,11 +4733,11 @@ function loadManifest(target) {
|
|
|
3881
4733
|
} catch (err) {
|
|
3882
4734
|
throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
|
|
3883
4735
|
}
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
4736
|
+
const problem = manifestProblem(parsed);
|
|
4737
|
+
if (problem) {
|
|
4738
|
+
throw new Error(
|
|
4739
|
+
`${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
|
|
4740
|
+
);
|
|
3889
4741
|
}
|
|
3890
4742
|
return parsed;
|
|
3891
4743
|
}
|
|
@@ -3997,7 +4849,7 @@ function renderConfig(answers) {
|
|
|
3997
4849
|
secrets: {
|
|
3998
4850
|
// [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
|
|
3999
4851
|
customPatterns: [],
|
|
4000
|
-
// Set true to drop the
|
|
4852
|
+
// Set true to drop the built-in rules above and keep only customPatterns.
|
|
4001
4853
|
disableDefaultPatterns: false,
|
|
4002
4854
|
},
|
|
4003
4855
|
|
|
@@ -4013,6 +4865,10 @@ function renderConfig(answers) {
|
|
|
4013
4865
|
},
|
|
4014
4866
|
// Skip the monolith above this size. null = no limit.
|
|
4015
4867
|
maxMonolithBytes: null,
|
|
4868
|
+
// Top-level switch for the monolith, equivalent to artifacts.monolith.
|
|
4869
|
+
// When both are present the artifact entry wins; this one exists so the
|
|
4870
|
+
// generated file lists every setting the tool accepts.
|
|
4871
|
+
includeMonolith: ${artifacts.monolith},
|
|
4016
4872
|
|
|
4017
4873
|
// \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
|
|
4018
4874
|
// afterGenerate: (manifest) => { /* write your own artifact */ }
|
|
@@ -4334,6 +5190,14 @@ function existingConfig(cwd) {
|
|
|
4334
5190
|
}
|
|
4335
5191
|
return null;
|
|
4336
5192
|
}
|
|
5193
|
+
function packageJsonHasConfig(cwd) {
|
|
5194
|
+
try {
|
|
5195
|
+
const parsed = JSON.parse(fs9__namespace.readFileSync(path10__namespace.join(cwd, "package.json"), "utf8"));
|
|
5196
|
+
return typeof parsed.snapshot === "object" && parsed.snapshot !== null;
|
|
5197
|
+
} catch {
|
|
5198
|
+
return false;
|
|
5199
|
+
}
|
|
5200
|
+
}
|
|
4337
5201
|
function artifactAnswers(options) {
|
|
4338
5202
|
const artifacts = { ...DEFAULT_ANSWERS.artifacts };
|
|
4339
5203
|
for (const name of options.skipArtifacts ?? []) {
|
|
@@ -4546,6 +5410,11 @@ async function runInit(options) {
|
|
|
4546
5410
|
`A configuration already exists (${present}). Re-run with --force to replace it.`
|
|
4547
5411
|
);
|
|
4548
5412
|
}
|
|
5413
|
+
if (packageJsonHasConfig(options.cwd) && !options.force) {
|
|
5414
|
+
throw new Error(
|
|
5415
|
+
'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.'
|
|
5416
|
+
);
|
|
5417
|
+
}
|
|
4549
5418
|
if (fs9__namespace.existsSync(configPath) && !options.force) {
|
|
4550
5419
|
throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
|
|
4551
5420
|
}
|
|
@@ -4565,7 +5434,7 @@ async function runInit(options) {
|
|
|
4565
5434
|
|
|
4566
5435
|
// src/index.ts
|
|
4567
5436
|
var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
|
|
4568
|
-
var GENERATOR_VERSION = "
|
|
5437
|
+
var GENERATOR_VERSION = "4.0.1";
|
|
4569
5438
|
var SCHEMA_VERSION = "3.0.0";
|
|
4570
5439
|
function sha256Hex2(input) {
|
|
4571
5440
|
return crypto__namespace.createHash("sha256").update(input).digest("hex");
|
|
@@ -4602,7 +5471,6 @@ function bump(counts, key) {
|
|
|
4602
5471
|
}
|
|
4603
5472
|
var RELATIVE_SPECIFIER = /^\.{1,2}\//;
|
|
4604
5473
|
var IMPORT_CANDIDATE_SUFFIXES = [
|
|
4605
|
-
"",
|
|
4606
5474
|
".ts",
|
|
4607
5475
|
".tsx",
|
|
4608
5476
|
".mts",
|
|
@@ -4616,23 +5484,59 @@ var IMPORT_CANDIDATE_SUFFIXES = [
|
|
|
4616
5484
|
"/index.js",
|
|
4617
5485
|
"/index.mjs"
|
|
4618
5486
|
];
|
|
4619
|
-
|
|
5487
|
+
var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
5488
|
+
".ts",
|
|
5489
|
+
".tsx",
|
|
5490
|
+
".mts",
|
|
5491
|
+
".cts",
|
|
5492
|
+
".js",
|
|
5493
|
+
".jsx",
|
|
5494
|
+
".mjs",
|
|
5495
|
+
".cjs"
|
|
5496
|
+
]);
|
|
5497
|
+
function buildImportResolver(files) {
|
|
5498
|
+
const byPath = /* @__PURE__ */ new Map();
|
|
5499
|
+
const byPathFolded = /* @__PURE__ */ new Map();
|
|
5500
|
+
const byStem = /* @__PURE__ */ new Map();
|
|
5501
|
+
const byStemFolded = /* @__PURE__ */ new Map();
|
|
5502
|
+
const remember = (map, key, value) => {
|
|
5503
|
+
if (!map.has(key)) map.set(key, value);
|
|
5504
|
+
};
|
|
5505
|
+
for (const file of files) {
|
|
5506
|
+
const relativePath = file.relativePath;
|
|
5507
|
+
remember(byPath, relativePath, relativePath);
|
|
5508
|
+
remember(byPathFolded, relativePath.toLowerCase(), relativePath);
|
|
5509
|
+
const extension = path10__namespace.posix.extname(relativePath);
|
|
5510
|
+
if (!SOURCE_EXTENSIONS.has(extension)) continue;
|
|
5511
|
+
const stem = relativePath.slice(0, -extension.length);
|
|
5512
|
+
remember(byStem, stem, relativePath);
|
|
5513
|
+
remember(byStemFolded, stem.toLowerCase(), relativePath);
|
|
5514
|
+
if (path10__namespace.posix.basename(stem) === "index") {
|
|
5515
|
+
const directory = path10__namespace.posix.dirname(stem);
|
|
5516
|
+
remember(byStem, directory, relativePath);
|
|
5517
|
+
remember(byStemFolded, directory.toLowerCase(), relativePath);
|
|
5518
|
+
}
|
|
5519
|
+
}
|
|
5520
|
+
return { byPath, byPathFolded, byStem, byStemFolded };
|
|
5521
|
+
}
|
|
5522
|
+
function resolveSpecifier(fromFile, specifier, resolver) {
|
|
4620
5523
|
if (!RELATIVE_SPECIFIER.test(specifier)) return null;
|
|
4621
|
-
const
|
|
4622
|
-
|
|
4623
|
-
|
|
5524
|
+
const joined = path10__namespace.posix.normalize(
|
|
5525
|
+
path10__namespace.posix.join(path10__namespace.posix.dirname(fromFile), specifier)
|
|
5526
|
+
);
|
|
5527
|
+
const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
|
|
5528
|
+
const direct = exact(joined);
|
|
5529
|
+
if (direct) return direct;
|
|
4624
5530
|
for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
|
|
4625
|
-
const
|
|
4626
|
-
if (
|
|
5531
|
+
const hit = exact(`${joined}${suffix}`);
|
|
5532
|
+
if (hit) return hit;
|
|
4627
5533
|
}
|
|
4628
|
-
const
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
}
|
|
4632
|
-
return null;
|
|
5534
|
+
const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
|
|
5535
|
+
const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
|
|
5536
|
+
return byStem(joined) ?? byStem(stem);
|
|
4633
5537
|
}
|
|
4634
5538
|
function buildIndexes(files) {
|
|
4635
|
-
const
|
|
5539
|
+
const resolver = buildImportResolver(files);
|
|
4636
5540
|
const importerCounts = /* @__PURE__ */ new Map();
|
|
4637
5541
|
const byFile = files.filter((file) => isSourceSummary(file.summary)).map((file) => {
|
|
4638
5542
|
const summary = file.summary;
|
|
@@ -4644,7 +5548,7 @@ function buildIndexes(files) {
|
|
|
4644
5548
|
});
|
|
4645
5549
|
for (const entry of byFile) {
|
|
4646
5550
|
for (const specifier of entry.imports) {
|
|
4647
|
-
const resolved = resolveSpecifier(entry.path, specifier,
|
|
5551
|
+
const resolved = resolveSpecifier(entry.path, specifier, resolver);
|
|
4648
5552
|
if (resolved) importerCounts.set(resolved, (importerCounts.get(resolved) ?? 0) + 1);
|
|
4649
5553
|
}
|
|
4650
5554
|
}
|
|
@@ -4735,29 +5639,224 @@ function resolveSafeOutputDirs(config) {
|
|
|
4735
5639
|
return { outDir, tmpDir };
|
|
4736
5640
|
}
|
|
4737
5641
|
var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
|
|
5642
|
+
var RUN_DIR_PREFIX = "run-";
|
|
5643
|
+
var STALE_RUN_MS = 60 * 60 * 1e3;
|
|
5644
|
+
function maskPathText(value, secretRules) {
|
|
5645
|
+
return findSecretInPath(value, secretRules)?.masked ?? value;
|
|
5646
|
+
}
|
|
5647
|
+
function maskSkipRecords(records, secretRules) {
|
|
5648
|
+
return maskSkipPaths(records, secretRules);
|
|
5649
|
+
}
|
|
5650
|
+
function isProcessAlive(pid) {
|
|
5651
|
+
try {
|
|
5652
|
+
process.kill(pid, 0);
|
|
5653
|
+
return true;
|
|
5654
|
+
} catch (err) {
|
|
5655
|
+
return err.code !== "ESRCH";
|
|
5656
|
+
}
|
|
5657
|
+
}
|
|
4738
5658
|
var OWNERSHIP_MARKER = ".codebase-snapshot";
|
|
4739
|
-
|
|
5659
|
+
var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
|
|
5660
|
+
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;
|
|
5661
|
+
function listEntries(dir) {
|
|
5662
|
+
const entries = [];
|
|
5663
|
+
const walk = (absolute, prefix) => {
|
|
5664
|
+
let children;
|
|
5665
|
+
try {
|
|
5666
|
+
children = fs9__namespace.readdirSync(absolute, { withFileTypes: true });
|
|
5667
|
+
} catch {
|
|
5668
|
+
return;
|
|
5669
|
+
}
|
|
5670
|
+
for (const child of children) {
|
|
5671
|
+
const relative4 = prefix === "" ? child.name : `${prefix}/${child.name}`;
|
|
5672
|
+
entries.push(relative4);
|
|
5673
|
+
if (child.isDirectory()) walk(path10__namespace.join(absolute, child.name), relative4);
|
|
5674
|
+
}
|
|
5675
|
+
};
|
|
5676
|
+
walk(dir, "");
|
|
5677
|
+
return entries.sort();
|
|
5678
|
+
}
|
|
5679
|
+
function markerPayload(dir, state, id, createdAt) {
|
|
4740
5680
|
const marker2 = {
|
|
4741
5681
|
generator: GENERATOR_NAME,
|
|
4742
5682
|
version: GENERATOR_VERSION,
|
|
4743
|
-
schemaVersion: SCHEMA_VERSION
|
|
5683
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5684
|
+
createdAt,
|
|
5685
|
+
state,
|
|
5686
|
+
id,
|
|
5687
|
+
pid: process.pid,
|
|
5688
|
+
entries: listEntries(dir)
|
|
4744
5689
|
};
|
|
4745
|
-
fs9__namespace.writeFileSync(path10__namespace.join(dir, OWNERSHIP_MARKER), `${JSON.stringify(marker2, null, 2)}
|
|
4746
|
-
`, "utf8");
|
|
4747
|
-
}
|
|
4748
|
-
function isOwnOutput(dir) {
|
|
4749
|
-
if (fs9__namespace.existsSync(path10__namespace.join(dir, OWNERSHIP_MARKER))) return true;
|
|
4750
5690
|
const manifestPath = path10__namespace.join(dir, "manifest.json");
|
|
4751
|
-
if (
|
|
5691
|
+
if (fs9__namespace.existsSync(manifestPath)) {
|
|
5692
|
+
marker2.manifestSha256 = sha256Hex2(fs9__namespace.readFileSync(manifestPath));
|
|
5693
|
+
}
|
|
5694
|
+
return marker2;
|
|
5695
|
+
}
|
|
5696
|
+
function writeOwnershipMarker(dir, marker2) {
|
|
5697
|
+
fs9__namespace.writeFileSync(
|
|
5698
|
+
path10__namespace.join(dir, OWNERSHIP_MARKER),
|
|
5699
|
+
`${JSON.stringify(marker2, null, 2)}
|
|
5700
|
+
`,
|
|
5701
|
+
"utf8"
|
|
5702
|
+
);
|
|
5703
|
+
}
|
|
5704
|
+
function readOwnershipMarker(dir) {
|
|
5705
|
+
const markerPath = path10__namespace.join(dir, OWNERSHIP_MARKER);
|
|
5706
|
+
if (!fs9__namespace.existsSync(markerPath)) {
|
|
5707
|
+
return { marker: null, problem: "it holds no .codebase-snapshot marker" };
|
|
5708
|
+
}
|
|
5709
|
+
let raw;
|
|
4752
5710
|
try {
|
|
4753
|
-
|
|
4754
|
-
const generator = parsed.generator;
|
|
4755
|
-
return typeof parsed.schemaVersion === "string" && Array.isArray(parsed.files) && typeof generator === "object" && generator !== null && generator.name === GENERATOR_NAME;
|
|
5711
|
+
raw = fs9__namespace.readFileSync(markerPath, "utf8");
|
|
4756
5712
|
} catch {
|
|
4757
|
-
return
|
|
5713
|
+
return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
|
|
5714
|
+
}
|
|
5715
|
+
if (raw.trim().length === 0) {
|
|
5716
|
+
return { marker: null, problem: "its .codebase-snapshot marker is empty" };
|
|
5717
|
+
}
|
|
5718
|
+
let parsed;
|
|
5719
|
+
try {
|
|
5720
|
+
parsed = JSON.parse(raw);
|
|
5721
|
+
} catch {
|
|
5722
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not valid JSON" };
|
|
5723
|
+
}
|
|
5724
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5725
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not a JSON object" };
|
|
5726
|
+
}
|
|
5727
|
+
const candidate = parsed;
|
|
5728
|
+
if (candidate.generator !== GENERATOR_NAME) {
|
|
5729
|
+
return { marker: null, problem: "its .codebase-snapshot marker names a different generator" };
|
|
5730
|
+
}
|
|
5731
|
+
if (typeof candidate.schemaVersion !== "string" || !SUPPORTED_MARKER_SCHEMAS.has(candidate.schemaVersion)) {
|
|
5732
|
+
return {
|
|
5733
|
+
marker: null,
|
|
5734
|
+
problem: `its .codebase-snapshot marker has a schema version this build does not understand (${String(candidate.schemaVersion)})`
|
|
5735
|
+
};
|
|
5736
|
+
}
|
|
5737
|
+
if (typeof candidate.id !== "string" || !SNAPSHOT_ID_PATTERN.test(candidate.id)) {
|
|
5738
|
+
return { marker: null, problem: "its .codebase-snapshot marker carries no snapshot id" };
|
|
5739
|
+
}
|
|
5740
|
+
if (candidate.state !== "staging" && candidate.state !== "complete") {
|
|
5741
|
+
return { marker: null, problem: "its .codebase-snapshot marker records no run state" };
|
|
5742
|
+
}
|
|
5743
|
+
return {
|
|
5744
|
+
marker: {
|
|
5745
|
+
generator: GENERATOR_NAME,
|
|
5746
|
+
version: typeof candidate.version === "string" ? candidate.version : "unknown",
|
|
5747
|
+
schemaVersion: candidate.schemaVersion,
|
|
5748
|
+
createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
|
|
5749
|
+
state: candidate.state,
|
|
5750
|
+
id: candidate.id,
|
|
5751
|
+
pid: typeof candidate.pid === "number" ? candidate.pid : void 0,
|
|
5752
|
+
manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
|
|
5753
|
+
entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
|
|
5754
|
+
},
|
|
5755
|
+
problem: null
|
|
5756
|
+
};
|
|
5757
|
+
}
|
|
5758
|
+
function describeManifestProblem(value) {
|
|
5759
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "is not a JSON object";
|
|
5760
|
+
const candidate = value;
|
|
5761
|
+
const generator = candidate.generator;
|
|
5762
|
+
if (typeof generator !== "object" || generator === null || generator.name !== GENERATOR_NAME) {
|
|
5763
|
+
return "was not written by this generator";
|
|
5764
|
+
}
|
|
5765
|
+
if (!Array.isArray(candidate.files)) return "has no files array";
|
|
5766
|
+
return null;
|
|
5767
|
+
}
|
|
5768
|
+
function assessSnapshotDirectory(dir) {
|
|
5769
|
+
const { marker: marker2, problem } = readOwnershipMarker(dir);
|
|
5770
|
+
if (!marker2) return { owned: false, problem, foreignEntries: [], marker: null };
|
|
5771
|
+
if (marker2.state !== "complete") {
|
|
5772
|
+
return {
|
|
5773
|
+
owned: false,
|
|
5774
|
+
problem: "its .codebase-snapshot marker records a run that never finished writing it",
|
|
5775
|
+
foreignEntries: [],
|
|
5776
|
+
marker: marker2
|
|
5777
|
+
};
|
|
5778
|
+
}
|
|
5779
|
+
const manifestPath = path10__namespace.join(dir, "manifest.json");
|
|
5780
|
+
if (fs9__namespace.existsSync(manifestPath)) {
|
|
5781
|
+
let raw;
|
|
5782
|
+
try {
|
|
5783
|
+
raw = fs9__namespace.readFileSync(manifestPath, "utf8");
|
|
5784
|
+
} catch {
|
|
5785
|
+
return {
|
|
5786
|
+
owned: false,
|
|
5787
|
+
problem: "its manifest.json could not be read",
|
|
5788
|
+
foreignEntries: [],
|
|
5789
|
+
marker: marker2
|
|
5790
|
+
};
|
|
5791
|
+
}
|
|
5792
|
+
let parsed;
|
|
5793
|
+
try {
|
|
5794
|
+
parsed = JSON.parse(raw);
|
|
5795
|
+
} catch {
|
|
5796
|
+
return {
|
|
5797
|
+
owned: false,
|
|
5798
|
+
problem: "its manifest.json is not valid JSON",
|
|
5799
|
+
foreignEntries: [],
|
|
5800
|
+
marker: marker2
|
|
5801
|
+
};
|
|
5802
|
+
}
|
|
5803
|
+
const manifestProblem2 = describeManifestProblem(parsed);
|
|
5804
|
+
if (manifestProblem2) {
|
|
5805
|
+
return {
|
|
5806
|
+
owned: false,
|
|
5807
|
+
problem: `its manifest.json ${manifestProblem2}`,
|
|
5808
|
+
foreignEntries: [],
|
|
5809
|
+
marker: marker2
|
|
5810
|
+
};
|
|
5811
|
+
}
|
|
5812
|
+
if (!marker2.manifestSha256) {
|
|
5813
|
+
return {
|
|
5814
|
+
owned: false,
|
|
5815
|
+
problem: "its .codebase-snapshot marker records no manifest digest",
|
|
5816
|
+
foreignEntries: [],
|
|
5817
|
+
marker: marker2
|
|
5818
|
+
};
|
|
5819
|
+
}
|
|
5820
|
+
if (sha256Hex2(raw) !== marker2.manifestSha256) {
|
|
5821
|
+
return {
|
|
5822
|
+
owned: false,
|
|
5823
|
+
problem: "its manifest.json does not match the digest recorded in its .codebase-snapshot marker",
|
|
5824
|
+
foreignEntries: [],
|
|
5825
|
+
marker: marker2
|
|
5826
|
+
};
|
|
5827
|
+
}
|
|
5828
|
+
} else if (marker2.manifestSha256) {
|
|
5829
|
+
return {
|
|
5830
|
+
owned: false,
|
|
5831
|
+
problem: "its manifest.json is missing although its marker records one",
|
|
5832
|
+
foreignEntries: [],
|
|
5833
|
+
marker: marker2
|
|
5834
|
+
};
|
|
5835
|
+
}
|
|
5836
|
+
const recorded = new Set(marker2.entries ?? []);
|
|
5837
|
+
if (recorded.size === 0) {
|
|
5838
|
+
return {
|
|
5839
|
+
owned: false,
|
|
5840
|
+
problem: "its .codebase-snapshot marker records no contents",
|
|
5841
|
+
foreignEntries: [],
|
|
5842
|
+
marker: marker2
|
|
5843
|
+
};
|
|
4758
5844
|
}
|
|
5845
|
+
const foreignEntries = listEntries(dir).filter(
|
|
5846
|
+
(entry) => !recorded.has(entry) && !PLACEHOLDER_ENTRIES.has(entry)
|
|
5847
|
+
);
|
|
5848
|
+
return {
|
|
5849
|
+
owned: foreignEntries.length === 0,
|
|
5850
|
+
problem: null,
|
|
5851
|
+
foreignEntries,
|
|
5852
|
+
marker: marker2
|
|
5853
|
+
};
|
|
5854
|
+
}
|
|
5855
|
+
function describeEntries(entries, limit = 5) {
|
|
5856
|
+
const shown = entries.slice(0, limit).join(", ");
|
|
5857
|
+
return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
|
|
4759
5858
|
}
|
|
4760
|
-
function assertReplaceable(target, label) {
|
|
5859
|
+
function assertReplaceable(target, label, secretRules) {
|
|
4761
5860
|
if (!fs9__namespace.existsSync(target)) return;
|
|
4762
5861
|
if (!fs9__namespace.statSync(target).isDirectory()) {
|
|
4763
5862
|
throw new Error(
|
|
@@ -4765,26 +5864,210 @@ function assertReplaceable(target, label) {
|
|
|
4765
5864
|
);
|
|
4766
5865
|
}
|
|
4767
5866
|
const entries = fs9__namespace.readdirSync(target);
|
|
4768
|
-
|
|
4769
|
-
if (
|
|
4770
|
-
|
|
4771
|
-
const
|
|
4772
|
-
|
|
5867
|
+
if (entries.length === 0) return;
|
|
5868
|
+
if (entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
|
|
5869
|
+
const { problem } = readOwnershipMarker(target);
|
|
5870
|
+
const verdict = assessSnapshotDirectory(target);
|
|
5871
|
+
if (verdict.owned) return;
|
|
5872
|
+
const shownForeign = verdict.foreignEntries.map((name) => maskPathText(name, secretRules));
|
|
5873
|
+
const shownEntries = entries.map((name) => maskPathText(name, secretRules));
|
|
5874
|
+
if (verdict.foreignEntries.length > 0) {
|
|
5875
|
+
throw new Error(
|
|
5876
|
+
`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.`
|
|
5877
|
+
);
|
|
5878
|
+
}
|
|
5879
|
+
const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
|
|
4773
5880
|
throw new Error(
|
|
4774
|
-
`Refusing to replace ${label} ${target}:
|
|
5881
|
+
`Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(shownEntries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
|
|
4775
5882
|
);
|
|
4776
5883
|
}
|
|
4777
|
-
function
|
|
5884
|
+
function assertStagingRootUsable(tmpDir, secretRules) {
|
|
5885
|
+
if (!fs9__namespace.existsSync(tmpDir)) return;
|
|
5886
|
+
if (!fs9__namespace.statSync(tmpDir).isDirectory()) {
|
|
5887
|
+
throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
|
|
5888
|
+
}
|
|
5889
|
+
const entries = fs9__namespace.readdirSync(tmpDir);
|
|
5890
|
+
if (entries.length === 0) return;
|
|
5891
|
+
const foreign = entries.filter((name) => {
|
|
5892
|
+
const full = path10__namespace.join(tmpDir, name);
|
|
5893
|
+
if (!name.startsWith(RUN_DIR_PREFIX)) return true;
|
|
5894
|
+
let stats;
|
|
5895
|
+
try {
|
|
5896
|
+
stats = fs9__namespace.statSync(full);
|
|
5897
|
+
} catch {
|
|
5898
|
+
return true;
|
|
5899
|
+
}
|
|
5900
|
+
if (!stats.isDirectory()) return true;
|
|
5901
|
+
const { marker: marker2 } = readOwnershipMarker(full);
|
|
5902
|
+
if (marker2) return false;
|
|
5903
|
+
try {
|
|
5904
|
+
return fs9__namespace.readdirSync(full).length > 0;
|
|
5905
|
+
} catch {
|
|
5906
|
+
return true;
|
|
5907
|
+
}
|
|
5908
|
+
});
|
|
5909
|
+
if (foreign.length > 0) {
|
|
5910
|
+
const shown = describeEntries(foreign.map((name) => maskPathText(name, secretRules)));
|
|
5911
|
+
throw new Error(
|
|
5912
|
+
`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.`
|
|
5913
|
+
);
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
function cleanStaleRunDirs(tmpDir) {
|
|
5917
|
+
let entries;
|
|
5918
|
+
try {
|
|
5919
|
+
entries = fs9__namespace.readdirSync(tmpDir);
|
|
5920
|
+
} catch {
|
|
5921
|
+
return;
|
|
5922
|
+
}
|
|
5923
|
+
for (const name of entries) {
|
|
5924
|
+
if (!name.startsWith(RUN_DIR_PREFIX)) continue;
|
|
5925
|
+
const full = path10__namespace.join(tmpDir, name);
|
|
5926
|
+
const { marker: marker2 } = readOwnershipMarker(full);
|
|
5927
|
+
if (marker2) {
|
|
5928
|
+
const created = Date.parse(marker2.createdAt);
|
|
5929
|
+
const aged = !Number.isFinite(created) || Date.now() - created > STALE_RUN_MS;
|
|
5930
|
+
const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
|
|
5931
|
+
if (aged || abandoned) {
|
|
5932
|
+
try {
|
|
5933
|
+
fs9__namespace.rmSync(full, { recursive: true, force: true });
|
|
5934
|
+
} catch {
|
|
5935
|
+
}
|
|
5936
|
+
}
|
|
5937
|
+
continue;
|
|
5938
|
+
}
|
|
5939
|
+
try {
|
|
5940
|
+
if (fs9__namespace.readdirSync(full).length === 0) fs9__namespace.rmSync(full, { recursive: true, force: true });
|
|
5941
|
+
} catch {
|
|
5942
|
+
}
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
function removeIfEmpty(dir) {
|
|
5946
|
+
try {
|
|
5947
|
+
if (fs9__namespace.readdirSync(dir).length === 0) fs9__namespace.rmSync(dir, { recursive: true, force: true });
|
|
5948
|
+
} catch {
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
function ensureParentDirectory(target, label) {
|
|
5952
|
+
const parent = path10__namespace.dirname(target);
|
|
5953
|
+
if (fs9__namespace.existsSync(parent) && !fs9__namespace.statSync(parent).isDirectory()) {
|
|
5954
|
+
throw new Error(
|
|
5955
|
+
`Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
|
|
5956
|
+
);
|
|
5957
|
+
}
|
|
5958
|
+
try {
|
|
5959
|
+
fs9__namespace.mkdirSync(parent, { recursive: true });
|
|
5960
|
+
} catch (err) {
|
|
5961
|
+
throw new Error(
|
|
5962
|
+
`Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
|
|
5963
|
+
);
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
function acquireRunLock(stagingRoot) {
|
|
5967
|
+
const key = lockKeyFor(stagingRoot);
|
|
5968
|
+
const lockPath = path10__namespace.join(os__namespace.tmpdir(), `codebase-snapshot-${key}.lock`);
|
|
5969
|
+
const payload = `${JSON.stringify({
|
|
5970
|
+
pid: process.pid,
|
|
5971
|
+
stagingRoot,
|
|
5972
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5973
|
+
})}
|
|
5974
|
+
`;
|
|
5975
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
5976
|
+
try {
|
|
5977
|
+
const handle = fs9__namespace.openSync(lockPath, "wx");
|
|
5978
|
+
try {
|
|
5979
|
+
fs9__namespace.writeSync(handle, payload);
|
|
5980
|
+
} finally {
|
|
5981
|
+
fs9__namespace.closeSync(handle);
|
|
5982
|
+
}
|
|
5983
|
+
return () => {
|
|
5984
|
+
try {
|
|
5985
|
+
fs9__namespace.rmSync(lockPath, { force: true });
|
|
5986
|
+
} catch {
|
|
5987
|
+
}
|
|
5988
|
+
};
|
|
5989
|
+
} catch (err) {
|
|
5990
|
+
if (err.code !== "EEXIST") throw err;
|
|
5991
|
+
if (attempt === 0 && lockOwnerIsGone(lockPath)) {
|
|
5992
|
+
fs9__namespace.rmSync(lockPath, { force: true });
|
|
5993
|
+
continue;
|
|
5994
|
+
}
|
|
5995
|
+
throw new Error(
|
|
5996
|
+
`Another generate run is already using the staging directory ${stagingRoot}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
|
|
5997
|
+
);
|
|
5998
|
+
}
|
|
5999
|
+
}
|
|
6000
|
+
throw new Error(`Could not acquire the generate lock for ${stagingRoot}.`);
|
|
6001
|
+
}
|
|
6002
|
+
function lockKeyFor(stagingRoot) {
|
|
6003
|
+
let key = stagingRoot;
|
|
6004
|
+
try {
|
|
6005
|
+
key = fs9__namespace.realpathSync(stagingRoot);
|
|
6006
|
+
} catch {
|
|
6007
|
+
}
|
|
6008
|
+
if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
|
|
6009
|
+
return sha256Hex2(key).slice(0, 16);
|
|
6010
|
+
}
|
|
6011
|
+
function lockOwnerIsGone(lockPath) {
|
|
6012
|
+
let raw;
|
|
6013
|
+
try {
|
|
6014
|
+
raw = fs9__namespace.readFileSync(lockPath, "utf8");
|
|
6015
|
+
} catch {
|
|
6016
|
+
return false;
|
|
6017
|
+
}
|
|
6018
|
+
let pid;
|
|
6019
|
+
try {
|
|
6020
|
+
pid = JSON.parse(raw).pid;
|
|
6021
|
+
} catch {
|
|
6022
|
+
return false;
|
|
6023
|
+
}
|
|
6024
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
6025
|
+
try {
|
|
6026
|
+
process.kill(pid, 0);
|
|
6027
|
+
return false;
|
|
6028
|
+
} catch (err) {
|
|
6029
|
+
return err.code === "ESRCH";
|
|
6030
|
+
}
|
|
6031
|
+
}
|
|
6032
|
+
function writeManifestAtomically(manifestPath, manifest) {
|
|
6033
|
+
const tempPath = `${manifestPath}.${process.pid}.tmp`;
|
|
6034
|
+
fs9__namespace.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
6035
|
+
try {
|
|
6036
|
+
fs9__namespace.renameSync(tempPath, manifestPath);
|
|
6037
|
+
} catch (err) {
|
|
6038
|
+
try {
|
|
6039
|
+
fs9__namespace.rmSync(tempPath, { force: true });
|
|
6040
|
+
} catch {
|
|
6041
|
+
}
|
|
6042
|
+
throw err;
|
|
6043
|
+
}
|
|
6044
|
+
}
|
|
6045
|
+
function swapIntoPlace(stagingDir, outDir) {
|
|
6046
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6047
|
+
const deadline = Date.now() + 3e3;
|
|
6048
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
6049
|
+
try {
|
|
6050
|
+
swapOnce(stagingDir, outDir);
|
|
6051
|
+
return;
|
|
6052
|
+
} catch (err) {
|
|
6053
|
+
const code = err.code;
|
|
6054
|
+
const racy = code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM";
|
|
6055
|
+
if (!racy || attempt >= 10 || Date.now() > deadline) throw err;
|
|
6056
|
+
sleepSync(50);
|
|
6057
|
+
}
|
|
6058
|
+
}
|
|
6059
|
+
}
|
|
6060
|
+
function swapOnce(stagingDir, outDir) {
|
|
4778
6061
|
const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
|
|
4779
6062
|
const hadPrevious = fs9__namespace.existsSync(outDir);
|
|
4780
6063
|
if (hadPrevious) fs9__namespace.renameSync(outDir, backupDir);
|
|
4781
6064
|
try {
|
|
4782
6065
|
try {
|
|
4783
|
-
fs9__namespace.renameSync(
|
|
6066
|
+
fs9__namespace.renameSync(stagingDir, outDir);
|
|
4784
6067
|
} catch (err) {
|
|
4785
6068
|
if (err.code === "EXDEV") {
|
|
4786
|
-
fs9__namespace.cpSync(
|
|
4787
|
-
fs9__namespace.rmSync(
|
|
6069
|
+
fs9__namespace.cpSync(stagingDir, outDir, { recursive: true });
|
|
6070
|
+
fs9__namespace.rmSync(stagingDir, { recursive: true, force: true });
|
|
4788
6071
|
} else {
|
|
4789
6072
|
throw err;
|
|
4790
6073
|
}
|
|
@@ -4798,6 +6081,10 @@ function swapIntoPlace(tmpDir, outDir) {
|
|
|
4798
6081
|
throw err;
|
|
4799
6082
|
}
|
|
4800
6083
|
}
|
|
6084
|
+
function sleepSync(milliseconds) {
|
|
6085
|
+
const buffer = new Int32Array(new SharedArrayBuffer(4));
|
|
6086
|
+
Atomics.wait(buffer, 0, 0, milliseconds);
|
|
6087
|
+
}
|
|
4801
6088
|
function artifactWarnings(artifacts) {
|
|
4802
6089
|
const messages = [];
|
|
4803
6090
|
if (!artifacts.manifest) {
|
|
@@ -4860,10 +6147,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4860
6147
|
const collection = collectFiles(config.cwd, {
|
|
4861
6148
|
includePatterns: config.include,
|
|
4862
6149
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
4863
|
-
includeSensitiveFiles: config.includeSensitiveFiles
|
|
6150
|
+
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
6151
|
+
secretRules: config.secretRules
|
|
4864
6152
|
});
|
|
4865
6153
|
const files = [];
|
|
4866
|
-
const
|
|
6154
|
+
const monolithBodies = /* @__PURE__ */ new Map();
|
|
6155
|
+
const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
|
|
4867
6156
|
const secretFindings = [];
|
|
4868
6157
|
const warnings = [];
|
|
4869
6158
|
const layerCounts = {};
|
|
@@ -4926,6 +6215,9 @@ function generateSnapshot(configInput, options) {
|
|
|
4926
6215
|
isPreviewTruncated = preview.isTruncated;
|
|
4927
6216
|
includedLines = preview.includedLines;
|
|
4928
6217
|
}
|
|
6218
|
+
if (config.includeMonolith && isPreviewTruncated) {
|
|
6219
|
+
monolithBodies.set(relativePath, redacted);
|
|
6220
|
+
}
|
|
4929
6221
|
let summary;
|
|
4930
6222
|
let summaryQuality;
|
|
4931
6223
|
try {
|
|
@@ -4934,12 +6226,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4934
6226
|
summaryQuality = outcome?.quality;
|
|
4935
6227
|
if (isSourceSummary(summary) && summary.parseErrors) {
|
|
4936
6228
|
warnings.push(
|
|
4937
|
-
`Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
|
|
6229
|
+
`Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
|
|
4938
6230
|
);
|
|
4939
6231
|
}
|
|
4940
6232
|
} catch (err) {
|
|
4941
6233
|
warnings.push(
|
|
4942
|
-
`Summary skipped for ${relativePath}: ${err.message}`.replace(/\s+/g, " ")
|
|
6234
|
+
`Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(/\s+/g, " ")
|
|
4943
6235
|
);
|
|
4944
6236
|
}
|
|
4945
6237
|
files.push({
|
|
@@ -4968,7 +6260,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4968
6260
|
).join("\n")
|
|
4969
6261
|
);
|
|
4970
6262
|
const fullContents = /* @__PURE__ */ new Map();
|
|
4971
|
-
for (const file of files)
|
|
6263
|
+
for (const file of files) {
|
|
6264
|
+
fullContents.set(
|
|
6265
|
+
file.relativePath,
|
|
6266
|
+
monolithBodies.get(file.relativePath) ?? file.content ?? ""
|
|
6267
|
+
);
|
|
6268
|
+
}
|
|
4972
6269
|
const parts = partitionByBudget(files, config, fingerprint);
|
|
4973
6270
|
const maxEffectiveBudget = Math.max(
|
|
4974
6271
|
config.maxPartBytes,
|
|
@@ -5043,21 +6340,28 @@ function generateSnapshot(configInput, options) {
|
|
|
5043
6340
|
const { content: _content, ...rest } = file;
|
|
5044
6341
|
return rest;
|
|
5045
6342
|
}),
|
|
5046
|
-
skipped,
|
|
6343
|
+
skipped: maskSkipRecords(skipped, config.secretRules),
|
|
5047
6344
|
warnings
|
|
5048
6345
|
};
|
|
5049
6346
|
manifest.warnings = [
|
|
5050
6347
|
...warnings,
|
|
5051
|
-
...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
|
|
6348
|
+
...secretFindings.map((finding) => `${finding.rule} at ${maskPathText(finding.path ?? "unknown", config.secretRules)}:${finding.line}`)
|
|
5052
6349
|
];
|
|
5053
6350
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
5054
6351
|
let monolithJson = null;
|
|
5055
6352
|
let monolithMarkdown = null;
|
|
5056
6353
|
if (config.includeMonolith) {
|
|
5057
|
-
const fullFiles = files.map((file) =>
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
6354
|
+
const fullFiles = files.map((file) => {
|
|
6355
|
+
const content = fullContents.get(file.relativePath) ?? "";
|
|
6356
|
+
return {
|
|
6357
|
+
...file,
|
|
6358
|
+
content,
|
|
6359
|
+
inclusionMode: "full",
|
|
6360
|
+
isPreviewTruncated: false,
|
|
6361
|
+
includedBytes: Buffer.byteLength(content, "utf8"),
|
|
6362
|
+
includedLines: countLines(content)
|
|
6363
|
+
};
|
|
6364
|
+
});
|
|
5061
6365
|
const json = generateMonolithJson(manifest, fullFiles);
|
|
5062
6366
|
const markdown = generateMonolithMarkdown(manifest, fullFiles);
|
|
5063
6367
|
const jsonBytes = Buffer.byteLength(json, "utf8");
|
|
@@ -5085,57 +6389,92 @@ function generateSnapshot(configInput, options) {
|
|
|
5085
6389
|
}
|
|
5086
6390
|
for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
|
|
5087
6391
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
6392
|
+
const { outDir, tmpDir } = resolveSafeOutputDirs(config);
|
|
6393
|
+
const { artifacts } = config;
|
|
6394
|
+
assertReplaceable(outDir, "outDir", config.secretRules);
|
|
6395
|
+
assertStagingRootUsable(tmpDir, config.secretRules);
|
|
5088
6396
|
if (dryRun) {
|
|
5089
6397
|
runAfterGenerate(config, manifest);
|
|
5090
6398
|
return manifest;
|
|
5091
6399
|
}
|
|
5092
|
-
const
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
6400
|
+
const releaseLock = acquireRunLock(tmpDir);
|
|
6401
|
+
let stagingDir = null;
|
|
6402
|
+
try {
|
|
6403
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6404
|
+
fs9__namespace.mkdirSync(tmpDir, { recursive: true });
|
|
6405
|
+
cleanStaleRunDirs(tmpDir);
|
|
6406
|
+
const snapshotId = crypto__namespace.randomUUID();
|
|
6407
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6408
|
+
stagingDir = path10__namespace.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
|
|
6409
|
+
fs9__namespace.mkdirSync(stagingDir);
|
|
6410
|
+
writeOwnershipMarker(stagingDir, {
|
|
6411
|
+
generator: GENERATOR_NAME,
|
|
6412
|
+
version: GENERATOR_VERSION,
|
|
6413
|
+
schemaVersion: SCHEMA_VERSION,
|
|
6414
|
+
createdAt,
|
|
6415
|
+
state: "staging",
|
|
6416
|
+
id: snapshotId,
|
|
6417
|
+
pid: process.pid
|
|
6418
|
+
});
|
|
6419
|
+
const partsDir = path10__namespace.join(stagingDir, "parts");
|
|
6420
|
+
const reportsDir = path10__namespace.join(stagingDir, "reports");
|
|
6421
|
+
if (artifacts.parts) fs9__namespace.mkdirSync(partsDir, { recursive: true });
|
|
6422
|
+
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
6423
|
+
fs9__namespace.mkdirSync(reportsDir, { recursive: true });
|
|
6424
|
+
}
|
|
6425
|
+
if (artifacts.parts) {
|
|
6426
|
+
for (const part of parts) {
|
|
6427
|
+
fs9__namespace.writeFileSync(path10__namespace.join(partsDir, part.filename), part.markdown, "utf8");
|
|
6428
|
+
}
|
|
5108
6429
|
}
|
|
6430
|
+
if (artifacts.projectTree) {
|
|
6431
|
+
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
6432
|
+
fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
6433
|
+
}
|
|
6434
|
+
if (artifacts.navigation) {
|
|
6435
|
+
fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
6436
|
+
}
|
|
6437
|
+
if (artifacts.audit) {
|
|
6438
|
+
fs9__namespace.writeFileSync(
|
|
6439
|
+
path10__namespace.join(reportsDir, "audit.md"),
|
|
6440
|
+
generateAuditReport(manifest, secretFindings, config.secretRules),
|
|
6441
|
+
"utf8"
|
|
6442
|
+
);
|
|
6443
|
+
}
|
|
6444
|
+
if (artifacts.readme) {
|
|
6445
|
+
fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
|
|
6446
|
+
}
|
|
6447
|
+
if (artifacts.manifest) {
|
|
6448
|
+
fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
6449
|
+
}
|
|
6450
|
+
if (monolithJson !== null) {
|
|
6451
|
+
fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
|
|
6452
|
+
}
|
|
6453
|
+
if (monolithMarkdown !== null) {
|
|
6454
|
+
fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
6455
|
+
}
|
|
6456
|
+
writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
|
|
6457
|
+
swapIntoPlace(stagingDir, outDir);
|
|
6458
|
+
stagingDir = null;
|
|
6459
|
+
const warningsBeforeHook = manifest.warnings.length;
|
|
6460
|
+
runAfterGenerate(config, manifest);
|
|
6461
|
+
if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
|
|
6462
|
+
writeManifestAtomically(path10__namespace.join(outDir, "manifest.json"), manifest);
|
|
6463
|
+
}
|
|
6464
|
+
writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
|
|
6465
|
+
return manifest;
|
|
6466
|
+
} catch (err) {
|
|
6467
|
+
if (stagingDir !== null) {
|
|
6468
|
+
try {
|
|
6469
|
+
fs9__namespace.rmSync(stagingDir, { recursive: true, force: true });
|
|
6470
|
+
} catch {
|
|
6471
|
+
}
|
|
6472
|
+
}
|
|
6473
|
+
throw err;
|
|
6474
|
+
} finally {
|
|
6475
|
+
removeIfEmpty(tmpDir);
|
|
6476
|
+
releaseLock();
|
|
5109
6477
|
}
|
|
5110
|
-
if (artifacts.projectTree) {
|
|
5111
|
-
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
5112
|
-
fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
5113
|
-
}
|
|
5114
|
-
if (artifacts.navigation) {
|
|
5115
|
-
fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
5116
|
-
}
|
|
5117
|
-
if (artifacts.audit) {
|
|
5118
|
-
fs9__namespace.writeFileSync(
|
|
5119
|
-
path10__namespace.join(reportsDir, "audit.md"),
|
|
5120
|
-
generateAuditReport(manifest, secretFindings),
|
|
5121
|
-
"utf8"
|
|
5122
|
-
);
|
|
5123
|
-
}
|
|
5124
|
-
if (artifacts.readme) {
|
|
5125
|
-
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
|
|
5126
|
-
}
|
|
5127
|
-
if (artifacts.manifest) {
|
|
5128
|
-
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
5129
|
-
}
|
|
5130
|
-
if (monolithJson !== null) {
|
|
5131
|
-
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
|
|
5132
|
-
}
|
|
5133
|
-
if (monolithMarkdown !== null) {
|
|
5134
|
-
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
5135
|
-
}
|
|
5136
|
-
swapIntoPlace(tmpDir, outDir);
|
|
5137
|
-
runAfterGenerate(config, manifest);
|
|
5138
|
-
return manifest;
|
|
5139
6478
|
}
|
|
5140
6479
|
async function generateSnapshotAsync(configInput, options) {
|
|
5141
6480
|
return generateSnapshot(configInput, options);
|
|
@@ -5162,6 +6501,7 @@ exports.detectLanguage = detectLanguage;
|
|
|
5162
6501
|
exports.detectPreset = detectPreset;
|
|
5163
6502
|
exports.explainPath = explainPath;
|
|
5164
6503
|
exports.fenceForContent = fenceForContent;
|
|
6504
|
+
exports.fenceForText = fenceForText;
|
|
5165
6505
|
exports.generateProjectTree = generateProjectTree;
|
|
5166
6506
|
exports.generateSnapshot = generateSnapshot;
|
|
5167
6507
|
exports.generateSnapshotAsync = generateSnapshotAsync;
|