@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
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as crypto from 'crypto';
|
|
2
2
|
import * as fs9 from 'fs';
|
|
3
|
+
import * as os from 'os';
|
|
3
4
|
import * as path10 from 'path';
|
|
4
5
|
import ts from 'typescript';
|
|
5
6
|
import { pathToFileURL } from 'url';
|
|
@@ -547,14 +548,42 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
547
548
|
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
548
549
|
return Boolean(modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword));
|
|
549
550
|
};
|
|
550
|
-
const
|
|
551
|
+
const rendersJsx = (node) => {
|
|
552
|
+
const pending = [node];
|
|
553
|
+
while (pending.length > 0) {
|
|
554
|
+
const current = pending.pop();
|
|
555
|
+
if (ts.isJsxElement(current) || ts.isJsxSelfClosingElement(current) || ts.isJsxFragment(current)) {
|
|
556
|
+
return true;
|
|
557
|
+
}
|
|
558
|
+
if (ts.isCallExpression(current)) {
|
|
559
|
+
const callee = current.expression;
|
|
560
|
+
const calleeName = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : "";
|
|
561
|
+
if (calleeName === "createElement") return true;
|
|
562
|
+
}
|
|
563
|
+
ts.forEachChild(current, (child) => {
|
|
564
|
+
pending.push(child);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
return false;
|
|
568
|
+
};
|
|
569
|
+
const REACT_BASE_CLASS = /(?:^|\.)(?:PureComponent|Component)$/;
|
|
570
|
+
const extendsReactBase = (node) => {
|
|
571
|
+
const heritage = (node.heritageClauses ?? []).find(
|
|
572
|
+
(clause) => clause.token === ts.SyntaxKind.ExtendsKeyword
|
|
573
|
+
);
|
|
574
|
+
const expression = heritage?.types[0]?.expression;
|
|
575
|
+
if (!expression) return false;
|
|
576
|
+
const text2 = ts.isIdentifier(expression) ? expression.text : expression.getText(sourceFile);
|
|
577
|
+
return REACT_BASE_CLASS.test(text2);
|
|
578
|
+
};
|
|
579
|
+
const recordFunctionLike = (name, exported, node) => {
|
|
551
580
|
summary.functions.push(name);
|
|
552
581
|
if (exported) {
|
|
553
582
|
summary.exports.push(name);
|
|
554
583
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
555
584
|
}
|
|
556
585
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
557
|
-
if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
|
|
586
|
+
if (/^[A-Z]/.test(name) && rendersJsx(node)) summary.reactComponents.push(name);
|
|
558
587
|
};
|
|
559
588
|
const stack = [sourceFile];
|
|
560
589
|
while (stack.length > 0) {
|
|
@@ -583,7 +612,7 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
583
612
|
if (ts.isFunctionDeclaration(node)) {
|
|
584
613
|
const exported = isExported(node);
|
|
585
614
|
if (node.name) {
|
|
586
|
-
recordFunctionLike(node.name.text, exported);
|
|
615
|
+
recordFunctionLike(node.name.text, exported, node);
|
|
587
616
|
if (isDefault(node)) summary.exports.push("default");
|
|
588
617
|
} else if (isDefault(node)) {
|
|
589
618
|
summary.exports.push("default");
|
|
@@ -594,7 +623,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
594
623
|
summary.classes.push(node.name.text);
|
|
595
624
|
if (isExported(node)) summary.exports.push(node.name.text);
|
|
596
625
|
if (isDefault(node)) summary.exports.push("default");
|
|
597
|
-
if (/^[A-Z]/.test(node.name.text))
|
|
626
|
+
if (/^[A-Z]/.test(node.name.text) && (extendsReactBase(node) || rendersJsx(node))) {
|
|
627
|
+
summary.reactComponents.push(node.name.text);
|
|
628
|
+
}
|
|
598
629
|
} else if (isDefault(node)) {
|
|
599
630
|
summary.exports.push("default");
|
|
600
631
|
}
|
|
@@ -622,7 +653,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
622
653
|
if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
|
|
623
654
|
}
|
|
624
655
|
if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
|
|
625
|
-
if (/^[A-Z]/.test(name)
|
|
656
|
+
if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
|
|
657
|
+
summary.reactComponents.push(name);
|
|
658
|
+
}
|
|
626
659
|
}
|
|
627
660
|
}
|
|
628
661
|
const children = [];
|
|
@@ -1676,6 +1709,416 @@ function toDetected(preset) {
|
|
|
1676
1709
|
function detectPreset(cwd, forced) {
|
|
1677
1710
|
return detectPresetFromContext(buildPresetContext(cwd), forced);
|
|
1678
1711
|
}
|
|
1712
|
+
|
|
1713
|
+
// src/security/patterns.ts
|
|
1714
|
+
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)";
|
|
1715
|
+
var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
|
|
1716
|
+
var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|process\\.env|Buffer|Record|object)\\b)";
|
|
1717
|
+
var UNQUOTED_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{1,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
1718
|
+
var UNQUOTED_AMBIGUOUS_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{8,512})(?![A-Za-z0-9_+=/.-]|\\()";
|
|
1719
|
+
var UNQUOTED_STRONG_NAME = `(?<!id[-_])${STRONG_CREDENTIAL_NAME}`;
|
|
1720
|
+
var DEFAULT_SECRET_PATTERNS = [
|
|
1721
|
+
{
|
|
1722
|
+
rule: "private-key-header",
|
|
1723
|
+
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
rule: "jwt-token",
|
|
1727
|
+
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
1728
|
+
},
|
|
1729
|
+
{
|
|
1730
|
+
rule: "aws-access-key-id",
|
|
1731
|
+
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
1732
|
+
},
|
|
1733
|
+
{
|
|
1734
|
+
// The name is no longer limited to the three snake_case spellings: an
|
|
1735
|
+
// `awsSecret` / `secretAccessKey` property leaked verbatim because only the
|
|
1736
|
+
// literal words matched. The value is masked alone, so the key survives.
|
|
1737
|
+
rule: "aws-secret-access-key",
|
|
1738
|
+
pattern: new RegExp(
|
|
1739
|
+
`${STRONG_CREDENTIAL_NAME}["']?\\s{0,8}[:=]\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
|
|
1740
|
+
"i"
|
|
1741
|
+
),
|
|
1742
|
+
valueGroup: 1
|
|
1743
|
+
},
|
|
1744
|
+
{
|
|
1745
|
+
// A 40-character base64 value with no key name in sight. A key-independent
|
|
1746
|
+
// rule is the only thing that catches `new AWS.SecretKey(…)` arguments and
|
|
1747
|
+
// pasted `~/.aws/credentials` fragments, but a bare length test also matches
|
|
1748
|
+
// every 40-char git SHA-1, so the value must mix case and digits and must
|
|
1749
|
+
// not be pure hex.
|
|
1750
|
+
rule: "aws-secret-access-key-value",
|
|
1751
|
+
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}/
|
|
1752
|
+
},
|
|
1753
|
+
{
|
|
1754
|
+
rule: "github-token",
|
|
1755
|
+
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
1756
|
+
},
|
|
1757
|
+
{
|
|
1758
|
+
// `xoxc-`/`xoxd-` are the user and refresh tokens, `xoxb`/`xoxp` the bot and
|
|
1759
|
+
// user tokens; the whole Slack family shares one prefix shape.
|
|
1760
|
+
rule: "slack-token",
|
|
1761
|
+
pattern: /xox[baprsdc]-[0-9A-Za-z-]{10,250}/
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
// Prefix-only vendors. Each prefix below is documented, globally unique and
|
|
1765
|
+
// cannot collide with an ordinary identifier, which is what makes a bare
|
|
1766
|
+
// prefix rule cheap and safe: `glpat-` (GitLab), `hvs.`/`s.` (Vault service
|
|
1767
|
+
// and batch tokens), `whsec_` (Stripe webhook secret), `dckr_pat_` (Docker
|
|
1768
|
+
// Hub), `hf_` (HuggingFace), `ya29.` (Google OAuth access token).
|
|
1769
|
+
rule: "known-vendor-token-prefix",
|
|
1770
|
+
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})/
|
|
1771
|
+
},
|
|
1772
|
+
{
|
|
1773
|
+
rule: "slack-webhook-url",
|
|
1774
|
+
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
rule: "stripe-secret-key",
|
|
1778
|
+
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
rule: "google-api-key",
|
|
1782
|
+
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
1783
|
+
},
|
|
1784
|
+
{
|
|
1785
|
+
rule: "google-oauth-client-secret",
|
|
1786
|
+
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
1787
|
+
},
|
|
1788
|
+
{
|
|
1789
|
+
rule: "openai-anthropic-key",
|
|
1790
|
+
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
1791
|
+
},
|
|
1792
|
+
{
|
|
1793
|
+
rule: "npm-token",
|
|
1794
|
+
pattern: /npm_[A-Za-z0-9]{36}/
|
|
1795
|
+
},
|
|
1796
|
+
{
|
|
1797
|
+
rule: "pypi-token",
|
|
1798
|
+
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
1799
|
+
},
|
|
1800
|
+
{
|
|
1801
|
+
rule: "sendgrid-key",
|
|
1802
|
+
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
1803
|
+
},
|
|
1804
|
+
{
|
|
1805
|
+
rule: "twilio-api-key",
|
|
1806
|
+
pattern: /SK[0-9a-fA-F]{32}/
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
rule: "digitalocean-token",
|
|
1810
|
+
pattern: /dop_v1_[0-9a-f]{64}/
|
|
1811
|
+
},
|
|
1812
|
+
{
|
|
1813
|
+
rule: "azure-storage-account-key",
|
|
1814
|
+
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
1815
|
+
},
|
|
1816
|
+
{
|
|
1817
|
+
// Scheme-less userinfo: `//user:password@host`.
|
|
1818
|
+
//
|
|
1819
|
+
// Enumerating schemes is a losing game — `mysqlx`, `svn`, `telnet`, `imap`,
|
|
1820
|
+
// `pop3`, `nntp`, `kafka`, `elasticsearch` each leaked a password while the
|
|
1821
|
+
// list grew by hand. This rule matches the shape instead, so any scheme
|
|
1822
|
+
// (including one invented tomorrow) is covered, provided the `//` is followed
|
|
1823
|
+
// immediately by the userinfo: `// see the docs` and a bare reminder comment
|
|
1824
|
+
// never match, because a space or the comment wording breaks the shape.
|
|
1825
|
+
rule: "uri-credentials",
|
|
1826
|
+
pattern: /\/\/[^\s:/@"'`;()<>,]{1,128}:[^\s:/@"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
|
|
1827
|
+
},
|
|
1828
|
+
{
|
|
1829
|
+
// The host tail stops at quotes, `;`, `)`, `<`, `>` and `,`: an unbounded
|
|
1830
|
+
// `[^\s/]` used to swallow the closing quote and the statement separator of
|
|
1831
|
+
// the string the URL was written in.
|
|
1832
|
+
rule: "database-uri-credentials",
|
|
1833
|
+
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}/
|
|
1834
|
+
},
|
|
1835
|
+
{
|
|
1836
|
+
rule: "basic-auth-url",
|
|
1837
|
+
pattern: /(?:https?|ftp|sftp|ssh|git|smtp|smtps|ldap|ldaps):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/i
|
|
1838
|
+
},
|
|
1839
|
+
{
|
|
1840
|
+
// Quoted credentials in any common key spelling. The value may be as short
|
|
1841
|
+
// as one character: `password: "hunter2"` is a credential whatever its
|
|
1842
|
+
// entropy, and a length floor was exactly how it survived into `parts`.
|
|
1843
|
+
//
|
|
1844
|
+
// The key is either bare (`dbPassword:`) or wholly quoted (`"dbPassword":`).
|
|
1845
|
+
// There is deliberately no optional quote *before* the separator: that let a
|
|
1846
|
+
// previous string's closing quote stand in for the key's, so
|
|
1847
|
+
// ``${pass ? " PASS" : " FAIL"}`` had `" FAIL"` masked as if `PASS` were
|
|
1848
|
+
// the key name.
|
|
1849
|
+
rule: "credential-assignment",
|
|
1850
|
+
pattern: new RegExp(
|
|
1851
|
+
`(?:["']${STRONG_CREDENTIAL_NAME}["']|${STRONG_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{1,512})\\1`,
|
|
1852
|
+
"i"
|
|
1853
|
+
),
|
|
1854
|
+
valueGroup: 2
|
|
1855
|
+
},
|
|
1856
|
+
{
|
|
1857
|
+
// `pass`, `id-token` and `credentials` are ambiguous on their own, so they
|
|
1858
|
+
// are accepted only against a quoted value long enough to be a credential.
|
|
1859
|
+
rule: "credential-assignment-weak",
|
|
1860
|
+
pattern: new RegExp(
|
|
1861
|
+
`(?:["']${WEAK_CREDENTIAL_NAME}["']|${WEAK_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{8,512})\\1`,
|
|
1862
|
+
"i"
|
|
1863
|
+
),
|
|
1864
|
+
valueGroup: 2
|
|
1865
|
+
},
|
|
1866
|
+
{
|
|
1867
|
+
// The unquoted twin for YAML, docker-compose and `.env`: those formats never
|
|
1868
|
+
// quote their values, so the rules above cannot see them. `valueGroup` keeps
|
|
1869
|
+
// `POSTGRES_PASSWORD:` in the output.
|
|
1870
|
+
rule: "credential-assignment-unquoted",
|
|
1871
|
+
pattern: new RegExp(
|
|
1872
|
+
`${UNQUOTED_STRONG_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
|
|
1873
|
+
"i"
|
|
1874
|
+
),
|
|
1875
|
+
valueGroup: 1
|
|
1876
|
+
},
|
|
1877
|
+
{
|
|
1878
|
+
// `pass: hunter2hunter2` and `credentials: hunter2hunter2`: the ambiguous
|
|
1879
|
+
// names, unquoted, with the length and non-digit floor that keeps
|
|
1880
|
+
// `pass: 3` / `credentials: none` readable.
|
|
1881
|
+
rule: "credential-assignment-unquoted-weak",
|
|
1882
|
+
pattern: new RegExp(
|
|
1883
|
+
`${WEAK_CREDENTIAL_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
|
|
1884
|
+
"i"
|
|
1885
|
+
),
|
|
1886
|
+
valueGroup: 1
|
|
1887
|
+
},
|
|
1888
|
+
{
|
|
1889
|
+
// `Authorization: Bearer <token>` and `Authorization: Basic <base64>`.
|
|
1890
|
+
//
|
|
1891
|
+
// The scheme is consumed by a lookbehind so the match is the token alone and
|
|
1892
|
+
// needs no `valueGroup`. A lone `Bearer`/`Basic`, and a word after it that
|
|
1893
|
+
// cannot be a credential (`Bearer authentication`), stay readable: the value
|
|
1894
|
+
// must contain a digit, be at least 32 chars, or carry base64 `=` padding.
|
|
1895
|
+
rule: "authorization-header",
|
|
1896
|
+
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_.+/=-])/
|
|
1897
|
+
},
|
|
1898
|
+
{
|
|
1899
|
+
// Broader names than before (`apiKey`, `clientSecret`, `privateKey` were
|
|
1900
|
+
// invisible to it) and value-only masking. Kept alongside the two rules
|
|
1901
|
+
// above because it also matches a *quoted key* followed by an unquoted
|
|
1902
|
+
// value, e.g. `"client_secret": abc123`.
|
|
1903
|
+
rule: "generic-secret-assignment",
|
|
1904
|
+
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["']([^"'\s]{8,256})["']/i,
|
|
1905
|
+
valueGroup: 1
|
|
1906
|
+
},
|
|
1907
|
+
{
|
|
1908
|
+
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
1909
|
+
// was therefore invisible to the rule above. Anchored to the line start and
|
|
1910
|
+
// limited to secret-looking names to stay at zero false positives.
|
|
1911
|
+
//
|
|
1912
|
+
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
1913
|
+
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
1914
|
+
// every offset of a long identifier, which made this rule quadratic
|
|
1915
|
+
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
1916
|
+
// The value is matched on its own — `=\s*(value)` — and masked alone.
|
|
1917
|
+
// Anchoring the value to end-of-line (`[^\s#]{6,512}$`) meant ANY trailing
|
|
1918
|
+
// comment or space disabled the rule, so `TOKEN=secret # rotate` and
|
|
1919
|
+
// `TOKEN=secret ` were emitted in clear; a CRLF file disabled it wholesale.
|
|
1920
|
+
rule: "env-secret-assignment",
|
|
1921
|
+
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,
|
|
1922
|
+
valueGroup: 1
|
|
1923
|
+
}
|
|
1924
|
+
];
|
|
1925
|
+
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
1926
|
+
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
1927
|
+
|
|
1928
|
+
// src/security/redact.ts
|
|
1929
|
+
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
1930
|
+
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
1931
|
+
function describeValue(value) {
|
|
1932
|
+
if (value === null) return "null";
|
|
1933
|
+
if (Array.isArray(value)) return "an array";
|
|
1934
|
+
if (value instanceof RegExp) return value.toString();
|
|
1935
|
+
if (typeof value === "string") return `the string ${JSON.stringify(value)}`;
|
|
1936
|
+
return `a ${typeof value}`;
|
|
1937
|
+
}
|
|
1938
|
+
function compilePattern(rule, pattern, valueGroup) {
|
|
1939
|
+
if (pattern instanceof RegExp) {
|
|
1940
|
+
const flags = new Set(pattern.flags.replace(/[gy]/g, "").split(""));
|
|
1941
|
+
if (valueGroup !== void 0) flags.add("d");
|
|
1942
|
+
return {
|
|
1943
|
+
rule,
|
|
1944
|
+
pattern: new RegExp(pattern.source, [...flags].join("")),
|
|
1945
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
if (typeof pattern === "string") {
|
|
1949
|
+
try {
|
|
1950
|
+
return {
|
|
1951
|
+
rule,
|
|
1952
|
+
pattern: new RegExp(pattern, valueGroup === void 0 ? "i" : "id"),
|
|
1953
|
+
...valueGroup === void 0 ? {} : { valueGroup }
|
|
1954
|
+
};
|
|
1955
|
+
} catch (err) {
|
|
1956
|
+
throw new Error(
|
|
1957
|
+
`Invalid regular expression for secret rule "${rule}": ${err.message}`
|
|
1958
|
+
);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
throw new Error(
|
|
1962
|
+
`Secret rule "${rule}" has a pattern that is neither a RegExp nor a string (received ${describeValue(pattern)}).`
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
function compileSecretRules(rules) {
|
|
1966
|
+
if (!Array.isArray(rules)) {
|
|
1967
|
+
throw new Error(
|
|
1968
|
+
`secrets.customPatterns must be an array of secret rules (received ${describeValue(rules)}).`
|
|
1969
|
+
);
|
|
1970
|
+
}
|
|
1971
|
+
let generated = 0;
|
|
1972
|
+
return rules.map((entry, index) => {
|
|
1973
|
+
const where = `secrets.customPatterns[${index}]`;
|
|
1974
|
+
if (entry instanceof RegExp) {
|
|
1975
|
+
generated += 1;
|
|
1976
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1977
|
+
}
|
|
1978
|
+
if (typeof entry === "string") {
|
|
1979
|
+
generated += 1;
|
|
1980
|
+
return compilePattern(`custom-${generated}`, entry, void 0);
|
|
1981
|
+
}
|
|
1982
|
+
if (typeof entry === "object" && entry !== null) {
|
|
1983
|
+
const candidate = entry;
|
|
1984
|
+
if (typeof candidate.rule !== "string" || candidate.rule.trim() === "") {
|
|
1985
|
+
throw new Error(
|
|
1986
|
+
`${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.`
|
|
1987
|
+
);
|
|
1988
|
+
}
|
|
1989
|
+
const valueGroup = candidate.valueGroup;
|
|
1990
|
+
if (valueGroup !== void 0) {
|
|
1991
|
+
if (typeof valueGroup !== "number" || !Number.isInteger(valueGroup) || valueGroup < 0) {
|
|
1992
|
+
throw new Error(
|
|
1993
|
+
`${where}.valueGroup must be a non-negative integer capture-group index (received ${describeValue(valueGroup)}).`
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
if (candidate.pattern === void 0) {
|
|
1998
|
+
throw new Error(
|
|
1999
|
+
`${where} (rule "${candidate.rule}") is missing \`pattern\`. Expected { rule, pattern: RegExp | string }.`
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
return compilePattern(
|
|
2003
|
+
candidate.rule,
|
|
2004
|
+
candidate.pattern,
|
|
2005
|
+
valueGroup
|
|
2006
|
+
);
|
|
2007
|
+
}
|
|
2008
|
+
throw new Error(
|
|
2009
|
+
`${where} must be a RegExp, a string, or { rule, pattern } (received ${describeValue(entry)}).`
|
|
2010
|
+
);
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
function toGlobal(rule) {
|
|
2014
|
+
const flags = new Set(rule.pattern.flags.replace(/[gy]/g, "").split(""));
|
|
2015
|
+
flags.add("g");
|
|
2016
|
+
if (rule.valueGroup !== void 0) flags.add("d");
|
|
2017
|
+
return new RegExp(rule.pattern.source, [...flags].join(""));
|
|
2018
|
+
}
|
|
2019
|
+
var LINE_BREAK = /(\r\n|\n|\r)/;
|
|
2020
|
+
function scanAndRedactSecrets(content, rules, filePath) {
|
|
2021
|
+
const findings = [];
|
|
2022
|
+
let redacted = content;
|
|
2023
|
+
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
2024
|
+
blockPattern.lastIndex = 0;
|
|
2025
|
+
let match;
|
|
2026
|
+
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
2027
|
+
if (match[0].length === 0) {
|
|
2028
|
+
blockPattern.lastIndex += 1;
|
|
2029
|
+
continue;
|
|
2030
|
+
}
|
|
2031
|
+
const line = redacted.slice(0, match.index).split("\n").length;
|
|
2032
|
+
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
2033
|
+
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
2034
|
+
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
2035
|
+
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
2036
|
+
}
|
|
2037
|
+
}
|
|
2038
|
+
const compiled = rules.map((rule) => ({
|
|
2039
|
+
rule: rule.rule,
|
|
2040
|
+
pattern: toGlobal(rule),
|
|
2041
|
+
...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
|
|
2042
|
+
}));
|
|
2043
|
+
const segments = redacted.split(LINE_BREAK);
|
|
2044
|
+
let lineNumber = 1;
|
|
2045
|
+
for (let index = 0; index < segments.length; index += 2) {
|
|
2046
|
+
const line = segments[index];
|
|
2047
|
+
if (line.length > 0) {
|
|
2048
|
+
const spans = [];
|
|
2049
|
+
const matchedRules = /* @__PURE__ */ new Set();
|
|
2050
|
+
for (const { rule, pattern, valueGroup } of compiled) {
|
|
2051
|
+
pattern.lastIndex = 0;
|
|
2052
|
+
let match;
|
|
2053
|
+
while ((match = pattern.exec(line)) !== null) {
|
|
2054
|
+
if (match[0].length === 0) {
|
|
2055
|
+
pattern.lastIndex += 1;
|
|
2056
|
+
continue;
|
|
2057
|
+
}
|
|
2058
|
+
const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
|
|
2059
|
+
spans.push(
|
|
2060
|
+
group ? { start: group[0], end: group[1], rule } : { start: match.index, end: match.index + match[0].length, rule }
|
|
2061
|
+
);
|
|
2062
|
+
matchedRules.add(rule);
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
if (spans.length > 0) {
|
|
2066
|
+
for (const rule of matchedRules) {
|
|
2067
|
+
findings.push({ rule, line: lineNumber, path: filePath });
|
|
2068
|
+
}
|
|
2069
|
+
segments[index] = applySpans(line, spans);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
if (index + 1 < segments.length) lineNumber += 1;
|
|
2073
|
+
}
|
|
2074
|
+
return { redacted: segments.join(""), findings };
|
|
2075
|
+
}
|
|
2076
|
+
var TOKEN_CHARS_LEFT = /[A-Za-z0-9_+/-]/;
|
|
2077
|
+
var TOKEN_CHARS_RIGHT = /[A-Za-z0-9_+/=-]/;
|
|
2078
|
+
function expandOverToken(line, start, end) {
|
|
2079
|
+
let left = start;
|
|
2080
|
+
let right = end;
|
|
2081
|
+
while (left > 0 && TOKEN_CHARS_LEFT.test(line[left - 1])) left -= 1;
|
|
2082
|
+
while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
|
|
2083
|
+
return { start: left, end: right };
|
|
2084
|
+
}
|
|
2085
|
+
function applySpans(line, spans) {
|
|
2086
|
+
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
2087
|
+
const merged = [];
|
|
2088
|
+
for (const span of ordered) {
|
|
2089
|
+
const clampedStart = Math.max(0, Math.min(span.start, line.length));
|
|
2090
|
+
const clampedEnd = Math.max(clampedStart, Math.min(span.end, line.length));
|
|
2091
|
+
const { start, end } = expandOverToken(line, clampedStart, clampedEnd);
|
|
2092
|
+
const last = merged[merged.length - 1];
|
|
2093
|
+
if (last && start <= last.end) last.end = Math.max(last.end, end);
|
|
2094
|
+
else merged.push({ start, end });
|
|
2095
|
+
}
|
|
2096
|
+
let out = "";
|
|
2097
|
+
let cursor = 0;
|
|
2098
|
+
for (const span of merged) {
|
|
2099
|
+
out += line.slice(cursor, span.start);
|
|
2100
|
+
out += REDACTION_MARKER;
|
|
2101
|
+
cursor = span.end;
|
|
2102
|
+
}
|
|
2103
|
+
out += line.slice(cursor);
|
|
2104
|
+
return out;
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// src/scanner/files.ts
|
|
2108
|
+
var PRESET_IGNORES = new Set(
|
|
2109
|
+
PRESETS.flatMap((preset) => preset.exclude ?? [])
|
|
2110
|
+
);
|
|
2111
|
+
function readLinkTarget(linkPath, fallback) {
|
|
2112
|
+
try {
|
|
2113
|
+
return fs9.readlinkSync(linkPath);
|
|
2114
|
+
} catch {
|
|
2115
|
+
return fallback;
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
function isInside(root, candidate) {
|
|
2119
|
+
const relative4 = path10.relative(root, candidate);
|
|
2120
|
+
return relative4 === "" || !relative4.startsWith(`..${path10.sep}`) && relative4 !== ".." && !path10.isAbsolute(relative4);
|
|
2121
|
+
}
|
|
1679
2122
|
function getGitInfo(cwd, fallbackBranch = "main") {
|
|
1680
2123
|
try {
|
|
1681
2124
|
const reported = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
|
|
@@ -1720,20 +2163,61 @@ var DEFAULT_IGNORED_FILES = [
|
|
|
1720
2163
|
"bun.lockb",
|
|
1721
2164
|
"composer.lock"
|
|
1722
2165
|
];
|
|
1723
|
-
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;
|
|
2166
|
+
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;
|
|
1724
2167
|
var SENSITIVE_PATH = /(?:^|\/)\.(?:aws|ssh|gnupg)\/|(?:^|\/)\.docker\/config\.json$|(?:^|\/)\.kube\/config$/i;
|
|
1725
2168
|
var SENSITIVE_EXT = /\.(?:pem|key|p12|pfx|jks|keystore|ppk|asc|kdbx|tfstate|tfvars)$/i;
|
|
1726
2169
|
var TEMPLATE_SUFFIX = /\.(?:example|sample|template|dist)(?=$|[.])/i;
|
|
2170
|
+
var BACKUP_SUFFIX = /(?:~|\.(?:bak|backup|old|orig|save|copy|swp|swo|tmp|part))$/i;
|
|
2171
|
+
function normaliseName(name) {
|
|
2172
|
+
let normalised = name.trim().replace(/[. ]+$/, "");
|
|
2173
|
+
let previous = "";
|
|
2174
|
+
while (previous !== normalised) {
|
|
2175
|
+
previous = normalised;
|
|
2176
|
+
normalised = normalised.replace(BACKUP_SUFFIX, "");
|
|
2177
|
+
}
|
|
2178
|
+
return normalised;
|
|
2179
|
+
}
|
|
1727
2180
|
var matchesSensitiveList = (posixPath, base) => SENSITIVE_BASENAME.test(base) || SENSITIVE_PATH.test(posixPath) || SENSITIVE_EXT.test(base);
|
|
1728
2181
|
function isSensitivePath(relativePath) {
|
|
1729
2182
|
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1730
|
-
const
|
|
2183
|
+
const raw = posixPath.split("/").pop() ?? posixPath;
|
|
2184
|
+
const base = normaliseName(raw);
|
|
1731
2185
|
if (TEMPLATE_SUFFIX.test(base)) {
|
|
1732
2186
|
const stripped = base.replace(new RegExp(TEMPLATE_SUFFIX.source, "gi"), "");
|
|
1733
2187
|
if (matchesSensitiveList(posixPath, stripped)) return false;
|
|
1734
2188
|
}
|
|
1735
2189
|
return matchesSensitiveList(posixPath, base);
|
|
1736
2190
|
}
|
|
2191
|
+
function findSecretInPath(relativePath) {
|
|
2192
|
+
const posixPath = relativePath.replace(/\\/g, "/");
|
|
2193
|
+
const spans = [];
|
|
2194
|
+
const rules = [];
|
|
2195
|
+
for (const { rule, pattern } of DEFAULT_SECRET_PATTERNS) {
|
|
2196
|
+
const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
|
|
2197
|
+
let match;
|
|
2198
|
+
while ((match = scan.exec(posixPath)) !== null) {
|
|
2199
|
+
if (match[0].length === 0) {
|
|
2200
|
+
scan.lastIndex += 1;
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
spans.push({ start: match.index, end: match.index + match[0].length, rule });
|
|
2204
|
+
if (!rules.includes(rule)) rules.push(rule);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
if (spans.length === 0) return null;
|
|
2208
|
+
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
2209
|
+
let out = "";
|
|
2210
|
+
let cursor = 0;
|
|
2211
|
+
for (const span of spans) {
|
|
2212
|
+
if (span.end <= cursor) continue;
|
|
2213
|
+
const start = Math.max(span.start, cursor);
|
|
2214
|
+
out += posixPath.slice(cursor, start);
|
|
2215
|
+
out += REDACTION_MARKER;
|
|
2216
|
+
cursor = Math.min(span.end, posixPath.length);
|
|
2217
|
+
}
|
|
2218
|
+
out += posixPath.slice(cursor);
|
|
2219
|
+
return { rules, masked: out };
|
|
2220
|
+
}
|
|
1737
2221
|
function globToRegExp(glob) {
|
|
1738
2222
|
const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1739
2223
|
const anchored = normalized.startsWith("/");
|
|
@@ -1854,49 +2338,177 @@ function defaultIgnoreReason(relativePath) {
|
|
|
1854
2338
|
}
|
|
1855
2339
|
return null;
|
|
1856
2340
|
}
|
|
1857
|
-
function
|
|
1858
|
-
const includePatterns = options.includePatterns ?? [];
|
|
1859
|
-
const excludePatterns = options.excludePatterns ?? [];
|
|
1860
|
-
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
1861
|
-
let candidates;
|
|
1862
|
-
let walkSkips = [];
|
|
2341
|
+
function listIgnoredPaths(cwd) {
|
|
1863
2342
|
try {
|
|
1864
2343
|
const stdout = childProcess.execSync(
|
|
1865
|
-
"git ls-files -z --
|
|
2344
|
+
"git ls-files -z --others --ignored --exclude-standard --directory",
|
|
1866
2345
|
{
|
|
1867
2346
|
cwd,
|
|
1868
2347
|
stdio: ["ignore", "pipe", "ignore"],
|
|
1869
2348
|
encoding: "utf8",
|
|
1870
|
-
maxBuffer:
|
|
2349
|
+
maxBuffer: 32 * 1024 * 1024
|
|
1871
2350
|
}
|
|
1872
2351
|
);
|
|
1873
|
-
|
|
2352
|
+
return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
1874
2353
|
} catch {
|
|
1875
|
-
|
|
1876
|
-
candidates = walked.files;
|
|
1877
|
-
walkSkips = walked.skipped;
|
|
2354
|
+
return [];
|
|
1878
2355
|
}
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
2356
|
+
}
|
|
2357
|
+
var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
|
|
2358
|
+
function isToolOwnedDirName(name) {
|
|
2359
|
+
return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
|
|
2360
|
+
}
|
|
2361
|
+
function hasSnapshotMarker(target) {
|
|
2362
|
+
try {
|
|
2363
|
+
return fs9.statSync(target).isDirectory() && fs9.existsSync(path10.join(target, ".codebase-snapshot"));
|
|
2364
|
+
} catch {
|
|
2365
|
+
return false;
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
function makeToolOwnedDecider(cwd) {
|
|
2369
|
+
const markers = /* @__PURE__ */ new Map();
|
|
2370
|
+
return (relativePath) => {
|
|
2371
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
2372
|
+
for (let depth = 1; depth < parts.length; depth += 1) {
|
|
2373
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
2374
|
+
if (depth === 1 && isToolOwnedDirName(prefix)) return true;
|
|
2375
|
+
let hasMarker = markers.get(prefix);
|
|
2376
|
+
if (hasMarker === void 0) {
|
|
2377
|
+
hasMarker = fs9.existsSync(path10.join(cwd, prefix, ".codebase-snapshot"));
|
|
2378
|
+
markers.set(prefix, hasMarker);
|
|
2379
|
+
}
|
|
2380
|
+
if (hasMarker) return true;
|
|
2381
|
+
}
|
|
2382
|
+
return false;
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
function makeLinkProbe() {
|
|
2386
|
+
const cache = /* @__PURE__ */ new Map();
|
|
2387
|
+
return (absolutePath) => {
|
|
2388
|
+
let known = cache.get(absolutePath);
|
|
2389
|
+
if (known === void 0) {
|
|
2390
|
+
try {
|
|
2391
|
+
known = fs9.lstatSync(absolutePath).isSymbolicLink();
|
|
2392
|
+
} catch {
|
|
2393
|
+
known = false;
|
|
2394
|
+
}
|
|
2395
|
+
cache.set(absolutePath, known);
|
|
2396
|
+
}
|
|
2397
|
+
return known;
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
function dropLinkedDirectories(cwd, candidates, skipped) {
|
|
2401
|
+
const isLink = makeLinkProbe();
|
|
2402
|
+
let rootReal = cwd;
|
|
2403
|
+
try {
|
|
2404
|
+
rootReal = fs9.realpathSync(cwd);
|
|
2405
|
+
} catch {
|
|
2406
|
+
}
|
|
2407
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2408
|
+
const kept = [];
|
|
2409
|
+
for (const candidate of candidates) {
|
|
2410
|
+
const parts = candidate.split("/");
|
|
2411
|
+
let linkedDir = null;
|
|
2412
|
+
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
2413
|
+
const prefix = parts.slice(0, depth).join("/");
|
|
2414
|
+
if (isLink(path10.join(cwd, prefix))) {
|
|
2415
|
+
linkedDir = prefix;
|
|
2416
|
+
break;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
if (linkedDir === null) {
|
|
2420
|
+
kept.push(candidate);
|
|
2421
|
+
continue;
|
|
2422
|
+
}
|
|
2423
|
+
if (reported.has(linkedDir)) continue;
|
|
2424
|
+
reported.add(linkedDir);
|
|
2425
|
+
const absolute = path10.join(cwd, linkedDir);
|
|
2426
|
+
let targetReal = absolute;
|
|
2427
|
+
try {
|
|
2428
|
+
targetReal = fs9.realpathSync(absolute);
|
|
2429
|
+
} catch {
|
|
2430
|
+
}
|
|
2431
|
+
const target = readLinkTarget(absolute, targetReal);
|
|
2432
|
+
const what = linkedDir === candidate ? "symbolic link" : "directory link";
|
|
2433
|
+
skipped.push(
|
|
2434
|
+
isInside(rootReal, targetReal) ? {
|
|
2435
|
+
path: linkedDir,
|
|
2436
|
+
reason: "symlink",
|
|
2437
|
+
detail: `${what} not followed (target: ${target})`
|
|
2438
|
+
} : {
|
|
2439
|
+
path: linkedDir,
|
|
2440
|
+
reason: "outside-root",
|
|
2441
|
+
detail: `link target resolves outside the scanned root (target: ${target}); not followed`
|
|
2442
|
+
}
|
|
2443
|
+
);
|
|
2444
|
+
}
|
|
2445
|
+
return kept;
|
|
2446
|
+
}
|
|
2447
|
+
function collectFiles(cwd, options = {}) {
|
|
2448
|
+
const includePatterns = options.includePatterns ?? [];
|
|
2449
|
+
const excludePatterns = options.excludePatterns ?? [];
|
|
2450
|
+
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
2451
|
+
const isToolOwned = makeToolOwnedDecider(cwd);
|
|
2452
|
+
let candidates;
|
|
2453
|
+
const skipped = [];
|
|
2454
|
+
try {
|
|
2455
|
+
const stdout = childProcess.execSync(
|
|
2456
|
+
"git ls-files -z --cached --others --exclude-standard",
|
|
2457
|
+
{
|
|
2458
|
+
cwd,
|
|
2459
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
2460
|
+
encoding: "utf8",
|
|
2461
|
+
maxBuffer: 50 * 1024 * 1024
|
|
2462
|
+
}
|
|
2463
|
+
);
|
|
2464
|
+
candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
|
|
2465
|
+
candidates = dropLinkedDirectories(cwd, candidates, skipped);
|
|
2466
|
+
const known = new Set(candidates);
|
|
2467
|
+
for (const ignoredPath of listIgnoredPaths(cwd)) {
|
|
2468
|
+
const trimmed = ignoredPath.replace(/\/+$/, "");
|
|
2469
|
+
if (known.has(trimmed) || known.has(ignoredPath)) continue;
|
|
2470
|
+
if (isToolOwned(trimmed)) continue;
|
|
2471
|
+
if (isSensitivePath(trimmed)) {
|
|
2472
|
+
skipped.push({
|
|
2473
|
+
path: trimmed,
|
|
2474
|
+
reason: "sensitive-file",
|
|
2475
|
+
detail: "credential-bearing file, ignored by .gitignore; enable includeSensitiveFiles to emit it"
|
|
2476
|
+
});
|
|
2477
|
+
} else {
|
|
2478
|
+
skipped.push({
|
|
2479
|
+
path: trimmed,
|
|
2480
|
+
reason: "gitignore",
|
|
2481
|
+
detail: ignoredPath.endsWith("/") ? "ignored by .gitignore (directory, not descended)" : "ignored by .gitignore"
|
|
2482
|
+
});
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
} catch {
|
|
2486
|
+
const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set(), true);
|
|
2487
|
+
candidates = walked.files;
|
|
2488
|
+
skipped.push(...walked.skipped);
|
|
2489
|
+
}
|
|
2490
|
+
const files = candidates.filter((file) => {
|
|
2491
|
+
if (isToolOwned(file)) return false;
|
|
2492
|
+
const ignored = defaultIgnoreReason(file);
|
|
2493
|
+
if (ignored) {
|
|
2494
|
+
skipped.push({ path: file, reason: "default-ignore", detail: ignored });
|
|
2495
|
+
return false;
|
|
2496
|
+
}
|
|
2497
|
+
if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesExclude(file, pattern))) {
|
|
2498
|
+
skipped.push({
|
|
2499
|
+
path: file,
|
|
2500
|
+
reason: "excluded",
|
|
2501
|
+
detail: `does not match any include pattern (${includePatterns.join(", ")})`
|
|
2502
|
+
});
|
|
2503
|
+
return false;
|
|
1893
2504
|
}
|
|
1894
2505
|
for (const pattern of excludePatterns) {
|
|
1895
2506
|
if (matchesExclude(file, pattern)) {
|
|
2507
|
+
const fromPreset = PRESET_IGNORES.has(pattern);
|
|
1896
2508
|
skipped.push({
|
|
1897
2509
|
path: file,
|
|
1898
|
-
reason: "excluded",
|
|
1899
|
-
detail: `matches exclude pattern "${pattern}"`
|
|
2510
|
+
reason: fromPreset ? "default-ignore" : "excluded",
|
|
2511
|
+
detail: fromPreset ? `preset ignore: "${pattern}"` : `matches exclude pattern "${pattern}"`
|
|
1900
2512
|
});
|
|
1901
2513
|
return false;
|
|
1902
2514
|
}
|
|
@@ -1909,11 +2521,20 @@ function collectFiles(cwd, options = {}) {
|
|
|
1909
2521
|
});
|
|
1910
2522
|
return false;
|
|
1911
2523
|
}
|
|
2524
|
+
const secret = findSecretInPath(file);
|
|
2525
|
+
if (secret) {
|
|
2526
|
+
skipped.push({
|
|
2527
|
+
path: secret.masked,
|
|
2528
|
+
reason: "sensitive-path",
|
|
2529
|
+
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`
|
|
2530
|
+
});
|
|
2531
|
+
return false;
|
|
2532
|
+
}
|
|
1912
2533
|
return true;
|
|
1913
2534
|
}).sort();
|
|
1914
2535
|
return { files, skipped };
|
|
1915
2536
|
}
|
|
1916
|
-
function walkDirectory(dir, baseDir, visited) {
|
|
2537
|
+
function walkDirectory(dir, baseDir, visited, isRoot = false) {
|
|
1917
2538
|
const results = { files: [], skipped: [] };
|
|
1918
2539
|
let realDir;
|
|
1919
2540
|
try {
|
|
@@ -1923,7 +2544,13 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1923
2544
|
}
|
|
1924
2545
|
if (visited.has(realDir)) return results;
|
|
1925
2546
|
visited.add(realDir);
|
|
1926
|
-
|
|
2547
|
+
let rootReal;
|
|
2548
|
+
try {
|
|
2549
|
+
rootReal = fs9.realpathSync(baseDir);
|
|
2550
|
+
} catch {
|
|
2551
|
+
rootReal = baseDir;
|
|
2552
|
+
}
|
|
2553
|
+
const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
|
|
1927
2554
|
let entries;
|
|
1928
2555
|
try {
|
|
1929
2556
|
entries = fs9.readdirSync(dir, { withFileTypes: true });
|
|
@@ -1932,30 +2559,79 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1932
2559
|
return results;
|
|
1933
2560
|
}
|
|
1934
2561
|
for (const entry of entries) {
|
|
1935
|
-
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) continue;
|
|
1936
2562
|
const fullPath = path10.join(dir, entry.name);
|
|
1937
2563
|
const relPath = path10.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
1938
|
-
if (
|
|
2564
|
+
if (isToolOwnedDirName(entry.name)) continue;
|
|
2565
|
+
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
|
|
2566
|
+
results.skipped.push({
|
|
2567
|
+
path: relPath,
|
|
2568
|
+
reason: "default-ignore",
|
|
2569
|
+
detail: `built-in ignore: directory "${entry.name}" (not descended)`
|
|
2570
|
+
});
|
|
2571
|
+
continue;
|
|
2572
|
+
}
|
|
2573
|
+
if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
|
|
2574
|
+
results.skipped.push({
|
|
2575
|
+
path: relPath,
|
|
2576
|
+
reason: "default-ignore",
|
|
2577
|
+
detail: "built-in ignore: filename"
|
|
2578
|
+
});
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
if (ignoredByGitignore(relPath)) {
|
|
2582
|
+
if (!hasSnapshotMarker(fullPath)) {
|
|
2583
|
+
results.skipped.push({
|
|
2584
|
+
path: relPath,
|
|
2585
|
+
reason: isSensitivePath(relPath) ? "sensitive-file" : "gitignore",
|
|
2586
|
+
detail: "ignored by .gitignore"
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
continue;
|
|
2590
|
+
}
|
|
1939
2591
|
let isDirectory = entry.isDirectory();
|
|
1940
2592
|
let isFile = entry.isFile();
|
|
1941
2593
|
if (entry.isSymbolicLink()) {
|
|
2594
|
+
let targetReal;
|
|
1942
2595
|
try {
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
2596
|
+
targetReal = fs9.realpathSync(fullPath);
|
|
2597
|
+
} catch {
|
|
2598
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
let stats;
|
|
2602
|
+
try {
|
|
2603
|
+
stats = fs9.statSync(fullPath);
|
|
2604
|
+
} catch {
|
|
2605
|
+
results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
|
|
2606
|
+
continue;
|
|
2607
|
+
}
|
|
2608
|
+
const linkTarget = readLinkTarget(fullPath, targetReal);
|
|
2609
|
+
if (!isInside(rootReal, targetReal)) {
|
|
2610
|
+
results.skipped.push({
|
|
2611
|
+
path: relPath,
|
|
2612
|
+
reason: "outside-root",
|
|
2613
|
+
detail: `link target resolves outside the scanned root (target: ${linkTarget}); not followed`
|
|
2614
|
+
});
|
|
2615
|
+
continue;
|
|
2616
|
+
}
|
|
2617
|
+
if (stats.isDirectory()) {
|
|
1950
2618
|
results.skipped.push({
|
|
1951
2619
|
path: relPath,
|
|
1952
2620
|
reason: "symlink",
|
|
1953
|
-
detail:
|
|
2621
|
+
detail: `directory link not followed (target: ${linkTarget})`
|
|
2622
|
+
});
|
|
2623
|
+
continue;
|
|
2624
|
+
}
|
|
2625
|
+
if (!stats.isFile()) {
|
|
2626
|
+
results.skipped.push({
|
|
2627
|
+
path: relPath,
|
|
2628
|
+
reason: "symlink",
|
|
2629
|
+
detail: `link to neither a file nor a directory (target: ${linkTarget})`
|
|
1954
2630
|
});
|
|
1955
|
-
} catch {
|
|
1956
|
-
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1957
2631
|
continue;
|
|
1958
2632
|
}
|
|
2633
|
+
isDirectory = false;
|
|
2634
|
+
isFile = true;
|
|
1959
2635
|
}
|
|
1960
2636
|
if (isDirectory) {
|
|
1961
2637
|
const nested = walkDirectory(fullPath, baseDir, visited);
|
|
@@ -1968,195 +2644,6 @@ function walkDirectory(dir, baseDir, visited) {
|
|
|
1968
2644
|
return results;
|
|
1969
2645
|
}
|
|
1970
2646
|
|
|
1971
|
-
// src/security/patterns.ts
|
|
1972
|
-
var DEFAULT_SECRET_PATTERNS = [
|
|
1973
|
-
{
|
|
1974
|
-
rule: "private-key-header",
|
|
1975
|
-
pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
|
|
1976
|
-
},
|
|
1977
|
-
{
|
|
1978
|
-
rule: "jwt-token",
|
|
1979
|
-
pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
|
|
1980
|
-
},
|
|
1981
|
-
{
|
|
1982
|
-
rule: "aws-access-key-id",
|
|
1983
|
-
pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
|
|
1984
|
-
},
|
|
1985
|
-
{
|
|
1986
|
-
rule: "aws-secret-access-key",
|
|
1987
|
-
pattern: /(?:aws_secret_access_key|aws_secret_key|secret_access_key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']?/i
|
|
1988
|
-
},
|
|
1989
|
-
{
|
|
1990
|
-
rule: "github-token",
|
|
1991
|
-
pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
|
|
1992
|
-
},
|
|
1993
|
-
{
|
|
1994
|
-
rule: "slack-token",
|
|
1995
|
-
pattern: /xox[baprs]-[0-9A-Za-z-]{10,250}/
|
|
1996
|
-
},
|
|
1997
|
-
{
|
|
1998
|
-
rule: "slack-webhook-url",
|
|
1999
|
-
pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
|
|
2000
|
-
},
|
|
2001
|
-
{
|
|
2002
|
-
rule: "stripe-secret-key",
|
|
2003
|
-
pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
|
|
2004
|
-
},
|
|
2005
|
-
{
|
|
2006
|
-
rule: "google-api-key",
|
|
2007
|
-
pattern: /AIza[0-9A-Za-z_-]{35}/
|
|
2008
|
-
},
|
|
2009
|
-
{
|
|
2010
|
-
rule: "google-oauth-client-secret",
|
|
2011
|
-
pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
|
|
2012
|
-
},
|
|
2013
|
-
{
|
|
2014
|
-
rule: "openai-anthropic-key",
|
|
2015
|
-
pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
|
|
2016
|
-
},
|
|
2017
|
-
{
|
|
2018
|
-
rule: "npm-token",
|
|
2019
|
-
pattern: /npm_[A-Za-z0-9]{36}/
|
|
2020
|
-
},
|
|
2021
|
-
{
|
|
2022
|
-
rule: "pypi-token",
|
|
2023
|
-
pattern: /pypi-[A-Za-z0-9_-]{50,180}/
|
|
2024
|
-
},
|
|
2025
|
-
{
|
|
2026
|
-
rule: "sendgrid-key",
|
|
2027
|
-
pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
|
|
2028
|
-
},
|
|
2029
|
-
{
|
|
2030
|
-
rule: "twilio-api-key",
|
|
2031
|
-
pattern: /SK[0-9a-fA-F]{32}/
|
|
2032
|
-
},
|
|
2033
|
-
{
|
|
2034
|
-
rule: "digitalocean-token",
|
|
2035
|
-
pattern: /dop_v1_[0-9a-f]{64}/
|
|
2036
|
-
},
|
|
2037
|
-
{
|
|
2038
|
-
rule: "azure-storage-account-key",
|
|
2039
|
-
pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
|
|
2040
|
-
},
|
|
2041
|
-
{
|
|
2042
|
-
rule: "database-uri-credentials",
|
|
2043
|
-
pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/
|
|
2044
|
-
},
|
|
2045
|
-
{
|
|
2046
|
-
rule: "basic-auth-url",
|
|
2047
|
-
pattern: /(?:https?|ftp|sftp|ssh|git):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/i
|
|
2048
|
-
},
|
|
2049
|
-
{
|
|
2050
|
-
rule: "generic-secret-assignment",
|
|
2051
|
-
pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["'][^"'\s]{8,256}["']/i
|
|
2052
|
-
},
|
|
2053
|
-
{
|
|
2054
|
-
// Covers `.env` / shell assignment syntax, which never quotes its values and
|
|
2055
|
-
// was therefore invisible to the rule above. Anchored to the line start and
|
|
2056
|
-
// limited to secret-looking names to stay at zero false positives.
|
|
2057
|
-
//
|
|
2058
|
-
// The name is matched with BOUNDED quantifiers on purpose: an unbounded
|
|
2059
|
-
// `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
|
|
2060
|
-
// every offset of a long identifier, which made this rule quadratic
|
|
2061
|
-
// (measured 5.6 s for a single 240 KB line before bounding).
|
|
2062
|
-
rule: "env-secret-assignment",
|
|
2063
|
-
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
|
|
2064
|
-
}
|
|
2065
|
-
];
|
|
2066
|
-
var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
|
|
2067
|
-
var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
|
|
2068
|
-
|
|
2069
|
-
// src/security/redact.ts
|
|
2070
|
-
var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
|
|
2071
|
-
var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
|
|
2072
|
-
function compileSecretRules(rules) {
|
|
2073
|
-
return rules.map((rule) => {
|
|
2074
|
-
if (rule.pattern instanceof RegExp) {
|
|
2075
|
-
return {
|
|
2076
|
-
rule: rule.rule,
|
|
2077
|
-
pattern: new RegExp(rule.pattern.source, rule.pattern.flags.replace(/[gy]/g, ""))
|
|
2078
|
-
};
|
|
2079
|
-
}
|
|
2080
|
-
if (typeof rule.pattern === "string") {
|
|
2081
|
-
try {
|
|
2082
|
-
return { rule: rule.rule, pattern: new RegExp(rule.pattern, "i") };
|
|
2083
|
-
} catch (err) {
|
|
2084
|
-
throw new Error(
|
|
2085
|
-
`Invalid regular expression for secret rule "${rule.rule}": ${err.message}`
|
|
2086
|
-
);
|
|
2087
|
-
}
|
|
2088
|
-
}
|
|
2089
|
-
throw new Error(
|
|
2090
|
-
`Secret rule "${String(rule.rule)}" has a pattern that is neither a RegExp nor a string.`
|
|
2091
|
-
);
|
|
2092
|
-
});
|
|
2093
|
-
}
|
|
2094
|
-
function toGlobal(pattern) {
|
|
2095
|
-
const flags = pattern.flags.replace(/[gy]/g, "");
|
|
2096
|
-
return new RegExp(pattern.source, `${flags}g`);
|
|
2097
|
-
}
|
|
2098
|
-
function scanAndRedactSecrets(content, rules, filePath) {
|
|
2099
|
-
const findings = [];
|
|
2100
|
-
let redacted = content;
|
|
2101
|
-
for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
|
|
2102
|
-
blockPattern.lastIndex = 0;
|
|
2103
|
-
let match;
|
|
2104
|
-
while ((match = blockPattern.exec(redacted)) !== null) {
|
|
2105
|
-
if (match[0].length === 0) {
|
|
2106
|
-
blockPattern.lastIndex += 1;
|
|
2107
|
-
continue;
|
|
2108
|
-
}
|
|
2109
|
-
const line = redacted.slice(0, match.index).split("\n").length;
|
|
2110
|
-
const newlines = (match[0].match(/\n/g) ?? []).length;
|
|
2111
|
-
findings.push({ rule: "private-key-block", line, path: filePath });
|
|
2112
|
-
redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
|
|
2113
|
-
blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
const compiled = rules.map((rule) => ({
|
|
2117
|
-
rule: rule.rule,
|
|
2118
|
-
pattern: toGlobal(rule.pattern)
|
|
2119
|
-
}));
|
|
2120
|
-
const lines = redacted.split("\n");
|
|
2121
|
-
const redactedLines = lines.map((line, index) => {
|
|
2122
|
-
if (line.length === 0) return line;
|
|
2123
|
-
const spans = [];
|
|
2124
|
-
const matchedRules = /* @__PURE__ */ new Set();
|
|
2125
|
-
for (const { rule, pattern } of compiled) {
|
|
2126
|
-
pattern.lastIndex = 0;
|
|
2127
|
-
let match;
|
|
2128
|
-
while ((match = pattern.exec(line)) !== null) {
|
|
2129
|
-
if (match[0].length === 0) {
|
|
2130
|
-
pattern.lastIndex += 1;
|
|
2131
|
-
continue;
|
|
2132
|
-
}
|
|
2133
|
-
spans.push({ start: match.index, end: match.index + match[0].length, rule });
|
|
2134
|
-
matchedRules.add(rule);
|
|
2135
|
-
}
|
|
2136
|
-
}
|
|
2137
|
-
if (spans.length === 0) return line;
|
|
2138
|
-
for (const rule of matchedRules) {
|
|
2139
|
-
findings.push({ rule, line: index + 1, path: filePath });
|
|
2140
|
-
}
|
|
2141
|
-
return applySpans(line, spans);
|
|
2142
|
-
});
|
|
2143
|
-
return { redacted: redactedLines.join("\n"), findings };
|
|
2144
|
-
}
|
|
2145
|
-
function applySpans(line, spans) {
|
|
2146
|
-
const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
|
|
2147
|
-
let out = "";
|
|
2148
|
-
let cursor = 0;
|
|
2149
|
-
for (const span of ordered) {
|
|
2150
|
-
if (span.end <= cursor) continue;
|
|
2151
|
-
const start = Math.max(span.start, cursor);
|
|
2152
|
-
out += line.slice(cursor, start);
|
|
2153
|
-
out += REDACTION_MARKER;
|
|
2154
|
-
cursor = Math.min(span.end, line.length);
|
|
2155
|
-
}
|
|
2156
|
-
out += line.slice(cursor);
|
|
2157
|
-
return out;
|
|
2158
|
-
}
|
|
2159
|
-
|
|
2160
2647
|
// src/config.ts
|
|
2161
2648
|
var CONFIG_FILENAMES = [
|
|
2162
2649
|
"snapshot.config.ts",
|
|
@@ -2206,7 +2693,12 @@ function normalizeResolvedConfig(config) {
|
|
|
2206
2693
|
),
|
|
2207
2694
|
include: resolveInclude(config.include),
|
|
2208
2695
|
preset: typeof config.preset === "string" ? config.preset : "generic",
|
|
2209
|
-
presetExclude:
|
|
2696
|
+
presetExclude: [
|
|
2697
|
+
.../* @__PURE__ */ new Set([
|
|
2698
|
+
...Array.isArray(config.presetExclude) ? config.presetExclude : [],
|
|
2699
|
+
...outputExcludePatterns(config.cwd, config.outDir, config.tmpDir)
|
|
2700
|
+
])
|
|
2701
|
+
],
|
|
2210
2702
|
layersMode: config.layersMode === "replace" ? "replace" : "merge",
|
|
2211
2703
|
budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
|
|
2212
2704
|
hooks: resolveHooks(config.hooks),
|
|
@@ -2243,7 +2735,68 @@ function supportsTypeScriptImports() {
|
|
|
2243
2735
|
return major > 22 || major === 22 && minor >= 6;
|
|
2244
2736
|
}
|
|
2245
2737
|
function stripJsonComments2(raw) {
|
|
2246
|
-
|
|
2738
|
+
const text2 = raw.replace(/^\uFEFF/, "");
|
|
2739
|
+
let out = "";
|
|
2740
|
+
let inString = false;
|
|
2741
|
+
let escaped = false;
|
|
2742
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2743
|
+
const char = text2[index];
|
|
2744
|
+
if (inString) {
|
|
2745
|
+
out += char;
|
|
2746
|
+
if (escaped) escaped = false;
|
|
2747
|
+
else if (char === "\\") escaped = true;
|
|
2748
|
+
else if (char === '"') inString = false;
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
if (char === '"') {
|
|
2752
|
+
inString = true;
|
|
2753
|
+
out += char;
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
if (char === "/" && text2[index + 1] === "/") {
|
|
2757
|
+
while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
|
|
2758
|
+
index -= 1;
|
|
2759
|
+
continue;
|
|
2760
|
+
}
|
|
2761
|
+
if (char === "/" && text2[index + 1] === "*") {
|
|
2762
|
+
index += 2;
|
|
2763
|
+
while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
|
|
2764
|
+
if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
|
|
2765
|
+
index += 1;
|
|
2766
|
+
}
|
|
2767
|
+
index += 1;
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
out += char;
|
|
2771
|
+
}
|
|
2772
|
+
return stripTrailingCommas(out);
|
|
2773
|
+
}
|
|
2774
|
+
function stripTrailingCommas(text2) {
|
|
2775
|
+
let out = "";
|
|
2776
|
+
let inString = false;
|
|
2777
|
+
let escaped = false;
|
|
2778
|
+
for (let index = 0; index < text2.length; index += 1) {
|
|
2779
|
+
const char = text2[index];
|
|
2780
|
+
if (inString) {
|
|
2781
|
+
out += char;
|
|
2782
|
+
if (escaped) escaped = false;
|
|
2783
|
+
else if (char === "\\") escaped = true;
|
|
2784
|
+
else if (char === '"') inString = false;
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
if (char === '"') {
|
|
2788
|
+
inString = true;
|
|
2789
|
+
out += char;
|
|
2790
|
+
continue;
|
|
2791
|
+
}
|
|
2792
|
+
if (char === ",") {
|
|
2793
|
+
let next = index + 1;
|
|
2794
|
+
while (next < text2.length && /\s/.test(text2[next])) next += 1;
|
|
2795
|
+
if (text2[next] === "}" || text2[next] === "]") continue;
|
|
2796
|
+
}
|
|
2797
|
+
out += char;
|
|
2798
|
+
}
|
|
2799
|
+
return out;
|
|
2247
2800
|
}
|
|
2248
2801
|
function parseJsonish(raw, file) {
|
|
2249
2802
|
try {
|
|
@@ -2252,6 +2805,47 @@ function parseJsonish(raw, file) {
|
|
|
2252
2805
|
throw new Error(`Could not parse ${file}: ${err.message}`);
|
|
2253
2806
|
}
|
|
2254
2807
|
}
|
|
2808
|
+
function describeValue2(value) {
|
|
2809
|
+
if (value === null) return "null";
|
|
2810
|
+
if (Array.isArray(value)) return "an array";
|
|
2811
|
+
if (value instanceof RegExp) return value.toString();
|
|
2812
|
+
if (typeof value === "function") return "a function";
|
|
2813
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
2814
|
+
if (typeof value === "object") {
|
|
2815
|
+
const keys = Object.keys(value);
|
|
2816
|
+
return keys.length > 0 ? `an object with keys ${keys.join(", ")}` : "an empty object";
|
|
2817
|
+
}
|
|
2818
|
+
return `${typeof value} (${String(value)})`;
|
|
2819
|
+
}
|
|
2820
|
+
var isThenable = (value) => typeof value.then === "function";
|
|
2821
|
+
function readConfigExport(exported, file) {
|
|
2822
|
+
if (exported === void 0 || exported === null) {
|
|
2823
|
+
throw new Error(
|
|
2824
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2825
|
+
);
|
|
2826
|
+
}
|
|
2827
|
+
if (Array.isArray(exported)) {
|
|
2828
|
+
throw new Error(
|
|
2829
|
+
`${file} exports an array; expected a single configuration object such as \`export default { outDir: "snapshot" }\`.`
|
|
2830
|
+
);
|
|
2831
|
+
}
|
|
2832
|
+
if (typeof exported === "function") {
|
|
2833
|
+
throw new Error(
|
|
2834
|
+
`${file} exports a function; expected a configuration object. Call the function yourself and export its result.`
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
if (typeof exported !== "object") {
|
|
2838
|
+
throw new Error(
|
|
2839
|
+
`${file} does not export a configuration object (received ${describeValue2(exported)}).`
|
|
2840
|
+
);
|
|
2841
|
+
}
|
|
2842
|
+
if (isThenable(exported)) {
|
|
2843
|
+
throw new Error(
|
|
2844
|
+
`${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;\`).`
|
|
2845
|
+
);
|
|
2846
|
+
}
|
|
2847
|
+
return exported;
|
|
2848
|
+
}
|
|
2255
2849
|
async function loadConfigFileDetailed(cwd) {
|
|
2256
2850
|
const warnings = [];
|
|
2257
2851
|
const errors = [];
|
|
@@ -2281,13 +2875,7 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
2281
2875
|
}
|
|
2282
2876
|
try {
|
|
2283
2877
|
const imported = await import(pathToFileURL(fullPath).href);
|
|
2284
|
-
const config = imported.default ?? imported;
|
|
2285
|
-
if (!config || typeof config !== "object") {
|
|
2286
|
-
const message = `${fullPath} does not export a configuration object.`;
|
|
2287
|
-
warnings.push(message);
|
|
2288
|
-
errors.push(message);
|
|
2289
|
-
continue;
|
|
2290
|
-
}
|
|
2878
|
+
const config = readConfigExport(imported.default ?? imported, fullPath);
|
|
2291
2879
|
return { config, file: fullPath, warnings, errors };
|
|
2292
2880
|
} catch (err) {
|
|
2293
2881
|
const message = `Could not load ${fullPath}: ${err.message}`;
|
|
@@ -2341,7 +2929,7 @@ var DEFAULT_ARTIFACTS = {
|
|
|
2341
2929
|
function isArtifactName(value) {
|
|
2342
2930
|
return ARTIFACT_NAMES.includes(value);
|
|
2343
2931
|
}
|
|
2344
|
-
function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
2932
|
+
function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
|
|
2345
2933
|
const artifacts = {
|
|
2346
2934
|
...DEFAULT_ARTIFACTS,
|
|
2347
2935
|
// `includeMonolith` predates this selection and stays the switch for it.
|
|
@@ -2363,19 +2951,119 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
|
|
|
2363
2951
|
artifacts[name] = value;
|
|
2364
2952
|
}
|
|
2365
2953
|
}
|
|
2366
|
-
|
|
2367
|
-
|
|
2954
|
+
if (monolithOverride !== void 0) {
|
|
2955
|
+
artifacts.monolith = monolithOverride;
|
|
2956
|
+
}
|
|
2957
|
+
for (const name of skipped ?? []) {
|
|
2958
|
+
if (!isArtifactName(name)) {
|
|
2959
|
+
throw new Error(
|
|
2960
|
+
`Unknown artifact "${name}". Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
|
|
2961
|
+
);
|
|
2962
|
+
}
|
|
2963
|
+
artifacts[name] = false;
|
|
2964
|
+
}
|
|
2965
|
+
return artifacts;
|
|
2966
|
+
}
|
|
2967
|
+
function strictPositiveInteger(value, fallback, label) {
|
|
2968
|
+
if (value === void 0) return fallback;
|
|
2969
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
|
|
2970
|
+
throw new Error(`${label} must be a positive integer (received ${describeValue2(value)}).`);
|
|
2971
|
+
}
|
|
2972
|
+
return value;
|
|
2973
|
+
}
|
|
2974
|
+
function strictBoolean(value, fallback, label) {
|
|
2975
|
+
if (value === void 0) return fallback;
|
|
2976
|
+
if (typeof value !== "boolean") {
|
|
2977
|
+
throw new Error(`${label} must be true or false (received ${describeValue2(value)}).`);
|
|
2978
|
+
}
|
|
2979
|
+
return value;
|
|
2980
|
+
}
|
|
2981
|
+
function strictString(value, fallback, label) {
|
|
2982
|
+
if (value === void 0) return fallback;
|
|
2983
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
2984
|
+
throw new Error(`${label} must be a non-empty string (received ${describeValue2(value)}).`);
|
|
2985
|
+
}
|
|
2986
|
+
return value;
|
|
2987
|
+
}
|
|
2988
|
+
function resolveExcludePatterns(value) {
|
|
2989
|
+
if (value === void 0) return [];
|
|
2990
|
+
if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string")) {
|
|
2991
|
+
throw new Error(
|
|
2992
|
+
`\`excludePatterns\` must be an array of glob strings such as ["**/*.test.ts"] (received ${describeValue2(value)}).`
|
|
2993
|
+
);
|
|
2994
|
+
}
|
|
2995
|
+
return value.map((pattern) => pattern.trim()).filter(Boolean);
|
|
2996
|
+
}
|
|
2997
|
+
var AUTO_PRESET_ALIASES = /* @__PURE__ */ new Set(["auto", "detected"]);
|
|
2998
|
+
function resolvePresetName(value) {
|
|
2999
|
+
if (value === void 0) return void 0;
|
|
3000
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
3001
|
+
throw new Error(
|
|
3002
|
+
`\`preset\` must be a preset name (received ${describeValue2(value)}). Use "detected" (or "auto") to detect one, or one of: ${listPresetNames().join(", ")}.`
|
|
3003
|
+
);
|
|
3004
|
+
}
|
|
3005
|
+
const name = value.trim();
|
|
3006
|
+
return AUTO_PRESET_ALIASES.has(name.toLowerCase()) ? void 0 : name;
|
|
3007
|
+
}
|
|
3008
|
+
function resolveRuleList(value, kind) {
|
|
3009
|
+
const collection = kind === "layer" ? "layers" : "kinds";
|
|
3010
|
+
const example = kind === "layer" ? `{ name: "api", match: "src/api/**" }` : `{ kind: "route", match: "src/routes/**" }`;
|
|
3011
|
+
if (value === void 0) return [];
|
|
3012
|
+
if (!Array.isArray(value)) {
|
|
3013
|
+
throw new Error(
|
|
3014
|
+
`\`${collection}\` must be an array of ${kind} rules (received ${describeValue2(value)}). A ${kind} rule is ${example}.`
|
|
3015
|
+
);
|
|
3016
|
+
}
|
|
3017
|
+
return value.map((entry, index) => {
|
|
3018
|
+
const where = `${collection}[${index}]`;
|
|
3019
|
+
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
|
|
3020
|
+
throw new Error(`${where} must be an object (received ${describeValue2(entry)}). A ${kind} rule is ${example}.`);
|
|
3021
|
+
}
|
|
3022
|
+
const record = entry;
|
|
3023
|
+
const keys = Object.keys(record);
|
|
3024
|
+
const canonical = kind === "layer" ? "name" : "kind";
|
|
3025
|
+
const alias = kind === "layer" ? "layer" : "name";
|
|
3026
|
+
const rawName = typeof record[canonical] === "string" ? record[canonical] : typeof record[alias] === "string" ? record[alias] : void 0;
|
|
3027
|
+
if (rawName === void 0 || rawName.trim() === "") {
|
|
2368
3028
|
throw new Error(
|
|
2369
|
-
|
|
3029
|
+
`${where} is missing \`${canonical}\` (received keys: ${keys.join(", ") || "none"}). A ${kind} rule is ${example}; \`${alias}\` is accepted as an alias of \`${canonical}\`.`
|
|
2370
3030
|
);
|
|
2371
3031
|
}
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
3032
|
+
const match = record.match;
|
|
3033
|
+
if (match === void 0) {
|
|
3034
|
+
throw new Error(
|
|
3035
|
+
`${where}.match is required (received keys: ${keys.join(", ") || "none"}). Use a glob string ("src/api/**"), a RegExp (/\\\\.tsx$/) or a function.`
|
|
3036
|
+
);
|
|
3037
|
+
}
|
|
3038
|
+
if (!(typeof match === "string" || match instanceof RegExp || typeof match === "function")) {
|
|
3039
|
+
throw new Error(`${where}.match must be a string, a RegExp or a function (received ${describeValue2(match)}).`);
|
|
3040
|
+
}
|
|
3041
|
+
if (typeof match === "string" && match.trim() === "") {
|
|
3042
|
+
throw new Error(`${where}.match must not be an empty string.`);
|
|
3043
|
+
}
|
|
3044
|
+
return {
|
|
3045
|
+
name: rawName.trim(),
|
|
3046
|
+
match
|
|
3047
|
+
};
|
|
3048
|
+
});
|
|
3049
|
+
}
|
|
3050
|
+
function resolveLayerRules(value) {
|
|
3051
|
+
return resolveRuleList(value, "layer").map((rule) => ({ name: rule.name, match: rule.match }));
|
|
2375
3052
|
}
|
|
2376
|
-
function
|
|
2377
|
-
|
|
2378
|
-
|
|
3053
|
+
function resolveKindRules(value) {
|
|
3054
|
+
return resolveRuleList(value, "kind").map((rule) => ({ kind: rule.name, match: rule.match }));
|
|
3055
|
+
}
|
|
3056
|
+
function outputExcludePatterns(cwd, outDir, tmpDir) {
|
|
3057
|
+
const patterns = [];
|
|
3058
|
+
for (const dir of [outDir, tmpDir]) {
|
|
3059
|
+
const absolute = path10.resolve(cwd, dir);
|
|
3060
|
+
const relative4 = path10.relative(cwd, absolute);
|
|
3061
|
+
if (!relative4 || relative4 === ".." || relative4.startsWith(`..${path10.sep}`) || path10.isAbsolute(relative4)) {
|
|
3062
|
+
continue;
|
|
3063
|
+
}
|
|
3064
|
+
patterns.push(`${relative4.replace(/\\/g, "/")}/**`);
|
|
3065
|
+
}
|
|
3066
|
+
return patterns;
|
|
2379
3067
|
}
|
|
2380
3068
|
var BYTE_UNITS = {
|
|
2381
3069
|
b: 1,
|
|
@@ -2444,11 +3132,13 @@ function resolveLayersMode(mode) {
|
|
|
2444
3132
|
}
|
|
2445
3133
|
return mode;
|
|
2446
3134
|
}
|
|
2447
|
-
function resolveBudgets(budgets, fallback) {
|
|
3135
|
+
function resolveBudgets(budgets, fallback, overrideDefault) {
|
|
2448
3136
|
const byLayer = {};
|
|
2449
3137
|
if (budgets === void 0) return { default: fallback, byLayer };
|
|
2450
3138
|
if (typeof budgets !== "object" || budgets === null || Array.isArray(budgets)) {
|
|
2451
|
-
throw new Error(
|
|
3139
|
+
throw new Error(
|
|
3140
|
+
`\`budgets\` must be an object such as { default: "120k", byLayer: { api: "60k" } } (received ${describeValue2(budgets)}).`
|
|
3141
|
+
);
|
|
2452
3142
|
}
|
|
2453
3143
|
if (budgets.byLayer !== void 0) {
|
|
2454
3144
|
if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
|
|
@@ -2463,10 +3153,12 @@ function resolveBudgets(budgets, fallback) {
|
|
|
2463
3153
|
);
|
|
2464
3154
|
}
|
|
2465
3155
|
}
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
3156
|
+
const resolvedDefault = boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default");
|
|
3157
|
+
if (!overrideDefault) return { default: resolvedDefault, byLayer };
|
|
3158
|
+
for (const [layer2, value] of Object.entries(byLayer)) {
|
|
3159
|
+
byLayer[layer2] = Math.min(value, fallback);
|
|
3160
|
+
}
|
|
3161
|
+
return { default: fallback, byLayer };
|
|
2470
3162
|
}
|
|
2471
3163
|
function resolveHooks(hooks) {
|
|
2472
3164
|
if (hooks === void 0) return {};
|
|
@@ -2479,6 +3171,11 @@ function resolveHooks(hooks) {
|
|
|
2479
3171
|
return { ...hooks };
|
|
2480
3172
|
}
|
|
2481
3173
|
function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
3174
|
+
if (userConfig?.cwd !== void 0 && (typeof userConfig.cwd !== "string" || userConfig.cwd.trim() === "")) {
|
|
3175
|
+
throw new Error(
|
|
3176
|
+
`\`cwd\` must be a non-empty path string (received ${describeValue2(userConfig.cwd)}).`
|
|
3177
|
+
);
|
|
3178
|
+
}
|
|
2482
3179
|
const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
|
|
2483
3180
|
const resolved = path10.resolve(candidate);
|
|
2484
3181
|
if (!fs9.existsSync(resolved)) {
|
|
@@ -2491,55 +3188,106 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
|
2491
3188
|
}
|
|
2492
3189
|
function resolveConfig(userConfig, cliOptions, baseDir) {
|
|
2493
3190
|
const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
|
|
2494
|
-
const forcedPreset = userConfig?.preset;
|
|
3191
|
+
const forcedPreset = resolvePresetName(userConfig?.preset);
|
|
2495
3192
|
if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
|
|
2496
3193
|
throw new Error(unknownPresetMessage(forcedPreset));
|
|
2497
3194
|
}
|
|
2498
3195
|
const preset = detectPreset(cwd, forcedPreset);
|
|
2499
3196
|
const layersMode = resolveLayersMode(userConfig?.layersMode);
|
|
2500
|
-
const
|
|
2501
|
-
const
|
|
2502
|
-
const
|
|
2503
|
-
const
|
|
2504
|
-
const
|
|
2505
|
-
|
|
3197
|
+
const userLayers = resolveLayerRules(userConfig?.layers);
|
|
3198
|
+
const userKinds = resolveKindRules(userConfig?.kinds);
|
|
3199
|
+
const layers = userLayers.length > 0 ? layersMode === "replace" ? userLayers : [...userLayers, ...preset.layers] : preset.layers;
|
|
3200
|
+
const kinds = userKinds.length > 0 ? layersMode === "replace" ? userKinds : [...userKinds, ...preset.kinds] : preset.kinds;
|
|
3201
|
+
const secrets = userConfig?.secrets;
|
|
3202
|
+
if (secrets !== void 0 && (typeof secrets !== "object" || Array.isArray(secrets))) {
|
|
3203
|
+
throw new Error(
|
|
3204
|
+
`\`secrets\` must be an object of { customPatterns, disableDefaultPatterns } (received ${describeValue2(secrets)}).`
|
|
3205
|
+
);
|
|
3206
|
+
}
|
|
3207
|
+
const customRules = secrets?.customPatterns ?? [];
|
|
3208
|
+
if (!Array.isArray(customRules)) {
|
|
3209
|
+
throw new Error(
|
|
3210
|
+
`\`secrets.customPatterns\` must be an array of secret rules (received ${describeValue2(customRules)}). Each entry is a RegExp, a string, or { rule, pattern }.`
|
|
3211
|
+
);
|
|
3212
|
+
}
|
|
3213
|
+
const disableDefaultPatterns = strictBoolean(
|
|
3214
|
+
secrets?.disableDefaultPatterns,
|
|
3215
|
+
false,
|
|
3216
|
+
"`secrets.disableDefaultPatterns`"
|
|
3217
|
+
);
|
|
3218
|
+
const aliasedRules = userConfig?.secretRules;
|
|
3219
|
+
if (aliasedRules !== void 0 && !Array.isArray(aliasedRules)) {
|
|
3220
|
+
throw new Error(
|
|
3221
|
+
`\`secretRules\` must be an array of { rule, pattern } entries (received ${describeValue2(aliasedRules)}).`
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
const compiledCustomRules = compileSecretRules([
|
|
3225
|
+
...customRules,
|
|
3226
|
+
...aliasedRules ?? []
|
|
3227
|
+
]);
|
|
3228
|
+
const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
|
|
3229
|
+
const cliMonolith = cliOptions?.includeMonolith;
|
|
3230
|
+
const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
|
|
2506
3231
|
const artifacts = resolveArtifacts(
|
|
2507
3232
|
userConfig?.artifacts,
|
|
2508
3233
|
cliOptions?.skipArtifacts,
|
|
2509
|
-
includeMonolith
|
|
3234
|
+
includeMonolith,
|
|
3235
|
+
cliMonolith
|
|
2510
3236
|
);
|
|
3237
|
+
const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
|
|
2511
3238
|
const maxPartBytes = boundedBytes(
|
|
2512
|
-
|
|
3239
|
+
explicitMaxPartBytes,
|
|
2513
3240
|
300 * 1024,
|
|
2514
3241
|
MIN_MAX_PART_BYTES,
|
|
2515
3242
|
"maxPartBytes"
|
|
2516
3243
|
);
|
|
2517
|
-
const budgets = resolveBudgets(
|
|
3244
|
+
const budgets = resolveBudgets(
|
|
3245
|
+
userConfig?.budgets,
|
|
3246
|
+
maxPartBytes,
|
|
3247
|
+
explicitMaxPartBytes !== void 0
|
|
3248
|
+
);
|
|
3249
|
+
const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
|
|
3250
|
+
const outDir = cliOptions?.outDir || configuredOutDir;
|
|
3251
|
+
const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
|
|
3252
|
+
const branch = strictString(userConfig?.branch, "main", "`branch`");
|
|
3253
|
+
const configuredSensitiveFiles = strictBoolean(
|
|
3254
|
+
userConfig?.includeSensitiveFiles,
|
|
3255
|
+
false,
|
|
3256
|
+
"`includeSensitiveFiles`"
|
|
3257
|
+
);
|
|
3258
|
+
const includeSensitiveFiles = strictBoolean(
|
|
3259
|
+
cliOptions?.includeSensitiveFiles,
|
|
3260
|
+
configuredSensitiveFiles,
|
|
3261
|
+
"`includeSensitiveFiles`"
|
|
3262
|
+
);
|
|
3263
|
+
const presetExclude = [
|
|
3264
|
+
.../* @__PURE__ */ new Set([...preset.exclude, ...outputExcludePatterns(cwd, outDir, tmpDir)])
|
|
3265
|
+
];
|
|
2518
3266
|
return markResolvedConfig({
|
|
2519
3267
|
cwd,
|
|
2520
|
-
branch
|
|
2521
|
-
outDir
|
|
2522
|
-
tmpDir
|
|
3268
|
+
branch,
|
|
3269
|
+
outDir,
|
|
3270
|
+
tmpDir,
|
|
2523
3271
|
maxPartBytes,
|
|
2524
3272
|
largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
|
|
2525
|
-
previewHeadLines:
|
|
2526
|
-
previewTailLines:
|
|
3273
|
+
previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
|
|
3274
|
+
previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
|
|
2527
3275
|
includeMonolith: artifacts.monolith,
|
|
2528
3276
|
maxMonolithBytes: optionalBytes(
|
|
2529
3277
|
cliOptions?.maxMonolithBytes !== void 0 ? cliOptions.maxMonolithBytes : userConfig?.maxMonolithBytes,
|
|
2530
3278
|
"maxMonolithBytes"
|
|
2531
3279
|
),
|
|
2532
|
-
includeSensitiveFiles
|
|
3280
|
+
includeSensitiveFiles,
|
|
2533
3281
|
include: resolveInclude(userConfig?.include),
|
|
2534
3282
|
preset: preset.presetName,
|
|
2535
|
-
presetExclude
|
|
3283
|
+
presetExclude,
|
|
2536
3284
|
artifacts,
|
|
2537
3285
|
layersMode,
|
|
2538
3286
|
budgets,
|
|
2539
3287
|
hooks: resolveHooks(userConfig?.hooks),
|
|
2540
3288
|
layers,
|
|
2541
3289
|
kinds,
|
|
2542
|
-
excludePatterns: userConfig?.excludePatterns
|
|
3290
|
+
excludePatterns: resolveExcludePatterns(userConfig?.excludePatterns),
|
|
2543
3291
|
secretRules
|
|
2544
3292
|
});
|
|
2545
3293
|
}
|
|
@@ -2594,7 +3342,11 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
2594
3342
|
`**Total Files:** ${manifest.counts.totalFiles} `,
|
|
2595
3343
|
`**Total Lines:** ${manifest.counts.totalLines.toLocaleString()} `,
|
|
2596
3344
|
`**Total Size:** ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB `,
|
|
2597
|
-
|
|
3345
|
+
// This counts the bytes that were *emitted* after redaction, not the bytes
|
|
3346
|
+
// that were masked: calling it "redacted bytes" made a repository with zero
|
|
3347
|
+
// secrets report a non-zero redacted-byte figure directly above "no secrets
|
|
3348
|
+
// detected". The number of masked spans is reported in section 1 instead.
|
|
3349
|
+
`**Emitted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
|
|
2598
3350
|
"",
|
|
2599
3351
|
"## 1. Security Scan & Redaction",
|
|
2600
3352
|
"",
|
|
@@ -2797,6 +3549,8 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
2797
3549
|
layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
|
|
2798
3550
|
kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
|
|
2799
3551
|
}
|
|
3552
|
+
const pathLines = included.map((file) => displayPath(file.relativePath));
|
|
3553
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
2800
3554
|
const header = [
|
|
2801
3555
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
2802
3556
|
"",
|
|
@@ -2809,9 +3563,9 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
2809
3563
|
"",
|
|
2810
3564
|
"## Files",
|
|
2811
3565
|
"",
|
|
2812
|
-
|
|
2813
|
-
...
|
|
2814
|
-
|
|
3566
|
+
`${listFence}text`,
|
|
3567
|
+
...pathLines,
|
|
3568
|
+
listFence,
|
|
2815
3569
|
"",
|
|
2816
3570
|
"## Contents",
|
|
2817
3571
|
""
|
|
@@ -2839,7 +3593,8 @@ function capValue(value, max) {
|
|
|
2839
3593
|
return `${value.slice(0, half)}\u2026${value.slice(value.length - half)}`;
|
|
2840
3594
|
}
|
|
2841
3595
|
var capMetadata = (value) => capValue(value, METADATA_VALUE_MAX);
|
|
2842
|
-
var
|
|
3596
|
+
var CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
|
|
3597
|
+
var displayPath = (value) => capValue(value.replace(CONTROL_CHARS, " "), DISPLAY_PATH_MAX);
|
|
2843
3598
|
function topEntries(counts, limit) {
|
|
2844
3599
|
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(", ");
|
|
2845
3600
|
}
|
|
@@ -2935,6 +3690,8 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
2935
3690
|
layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
|
|
2936
3691
|
kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
|
|
2937
3692
|
}
|
|
3693
|
+
const pathLines = group.map((s) => displayPath(s.file.relativePath));
|
|
3694
|
+
const listFence = fenceForText(pathLines.join("\n"), 3);
|
|
2938
3695
|
const header = [
|
|
2939
3696
|
`# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
|
|
2940
3697
|
"",
|
|
@@ -2947,9 +3704,9 @@ function renderGroups(groups, contentFingerprint) {
|
|
|
2947
3704
|
"",
|
|
2948
3705
|
"## Files",
|
|
2949
3706
|
"",
|
|
2950
|
-
|
|
2951
|
-
...
|
|
2952
|
-
|
|
3707
|
+
`${listFence}text`,
|
|
3708
|
+
...pathLines,
|
|
3709
|
+
listFence,
|
|
2953
3710
|
"",
|
|
2954
3711
|
"## Contents",
|
|
2955
3712
|
""
|
|
@@ -3008,9 +3765,12 @@ function renderFileSection(file) {
|
|
|
3008
3765
|
].join("\n");
|
|
3009
3766
|
}
|
|
3010
3767
|
function fenceForContent(content) {
|
|
3768
|
+
return fenceForText(content, 4);
|
|
3769
|
+
}
|
|
3770
|
+
function fenceForText(content, minimum) {
|
|
3011
3771
|
const matches = content.match(/`+/g);
|
|
3012
3772
|
const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
|
|
3013
|
-
return "`".repeat(Math.max(
|
|
3773
|
+
return "`".repeat(Math.max(minimum, maxRun + 1));
|
|
3014
3774
|
}
|
|
3015
3775
|
function renderSummary(summary) {
|
|
3016
3776
|
if (!summary) return [];
|
|
@@ -3159,6 +3919,7 @@ function generateReadme(manifest) {
|
|
|
3159
3919
|
|
|
3160
3920
|
// src/generator/tree.ts
|
|
3161
3921
|
var MAX_TREE_LINES = 2e4;
|
|
3922
|
+
var CONTROL_CHARS2 = /[\u0000-\u001f\u007f]/g;
|
|
3162
3923
|
function generateProjectTree(filePaths) {
|
|
3163
3924
|
const root = { children: /* @__PURE__ */ new Map() };
|
|
3164
3925
|
for (const filePath of filePaths) {
|
|
@@ -3173,7 +3934,7 @@ function generateProjectTree(filePaths) {
|
|
|
3173
3934
|
current = child;
|
|
3174
3935
|
}
|
|
3175
3936
|
}
|
|
3176
|
-
const lines = ["
|
|
3937
|
+
const lines = ["."];
|
|
3177
3938
|
let truncatedAt = -1;
|
|
3178
3939
|
function walk(node, prefix) {
|
|
3179
3940
|
if (truncatedAt !== -1) return;
|
|
@@ -3188,7 +3949,8 @@ function generateProjectTree(filePaths) {
|
|
|
3188
3949
|
const isLast = index === keys.length - 1;
|
|
3189
3950
|
const child = node.children.get(key);
|
|
3190
3951
|
const isDirectory = child.children.size > 0;
|
|
3191
|
-
|
|
3952
|
+
const label = key.replace(CONTROL_CHARS2, " ");
|
|
3953
|
+
lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
|
|
3192
3954
|
if (lines.length >= MAX_TREE_LINES) {
|
|
3193
3955
|
truncatedAt = lines.length;
|
|
3194
3956
|
return;
|
|
@@ -3200,8 +3962,8 @@ function generateProjectTree(filePaths) {
|
|
|
3200
3962
|
if (truncatedAt !== -1) {
|
|
3201
3963
|
lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
|
|
3202
3964
|
}
|
|
3203
|
-
lines.
|
|
3204
|
-
return lines.join("\n");
|
|
3965
|
+
const fence = fenceForText(lines.join("\n"), 3);
|
|
3966
|
+
return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
|
|
3205
3967
|
}
|
|
3206
3968
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
3207
3969
|
".png",
|
|
@@ -3345,6 +4107,15 @@ function emptyDrift() {
|
|
|
3345
4107
|
function sha256Hex(buffer) {
|
|
3346
4108
|
return crypto.createHash("sha256").update(buffer).digest("hex");
|
|
3347
4109
|
}
|
|
4110
|
+
function wouldNowBeIncluded(cwd, relativePath) {
|
|
4111
|
+
const absolutePath = path10.join(cwd, relativePath);
|
|
4112
|
+
try {
|
|
4113
|
+
if (!fs9.statSync(absolutePath).isFile()) return false;
|
|
4114
|
+
return !isBinaryFile(absolutePath);
|
|
4115
|
+
} catch {
|
|
4116
|
+
return false;
|
|
4117
|
+
}
|
|
4118
|
+
}
|
|
3348
4119
|
function validateSnapshot(configInput) {
|
|
3349
4120
|
assertUsableConfigInput(configInput);
|
|
3350
4121
|
const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
|
|
@@ -3399,9 +4170,9 @@ function validateSnapshot(configInput) {
|
|
|
3399
4170
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
3400
4171
|
});
|
|
3401
4172
|
for (const diskFile of collection.files) {
|
|
3402
|
-
if (skippedSet.has(diskFile)) continue;
|
|
3403
4173
|
const snapshotHash = manifestFileMap.get(diskFile);
|
|
3404
4174
|
if (!snapshotHash) {
|
|
4175
|
+
if (skippedSet.has(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
|
|
3405
4176
|
drift.missingInSnapshot.push(diskFile);
|
|
3406
4177
|
continue;
|
|
3407
4178
|
}
|
|
@@ -3682,16 +4453,15 @@ function describeMatcher(matcher) {
|
|
|
3682
4453
|
if (typeof matcher === "string") return matcher;
|
|
3683
4454
|
if (matcher instanceof RegExp) return String(matcher);
|
|
3684
4455
|
if (typeof matcher === "function") {
|
|
3685
|
-
|
|
3686
|
-
return source.length > 70 ? `${source.slice(0, 67)}...` : source;
|
|
4456
|
+
return "function (custom predicate)";
|
|
3687
4457
|
}
|
|
3688
4458
|
return String(matcher);
|
|
3689
4459
|
}
|
|
3690
4460
|
function normalizeTarget(cwd, target) {
|
|
3691
4461
|
const absolute = path10.resolve(cwd, target);
|
|
3692
|
-
const
|
|
3693
|
-
if (
|
|
3694
|
-
return
|
|
4462
|
+
const relative4 = path10.relative(cwd, absolute);
|
|
4463
|
+
if (relative4.startsWith("..") || path10.isAbsolute(relative4)) return target;
|
|
4464
|
+
return relative4.split(path10.sep).join("/");
|
|
3695
4465
|
}
|
|
3696
4466
|
function findRuleMatch(rules, relativePath) {
|
|
3697
4467
|
return rules.find(
|
|
@@ -3707,10 +4477,10 @@ function readManifest2(config) {
|
|
|
3707
4477
|
}
|
|
3708
4478
|
}
|
|
3709
4479
|
function explainPath(config, target) {
|
|
3710
|
-
const
|
|
3711
|
-
const absolute = path10.join(config.cwd,
|
|
4480
|
+
const relative4 = normalizeTarget(config.cwd, target);
|
|
4481
|
+
const absolute = path10.join(config.cwd, relative4);
|
|
3712
4482
|
const result = {
|
|
3713
|
-
path:
|
|
4483
|
+
path: relative4,
|
|
3714
4484
|
exists: fs9.existsSync(absolute),
|
|
3715
4485
|
included: false
|
|
3716
4486
|
};
|
|
@@ -3719,8 +4489,8 @@ function explainPath(config, target) {
|
|
|
3719
4489
|
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
3720
4490
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
3721
4491
|
});
|
|
3722
|
-
if (!collection.files.includes(
|
|
3723
|
-
const record = collection.skipped.find((entry2) => entry2.path ===
|
|
4492
|
+
if (!collection.files.includes(relative4)) {
|
|
4493
|
+
const record = collection.skipped.find((entry2) => entry2.path === relative4);
|
|
3724
4494
|
if (record) {
|
|
3725
4495
|
result.reason = record.reason;
|
|
3726
4496
|
result.detail = record.detail;
|
|
@@ -3734,24 +4504,30 @@ function explainPath(config, target) {
|
|
|
3734
4504
|
return result;
|
|
3735
4505
|
}
|
|
3736
4506
|
result.included = true;
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
3740
|
-
|
|
4507
|
+
if (isBinaryFile(absolute)) {
|
|
4508
|
+
result.included = false;
|
|
4509
|
+
result.reason = "binary";
|
|
4510
|
+
result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
|
|
4511
|
+
return result;
|
|
4512
|
+
}
|
|
4513
|
+
result.layer = classifyLayer(relative4, config.layers);
|
|
4514
|
+
result.kind = classifyKind(relative4, config.kinds);
|
|
4515
|
+
result.language = detectLanguage(relative4);
|
|
4516
|
+
const layerRule = findRuleMatch(config.layers, relative4);
|
|
3741
4517
|
if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
|
|
3742
|
-
const kindRule = findRuleMatch(config.kinds,
|
|
4518
|
+
const kindRule = findRuleMatch(config.kinds, relative4);
|
|
3743
4519
|
if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
|
|
3744
4520
|
if (fs9.existsSync(absolute) && fs9.statSync(absolute).isFile()) {
|
|
3745
4521
|
const buffer = fs9.readFileSync(absolute);
|
|
3746
4522
|
const text2 = buffer.toString("utf8");
|
|
3747
4523
|
result.bytes = buffer.length;
|
|
3748
4524
|
result.lines = countLines(text2);
|
|
3749
|
-
const { findings } = scanAndRedactSecrets(text2, config.secretRules,
|
|
4525
|
+
const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative4);
|
|
3750
4526
|
result.secretCount = findings.length;
|
|
3751
4527
|
result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
|
|
3752
4528
|
}
|
|
3753
4529
|
const manifest = readManifest2(config);
|
|
3754
|
-
const entry = manifest?.files.find((file) => file.relativePath ===
|
|
4530
|
+
const entry = manifest?.files.find((file) => file.relativePath === relative4);
|
|
3755
4531
|
result.part = entry?.part ?? null;
|
|
3756
4532
|
result.inclusionMode = entry?.inclusionMode;
|
|
3757
4533
|
return result;
|
|
@@ -3806,7 +4582,40 @@ function hashMap(manifest) {
|
|
|
3806
4582
|
}
|
|
3807
4583
|
return map;
|
|
3808
4584
|
}
|
|
4585
|
+
function manifestProblem(value) {
|
|
4586
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
|
|
4587
|
+
const candidate = value;
|
|
4588
|
+
if (!Array.isArray(candidate.files)) return 'no "files" array';
|
|
4589
|
+
for (const entry of candidate.files) {
|
|
4590
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
4591
|
+
return 'a "files" entry is not an object';
|
|
4592
|
+
}
|
|
4593
|
+
if (typeof entry.relativePath !== "string") {
|
|
4594
|
+
return 'a "files" entry has no "relativePath"';
|
|
4595
|
+
}
|
|
4596
|
+
}
|
|
4597
|
+
const counts = candidate.counts;
|
|
4598
|
+
if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
|
|
4599
|
+
return 'no "counts" object';
|
|
4600
|
+
}
|
|
4601
|
+
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
4602
|
+
if (typeof counts[key] !== "number") {
|
|
4603
|
+
return `no numeric "counts.${key}"`;
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
|
|
4607
|
+
if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
|
|
4608
|
+
return null;
|
|
4609
|
+
}
|
|
4610
|
+
function assertUsableManifest(value, label) {
|
|
4611
|
+
const problem = manifestProblem(value);
|
|
4612
|
+
if (problem) {
|
|
4613
|
+
throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
3809
4616
|
function compareManifests(before, after) {
|
|
4617
|
+
assertUsableManifest(before, "before");
|
|
4618
|
+
assertUsableManifest(after, "after");
|
|
3810
4619
|
const left = hashMap(before);
|
|
3811
4620
|
const right = hashMap(after);
|
|
3812
4621
|
const added = [];
|
|
@@ -3852,11 +4661,11 @@ function loadManifest(target) {
|
|
|
3852
4661
|
} catch (err) {
|
|
3853
4662
|
throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
|
|
3854
4663
|
}
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
4664
|
+
const problem = manifestProblem(parsed);
|
|
4665
|
+
if (problem) {
|
|
4666
|
+
throw new Error(
|
|
4667
|
+
`${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
|
|
4668
|
+
);
|
|
3860
4669
|
}
|
|
3861
4670
|
return parsed;
|
|
3862
4671
|
}
|
|
@@ -3968,7 +4777,7 @@ function renderConfig(answers) {
|
|
|
3968
4777
|
secrets: {
|
|
3969
4778
|
// [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
|
|
3970
4779
|
customPatterns: [],
|
|
3971
|
-
// Set true to drop the
|
|
4780
|
+
// Set true to drop the built-in rules above and keep only customPatterns.
|
|
3972
4781
|
disableDefaultPatterns: false,
|
|
3973
4782
|
},
|
|
3974
4783
|
|
|
@@ -3984,6 +4793,10 @@ function renderConfig(answers) {
|
|
|
3984
4793
|
},
|
|
3985
4794
|
// Skip the monolith above this size. null = no limit.
|
|
3986
4795
|
maxMonolithBytes: null,
|
|
4796
|
+
// Top-level switch for the monolith, equivalent to artifacts.monolith.
|
|
4797
|
+
// When both are present the artifact entry wins; this one exists so the
|
|
4798
|
+
// generated file lists every setting the tool accepts.
|
|
4799
|
+
includeMonolith: ${artifacts.monolith},
|
|
3987
4800
|
|
|
3988
4801
|
// \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
|
|
3989
4802
|
// afterGenerate: (manifest) => { /* write your own artifact */ }
|
|
@@ -4305,6 +5118,14 @@ function existingConfig(cwd) {
|
|
|
4305
5118
|
}
|
|
4306
5119
|
return null;
|
|
4307
5120
|
}
|
|
5121
|
+
function packageJsonHasConfig(cwd) {
|
|
5122
|
+
try {
|
|
5123
|
+
const parsed = JSON.parse(fs9.readFileSync(path10.join(cwd, "package.json"), "utf8"));
|
|
5124
|
+
return typeof parsed.snapshot === "object" && parsed.snapshot !== null;
|
|
5125
|
+
} catch {
|
|
5126
|
+
return false;
|
|
5127
|
+
}
|
|
5128
|
+
}
|
|
4308
5129
|
function artifactAnswers(options) {
|
|
4309
5130
|
const artifacts = { ...DEFAULT_ANSWERS.artifacts };
|
|
4310
5131
|
for (const name of options.skipArtifacts ?? []) {
|
|
@@ -4517,6 +5338,11 @@ async function runInit(options) {
|
|
|
4517
5338
|
`A configuration already exists (${present}). Re-run with --force to replace it.`
|
|
4518
5339
|
);
|
|
4519
5340
|
}
|
|
5341
|
+
if (packageJsonHasConfig(options.cwd) && !options.force) {
|
|
5342
|
+
throw new Error(
|
|
5343
|
+
'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.'
|
|
5344
|
+
);
|
|
5345
|
+
}
|
|
4520
5346
|
if (fs9.existsSync(configPath) && !options.force) {
|
|
4521
5347
|
throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
|
|
4522
5348
|
}
|
|
@@ -4536,7 +5362,7 @@ async function runInit(options) {
|
|
|
4536
5362
|
|
|
4537
5363
|
// src/index.ts
|
|
4538
5364
|
var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
|
|
4539
|
-
var GENERATOR_VERSION = "
|
|
5365
|
+
var GENERATOR_VERSION = "4.0.0";
|
|
4540
5366
|
var SCHEMA_VERSION = "3.0.0";
|
|
4541
5367
|
function sha256Hex2(input) {
|
|
4542
5368
|
return crypto.createHash("sha256").update(input).digest("hex");
|
|
@@ -4706,27 +5532,213 @@ function resolveSafeOutputDirs(config) {
|
|
|
4706
5532
|
return { outDir, tmpDir };
|
|
4707
5533
|
}
|
|
4708
5534
|
var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
|
|
5535
|
+
var STAGING_ENTRIES = /* @__PURE__ */ new Set([
|
|
5536
|
+
".codebase-snapshot",
|
|
5537
|
+
"manifest.json",
|
|
5538
|
+
"README.md",
|
|
5539
|
+
"parts",
|
|
5540
|
+
"reports",
|
|
5541
|
+
"full-codebase.json",
|
|
5542
|
+
"full-codebase.md"
|
|
5543
|
+
]);
|
|
4709
5544
|
var OWNERSHIP_MARKER = ".codebase-snapshot";
|
|
4710
|
-
|
|
5545
|
+
var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
|
|
5546
|
+
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;
|
|
5547
|
+
function listEntries(dir) {
|
|
5548
|
+
const entries = [];
|
|
5549
|
+
const walk = (absolute, prefix) => {
|
|
5550
|
+
let children;
|
|
5551
|
+
try {
|
|
5552
|
+
children = fs9.readdirSync(absolute, { withFileTypes: true });
|
|
5553
|
+
} catch {
|
|
5554
|
+
return;
|
|
5555
|
+
}
|
|
5556
|
+
for (const child of children) {
|
|
5557
|
+
const relative4 = prefix === "" ? child.name : `${prefix}/${child.name}`;
|
|
5558
|
+
entries.push(relative4);
|
|
5559
|
+
if (child.isDirectory()) walk(path10.join(absolute, child.name), relative4);
|
|
5560
|
+
}
|
|
5561
|
+
};
|
|
5562
|
+
walk(dir, "");
|
|
5563
|
+
return entries.sort();
|
|
5564
|
+
}
|
|
5565
|
+
function markerPayload(dir, state, id, createdAt) {
|
|
4711
5566
|
const marker2 = {
|
|
4712
5567
|
generator: GENERATOR_NAME,
|
|
4713
5568
|
version: GENERATOR_VERSION,
|
|
4714
|
-
schemaVersion: SCHEMA_VERSION
|
|
5569
|
+
schemaVersion: SCHEMA_VERSION,
|
|
5570
|
+
createdAt,
|
|
5571
|
+
state,
|
|
5572
|
+
id,
|
|
5573
|
+
entries: listEntries(dir)
|
|
4715
5574
|
};
|
|
4716
|
-
fs9.writeFileSync(path10.join(dir, OWNERSHIP_MARKER), `${JSON.stringify(marker2, null, 2)}
|
|
4717
|
-
`, "utf8");
|
|
4718
|
-
}
|
|
4719
|
-
function isOwnOutput(dir) {
|
|
4720
|
-
if (fs9.existsSync(path10.join(dir, OWNERSHIP_MARKER))) return true;
|
|
4721
5575
|
const manifestPath = path10.join(dir, "manifest.json");
|
|
4722
|
-
if (
|
|
5576
|
+
if (fs9.existsSync(manifestPath)) {
|
|
5577
|
+
marker2.manifestSha256 = sha256Hex2(fs9.readFileSync(manifestPath));
|
|
5578
|
+
}
|
|
5579
|
+
return marker2;
|
|
5580
|
+
}
|
|
5581
|
+
function writeOwnershipMarker(dir, marker2) {
|
|
5582
|
+
fs9.writeFileSync(
|
|
5583
|
+
path10.join(dir, OWNERSHIP_MARKER),
|
|
5584
|
+
`${JSON.stringify(marker2, null, 2)}
|
|
5585
|
+
`,
|
|
5586
|
+
"utf8"
|
|
5587
|
+
);
|
|
5588
|
+
}
|
|
5589
|
+
function readOwnershipMarker(dir) {
|
|
5590
|
+
const markerPath = path10.join(dir, OWNERSHIP_MARKER);
|
|
5591
|
+
if (!fs9.existsSync(markerPath)) {
|
|
5592
|
+
return { marker: null, problem: "it holds no .codebase-snapshot marker" };
|
|
5593
|
+
}
|
|
5594
|
+
let raw;
|
|
4723
5595
|
try {
|
|
4724
|
-
|
|
4725
|
-
const generator = parsed.generator;
|
|
4726
|
-
return typeof parsed.schemaVersion === "string" && Array.isArray(parsed.files) && typeof generator === "object" && generator !== null && generator.name === GENERATOR_NAME;
|
|
5596
|
+
raw = fs9.readFileSync(markerPath, "utf8");
|
|
4727
5597
|
} catch {
|
|
4728
|
-
return
|
|
5598
|
+
return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
|
|
5599
|
+
}
|
|
5600
|
+
if (raw.trim().length === 0) {
|
|
5601
|
+
return { marker: null, problem: "its .codebase-snapshot marker is empty" };
|
|
5602
|
+
}
|
|
5603
|
+
let parsed;
|
|
5604
|
+
try {
|
|
5605
|
+
parsed = JSON.parse(raw);
|
|
5606
|
+
} catch {
|
|
5607
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not valid JSON" };
|
|
5608
|
+
}
|
|
5609
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
5610
|
+
return { marker: null, problem: "its .codebase-snapshot marker is not a JSON object" };
|
|
5611
|
+
}
|
|
5612
|
+
const candidate = parsed;
|
|
5613
|
+
if (candidate.generator !== GENERATOR_NAME) {
|
|
5614
|
+
return { marker: null, problem: "its .codebase-snapshot marker names a different generator" };
|
|
5615
|
+
}
|
|
5616
|
+
if (typeof candidate.schemaVersion !== "string" || !SUPPORTED_MARKER_SCHEMAS.has(candidate.schemaVersion)) {
|
|
5617
|
+
return {
|
|
5618
|
+
marker: null,
|
|
5619
|
+
problem: `its .codebase-snapshot marker has a schema version this build does not understand (${String(candidate.schemaVersion)})`
|
|
5620
|
+
};
|
|
5621
|
+
}
|
|
5622
|
+
if (typeof candidate.id !== "string" || !SNAPSHOT_ID_PATTERN.test(candidate.id)) {
|
|
5623
|
+
return { marker: null, problem: "its .codebase-snapshot marker carries no snapshot id" };
|
|
5624
|
+
}
|
|
5625
|
+
if (candidate.state !== "staging" && candidate.state !== "complete") {
|
|
5626
|
+
return { marker: null, problem: "its .codebase-snapshot marker records no run state" };
|
|
5627
|
+
}
|
|
5628
|
+
return {
|
|
5629
|
+
marker: {
|
|
5630
|
+
generator: GENERATOR_NAME,
|
|
5631
|
+
version: typeof candidate.version === "string" ? candidate.version : "unknown",
|
|
5632
|
+
schemaVersion: candidate.schemaVersion,
|
|
5633
|
+
createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
|
|
5634
|
+
state: candidate.state,
|
|
5635
|
+
id: candidate.id,
|
|
5636
|
+
manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
|
|
5637
|
+
entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
|
|
5638
|
+
},
|
|
5639
|
+
problem: null
|
|
5640
|
+
};
|
|
5641
|
+
}
|
|
5642
|
+
function describeManifestProblem(value) {
|
|
5643
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "is not a JSON object";
|
|
5644
|
+
const candidate = value;
|
|
5645
|
+
const generator = candidate.generator;
|
|
5646
|
+
if (typeof generator !== "object" || generator === null || generator.name !== GENERATOR_NAME) {
|
|
5647
|
+
return "was not written by this generator";
|
|
5648
|
+
}
|
|
5649
|
+
if (!Array.isArray(candidate.files)) return "has no files array";
|
|
5650
|
+
return null;
|
|
5651
|
+
}
|
|
5652
|
+
function assessSnapshotDirectory(dir) {
|
|
5653
|
+
const { marker: marker2, problem } = readOwnershipMarker(dir);
|
|
5654
|
+
if (!marker2) return { owned: false, problem, foreignEntries: [], marker: null };
|
|
5655
|
+
if (marker2.state !== "complete") {
|
|
5656
|
+
return {
|
|
5657
|
+
owned: false,
|
|
5658
|
+
problem: "its .codebase-snapshot marker records a run that never finished writing it",
|
|
5659
|
+
foreignEntries: [],
|
|
5660
|
+
marker: marker2
|
|
5661
|
+
};
|
|
5662
|
+
}
|
|
5663
|
+
const manifestPath = path10.join(dir, "manifest.json");
|
|
5664
|
+
if (fs9.existsSync(manifestPath)) {
|
|
5665
|
+
let raw;
|
|
5666
|
+
try {
|
|
5667
|
+
raw = fs9.readFileSync(manifestPath, "utf8");
|
|
5668
|
+
} catch {
|
|
5669
|
+
return {
|
|
5670
|
+
owned: false,
|
|
5671
|
+
problem: "its manifest.json could not be read",
|
|
5672
|
+
foreignEntries: [],
|
|
5673
|
+
marker: marker2
|
|
5674
|
+
};
|
|
5675
|
+
}
|
|
5676
|
+
let parsed;
|
|
5677
|
+
try {
|
|
5678
|
+
parsed = JSON.parse(raw);
|
|
5679
|
+
} catch {
|
|
5680
|
+
return {
|
|
5681
|
+
owned: false,
|
|
5682
|
+
problem: "its manifest.json is not valid JSON",
|
|
5683
|
+
foreignEntries: [],
|
|
5684
|
+
marker: marker2
|
|
5685
|
+
};
|
|
5686
|
+
}
|
|
5687
|
+
const manifestProblem2 = describeManifestProblem(parsed);
|
|
5688
|
+
if (manifestProblem2) {
|
|
5689
|
+
return {
|
|
5690
|
+
owned: false,
|
|
5691
|
+
problem: `its manifest.json ${manifestProblem2}`,
|
|
5692
|
+
foreignEntries: [],
|
|
5693
|
+
marker: marker2
|
|
5694
|
+
};
|
|
5695
|
+
}
|
|
5696
|
+
if (!marker2.manifestSha256) {
|
|
5697
|
+
return {
|
|
5698
|
+
owned: false,
|
|
5699
|
+
problem: "its .codebase-snapshot marker records no manifest digest",
|
|
5700
|
+
foreignEntries: [],
|
|
5701
|
+
marker: marker2
|
|
5702
|
+
};
|
|
5703
|
+
}
|
|
5704
|
+
if (sha256Hex2(raw) !== marker2.manifestSha256) {
|
|
5705
|
+
return {
|
|
5706
|
+
owned: false,
|
|
5707
|
+
problem: "its manifest.json does not match the digest recorded in its .codebase-snapshot marker",
|
|
5708
|
+
foreignEntries: [],
|
|
5709
|
+
marker: marker2
|
|
5710
|
+
};
|
|
5711
|
+
}
|
|
5712
|
+
} else if (marker2.manifestSha256) {
|
|
5713
|
+
return {
|
|
5714
|
+
owned: false,
|
|
5715
|
+
problem: "its manifest.json is missing although its marker records one",
|
|
5716
|
+
foreignEntries: [],
|
|
5717
|
+
marker: marker2
|
|
5718
|
+
};
|
|
4729
5719
|
}
|
|
5720
|
+
const recorded = new Set(marker2.entries ?? []);
|
|
5721
|
+
if (recorded.size === 0) {
|
|
5722
|
+
return {
|
|
5723
|
+
owned: false,
|
|
5724
|
+
problem: "its .codebase-snapshot marker records no contents",
|
|
5725
|
+
foreignEntries: [],
|
|
5726
|
+
marker: marker2
|
|
5727
|
+
};
|
|
5728
|
+
}
|
|
5729
|
+
const foreignEntries = listEntries(dir).filter(
|
|
5730
|
+
(entry) => !recorded.has(entry) && !PLACEHOLDER_ENTRIES.has(entry)
|
|
5731
|
+
);
|
|
5732
|
+
return {
|
|
5733
|
+
owned: foreignEntries.length === 0,
|
|
5734
|
+
problem: null,
|
|
5735
|
+
foreignEntries,
|
|
5736
|
+
marker: marker2
|
|
5737
|
+
};
|
|
5738
|
+
}
|
|
5739
|
+
function describeEntries(entries, limit = 5) {
|
|
5740
|
+
const shown = entries.slice(0, limit).join(", ");
|
|
5741
|
+
return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
|
|
4730
5742
|
}
|
|
4731
5743
|
function assertReplaceable(target, label) {
|
|
4732
5744
|
if (!fs9.existsSync(target)) return;
|
|
@@ -4736,16 +5748,124 @@ function assertReplaceable(target, label) {
|
|
|
4736
5748
|
);
|
|
4737
5749
|
}
|
|
4738
5750
|
const entries = fs9.readdirSync(target);
|
|
4739
|
-
|
|
4740
|
-
if (
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
5751
|
+
if (entries.length === 0) return;
|
|
5752
|
+
if (label === "outDir" && entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
|
|
5753
|
+
const { marker: marker2, problem } = readOwnershipMarker(target);
|
|
5754
|
+
if (label === "tmpDir") {
|
|
5755
|
+
if (!marker2) {
|
|
5756
|
+
throw new Error(
|
|
5757
|
+
`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.`
|
|
5758
|
+
);
|
|
5759
|
+
}
|
|
5760
|
+
const unexpected = entries.filter((name) => !STAGING_ENTRIES.has(name));
|
|
5761
|
+
if (unexpected.length > 0) {
|
|
5762
|
+
throw new Error(
|
|
5763
|
+
`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.`
|
|
5764
|
+
);
|
|
5765
|
+
}
|
|
5766
|
+
return;
|
|
5767
|
+
}
|
|
5768
|
+
const verdict = assessSnapshotDirectory(target);
|
|
5769
|
+
if (verdict.owned) return;
|
|
5770
|
+
if (verdict.foreignEntries.length > 0) {
|
|
5771
|
+
throw new Error(
|
|
5772
|
+
`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.`
|
|
5773
|
+
);
|
|
5774
|
+
}
|
|
5775
|
+
const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
|
|
4744
5776
|
throw new Error(
|
|
4745
|
-
`Refusing to replace ${label} ${target}:
|
|
5777
|
+
`Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(entries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
|
|
5778
|
+
);
|
|
5779
|
+
}
|
|
5780
|
+
function ensureParentDirectory(target, label) {
|
|
5781
|
+
const parent = path10.dirname(target);
|
|
5782
|
+
if (fs9.existsSync(parent) && !fs9.statSync(parent).isDirectory()) {
|
|
5783
|
+
throw new Error(
|
|
5784
|
+
`Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
|
|
5785
|
+
);
|
|
5786
|
+
}
|
|
5787
|
+
try {
|
|
5788
|
+
fs9.mkdirSync(parent, { recursive: true });
|
|
5789
|
+
} catch (err) {
|
|
5790
|
+
throw new Error(
|
|
5791
|
+
`Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
|
|
5792
|
+
);
|
|
5793
|
+
}
|
|
5794
|
+
}
|
|
5795
|
+
function acquireRunLock(outDir) {
|
|
5796
|
+
const lockPath = path10.join(
|
|
5797
|
+
os.tmpdir(),
|
|
5798
|
+
`codebase-snapshot-${sha256Hex2(outDir).slice(0, 16)}.lock`
|
|
4746
5799
|
);
|
|
5800
|
+
const payload = `${JSON.stringify({
|
|
5801
|
+
pid: process.pid,
|
|
5802
|
+
outDir,
|
|
5803
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5804
|
+
})}
|
|
5805
|
+
`;
|
|
5806
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
5807
|
+
try {
|
|
5808
|
+
const handle = fs9.openSync(lockPath, "wx");
|
|
5809
|
+
try {
|
|
5810
|
+
fs9.writeSync(handle, payload);
|
|
5811
|
+
} finally {
|
|
5812
|
+
fs9.closeSync(handle);
|
|
5813
|
+
}
|
|
5814
|
+
return () => {
|
|
5815
|
+
try {
|
|
5816
|
+
fs9.rmSync(lockPath, { force: true });
|
|
5817
|
+
} catch {
|
|
5818
|
+
}
|
|
5819
|
+
};
|
|
5820
|
+
} catch (err) {
|
|
5821
|
+
if (err.code !== "EEXIST") throw err;
|
|
5822
|
+
if (attempt === 0 && lockOwnerIsGone(lockPath)) {
|
|
5823
|
+
fs9.rmSync(lockPath, { force: true });
|
|
5824
|
+
continue;
|
|
5825
|
+
}
|
|
5826
|
+
throw new Error(
|
|
5827
|
+
`Another generate run is already writing ${outDir}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
|
|
5828
|
+
);
|
|
5829
|
+
}
|
|
5830
|
+
}
|
|
5831
|
+
throw new Error(`Could not acquire the generate lock for ${outDir}.`);
|
|
5832
|
+
}
|
|
5833
|
+
function lockOwnerIsGone(lockPath) {
|
|
5834
|
+
let raw;
|
|
5835
|
+
try {
|
|
5836
|
+
raw = fs9.readFileSync(lockPath, "utf8");
|
|
5837
|
+
} catch {
|
|
5838
|
+
return false;
|
|
5839
|
+
}
|
|
5840
|
+
let pid;
|
|
5841
|
+
try {
|
|
5842
|
+
pid = JSON.parse(raw).pid;
|
|
5843
|
+
} catch {
|
|
5844
|
+
return false;
|
|
5845
|
+
}
|
|
5846
|
+
if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
|
|
5847
|
+
try {
|
|
5848
|
+
process.kill(pid, 0);
|
|
5849
|
+
return false;
|
|
5850
|
+
} catch (err) {
|
|
5851
|
+
return err.code === "ESRCH";
|
|
5852
|
+
}
|
|
5853
|
+
}
|
|
5854
|
+
function writeManifestAtomically(manifestPath, manifest) {
|
|
5855
|
+
const tempPath = `${manifestPath}.${process.pid}.tmp`;
|
|
5856
|
+
fs9.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
5857
|
+
try {
|
|
5858
|
+
fs9.renameSync(tempPath, manifestPath);
|
|
5859
|
+
} catch (err) {
|
|
5860
|
+
try {
|
|
5861
|
+
fs9.rmSync(tempPath, { force: true });
|
|
5862
|
+
} catch {
|
|
5863
|
+
}
|
|
5864
|
+
throw err;
|
|
5865
|
+
}
|
|
4747
5866
|
}
|
|
4748
5867
|
function swapIntoPlace(tmpDir, outDir) {
|
|
5868
|
+
ensureParentDirectory(outDir, "outDir");
|
|
4749
5869
|
const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
|
|
4750
5870
|
const hadPrevious = fs9.existsSync(outDir);
|
|
4751
5871
|
if (hadPrevious) fs9.renameSync(outDir, backupDir);
|
|
@@ -4834,6 +5954,7 @@ function generateSnapshot(configInput, options) {
|
|
|
4834
5954
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
4835
5955
|
});
|
|
4836
5956
|
const files = [];
|
|
5957
|
+
const monolithBodies = /* @__PURE__ */ new Map();
|
|
4837
5958
|
const skipped = [...collection.skipped];
|
|
4838
5959
|
const secretFindings = [];
|
|
4839
5960
|
const warnings = [];
|
|
@@ -4897,6 +6018,9 @@ function generateSnapshot(configInput, options) {
|
|
|
4897
6018
|
isPreviewTruncated = preview.isTruncated;
|
|
4898
6019
|
includedLines = preview.includedLines;
|
|
4899
6020
|
}
|
|
6021
|
+
if (config.includeMonolith && isPreviewTruncated) {
|
|
6022
|
+
monolithBodies.set(relativePath, redacted);
|
|
6023
|
+
}
|
|
4900
6024
|
let summary;
|
|
4901
6025
|
let summaryQuality;
|
|
4902
6026
|
try {
|
|
@@ -4939,7 +6063,12 @@ function generateSnapshot(configInput, options) {
|
|
|
4939
6063
|
).join("\n")
|
|
4940
6064
|
);
|
|
4941
6065
|
const fullContents = /* @__PURE__ */ new Map();
|
|
4942
|
-
for (const file of files)
|
|
6066
|
+
for (const file of files) {
|
|
6067
|
+
fullContents.set(
|
|
6068
|
+
file.relativePath,
|
|
6069
|
+
monolithBodies.get(file.relativePath) ?? file.content ?? ""
|
|
6070
|
+
);
|
|
6071
|
+
}
|
|
4943
6072
|
const parts = partitionByBudget(files, config, fingerprint);
|
|
4944
6073
|
const maxEffectiveBudget = Math.max(
|
|
4945
6074
|
config.maxPartBytes,
|
|
@@ -5025,10 +6154,17 @@ function generateSnapshot(configInput, options) {
|
|
|
5025
6154
|
let monolithJson = null;
|
|
5026
6155
|
let monolithMarkdown = null;
|
|
5027
6156
|
if (config.includeMonolith) {
|
|
5028
|
-
const fullFiles = files.map((file) =>
|
|
5029
|
-
|
|
5030
|
-
|
|
5031
|
-
|
|
6157
|
+
const fullFiles = files.map((file) => {
|
|
6158
|
+
const content = fullContents.get(file.relativePath) ?? "";
|
|
6159
|
+
return {
|
|
6160
|
+
...file,
|
|
6161
|
+
content,
|
|
6162
|
+
inclusionMode: "full",
|
|
6163
|
+
isPreviewTruncated: false,
|
|
6164
|
+
includedBytes: Buffer.byteLength(content, "utf8"),
|
|
6165
|
+
includedLines: countLines(content)
|
|
6166
|
+
};
|
|
6167
|
+
});
|
|
5032
6168
|
const json = generateMonolithJson(manifest, fullFiles);
|
|
5033
6169
|
const markdown = generateMonolithMarkdown(manifest, fullFiles);
|
|
5034
6170
|
const jsonBytes = Buffer.byteLength(json, "utf8");
|
|
@@ -5056,62 +6192,94 @@ function generateSnapshot(configInput, options) {
|
|
|
5056
6192
|
}
|
|
5057
6193
|
for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
|
|
5058
6194
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
6195
|
+
const { outDir, tmpDir } = resolveSafeOutputDirs(config);
|
|
6196
|
+
const { artifacts } = config;
|
|
6197
|
+
assertReplaceable(tmpDir, "tmpDir");
|
|
6198
|
+
assertReplaceable(outDir, "outDir");
|
|
5059
6199
|
if (dryRun) {
|
|
5060
6200
|
runAfterGenerate(config, manifest);
|
|
5061
6201
|
return manifest;
|
|
5062
6202
|
}
|
|
5063
|
-
const
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
6203
|
+
const releaseLock = acquireRunLock(outDir);
|
|
6204
|
+
let stagingStarted = false;
|
|
6205
|
+
try {
|
|
6206
|
+
ensureParentDirectory(outDir, "outDir");
|
|
6207
|
+
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
6208
|
+
fs9.mkdirSync(tmpDir, { recursive: true });
|
|
6209
|
+
stagingStarted = true;
|
|
6210
|
+
const snapshotId = crypto.randomUUID();
|
|
6211
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6212
|
+
writeOwnershipMarker(tmpDir, {
|
|
6213
|
+
generator: GENERATOR_NAME,
|
|
6214
|
+
version: GENERATOR_VERSION,
|
|
6215
|
+
schemaVersion: SCHEMA_VERSION,
|
|
6216
|
+
createdAt,
|
|
6217
|
+
state: "staging",
|
|
6218
|
+
id: snapshotId
|
|
6219
|
+
});
|
|
6220
|
+
const partsDir = path10.join(tmpDir, "parts");
|
|
6221
|
+
const reportsDir = path10.join(tmpDir, "reports");
|
|
6222
|
+
if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
|
|
6223
|
+
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
6224
|
+
fs9.mkdirSync(reportsDir, { recursive: true });
|
|
6225
|
+
}
|
|
6226
|
+
if (artifacts.parts) {
|
|
6227
|
+
for (const part of parts) {
|
|
6228
|
+
fs9.writeFileSync(path10.join(partsDir, part.filename), part.markdown, "utf8");
|
|
6229
|
+
}
|
|
5079
6230
|
}
|
|
6231
|
+
if (artifacts.projectTree) {
|
|
6232
|
+
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
6233
|
+
fs9.writeFileSync(path10.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
6234
|
+
}
|
|
6235
|
+
if (artifacts.navigation) {
|
|
6236
|
+
fs9.writeFileSync(path10.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
6237
|
+
}
|
|
6238
|
+
if (artifacts.audit) {
|
|
6239
|
+
fs9.writeFileSync(
|
|
6240
|
+
path10.join(reportsDir, "audit.md"),
|
|
6241
|
+
generateAuditReport(manifest, secretFindings),
|
|
6242
|
+
"utf8"
|
|
6243
|
+
);
|
|
6244
|
+
}
|
|
6245
|
+
if (artifacts.readme) {
|
|
6246
|
+
fs9.writeFileSync(path10.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
|
|
6247
|
+
}
|
|
6248
|
+
if (artifacts.manifest) {
|
|
6249
|
+
fs9.writeFileSync(path10.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
6250
|
+
}
|
|
6251
|
+
if (monolithJson !== null) {
|
|
6252
|
+
fs9.writeFileSync(path10.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
|
|
6253
|
+
}
|
|
6254
|
+
if (monolithMarkdown !== null) {
|
|
6255
|
+
fs9.writeFileSync(path10.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
6256
|
+
}
|
|
6257
|
+
writeOwnershipMarker(tmpDir, markerPayload(tmpDir, "complete", snapshotId, createdAt));
|
|
6258
|
+
swapIntoPlace(tmpDir, outDir);
|
|
6259
|
+
stagingStarted = false;
|
|
6260
|
+
const warningsBeforeHook = manifest.warnings.length;
|
|
6261
|
+
runAfterGenerate(config, manifest);
|
|
6262
|
+
if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
|
|
6263
|
+
writeManifestAtomically(path10.join(outDir, "manifest.json"), manifest);
|
|
6264
|
+
}
|
|
6265
|
+
writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
|
|
6266
|
+
return manifest;
|
|
6267
|
+
} catch (err) {
|
|
6268
|
+
if (stagingStarted) {
|
|
6269
|
+
try {
|
|
6270
|
+
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
6271
|
+
} catch {
|
|
6272
|
+
}
|
|
6273
|
+
}
|
|
6274
|
+
throw err;
|
|
6275
|
+
} finally {
|
|
6276
|
+
releaseLock();
|
|
5080
6277
|
}
|
|
5081
|
-
if (artifacts.projectTree) {
|
|
5082
|
-
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
5083
|
-
fs9.writeFileSync(path10.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
5084
|
-
}
|
|
5085
|
-
if (artifacts.navigation) {
|
|
5086
|
-
fs9.writeFileSync(path10.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
5087
|
-
}
|
|
5088
|
-
if (artifacts.audit) {
|
|
5089
|
-
fs9.writeFileSync(
|
|
5090
|
-
path10.join(reportsDir, "audit.md"),
|
|
5091
|
-
generateAuditReport(manifest, secretFindings),
|
|
5092
|
-
"utf8"
|
|
5093
|
-
);
|
|
5094
|
-
}
|
|
5095
|
-
if (artifacts.readme) {
|
|
5096
|
-
fs9.writeFileSync(path10.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
|
|
5097
|
-
}
|
|
5098
|
-
if (artifacts.manifest) {
|
|
5099
|
-
fs9.writeFileSync(path10.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
5100
|
-
}
|
|
5101
|
-
if (monolithJson !== null) {
|
|
5102
|
-
fs9.writeFileSync(path10.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
|
|
5103
|
-
}
|
|
5104
|
-
if (monolithMarkdown !== null) {
|
|
5105
|
-
fs9.writeFileSync(path10.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
5106
|
-
}
|
|
5107
|
-
swapIntoPlace(tmpDir, outDir);
|
|
5108
|
-
runAfterGenerate(config, manifest);
|
|
5109
|
-
return manifest;
|
|
5110
6278
|
}
|
|
5111
6279
|
async function generateSnapshotAsync(configInput, options) {
|
|
5112
6280
|
return generateSnapshot(configInput, options);
|
|
5113
6281
|
}
|
|
5114
6282
|
|
|
5115
|
-
export { ARTIFACT_NAMES, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, GENERATOR_NAME, GENERATOR_VERSION, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, PRESETS, PromptCancelled, SCHEMA_VERSION, anchorForPath, assertReplaceable, assertUsableConfigInput, collectFiles, compareManifests, countLines, defineConfig, detectLanguage, detectPreset, explainPath, fenceForContent, generateProjectTree, generateSnapshot, generateSnapshotAsync, getPreset, globToRegExp, isBinaryBuffer, isBinaryExtension, isBinaryFile, isInteractive, isResolvedConfig, isSensitivePath, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, profileForPath, renderConfig, renderDiffText, renderExplainText, renderMinimalConfig, renumberParts, resolveConfig, resolveSafeOutputDirs, runInit, scanAndRedactSecrets, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, validateSnapshot };
|
|
5116
|
-
//# sourceMappingURL=chunk-
|
|
5117
|
-
//# sourceMappingURL=chunk-
|
|
6283
|
+
export { ARTIFACT_NAMES, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, GENERATOR_NAME, GENERATOR_VERSION, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, PRESETS, PromptCancelled, SCHEMA_VERSION, anchorForPath, assertReplaceable, assertUsableConfigInput, collectFiles, compareManifests, countLines, defineConfig, detectLanguage, detectPreset, explainPath, fenceForContent, fenceForText, generateProjectTree, generateSnapshot, generateSnapshotAsync, getPreset, globToRegExp, isBinaryBuffer, isBinaryExtension, isBinaryFile, isInteractive, isResolvedConfig, isSensitivePath, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, profileForPath, renderConfig, renderDiffText, renderExplainText, renderMinimalConfig, renumberParts, resolveConfig, resolveSafeOutputDirs, runInit, scanAndRedactSecrets, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, validateSnapshot };
|
|
6284
|
+
//# sourceMappingURL=chunk-FM4OOQPF.js.map
|
|
6285
|
+
//# sourceMappingURL=chunk-FM4OOQPF.js.map
|