@ankurhalder/codebase-snapshot 3.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -310
- package/dist/bin.js +1939 -570
- package/dist/{chunk-5B4UVQLR.js → chunk-FM4OOQPF.js} +1558 -390
- package/dist/cli.cjs +1907 -537
- package/dist/cli.d.cts +3 -1
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +265 -64
- package/dist/index.cjs +1557 -387
- package/dist/index.d.cts +22 -28
- package/dist/index.d.ts +22 -28
- package/dist/index.js +1 -1
- package/dist/{types-CJqkePM4.d.cts → types-BdpT_4b8.d.cts} +37 -4
- package/dist/{types-CJqkePM4.d.ts → types-BdpT_4b8.d.ts} +37 -4
- package/docs/agent.md +77 -37
- package/docs/cli.md +315 -0
- package/docs/configuration.md +254 -226
- package/docs/install.md +114 -86
- package/docs/output.md +213 -0
- package/docs/troubleshooting.md +277 -0
- package/package.json +7 -4
package/dist/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,61 @@ 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
|
+
function findSecretInPath(relativePath) {
|
|
2222
|
+
const posixPath = relativePath.replace(/\\/g, "/");
|
|
2223
|
+
const spans = [];
|
|
2224
|
+
const rules = [];
|
|
2225
|
+
for (const { rule, pattern } of DEFAULT_SECRET_PATTERNS) {
|
|
2226
|
+
const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
|
|
2227
|
+
let match;
|
|
2228
|
+
while ((match = scan.exec(posixPath)) !== null) {
|
|
2229
|
+
if (match[0].length === 0) {
|
|
2230
|
+
scan.lastIndex += 1;
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
spans.push({ start: match.index, end: match.index + match[0].length, rule });
|
|
2234
|
+
if (!rules.includes(rule)) rules.push(rule);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
if (spans.length === 0) return null;
|
|
2238
|
+
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
2239
|
+
let out = "";
|
|
2240
|
+
let cursor = 0;
|
|
2241
|
+
for (const span of spans) {
|
|
2242
|
+
if (span.end <= cursor) continue;
|
|
2243
|
+
const start = Math.max(span.start, cursor);
|
|
2244
|
+
out += posixPath.slice(cursor, start);
|
|
2245
|
+
out += REDACTION_MARKER;
|
|
2246
|
+
cursor = Math.min(span.end, posixPath.length);
|
|
2247
|
+
}
|
|
2248
|
+
out += posixPath.slice(cursor);
|
|
2249
|
+
return { rules, masked: out };
|
|
2250
|
+
}
|
|
1766
2251
|
function globToRegExp(glob) {
|
|
1767
2252
|
const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1768
2253
|
const anchored = normalized.startsWith("/");
|
|
@@ -1883,49 +2368,177 @@ function defaultIgnoreReason(relativePath) {
|
|
|
1883
2368
|
}
|
|
1884
2369
|
return null;
|
|
1885
2370
|
}
|
|
1886
|
-
function
|
|
1887
|
-
const includePatterns = options.includePatterns ?? [];
|
|
1888
|
-
const excludePatterns = options.excludePatterns ?? [];
|
|
1889
|
-
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
1890
|
-
let candidates;
|
|
1891
|
-
let walkSkips = [];
|
|
2371
|
+
function listIgnoredPaths(cwd) {
|
|
1892
2372
|
try {
|
|
1893
2373
|
const stdout = childProcess__namespace.execSync(
|
|
1894
|
-
"git ls-files -z --
|
|
2374
|
+
"git ls-files -z --others --ignored --exclude-standard --directory",
|
|
1895
2375
|
{
|
|
1896
2376
|
cwd,
|
|
1897
2377
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1898
2378
|
encoding: "utf8",
|
|
1899
|
-
maxBuffer:
|
|
2379
|
+
maxBuffer: 32 * 1024 * 1024
|
|
1900
2380
|
}
|
|
1901
2381
|
);
|
|
1902
|
-
|
|
2382
|
+
return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1903
2383
|
} catch {
|
|
1904
|
-
|
|
1905
|
-
candidates = walked.files;
|
|
1906
|
-
walkSkips = walked.skipped;
|
|
2384
|
+
return [];
|
|
1907
2385
|
}
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
2386
|
+
}
|
|
2387
|
+
var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
|
|
2388
|
+
function isToolOwnedDirName(name) {
|
|
2389
|
+
return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
|
|
2390
|
+
}
|
|
2391
|
+
function hasSnapshotMarker(target) {
|
|
2392
|
+
try {
|
|
2393
|
+
return fs9__namespace.statSync(target).isDirectory() && fs9__namespace.existsSync(path10__namespace.join(target, ".codebase-snapshot"));
|
|
2394
|
+
} catch {
|
|
2395
|
+
return false;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
function makeToolOwnedDecider(cwd) {
|
|
2399
|
+
const markers = /* @__PURE__ */ new Map();
|
|
2400
|
+
return (relativePath) => {
|
|
2401
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
2402
|
+
for (let depth = 1; depth < parts.length; depth += 1) {
|
|
2403
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
2404
|
+
if (depth === 1 && isToolOwnedDirName(prefix)) return true;
|
|
2405
|
+
let hasMarker = markers.get(prefix);
|
|
2406
|
+
if (hasMarker === void 0) {
|
|
2407
|
+
hasMarker = fs9__namespace.existsSync(path10__namespace.join(cwd, prefix, ".codebase-snapshot"));
|
|
2408
|
+
markers.set(prefix, hasMarker);
|
|
2409
|
+
}
|
|
2410
|
+
if (hasMarker) return true;
|
|
2411
|
+
}
|
|
2412
|
+
return false;
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
function makeLinkProbe() {
|
|
2416
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2417
|
+
return (absolutePath) => {
|
|
2418
|
+
let known = cache.get(absolutePath);
|
|
2419
|
+
if (known === void 0) {
|
|
2420
|
+
try {
|
|
2421
|
+
known = fs9__namespace.lstatSync(absolutePath).isSymbolicLink();
|
|
2422
|
+
} catch {
|
|
2423
|
+
known = false;
|
|
2424
|
+
}
|
|
2425
|
+
cache.set(absolutePath, known);
|
|
2426
|
+
}
|
|
2427
|
+
return known;
|
|
2428
|
+
};
|
|
2429
|
+
}
|
|
2430
|
+
function dropLinkedDirectories(cwd, candidates, skipped) {
|
|
2431
|
+
const isLink = makeLinkProbe();
|
|
2432
|
+
let rootReal = cwd;
|
|
2433
|
+
try {
|
|
2434
|
+
rootReal = fs9__namespace.realpathSync(cwd);
|
|
2435
|
+
} catch {
|
|
2436
|
+
}
|
|
2437
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2438
|
+
const kept = [];
|
|
2439
|
+
for (const candidate of candidates) {
|
|
2440
|
+
const parts = candidate.split("/");
|
|
2441
|
+
let linkedDir = null;
|
|
2442
|
+
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
2443
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
2444
|
+
if (isLink(path10__namespace.join(cwd, prefix))) {
|
|
2445
|
+
linkedDir = prefix;
|
|
2446
|
+
break;
|
|
2447
|
+
}
|
|
2448
|
+
}
|
|
2449
|
+
if (linkedDir === null) {
|
|
2450
|
+
kept.push(candidate);
|
|
2451
|
+
continue;
|
|
2452
|
+
}
|
|
2453
|
+
if (reported.has(linkedDir)) continue;
|
|
2454
|
+
reported.add(linkedDir);
|
|
2455
|
+
const absolute = path10__namespace.join(cwd, linkedDir);
|
|
2456
|
+
let targetReal = absolute;
|
|
2457
|
+
try {
|
|
2458
|
+
targetReal = fs9__namespace.realpathSync(absolute);
|
|
2459
|
+
} catch {
|
|
2460
|
+
}
|
|
2461
|
+
const target = readLinkTarget(absolute, targetReal);
|
|
2462
|
+
const what = linkedDir === candidate ? "symbolic link" : "directory link";
|
|
2463
|
+
skipped.push(
|
|
2464
|
+
isInside(rootReal, targetReal) ? {
|
|
2465
|
+
path: linkedDir,
|
|
2466
|
+
reason: "symlink",
|
|
2467
|
+
detail: `${what} not followed (target: ${target})`
|
|
2468
|
+
} : {
|
|
2469
|
+
path: linkedDir,
|
|
2470
|
+
reason: "outside-root",
|
|
2471
|
+
detail: `link target resolves outside the scanned root (target: ${target}); not followed`
|
|
2472
|
+
}
|
|
2473
|
+
);
|
|
2474
|
+
}
|
|
2475
|
+
return kept;
|
|
2476
|
+
}
|
|
2477
|
+
function collectFiles(cwd, options = {}) {
|
|
2478
|
+
const includePatterns = options.includePatterns ?? [];
|
|
2479
|
+
const excludePatterns = options.excludePatterns ?? [];
|
|
2480
|
+
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
2481
|
+
const isToolOwned = makeToolOwnedDecider(cwd);
|
|
2482
|
+
let candidates;
|
|
2483
|
+
const skipped = [];
|
|
2484
|
+
try {
|
|
2485
|
+
const stdout = childProcess__namespace.execSync(
|
|
2486
|
+
"git ls-files -z --cached --others --exclude-standard",
|
|
2487
|
+
{
|
|
2488
|
+
cwd,
|
|
2489
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2490
|
+
encoding: "utf8",
|
|
2491
|
+
maxBuffer: 50 * 1024 * 1024
|
|
2492
|
+
}
|
|
2493
|
+
);
|
|
2494
|
+
candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
2495
|
+
candidates = dropLinkedDirectories(cwd, candidates, skipped);
|
|
2496
|
+
const known = new Set(candidates);
|
|
2497
|
+
for (const ignoredPath of listIgnoredPaths(cwd)) {
|
|
2498
|
+
const trimmed = ignoredPath.replace(/\/+$/, "");
|
|
2499
|
+
if (known.has(trimmed) || known.has(ignoredPath)) continue;
|
|
2500
|
+
if (isToolOwned(trimmed)) continue;
|
|
2501
|
+
if (isSensitivePath(trimmed)) {
|
|
2502
|
+
skipped.push({
|
|
2503
|
+
path: trimmed,
|
|
2504
|
+
reason: "sensitive-file",
|
|
2505
|
+
detail: "credential-bearing file, ignored by .gitignore; enable includeSensitiveFiles to emit it"
|
|
2506
|
+
});
|
|
2507
|
+
} else {
|
|
2508
|
+
skipped.push({
|
|
2509
|
+
path: trimmed,
|
|
2510
|
+
reason: "gitignore",
|
|
2511
|
+
detail: ignoredPath.endsWith("/") ? "ignored by .gitignore (directory, not descended)" : "ignored by .gitignore"
|
|
2512
|
+
});
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
} catch {
|
|
2516
|
+
const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set(), true);
|
|
2517
|
+
candidates = walked.files;
|
|
2518
|
+
skipped.push(...walked.skipped);
|
|
2519
|
+
}
|
|
2520
|
+
const files = candidates.filter((file) => {
|
|
2521
|
+
if (isToolOwned(file)) return false;
|
|
2522
|
+
const ignored = defaultIgnoreReason(file);
|
|
2523
|
+
if (ignored) {
|
|
2524
|
+
skipped.push({ path: file, reason: "default-ignore", detail: ignored });
|
|
2525
|
+
return false;
|
|
2526
|
+
}
|
|
2527
|
+
if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesExclude(file, pattern))) {
|
|
2528
|
+
skipped.push({
|
|
2529
|
+
path: file,
|
|
2530
|
+
reason: "excluded",
|
|
2531
|
+
detail: `does not match any include pattern (${includePatterns.join(", ")})`
|
|
2532
|
+
});
|
|
2533
|
+
return false;
|
|
1922
2534
|
}
|
|
1923
2535
|
for (const pattern of excludePatterns) {
|
|
1924
2536
|
if (matchesExclude(file, pattern)) {
|
|
2537
|
+
const fromPreset = PRESET_IGNORES.has(pattern);
|
|
1925
2538
|
skipped.push({
|
|
1926
2539
|
path: file,
|
|
1927
|
-
reason: "excluded",
|
|
1928
|
-
detail: `matches exclude pattern "${pattern}"`
|
|
2540
|
+
reason: fromPreset ? "default-ignore" : "excluded",
|
|
2541
|
+
detail: fromPreset ? `preset ignore: "${pattern}"` : `matches exclude pattern "${pattern}"`
|
|
1929
2542
|
});
|
|
1930
2543
|
return false;
|
|
1931
2544
|
}
|
|
@@ -1938,11 +2551,20 @@ function collectFiles(cwd, options = {}) {
|
|
|
1938
2551
|
});
|
|
1939
2552
|
return false;
|
|
1940
2553
|
}
|
|
2554
|
+
const secret = findSecretInPath(file);
|
|
2555
|
+
if (secret) {
|
|
2556
|
+
skipped.push({
|
|
2557
|
+
path: secret.masked,
|
|
2558
|
+
reason: "sensitive-path",
|
|
2559
|
+
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`
|
|
2560
|
+
});
|
|
2561
|
+
return false;
|
|
2562
|
+
}
|
|
1941
2563
|
return true;
|
|
1942
2564
|
}).sort();
|
|
1943
2565
|
return { files, skipped };
|
|
1944
2566
|
}
|
|
1945
|
-
function walkDirectory(dir, baseDir, visited) {
|
|
2567
|
+
function walkDirectory(dir, baseDir, visited, isRoot = false) {
|
|
1946
2568
|
const results = { files: [], skipped: [] };
|
|
1947
2569
|
let realDir;
|
|
1948
2570
|
try {
|
|
@@ -1952,7 +2574,13 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1952
2574
|
}
|
|
1953
2575
|
if (visited.has(realDir)) return results;
|
|
1954
2576
|
visited.add(realDir);
|
|
1955
|
-
|
|
2577
|
+
let rootReal;
|
|
2578
|
+
try {
|
|
2579
|
+
rootReal = fs9__namespace.realpathSync(baseDir);
|
|
2580
|
+
} catch {
|
|
2581
|
+
rootReal = baseDir;
|
|
2582
|
+
}
|
|
2583
|
+
const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
|
|
1956
2584
|
let entries;
|
|
1957
2585
|
try {
|
|
1958
2586
|
entries = fs9__namespace.readdirSync(dir, { withFileTypes: true });
|
|
@@ -1961,30 +2589,79 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1961
2589
|
return results;
|
|
1962
2590
|
}
|
|
1963
2591
|
for (const entry of entries) {
|
|
1964
|
-
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) continue;
|
|
1965
2592
|
const fullPath = path10__namespace.join(dir, entry.name);
|
|
1966
2593
|
const relPath = path10__namespace.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
1967
|
-
if (
|
|
2594
|
+
if (isToolOwnedDirName(entry.name)) continue;
|
|
2595
|
+
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
|
|
2596
|
+
results.skipped.push({
|
|
2597
|
+
path: relPath,
|
|
2598
|
+
reason: "default-ignore",
|
|
2599
|
+
detail: `built-in ignore: directory "${entry.name}" (not descended)`
|
|
2600
|
+
});
|
|
2601
|
+
continue;
|
|
2602
|
+
}
|
|
2603
|
+
if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
|
|
2604
|
+
results.skipped.push({
|
|
2605
|
+
path: relPath,
|
|
2606
|
+
reason: "default-ignore",
|
|
2607
|
+
detail: "built-in ignore: filename"
|
|
2608
|
+
});
|
|
2609
|
+
continue;
|
|
2610
|
+
}
|
|
2611
|
+
if (ignoredByGitignore(relPath)) {
|
|
2612
|
+
if (!hasSnapshotMarker(fullPath)) {
|
|
2613
|
+
results.skipped.push({
|
|
2614
|
+
path: relPath,
|
|
2615
|
+
reason: isSensitivePath(relPath) ? "sensitive-file" : "gitignore",
|
|
2616
|
+
detail: "ignored by .gitignore"
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
continue;
|
|
2620
|
+
}
|
|
1968
2621
|
let isDirectory = entry.isDirectory();
|
|
1969
2622
|
let isFile = entry.isFile();
|
|
1970
2623
|
if (entry.isSymbolicLink()) {
|
|
2624
|
+
let targetReal;
|
|
1971
2625
|
try {
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
2626
|
+
targetReal = fs9__namespace.realpathSync(fullPath);
|
|
2627
|
+
} catch {
|
|
2628
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
2629
|
+
continue;
|
|
2630
|
+
}
|
|
2631
|
+
let stats;
|
|
2632
|
+
try {
|
|
2633
|
+
stats = fs9__namespace.statSync(fullPath);
|
|
2634
|
+
} catch {
|
|
2635
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
|
|
2636
|
+
continue;
|
|
2637
|
+
}
|
|
2638
|
+
const linkTarget = readLinkTarget(fullPath, targetReal);
|
|
2639
|
+
if (!isInside(rootReal, targetReal)) {
|
|
2640
|
+
results.skipped.push({
|
|
2641
|
+
path: relPath,
|
|
2642
|
+
reason: "outside-root",
|
|
2643
|
+
detail: `link target resolves outside the scanned root (target: ${linkTarget}); not followed`
|
|
2644
|
+
});
|
|
2645
|
+
continue;
|
|
2646
|
+
}
|
|
2647
|
+
if (stats.isDirectory()) {
|
|
1979
2648
|
results.skipped.push({
|
|
1980
2649
|
path: relPath,
|
|
1981
2650
|
reason: "symlink",
|
|
1982
|
-
detail:
|
|
2651
|
+
detail: `directory link not followed (target: ${linkTarget})`
|
|
2652
|
+
});
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2655
|
+
if (!stats.isFile()) {
|
|
2656
|
+
results.skipped.push({
|
|
2657
|
+
path: relPath,
|
|
2658
|
+
reason: "symlink",
|
|
2659
|
+
detail: `link to neither a file nor a directory (target: ${linkTarget})`
|
|
1983
2660
|
});
|
|
1984
|
-
} catch {
|
|
1985
|
-
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1986
2661
|
continue;
|
|
1987
2662
|
}
|
|
2663
|
+
isDirectory = false;
|
|
2664
|
+
isFile = true;
|
|
1988
2665
|
}
|
|
1989
2666
|
if (isDirectory) {
|
|
1990
2667
|
const nested = walkDirectory(fullPath, baseDir, visited);
|
|
@@ -1997,195 +2674,6 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1997
2674
|
return results;
|
|
1998
2675
|
}
|
|
1999
2676
|
|
|
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
2677
|
// src/config.ts
|
|
2190
2678
|
var CONFIG_FILENAMES = [
|
|
2191
2679
|
"snapshot.config.ts",
|
|
@@ -2235,7 +2723,12 @@ function normalizeResolvedConfig(config) {
|
|
|
2235
2723
|
),
|
|
2236
2724
|
include: resolveInclude(config.include),
|
|
2237
2725
|
preset: typeof config.preset === "string" ? config.preset : "generic",
|
|
2238
|
-
presetExclude:
|
|
2726
|
+
presetExclude: [
|
|
2727
|
+
.../* @__PURE__ */ new Set([
|
|
2728
|
+
...Array.isArray(config.presetExclude) ? config.presetExclude : [],
|
|
2729
|
+
...outputExcludePatterns(config.cwd, config.outDir, config.tmpDir)
|
|
2730
|
+
])
|
|
2731
|
+
],
|
|
2239
2732
|
layersMode: config.layersMode === "replace" ? "replace" : "merge",
|
|
2240
2733
|
budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
|
|
2241
2734
|
hooks: resolveHooks(config.hooks),
|
|
@@ -2272,7 +2765,68 @@ function supportsTypeScriptImports() {
|
|
|
2272
2765
|
return major > 22 || major === 22 && minor >= 6;
|
|
2273
2766
|
}
|
|
2274
2767
|
function stripJsonComments2(raw) {
|
|
2275
|
-
|
|
2768
|
+
const text2 = raw.replace(/^\uFEFF/, "");
|
|
2769
|
+
let out = "";
|
|
2770
|
+
let inString = false;
|
|
2771
|
+
let escaped = false;
|
|
2772
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2773
|
+
const char = text2[index];
|
|
2774
|
+
if (inString) {
|
|
2775
|
+
out += char;
|
|
2776
|
+
if (escaped) escaped = false;
|
|
2777
|
+
else if (char === "\\") escaped = true;
|
|
2778
|
+
else if (char === '"') inString = false;
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
if (char === '"') {
|
|
2782
|
+
inString = true;
|
|
2783
|
+
out += char;
|
|
2784
|
+
continue;
|
|
2785
|
+
}
|
|
2786
|
+
if (char === "/" && text2[index + 1] === "/") {
|
|
2787
|
+
while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
|
|
2788
|
+
index -= 1;
|
|
2789
|
+
continue;
|
|
2790
|
+
}
|
|
2791
|
+
if (char === "/" && text2[index + 1] === "*") {
|
|
2792
|
+
index += 2;
|
|
2793
|
+
while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
|
|
2794
|
+
if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
|
|
2795
|
+
index += 1;
|
|
2796
|
+
}
|
|
2797
|
+
index += 1;
|
|
2798
|
+
continue;
|
|
2799
|
+
}
|
|
2800
|
+
out += char;
|
|
2801
|
+
}
|
|
2802
|
+
return stripTrailingCommas(out);
|
|
2803
|
+
}
|
|
2804
|
+
function stripTrailingCommas(text2) {
|
|
2805
|
+
let out = "";
|
|
2806
|
+
let inString = false;
|
|
2807
|
+
let escaped = false;
|
|
2808
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2809
|
+
const char = text2[index];
|
|
2810
|
+
if (inString) {
|
|
2811
|
+
out += char;
|
|
2812
|
+
if (escaped) escaped = false;
|
|
2813
|
+
else if (char === "\\") escaped = true;
|
|
2814
|
+
else if (char === '"') inString = false;
|
|
2815
|
+
continue;
|
|
2816
|
+
}
|
|
2817
|
+
if (char === '"') {
|
|
2818
|
+
inString = true;
|
|
2819
|
+
out += char;
|
|
2820
|
+
continue;
|
|
2821
|
+
}
|
|
2822
|
+
if (char === ",") {
|
|
2823
|
+
let next = index + 1;
|
|
2824
|
+
while (next < text2.length && /\s/.test(text2[next])) next += 1;
|
|
2825
|
+
if (text2[next] === "}" || text2[next] === "]") continue;
|
|
2826
|
+
}
|
|
2827
|
+
out += char;
|
|
2828
|
+
}
|
|
2829
|
+
return out;
|
|
2276
2830
|
}
|
|
2277
2831
|
function parseJsonish(raw, file) {
|
|
2278
2832
|
try {
|
|
@@ -2281,6 +2835,47 @@ function parseJsonish(raw, file) {
|
|
|
2281
2835
|
throw new Error(`Could not parse ${file}: ${err.message}`);
|
|
2282
2836
|
}
|
|
2283
2837
|
}
|
|
2838
|
+
function describeValue2(value) {
|
|
2839
|
+
if (value === null) return "null";
|
|
2840
|
+
if (Array.isArray(value)) return "an array";
|
|
2841
|
+
if (value instanceof RegExp) return value.toString();
|
|
2842
|
+
if (typeof value === "function") return "a function";
|
|
2843
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
2844
|
+
if (typeof value === "object") {
|
|
2845
|
+
const keys = Object.keys(value);
|
|
2846
|
+
return keys.length > 0 ? `an object with keys ${keys.join(", ")}` : "an empty object";
|
|
2847
|
+
}
|
|
2848
|
+
return `${typeof value} (${String(value)})`;
|
|
2849
|
+
}
|
|
2850
|
+
var isThenable = (value) => typeof value.then === "function";
|
|
2851
|
+
function readConfigExport(exported, file) {
|
|
2852
|
+
if (exported === void 0 || exported === null) {
|
|
2853
|
+
throw new Error(
|
|
2854
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2855
|
+
);
|
|
2856
|
+
}
|
|
2857
|
+
if (Array.isArray(exported)) {
|
|
2858
|
+
throw new Error(
|
|
2859
|
+
`${file} exports an array; expected a single configuration object such as \`export default { outDir: "snapshot" }\`.`
|
|
2860
|
+
);
|
|
2861
|
+
}
|
|
2862
|
+
if (typeof exported === "function") {
|
|
2863
|
+
throw new Error(
|
|
2864
|
+
`${file} exports a function; expected a configuration object. Call the function yourself and export its result.`
|
|
2865
|
+
);
|
|
2866
|
+
}
|
|
2867
|
+
if (typeof exported !== "object") {
|
|
2868
|
+
throw new Error(
|
|
2869
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2870
|
+
);
|
|
2871
|
+
}
|
|
2872
|
+
if (isThenable(exported)) {
|
|
2873
|
+
throw new Error(
|
|
2874
|
+
`${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;\`).`
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
return exported;
|
|
2878
|
+
}
|
|
2284
2879
|
async function loadConfigFileDetailed(cwd) {
|
|
2285
2880
|
const warnings = [];
|
|
2286
2881
|
const errors = [];
|
|
@@ -2310,13 +2905,7 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
2310
2905
|
}
|
|
2311
2906
|
try {
|
|
2312
2907
|
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
|
-
}
|
|
2908
|
+
const config = readConfigExport(imported.default ?? imported, fullPath);
|
|
2320
2909
|
return { config, file: fullPath, warnings, errors };
|
|
2321
2910
|
} catch (err) {
|
|
2322
2911
|
const message = `Could not load ${fullPath}: ${err.message}`;
|
|
@@ -2370,7 +2959,7 @@ var DEFAULT_ARTIFACTS = {
|
|
|
2370
2959
|
function isArtifactName(value) {
|
|
2371
2960
|
return ARTIFACT_NAMES.includes(value);
|
|
2372
2961
|
}
|
|
2373
|
-
function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
2962
|
+
function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
|
|
2374
2963
|
const artifacts = {
|
|
2375
2964
|
...DEFAULT_ARTIFACTS,
|
|
2376
2965
|
// `includeMonolith` predates this selection and stays the switch for it.
|
|
@@ -2392,19 +2981,119 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
2392
2981
|
artifacts[name] = value;
|
|
2393
2982
|
}
|
|
2394
2983
|
}
|
|
2395
|
-
|
|
2396
|
-
|
|
2984
|
+
if (monolithOverride !== void 0) {
|
|
2985
|
+
artifacts.monolith = monolithOverride;
|
|
2986
|
+
}
|
|
2987
|
+
for (const name of skipped ?? []) {
|
|
2988
|
+
if (!isArtifactName(name)) {
|
|
2989
|
+
throw new Error(
|
|
2990
|
+
`Unknown artifact "${name}". Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
|
|
2991
|
+
);
|
|
2992
|
+
}
|
|
2993
|
+
artifacts[name] = false;
|
|
2994
|
+
}
|
|
2995
|
+
return artifacts;
|
|
2996
|
+
}
|
|
2997
|
+
function strictPositiveInteger(value, fallback, label) {
|
|
2998
|
+
if (value === void 0) return fallback;
|
|
2999
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
3000
|
+
throw new Error(`${label} must be a positive integer (received ${describeValue2(value)}).`);
|
|
3001
|
+
}
|
|
3002
|
+
return value;
|
|
3003
|
+
}
|
|
3004
|
+
function strictBoolean(value, fallback, label) {
|
|
3005
|
+
if (value === void 0) return fallback;
|
|
3006
|
+
if (typeof value !== "boolean") {
|
|
3007
|
+
throw new Error(`${label} must be true or false (received ${describeValue2(value)}).`);
|
|
3008
|
+
}
|
|
3009
|
+
return value;
|
|
3010
|
+
}
|
|
3011
|
+
function strictString(value, fallback, label) {
|
|
3012
|
+
if (value === void 0) return fallback;
|
|
3013
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
3014
|
+
throw new Error(`${label} must be a non-empty string (received ${describeValue2(value)}).`);
|
|
3015
|
+
}
|
|
3016
|
+
return value;
|
|
3017
|
+
}
|
|
3018
|
+
function resolveExcludePatterns(value) {
|
|
3019
|
+
if (value === void 0) return [];
|
|
3020
|
+
if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string")) {
|
|
3021
|
+
throw new Error(
|
|
3022
|
+
`\`excludePatterns\` must be an array of glob strings such as ["**/*.test.ts"] (received ${describeValue2(value)}).`
|
|
3023
|
+
);
|
|
3024
|
+
}
|
|
3025
|
+
return value.map((pattern) => pattern.trim()).filter(Boolean);
|
|
3026
|
+
}
|
|
3027
|
+
var AUTO_PRESET_ALIASES = /* @__PURE__ */ new Set(["auto", "detected"]);
|
|
3028
|
+
function resolvePresetName(value) {
|
|
3029
|
+
if (value === void 0) return void 0;
|
|
3030
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
3031
|
+
throw new Error(
|
|
3032
|
+
`\`preset\` must be a preset name (received ${describeValue2(value)}). Use "detected" (or "auto") to detect one, or one of: ${listPresetNames().join(", ")}.`
|
|
3033
|
+
);
|
|
3034
|
+
}
|
|
3035
|
+
const name = value.trim();
|
|
3036
|
+
return AUTO_PRESET_ALIASES.has(name.toLowerCase()) ? void 0 : name;
|
|
3037
|
+
}
|
|
3038
|
+
function resolveRuleList(value, kind) {
|
|
3039
|
+
const collection = kind === "layer" ? "layers" : "kinds";
|
|
3040
|
+
const example = kind === "layer" ? `{ name: "api", match: "src/api/**" }` : `{ kind: "route", match: "src/routes/**" }`;
|
|
3041
|
+
if (value === void 0) return [];
|
|
3042
|
+
if (!Array.isArray(value)) {
|
|
3043
|
+
throw new Error(
|
|
3044
|
+
`\`${collection}\` must be an array of ${kind} rules (received ${describeValue2(value)}). A ${kind} rule is ${example}.`
|
|
3045
|
+
);
|
|
3046
|
+
}
|
|
3047
|
+
return value.map((entry, index) => {
|
|
3048
|
+
const where = `${collection}[${index}]`;
|
|
3049
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
3050
|
+
throw new Error(`${where} must be an object (received ${describeValue2(entry)}). A ${kind} rule is ${example}.`);
|
|
3051
|
+
}
|
|
3052
|
+
const record = entry;
|
|
3053
|
+
const keys = Object.keys(record);
|
|
3054
|
+
const canonical = kind === "layer" ? "name" : "kind";
|
|
3055
|
+
const alias = kind === "layer" ? "layer" : "name";
|
|
3056
|
+
const rawName = typeof record[canonical] === "string" ? record[canonical] : typeof record[alias] === "string" ? record[alias] : void 0;
|
|
3057
|
+
if (rawName === void 0 || rawName.trim() === "") {
|
|
2397
3058
|
throw new Error(
|
|
2398
|
-
|
|
3059
|
+
`${where} is missing \`${canonical}\` (received keys: ${keys.join(", ") || "none"}). A ${kind} rule is ${example}; \`${alias}\` is accepted as an alias of \`${canonical}\`.`
|
|
2399
3060
|
);
|
|
2400
3061
|
}
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
3062
|
+
const match = record.match;
|
|
3063
|
+
if (match === void 0) {
|
|
3064
|
+
throw new Error(
|
|
3065
|
+
`${where}.match is required (received keys: ${keys.join(", ") || "none"}). Use a glob string ("src/api/**"), a RegExp (/\\\\.tsx$/) or a function.`
|
|
3066
|
+
);
|
|
3067
|
+
}
|
|
3068
|
+
if (!(typeof match === "string" || match instanceof RegExp || typeof match === "function")) {
|
|
3069
|
+
throw new Error(`${where}.match must be a string, a RegExp or a function (received ${describeValue2(match)}).`);
|
|
3070
|
+
}
|
|
3071
|
+
if (typeof match === "string" && match.trim() === "") {
|
|
3072
|
+
throw new Error(`${where}.match must not be an empty string.`);
|
|
3073
|
+
}
|
|
3074
|
+
return {
|
|
3075
|
+
name: rawName.trim(),
|
|
3076
|
+
match
|
|
3077
|
+
};
|
|
3078
|
+
});
|
|
3079
|
+
}
|
|
3080
|
+
function resolveLayerRules(value) {
|
|
3081
|
+
return resolveRuleList(value, "layer").map((rule) => ({ name: rule.name, match: rule.match }));
|
|
2404
3082
|
}
|
|
2405
|
-
function
|
|
2406
|
-
|
|
2407
|
-
|
|
3083
|
+
function resolveKindRules(value) {
|
|
3084
|
+
return resolveRuleList(value, "kind").map((rule) => ({ kind: rule.name, match: rule.match }));
|
|
3085
|
+
}
|
|
3086
|
+
function outputExcludePatterns(cwd, outDir, tmpDir) {
|
|
3087
|
+
const patterns = [];
|
|
3088
|
+
for (const dir of [outDir, tmpDir]) {
|
|
3089
|
+
const absolute = path10__namespace.resolve(cwd, dir);
|
|
3090
|
+
const relative4 = path10__namespace.relative(cwd, absolute);
|
|
3091
|
+
if (!relative4 || relative4 === ".." || relative4.startsWith(`..${path10__namespace.sep}`) || path10__namespace.isAbsolute(relative4)) {
|
|
3092
|
+
continue;
|
|
3093
|
+
}
|
|
3094
|
+
patterns.push(`${relative4.replace(/\\/g, "/")}/**`);
|
|
3095
|
+
}
|
|
3096
|
+
return patterns;
|
|
2408
3097
|
}
|
|
2409
3098
|
var BYTE_UNITS = {
|
|
2410
3099
|
b: 1,
|
|
@@ -2473,11 +3162,13 @@ function resolveLayersMode(mode) {
|
|
|
2473
3162
|
}
|
|
2474
3163
|
return mode;
|
|
2475
3164
|
}
|
|
2476
|
-
function resolveBudgets(budgets, fallback) {
|
|
3165
|
+
function resolveBudgets(budgets, fallback, overrideDefault) {
|
|
2477
3166
|
const byLayer = {};
|
|
2478
3167
|
if (budgets === void 0) return { default: fallback, byLayer };
|
|
2479
3168
|
if (typeof budgets !== "object" || budgets === null || Array.isArray(budgets)) {
|
|
2480
|
-
throw new Error(
|
|
3169
|
+
throw new Error(
|
|
3170
|
+
`\`budgets\` must be an object such as { default: "120k", byLayer: { api: "60k" } } (received ${describeValue2(budgets)}).`
|
|
3171
|
+
);
|
|
2481
3172
|
}
|
|
2482
3173
|
if (budgets.byLayer !== void 0) {
|
|
2483
3174
|
if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
|
|
@@ -2492,10 +3183,12 @@ function resolveBudgets(budgets, fallback) {
|
|
|
2492
3183
|
);
|
|
2493
3184
|
}
|
|
2494
3185
|
}
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
3186
|
+
const resolvedDefault = boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default");
|
|
3187
|
+
if (!overrideDefault) return { default: resolvedDefault, byLayer };
|
|
3188
|
+
for (const [layer2, value] of Object.entries(byLayer)) {
|
|
3189
|
+
byLayer[layer2] = Math.min(value, fallback);
|
|
3190
|
+
}
|
|
3191
|
+
return { default: fallback, byLayer };
|
|
2499
3192
|
}
|
|
2500
3193
|
function resolveHooks(hooks) {
|
|
2501
3194
|
if (hooks === void 0) return {};
|
|
@@ -2508,6 +3201,11 @@ function resolveHooks(hooks) {
|
|
|
2508
3201
|
return { ...hooks };
|
|
2509
3202
|
}
|
|
2510
3203
|
function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
3204
|
+
if (userConfig?.cwd !== void 0 && (typeof userConfig.cwd !== "string" || userConfig.cwd.trim() === "")) {
|
|
3205
|
+
throw new Error(
|
|
3206
|
+
`\`cwd\` must be a non-empty path string (received ${describeValue2(userConfig.cwd)}).`
|
|
3207
|
+
);
|
|
3208
|
+
}
|
|
2511
3209
|
const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
|
|
2512
3210
|
const resolved = path10__namespace.resolve(candidate);
|
|
2513
3211
|
if (!fs9__namespace.existsSync(resolved)) {
|
|
@@ -2520,55 +3218,106 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
|
2520
3218
|
}
|
|
2521
3219
|
function resolveConfig(userConfig, cliOptions, baseDir) {
|
|
2522
3220
|
const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
|
|
2523
|
-
const forcedPreset = userConfig?.preset;
|
|
3221
|
+
const forcedPreset = resolvePresetName(userConfig?.preset);
|
|
2524
3222
|
if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
|
|
2525
3223
|
throw new Error(unknownPresetMessage(forcedPreset));
|
|
2526
3224
|
}
|
|
2527
3225
|
const preset = detectPreset(cwd, forcedPreset);
|
|
2528
3226
|
const layersMode = resolveLayersMode(userConfig?.layersMode);
|
|
2529
|
-
const
|
|
2530
|
-
const
|
|
2531
|
-
const
|
|
2532
|
-
const
|
|
2533
|
-
const
|
|
2534
|
-
|
|
3227
|
+
const userLayers = resolveLayerRules(userConfig?.layers);
|
|
3228
|
+
const userKinds = resolveKindRules(userConfig?.kinds);
|
|
3229
|
+
const layers = userLayers.length > 0 ? layersMode === "replace" ? userLayers : [...userLayers, ...preset.layers] : preset.layers;
|
|
3230
|
+
const kinds = userKinds.length > 0 ? layersMode === "replace" ? userKinds : [...userKinds, ...preset.kinds] : preset.kinds;
|
|
3231
|
+
const secrets = userConfig?.secrets;
|
|
3232
|
+
if (secrets !== void 0 && (typeof secrets !== "object" || Array.isArray(secrets))) {
|
|
3233
|
+
throw new Error(
|
|
3234
|
+
`\`secrets\` must be an object of { customPatterns, disableDefaultPatterns } (received ${describeValue2(secrets)}).`
|
|
3235
|
+
);
|
|
3236
|
+
}
|
|
3237
|
+
const customRules = secrets?.customPatterns ?? [];
|
|
3238
|
+
if (!Array.isArray(customRules)) {
|
|
3239
|
+
throw new Error(
|
|
3240
|
+
`\`secrets.customPatterns\` must be an array of secret rules (received ${describeValue2(customRules)}). Each entry is a RegExp, a string, or { rule, pattern }.`
|
|
3241
|
+
);
|
|
3242
|
+
}
|
|
3243
|
+
const disableDefaultPatterns = strictBoolean(
|
|
3244
|
+
secrets?.disableDefaultPatterns,
|
|
3245
|
+
false,
|
|
3246
|
+
"`secrets.disableDefaultPatterns`"
|
|
3247
|
+
);
|
|
3248
|
+
const aliasedRules = userConfig?.secretRules;
|
|
3249
|
+
if (aliasedRules !== void 0 && !Array.isArray(aliasedRules)) {
|
|
3250
|
+
throw new Error(
|
|
3251
|
+
`\`secretRules\` must be an array of { rule, pattern } entries (received ${describeValue2(aliasedRules)}).`
|
|
3252
|
+
);
|
|
3253
|
+
}
|
|
3254
|
+
const compiledCustomRules = compileSecretRules([
|
|
3255
|
+
...customRules,
|
|
3256
|
+
...aliasedRules ?? []
|
|
3257
|
+
]);
|
|
3258
|
+
const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
|
|
3259
|
+
const cliMonolith = cliOptions?.includeMonolith;
|
|
3260
|
+
const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
|
|
2535
3261
|
const artifacts = resolveArtifacts(
|
|
2536
3262
|
userConfig?.artifacts,
|
|
2537
3263
|
cliOptions?.skipArtifacts,
|
|
2538
|
-
includeMonolith
|
|
3264
|
+
includeMonolith,
|
|
3265
|
+
cliMonolith
|
|
2539
3266
|
);
|
|
3267
|
+
const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
|
|
2540
3268
|
const maxPartBytes = boundedBytes(
|
|
2541
|
-
|
|
3269
|
+
explicitMaxPartBytes,
|
|
2542
3270
|
300 * 1024,
|
|
2543
3271
|
MIN_MAX_PART_BYTES,
|
|
2544
3272
|
"maxPartBytes"
|
|
2545
3273
|
);
|
|
2546
|
-
const budgets = resolveBudgets(
|
|
3274
|
+
const budgets = resolveBudgets(
|
|
3275
|
+
userConfig?.budgets,
|
|
3276
|
+
maxPartBytes,
|
|
3277
|
+
explicitMaxPartBytes !== void 0
|
|
3278
|
+
);
|
|
3279
|
+
const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
|
|
3280
|
+
const outDir = cliOptions?.outDir || configuredOutDir;
|
|
3281
|
+
const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
|
|
3282
|
+
const branch = strictString(userConfig?.branch, "main", "`branch`");
|
|
3283
|
+
const configuredSensitiveFiles = strictBoolean(
|
|
3284
|
+
userConfig?.includeSensitiveFiles,
|
|
3285
|
+
false,
|
|
3286
|
+
"`includeSensitiveFiles`"
|
|
3287
|
+
);
|
|
3288
|
+
const includeSensitiveFiles = strictBoolean(
|
|
3289
|
+
cliOptions?.includeSensitiveFiles,
|
|
3290
|
+
configuredSensitiveFiles,
|
|
3291
|
+
"`includeSensitiveFiles`"
|
|
3292
|
+
);
|
|
3293
|
+
const presetExclude = [
|
|
3294
|
+
.../* @__PURE__ */ new Set([...preset.exclude, ...outputExcludePatterns(cwd, outDir, tmpDir)])
|
|
3295
|
+
];
|
|
2547
3296
|
return markResolvedConfig({
|
|
2548
3297
|
cwd,
|
|
2549
|
-
branch
|
|
2550
|
-
outDir
|
|
2551
|
-
tmpDir
|
|
3298
|
+
branch,
|
|
3299
|
+
outDir,
|
|
3300
|
+
tmpDir,
|
|
2552
3301
|
maxPartBytes,
|
|
2553
3302
|
largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
|
|
2554
|
-
previewHeadLines:
|
|
2555
|
-
previewTailLines:
|
|
3303
|
+
previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
|
|
3304
|
+
previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
|
|
2556
3305
|
includeMonolith: artifacts.monolith,
|
|
2557
3306
|
maxMonolithBytes: optionalBytes(
|
|
2558
3307
|
cliOptions?.maxMonolithBytes !== void 0 ? cliOptions.maxMonolithBytes : userConfig?.maxMonolithBytes,
|
|
2559
3308
|
"maxMonolithBytes"
|
|
2560
3309
|
),
|
|
2561
|
-
includeSensitiveFiles
|
|
3310
|
+
includeSensitiveFiles,
|
|
2562
3311
|
include: resolveInclude(userConfig?.include),
|
|
2563
3312
|
preset: preset.presetName,
|
|
2564
|
-
presetExclude
|
|
3313
|
+
presetExclude,
|
|
2565
3314
|
artifacts,
|
|
2566
3315
|
layersMode,
|
|
2567
3316
|
budgets,
|
|
2568
3317
|
hooks: resolveHooks(userConfig?.hooks),
|
|
2569
3318
|
layers,
|
|
2570
3319
|
kinds,
|
|
2571
|
-
excludePatterns: userConfig?.excludePatterns
|
|
3320
|
+
excludePatterns: resolveExcludePatterns(userConfig?.excludePatterns),
|
|
2572
3321
|
secretRules
|
|
2573
3322
|
});
|
|
2574
3323
|
}
|
|
@@ -2623,7 +3372,11 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
2623
3372
|
`**Total Files:** ${manifest.counts.totalFiles} `,
|
|
2624
3373
|
`**Total Lines:** ${manifest.counts.totalLines.toLocaleString()} `,
|
|
2625
3374
|
`**Total Size:** ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB `,
|
|
2626
|
-
|
|
3375
|
+
// This counts the bytes that were *emitted* after redaction, not the bytes
|
|
3376
|
+
// that were masked: calling it "redacted bytes" made a repository with zero
|
|
3377
|
+
// secrets report a non-zero redacted-byte figure directly above "no secrets
|
|
3378
|
+
// detected". The number of masked spans is reported in section 1 instead.
|
|
3379
|
+
`**Emitted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
|
|
2627
3380
|
"",
|
|
2628
3381
|
"## 1. Security Scan & Redaction",
|
|
2629
3382
|
"",
|
|
@@ -2826,6 +3579,8 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
2826
3579
|
layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
|
|
2827
3580
|
kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
|
|
2828
3581
|
}
|
|
3582
|
+
const pathLines = included.map((file) => displayPath(file.relativePath));
|
|
3583
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
2829
3584
|
const header = [
|
|
2830
3585
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
2831
3586
|
"",
|
|
@@ -2838,9 +3593,9 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
2838
3593
|
"",
|
|
2839
3594
|
"## Files",
|
|
2840
3595
|
"",
|
|
2841
|
-
|
|
2842
|
-
...
|
|
2843
|
-
|
|
3596
|
+
`${listFence}text`,
|
|
3597
|
+
...pathLines,
|
|
3598
|
+
listFence,
|
|
2844
3599
|
"",
|
|
2845
3600
|
"## Contents",
|
|
2846
3601
|
""
|
|
@@ -2868,7 +3623,8 @@ function capValue(value, max) {
|
|
|
2868
3623
|
return `${value.slice(0, half)}\u2026${value.slice(value.length - half)}`;
|
|
2869
3624
|
}
|
|
2870
3625
|
var capMetadata = (value) => capValue(value, METADATA_VALUE_MAX);
|
|
2871
|
-
var
|
|
3626
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
|
|
3627
|
+
var displayPath = (value) => capValue(value.replace(CONTROL_CHARS, " "), DISPLAY_PATH_MAX);
|
|
2872
3628
|
function topEntries(counts, limit) {
|
|
2873
3629
|
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
3630
|
}
|
|
@@ -2964,6 +3720,8 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
2964
3720
|
layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
|
|
2965
3721
|
kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
|
|
2966
3722
|
}
|
|
3723
|
+
const pathLines = group.map((s) => displayPath(s.file.relativePath));
|
|
3724
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
2967
3725
|
const header = [
|
|
2968
3726
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
2969
3727
|
"",
|
|
@@ -2976,9 +3734,9 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
2976
3734
|
"",
|
|
2977
3735
|
"## Files",
|
|
2978
3736
|
"",
|
|
2979
|
-
|
|
2980
|
-
...
|
|
2981
|
-
|
|
3737
|
+
`${listFence}text`,
|
|
3738
|
+
...pathLines,
|
|
3739
|
+
listFence,
|
|
2982
3740
|
"",
|
|
2983
3741
|
"## Contents",
|
|
2984
3742
|
""
|
|
@@ -3037,9 +3795,12 @@ function renderFileSection(file) {
|
|
|
3037
3795
|
].join("\n");
|
|
3038
3796
|
}
|
|
3039
3797
|
function fenceForContent(content) {
|
|
3798
|
+
return fenceForText(content, 4);
|
|
3799
|
+
}
|
|
3800
|
+
function fenceForText(content, minimum) {
|
|
3040
3801
|
const matches = content.match(/`+/g);
|
|
3041
3802
|
const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
|
|
3042
|
-
return "`".repeat(Math.max(
|
|
3803
|
+
return "`".repeat(Math.max(minimum, maxRun + 1));
|
|
3043
3804
|
}
|
|
3044
3805
|
function renderSummary(summary) {
|
|
3045
3806
|
if (!summary) return [];
|
|
@@ -3188,6 +3949,7 @@ function generateReadme(manifest) {
|
|
|
3188
3949
|
|
|
3189
3950
|
// src/generator/tree.ts
|
|
3190
3951
|
var MAX_TREE_LINES = 2e4;
|
|
3952
|
+
var CONTROL_CHARS2 = /[\u0000-\u001f\u007f]/g;
|
|
3191
3953
|
function generateProjectTree(filePaths) {
|
|
3192
3954
|
const root = { children: /* @__PURE__ */ new Map() };
|
|
3193
3955
|
for (const filePath of filePaths) {
|
|
@@ -3202,7 +3964,7 @@ function generateProjectTree(filePaths) {
|
|
|
3202
3964
|
current = child;
|
|
3203
3965
|
}
|
|
3204
3966
|
}
|
|
3205
|
-
const lines = ["
|
|
3967
|
+
const lines = ["."];
|
|
3206
3968
|
let truncatedAt = -1;
|
|
3207
3969
|
function walk(node, prefix) {
|
|
3208
3970
|
if (truncatedAt !== -1) return;
|
|
@@ -3217,7 +3979,8 @@ function generateProjectTree(filePaths) {
|
|
|
3217
3979
|
const isLast = index === keys.length - 1;
|
|
3218
3980
|
const child = node.children.get(key);
|
|
3219
3981
|
const isDirectory = child.children.size > 0;
|
|
3220
|
-
|
|
3982
|
+
const label = key.replace(CONTROL_CHARS2, " ");
|
|
3983
|
+
lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
|
|
3221
3984
|
if (lines.length >= MAX_TREE_LINES) {
|
|
3222
3985
|
truncatedAt = lines.length;
|
|
3223
3986
|
return;
|
|
@@ -3229,8 +3992,8 @@ function generateProjectTree(filePaths) {
|
|
|
3229
3992
|
if (truncatedAt !== -1) {
|
|
3230
3993
|
lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
|
|
3231
3994
|
}
|
|
3232
|
-
lines.
|
|
3233
|
-
return lines.join("\n");
|
|
3995
|
+
const fence = fenceForText(lines.join("\n"), 3);
|
|
3996
|
+
return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
|
|
3234
3997
|
}
|
|
3235
3998
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3236
3999
|
".png",
|
|
@@ -3374,6 +4137,15 @@ function emptyDrift() {
|
|
|
3374
4137
|
function sha256Hex(buffer) {
|
|
3375
4138
|
return crypto__namespace.createHash("sha256").update(buffer).digest("hex");
|
|
3376
4139
|
}
|
|
4140
|
+
function wouldNowBeIncluded(cwd, relativePath) {
|
|
4141
|
+
const absolutePath = path10__namespace.join(cwd, relativePath);
|
|
4142
|
+
try {
|
|
4143
|
+
if (!fs9__namespace.statSync(absolutePath).isFile()) return false;
|
|
4144
|
+
return !isBinaryFile(absolutePath);
|
|
4145
|
+
} catch {
|
|
4146
|
+
return false;
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
3377
4149
|
function validateSnapshot(configInput) {
|
|
3378
4150
|
assertUsableConfigInput(configInput);
|
|
3379
4151
|
const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
|
|
@@ -3428,9 +4200,9 @@ function validateSnapshot(configInput) {
|
|
|
3428
4200
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
3429
4201
|
});
|
|
3430
4202
|
for (const diskFile of collection.files) {
|
|
3431
|
-
if (skippedSet.has(diskFile)) continue;
|
|
3432
4203
|
const snapshotHash = manifestFileMap.get(diskFile);
|
|
3433
4204
|
if (!snapshotHash) {
|
|
4205
|
+
if (skippedSet.has(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
|
|
3434
4206
|
drift.missingInSnapshot.push(diskFile);
|
|
3435
4207
|
continue;
|
|
3436
4208
|
}
|
|
@@ -3711,16 +4483,15 @@ function describeMatcher(matcher) {
|
|
|
3711
4483
|
if (typeof matcher === "string") return matcher;
|
|
3712
4484
|
if (matcher instanceof RegExp) return String(matcher);
|
|
3713
4485
|
if (typeof matcher === "function") {
|
|
3714
|
-
|
|
3715
|
-
return source.length > 70 ? `${source.slice(0, 67)}...` : source;
|
|
4486
|
+
return "function (custom predicate)";
|
|
3716
4487
|
}
|
|
3717
4488
|
return String(matcher);
|
|
3718
4489
|
}
|
|
3719
4490
|
function normalizeTarget(cwd, target) {
|
|
3720
4491
|
const absolute = path10__namespace.resolve(cwd, target);
|
|
3721
|
-
const
|
|
3722
|
-
if (
|
|
3723
|
-
return
|
|
4492
|
+
const relative4 = path10__namespace.relative(cwd, absolute);
|
|
4493
|
+
if (relative4.startsWith("..") || path10__namespace.isAbsolute(relative4)) return target;
|
|
4494
|
+
return relative4.split(path10__namespace.sep).join("/");
|
|
3724
4495
|
}
|
|
3725
4496
|
function findRuleMatch(rules, relativePath) {
|
|
3726
4497
|
return rules.find(
|
|
@@ -3736,10 +4507,10 @@ function readManifest2(config) {
|
|
|
3736
4507
|
}
|
|
3737
4508
|
}
|
|
3738
4509
|
function explainPath(config, target) {
|
|
3739
|
-
const
|
|
3740
|
-
const absolute = path10__namespace.join(config.cwd,
|
|
4510
|
+
const relative4 = normalizeTarget(config.cwd, target);
|
|
4511
|
+
const absolute = path10__namespace.join(config.cwd, relative4);
|
|
3741
4512
|
const result = {
|
|
3742
|
-
path:
|
|
4513
|
+
path: relative4,
|
|
3743
4514
|
exists: fs9__namespace.existsSync(absolute),
|
|
3744
4515
|
included: false
|
|
3745
4516
|
};
|
|
@@ -3748,8 +4519,8 @@ function explainPath(config, target) {
|
|
|
3748
4519
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
3749
4520
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
3750
4521
|
});
|
|
3751
|
-
if (!collection.files.includes(
|
|
3752
|
-
const record = collection.skipped.find((entry2) => entry2.path ===
|
|
4522
|
+
if (!collection.files.includes(relative4)) {
|
|
4523
|
+
const record = collection.skipped.find((entry2) => entry2.path === relative4);
|
|
3753
4524
|
if (record) {
|
|
3754
4525
|
result.reason = record.reason;
|
|
3755
4526
|
result.detail = record.detail;
|
|
@@ -3763,24 +4534,30 @@ function explainPath(config, target) {
|
|
|
3763
4534
|
return result;
|
|
3764
4535
|
}
|
|
3765
4536
|
result.included = true;
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
4537
|
+
if (isBinaryFile(absolute)) {
|
|
4538
|
+
result.included = false;
|
|
4539
|
+
result.reason = "binary";
|
|
4540
|
+
result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
|
|
4541
|
+
return result;
|
|
4542
|
+
}
|
|
4543
|
+
result.layer = classifyLayer(relative4, config.layers);
|
|
4544
|
+
result.kind = classifyKind(relative4, config.kinds);
|
|
4545
|
+
result.language = detectLanguage(relative4);
|
|
4546
|
+
const layerRule = findRuleMatch(config.layers, relative4);
|
|
3770
4547
|
if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
|
|
3771
|
-
const kindRule = findRuleMatch(config.kinds,
|
|
4548
|
+
const kindRule = findRuleMatch(config.kinds, relative4);
|
|
3772
4549
|
if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
|
|
3773
4550
|
if (fs9__namespace.existsSync(absolute) && fs9__namespace.statSync(absolute).isFile()) {
|
|
3774
4551
|
const buffer = fs9__namespace.readFileSync(absolute);
|
|
3775
4552
|
const text2 = buffer.toString("utf8");
|
|
3776
4553
|
result.bytes = buffer.length;
|
|
3777
4554
|
result.lines = countLines(text2);
|
|
3778
|
-
const { findings } = scanAndRedactSecrets(text2, config.secretRules,
|
|
4555
|
+
const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative4);
|
|
3779
4556
|
result.secretCount = findings.length;
|
|
3780
4557
|
result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
|
|
3781
4558
|
}
|
|
3782
4559
|
const manifest = readManifest2(config);
|
|
3783
|
-
const entry = manifest?.files.find((file) => file.relativePath ===
|
|
4560
|
+
const entry = manifest?.files.find((file) => file.relativePath === relative4);
|
|
3784
4561
|
result.part = entry?.part ?? null;
|
|
3785
4562
|
result.inclusionMode = entry?.inclusionMode;
|
|
3786
4563
|
return result;
|
|
@@ -3835,7 +4612,40 @@ function hashMap(manifest) {
|
|
|
3835
4612
|
}
|
|
3836
4613
|
return map;
|
|
3837
4614
|
}
|
|
4615
|
+
function manifestProblem(value) {
|
|
4616
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
|
|
4617
|
+
const candidate = value;
|
|
4618
|
+
if (!Array.isArray(candidate.files)) return 'no "files" array';
|
|
4619
|
+
for (const entry of candidate.files) {
|
|
4620
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
4621
|
+
return 'a "files" entry is not an object';
|
|
4622
|
+
}
|
|
4623
|
+
if (typeof entry.relativePath !== "string") {
|
|
4624
|
+
return 'a "files" entry has no "relativePath"';
|
|
4625
|
+
}
|
|
4626
|
+
}
|
|
4627
|
+
const counts = candidate.counts;
|
|
4628
|
+
if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
|
|
4629
|
+
return 'no "counts" object';
|
|
4630
|
+
}
|
|
4631
|
+
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
4632
|
+
if (typeof counts[key] !== "number") {
|
|
4633
|
+
return `no numeric "counts.${key}"`;
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4636
|
+
if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
|
|
4637
|
+
if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
|
|
4638
|
+
return null;
|
|
4639
|
+
}
|
|
4640
|
+
function assertUsableManifest(value, label) {
|
|
4641
|
+
const problem = manifestProblem(value);
|
|
4642
|
+
if (problem) {
|
|
4643
|
+
throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
|
|
4644
|
+
}
|
|
4645
|
+
}
|
|
3838
4646
|
function compareManifests(before, after) {
|
|
4647
|
+
assertUsableManifest(before, "before");
|
|
4648
|
+
assertUsableManifest(after, "after");
|
|
3839
4649
|
const left = hashMap(before);
|
|
3840
4650
|
const right = hashMap(after);
|
|
3841
4651
|
const added = [];
|
|
@@ -3881,11 +4691,11 @@ function loadManifest(target) {
|
|
|
3881
4691
|
} catch (err) {
|
|
3882
4692
|
throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
|
|
3883
4693
|
}
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
4694
|
+
const problem = manifestProblem(parsed);
|
|
4695
|
+
if (problem) {
|
|
4696
|
+
throw new Error(
|
|
4697
|
+
`${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
|
|
4698
|
+
);
|
|
3889
4699
|
}
|
|
3890
4700
|
return parsed;
|
|
3891
4701
|
}
|
|
@@ -3997,7 +4807,7 @@ function renderConfig(answers) {
|
|
|
3997
4807
|
secrets: {
|
|
3998
4808
|
// [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
|
|
3999
4809
|
customPatterns: [],
|
|
4000
|
-
// Set true to drop the
|
|
4810
|
+
// Set true to drop the built-in rules above and keep only customPatterns.
|
|
4001
4811
|
disableDefaultPatterns: false,
|
|
4002
4812
|
},
|
|
4003
4813
|
|
|
@@ -4013,6 +4823,10 @@ function renderConfig(answers) {
|
|
|
4013
4823
|
},
|
|
4014
4824
|
// Skip the monolith above this size. null = no limit.
|
|
4015
4825
|
maxMonolithBytes: null,
|
|
4826
|
+
// Top-level switch for the monolith, equivalent to artifacts.monolith.
|
|
4827
|
+
// When both are present the artifact entry wins; this one exists so the
|
|
4828
|
+
// generated file lists every setting the tool accepts.
|
|
4829
|
+
includeMonolith: ${artifacts.monolith},
|
|
4016
4830
|
|
|
4017
4831
|
// \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
4832
|
// afterGenerate: (manifest) => { /* write your own artifact */ }
|
|
@@ -4334,6 +5148,14 @@ function existingConfig(cwd) {
|
|
|
4334
5148
|
}
|
|
4335
5149
|
return null;
|
|
4336
5150
|
}
|
|
5151
|
+
function packageJsonHasConfig(cwd) {
|
|
5152
|
+
try {
|
|
5153
|
+
const parsed = JSON.parse(fs9__namespace.readFileSync(path10__namespace.join(cwd, "package.json"), "utf8"));
|
|
5154
|
+
return typeof parsed.snapshot === "object" && parsed.snapshot !== null;
|
|
5155
|
+
} catch {
|
|
5156
|
+
return false;
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
4337
5159
|
function artifactAnswers(options) {
|
|
4338
5160
|
const artifacts = { ...DEFAULT_ANSWERS.artifacts };
|
|
4339
5161
|
for (const name of options.skipArtifacts ?? []) {
|
|
@@ -4546,6 +5368,11 @@ async function runInit(options) {
|
|
|
4546
5368
|
`A configuration already exists (${present}). Re-run with --force to replace it.`
|
|
4547
5369
|
);
|
|
4548
5370
|
}
|
|
5371
|
+
if (packageJsonHasConfig(options.cwd) && !options.force) {
|
|
5372
|
+
throw new Error(
|
|
5373
|
+
'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.'
|
|
5374
|
+
);
|
|
5375
|
+
}
|
|
4549
5376
|
if (fs9__namespace.existsSync(configPath) && !options.force) {
|
|
4550
5377
|
throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
|
|
4551
5378
|
}
|
|
@@ -4565,7 +5392,7 @@ async function runInit(options) {
|
|
|
4565
5392
|
|
|
4566
5393
|
// src/index.ts
|
|
4567
5394
|
var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
|
|
4568
|
-
var GENERATOR_VERSION = "
|
|
5395
|
+
var GENERATOR_VERSION = "4.0.0";
|
|
4569
5396
|
var SCHEMA_VERSION = "3.0.0";
|
|
4570
5397
|
function sha256Hex2(input) {
|
|
4571
5398
|
return crypto__namespace.createHash("sha256").update(input).digest("hex");
|
|
@@ -4735,27 +5562,213 @@ function resolveSafeOutputDirs(config) {
|
|
|
4735
5562
|
return { outDir, tmpDir };
|
|
4736
5563
|
}
|
|
4737
5564
|
var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
|
|
5565
|
+
var STAGING_ENTRIES = /* @__PURE__ */ new Set([
|
|
5566
|
+
".codebase-snapshot",
|
|
5567
|
+
"manifest.json",
|
|
5568
|
+
"README.md",
|
|
5569
|
+
"parts",
|
|
5570
|
+
"reports",
|
|
5571
|
+
"full-codebase.json",
|
|
5572
|
+
"full-codebase.md"
|
|
5573
|
+
]);
|
|
4738
5574
|
var OWNERSHIP_MARKER = ".codebase-snapshot";
|
|
4739
|
-
|
|
5575
|
+
var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
|
|
5576
|
+
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;
|
|
5577
|
+
function listEntries(dir) {
|
|
5578
|
+
const entries = [];
|
|
5579
|
+
const walk = (absolute, prefix) => {
|
|
5580
|
+
let children;
|
|
5581
|
+
try {
|
|
5582
|
+
children = fs9__namespace.readdirSync(absolute, { withFileTypes: true });
|
|
5583
|
+
} catch {
|
|
5584
|
+
return;
|
|
5585
|
+
}
|
|
5586
|
+
for (const child of children) {
|
|
5587
|
+
const relative4 = prefix === "" ? child.name : `${prefix}/${child.name}`;
|
|
5588
|
+
entries.push(relative4);
|
|
5589
|
+
if (child.isDirectory()) walk(path10__namespace.join(absolute, child.name), relative4);
|
|
5590
|
+
}
|
|
5591
|
+
};
|
|
5592
|
+
walk(dir, "");
|
|
5593
|
+
return entries.sort();
|
|
5594
|
+
}
|
|
5595
|
+
function markerPayload(dir, state, id, createdAt) {
|
|
4740
5596
|
const marker2 = {
|
|
4741
5597
|
generator: GENERATOR_NAME,
|
|
4742
5598
|
version: GENERATOR_VERSION,
|
|
4743
|
-
schemaVersion: SCHEMA_VERSION
|
|
5599
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5600
|
+
createdAt,
|
|
5601
|
+
state,
|
|
5602
|
+
id,
|
|
5603
|
+
entries: listEntries(dir)
|
|
4744
5604
|
};
|
|
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
5605
|
const manifestPath = path10__namespace.join(dir, "manifest.json");
|
|
4751
|
-
if (
|
|
5606
|
+
if (fs9__namespace.existsSync(manifestPath)) {
|
|
5607
|
+
marker2.manifestSha256 = sha256Hex2(fs9__namespace.readFileSync(manifestPath));
|
|
5608
|
+
}
|
|
5609
|
+
return marker2;
|
|
5610
|
+
}
|
|
5611
|
+
function writeOwnershipMarker(dir, marker2) {
|
|
5612
|
+
fs9__namespace.writeFileSync(
|
|
5613
|
+
path10__namespace.join(dir, OWNERSHIP_MARKER),
|
|
5614
|
+
`${JSON.stringify(marker2, null, 2)}
|
|
5615
|
+
`,
|
|
5616
|
+
"utf8"
|
|
5617
|
+
);
|
|
5618
|
+
}
|
|
5619
|
+
function readOwnershipMarker(dir) {
|
|
5620
|
+
const markerPath = path10__namespace.join(dir, OWNERSHIP_MARKER);
|
|
5621
|
+
if (!fs9__namespace.existsSync(markerPath)) {
|
|
5622
|
+
return { marker: null, problem: "it holds no .codebase-snapshot marker" };
|
|
5623
|
+
}
|
|
5624
|
+
let raw;
|
|
4752
5625
|
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;
|
|
5626
|
+
raw = fs9__namespace.readFileSync(markerPath, "utf8");
|
|
4756
5627
|
} catch {
|
|
4757
|
-
return
|
|
5628
|
+
return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
|
|
5629
|
+
}
|
|
5630
|
+
if (raw.trim().length === 0) {
|
|
5631
|
+
return { marker: null, problem: "its .codebase-snapshot marker is empty" };
|
|
5632
|
+
}
|
|
5633
|
+
let parsed;
|
|
5634
|
+
try {
|
|
5635
|
+
parsed = JSON.parse(raw);
|
|
5636
|
+
} catch {
|
|
5637
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not valid JSON" };
|
|
5638
|
+
}
|
|
5639
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5640
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not a JSON object" };
|
|
5641
|
+
}
|
|
5642
|
+
const candidate = parsed;
|
|
5643
|
+
if (candidate.generator !== GENERATOR_NAME) {
|
|
5644
|
+
return { marker: null, problem: "its .codebase-snapshot marker names a different generator" };
|
|
5645
|
+
}
|
|
5646
|
+
if (typeof candidate.schemaVersion !== "string" || !SUPPORTED_MARKER_SCHEMAS.has(candidate.schemaVersion)) {
|
|
5647
|
+
return {
|
|
5648
|
+
marker: null,
|
|
5649
|
+
problem: `its .codebase-snapshot marker has a schema version this build does not understand (${String(candidate.schemaVersion)})`
|
|
5650
|
+
};
|
|
5651
|
+
}
|
|
5652
|
+
if (typeof candidate.id !== "string" || !SNAPSHOT_ID_PATTERN.test(candidate.id)) {
|
|
5653
|
+
return { marker: null, problem: "its .codebase-snapshot marker carries no snapshot id" };
|
|
5654
|
+
}
|
|
5655
|
+
if (candidate.state !== "staging" && candidate.state !== "complete") {
|
|
5656
|
+
return { marker: null, problem: "its .codebase-snapshot marker records no run state" };
|
|
5657
|
+
}
|
|
5658
|
+
return {
|
|
5659
|
+
marker: {
|
|
5660
|
+
generator: GENERATOR_NAME,
|
|
5661
|
+
version: typeof candidate.version === "string" ? candidate.version : "unknown",
|
|
5662
|
+
schemaVersion: candidate.schemaVersion,
|
|
5663
|
+
createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
|
|
5664
|
+
state: candidate.state,
|
|
5665
|
+
id: candidate.id,
|
|
5666
|
+
manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
|
|
5667
|
+
entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
|
|
5668
|
+
},
|
|
5669
|
+
problem: null
|
|
5670
|
+
};
|
|
5671
|
+
}
|
|
5672
|
+
function describeManifestProblem(value) {
|
|
5673
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "is not a JSON object";
|
|
5674
|
+
const candidate = value;
|
|
5675
|
+
const generator = candidate.generator;
|
|
5676
|
+
if (typeof generator !== "object" || generator === null || generator.name !== GENERATOR_NAME) {
|
|
5677
|
+
return "was not written by this generator";
|
|
5678
|
+
}
|
|
5679
|
+
if (!Array.isArray(candidate.files)) return "has no files array";
|
|
5680
|
+
return null;
|
|
5681
|
+
}
|
|
5682
|
+
function assessSnapshotDirectory(dir) {
|
|
5683
|
+
const { marker: marker2, problem } = readOwnershipMarker(dir);
|
|
5684
|
+
if (!marker2) return { owned: false, problem, foreignEntries: [], marker: null };
|
|
5685
|
+
if (marker2.state !== "complete") {
|
|
5686
|
+
return {
|
|
5687
|
+
owned: false,
|
|
5688
|
+
problem: "its .codebase-snapshot marker records a run that never finished writing it",
|
|
5689
|
+
foreignEntries: [],
|
|
5690
|
+
marker: marker2
|
|
5691
|
+
};
|
|
5692
|
+
}
|
|
5693
|
+
const manifestPath = path10__namespace.join(dir, "manifest.json");
|
|
5694
|
+
if (fs9__namespace.existsSync(manifestPath)) {
|
|
5695
|
+
let raw;
|
|
5696
|
+
try {
|
|
5697
|
+
raw = fs9__namespace.readFileSync(manifestPath, "utf8");
|
|
5698
|
+
} catch {
|
|
5699
|
+
return {
|
|
5700
|
+
owned: false,
|
|
5701
|
+
problem: "its manifest.json could not be read",
|
|
5702
|
+
foreignEntries: [],
|
|
5703
|
+
marker: marker2
|
|
5704
|
+
};
|
|
5705
|
+
}
|
|
5706
|
+
let parsed;
|
|
5707
|
+
try {
|
|
5708
|
+
parsed = JSON.parse(raw);
|
|
5709
|
+
} catch {
|
|
5710
|
+
return {
|
|
5711
|
+
owned: false,
|
|
5712
|
+
problem: "its manifest.json is not valid JSON",
|
|
5713
|
+
foreignEntries: [],
|
|
5714
|
+
marker: marker2
|
|
5715
|
+
};
|
|
5716
|
+
}
|
|
5717
|
+
const manifestProblem2 = describeManifestProblem(parsed);
|
|
5718
|
+
if (manifestProblem2) {
|
|
5719
|
+
return {
|
|
5720
|
+
owned: false,
|
|
5721
|
+
problem: `its manifest.json ${manifestProblem2}`,
|
|
5722
|
+
foreignEntries: [],
|
|
5723
|
+
marker: marker2
|
|
5724
|
+
};
|
|
5725
|
+
}
|
|
5726
|
+
if (!marker2.manifestSha256) {
|
|
5727
|
+
return {
|
|
5728
|
+
owned: false,
|
|
5729
|
+
problem: "its .codebase-snapshot marker records no manifest digest",
|
|
5730
|
+
foreignEntries: [],
|
|
5731
|
+
marker: marker2
|
|
5732
|
+
};
|
|
5733
|
+
}
|
|
5734
|
+
if (sha256Hex2(raw) !== marker2.manifestSha256) {
|
|
5735
|
+
return {
|
|
5736
|
+
owned: false,
|
|
5737
|
+
problem: "its manifest.json does not match the digest recorded in its .codebase-snapshot marker",
|
|
5738
|
+
foreignEntries: [],
|
|
5739
|
+
marker: marker2
|
|
5740
|
+
};
|
|
5741
|
+
}
|
|
5742
|
+
} else if (marker2.manifestSha256) {
|
|
5743
|
+
return {
|
|
5744
|
+
owned: false,
|
|
5745
|
+
problem: "its manifest.json is missing although its marker records one",
|
|
5746
|
+
foreignEntries: [],
|
|
5747
|
+
marker: marker2
|
|
5748
|
+
};
|
|
4758
5749
|
}
|
|
5750
|
+
const recorded = new Set(marker2.entries ?? []);
|
|
5751
|
+
if (recorded.size === 0) {
|
|
5752
|
+
return {
|
|
5753
|
+
owned: false,
|
|
5754
|
+
problem: "its .codebase-snapshot marker records no contents",
|
|
5755
|
+
foreignEntries: [],
|
|
5756
|
+
marker: marker2
|
|
5757
|
+
};
|
|
5758
|
+
}
|
|
5759
|
+
const foreignEntries = listEntries(dir).filter(
|
|
5760
|
+
(entry) => !recorded.has(entry) && !PLACEHOLDER_ENTRIES.has(entry)
|
|
5761
|
+
);
|
|
5762
|
+
return {
|
|
5763
|
+
owned: foreignEntries.length === 0,
|
|
5764
|
+
problem: null,
|
|
5765
|
+
foreignEntries,
|
|
5766
|
+
marker: marker2
|
|
5767
|
+
};
|
|
5768
|
+
}
|
|
5769
|
+
function describeEntries(entries, limit = 5) {
|
|
5770
|
+
const shown = entries.slice(0, limit).join(", ");
|
|
5771
|
+
return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
|
|
4759
5772
|
}
|
|
4760
5773
|
function assertReplaceable(target, label) {
|
|
4761
5774
|
if (!fs9__namespace.existsSync(target)) return;
|
|
@@ -4765,16 +5778,124 @@ function assertReplaceable(target, label) {
|
|
|
4765
5778
|
);
|
|
4766
5779
|
}
|
|
4767
5780
|
const entries = fs9__namespace.readdirSync(target);
|
|
4768
|
-
|
|
4769
|
-
if (
|
|
4770
|
-
|
|
4771
|
-
|
|
4772
|
-
|
|
5781
|
+
if (entries.length === 0) return;
|
|
5782
|
+
if (label === "outDir" && entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
|
|
5783
|
+
const { marker: marker2, problem } = readOwnershipMarker(target);
|
|
5784
|
+
if (label === "tmpDir") {
|
|
5785
|
+
if (!marker2) {
|
|
5786
|
+
throw new Error(
|
|
5787
|
+
`Refusing to replace tmpDir ${target}: ${problem ?? "it does not carry this generator's marker"}. This generator only clears a staging directory it wrote. Point tmpDir at a new or empty directory, or remove ${target} yourself.`
|
|
5788
|
+
);
|
|
5789
|
+
}
|
|
5790
|
+
const unexpected = entries.filter((name) => !STAGING_ENTRIES.has(name));
|
|
5791
|
+
if (unexpected.length > 0) {
|
|
5792
|
+
throw new Error(
|
|
5793
|
+
`Refusing to replace tmpDir ${target}: it holds ${describeEntries(unexpected)}, which this generator did not write. Point tmpDir at a new or empty directory, or remove those files yourself.`
|
|
5794
|
+
);
|
|
5795
|
+
}
|
|
5796
|
+
return;
|
|
5797
|
+
}
|
|
5798
|
+
const verdict = assessSnapshotDirectory(target);
|
|
5799
|
+
if (verdict.owned) return;
|
|
5800
|
+
if (verdict.foreignEntries.length > 0) {
|
|
5801
|
+
throw new Error(
|
|
5802
|
+
`Refusing to replace ${label} ${target}: it carries this generator's ownership marker, but these entries are not part of the snapshot it recorded: ${describeEntries(verdict.foreignEntries)}. Remove them yourself, or point ${label} at a new or empty directory.`
|
|
5803
|
+
);
|
|
5804
|
+
}
|
|
5805
|
+
const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
|
|
4773
5806
|
throw new Error(
|
|
4774
|
-
`Refusing to replace ${label} ${target}:
|
|
5807
|
+
`Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(entries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
|
|
5808
|
+
);
|
|
5809
|
+
}
|
|
5810
|
+
function ensureParentDirectory(target, label) {
|
|
5811
|
+
const parent = path10__namespace.dirname(target);
|
|
5812
|
+
if (fs9__namespace.existsSync(parent) && !fs9__namespace.statSync(parent).isDirectory()) {
|
|
5813
|
+
throw new Error(
|
|
5814
|
+
`Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
|
|
5815
|
+
);
|
|
5816
|
+
}
|
|
5817
|
+
try {
|
|
5818
|
+
fs9__namespace.mkdirSync(parent, { recursive: true });
|
|
5819
|
+
} catch (err) {
|
|
5820
|
+
throw new Error(
|
|
5821
|
+
`Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
|
|
5822
|
+
);
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
function acquireRunLock(outDir) {
|
|
5826
|
+
const lockPath = path10__namespace.join(
|
|
5827
|
+
os__namespace.tmpdir(),
|
|
5828
|
+
`codebase-snapshot-${sha256Hex2(outDir).slice(0, 16)}.lock`
|
|
4775
5829
|
);
|
|
5830
|
+
const payload = `${JSON.stringify({
|
|
5831
|
+
pid: process.pid,
|
|
5832
|
+
outDir,
|
|
5833
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5834
|
+
})}
|
|
5835
|
+
`;
|
|
5836
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
5837
|
+
try {
|
|
5838
|
+
const handle = fs9__namespace.openSync(lockPath, "wx");
|
|
5839
|
+
try {
|
|
5840
|
+
fs9__namespace.writeSync(handle, payload);
|
|
5841
|
+
} finally {
|
|
5842
|
+
fs9__namespace.closeSync(handle);
|
|
5843
|
+
}
|
|
5844
|
+
return () => {
|
|
5845
|
+
try {
|
|
5846
|
+
fs9__namespace.rmSync(lockPath, { force: true });
|
|
5847
|
+
} catch {
|
|
5848
|
+
}
|
|
5849
|
+
};
|
|
5850
|
+
} catch (err) {
|
|
5851
|
+
if (err.code !== "EEXIST") throw err;
|
|
5852
|
+
if (attempt === 0 && lockOwnerIsGone(lockPath)) {
|
|
5853
|
+
fs9__namespace.rmSync(lockPath, { force: true });
|
|
5854
|
+
continue;
|
|
5855
|
+
}
|
|
5856
|
+
throw new Error(
|
|
5857
|
+
`Another generate run is already writing ${outDir}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
|
|
5858
|
+
);
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
5861
|
+
throw new Error(`Could not acquire the generate lock for ${outDir}.`);
|
|
5862
|
+
}
|
|
5863
|
+
function lockOwnerIsGone(lockPath) {
|
|
5864
|
+
let raw;
|
|
5865
|
+
try {
|
|
5866
|
+
raw = fs9__namespace.readFileSync(lockPath, "utf8");
|
|
5867
|
+
} catch {
|
|
5868
|
+
return false;
|
|
5869
|
+
}
|
|
5870
|
+
let pid;
|
|
5871
|
+
try {
|
|
5872
|
+
pid = JSON.parse(raw).pid;
|
|
5873
|
+
} catch {
|
|
5874
|
+
return false;
|
|
5875
|
+
}
|
|
5876
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
5877
|
+
try {
|
|
5878
|
+
process.kill(pid, 0);
|
|
5879
|
+
return false;
|
|
5880
|
+
} catch (err) {
|
|
5881
|
+
return err.code === "ESRCH";
|
|
5882
|
+
}
|
|
5883
|
+
}
|
|
5884
|
+
function writeManifestAtomically(manifestPath, manifest) {
|
|
5885
|
+
const tempPath = `${manifestPath}.${process.pid}.tmp`;
|
|
5886
|
+
fs9__namespace.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
5887
|
+
try {
|
|
5888
|
+
fs9__namespace.renameSync(tempPath, manifestPath);
|
|
5889
|
+
} catch (err) {
|
|
5890
|
+
try {
|
|
5891
|
+
fs9__namespace.rmSync(tempPath, { force: true });
|
|
5892
|
+
} catch {
|
|
5893
|
+
}
|
|
5894
|
+
throw err;
|
|
5895
|
+
}
|
|
4776
5896
|
}
|
|
4777
5897
|
function swapIntoPlace(tmpDir, outDir) {
|
|
5898
|
+
ensureParentDirectory(outDir, "outDir");
|
|
4778
5899
|
const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
|
|
4779
5900
|
const hadPrevious = fs9__namespace.existsSync(outDir);
|
|
4780
5901
|
if (hadPrevious) fs9__namespace.renameSync(outDir, backupDir);
|
|
@@ -4863,6 +5984,7 @@ function generateSnapshot(configInput, options) {
|
|
|
4863
5984
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
4864
5985
|
});
|
|
4865
5986
|
const files = [];
|
|
5987
|
+
const monolithBodies = /* @__PURE__ */ new Map();
|
|
4866
5988
|
const skipped = [...collection.skipped];
|
|
4867
5989
|
const secretFindings = [];
|
|
4868
5990
|
const warnings = [];
|
|
@@ -4926,6 +6048,9 @@ function generateSnapshot(configInput, options) {
|
|
|
4926
6048
|
isPreviewTruncated = preview.isTruncated;
|
|
4927
6049
|
includedLines = preview.includedLines;
|
|
4928
6050
|
}
|
|
6051
|
+
if (config.includeMonolith && isPreviewTruncated) {
|
|
6052
|
+
monolithBodies.set(relativePath, redacted);
|
|
6053
|
+
}
|
|
4929
6054
|
let summary;
|
|
4930
6055
|
let summaryQuality;
|
|
4931
6056
|
try {
|
|
@@ -4968,7 +6093,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4968
6093
|
).join("\n")
|
|
4969
6094
|
);
|
|
4970
6095
|
const fullContents = /* @__PURE__ */ new Map();
|
|
4971
|
-
for (const file of files)
|
|
6096
|
+
for (const file of files) {
|
|
6097
|
+
fullContents.set(
|
|
6098
|
+
file.relativePath,
|
|
6099
|
+
monolithBodies.get(file.relativePath) ?? file.content ?? ""
|
|
6100
|
+
);
|
|
6101
|
+
}
|
|
4972
6102
|
const parts = partitionByBudget(files, config, fingerprint);
|
|
4973
6103
|
const maxEffectiveBudget = Math.max(
|
|
4974
6104
|
config.maxPartBytes,
|
|
@@ -5054,10 +6184,17 @@ function generateSnapshot(configInput, options) {
|
|
|
5054
6184
|
let monolithJson = null;
|
|
5055
6185
|
let monolithMarkdown = null;
|
|
5056
6186
|
if (config.includeMonolith) {
|
|
5057
|
-
const fullFiles = files.map((file) =>
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
6187
|
+
const fullFiles = files.map((file) => {
|
|
6188
|
+
const content = fullContents.get(file.relativePath) ?? "";
|
|
6189
|
+
return {
|
|
6190
|
+
...file,
|
|
6191
|
+
content,
|
|
6192
|
+
inclusionMode: "full",
|
|
6193
|
+
isPreviewTruncated: false,
|
|
6194
|
+
includedBytes: Buffer.byteLength(content, "utf8"),
|
|
6195
|
+
includedLines: countLines(content)
|
|
6196
|
+
};
|
|
6197
|
+
});
|
|
5061
6198
|
const json = generateMonolithJson(manifest, fullFiles);
|
|
5062
6199
|
const markdown = generateMonolithMarkdown(manifest, fullFiles);
|
|
5063
6200
|
const jsonBytes = Buffer.byteLength(json, "utf8");
|
|
@@ -5085,57 +6222,89 @@ function generateSnapshot(configInput, options) {
|
|
|
5085
6222
|
}
|
|
5086
6223
|
for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
|
|
5087
6224
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
6225
|
+
const { outDir, tmpDir } = resolveSafeOutputDirs(config);
|
|
6226
|
+
const { artifacts } = config;
|
|
6227
|
+
assertReplaceable(tmpDir, "tmpDir");
|
|
6228
|
+
assertReplaceable(outDir, "outDir");
|
|
5088
6229
|
if (dryRun) {
|
|
5089
6230
|
runAfterGenerate(config, manifest);
|
|
5090
6231
|
return manifest;
|
|
5091
6232
|
}
|
|
5092
|
-
const
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5100
|
-
|
|
5101
|
-
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5107
|
-
|
|
6233
|
+
const releaseLock = acquireRunLock(outDir);
|
|
6234
|
+
let stagingStarted = false;
|
|
6235
|
+
try {
|
|
6236
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6237
|
+
fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
|
|
6238
|
+
fs9__namespace.mkdirSync(tmpDir, { recursive: true });
|
|
6239
|
+
stagingStarted = true;
|
|
6240
|
+
const snapshotId = crypto__namespace.randomUUID();
|
|
6241
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6242
|
+
writeOwnershipMarker(tmpDir, {
|
|
6243
|
+
generator: GENERATOR_NAME,
|
|
6244
|
+
version: GENERATOR_VERSION,
|
|
6245
|
+
schemaVersion: SCHEMA_VERSION,
|
|
6246
|
+
createdAt,
|
|
6247
|
+
state: "staging",
|
|
6248
|
+
id: snapshotId
|
|
6249
|
+
});
|
|
6250
|
+
const partsDir = path10__namespace.join(tmpDir, "parts");
|
|
6251
|
+
const reportsDir = path10__namespace.join(tmpDir, "reports");
|
|
6252
|
+
if (artifacts.parts) fs9__namespace.mkdirSync(partsDir, { recursive: true });
|
|
6253
|
+
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
6254
|
+
fs9__namespace.mkdirSync(reportsDir, { recursive: true });
|
|
6255
|
+
}
|
|
6256
|
+
if (artifacts.parts) {
|
|
6257
|
+
for (const part of parts) {
|
|
6258
|
+
fs9__namespace.writeFileSync(path10__namespace.join(partsDir, part.filename), part.markdown, "utf8");
|
|
6259
|
+
}
|
|
5108
6260
|
}
|
|
6261
|
+
if (artifacts.projectTree) {
|
|
6262
|
+
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
6263
|
+
fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
6264
|
+
}
|
|
6265
|
+
if (artifacts.navigation) {
|
|
6266
|
+
fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
6267
|
+
}
|
|
6268
|
+
if (artifacts.audit) {
|
|
6269
|
+
fs9__namespace.writeFileSync(
|
|
6270
|
+
path10__namespace.join(reportsDir, "audit.md"),
|
|
6271
|
+
generateAuditReport(manifest, secretFindings),
|
|
6272
|
+
"utf8"
|
|
6273
|
+
);
|
|
6274
|
+
}
|
|
6275
|
+
if (artifacts.readme) {
|
|
6276
|
+
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
|
|
6277
|
+
}
|
|
6278
|
+
if (artifacts.manifest) {
|
|
6279
|
+
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
6280
|
+
}
|
|
6281
|
+
if (monolithJson !== null) {
|
|
6282
|
+
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
|
|
6283
|
+
}
|
|
6284
|
+
if (monolithMarkdown !== null) {
|
|
6285
|
+
fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
6286
|
+
}
|
|
6287
|
+
writeOwnershipMarker(tmpDir, markerPayload(tmpDir, "complete", snapshotId, createdAt));
|
|
6288
|
+
swapIntoPlace(tmpDir, outDir);
|
|
6289
|
+
stagingStarted = false;
|
|
6290
|
+
const warningsBeforeHook = manifest.warnings.length;
|
|
6291
|
+
runAfterGenerate(config, manifest);
|
|
6292
|
+
if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
|
|
6293
|
+
writeManifestAtomically(path10__namespace.join(outDir, "manifest.json"), manifest);
|
|
6294
|
+
}
|
|
6295
|
+
writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
|
|
6296
|
+
return manifest;
|
|
6297
|
+
} catch (err) {
|
|
6298
|
+
if (stagingStarted) {
|
|
6299
|
+
try {
|
|
6300
|
+
fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
|
|
6301
|
+
} catch {
|
|
6302
|
+
}
|
|
6303
|
+
}
|
|
6304
|
+
throw err;
|
|
6305
|
+
} finally {
|
|
6306
|
+
releaseLock();
|
|
5109
6307
|
}
|
|
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
6308
|
}
|
|
5140
6309
|
async function generateSnapshotAsync(configInput, options) {
|
|
5141
6310
|
return generateSnapshot(configInput, options);
|
|
@@ -5162,6 +6331,7 @@ exports.detectLanguage = detectLanguage;
|
|
|
5162
6331
|
exports.detectPreset = detectPreset;
|
|
5163
6332
|
exports.explainPath = explainPath;
|
|
5164
6333
|
exports.fenceForContent = fenceForContent;
|
|
6334
|
+
exports.fenceForText = fenceForText;
|
|
5165
6335
|
exports.generateProjectTree = generateProjectTree;
|
|
5166
6336
|
exports.generateSnapshot = generateSnapshot;
|
|
5167
6337
|
exports.generateSnapshotAsync = generateSnapshotAsync;
|