@ankurhalder/codebase-snapshot 3.0.0 → 4.0.1

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