@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/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  var crypto = require('crypto');
4
4
  var fs9 = require('fs');
5
+ var os = require('os');
5
6
  var path10 = require('path');
6
7
  var ts = require('typescript');
7
8
  var url = require('url');
@@ -30,6 +31,7 @@ function _interopNamespace(e) {
30
31
 
31
32
  var crypto__namespace = /*#__PURE__*/_interopNamespace(crypto);
32
33
  var fs9__namespace = /*#__PURE__*/_interopNamespace(fs9);
34
+ var os__namespace = /*#__PURE__*/_interopNamespace(os);
33
35
  var path10__namespace = /*#__PURE__*/_interopNamespace(path10);
34
36
  var ts__default = /*#__PURE__*/_interopDefault(ts);
35
37
  var childProcess__namespace = /*#__PURE__*/_interopNamespace(childProcess);
@@ -576,14 +578,42 @@ function summarizeTypeScriptSource(relativePath, content) {
576
578
  const modifiers = ts__default.default.canHaveModifiers(node) ? ts__default.default.getModifiers(node) : void 0;
577
579
  return Boolean(modifiers?.some((m) => m.kind === ts__default.default.SyntaxKind.DefaultKeyword));
578
580
  };
579
- const recordFunctionLike = (name, exported) => {
581
+ const rendersJsx = (node) => {
582
+ const pending = [node];
583
+ while (pending.length > 0) {
584
+ const current = pending.pop();
585
+ if (ts__default.default.isJsxElement(current) || ts__default.default.isJsxSelfClosingElement(current) || ts__default.default.isJsxFragment(current)) {
586
+ return true;
587
+ }
588
+ if (ts__default.default.isCallExpression(current)) {
589
+ const callee = current.expression;
590
+ const calleeName = ts__default.default.isIdentifier(callee) ? callee.text : ts__default.default.isPropertyAccessExpression(callee) ? callee.name.text : "";
591
+ if (calleeName === "createElement") return true;
592
+ }
593
+ ts__default.default.forEachChild(current, (child) => {
594
+ pending.push(child);
595
+ });
596
+ }
597
+ return false;
598
+ };
599
+ const REACT_BASE_CLASS = /(?:^|\.)(?:PureComponent|Component)$/;
600
+ const extendsReactBase = (node) => {
601
+ const heritage = (node.heritageClauses ?? []).find(
602
+ (clause) => clause.token === ts__default.default.SyntaxKind.ExtendsKeyword
603
+ );
604
+ const expression = heritage?.types[0]?.expression;
605
+ if (!expression) return false;
606
+ const text2 = ts__default.default.isIdentifier(expression) ? expression.text : expression.getText(sourceFile);
607
+ return REACT_BASE_CLASS.test(text2);
608
+ };
609
+ const recordFunctionLike = (name, exported, node) => {
580
610
  summary.functions.push(name);
581
611
  if (exported) {
582
612
  summary.exports.push(name);
583
613
  if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
584
614
  }
585
615
  if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
586
- if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
616
+ if (/^[A-Z]/.test(name) && rendersJsx(node)) summary.reactComponents.push(name);
587
617
  };
588
618
  const stack = [sourceFile];
589
619
  while (stack.length > 0) {
@@ -612,7 +642,7 @@ function summarizeTypeScriptSource(relativePath, content) {
612
642
  if (ts__default.default.isFunctionDeclaration(node)) {
613
643
  const exported = isExported(node);
614
644
  if (node.name) {
615
- recordFunctionLike(node.name.text, exported);
645
+ recordFunctionLike(node.name.text, exported, node);
616
646
  if (isDefault(node)) summary.exports.push("default");
617
647
  } else if (isDefault(node)) {
618
648
  summary.exports.push("default");
@@ -623,7 +653,9 @@ function summarizeTypeScriptSource(relativePath, content) {
623
653
  summary.classes.push(node.name.text);
624
654
  if (isExported(node)) summary.exports.push(node.name.text);
625
655
  if (isDefault(node)) summary.exports.push("default");
626
- if (/^[A-Z]/.test(node.name.text)) summary.reactComponents.push(node.name.text);
656
+ if (/^[A-Z]/.test(node.name.text) && (extendsReactBase(node) || rendersJsx(node))) {
657
+ summary.reactComponents.push(node.name.text);
658
+ }
627
659
  } else if (isDefault(node)) {
628
660
  summary.exports.push("default");
629
661
  }
@@ -651,7 +683,9 @@ function summarizeTypeScriptSource(relativePath, content) {
651
683
  if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
652
684
  }
653
685
  if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
654
- if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
686
+ if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
687
+ summary.reactComponents.push(name);
688
+ }
655
689
  }
656
690
  }
657
691
  const children = [];
@@ -1705,6 +1739,416 @@ function toDetected(preset) {
1705
1739
  function detectPreset(cwd, forced) {
1706
1740
  return detectPresetFromContext(buildPresetContext(cwd), forced);
1707
1741
  }
1742
+
1743
+ // src/security/patterns.ts
1744
+ var STRONG_CREDENTIAL_NAME = "(?:password|passwd|passphrase|pwd|api[_-]?key|api[_-]?secret|secret[_-]?access[_-]?key|secret[_-]?key|client[_-]?secret[_-]?key|client[_-]?secret|private[_-]?key|aws[_-]?secret[_-]?access[_-]?key|access[_-]?token|refresh[_-]?token|auth[_-]?token|session[_-]?token|secret|token)";
1745
+ var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
1746
+ var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|process\\.env|Buffer|Record|object)\\b)";
1747
+ var UNQUOTED_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{1,512})(?![A-Za-z0-9_+=/.-]|\\()";
1748
+ var UNQUOTED_AMBIGUOUS_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{8,512})(?![A-Za-z0-9_+=/.-]|\\()";
1749
+ var UNQUOTED_STRONG_NAME = `(?<!id[-_])${STRONG_CREDENTIAL_NAME}`;
1750
+ var DEFAULT_SECRET_PATTERNS = [
1751
+ {
1752
+ rule: "private-key-header",
1753
+ pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
1754
+ },
1755
+ {
1756
+ rule: "jwt-token",
1757
+ pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
1758
+ },
1759
+ {
1760
+ rule: "aws-access-key-id",
1761
+ pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
1762
+ },
1763
+ {
1764
+ // The name is no longer limited to the three snake_case spellings: an
1765
+ // `awsSecret` / `secretAccessKey` property leaked verbatim because only the
1766
+ // literal words matched. The value is masked alone, so the key survives.
1767
+ rule: "aws-secret-access-key",
1768
+ pattern: new RegExp(
1769
+ `${STRONG_CREDENTIAL_NAME}["']?\\s{0,8}[:=]\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
1770
+ "i"
1771
+ ),
1772
+ valueGroup: 1
1773
+ },
1774
+ {
1775
+ // A 40-character base64 value with no key name in sight. A key-independent
1776
+ // rule is the only thing that catches `new AWS.SecretKey(…)` arguments and
1777
+ // pasted `~/.aws/credentials` fragments, but a bare length test also matches
1778
+ // every 40-char git SHA-1, so the value must mix case and digits and must
1779
+ // not be pure hex.
1780
+ rule: "aws-secret-access-key-value",
1781
+ pattern: /(?<![A-Za-z0-9/+=])(?![0-9a-fA-F]{40}(?![A-Za-z0-9/+=]))(?=[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=]))(?=[A-Za-z0-9/+=]{0,39}[A-Z])(?=[A-Za-z0-9/+=]{0,39}[a-z])(?=[A-Za-z0-9/+=]{0,39}[0-9])[A-Za-z0-9/+=]{40}/
1782
+ },
1783
+ {
1784
+ rule: "github-token",
1785
+ pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
1786
+ },
1787
+ {
1788
+ // `xoxc-`/`xoxd-` are the user and refresh tokens, `xoxb`/`xoxp` the bot and
1789
+ // user tokens; the whole Slack family shares one prefix shape.
1790
+ rule: "slack-token",
1791
+ pattern: /xox[baprsdc]-[0-9A-Za-z-]{10,250}/
1792
+ },
1793
+ {
1794
+ // Prefix-only vendors. Each prefix below is documented, globally unique and
1795
+ // cannot collide with an ordinary identifier, which is what makes a bare
1796
+ // prefix rule cheap and safe: `glpat-` (GitLab), `hvs.`/`s.` (Vault service
1797
+ // and batch tokens), `whsec_` (Stripe webhook secret), `dckr_pat_` (Docker
1798
+ // Hub), `hf_` (HuggingFace), `ya29.` (Google OAuth access token).
1799
+ rule: "known-vendor-token-prefix",
1800
+ pattern: /\b(?:glpat-[A-Za-z0-9_-]{20,64}|hvs\.[A-Za-z0-9_-]{20,128}|s\.[A-Za-z0-9]{20,64}|whsec_[A-Za-z0-9]{20,64}|dckr_pat_[A-Za-z0-9_-]{20,64}|hf_[A-Za-z0-9]{30,64}|ya29\.[A-Za-z0-9_-]{20,128})/
1801
+ },
1802
+ {
1803
+ rule: "slack-webhook-url",
1804
+ pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
1805
+ },
1806
+ {
1807
+ rule: "stripe-secret-key",
1808
+ pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
1809
+ },
1810
+ {
1811
+ rule: "google-api-key",
1812
+ pattern: /AIza[0-9A-Za-z_-]{35}/
1813
+ },
1814
+ {
1815
+ rule: "google-oauth-client-secret",
1816
+ pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
1817
+ },
1818
+ {
1819
+ rule: "openai-anthropic-key",
1820
+ pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
1821
+ },
1822
+ {
1823
+ rule: "npm-token",
1824
+ pattern: /npm_[A-Za-z0-9]{36}/
1825
+ },
1826
+ {
1827
+ rule: "pypi-token",
1828
+ pattern: /pypi-[A-Za-z0-9_-]{50,180}/
1829
+ },
1830
+ {
1831
+ rule: "sendgrid-key",
1832
+ pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
1833
+ },
1834
+ {
1835
+ rule: "twilio-api-key",
1836
+ pattern: /SK[0-9a-fA-F]{32}/
1837
+ },
1838
+ {
1839
+ rule: "digitalocean-token",
1840
+ pattern: /dop_v1_[0-9a-f]{64}/
1841
+ },
1842
+ {
1843
+ rule: "azure-storage-account-key",
1844
+ pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
1845
+ },
1846
+ {
1847
+ // Scheme-less userinfo: `//user:password@host`.
1848
+ //
1849
+ // Enumerating schemes is a losing game — `mysqlx`, `svn`, `telnet`, `imap`,
1850
+ // `pop3`, `nntp`, `kafka`, `elasticsearch` each leaked a password while the
1851
+ // list grew by hand. This rule matches the shape instead, so any scheme
1852
+ // (including one invented tomorrow) is covered, provided the `//` is followed
1853
+ // immediately by the userinfo: `// see the docs` and a bare reminder comment
1854
+ // never match, because a space or the comment wording breaks the shape.
1855
+ rule: "uri-credentials",
1856
+ pattern: /\/\/[^\s:/@"'`;()<>,]{1,128}:[^\s:/@"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
1857
+ },
1858
+ {
1859
+ // The host tail stops at quotes, `;`, `)`, `<`, `>` and `,`: an unbounded
1860
+ // `[^\s/]` used to swallow the closing quote and the statement separator of
1861
+ // the string the URL was written in.
1862
+ rule: "database-uri-credentials",
1863
+ pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse|mssql|sqlserver|oracle):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
1864
+ },
1865
+ {
1866
+ rule: "basic-auth-url",
1867
+ pattern: /(?:https?|ftp|sftp|ssh|git|smtp|smtps|ldap|ldaps):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/i
1868
+ },
1869
+ {
1870
+ // Quoted credentials in any common key spelling. The value may be as short
1871
+ // as one character: `password: "hunter2"` is a credential whatever its
1872
+ // entropy, and a length floor was exactly how it survived into `parts`.
1873
+ //
1874
+ // The key is either bare (`dbPassword:`) or wholly quoted (`"dbPassword":`).
1875
+ // There is deliberately no optional quote *before* the separator: that let a
1876
+ // previous string's closing quote stand in for the key's, so
1877
+ // ``${pass ? " PASS" : " FAIL"}`` had `" FAIL"` masked as if `PASS` were
1878
+ // the key name.
1879
+ rule: "credential-assignment",
1880
+ pattern: new RegExp(
1881
+ `(?:["']${STRONG_CREDENTIAL_NAME}["']|${STRONG_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{1,512})\\1`,
1882
+ "i"
1883
+ ),
1884
+ valueGroup: 2
1885
+ },
1886
+ {
1887
+ // `pass`, `id-token` and `credentials` are ambiguous on their own, so they
1888
+ // are accepted only against a quoted value long enough to be a credential.
1889
+ rule: "credential-assignment-weak",
1890
+ pattern: new RegExp(
1891
+ `(?:["']${WEAK_CREDENTIAL_NAME}["']|${WEAK_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{8,512})\\1`,
1892
+ "i"
1893
+ ),
1894
+ valueGroup: 2
1895
+ },
1896
+ {
1897
+ // The unquoted twin for YAML, docker-compose and `.env`: those formats never
1898
+ // quote their values, so the rules above cannot see them. `valueGroup` keeps
1899
+ // `POSTGRES_PASSWORD:` in the output.
1900
+ rule: "credential-assignment-unquoted",
1901
+ pattern: new RegExp(
1902
+ `${UNQUOTED_STRONG_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
1903
+ "i"
1904
+ ),
1905
+ valueGroup: 1
1906
+ },
1907
+ {
1908
+ // `pass: hunter2hunter2` and `credentials: hunter2hunter2`: the ambiguous
1909
+ // names, unquoted, with the length and non-digit floor that keeps
1910
+ // `pass: 3` / `credentials: none` readable.
1911
+ rule: "credential-assignment-unquoted-weak",
1912
+ pattern: new RegExp(
1913
+ `${WEAK_CREDENTIAL_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
1914
+ "i"
1915
+ ),
1916
+ valueGroup: 1
1917
+ },
1918
+ {
1919
+ // `Authorization: Bearer <token>` and `Authorization: Basic <base64>`.
1920
+ //
1921
+ // The scheme is consumed by a lookbehind so the match is the token alone and
1922
+ // needs no `valueGroup`. A lone `Bearer`/`Basic`, and a word after it that
1923
+ // cannot be a credential (`Bearer authentication`), stay readable: the value
1924
+ // must contain a digit, be at least 32 chars, or carry base64 `=` padding.
1925
+ rule: "authorization-header",
1926
+ pattern: /(?<=(?:Bearer|Basic)\s{1,8})(?:[A-Za-z0-9_.+/-]{0,31}[0-9][A-Za-z0-9_.+/-]{0,1024}|[A-Za-z0-9_.+/-]{32,1024}|[A-Za-z0-9_.+/-]{8,1024}={1,2})(?![A-Za-z0-9_.+/=-])/
1927
+ },
1928
+ {
1929
+ // Broader names than before (`apiKey`, `clientSecret`, `privateKey` were
1930
+ // invisible to it) and value-only masking. Kept alongside the two rules
1931
+ // above because it also matches a *quoted key* followed by an unquoted
1932
+ // value, e.g. `"client_secret": abc123`.
1933
+ rule: "generic-secret-assignment",
1934
+ pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["']([^"'\s]{8,256})["']/i,
1935
+ valueGroup: 1
1936
+ },
1937
+ {
1938
+ // Covers `.env` / shell assignment syntax, which never quotes its values and
1939
+ // was therefore invisible to the rule above. Anchored to the line start and
1940
+ // limited to secret-looking names to stay at zero false positives.
1941
+ //
1942
+ // The name is matched with BOUNDED quantifiers on purpose: an unbounded
1943
+ // `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
1944
+ // every offset of a long identifier, which made this rule quadratic
1945
+ // (measured 5.6 s for a single 240 KB line before bounding).
1946
+ // The value is matched on its own — `=\s*(value)` — and masked alone.
1947
+ // Anchoring the value to end-of-line (`[^\s#]{6,512}$`) meant ANY trailing
1948
+ // comment or space disabled the rule, so `TOKEN=secret # rotate` and
1949
+ // `TOKEN=secret ` were emitted in clear; a CRLF file disabled it wholesale.
1950
+ rule: "env-secret-assignment",
1951
+ pattern: /^\s{0,64}(?:export\s{1,8})?[A-Za-z0-9_]{0,96}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)[A-Za-z0-9_]{0,96}\s{0,8}=\s{0,8}(?!["'`$<{[\]\s])([^\s#;"'`)]{1,512})/i,
1952
+ valueGroup: 1
1953
+ }
1954
+ ];
1955
+ var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
1956
+ var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
1957
+
1958
+ // src/security/redact.ts
1959
+ var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
1960
+ var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
1961
+ function describeValue(value) {
1962
+ if (value === null) return "null";
1963
+ if (Array.isArray(value)) return "an array";
1964
+ if (value instanceof RegExp) return value.toString();
1965
+ if (typeof value === "string") return `the string ${JSON.stringify(value)}`;
1966
+ return `a ${typeof value}`;
1967
+ }
1968
+ function compilePattern(rule, pattern, valueGroup) {
1969
+ if (pattern instanceof RegExp) {
1970
+ const flags = new Set(pattern.flags.replace(/[gy]/g, "").split(""));
1971
+ if (valueGroup !== void 0) flags.add("d");
1972
+ return {
1973
+ rule,
1974
+ pattern: new RegExp(pattern.source, [...flags].join("")),
1975
+ ...valueGroup === void 0 ? {} : { valueGroup }
1976
+ };
1977
+ }
1978
+ if (typeof pattern === "string") {
1979
+ try {
1980
+ return {
1981
+ rule,
1982
+ pattern: new RegExp(pattern, valueGroup === void 0 ? "i" : "id"),
1983
+ ...valueGroup === void 0 ? {} : { valueGroup }
1984
+ };
1985
+ } catch (err) {
1986
+ throw new Error(
1987
+ `Invalid regular expression for secret rule "${rule}": ${err.message}`
1988
+ );
1989
+ }
1990
+ }
1991
+ throw new Error(
1992
+ `Secret rule "${rule}" has a pattern that is neither a RegExp nor a string (received ${describeValue(pattern)}).`
1993
+ );
1994
+ }
1995
+ function compileSecretRules(rules) {
1996
+ if (!Array.isArray(rules)) {
1997
+ throw new Error(
1998
+ `secrets.customPatterns must be an array of secret rules (received ${describeValue(rules)}).`
1999
+ );
2000
+ }
2001
+ let generated = 0;
2002
+ return rules.map((entry, index) => {
2003
+ const where = `secrets.customPatterns[${index}]`;
2004
+ if (entry instanceof RegExp) {
2005
+ generated += 1;
2006
+ return compilePattern(`custom-${generated}`, entry, void 0);
2007
+ }
2008
+ if (typeof entry === "string") {
2009
+ generated += 1;
2010
+ return compilePattern(`custom-${generated}`, entry, void 0);
2011
+ }
2012
+ if (typeof entry === "object" && entry !== null) {
2013
+ const candidate = entry;
2014
+ if (typeof candidate.rule !== "string" || candidate.rule.trim() === "") {
2015
+ throw new Error(
2016
+ `${where} is missing \`rule\` (received keys: ${Object.keys(candidate).join(", ") || "none"}). A named rule is { rule: "corp-token", pattern: /CORP-[A-Z0-9]{8}/ }, or pass the RegExp itself.`
2017
+ );
2018
+ }
2019
+ const valueGroup = candidate.valueGroup;
2020
+ if (valueGroup !== void 0) {
2021
+ if (typeof valueGroup !== "number" || !Number.isInteger(valueGroup) || valueGroup < 0) {
2022
+ throw new Error(
2023
+ `${where}.valueGroup must be a non-negative integer capture-group index (received ${describeValue(valueGroup)}).`
2024
+ );
2025
+ }
2026
+ }
2027
+ if (candidate.pattern === void 0) {
2028
+ throw new Error(
2029
+ `${where} (rule "${candidate.rule}") is missing \`pattern\`. Expected { rule, pattern: RegExp | string }.`
2030
+ );
2031
+ }
2032
+ return compilePattern(
2033
+ candidate.rule,
2034
+ candidate.pattern,
2035
+ valueGroup
2036
+ );
2037
+ }
2038
+ throw new Error(
2039
+ `${where} must be a RegExp, a string, or { rule, pattern } (received ${describeValue(entry)}).`
2040
+ );
2041
+ });
2042
+ }
2043
+ function toGlobal(rule) {
2044
+ const flags = new Set(rule.pattern.flags.replace(/[gy]/g, "").split(""));
2045
+ flags.add("g");
2046
+ if (rule.valueGroup !== void 0) flags.add("d");
2047
+ return new RegExp(rule.pattern.source, [...flags].join(""));
2048
+ }
2049
+ var LINE_BREAK = /(\r\n|\n|\r)/;
2050
+ function scanAndRedactSecrets(content, rules, filePath) {
2051
+ const findings = [];
2052
+ let redacted = content;
2053
+ for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
2054
+ blockPattern.lastIndex = 0;
2055
+ let match;
2056
+ while ((match = blockPattern.exec(redacted)) !== null) {
2057
+ if (match[0].length === 0) {
2058
+ blockPattern.lastIndex += 1;
2059
+ continue;
2060
+ }
2061
+ const line = redacted.slice(0, match.index).split("\n").length;
2062
+ const newlines = (match[0].match(/\n/g) ?? []).length;
2063
+ findings.push({ rule: "private-key-block", line, path: filePath });
2064
+ redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
2065
+ blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
2066
+ }
2067
+ }
2068
+ const compiled = rules.map((rule) => ({
2069
+ rule: rule.rule,
2070
+ pattern: toGlobal(rule),
2071
+ ...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
2072
+ }));
2073
+ const segments = redacted.split(LINE_BREAK);
2074
+ let lineNumber = 1;
2075
+ for (let index = 0; index < segments.length; index += 2) {
2076
+ const line = segments[index];
2077
+ if (line.length > 0) {
2078
+ const spans = [];
2079
+ const matchedRules = /* @__PURE__ */ new Set();
2080
+ for (const { rule, pattern, valueGroup } of compiled) {
2081
+ pattern.lastIndex = 0;
2082
+ let match;
2083
+ while ((match = pattern.exec(line)) !== null) {
2084
+ if (match[0].length === 0) {
2085
+ pattern.lastIndex += 1;
2086
+ continue;
2087
+ }
2088
+ const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
2089
+ spans.push(
2090
+ group ? { start: group[0], end: group[1], rule } : { start: match.index, end: match.index + match[0].length, rule }
2091
+ );
2092
+ matchedRules.add(rule);
2093
+ }
2094
+ }
2095
+ if (spans.length > 0) {
2096
+ for (const rule of matchedRules) {
2097
+ findings.push({ rule, line: lineNumber, path: filePath });
2098
+ }
2099
+ segments[index] = applySpans(line, spans);
2100
+ }
2101
+ }
2102
+ if (index + 1 < segments.length) lineNumber += 1;
2103
+ }
2104
+ return { redacted: segments.join(""), findings };
2105
+ }
2106
+ var TOKEN_CHARS_LEFT = /[A-Za-z0-9_+/-]/;
2107
+ var TOKEN_CHARS_RIGHT = /[A-Za-z0-9_+/=-]/;
2108
+ function expandOverToken(line, start, end) {
2109
+ let left = start;
2110
+ let right = end;
2111
+ while (left > 0 && TOKEN_CHARS_LEFT.test(line[left - 1])) left -= 1;
2112
+ while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
2113
+ return { start: left, end: right };
2114
+ }
2115
+ function applySpans(line, spans) {
2116
+ const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
2117
+ const merged = [];
2118
+ for (const span of ordered) {
2119
+ const clampedStart = Math.max(0, Math.min(span.start, line.length));
2120
+ const clampedEnd = Math.max(clampedStart, Math.min(span.end, line.length));
2121
+ const { start, end } = expandOverToken(line, clampedStart, clampedEnd);
2122
+ const last = merged[merged.length - 1];
2123
+ if (last && start <= last.end) last.end = Math.max(last.end, end);
2124
+ else merged.push({ start, end });
2125
+ }
2126
+ let out = "";
2127
+ let cursor = 0;
2128
+ for (const span of merged) {
2129
+ out += line.slice(cursor, span.start);
2130
+ out += REDACTION_MARKER;
2131
+ cursor = span.end;
2132
+ }
2133
+ out += line.slice(cursor);
2134
+ return out;
2135
+ }
2136
+
2137
+ // src/scanner/files.ts
2138
+ var PRESET_IGNORES = new Set(
2139
+ PRESETS.flatMap((preset) => preset.exclude ?? [])
2140
+ );
2141
+ function readLinkTarget(linkPath, fallback) {
2142
+ try {
2143
+ return fs9__namespace.readlinkSync(linkPath);
2144
+ } catch {
2145
+ return fallback;
2146
+ }
2147
+ }
2148
+ function isInside(root, candidate) {
2149
+ const relative4 = path10__namespace.relative(root, candidate);
2150
+ return relative4 === "" || !relative4.startsWith(`..${path10__namespace.sep}`) && relative4 !== ".." && !path10__namespace.isAbsolute(relative4);
2151
+ }
1708
2152
  function getGitInfo(cwd, fallbackBranch = "main") {
1709
2153
  try {
1710
2154
  const reported = childProcess__namespace.execSync("git rev-parse --abbrev-ref HEAD", {
@@ -1749,20 +2193,61 @@ var DEFAULT_IGNORED_FILES = [
1749
2193
  "bun.lockb",
1750
2194
  "composer.lock"
1751
2195
  ];
1752
- var SENSITIVE_BASENAME = /^(?:\.env(?:[._-][A-Za-z0-9_-]*)*|\.env(?:backup|bak|old|orig|copy|save)|\.envrc|\.npmrc(?:\.[A-Za-z0-9_-]+)?|\.yarnrc(?:\.yml)?|\.netrc(?:\.[A-Za-z0-9_-]+)?|\.git-credentials|\.htpasswd|\.pgpass|id_[a-z0-9_]+(?:\.[A-Za-z0-9_-]+)?|credentials(?:\.json)?|service[-_]?account(?:\.[a-z0-9_-]+)*\.json|secrets?\.(?:json|ya?ml))$/i;
2196
+ var SENSITIVE_BASENAME = /^(?:\.env(?:[._-][A-Za-z0-9_-]*)*|\.env(?:backup|bak|old|orig|copy|save)|\.envrc|\.npmrc(?:\.[A-Za-z0-9_-]+)?|\.yarnrc(?:\.yml)?|\.netrc(?:\.[A-Za-z0-9_-]+)?|\.git-credentials|\.htpasswd|\.pgpass|\.vault-token|vault[-_]agent[-_]token|id_[a-z0-9_]+(?:\.[A-Za-z0-9_-]+)?|credentials(?:\.(?:json|ya?ml|toml))?|service[-_]?account(?:\.[a-z0-9_-]+)*\.json|secrets?\.(?:json|ya?ml|toml)|env|[A-Za-z0-9_-]{1,64}\.env)$/i;
1753
2197
  var SENSITIVE_PATH = /(?:^|\/)\.(?:aws|ssh|gnupg)\/|(?:^|\/)\.docker\/config\.json$|(?:^|\/)\.kube\/config$/i;
1754
2198
  var SENSITIVE_EXT = /\.(?:pem|key|p12|pfx|jks|keystore|ppk|asc|kdbx|tfstate|tfvars)$/i;
1755
2199
  var TEMPLATE_SUFFIX = /\.(?:example|sample|template|dist)(?=$|[.])/i;
2200
+ var BACKUP_SUFFIX = /(?:~|\.(?:bak|backup|old|orig|save|copy|swp|swo|tmp|part))$/i;
2201
+ function normaliseName(name) {
2202
+ let normalised = name.trim().replace(/[. ]+$/, "");
2203
+ let previous = "";
2204
+ while (previous !== normalised) {
2205
+ previous = normalised;
2206
+ normalised = normalised.replace(BACKUP_SUFFIX, "");
2207
+ }
2208
+ return normalised;
2209
+ }
1756
2210
  var matchesSensitiveList = (posixPath, base) => SENSITIVE_BASENAME.test(base) || SENSITIVE_PATH.test(posixPath) || SENSITIVE_EXT.test(base);
1757
2211
  function isSensitivePath(relativePath) {
1758
2212
  const posixPath = relativePath.replace(/\\/g, "/");
1759
- const base = posixPath.split("/").pop() ?? posixPath;
2213
+ const raw = posixPath.split("/").pop() ?? posixPath;
2214
+ const base = normaliseName(raw);
1760
2215
  if (TEMPLATE_SUFFIX.test(base)) {
1761
2216
  const stripped = base.replace(new RegExp(TEMPLATE_SUFFIX.source, "gi"), "");
1762
2217
  if (matchesSensitiveList(posixPath, stripped)) return false;
1763
2218
  }
1764
2219
  return matchesSensitiveList(posixPath, base);
1765
2220
  }
2221
+ function findSecretInPath(relativePath) {
2222
+ const posixPath = relativePath.replace(/\\/g, "/");
2223
+ const spans = [];
2224
+ const rules = [];
2225
+ for (const { rule, pattern } of DEFAULT_SECRET_PATTERNS) {
2226
+ const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
2227
+ let match;
2228
+ while ((match = scan.exec(posixPath)) !== null) {
2229
+ if (match[0].length === 0) {
2230
+ scan.lastIndex += 1;
2231
+ continue;
2232
+ }
2233
+ spans.push({ start: match.index, end: match.index + match[0].length, rule });
2234
+ if (!rules.includes(rule)) rules.push(rule);
2235
+ }
2236
+ }
2237
+ if (spans.length === 0) return null;
2238
+ spans.sort((a, b) => a.start - b.start || a.end - b.end);
2239
+ let out = "";
2240
+ let cursor = 0;
2241
+ for (const span of spans) {
2242
+ if (span.end <= cursor) continue;
2243
+ const start = Math.max(span.start, cursor);
2244
+ out += posixPath.slice(cursor, start);
2245
+ out += REDACTION_MARKER;
2246
+ cursor = Math.min(span.end, posixPath.length);
2247
+ }
2248
+ out += posixPath.slice(cursor);
2249
+ return { rules, masked: out };
2250
+ }
1766
2251
  function globToRegExp(glob) {
1767
2252
  const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1768
2253
  const anchored = normalized.startsWith("/");
@@ -1883,49 +2368,177 @@ function defaultIgnoreReason(relativePath) {
1883
2368
  }
1884
2369
  return null;
1885
2370
  }
1886
- function collectFiles(cwd, options = {}) {
1887
- const includePatterns = options.includePatterns ?? [];
1888
- const excludePatterns = options.excludePatterns ?? [];
1889
- const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
1890
- let candidates;
1891
- let walkSkips = [];
2371
+ function listIgnoredPaths(cwd) {
1892
2372
  try {
1893
2373
  const stdout = childProcess__namespace.execSync(
1894
- "git ls-files -z --cached --others --exclude-standard",
2374
+ "git ls-files -z --others --ignored --exclude-standard --directory",
1895
2375
  {
1896
2376
  cwd,
1897
2377
  stdio: ["ignore", "pipe", "ignore"],
1898
2378
  encoding: "utf8",
1899
- maxBuffer: 50 * 1024 * 1024
2379
+ maxBuffer: 32 * 1024 * 1024
1900
2380
  }
1901
2381
  );
1902
- candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
2382
+ return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
1903
2383
  } catch {
1904
- const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set());
1905
- candidates = walked.files;
1906
- walkSkips = walked.skipped;
2384
+ return [];
1907
2385
  }
1908
- const skipped = [...walkSkips];
1909
- const files = candidates.filter((file) => {
1910
- const ignored = defaultIgnoreReason(file);
1911
- if (ignored) {
1912
- skipped.push({ path: file, reason: "default-ignore", detail: ignored });
1913
- return false;
1914
- }
1915
- if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesExclude(file, pattern))) {
1916
- skipped.push({
1917
- path: file,
1918
- reason: "excluded",
1919
- detail: `does not match any include pattern (${includePatterns.join(", ")})`
1920
- });
1921
- return false;
2386
+ }
2387
+ var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
2388
+ function isToolOwnedDirName(name) {
2389
+ return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
2390
+ }
2391
+ function hasSnapshotMarker(target) {
2392
+ try {
2393
+ return fs9__namespace.statSync(target).isDirectory() && fs9__namespace.existsSync(path10__namespace.join(target, ".codebase-snapshot"));
2394
+ } catch {
2395
+ return false;
2396
+ }
2397
+ }
2398
+ function makeToolOwnedDecider(cwd) {
2399
+ const markers = /* @__PURE__ */ new Map();
2400
+ return (relativePath) => {
2401
+ const parts = relativePath.replace(/\\/g, "/").split("/");
2402
+ for (let depth = 1; depth < parts.length; depth += 1) {
2403
+ const prefix = parts.slice(0, depth).join("/");
2404
+ if (depth === 1 && isToolOwnedDirName(prefix)) return true;
2405
+ let hasMarker = markers.get(prefix);
2406
+ if (hasMarker === void 0) {
2407
+ hasMarker = fs9__namespace.existsSync(path10__namespace.join(cwd, prefix, ".codebase-snapshot"));
2408
+ markers.set(prefix, hasMarker);
2409
+ }
2410
+ if (hasMarker) return true;
2411
+ }
2412
+ return false;
2413
+ };
2414
+ }
2415
+ function makeLinkProbe() {
2416
+ const cache = /* @__PURE__ */ new Map();
2417
+ return (absolutePath) => {
2418
+ let known = cache.get(absolutePath);
2419
+ if (known === void 0) {
2420
+ try {
2421
+ known = fs9__namespace.lstatSync(absolutePath).isSymbolicLink();
2422
+ } catch {
2423
+ known = false;
2424
+ }
2425
+ cache.set(absolutePath, known);
2426
+ }
2427
+ return known;
2428
+ };
2429
+ }
2430
+ function dropLinkedDirectories(cwd, candidates, skipped) {
2431
+ const isLink = makeLinkProbe();
2432
+ let rootReal = cwd;
2433
+ try {
2434
+ rootReal = fs9__namespace.realpathSync(cwd);
2435
+ } catch {
2436
+ }
2437
+ const reported = /* @__PURE__ */ new Set();
2438
+ const kept = [];
2439
+ for (const candidate of candidates) {
2440
+ const parts = candidate.split("/");
2441
+ let linkedDir = null;
2442
+ for (let depth = 1; depth <= parts.length; depth += 1) {
2443
+ const prefix = parts.slice(0, depth).join("/");
2444
+ if (isLink(path10__namespace.join(cwd, prefix))) {
2445
+ linkedDir = prefix;
2446
+ break;
2447
+ }
2448
+ }
2449
+ if (linkedDir === null) {
2450
+ kept.push(candidate);
2451
+ continue;
2452
+ }
2453
+ if (reported.has(linkedDir)) continue;
2454
+ reported.add(linkedDir);
2455
+ const absolute = path10__namespace.join(cwd, linkedDir);
2456
+ let targetReal = absolute;
2457
+ try {
2458
+ targetReal = fs9__namespace.realpathSync(absolute);
2459
+ } catch {
2460
+ }
2461
+ const target = readLinkTarget(absolute, targetReal);
2462
+ const what = linkedDir === candidate ? "symbolic link" : "directory link";
2463
+ skipped.push(
2464
+ isInside(rootReal, targetReal) ? {
2465
+ path: linkedDir,
2466
+ reason: "symlink",
2467
+ detail: `${what} not followed (target: ${target})`
2468
+ } : {
2469
+ path: linkedDir,
2470
+ reason: "outside-root",
2471
+ detail: `link target resolves outside the scanned root (target: ${target}); not followed`
2472
+ }
2473
+ );
2474
+ }
2475
+ return kept;
2476
+ }
2477
+ function collectFiles(cwd, options = {}) {
2478
+ const includePatterns = options.includePatterns ?? [];
2479
+ const excludePatterns = options.excludePatterns ?? [];
2480
+ const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
2481
+ const isToolOwned = makeToolOwnedDecider(cwd);
2482
+ let candidates;
2483
+ const skipped = [];
2484
+ try {
2485
+ const stdout = childProcess__namespace.execSync(
2486
+ "git ls-files -z --cached --others --exclude-standard",
2487
+ {
2488
+ cwd,
2489
+ stdio: ["ignore", "pipe", "ignore"],
2490
+ encoding: "utf8",
2491
+ maxBuffer: 50 * 1024 * 1024
2492
+ }
2493
+ );
2494
+ candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
2495
+ candidates = dropLinkedDirectories(cwd, candidates, skipped);
2496
+ const known = new Set(candidates);
2497
+ for (const ignoredPath of listIgnoredPaths(cwd)) {
2498
+ const trimmed = ignoredPath.replace(/\/+$/, "");
2499
+ if (known.has(trimmed) || known.has(ignoredPath)) continue;
2500
+ if (isToolOwned(trimmed)) continue;
2501
+ if (isSensitivePath(trimmed)) {
2502
+ skipped.push({
2503
+ path: trimmed,
2504
+ reason: "sensitive-file",
2505
+ detail: "credential-bearing file, ignored by .gitignore; enable includeSensitiveFiles to emit it"
2506
+ });
2507
+ } else {
2508
+ skipped.push({
2509
+ path: trimmed,
2510
+ reason: "gitignore",
2511
+ detail: ignoredPath.endsWith("/") ? "ignored by .gitignore (directory, not descended)" : "ignored by .gitignore"
2512
+ });
2513
+ }
2514
+ }
2515
+ } catch {
2516
+ const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set(), true);
2517
+ candidates = walked.files;
2518
+ skipped.push(...walked.skipped);
2519
+ }
2520
+ const files = candidates.filter((file) => {
2521
+ if (isToolOwned(file)) return false;
2522
+ const ignored = defaultIgnoreReason(file);
2523
+ if (ignored) {
2524
+ skipped.push({ path: file, reason: "default-ignore", detail: ignored });
2525
+ return false;
2526
+ }
2527
+ if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesExclude(file, pattern))) {
2528
+ skipped.push({
2529
+ path: file,
2530
+ reason: "excluded",
2531
+ detail: `does not match any include pattern (${includePatterns.join(", ")})`
2532
+ });
2533
+ return false;
1922
2534
  }
1923
2535
  for (const pattern of excludePatterns) {
1924
2536
  if (matchesExclude(file, pattern)) {
2537
+ const fromPreset = PRESET_IGNORES.has(pattern);
1925
2538
  skipped.push({
1926
2539
  path: file,
1927
- reason: "excluded",
1928
- detail: `matches exclude pattern "${pattern}"`
2540
+ reason: fromPreset ? "default-ignore" : "excluded",
2541
+ detail: fromPreset ? `preset ignore: "${pattern}"` : `matches exclude pattern "${pattern}"`
1929
2542
  });
1930
2543
  return false;
1931
2544
  }
@@ -1938,11 +2551,20 @@ function collectFiles(cwd, options = {}) {
1938
2551
  });
1939
2552
  return false;
1940
2553
  }
2554
+ const secret = findSecretInPath(file);
2555
+ if (secret) {
2556
+ skipped.push({
2557
+ path: secret.masked,
2558
+ reason: "sensitive-path",
2559
+ detail: `path matches secret rule "${secret.rules.join('", "')}"; the file was skipped and its path is masked here because paths are emitted into every artifact`
2560
+ });
2561
+ return false;
2562
+ }
1941
2563
  return true;
1942
2564
  }).sort();
1943
2565
  return { files, skipped };
1944
2566
  }
1945
- function walkDirectory(dir, baseDir, visited) {
2567
+ function walkDirectory(dir, baseDir, visited, isRoot = false) {
1946
2568
  const results = { files: [], skipped: [] };
1947
2569
  let realDir;
1948
2570
  try {
@@ -1952,7 +2574,13 @@ function walkDirectory(dir, baseDir, visited) {
1952
2574
  }
1953
2575
  if (visited.has(realDir)) return results;
1954
2576
  visited.add(realDir);
1955
- const ignoredByGitignore = dir === baseDir ? readGitignoreMatcher(baseDir) : () => false;
2577
+ let rootReal;
2578
+ try {
2579
+ rootReal = fs9__namespace.realpathSync(baseDir);
2580
+ } catch {
2581
+ rootReal = baseDir;
2582
+ }
2583
+ const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
1956
2584
  let entries;
1957
2585
  try {
1958
2586
  entries = fs9__namespace.readdirSync(dir, { withFileTypes: true });
@@ -1961,30 +2589,79 @@ function walkDirectory(dir, baseDir, visited) {
1961
2589
  return results;
1962
2590
  }
1963
2591
  for (const entry of entries) {
1964
- if (DEFAULT_IGNORED_DIRS.includes(entry.name)) continue;
1965
2592
  const fullPath = path10__namespace.join(dir, entry.name);
1966
2593
  const relPath = path10__namespace.relative(baseDir, fullPath).replace(/\\/g, "/");
1967
- if (ignoredByGitignore(relPath)) continue;
2594
+ if (isToolOwnedDirName(entry.name)) continue;
2595
+ if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
2596
+ results.skipped.push({
2597
+ path: relPath,
2598
+ reason: "default-ignore",
2599
+ detail: `built-in ignore: directory "${entry.name}" (not descended)`
2600
+ });
2601
+ continue;
2602
+ }
2603
+ if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
2604
+ results.skipped.push({
2605
+ path: relPath,
2606
+ reason: "default-ignore",
2607
+ detail: "built-in ignore: filename"
2608
+ });
2609
+ continue;
2610
+ }
2611
+ if (ignoredByGitignore(relPath)) {
2612
+ if (!hasSnapshotMarker(fullPath)) {
2613
+ results.skipped.push({
2614
+ path: relPath,
2615
+ reason: isSensitivePath(relPath) ? "sensitive-file" : "gitignore",
2616
+ detail: "ignored by .gitignore"
2617
+ });
2618
+ }
2619
+ continue;
2620
+ }
1968
2621
  let isDirectory = entry.isDirectory();
1969
2622
  let isFile = entry.isFile();
1970
2623
  if (entry.isSymbolicLink()) {
2624
+ let targetReal;
1971
2625
  try {
1972
- const stats = fs9__namespace.statSync(fullPath);
1973
- isDirectory = stats.isDirectory();
1974
- isFile = stats.isFile();
1975
- if (!isDirectory && !isFile) {
1976
- results.skipped.push({ path: relPath, reason: "symlink", detail: "not a file or directory" });
1977
- continue;
1978
- }
2626
+ targetReal = fs9__namespace.realpathSync(fullPath);
2627
+ } catch {
2628
+ results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
2629
+ continue;
2630
+ }
2631
+ let stats;
2632
+ try {
2633
+ stats = fs9__namespace.statSync(fullPath);
2634
+ } catch {
2635
+ results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
2636
+ continue;
2637
+ }
2638
+ const linkTarget = readLinkTarget(fullPath, targetReal);
2639
+ if (!isInside(rootReal, targetReal)) {
2640
+ results.skipped.push({
2641
+ path: relPath,
2642
+ reason: "outside-root",
2643
+ detail: `link target resolves outside the scanned root (target: ${linkTarget}); not followed`
2644
+ });
2645
+ continue;
2646
+ }
2647
+ if (stats.isDirectory()) {
1979
2648
  results.skipped.push({
1980
2649
  path: relPath,
1981
2650
  reason: "symlink",
1982
- detail: isDirectory ? "followed directory symlink" : "followed file symlink"
2651
+ detail: `directory link not followed (target: ${linkTarget})`
2652
+ });
2653
+ continue;
2654
+ }
2655
+ if (!stats.isFile()) {
2656
+ results.skipped.push({
2657
+ path: relPath,
2658
+ reason: "symlink",
2659
+ detail: `link to neither a file nor a directory (target: ${linkTarget})`
1983
2660
  });
1984
- } catch {
1985
- results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
1986
2661
  continue;
1987
2662
  }
2663
+ isDirectory = false;
2664
+ isFile = true;
1988
2665
  }
1989
2666
  if (isDirectory) {
1990
2667
  const nested = walkDirectory(fullPath, baseDir, visited);
@@ -1997,195 +2674,6 @@ function walkDirectory(dir, baseDir, visited) {
1997
2674
  return results;
1998
2675
  }
1999
2676
 
2000
- // src/security/patterns.ts
2001
- var DEFAULT_SECRET_PATTERNS = [
2002
- {
2003
- rule: "private-key-header",
2004
- pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
2005
- },
2006
- {
2007
- rule: "jwt-token",
2008
- pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
2009
- },
2010
- {
2011
- rule: "aws-access-key-id",
2012
- pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
2013
- },
2014
- {
2015
- rule: "aws-secret-access-key",
2016
- pattern: /(?:aws_secret_access_key|aws_secret_key|secret_access_key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']?/i
2017
- },
2018
- {
2019
- rule: "github-token",
2020
- pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
2021
- },
2022
- {
2023
- rule: "slack-token",
2024
- pattern: /xox[baprs]-[0-9A-Za-z-]{10,250}/
2025
- },
2026
- {
2027
- rule: "slack-webhook-url",
2028
- pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
2029
- },
2030
- {
2031
- rule: "stripe-secret-key",
2032
- pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
2033
- },
2034
- {
2035
- rule: "google-api-key",
2036
- pattern: /AIza[0-9A-Za-z_-]{35}/
2037
- },
2038
- {
2039
- rule: "google-oauth-client-secret",
2040
- pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
2041
- },
2042
- {
2043
- rule: "openai-anthropic-key",
2044
- pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
2045
- },
2046
- {
2047
- rule: "npm-token",
2048
- pattern: /npm_[A-Za-z0-9]{36}/
2049
- },
2050
- {
2051
- rule: "pypi-token",
2052
- pattern: /pypi-[A-Za-z0-9_-]{50,180}/
2053
- },
2054
- {
2055
- rule: "sendgrid-key",
2056
- pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
2057
- },
2058
- {
2059
- rule: "twilio-api-key",
2060
- pattern: /SK[0-9a-fA-F]{32}/
2061
- },
2062
- {
2063
- rule: "digitalocean-token",
2064
- pattern: /dop_v1_[0-9a-f]{64}/
2065
- },
2066
- {
2067
- rule: "azure-storage-account-key",
2068
- pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
2069
- },
2070
- {
2071
- rule: "database-uri-credentials",
2072
- pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/
2073
- },
2074
- {
2075
- rule: "basic-auth-url",
2076
- pattern: /(?:https?|ftp|sftp|ssh|git):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/i
2077
- },
2078
- {
2079
- rule: "generic-secret-assignment",
2080
- pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["'][^"'\s]{8,256}["']/i
2081
- },
2082
- {
2083
- // Covers `.env` / shell assignment syntax, which never quotes its values and
2084
- // was therefore invisible to the rule above. Anchored to the line start and
2085
- // limited to secret-looking names to stay at zero false positives.
2086
- //
2087
- // The name is matched with BOUNDED quantifiers on purpose: an unbounded
2088
- // `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
2089
- // every offset of a long identifier, which made this rule quadratic
2090
- // (measured 5.6 s for a single 240 KB line before bounding).
2091
- rule: "env-secret-assignment",
2092
- pattern: /^\s{0,64}(?:export\s{1,8})?[A-Za-z0-9_]{0,96}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIAL)[A-Za-z0-9_]{0,96}\s{0,8}=\s{0,8}(?![\s$<{[])[^\s#]{6,512}$/i
2093
- }
2094
- ];
2095
- var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
2096
- var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
2097
-
2098
- // src/security/redact.ts
2099
- var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
2100
- var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
2101
- function compileSecretRules(rules) {
2102
- return rules.map((rule) => {
2103
- if (rule.pattern instanceof RegExp) {
2104
- return {
2105
- rule: rule.rule,
2106
- pattern: new RegExp(rule.pattern.source, rule.pattern.flags.replace(/[gy]/g, ""))
2107
- };
2108
- }
2109
- if (typeof rule.pattern === "string") {
2110
- try {
2111
- return { rule: rule.rule, pattern: new RegExp(rule.pattern, "i") };
2112
- } catch (err) {
2113
- throw new Error(
2114
- `Invalid regular expression for secret rule "${rule.rule}": ${err.message}`
2115
- );
2116
- }
2117
- }
2118
- throw new Error(
2119
- `Secret rule "${String(rule.rule)}" has a pattern that is neither a RegExp nor a string.`
2120
- );
2121
- });
2122
- }
2123
- function toGlobal(pattern) {
2124
- const flags = pattern.flags.replace(/[gy]/g, "");
2125
- return new RegExp(pattern.source, `${flags}g`);
2126
- }
2127
- function scanAndRedactSecrets(content, rules, filePath) {
2128
- const findings = [];
2129
- let redacted = content;
2130
- for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
2131
- blockPattern.lastIndex = 0;
2132
- let match;
2133
- while ((match = blockPattern.exec(redacted)) !== null) {
2134
- if (match[0].length === 0) {
2135
- blockPattern.lastIndex += 1;
2136
- continue;
2137
- }
2138
- const line = redacted.slice(0, match.index).split("\n").length;
2139
- const newlines = (match[0].match(/\n/g) ?? []).length;
2140
- findings.push({ rule: "private-key-block", line, path: filePath });
2141
- redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
2142
- blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
2143
- }
2144
- }
2145
- const compiled = rules.map((rule) => ({
2146
- rule: rule.rule,
2147
- pattern: toGlobal(rule.pattern)
2148
- }));
2149
- const lines = redacted.split("\n");
2150
- const redactedLines = lines.map((line, index) => {
2151
- if (line.length === 0) return line;
2152
- const spans = [];
2153
- const matchedRules = /* @__PURE__ */ new Set();
2154
- for (const { rule, pattern } of compiled) {
2155
- pattern.lastIndex = 0;
2156
- let match;
2157
- while ((match = pattern.exec(line)) !== null) {
2158
- if (match[0].length === 0) {
2159
- pattern.lastIndex += 1;
2160
- continue;
2161
- }
2162
- spans.push({ start: match.index, end: match.index + match[0].length, rule });
2163
- matchedRules.add(rule);
2164
- }
2165
- }
2166
- if (spans.length === 0) return line;
2167
- for (const rule of matchedRules) {
2168
- findings.push({ rule, line: index + 1, path: filePath });
2169
- }
2170
- return applySpans(line, spans);
2171
- });
2172
- return { redacted: redactedLines.join("\n"), findings };
2173
- }
2174
- function applySpans(line, spans) {
2175
- const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
2176
- let out = "";
2177
- let cursor = 0;
2178
- for (const span of ordered) {
2179
- if (span.end <= cursor) continue;
2180
- const start = Math.max(span.start, cursor);
2181
- out += line.slice(cursor, start);
2182
- out += REDACTION_MARKER;
2183
- cursor = Math.min(span.end, line.length);
2184
- }
2185
- out += line.slice(cursor);
2186
- return out;
2187
- }
2188
-
2189
2677
  // src/config.ts
2190
2678
  var CONFIG_FILENAMES = [
2191
2679
  "snapshot.config.ts",
@@ -2235,7 +2723,12 @@ function normalizeResolvedConfig(config) {
2235
2723
  ),
2236
2724
  include: resolveInclude(config.include),
2237
2725
  preset: typeof config.preset === "string" ? config.preset : "generic",
2238
- presetExclude: Array.isArray(config.presetExclude) ? config.presetExclude : [],
2726
+ presetExclude: [
2727
+ .../* @__PURE__ */ new Set([
2728
+ ...Array.isArray(config.presetExclude) ? config.presetExclude : [],
2729
+ ...outputExcludePatterns(config.cwd, config.outDir, config.tmpDir)
2730
+ ])
2731
+ ],
2239
2732
  layersMode: config.layersMode === "replace" ? "replace" : "merge",
2240
2733
  budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
2241
2734
  hooks: resolveHooks(config.hooks),
@@ -2272,7 +2765,68 @@ function supportsTypeScriptImports() {
2272
2765
  return major > 22 || major === 22 && minor >= 6;
2273
2766
  }
2274
2767
  function stripJsonComments2(raw) {
2275
- return raw.replace(/^\uFEFF/, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'\\])\/\/.*$/gm, "$1");
2768
+ const text2 = raw.replace(/^\uFEFF/, "");
2769
+ let out = "";
2770
+ let inString = false;
2771
+ let escaped = false;
2772
+ for (let index = 0; index < text2.length; index += 1) {
2773
+ const char = text2[index];
2774
+ if (inString) {
2775
+ out += char;
2776
+ if (escaped) escaped = false;
2777
+ else if (char === "\\") escaped = true;
2778
+ else if (char === '"') inString = false;
2779
+ continue;
2780
+ }
2781
+ if (char === '"') {
2782
+ inString = true;
2783
+ out += char;
2784
+ continue;
2785
+ }
2786
+ if (char === "/" && text2[index + 1] === "/") {
2787
+ while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
2788
+ index -= 1;
2789
+ continue;
2790
+ }
2791
+ if (char === "/" && text2[index + 1] === "*") {
2792
+ index += 2;
2793
+ while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
2794
+ if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
2795
+ index += 1;
2796
+ }
2797
+ index += 1;
2798
+ continue;
2799
+ }
2800
+ out += char;
2801
+ }
2802
+ return stripTrailingCommas(out);
2803
+ }
2804
+ function stripTrailingCommas(text2) {
2805
+ let out = "";
2806
+ let inString = false;
2807
+ let escaped = false;
2808
+ for (let index = 0; index < text2.length; index += 1) {
2809
+ const char = text2[index];
2810
+ if (inString) {
2811
+ out += char;
2812
+ if (escaped) escaped = false;
2813
+ else if (char === "\\") escaped = true;
2814
+ else if (char === '"') inString = false;
2815
+ continue;
2816
+ }
2817
+ if (char === '"') {
2818
+ inString = true;
2819
+ out += char;
2820
+ continue;
2821
+ }
2822
+ if (char === ",") {
2823
+ let next = index + 1;
2824
+ while (next < text2.length && /\s/.test(text2[next])) next += 1;
2825
+ if (text2[next] === "}" || text2[next] === "]") continue;
2826
+ }
2827
+ out += char;
2828
+ }
2829
+ return out;
2276
2830
  }
2277
2831
  function parseJsonish(raw, file) {
2278
2832
  try {
@@ -2281,6 +2835,47 @@ function parseJsonish(raw, file) {
2281
2835
  throw new Error(`Could not parse ${file}: ${err.message}`);
2282
2836
  }
2283
2837
  }
2838
+ function describeValue2(value) {
2839
+ if (value === null) return "null";
2840
+ if (Array.isArray(value)) return "an array";
2841
+ if (value instanceof RegExp) return value.toString();
2842
+ if (typeof value === "function") return "a function";
2843
+ if (typeof value === "string") return JSON.stringify(value);
2844
+ if (typeof value === "object") {
2845
+ const keys = Object.keys(value);
2846
+ return keys.length > 0 ? `an object with keys ${keys.join(", ")}` : "an empty object";
2847
+ }
2848
+ return `${typeof value} (${String(value)})`;
2849
+ }
2850
+ var isThenable = (value) => typeof value.then === "function";
2851
+ function readConfigExport(exported, file) {
2852
+ if (exported === void 0 || exported === null) {
2853
+ throw new Error(
2854
+ `${file} does not export a configuration object (received ${describeValue2(exported)}).`
2855
+ );
2856
+ }
2857
+ if (Array.isArray(exported)) {
2858
+ throw new Error(
2859
+ `${file} exports an array; expected a single configuration object such as \`export default { outDir: "snapshot" }\`.`
2860
+ );
2861
+ }
2862
+ if (typeof exported === "function") {
2863
+ throw new Error(
2864
+ `${file} exports a function; expected a configuration object. Call the function yourself and export its result.`
2865
+ );
2866
+ }
2867
+ if (typeof exported !== "object") {
2868
+ throw new Error(
2869
+ `${file} does not export a configuration object (received ${describeValue2(exported)}).`
2870
+ );
2871
+ }
2872
+ if (isThenable(exported)) {
2873
+ throw new Error(
2874
+ `${file} exports a Promise/thenable, not a configuration object. An async config is read synchronously, so its settings would be ignored: export a plain object (\`const config = {\u2026}; export default config;\`).`
2875
+ );
2876
+ }
2877
+ return exported;
2878
+ }
2284
2879
  async function loadConfigFileDetailed(cwd) {
2285
2880
  const warnings = [];
2286
2881
  const errors = [];
@@ -2310,13 +2905,7 @@ async function loadConfigFileDetailed(cwd) {
2310
2905
  }
2311
2906
  try {
2312
2907
  const imported = await import(url.pathToFileURL(fullPath).href);
2313
- const config = imported.default ?? imported;
2314
- if (!config || typeof config !== "object") {
2315
- const message = `${fullPath} does not export a configuration object.`;
2316
- warnings.push(message);
2317
- errors.push(message);
2318
- continue;
2319
- }
2908
+ const config = readConfigExport(imported.default ?? imported, fullPath);
2320
2909
  return { config, file: fullPath, warnings, errors };
2321
2910
  } catch (err) {
2322
2911
  const message = `Could not load ${fullPath}: ${err.message}`;
@@ -2370,7 +2959,7 @@ var DEFAULT_ARTIFACTS = {
2370
2959
  function isArtifactName(value) {
2371
2960
  return ARTIFACT_NAMES.includes(value);
2372
2961
  }
2373
- function resolveArtifacts(requested, skipped, includeMonolith) {
2962
+ function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
2374
2963
  const artifacts = {
2375
2964
  ...DEFAULT_ARTIFACTS,
2376
2965
  // `includeMonolith` predates this selection and stays the switch for it.
@@ -2392,19 +2981,119 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
2392
2981
  artifacts[name] = value;
2393
2982
  }
2394
2983
  }
2395
- for (const name of skipped ?? []) {
2396
- if (!isArtifactName(name)) {
2984
+ if (monolithOverride !== void 0) {
2985
+ artifacts.monolith = monolithOverride;
2986
+ }
2987
+ for (const name of skipped ?? []) {
2988
+ if (!isArtifactName(name)) {
2989
+ throw new Error(
2990
+ `Unknown artifact "${name}". Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
2991
+ );
2992
+ }
2993
+ artifacts[name] = false;
2994
+ }
2995
+ return artifacts;
2996
+ }
2997
+ function strictPositiveInteger(value, fallback, label) {
2998
+ if (value === void 0) return fallback;
2999
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
3000
+ throw new Error(`${label} must be a positive integer (received ${describeValue2(value)}).`);
3001
+ }
3002
+ return value;
3003
+ }
3004
+ function strictBoolean(value, fallback, label) {
3005
+ if (value === void 0) return fallback;
3006
+ if (typeof value !== "boolean") {
3007
+ throw new Error(`${label} must be true or false (received ${describeValue2(value)}).`);
3008
+ }
3009
+ return value;
3010
+ }
3011
+ function strictString(value, fallback, label) {
3012
+ if (value === void 0) return fallback;
3013
+ if (typeof value !== "string" || value.trim() === "") {
3014
+ throw new Error(`${label} must be a non-empty string (received ${describeValue2(value)}).`);
3015
+ }
3016
+ return value;
3017
+ }
3018
+ function resolveExcludePatterns(value) {
3019
+ if (value === void 0) return [];
3020
+ if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string")) {
3021
+ throw new Error(
3022
+ `\`excludePatterns\` must be an array of glob strings such as ["**/*.test.ts"] (received ${describeValue2(value)}).`
3023
+ );
3024
+ }
3025
+ return value.map((pattern) => pattern.trim()).filter(Boolean);
3026
+ }
3027
+ var AUTO_PRESET_ALIASES = /* @__PURE__ */ new Set(["auto", "detected"]);
3028
+ function resolvePresetName(value) {
3029
+ if (value === void 0) return void 0;
3030
+ if (typeof value !== "string" || value.trim() === "") {
3031
+ throw new Error(
3032
+ `\`preset\` must be a preset name (received ${describeValue2(value)}). Use "detected" (or "auto") to detect one, or one of: ${listPresetNames().join(", ")}.`
3033
+ );
3034
+ }
3035
+ const name = value.trim();
3036
+ return AUTO_PRESET_ALIASES.has(name.toLowerCase()) ? void 0 : name;
3037
+ }
3038
+ function resolveRuleList(value, kind) {
3039
+ const collection = kind === "layer" ? "layers" : "kinds";
3040
+ const example = kind === "layer" ? `{ name: "api", match: "src/api/**" }` : `{ kind: "route", match: "src/routes/**" }`;
3041
+ if (value === void 0) return [];
3042
+ if (!Array.isArray(value)) {
3043
+ throw new Error(
3044
+ `\`${collection}\` must be an array of ${kind} rules (received ${describeValue2(value)}). A ${kind} rule is ${example}.`
3045
+ );
3046
+ }
3047
+ return value.map((entry, index) => {
3048
+ const where = `${collection}[${index}]`;
3049
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
3050
+ throw new Error(`${where} must be an object (received ${describeValue2(entry)}). A ${kind} rule is ${example}.`);
3051
+ }
3052
+ const record = entry;
3053
+ const keys = Object.keys(record);
3054
+ const canonical = kind === "layer" ? "name" : "kind";
3055
+ const alias = kind === "layer" ? "layer" : "name";
3056
+ const rawName = typeof record[canonical] === "string" ? record[canonical] : typeof record[alias] === "string" ? record[alias] : void 0;
3057
+ if (rawName === void 0 || rawName.trim() === "") {
2397
3058
  throw new Error(
2398
- `Unknown artifact "${name}". Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
3059
+ `${where} is missing \`${canonical}\` (received keys: ${keys.join(", ") || "none"}). A ${kind} rule is ${example}; \`${alias}\` is accepted as an alias of \`${canonical}\`.`
2399
3060
  );
2400
3061
  }
2401
- artifacts[name] = false;
2402
- }
2403
- return artifacts;
3062
+ const match = record.match;
3063
+ if (match === void 0) {
3064
+ throw new Error(
3065
+ `${where}.match is required (received keys: ${keys.join(", ") || "none"}). Use a glob string ("src/api/**"), a RegExp (/\\\\.tsx$/) or a function.`
3066
+ );
3067
+ }
3068
+ if (!(typeof match === "string" || match instanceof RegExp || typeof match === "function")) {
3069
+ throw new Error(`${where}.match must be a string, a RegExp or a function (received ${describeValue2(match)}).`);
3070
+ }
3071
+ if (typeof match === "string" && match.trim() === "") {
3072
+ throw new Error(`${where}.match must not be an empty string.`);
3073
+ }
3074
+ return {
3075
+ name: rawName.trim(),
3076
+ match
3077
+ };
3078
+ });
3079
+ }
3080
+ function resolveLayerRules(value) {
3081
+ return resolveRuleList(value, "layer").map((rule) => ({ name: rule.name, match: rule.match }));
2404
3082
  }
2405
- function positiveInteger(value, fallback) {
2406
- if (!Number.isFinite(value) || value <= 0) return fallback;
2407
- return Math.floor(value);
3083
+ function resolveKindRules(value) {
3084
+ return resolveRuleList(value, "kind").map((rule) => ({ kind: rule.name, match: rule.match }));
3085
+ }
3086
+ function outputExcludePatterns(cwd, outDir, tmpDir) {
3087
+ const patterns = [];
3088
+ for (const dir of [outDir, tmpDir]) {
3089
+ const absolute = path10__namespace.resolve(cwd, dir);
3090
+ const relative4 = path10__namespace.relative(cwd, absolute);
3091
+ if (!relative4 || relative4 === ".." || relative4.startsWith(`..${path10__namespace.sep}`) || path10__namespace.isAbsolute(relative4)) {
3092
+ continue;
3093
+ }
3094
+ patterns.push(`${relative4.replace(/\\/g, "/")}/**`);
3095
+ }
3096
+ return patterns;
2408
3097
  }
2409
3098
  var BYTE_UNITS = {
2410
3099
  b: 1,
@@ -2473,11 +3162,13 @@ function resolveLayersMode(mode) {
2473
3162
  }
2474
3163
  return mode;
2475
3164
  }
2476
- function resolveBudgets(budgets, fallback) {
3165
+ function resolveBudgets(budgets, fallback, overrideDefault) {
2477
3166
  const byLayer = {};
2478
3167
  if (budgets === void 0) return { default: fallback, byLayer };
2479
3168
  if (typeof budgets !== "object" || budgets === null || Array.isArray(budgets)) {
2480
- throw new Error("`budgets` must be an object.");
3169
+ throw new Error(
3170
+ `\`budgets\` must be an object such as { default: "120k", byLayer: { api: "60k" } } (received ${describeValue2(budgets)}).`
3171
+ );
2481
3172
  }
2482
3173
  if (budgets.byLayer !== void 0) {
2483
3174
  if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
@@ -2492,10 +3183,12 @@ function resolveBudgets(budgets, fallback) {
2492
3183
  );
2493
3184
  }
2494
3185
  }
2495
- return {
2496
- default: boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default"),
2497
- byLayer
2498
- };
3186
+ const resolvedDefault = boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default");
3187
+ if (!overrideDefault) return { default: resolvedDefault, byLayer };
3188
+ for (const [layer2, value] of Object.entries(byLayer)) {
3189
+ byLayer[layer2] = Math.min(value, fallback);
3190
+ }
3191
+ return { default: fallback, byLayer };
2499
3192
  }
2500
3193
  function resolveHooks(hooks) {
2501
3194
  if (hooks === void 0) return {};
@@ -2508,6 +3201,11 @@ function resolveHooks(hooks) {
2508
3201
  return { ...hooks };
2509
3202
  }
2510
3203
  function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
3204
+ if (userConfig?.cwd !== void 0 && (typeof userConfig.cwd !== "string" || userConfig.cwd.trim() === "")) {
3205
+ throw new Error(
3206
+ `\`cwd\` must be a non-empty path string (received ${describeValue2(userConfig.cwd)}).`
3207
+ );
3208
+ }
2511
3209
  const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
2512
3210
  const resolved = path10__namespace.resolve(candidate);
2513
3211
  if (!fs9__namespace.existsSync(resolved)) {
@@ -2520,55 +3218,106 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
2520
3218
  }
2521
3219
  function resolveConfig(userConfig, cliOptions, baseDir) {
2522
3220
  const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
2523
- const forcedPreset = userConfig?.preset;
3221
+ const forcedPreset = resolvePresetName(userConfig?.preset);
2524
3222
  if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
2525
3223
  throw new Error(unknownPresetMessage(forcedPreset));
2526
3224
  }
2527
3225
  const preset = detectPreset(cwd, forcedPreset);
2528
3226
  const layersMode = resolveLayersMode(userConfig?.layersMode);
2529
- const layers = userConfig?.layers && userConfig.layers.length > 0 ? layersMode === "replace" ? userConfig.layers : [...userConfig.layers, ...preset.layers] : preset.layers;
2530
- const kinds = userConfig?.kinds && userConfig.kinds.length > 0 ? layersMode === "replace" ? userConfig.kinds : [...userConfig.kinds, ...preset.kinds] : preset.kinds;
2531
- const customRules = userConfig?.secrets?.customPatterns ?? [];
2532
- const rawRules = userConfig?.secrets?.disableDefaultPatterns ? customRules : [...DEFAULT_SECRET_PATTERNS, ...customRules];
2533
- const secretRules = compileSecretRules(rawRules);
2534
- const includeMonolith = cliOptions?.includeMonolith ?? userConfig?.includeMonolith ?? false;
3227
+ const userLayers = resolveLayerRules(userConfig?.layers);
3228
+ const userKinds = resolveKindRules(userConfig?.kinds);
3229
+ const layers = userLayers.length > 0 ? layersMode === "replace" ? userLayers : [...userLayers, ...preset.layers] : preset.layers;
3230
+ const kinds = userKinds.length > 0 ? layersMode === "replace" ? userKinds : [...userKinds, ...preset.kinds] : preset.kinds;
3231
+ const secrets = userConfig?.secrets;
3232
+ if (secrets !== void 0 && (typeof secrets !== "object" || Array.isArray(secrets))) {
3233
+ throw new Error(
3234
+ `\`secrets\` must be an object of { customPatterns, disableDefaultPatterns } (received ${describeValue2(secrets)}).`
3235
+ );
3236
+ }
3237
+ const customRules = secrets?.customPatterns ?? [];
3238
+ if (!Array.isArray(customRules)) {
3239
+ throw new Error(
3240
+ `\`secrets.customPatterns\` must be an array of secret rules (received ${describeValue2(customRules)}). Each entry is a RegExp, a string, or { rule, pattern }.`
3241
+ );
3242
+ }
3243
+ const disableDefaultPatterns = strictBoolean(
3244
+ secrets?.disableDefaultPatterns,
3245
+ false,
3246
+ "`secrets.disableDefaultPatterns`"
3247
+ );
3248
+ const aliasedRules = userConfig?.secretRules;
3249
+ if (aliasedRules !== void 0 && !Array.isArray(aliasedRules)) {
3250
+ throw new Error(
3251
+ `\`secretRules\` must be an array of { rule, pattern } entries (received ${describeValue2(aliasedRules)}).`
3252
+ );
3253
+ }
3254
+ const compiledCustomRules = compileSecretRules([
3255
+ ...customRules,
3256
+ ...aliasedRules ?? []
3257
+ ]);
3258
+ const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
3259
+ const cliMonolith = cliOptions?.includeMonolith;
3260
+ const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
2535
3261
  const artifacts = resolveArtifacts(
2536
3262
  userConfig?.artifacts,
2537
3263
  cliOptions?.skipArtifacts,
2538
- includeMonolith
3264
+ includeMonolith,
3265
+ cliMonolith
2539
3266
  );
3267
+ const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
2540
3268
  const maxPartBytes = boundedBytes(
2541
- cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes,
3269
+ explicitMaxPartBytes,
2542
3270
  300 * 1024,
2543
3271
  MIN_MAX_PART_BYTES,
2544
3272
  "maxPartBytes"
2545
3273
  );
2546
- const budgets = resolveBudgets(userConfig?.budgets, maxPartBytes);
3274
+ const budgets = resolveBudgets(
3275
+ userConfig?.budgets,
3276
+ maxPartBytes,
3277
+ explicitMaxPartBytes !== void 0
3278
+ );
3279
+ const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
3280
+ const outDir = cliOptions?.outDir || configuredOutDir;
3281
+ const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
3282
+ const branch = strictString(userConfig?.branch, "main", "`branch`");
3283
+ const configuredSensitiveFiles = strictBoolean(
3284
+ userConfig?.includeSensitiveFiles,
3285
+ false,
3286
+ "`includeSensitiveFiles`"
3287
+ );
3288
+ const includeSensitiveFiles = strictBoolean(
3289
+ cliOptions?.includeSensitiveFiles,
3290
+ configuredSensitiveFiles,
3291
+ "`includeSensitiveFiles`"
3292
+ );
3293
+ const presetExclude = [
3294
+ .../* @__PURE__ */ new Set([...preset.exclude, ...outputExcludePatterns(cwd, outDir, tmpDir)])
3295
+ ];
2547
3296
  return markResolvedConfig({
2548
3297
  cwd,
2549
- branch: userConfig?.branch || "main",
2550
- outDir: cliOptions?.outDir || userConfig?.outDir || "snapshot",
2551
- tmpDir: userConfig?.tmpDir || "snapshot.tmp",
3298
+ branch,
3299
+ outDir,
3300
+ tmpDir,
2552
3301
  maxPartBytes,
2553
3302
  largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
2554
- previewHeadLines: positiveInteger(userConfig?.previewHeadLines ?? 80, 80),
2555
- previewTailLines: positiveInteger(userConfig?.previewTailLines ?? 40, 40),
3303
+ previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
3304
+ previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
2556
3305
  includeMonolith: artifacts.monolith,
2557
3306
  maxMonolithBytes: optionalBytes(
2558
3307
  cliOptions?.maxMonolithBytes !== void 0 ? cliOptions.maxMonolithBytes : userConfig?.maxMonolithBytes,
2559
3308
  "maxMonolithBytes"
2560
3309
  ),
2561
- includeSensitiveFiles: cliOptions?.includeSensitiveFiles ?? userConfig?.includeSensitiveFiles ?? false,
3310
+ includeSensitiveFiles,
2562
3311
  include: resolveInclude(userConfig?.include),
2563
3312
  preset: preset.presetName,
2564
- presetExclude: [...preset.exclude],
3313
+ presetExclude,
2565
3314
  artifacts,
2566
3315
  layersMode,
2567
3316
  budgets,
2568
3317
  hooks: resolveHooks(userConfig?.hooks),
2569
3318
  layers,
2570
3319
  kinds,
2571
- excludePatterns: userConfig?.excludePatterns ?? [],
3320
+ excludePatterns: resolveExcludePatterns(userConfig?.excludePatterns),
2572
3321
  secretRules
2573
3322
  });
2574
3323
  }
@@ -2623,7 +3372,11 @@ function generateAuditReport(manifest, secretFindings) {
2623
3372
  `**Total Files:** ${manifest.counts.totalFiles} `,
2624
3373
  `**Total Lines:** ${manifest.counts.totalLines.toLocaleString()} `,
2625
3374
  `**Total Size:** ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB `,
2626
- `**Redacted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
3375
+ // This counts the bytes that were *emitted* after redaction, not the bytes
3376
+ // that were masked: calling it "redacted bytes" made a repository with zero
3377
+ // secrets report a non-zero redacted-byte figure directly above "no secrets
3378
+ // detected". The number of masked spans is reported in section 1 instead.
3379
+ `**Emitted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
2627
3380
  "",
2628
3381
  "## 1. Security Scan & Redaction",
2629
3382
  "",
@@ -2826,6 +3579,8 @@ function renumberParts(parts, contentFingerprint) {
2826
3579
  layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
2827
3580
  kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
2828
3581
  }
3582
+ const pathLines = included.map((file) => displayPath(file.relativePath));
3583
+ const listFence = fenceForText(pathLines.join("\n"), 3);
2829
3584
  const header = [
2830
3585
  `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
2831
3586
  "",
@@ -2838,9 +3593,9 @@ function renumberParts(parts, contentFingerprint) {
2838
3593
  "",
2839
3594
  "## Files",
2840
3595
  "",
2841
- "```text",
2842
- ...included.map((file) => displayPath(file.relativePath)),
2843
- "```",
3596
+ `${listFence}text`,
3597
+ ...pathLines,
3598
+ listFence,
2844
3599
  "",
2845
3600
  "## Contents",
2846
3601
  ""
@@ -2868,7 +3623,8 @@ function capValue(value, max) {
2868
3623
  return `${value.slice(0, half)}\u2026${value.slice(value.length - half)}`;
2869
3624
  }
2870
3625
  var capMetadata = (value) => capValue(value, METADATA_VALUE_MAX);
2871
- var displayPath = (value) => capValue(value, DISPLAY_PATH_MAX);
3626
+ var CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
3627
+ var displayPath = (value) => capValue(value.replace(CONTROL_CHARS, " "), DISPLAY_PATH_MAX);
2872
3628
  function topEntries(counts, limit) {
2873
3629
  return Object.entries(counts).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)).slice(0, limit).map(([key, value]) => `${capMetadata(key)} (${value})`).join(", ");
2874
3630
  }
@@ -2964,6 +3720,8 @@ function renderGroups(groups, contentFingerprint) {
2964
3720
  layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
2965
3721
  kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
2966
3722
  }
3723
+ const pathLines = group.map((s) => displayPath(s.file.relativePath));
3724
+ const listFence = fenceForText(pathLines.join("\n"), 3);
2967
3725
  const header = [
2968
3726
  `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
2969
3727
  "",
@@ -2976,9 +3734,9 @@ function renderGroups(groups, contentFingerprint) {
2976
3734
  "",
2977
3735
  "## Files",
2978
3736
  "",
2979
- "```text",
2980
- ...group.map((s) => displayPath(s.file.relativePath)),
2981
- "```",
3737
+ `${listFence}text`,
3738
+ ...pathLines,
3739
+ listFence,
2982
3740
  "",
2983
3741
  "## Contents",
2984
3742
  ""
@@ -3037,9 +3795,12 @@ function renderFileSection(file) {
3037
3795
  ].join("\n");
3038
3796
  }
3039
3797
  function fenceForContent(content) {
3798
+ return fenceForText(content, 4);
3799
+ }
3800
+ function fenceForText(content, minimum) {
3040
3801
  const matches = content.match(/`+/g);
3041
3802
  const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
3042
- return "`".repeat(Math.max(4, maxRun + 1));
3803
+ return "`".repeat(Math.max(minimum, maxRun + 1));
3043
3804
  }
3044
3805
  function renderSummary(summary) {
3045
3806
  if (!summary) return [];
@@ -3188,6 +3949,7 @@ function generateReadme(manifest) {
3188
3949
 
3189
3950
  // src/generator/tree.ts
3190
3951
  var MAX_TREE_LINES = 2e4;
3952
+ var CONTROL_CHARS2 = /[\u0000-\u001f\u007f]/g;
3191
3953
  function generateProjectTree(filePaths) {
3192
3954
  const root = { children: /* @__PURE__ */ new Map() };
3193
3955
  for (const filePath of filePaths) {
@@ -3202,7 +3964,7 @@ function generateProjectTree(filePaths) {
3202
3964
  current = child;
3203
3965
  }
3204
3966
  }
3205
- const lines = ["# Project Tree", "", "```text", "."];
3967
+ const lines = ["."];
3206
3968
  let truncatedAt = -1;
3207
3969
  function walk(node, prefix) {
3208
3970
  if (truncatedAt !== -1) return;
@@ -3217,7 +3979,8 @@ function generateProjectTree(filePaths) {
3217
3979
  const isLast = index === keys.length - 1;
3218
3980
  const child = node.children.get(key);
3219
3981
  const isDirectory = child.children.size > 0;
3220
- lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${key}${isDirectory ? "/" : ""}`);
3982
+ const label = key.replace(CONTROL_CHARS2, " ");
3983
+ lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
3221
3984
  if (lines.length >= MAX_TREE_LINES) {
3222
3985
  truncatedAt = lines.length;
3223
3986
  return;
@@ -3229,8 +3992,8 @@ function generateProjectTree(filePaths) {
3229
3992
  if (truncatedAt !== -1) {
3230
3993
  lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
3231
3994
  }
3232
- lines.push("```", "");
3233
- return lines.join("\n");
3995
+ const fence = fenceForText(lines.join("\n"), 3);
3996
+ return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
3234
3997
  }
3235
3998
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
3236
3999
  ".png",
@@ -3374,6 +4137,15 @@ function emptyDrift() {
3374
4137
  function sha256Hex(buffer) {
3375
4138
  return crypto__namespace.createHash("sha256").update(buffer).digest("hex");
3376
4139
  }
4140
+ function wouldNowBeIncluded(cwd, relativePath) {
4141
+ const absolutePath = path10__namespace.join(cwd, relativePath);
4142
+ try {
4143
+ if (!fs9__namespace.statSync(absolutePath).isFile()) return false;
4144
+ return !isBinaryFile(absolutePath);
4145
+ } catch {
4146
+ return false;
4147
+ }
4148
+ }
3377
4149
  function validateSnapshot(configInput) {
3378
4150
  assertUsableConfigInput(configInput);
3379
4151
  const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
@@ -3428,9 +4200,9 @@ function validateSnapshot(configInput) {
3428
4200
  includeSensitiveFiles: config.includeSensitiveFiles
3429
4201
  });
3430
4202
  for (const diskFile of collection.files) {
3431
- if (skippedSet.has(diskFile)) continue;
3432
4203
  const snapshotHash = manifestFileMap.get(diskFile);
3433
4204
  if (!snapshotHash) {
4205
+ if (skippedSet.has(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
3434
4206
  drift.missingInSnapshot.push(diskFile);
3435
4207
  continue;
3436
4208
  }
@@ -3711,16 +4483,15 @@ function describeMatcher(matcher) {
3711
4483
  if (typeof matcher === "string") return matcher;
3712
4484
  if (matcher instanceof RegExp) return String(matcher);
3713
4485
  if (typeof matcher === "function") {
3714
- const source = String(matcher).replace(/\s+/g, " ").trim();
3715
- return source.length > 70 ? `${source.slice(0, 67)}...` : source;
4486
+ return "function (custom predicate)";
3716
4487
  }
3717
4488
  return String(matcher);
3718
4489
  }
3719
4490
  function normalizeTarget(cwd, target) {
3720
4491
  const absolute = path10__namespace.resolve(cwd, target);
3721
- const relative3 = path10__namespace.relative(cwd, absolute);
3722
- if (relative3.startsWith("..") || path10__namespace.isAbsolute(relative3)) return target;
3723
- return relative3.split(path10__namespace.sep).join("/");
4492
+ const relative4 = path10__namespace.relative(cwd, absolute);
4493
+ if (relative4.startsWith("..") || path10__namespace.isAbsolute(relative4)) return target;
4494
+ return relative4.split(path10__namespace.sep).join("/");
3724
4495
  }
3725
4496
  function findRuleMatch(rules, relativePath) {
3726
4497
  return rules.find(
@@ -3736,10 +4507,10 @@ function readManifest2(config) {
3736
4507
  }
3737
4508
  }
3738
4509
  function explainPath(config, target) {
3739
- const relative3 = normalizeTarget(config.cwd, target);
3740
- const absolute = path10__namespace.join(config.cwd, relative3);
4510
+ const relative4 = normalizeTarget(config.cwd, target);
4511
+ const absolute = path10__namespace.join(config.cwd, relative4);
3741
4512
  const result = {
3742
- path: relative3,
4513
+ path: relative4,
3743
4514
  exists: fs9__namespace.existsSync(absolute),
3744
4515
  included: false
3745
4516
  };
@@ -3748,8 +4519,8 @@ function explainPath(config, target) {
3748
4519
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
3749
4520
  includeSensitiveFiles: config.includeSensitiveFiles
3750
4521
  });
3751
- if (!collection.files.includes(relative3)) {
3752
- const record = collection.skipped.find((entry2) => entry2.path === relative3);
4522
+ if (!collection.files.includes(relative4)) {
4523
+ const record = collection.skipped.find((entry2) => entry2.path === relative4);
3753
4524
  if (record) {
3754
4525
  result.reason = record.reason;
3755
4526
  result.detail = record.detail;
@@ -3763,24 +4534,30 @@ function explainPath(config, target) {
3763
4534
  return result;
3764
4535
  }
3765
4536
  result.included = true;
3766
- result.layer = classifyLayer(relative3, config.layers);
3767
- result.kind = classifyKind(relative3, config.kinds);
3768
- result.language = detectLanguage(relative3);
3769
- const layerRule = findRuleMatch(config.layers, relative3);
4537
+ if (isBinaryFile(absolute)) {
4538
+ result.included = false;
4539
+ result.reason = "binary";
4540
+ result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
4541
+ return result;
4542
+ }
4543
+ result.layer = classifyLayer(relative4, config.layers);
4544
+ result.kind = classifyKind(relative4, config.kinds);
4545
+ result.language = detectLanguage(relative4);
4546
+ const layerRule = findRuleMatch(config.layers, relative4);
3770
4547
  if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
3771
- const kindRule = findRuleMatch(config.kinds, relative3);
4548
+ const kindRule = findRuleMatch(config.kinds, relative4);
3772
4549
  if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
3773
4550
  if (fs9__namespace.existsSync(absolute) && fs9__namespace.statSync(absolute).isFile()) {
3774
4551
  const buffer = fs9__namespace.readFileSync(absolute);
3775
4552
  const text2 = buffer.toString("utf8");
3776
4553
  result.bytes = buffer.length;
3777
4554
  result.lines = countLines(text2);
3778
- const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative3);
4555
+ const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative4);
3779
4556
  result.secretCount = findings.length;
3780
4557
  result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
3781
4558
  }
3782
4559
  const manifest = readManifest2(config);
3783
- const entry = manifest?.files.find((file) => file.relativePath === relative3);
4560
+ const entry = manifest?.files.find((file) => file.relativePath === relative4);
3784
4561
  result.part = entry?.part ?? null;
3785
4562
  result.inclusionMode = entry?.inclusionMode;
3786
4563
  return result;
@@ -3835,7 +4612,40 @@ function hashMap(manifest) {
3835
4612
  }
3836
4613
  return map;
3837
4614
  }
4615
+ function manifestProblem(value) {
4616
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
4617
+ const candidate = value;
4618
+ if (!Array.isArray(candidate.files)) return 'no "files" array';
4619
+ for (const entry of candidate.files) {
4620
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
4621
+ return 'a "files" entry is not an object';
4622
+ }
4623
+ if (typeof entry.relativePath !== "string") {
4624
+ return 'a "files" entry has no "relativePath"';
4625
+ }
4626
+ }
4627
+ const counts = candidate.counts;
4628
+ if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
4629
+ return 'no "counts" object';
4630
+ }
4631
+ for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
4632
+ if (typeof counts[key] !== "number") {
4633
+ return `no numeric "counts.${key}"`;
4634
+ }
4635
+ }
4636
+ if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
4637
+ if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
4638
+ return null;
4639
+ }
4640
+ function assertUsableManifest(value, label) {
4641
+ const problem = manifestProblem(value);
4642
+ if (problem) {
4643
+ throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
4644
+ }
4645
+ }
3838
4646
  function compareManifests(before, after) {
4647
+ assertUsableManifest(before, "before");
4648
+ assertUsableManifest(after, "after");
3839
4649
  const left = hashMap(before);
3840
4650
  const right = hashMap(after);
3841
4651
  const added = [];
@@ -3881,11 +4691,11 @@ function loadManifest(target) {
3881
4691
  } catch (err) {
3882
4692
  throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
3883
4693
  }
3884
- if (!parsed || !Array.isArray(parsed.files)) {
3885
- throw new Error(`${manifestPath} is not a snapshot manifest (no "files" array).`);
3886
- }
3887
- if (!parsed.counts || typeof parsed.counts !== "object") {
3888
- throw new Error(`${manifestPath} is not a snapshot manifest (no "counts" object).`);
4694
+ const problem = manifestProblem(parsed);
4695
+ if (problem) {
4696
+ throw new Error(
4697
+ `${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
4698
+ );
3889
4699
  }
3890
4700
  return parsed;
3891
4701
  }
@@ -3997,7 +4807,7 @@ function renderConfig(answers) {
3997
4807
  secrets: {
3998
4808
  // [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
3999
4809
  customPatterns: [],
4000
- // Set true to drop the 21 built-in rules.
4810
+ // Set true to drop the built-in rules above and keep only customPatterns.
4001
4811
  disableDefaultPatterns: false,
4002
4812
  },
4003
4813
 
@@ -4013,6 +4823,10 @@ function renderConfig(answers) {
4013
4823
  },
4014
4824
  // Skip the monolith above this size. null = no limit.
4015
4825
  maxMonolithBytes: null,
4826
+ // Top-level switch for the monolith, equivalent to artifacts.monolith.
4827
+ // When both are present the artifact entry wins; this one exists so the
4828
+ // generated file lists every setting the tool accepts.
4829
+ includeMonolith: ${artifacts.monolith},
4016
4830
 
4017
4831
  // \u2500\u2500 Advanced \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
4018
4832
  // afterGenerate: (manifest) => { /* write your own artifact */ }
@@ -4334,6 +5148,14 @@ function existingConfig(cwd) {
4334
5148
  }
4335
5149
  return null;
4336
5150
  }
5151
+ function packageJsonHasConfig(cwd) {
5152
+ try {
5153
+ const parsed = JSON.parse(fs9__namespace.readFileSync(path10__namespace.join(cwd, "package.json"), "utf8"));
5154
+ return typeof parsed.snapshot === "object" && parsed.snapshot !== null;
5155
+ } catch {
5156
+ return false;
5157
+ }
5158
+ }
4337
5159
  function artifactAnswers(options) {
4338
5160
  const artifacts = { ...DEFAULT_ANSWERS.artifacts };
4339
5161
  for (const name of options.skipArtifacts ?? []) {
@@ -4546,6 +5368,11 @@ async function runInit(options) {
4546
5368
  `A configuration already exists (${present}). Re-run with --force to replace it.`
4547
5369
  );
4548
5370
  }
5371
+ if (packageJsonHasConfig(options.cwd) && !options.force) {
5372
+ throw new Error(
5373
+ 'package.json already defines a "snapshot" config. Remove that key, or re-run with --force to write a config file that takes precedence over it.'
5374
+ );
5375
+ }
4549
5376
  if (fs9__namespace.existsSync(configPath) && !options.force) {
4550
5377
  throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
4551
5378
  }
@@ -4565,7 +5392,7 @@ async function runInit(options) {
4565
5392
 
4566
5393
  // src/index.ts
4567
5394
  var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
4568
- var GENERATOR_VERSION = "3.0.0";
5395
+ var GENERATOR_VERSION = "4.0.0";
4569
5396
  var SCHEMA_VERSION = "3.0.0";
4570
5397
  function sha256Hex2(input) {
4571
5398
  return crypto__namespace.createHash("sha256").update(input).digest("hex");
@@ -4735,27 +5562,213 @@ function resolveSafeOutputDirs(config) {
4735
5562
  return { outDir, tmpDir };
4736
5563
  }
4737
5564
  var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
5565
+ var STAGING_ENTRIES = /* @__PURE__ */ new Set([
5566
+ ".codebase-snapshot",
5567
+ "manifest.json",
5568
+ "README.md",
5569
+ "parts",
5570
+ "reports",
5571
+ "full-codebase.json",
5572
+ "full-codebase.md"
5573
+ ]);
4738
5574
  var OWNERSHIP_MARKER = ".codebase-snapshot";
4739
- function writeOwnershipMarker(dir) {
5575
+ var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
5576
+ var SNAPSHOT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5577
+ function listEntries(dir) {
5578
+ const entries = [];
5579
+ const walk = (absolute, prefix) => {
5580
+ let children;
5581
+ try {
5582
+ children = fs9__namespace.readdirSync(absolute, { withFileTypes: true });
5583
+ } catch {
5584
+ return;
5585
+ }
5586
+ for (const child of children) {
5587
+ const relative4 = prefix === "" ? child.name : `${prefix}/${child.name}`;
5588
+ entries.push(relative4);
5589
+ if (child.isDirectory()) walk(path10__namespace.join(absolute, child.name), relative4);
5590
+ }
5591
+ };
5592
+ walk(dir, "");
5593
+ return entries.sort();
5594
+ }
5595
+ function markerPayload(dir, state, id, createdAt) {
4740
5596
  const marker2 = {
4741
5597
  generator: GENERATOR_NAME,
4742
5598
  version: GENERATOR_VERSION,
4743
- schemaVersion: SCHEMA_VERSION
5599
+ schemaVersion: SCHEMA_VERSION,
5600
+ createdAt,
5601
+ state,
5602
+ id,
5603
+ entries: listEntries(dir)
4744
5604
  };
4745
- fs9__namespace.writeFileSync(path10__namespace.join(dir, OWNERSHIP_MARKER), `${JSON.stringify(marker2, null, 2)}
4746
- `, "utf8");
4747
- }
4748
- function isOwnOutput(dir) {
4749
- if (fs9__namespace.existsSync(path10__namespace.join(dir, OWNERSHIP_MARKER))) return true;
4750
5605
  const manifestPath = path10__namespace.join(dir, "manifest.json");
4751
- if (!fs9__namespace.existsSync(manifestPath)) return false;
5606
+ if (fs9__namespace.existsSync(manifestPath)) {
5607
+ marker2.manifestSha256 = sha256Hex2(fs9__namespace.readFileSync(manifestPath));
5608
+ }
5609
+ return marker2;
5610
+ }
5611
+ function writeOwnershipMarker(dir, marker2) {
5612
+ fs9__namespace.writeFileSync(
5613
+ path10__namespace.join(dir, OWNERSHIP_MARKER),
5614
+ `${JSON.stringify(marker2, null, 2)}
5615
+ `,
5616
+ "utf8"
5617
+ );
5618
+ }
5619
+ function readOwnershipMarker(dir) {
5620
+ const markerPath = path10__namespace.join(dir, OWNERSHIP_MARKER);
5621
+ if (!fs9__namespace.existsSync(markerPath)) {
5622
+ return { marker: null, problem: "it holds no .codebase-snapshot marker" };
5623
+ }
5624
+ let raw;
4752
5625
  try {
4753
- const parsed = JSON.parse(fs9__namespace.readFileSync(manifestPath, "utf8"));
4754
- const generator = parsed.generator;
4755
- return typeof parsed.schemaVersion === "string" && Array.isArray(parsed.files) && typeof generator === "object" && generator !== null && generator.name === GENERATOR_NAME;
5626
+ raw = fs9__namespace.readFileSync(markerPath, "utf8");
4756
5627
  } catch {
4757
- return false;
5628
+ return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
5629
+ }
5630
+ if (raw.trim().length === 0) {
5631
+ return { marker: null, problem: "its .codebase-snapshot marker is empty" };
5632
+ }
5633
+ let parsed;
5634
+ try {
5635
+ parsed = JSON.parse(raw);
5636
+ } catch {
5637
+ return { marker: null, problem: "its .codebase-snapshot marker is not valid JSON" };
5638
+ }
5639
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
5640
+ return { marker: null, problem: "its .codebase-snapshot marker is not a JSON object" };
5641
+ }
5642
+ const candidate = parsed;
5643
+ if (candidate.generator !== GENERATOR_NAME) {
5644
+ return { marker: null, problem: "its .codebase-snapshot marker names a different generator" };
5645
+ }
5646
+ if (typeof candidate.schemaVersion !== "string" || !SUPPORTED_MARKER_SCHEMAS.has(candidate.schemaVersion)) {
5647
+ return {
5648
+ marker: null,
5649
+ problem: `its .codebase-snapshot marker has a schema version this build does not understand (${String(candidate.schemaVersion)})`
5650
+ };
5651
+ }
5652
+ if (typeof candidate.id !== "string" || !SNAPSHOT_ID_PATTERN.test(candidate.id)) {
5653
+ return { marker: null, problem: "its .codebase-snapshot marker carries no snapshot id" };
5654
+ }
5655
+ if (candidate.state !== "staging" && candidate.state !== "complete") {
5656
+ return { marker: null, problem: "its .codebase-snapshot marker records no run state" };
5657
+ }
5658
+ return {
5659
+ marker: {
5660
+ generator: GENERATOR_NAME,
5661
+ version: typeof candidate.version === "string" ? candidate.version : "unknown",
5662
+ schemaVersion: candidate.schemaVersion,
5663
+ createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
5664
+ state: candidate.state,
5665
+ id: candidate.id,
5666
+ manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
5667
+ entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
5668
+ },
5669
+ problem: null
5670
+ };
5671
+ }
5672
+ function describeManifestProblem(value) {
5673
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "is not a JSON object";
5674
+ const candidate = value;
5675
+ const generator = candidate.generator;
5676
+ if (typeof generator !== "object" || generator === null || generator.name !== GENERATOR_NAME) {
5677
+ return "was not written by this generator";
5678
+ }
5679
+ if (!Array.isArray(candidate.files)) return "has no files array";
5680
+ return null;
5681
+ }
5682
+ function assessSnapshotDirectory(dir) {
5683
+ const { marker: marker2, problem } = readOwnershipMarker(dir);
5684
+ if (!marker2) return { owned: false, problem, foreignEntries: [], marker: null };
5685
+ if (marker2.state !== "complete") {
5686
+ return {
5687
+ owned: false,
5688
+ problem: "its .codebase-snapshot marker records a run that never finished writing it",
5689
+ foreignEntries: [],
5690
+ marker: marker2
5691
+ };
5692
+ }
5693
+ const manifestPath = path10__namespace.join(dir, "manifest.json");
5694
+ if (fs9__namespace.existsSync(manifestPath)) {
5695
+ let raw;
5696
+ try {
5697
+ raw = fs9__namespace.readFileSync(manifestPath, "utf8");
5698
+ } catch {
5699
+ return {
5700
+ owned: false,
5701
+ problem: "its manifest.json could not be read",
5702
+ foreignEntries: [],
5703
+ marker: marker2
5704
+ };
5705
+ }
5706
+ let parsed;
5707
+ try {
5708
+ parsed = JSON.parse(raw);
5709
+ } catch {
5710
+ return {
5711
+ owned: false,
5712
+ problem: "its manifest.json is not valid JSON",
5713
+ foreignEntries: [],
5714
+ marker: marker2
5715
+ };
5716
+ }
5717
+ const manifestProblem2 = describeManifestProblem(parsed);
5718
+ if (manifestProblem2) {
5719
+ return {
5720
+ owned: false,
5721
+ problem: `its manifest.json ${manifestProblem2}`,
5722
+ foreignEntries: [],
5723
+ marker: marker2
5724
+ };
5725
+ }
5726
+ if (!marker2.manifestSha256) {
5727
+ return {
5728
+ owned: false,
5729
+ problem: "its .codebase-snapshot marker records no manifest digest",
5730
+ foreignEntries: [],
5731
+ marker: marker2
5732
+ };
5733
+ }
5734
+ if (sha256Hex2(raw) !== marker2.manifestSha256) {
5735
+ return {
5736
+ owned: false,
5737
+ problem: "its manifest.json does not match the digest recorded in its .codebase-snapshot marker",
5738
+ foreignEntries: [],
5739
+ marker: marker2
5740
+ };
5741
+ }
5742
+ } else if (marker2.manifestSha256) {
5743
+ return {
5744
+ owned: false,
5745
+ problem: "its manifest.json is missing although its marker records one",
5746
+ foreignEntries: [],
5747
+ marker: marker2
5748
+ };
4758
5749
  }
5750
+ const recorded = new Set(marker2.entries ?? []);
5751
+ if (recorded.size === 0) {
5752
+ return {
5753
+ owned: false,
5754
+ problem: "its .codebase-snapshot marker records no contents",
5755
+ foreignEntries: [],
5756
+ marker: marker2
5757
+ };
5758
+ }
5759
+ const foreignEntries = listEntries(dir).filter(
5760
+ (entry) => !recorded.has(entry) && !PLACEHOLDER_ENTRIES.has(entry)
5761
+ );
5762
+ return {
5763
+ owned: foreignEntries.length === 0,
5764
+ problem: null,
5765
+ foreignEntries,
5766
+ marker: marker2
5767
+ };
5768
+ }
5769
+ function describeEntries(entries, limit = 5) {
5770
+ const shown = entries.slice(0, limit).join(", ");
5771
+ return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
4759
5772
  }
4760
5773
  function assertReplaceable(target, label) {
4761
5774
  if (!fs9__namespace.existsSync(target)) return;
@@ -4765,16 +5778,124 @@ function assertReplaceable(target, label) {
4765
5778
  );
4766
5779
  }
4767
5780
  const entries = fs9__namespace.readdirSync(target);
4768
- const meaningful = entries.filter((name) => !PLACEHOLDER_ENTRIES.has(name));
4769
- if (meaningful.length === 0) return;
4770
- if (isOwnOutput(target)) return;
4771
- const preview = meaningful.slice(0, 5).join(", ");
4772
- const more = meaningful.length > 5 ? `, and ${meaningful.length - 5} more` : "";
5781
+ if (entries.length === 0) return;
5782
+ if (label === "outDir" && entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
5783
+ const { marker: marker2, problem } = readOwnershipMarker(target);
5784
+ if (label === "tmpDir") {
5785
+ if (!marker2) {
5786
+ throw new Error(
5787
+ `Refusing to replace tmpDir ${target}: ${problem ?? "it does not carry this generator's marker"}. This generator only clears a staging directory it wrote. Point tmpDir at a new or empty directory, or remove ${target} yourself.`
5788
+ );
5789
+ }
5790
+ const unexpected = entries.filter((name) => !STAGING_ENTRIES.has(name));
5791
+ if (unexpected.length > 0) {
5792
+ throw new Error(
5793
+ `Refusing to replace tmpDir ${target}: it holds ${describeEntries(unexpected)}, which this generator did not write. Point tmpDir at a new or empty directory, or remove those files yourself.`
5794
+ );
5795
+ }
5796
+ return;
5797
+ }
5798
+ const verdict = assessSnapshotDirectory(target);
5799
+ if (verdict.owned) return;
5800
+ if (verdict.foreignEntries.length > 0) {
5801
+ throw new Error(
5802
+ `Refusing to replace ${label} ${target}: it carries this generator's ownership marker, but these entries are not part of the snapshot it recorded: ${describeEntries(verdict.foreignEntries)}. Remove them yourself, or point ${label} at a new or empty directory.`
5803
+ );
5804
+ }
5805
+ const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
4773
5806
  throw new Error(
4774
- `Refusing to replace ${label} ${target}: it exists, is not empty, and does not look like a snapshot this generator wrote (found ${preview}${more}). Point ${label} at a new or empty directory, or delete it yourself first.`
5807
+ `Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(entries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
5808
+ );
5809
+ }
5810
+ function ensureParentDirectory(target, label) {
5811
+ const parent = path10__namespace.dirname(target);
5812
+ if (fs9__namespace.existsSync(parent) && !fs9__namespace.statSync(parent).isDirectory()) {
5813
+ throw new Error(
5814
+ `Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
5815
+ );
5816
+ }
5817
+ try {
5818
+ fs9__namespace.mkdirSync(parent, { recursive: true });
5819
+ } catch (err) {
5820
+ throw new Error(
5821
+ `Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
5822
+ );
5823
+ }
5824
+ }
5825
+ function acquireRunLock(outDir) {
5826
+ const lockPath = path10__namespace.join(
5827
+ os__namespace.tmpdir(),
5828
+ `codebase-snapshot-${sha256Hex2(outDir).slice(0, 16)}.lock`
4775
5829
  );
5830
+ const payload = `${JSON.stringify({
5831
+ pid: process.pid,
5832
+ outDir,
5833
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
5834
+ })}
5835
+ `;
5836
+ for (let attempt = 0; attempt < 2; attempt += 1) {
5837
+ try {
5838
+ const handle = fs9__namespace.openSync(lockPath, "wx");
5839
+ try {
5840
+ fs9__namespace.writeSync(handle, payload);
5841
+ } finally {
5842
+ fs9__namespace.closeSync(handle);
5843
+ }
5844
+ return () => {
5845
+ try {
5846
+ fs9__namespace.rmSync(lockPath, { force: true });
5847
+ } catch {
5848
+ }
5849
+ };
5850
+ } catch (err) {
5851
+ if (err.code !== "EEXIST") throw err;
5852
+ if (attempt === 0 && lockOwnerIsGone(lockPath)) {
5853
+ fs9__namespace.rmSync(lockPath, { force: true });
5854
+ continue;
5855
+ }
5856
+ throw new Error(
5857
+ `Another generate run is already writing ${outDir}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
5858
+ );
5859
+ }
5860
+ }
5861
+ throw new Error(`Could not acquire the generate lock for ${outDir}.`);
5862
+ }
5863
+ function lockOwnerIsGone(lockPath) {
5864
+ let raw;
5865
+ try {
5866
+ raw = fs9__namespace.readFileSync(lockPath, "utf8");
5867
+ } catch {
5868
+ return false;
5869
+ }
5870
+ let pid;
5871
+ try {
5872
+ pid = JSON.parse(raw).pid;
5873
+ } catch {
5874
+ return false;
5875
+ }
5876
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
5877
+ try {
5878
+ process.kill(pid, 0);
5879
+ return false;
5880
+ } catch (err) {
5881
+ return err.code === "ESRCH";
5882
+ }
5883
+ }
5884
+ function writeManifestAtomically(manifestPath, manifest) {
5885
+ const tempPath = `${manifestPath}.${process.pid}.tmp`;
5886
+ fs9__namespace.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
5887
+ try {
5888
+ fs9__namespace.renameSync(tempPath, manifestPath);
5889
+ } catch (err) {
5890
+ try {
5891
+ fs9__namespace.rmSync(tempPath, { force: true });
5892
+ } catch {
5893
+ }
5894
+ throw err;
5895
+ }
4776
5896
  }
4777
5897
  function swapIntoPlace(tmpDir, outDir) {
5898
+ ensureParentDirectory(outDir, "outDir");
4778
5899
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
4779
5900
  const hadPrevious = fs9__namespace.existsSync(outDir);
4780
5901
  if (hadPrevious) fs9__namespace.renameSync(outDir, backupDir);
@@ -4863,6 +5984,7 @@ function generateSnapshot(configInput, options) {
4863
5984
  includeSensitiveFiles: config.includeSensitiveFiles
4864
5985
  });
4865
5986
  const files = [];
5987
+ const monolithBodies = /* @__PURE__ */ new Map();
4866
5988
  const skipped = [...collection.skipped];
4867
5989
  const secretFindings = [];
4868
5990
  const warnings = [];
@@ -4926,6 +6048,9 @@ function generateSnapshot(configInput, options) {
4926
6048
  isPreviewTruncated = preview.isTruncated;
4927
6049
  includedLines = preview.includedLines;
4928
6050
  }
6051
+ if (config.includeMonolith && isPreviewTruncated) {
6052
+ monolithBodies.set(relativePath, redacted);
6053
+ }
4929
6054
  let summary;
4930
6055
  let summaryQuality;
4931
6056
  try {
@@ -4968,7 +6093,12 @@ function generateSnapshot(configInput, options) {
4968
6093
  ).join("\n")
4969
6094
  );
4970
6095
  const fullContents = /* @__PURE__ */ new Map();
4971
- for (const file of files) fullContents.set(file.relativePath, file.content ?? "");
6096
+ for (const file of files) {
6097
+ fullContents.set(
6098
+ file.relativePath,
6099
+ monolithBodies.get(file.relativePath) ?? file.content ?? ""
6100
+ );
6101
+ }
4972
6102
  const parts = partitionByBudget(files, config, fingerprint);
4973
6103
  const maxEffectiveBudget = Math.max(
4974
6104
  config.maxPartBytes,
@@ -5054,10 +6184,17 @@ function generateSnapshot(configInput, options) {
5054
6184
  let monolithJson = null;
5055
6185
  let monolithMarkdown = null;
5056
6186
  if (config.includeMonolith) {
5057
- const fullFiles = files.map((file) => ({
5058
- ...file,
5059
- content: fullContents.get(file.relativePath) ?? ""
5060
- }));
6187
+ const fullFiles = files.map((file) => {
6188
+ const content = fullContents.get(file.relativePath) ?? "";
6189
+ return {
6190
+ ...file,
6191
+ content,
6192
+ inclusionMode: "full",
6193
+ isPreviewTruncated: false,
6194
+ includedBytes: Buffer.byteLength(content, "utf8"),
6195
+ includedLines: countLines(content)
6196
+ };
6197
+ });
5061
6198
  const json = generateMonolithJson(manifest, fullFiles);
5062
6199
  const markdown = generateMonolithMarkdown(manifest, fullFiles);
5063
6200
  const jsonBytes = Buffer.byteLength(json, "utf8");
@@ -5085,57 +6222,89 @@ function generateSnapshot(configInput, options) {
5085
6222
  }
5086
6223
  for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
5087
6224
  manifest.totals.warningCount = manifest.warnings.length;
6225
+ const { outDir, tmpDir } = resolveSafeOutputDirs(config);
6226
+ const { artifacts } = config;
6227
+ assertReplaceable(tmpDir, "tmpDir");
6228
+ assertReplaceable(outDir, "outDir");
5088
6229
  if (dryRun) {
5089
6230
  runAfterGenerate(config, manifest);
5090
6231
  return manifest;
5091
6232
  }
5092
- const { outDir, tmpDir } = resolveSafeOutputDirs(config);
5093
- assertReplaceable(tmpDir, "tmpDir");
5094
- assertReplaceable(outDir, "outDir");
5095
- const { artifacts } = config;
5096
- fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
5097
- fs9__namespace.mkdirSync(tmpDir, { recursive: true });
5098
- writeOwnershipMarker(tmpDir);
5099
- const partsDir = path10__namespace.join(tmpDir, "parts");
5100
- const reportsDir = path10__namespace.join(tmpDir, "reports");
5101
- if (artifacts.parts) fs9__namespace.mkdirSync(partsDir, { recursive: true });
5102
- if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
5103
- fs9__namespace.mkdirSync(reportsDir, { recursive: true });
5104
- }
5105
- if (artifacts.parts) {
5106
- for (const part of parts) {
5107
- fs9__namespace.writeFileSync(path10__namespace.join(partsDir, part.filename), part.markdown, "utf8");
6233
+ const releaseLock = acquireRunLock(outDir);
6234
+ let stagingStarted = false;
6235
+ try {
6236
+ ensureParentDirectory(outDir, "outDir");
6237
+ fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
6238
+ fs9__namespace.mkdirSync(tmpDir, { recursive: true });
6239
+ stagingStarted = true;
6240
+ const snapshotId = crypto__namespace.randomUUID();
6241
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6242
+ writeOwnershipMarker(tmpDir, {
6243
+ generator: GENERATOR_NAME,
6244
+ version: GENERATOR_VERSION,
6245
+ schemaVersion: SCHEMA_VERSION,
6246
+ createdAt,
6247
+ state: "staging",
6248
+ id: snapshotId
6249
+ });
6250
+ const partsDir = path10__namespace.join(tmpDir, "parts");
6251
+ const reportsDir = path10__namespace.join(tmpDir, "reports");
6252
+ if (artifacts.parts) fs9__namespace.mkdirSync(partsDir, { recursive: true });
6253
+ if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
6254
+ fs9__namespace.mkdirSync(reportsDir, { recursive: true });
6255
+ }
6256
+ if (artifacts.parts) {
6257
+ for (const part of parts) {
6258
+ fs9__namespace.writeFileSync(path10__namespace.join(partsDir, part.filename), part.markdown, "utf8");
6259
+ }
5108
6260
  }
6261
+ if (artifacts.projectTree) {
6262
+ const projectTree = generateProjectTree(files.map((file) => file.relativePath));
6263
+ fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "project-tree.md"), projectTree, "utf8");
6264
+ }
6265
+ if (artifacts.navigation) {
6266
+ fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
6267
+ }
6268
+ if (artifacts.audit) {
6269
+ fs9__namespace.writeFileSync(
6270
+ path10__namespace.join(reportsDir, "audit.md"),
6271
+ generateAuditReport(manifest, secretFindings),
6272
+ "utf8"
6273
+ );
6274
+ }
6275
+ if (artifacts.readme) {
6276
+ fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
6277
+ }
6278
+ if (artifacts.manifest) {
6279
+ fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6280
+ }
6281
+ if (monolithJson !== null) {
6282
+ fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
6283
+ }
6284
+ if (monolithMarkdown !== null) {
6285
+ fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
6286
+ }
6287
+ writeOwnershipMarker(tmpDir, markerPayload(tmpDir, "complete", snapshotId, createdAt));
6288
+ swapIntoPlace(tmpDir, outDir);
6289
+ stagingStarted = false;
6290
+ const warningsBeforeHook = manifest.warnings.length;
6291
+ runAfterGenerate(config, manifest);
6292
+ if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
6293
+ writeManifestAtomically(path10__namespace.join(outDir, "manifest.json"), manifest);
6294
+ }
6295
+ writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
6296
+ return manifest;
6297
+ } catch (err) {
6298
+ if (stagingStarted) {
6299
+ try {
6300
+ fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
6301
+ } catch {
6302
+ }
6303
+ }
6304
+ throw err;
6305
+ } finally {
6306
+ releaseLock();
5109
6307
  }
5110
- if (artifacts.projectTree) {
5111
- const projectTree = generateProjectTree(files.map((file) => file.relativePath));
5112
- fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "project-tree.md"), projectTree, "utf8");
5113
- }
5114
- if (artifacts.navigation) {
5115
- fs9__namespace.writeFileSync(path10__namespace.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
5116
- }
5117
- if (artifacts.audit) {
5118
- fs9__namespace.writeFileSync(
5119
- path10__namespace.join(reportsDir, "audit.md"),
5120
- generateAuditReport(manifest, secretFindings),
5121
- "utf8"
5122
- );
5123
- }
5124
- if (artifacts.readme) {
5125
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
5126
- }
5127
- if (artifacts.manifest) {
5128
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
5129
- }
5130
- if (monolithJson !== null) {
5131
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
5132
- }
5133
- if (monolithMarkdown !== null) {
5134
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
5135
- }
5136
- swapIntoPlace(tmpDir, outDir);
5137
- runAfterGenerate(config, manifest);
5138
- return manifest;
5139
6308
  }
5140
6309
  async function generateSnapshotAsync(configInput, options) {
5141
6310
  return generateSnapshot(configInput, options);
@@ -5162,6 +6331,7 @@ exports.detectLanguage = detectLanguage;
5162
6331
  exports.detectPreset = detectPreset;
5163
6332
  exports.explainPath = explainPath;
5164
6333
  exports.fenceForContent = fenceForContent;
6334
+ exports.fenceForText = fenceForText;
5165
6335
  exports.generateProjectTree = generateProjectTree;
5166
6336
  exports.generateSnapshot = generateSnapshot;
5167
6337
  exports.generateSnapshotAsync = generateSnapshotAsync;