@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.
@@ -1,5 +1,6 @@
1
1
  import * as crypto from 'crypto';
2
2
  import * as fs9 from 'fs';
3
+ import * as os from 'os';
3
4
  import * as path10 from 'path';
4
5
  import ts from 'typescript';
5
6
  import { pathToFileURL } from 'url';
@@ -547,14 +548,42 @@ function summarizeTypeScriptSource(relativePath, content) {
547
548
  const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
548
549
  return Boolean(modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword));
549
550
  };
550
- const recordFunctionLike = (name, exported) => {
551
+ const rendersJsx = (node) => {
552
+ const pending = [node];
553
+ while (pending.length > 0) {
554
+ const current = pending.pop();
555
+ if (ts.isJsxElement(current) || ts.isJsxSelfClosingElement(current) || ts.isJsxFragment(current)) {
556
+ return true;
557
+ }
558
+ if (ts.isCallExpression(current)) {
559
+ const callee = current.expression;
560
+ const calleeName = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : "";
561
+ if (calleeName === "createElement") return true;
562
+ }
563
+ ts.forEachChild(current, (child) => {
564
+ pending.push(child);
565
+ });
566
+ }
567
+ return false;
568
+ };
569
+ const REACT_BASE_CLASS = /(?:^|\.)(?:PureComponent|Component)$/;
570
+ const extendsReactBase = (node) => {
571
+ const heritage = (node.heritageClauses ?? []).find(
572
+ (clause) => clause.token === ts.SyntaxKind.ExtendsKeyword
573
+ );
574
+ const expression = heritage?.types[0]?.expression;
575
+ if (!expression) return false;
576
+ const text2 = ts.isIdentifier(expression) ? expression.text : expression.getText(sourceFile);
577
+ return REACT_BASE_CLASS.test(text2);
578
+ };
579
+ const recordFunctionLike = (name, exported, node) => {
551
580
  summary.functions.push(name);
552
581
  if (exported) {
553
582
  summary.exports.push(name);
554
583
  if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
555
584
  }
556
585
  if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
557
- if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
586
+ if (/^[A-Z]/.test(name) && rendersJsx(node)) summary.reactComponents.push(name);
558
587
  };
559
588
  const stack = [sourceFile];
560
589
  while (stack.length > 0) {
@@ -583,7 +612,7 @@ function summarizeTypeScriptSource(relativePath, content) {
583
612
  if (ts.isFunctionDeclaration(node)) {
584
613
  const exported = isExported(node);
585
614
  if (node.name) {
586
- recordFunctionLike(node.name.text, exported);
615
+ recordFunctionLike(node.name.text, exported, node);
587
616
  if (isDefault(node)) summary.exports.push("default");
588
617
  } else if (isDefault(node)) {
589
618
  summary.exports.push("default");
@@ -594,7 +623,9 @@ function summarizeTypeScriptSource(relativePath, content) {
594
623
  summary.classes.push(node.name.text);
595
624
  if (isExported(node)) summary.exports.push(node.name.text);
596
625
  if (isDefault(node)) summary.exports.push("default");
597
- if (/^[A-Z]/.test(node.name.text)) summary.reactComponents.push(node.name.text);
626
+ if (/^[A-Z]/.test(node.name.text) && (extendsReactBase(node) || rendersJsx(node))) {
627
+ summary.reactComponents.push(node.name.text);
628
+ }
598
629
  } else if (isDefault(node)) {
599
630
  summary.exports.push("default");
600
631
  }
@@ -622,7 +653,9 @@ function summarizeTypeScriptSource(relativePath, content) {
622
653
  if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
623
654
  }
624
655
  if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
625
- if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
656
+ if (/^[A-Z]/.test(name) && declaration.initializer !== void 0 && rendersJsx(declaration.initializer)) {
657
+ summary.reactComponents.push(name);
658
+ }
626
659
  }
627
660
  }
628
661
  const children = [];
@@ -1676,6 +1709,416 @@ function toDetected(preset) {
1676
1709
  function detectPreset(cwd, forced) {
1677
1710
  return detectPresetFromContext(buildPresetContext(cwd), forced);
1678
1711
  }
1712
+
1713
+ // src/security/patterns.ts
1714
+ var STRONG_CREDENTIAL_NAME = "(?:password|passwd|passphrase|pwd|api[_-]?key|api[_-]?secret|secret[_-]?access[_-]?key|secret[_-]?key|client[_-]?secret[_-]?key|client[_-]?secret|private[_-]?key|aws[_-]?secret[_-]?access[_-]?key|access[_-]?token|refresh[_-]?token|auth[_-]?token|session[_-]?token|secret|token)";
1715
+ var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
1716
+ var NOT_A_LITERAL_VALUE = "(?!(?:string|number|boolean|bigint|symbol|unknown|any|never|void|null|undefined|true|false|process\\.env|Buffer|Record|object)\\b)";
1717
+ var UNQUOTED_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{1,512})(?![A-Za-z0-9_+=/.-]|\\()";
1718
+ var UNQUOTED_AMBIGUOUS_LITERAL = "(?![0-9]+(?![A-Za-z0-9_+=/.-]))([A-Za-z0-9_+=/-]{8,512})(?![A-Za-z0-9_+=/.-]|\\()";
1719
+ var UNQUOTED_STRONG_NAME = `(?<!id[-_])${STRONG_CREDENTIAL_NAME}`;
1720
+ var DEFAULT_SECRET_PATTERNS = [
1721
+ {
1722
+ rule: "private-key-header",
1723
+ pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
1724
+ },
1725
+ {
1726
+ rule: "jwt-token",
1727
+ pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
1728
+ },
1729
+ {
1730
+ rule: "aws-access-key-id",
1731
+ pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
1732
+ },
1733
+ {
1734
+ // The name is no longer limited to the three snake_case spellings: an
1735
+ // `awsSecret` / `secretAccessKey` property leaked verbatim because only the
1736
+ // literal words matched. The value is masked alone, so the key survives.
1737
+ rule: "aws-secret-access-key",
1738
+ pattern: new RegExp(
1739
+ `${STRONG_CREDENTIAL_NAME}["']?\\s{0,8}[:=]\\s{0,8}["']?([A-Za-z0-9/+=]{40})["']?`,
1740
+ "i"
1741
+ ),
1742
+ valueGroup: 1
1743
+ },
1744
+ {
1745
+ // A 40-character base64 value with no key name in sight. A key-independent
1746
+ // rule is the only thing that catches `new AWS.SecretKey(…)` arguments and
1747
+ // pasted `~/.aws/credentials` fragments, but a bare length test also matches
1748
+ // every 40-char git SHA-1, so the value must mix case and digits and must
1749
+ // not be pure hex.
1750
+ rule: "aws-secret-access-key-value",
1751
+ pattern: /(?<![A-Za-z0-9/+=])(?![0-9a-fA-F]{40}(?![A-Za-z0-9/+=]))(?=[A-Za-z0-9/+=]{40}(?![A-Za-z0-9/+=]))(?=[A-Za-z0-9/+=]{0,39}[A-Z])(?=[A-Za-z0-9/+=]{0,39}[a-z])(?=[A-Za-z0-9/+=]{0,39}[0-9])[A-Za-z0-9/+=]{40}/
1752
+ },
1753
+ {
1754
+ rule: "github-token",
1755
+ pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
1756
+ },
1757
+ {
1758
+ // `xoxc-`/`xoxd-` are the user and refresh tokens, `xoxb`/`xoxp` the bot and
1759
+ // user tokens; the whole Slack family shares one prefix shape.
1760
+ rule: "slack-token",
1761
+ pattern: /xox[baprsdc]-[0-9A-Za-z-]{10,250}/
1762
+ },
1763
+ {
1764
+ // Prefix-only vendors. Each prefix below is documented, globally unique and
1765
+ // cannot collide with an ordinary identifier, which is what makes a bare
1766
+ // prefix rule cheap and safe: `glpat-` (GitLab), `hvs.`/`s.` (Vault service
1767
+ // and batch tokens), `whsec_` (Stripe webhook secret), `dckr_pat_` (Docker
1768
+ // Hub), `hf_` (HuggingFace), `ya29.` (Google OAuth access token).
1769
+ rule: "known-vendor-token-prefix",
1770
+ pattern: /\b(?:glpat-[A-Za-z0-9_-]{20,64}|hvs\.[A-Za-z0-9_-]{20,128}|s\.[A-Za-z0-9]{20,64}|whsec_[A-Za-z0-9]{20,64}|dckr_pat_[A-Za-z0-9_-]{20,64}|hf_[A-Za-z0-9]{30,64}|ya29\.[A-Za-z0-9_-]{20,128})/
1771
+ },
1772
+ {
1773
+ rule: "slack-webhook-url",
1774
+ pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
1775
+ },
1776
+ {
1777
+ rule: "stripe-secret-key",
1778
+ pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
1779
+ },
1780
+ {
1781
+ rule: "google-api-key",
1782
+ pattern: /AIza[0-9A-Za-z_-]{35}/
1783
+ },
1784
+ {
1785
+ rule: "google-oauth-client-secret",
1786
+ pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
1787
+ },
1788
+ {
1789
+ rule: "openai-anthropic-key",
1790
+ pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
1791
+ },
1792
+ {
1793
+ rule: "npm-token",
1794
+ pattern: /npm_[A-Za-z0-9]{36}/
1795
+ },
1796
+ {
1797
+ rule: "pypi-token",
1798
+ pattern: /pypi-[A-Za-z0-9_-]{50,180}/
1799
+ },
1800
+ {
1801
+ rule: "sendgrid-key",
1802
+ pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
1803
+ },
1804
+ {
1805
+ rule: "twilio-api-key",
1806
+ pattern: /SK[0-9a-fA-F]{32}/
1807
+ },
1808
+ {
1809
+ rule: "digitalocean-token",
1810
+ pattern: /dop_v1_[0-9a-f]{64}/
1811
+ },
1812
+ {
1813
+ rule: "azure-storage-account-key",
1814
+ pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
1815
+ },
1816
+ {
1817
+ // Scheme-less userinfo: `//user:password@host`.
1818
+ //
1819
+ // Enumerating schemes is a losing game — `mysqlx`, `svn`, `telnet`, `imap`,
1820
+ // `pop3`, `nntp`, `kafka`, `elasticsearch` each leaked a password while the
1821
+ // list grew by hand. This rule matches the shape instead, so any scheme
1822
+ // (including one invented tomorrow) is covered, provided the `//` is followed
1823
+ // immediately by the userinfo: `// see the docs` and a bare reminder comment
1824
+ // never match, because a space or the comment wording breaks the shape.
1825
+ rule: "uri-credentials",
1826
+ pattern: /\/\/[^\s:/@"'`;()<>,]{1,128}:[^\s:/@"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
1827
+ },
1828
+ {
1829
+ // The host tail stops at quotes, `;`, `)`, `<`, `>` and `,`: an unbounded
1830
+ // `[^\s/]` used to swallow the closing quote and the statement separator of
1831
+ // the string the URL was written in.
1832
+ rule: "database-uri-credentials",
1833
+ pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse|mssql|sqlserver|oracle):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/
1834
+ },
1835
+ {
1836
+ rule: "basic-auth-url",
1837
+ pattern: /(?:https?|ftp|sftp|ssh|git|smtp|smtps|ldap|ldaps):\/\/[^\s:@/"'`;()<>,]{1,128}:[^\s@/"'`;()<>,]{1,256}@[^\s/"'`;()<>,]{1,256}/i
1838
+ },
1839
+ {
1840
+ // Quoted credentials in any common key spelling. The value may be as short
1841
+ // as one character: `password: "hunter2"` is a credential whatever its
1842
+ // entropy, and a length floor was exactly how it survived into `parts`.
1843
+ //
1844
+ // The key is either bare (`dbPassword:`) or wholly quoted (`"dbPassword":`).
1845
+ // There is deliberately no optional quote *before* the separator: that let a
1846
+ // previous string's closing quote stand in for the key's, so
1847
+ // ``${pass ? " PASS" : " FAIL"}`` had `" FAIL"` masked as if `PASS` were
1848
+ // the key name.
1849
+ rule: "credential-assignment",
1850
+ pattern: new RegExp(
1851
+ `(?:["']${STRONG_CREDENTIAL_NAME}["']|${STRONG_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{1,512})\\1`,
1852
+ "i"
1853
+ ),
1854
+ valueGroup: 2
1855
+ },
1856
+ {
1857
+ // `pass`, `id-token` and `credentials` are ambiguous on their own, so they
1858
+ // are accepted only against a quoted value long enough to be a credential.
1859
+ rule: "credential-assignment-weak",
1860
+ pattern: new RegExp(
1861
+ `(?:["']${WEAK_CREDENTIAL_NAME}["']|${WEAK_CREDENTIAL_NAME})\\s{0,8}[:=]\\s{0,8}(["'\`])([^"'\`\\n]{8,512})\\1`,
1862
+ "i"
1863
+ ),
1864
+ valueGroup: 2
1865
+ },
1866
+ {
1867
+ // The unquoted twin for YAML, docker-compose and `.env`: those formats never
1868
+ // quote their values, so the rules above cannot see them. `valueGroup` keeps
1869
+ // `POSTGRES_PASSWORD:` in the output.
1870
+ rule: "credential-assignment-unquoted",
1871
+ pattern: new RegExp(
1872
+ `${UNQUOTED_STRONG_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_LITERAL}`,
1873
+ "i"
1874
+ ),
1875
+ valueGroup: 1
1876
+ },
1877
+ {
1878
+ // `pass: hunter2hunter2` and `credentials: hunter2hunter2`: the ambiguous
1879
+ // names, unquoted, with the length and non-digit floor that keeps
1880
+ // `pass: 3` / `credentials: none` readable.
1881
+ rule: "credential-assignment-unquoted-weak",
1882
+ pattern: new RegExp(
1883
+ `${WEAK_CREDENTIAL_NAME}\\s{0,8}[:=]\\s{0,8}${NOT_A_LITERAL_VALUE}${UNQUOTED_AMBIGUOUS_LITERAL}`,
1884
+ "i"
1885
+ ),
1886
+ valueGroup: 1
1887
+ },
1888
+ {
1889
+ // `Authorization: Bearer <token>` and `Authorization: Basic <base64>`.
1890
+ //
1891
+ // The scheme is consumed by a lookbehind so the match is the token alone and
1892
+ // needs no `valueGroup`. A lone `Bearer`/`Basic`, and a word after it that
1893
+ // cannot be a credential (`Bearer authentication`), stay readable: the value
1894
+ // must contain a digit, be at least 32 chars, or carry base64 `=` padding.
1895
+ rule: "authorization-header",
1896
+ pattern: /(?<=(?:Bearer|Basic)\s{1,8})(?:[A-Za-z0-9_.+/-]{0,31}[0-9][A-Za-z0-9_.+/-]{0,1024}|[A-Za-z0-9_.+/-]{32,1024}|[A-Za-z0-9_.+/-]{8,1024}={1,2})(?![A-Za-z0-9_.+/=-])/
1897
+ },
1898
+ {
1899
+ // Broader names than before (`apiKey`, `clientSecret`, `privateKey` were
1900
+ // invisible to it) and value-only masking. Kept alongside the two rules
1901
+ // above because it also matches a *quoted key* followed by an unquoted
1902
+ // value, e.g. `"client_secret": abc123`.
1903
+ rule: "generic-secret-assignment",
1904
+ pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["']([^"'\s]{8,256})["']/i,
1905
+ valueGroup: 1
1906
+ },
1907
+ {
1908
+ // Covers `.env` / shell assignment syntax, which never quotes its values and
1909
+ // was therefore invisible to the rule above. Anchored to the line start and
1910
+ // limited to secret-looking names to stay at zero false positives.
1911
+ //
1912
+ // The name is matched with BOUNDED quantifiers on purpose: an unbounded
1913
+ // `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
1914
+ // every offset of a long identifier, which made this rule quadratic
1915
+ // (measured 5.6 s for a single 240 KB line before bounding).
1916
+ // The value is matched on its own — `=\s*(value)` — and masked alone.
1917
+ // Anchoring the value to end-of-line (`[^\s#]{6,512}$`) meant ANY trailing
1918
+ // comment or space disabled the rule, so `TOKEN=secret # rotate` and
1919
+ // `TOKEN=secret ` were emitted in clear; a CRLF file disabled it wholesale.
1920
+ rule: "env-secret-assignment",
1921
+ pattern: /^\s{0,64}(?:export\s{1,8})?[A-Za-z0-9_]{0,96}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL)[A-Za-z0-9_]{0,96}\s{0,8}=\s{0,8}(?!["'`$<{[\]\s])([^\s#;"'`)]{1,512})/i,
1922
+ valueGroup: 1
1923
+ }
1924
+ ];
1925
+ var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
1926
+ var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
1927
+
1928
+ // src/security/redact.ts
1929
+ var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
1930
+ var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
1931
+ function describeValue(value) {
1932
+ if (value === null) return "null";
1933
+ if (Array.isArray(value)) return "an array";
1934
+ if (value instanceof RegExp) return value.toString();
1935
+ if (typeof value === "string") return `the string ${JSON.stringify(value)}`;
1936
+ return `a ${typeof value}`;
1937
+ }
1938
+ function compilePattern(rule, pattern, valueGroup) {
1939
+ if (pattern instanceof RegExp) {
1940
+ const flags = new Set(pattern.flags.replace(/[gy]/g, "").split(""));
1941
+ if (valueGroup !== void 0) flags.add("d");
1942
+ return {
1943
+ rule,
1944
+ pattern: new RegExp(pattern.source, [...flags].join("")),
1945
+ ...valueGroup === void 0 ? {} : { valueGroup }
1946
+ };
1947
+ }
1948
+ if (typeof pattern === "string") {
1949
+ try {
1950
+ return {
1951
+ rule,
1952
+ pattern: new RegExp(pattern, valueGroup === void 0 ? "i" : "id"),
1953
+ ...valueGroup === void 0 ? {} : { valueGroup }
1954
+ };
1955
+ } catch (err) {
1956
+ throw new Error(
1957
+ `Invalid regular expression for secret rule "${rule}": ${err.message}`
1958
+ );
1959
+ }
1960
+ }
1961
+ throw new Error(
1962
+ `Secret rule "${rule}" has a pattern that is neither a RegExp nor a string (received ${describeValue(pattern)}).`
1963
+ );
1964
+ }
1965
+ function compileSecretRules(rules) {
1966
+ if (!Array.isArray(rules)) {
1967
+ throw new Error(
1968
+ `secrets.customPatterns must be an array of secret rules (received ${describeValue(rules)}).`
1969
+ );
1970
+ }
1971
+ let generated = 0;
1972
+ return rules.map((entry, index) => {
1973
+ const where = `secrets.customPatterns[${index}]`;
1974
+ if (entry instanceof RegExp) {
1975
+ generated += 1;
1976
+ return compilePattern(`custom-${generated}`, entry, void 0);
1977
+ }
1978
+ if (typeof entry === "string") {
1979
+ generated += 1;
1980
+ return compilePattern(`custom-${generated}`, entry, void 0);
1981
+ }
1982
+ if (typeof entry === "object" && entry !== null) {
1983
+ const candidate = entry;
1984
+ if (typeof candidate.rule !== "string" || candidate.rule.trim() === "") {
1985
+ throw new Error(
1986
+ `${where} is missing \`rule\` (received keys: ${Object.keys(candidate).join(", ") || "none"}). A named rule is { rule: "corp-token", pattern: /CORP-[A-Z0-9]{8}/ }, or pass the RegExp itself.`
1987
+ );
1988
+ }
1989
+ const valueGroup = candidate.valueGroup;
1990
+ if (valueGroup !== void 0) {
1991
+ if (typeof valueGroup !== "number" || !Number.isInteger(valueGroup) || valueGroup < 0) {
1992
+ throw new Error(
1993
+ `${where}.valueGroup must be a non-negative integer capture-group index (received ${describeValue(valueGroup)}).`
1994
+ );
1995
+ }
1996
+ }
1997
+ if (candidate.pattern === void 0) {
1998
+ throw new Error(
1999
+ `${where} (rule "${candidate.rule}") is missing \`pattern\`. Expected { rule, pattern: RegExp | string }.`
2000
+ );
2001
+ }
2002
+ return compilePattern(
2003
+ candidate.rule,
2004
+ candidate.pattern,
2005
+ valueGroup
2006
+ );
2007
+ }
2008
+ throw new Error(
2009
+ `${where} must be a RegExp, a string, or { rule, pattern } (received ${describeValue(entry)}).`
2010
+ );
2011
+ });
2012
+ }
2013
+ function toGlobal(rule) {
2014
+ const flags = new Set(rule.pattern.flags.replace(/[gy]/g, "").split(""));
2015
+ flags.add("g");
2016
+ if (rule.valueGroup !== void 0) flags.add("d");
2017
+ return new RegExp(rule.pattern.source, [...flags].join(""));
2018
+ }
2019
+ var LINE_BREAK = /(\r\n|\n|\r)/;
2020
+ function scanAndRedactSecrets(content, rules, filePath) {
2021
+ const findings = [];
2022
+ let redacted = content;
2023
+ for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
2024
+ blockPattern.lastIndex = 0;
2025
+ let match;
2026
+ while ((match = blockPattern.exec(redacted)) !== null) {
2027
+ if (match[0].length === 0) {
2028
+ blockPattern.lastIndex += 1;
2029
+ continue;
2030
+ }
2031
+ const line = redacted.slice(0, match.index).split("\n").length;
2032
+ const newlines = (match[0].match(/\n/g) ?? []).length;
2033
+ findings.push({ rule: "private-key-block", line, path: filePath });
2034
+ redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
2035
+ blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
2036
+ }
2037
+ }
2038
+ const compiled = rules.map((rule) => ({
2039
+ rule: rule.rule,
2040
+ pattern: toGlobal(rule),
2041
+ ...rule.valueGroup === void 0 ? {} : { valueGroup: rule.valueGroup }
2042
+ }));
2043
+ const segments = redacted.split(LINE_BREAK);
2044
+ let lineNumber = 1;
2045
+ for (let index = 0; index < segments.length; index += 2) {
2046
+ const line = segments[index];
2047
+ if (line.length > 0) {
2048
+ const spans = [];
2049
+ const matchedRules = /* @__PURE__ */ new Set();
2050
+ for (const { rule, pattern, valueGroup } of compiled) {
2051
+ pattern.lastIndex = 0;
2052
+ let match;
2053
+ while ((match = pattern.exec(line)) !== null) {
2054
+ if (match[0].length === 0) {
2055
+ pattern.lastIndex += 1;
2056
+ continue;
2057
+ }
2058
+ const group = valueGroup === void 0 ? void 0 : match.indices?.[valueGroup];
2059
+ spans.push(
2060
+ group ? { start: group[0], end: group[1], rule } : { start: match.index, end: match.index + match[0].length, rule }
2061
+ );
2062
+ matchedRules.add(rule);
2063
+ }
2064
+ }
2065
+ if (spans.length > 0) {
2066
+ for (const rule of matchedRules) {
2067
+ findings.push({ rule, line: lineNumber, path: filePath });
2068
+ }
2069
+ segments[index] = applySpans(line, spans);
2070
+ }
2071
+ }
2072
+ if (index + 1 < segments.length) lineNumber += 1;
2073
+ }
2074
+ return { redacted: segments.join(""), findings };
2075
+ }
2076
+ var TOKEN_CHARS_LEFT = /[A-Za-z0-9_+/-]/;
2077
+ var TOKEN_CHARS_RIGHT = /[A-Za-z0-9_+/=-]/;
2078
+ function expandOverToken(line, start, end) {
2079
+ let left = start;
2080
+ let right = end;
2081
+ while (left > 0 && TOKEN_CHARS_LEFT.test(line[left - 1])) left -= 1;
2082
+ while (right < line.length && TOKEN_CHARS_RIGHT.test(line[right])) right += 1;
2083
+ return { start: left, end: right };
2084
+ }
2085
+ function applySpans(line, spans) {
2086
+ const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
2087
+ const merged = [];
2088
+ for (const span of ordered) {
2089
+ const clampedStart = Math.max(0, Math.min(span.start, line.length));
2090
+ const clampedEnd = Math.max(clampedStart, Math.min(span.end, line.length));
2091
+ const { start, end } = expandOverToken(line, clampedStart, clampedEnd);
2092
+ const last = merged[merged.length - 1];
2093
+ if (last && start <= last.end) last.end = Math.max(last.end, end);
2094
+ else merged.push({ start, end });
2095
+ }
2096
+ let out = "";
2097
+ let cursor = 0;
2098
+ for (const span of merged) {
2099
+ out += line.slice(cursor, span.start);
2100
+ out += REDACTION_MARKER;
2101
+ cursor = span.end;
2102
+ }
2103
+ out += line.slice(cursor);
2104
+ return out;
2105
+ }
2106
+
2107
+ // src/scanner/files.ts
2108
+ var PRESET_IGNORES = new Set(
2109
+ PRESETS.flatMap((preset) => preset.exclude ?? [])
2110
+ );
2111
+ function readLinkTarget(linkPath, fallback) {
2112
+ try {
2113
+ return fs9.readlinkSync(linkPath);
2114
+ } catch {
2115
+ return fallback;
2116
+ }
2117
+ }
2118
+ function isInside(root, candidate) {
2119
+ const relative4 = path10.relative(root, candidate);
2120
+ return relative4 === "" || !relative4.startsWith(`..${path10.sep}`) && relative4 !== ".." && !path10.isAbsolute(relative4);
2121
+ }
1679
2122
  function getGitInfo(cwd, fallbackBranch = "main") {
1680
2123
  try {
1681
2124
  const reported = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
@@ -1720,20 +2163,79 @@ var DEFAULT_IGNORED_FILES = [
1720
2163
  "bun.lockb",
1721
2164
  "composer.lock"
1722
2165
  ];
1723
- var SENSITIVE_BASENAME = /^(?:\.env(?:[._-][A-Za-z0-9_-]*)*|\.env(?:backup|bak|old|orig|copy|save)|\.envrc|\.npmrc(?:\.[A-Za-z0-9_-]+)?|\.yarnrc(?:\.yml)?|\.netrc(?:\.[A-Za-z0-9_-]+)?|\.git-credentials|\.htpasswd|\.pgpass|id_[a-z0-9_]+(?:\.[A-Za-z0-9_-]+)?|credentials(?:\.json)?|service[-_]?account(?:\.[a-z0-9_-]+)*\.json|secrets?\.(?:json|ya?ml))$/i;
2166
+ var SENSITIVE_BASENAME = /^(?:\.env(?:[._-][A-Za-z0-9_-]*)*|\.env(?:backup|bak|old|orig|copy|save)|\.envrc|\.npmrc(?:\.[A-Za-z0-9_-]+)?|\.yarnrc(?:\.yml)?|\.netrc(?:\.[A-Za-z0-9_-]+)?|\.git-credentials|\.htpasswd|\.pgpass|\.vault-token|vault[-_]agent[-_]token|id_[a-z0-9_]+(?:\.[A-Za-z0-9_-]+)?|credentials(?:\.(?:json|ya?ml|toml))?|service[-_]?account(?:\.[a-z0-9_-]+)*\.json|secrets?\.(?:json|ya?ml|toml)|env|[A-Za-z0-9_-]{1,64}\.env)$/i;
1724
2167
  var SENSITIVE_PATH = /(?:^|\/)\.(?:aws|ssh|gnupg)\/|(?:^|\/)\.docker\/config\.json$|(?:^|\/)\.kube\/config$/i;
1725
2168
  var SENSITIVE_EXT = /\.(?:pem|key|p12|pfx|jks|keystore|ppk|asc|kdbx|tfstate|tfvars)$/i;
1726
2169
  var TEMPLATE_SUFFIX = /\.(?:example|sample|template|dist)(?=$|[.])/i;
2170
+ var BACKUP_SUFFIX = /(?:~|\.(?:bak|backup|old|orig|save|copy|swp|swo|tmp|part))$/i;
2171
+ function normaliseName(name) {
2172
+ let normalised = name.trim().replace(/[. ]+$/, "");
2173
+ let previous = "";
2174
+ while (previous !== normalised) {
2175
+ previous = normalised;
2176
+ normalised = normalised.replace(BACKUP_SUFFIX, "");
2177
+ }
2178
+ return normalised;
2179
+ }
1727
2180
  var matchesSensitiveList = (posixPath, base) => SENSITIVE_BASENAME.test(base) || SENSITIVE_PATH.test(posixPath) || SENSITIVE_EXT.test(base);
1728
2181
  function isSensitivePath(relativePath) {
1729
2182
  const posixPath = relativePath.replace(/\\/g, "/");
1730
- const base = posixPath.split("/").pop() ?? posixPath;
2183
+ const raw = posixPath.split("/").pop() ?? posixPath;
2184
+ const base = normaliseName(raw);
1731
2185
  if (TEMPLATE_SUFFIX.test(base)) {
1732
2186
  const stripped = base.replace(new RegExp(TEMPLATE_SUFFIX.source, "gi"), "");
1733
2187
  if (matchesSensitiveList(posixPath, stripped)) return false;
1734
2188
  }
1735
2189
  return matchesSensitiveList(posixPath, base);
1736
2190
  }
2191
+ var activeSecretRules = null;
2192
+ function setActiveSecretRules(rules) {
2193
+ activeSecretRules = rules === null ? null : [...rules];
2194
+ }
2195
+ function pathScanRules(extraRules) {
2196
+ const fallback = extraRules ?? activeSecretRules ?? [];
2197
+ return [...DEFAULT_SECRET_PATTERNS, ...fallback];
2198
+ }
2199
+ function findSecretInPath(relativePath, extraRules) {
2200
+ const posixPath = relativePath.replace(/\\/g, "/");
2201
+ const spans = [];
2202
+ const rules = [];
2203
+ for (const { rule, pattern } of pathScanRules(extraRules)) {
2204
+ const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
2205
+ let match;
2206
+ while ((match = scan.exec(posixPath)) !== null) {
2207
+ if (match[0].length === 0) {
2208
+ scan.lastIndex += 1;
2209
+ continue;
2210
+ }
2211
+ spans.push({ start: match.index, end: match.index + match[0].length });
2212
+ if (!rules.includes(rule)) rules.push(rule);
2213
+ }
2214
+ }
2215
+ if (spans.length === 0) return null;
2216
+ spans.sort((a, b) => a.start - b.start || a.end - b.end);
2217
+ const merged = [];
2218
+ for (const span of spans) {
2219
+ const last = merged[merged.length - 1];
2220
+ if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
2221
+ else merged.push({ start: span.start, end: span.end });
2222
+ }
2223
+ let out = "";
2224
+ let cursor = 0;
2225
+ for (const span of merged) {
2226
+ out += posixPath.slice(cursor, span.start);
2227
+ out += REDACTION_MARKER;
2228
+ cursor = span.end;
2229
+ }
2230
+ out += posixPath.slice(cursor);
2231
+ return { rules, masked: out };
2232
+ }
2233
+ function maskSkipPaths(records, extraRules) {
2234
+ return records.map((record) => {
2235
+ const hit = findSecretInPath(record.path, extraRules);
2236
+ return hit ? { ...record, path: hit.masked } : record;
2237
+ });
2238
+ }
1737
2239
  function globToRegExp(glob) {
1738
2240
  const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1739
2241
  const anchored = normalized.startsWith("/");
@@ -1854,49 +2356,178 @@ function defaultIgnoreReason(relativePath) {
1854
2356
  }
1855
2357
  return null;
1856
2358
  }
1857
- function collectFiles(cwd, options = {}) {
1858
- const includePatterns = options.includePatterns ?? [];
1859
- const excludePatterns = options.excludePatterns ?? [];
1860
- const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
1861
- let candidates;
1862
- let walkSkips = [];
2359
+ function listIgnoredPaths(cwd) {
1863
2360
  try {
1864
2361
  const stdout = childProcess.execSync(
1865
- "git ls-files -z --cached --others --exclude-standard",
2362
+ "git ls-files -z --others --ignored --exclude-standard --directory",
1866
2363
  {
1867
2364
  cwd,
1868
2365
  stdio: ["ignore", "pipe", "ignore"],
1869
2366
  encoding: "utf8",
1870
- maxBuffer: 50 * 1024 * 1024
2367
+ maxBuffer: 32 * 1024 * 1024
1871
2368
  }
1872
2369
  );
1873
- candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
2370
+ return stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
1874
2371
  } catch {
1875
- const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set());
1876
- candidates = walked.files;
1877
- walkSkips = walked.skipped;
2372
+ return [];
1878
2373
  }
1879
- const skipped = [...walkSkips];
1880
- const files = candidates.filter((file) => {
1881
- const ignored = defaultIgnoreReason(file);
1882
- if (ignored) {
1883
- skipped.push({ path: file, reason: "default-ignore", detail: ignored });
1884
- return false;
1885
- }
1886
- if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesExclude(file, pattern))) {
1887
- skipped.push({
1888
- path: file,
1889
- reason: "excluded",
2374
+ }
2375
+ var TOOL_OWNED_DIR_NAMES = /* @__PURE__ */ new Set(["snapshot", "snapshot.tmp"]);
2376
+ function isToolOwnedDirName(name) {
2377
+ return TOOL_OWNED_DIR_NAMES.has(name) || name.startsWith("snapshot.backup-");
2378
+ }
2379
+ function hasSnapshotMarker(target) {
2380
+ try {
2381
+ return fs9.statSync(target).isDirectory() && fs9.existsSync(path10.join(target, ".codebase-snapshot"));
2382
+ } catch {
2383
+ return false;
2384
+ }
2385
+ }
2386
+ function makeToolOwnedDecider(cwd) {
2387
+ const markers = /* @__PURE__ */ new Map();
2388
+ return (relativePath) => {
2389
+ const parts = relativePath.replace(/\\/g, "/").split("/");
2390
+ for (let depth = 1; depth < parts.length; depth += 1) {
2391
+ const prefix = parts.slice(0, depth).join("/");
2392
+ if (depth === 1 && isToolOwnedDirName(prefix)) return true;
2393
+ let hasMarker = markers.get(prefix);
2394
+ if (hasMarker === void 0) {
2395
+ hasMarker = fs9.existsSync(path10.join(cwd, prefix, ".codebase-snapshot"));
2396
+ markers.set(prefix, hasMarker);
2397
+ }
2398
+ if (hasMarker) return true;
2399
+ }
2400
+ return false;
2401
+ };
2402
+ }
2403
+ function makeLinkProbe() {
2404
+ const cache = /* @__PURE__ */ new Map();
2405
+ return (absolutePath) => {
2406
+ let known = cache.get(absolutePath);
2407
+ if (known === void 0) {
2408
+ try {
2409
+ known = fs9.lstatSync(absolutePath).isSymbolicLink();
2410
+ } catch {
2411
+ known = false;
2412
+ }
2413
+ cache.set(absolutePath, known);
2414
+ }
2415
+ return known;
2416
+ };
2417
+ }
2418
+ function dropLinkedDirectories(cwd, candidates, skipped) {
2419
+ const isLink = makeLinkProbe();
2420
+ let rootReal = cwd;
2421
+ try {
2422
+ rootReal = fs9.realpathSync(cwd);
2423
+ } catch {
2424
+ }
2425
+ const reported = /* @__PURE__ */ new Set();
2426
+ const kept = [];
2427
+ for (const candidate of candidates) {
2428
+ const parts = candidate.split("/");
2429
+ let linkedDir = null;
2430
+ for (let depth = 1; depth <= parts.length; depth += 1) {
2431
+ const prefix = parts.slice(0, depth).join("/");
2432
+ if (isLink(path10.join(cwd, prefix))) {
2433
+ linkedDir = prefix;
2434
+ break;
2435
+ }
2436
+ }
2437
+ if (linkedDir === null) {
2438
+ kept.push(candidate);
2439
+ continue;
2440
+ }
2441
+ if (reported.has(linkedDir)) continue;
2442
+ reported.add(linkedDir);
2443
+ const absolute = path10.join(cwd, linkedDir);
2444
+ let targetReal = absolute;
2445
+ try {
2446
+ targetReal = fs9.realpathSync(absolute);
2447
+ } catch {
2448
+ }
2449
+ const target = readLinkTarget(absolute, targetReal);
2450
+ const what = linkedDir === candidate ? "symbolic link" : "directory link";
2451
+ skipped.push(
2452
+ isInside(rootReal, targetReal) ? {
2453
+ path: linkedDir,
2454
+ reason: "symlink",
2455
+ detail: `${what} not followed (target: ${target})`
2456
+ } : {
2457
+ path: linkedDir,
2458
+ reason: "outside-root",
2459
+ detail: `link target resolves outside the scanned root (target: ${target}); not followed`
2460
+ }
2461
+ );
2462
+ }
2463
+ return kept;
2464
+ }
2465
+ function collectFiles(cwd, options = {}) {
2466
+ const includePatterns = options.includePatterns ?? [];
2467
+ const excludePatterns = options.excludePatterns ?? [];
2468
+ const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
2469
+ const pathRules = options.secretRules;
2470
+ const isToolOwned = makeToolOwnedDecider(cwd);
2471
+ let candidates;
2472
+ const skipped = [];
2473
+ try {
2474
+ const stdout = childProcess.execSync(
2475
+ "git ls-files -z --cached --others --exclude-standard",
2476
+ {
2477
+ cwd,
2478
+ stdio: ["ignore", "pipe", "ignore"],
2479
+ encoding: "utf8",
2480
+ maxBuffer: 50 * 1024 * 1024
2481
+ }
2482
+ );
2483
+ candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
2484
+ candidates = dropLinkedDirectories(cwd, candidates, skipped);
2485
+ const known = new Set(candidates);
2486
+ for (const ignoredPath of listIgnoredPaths(cwd)) {
2487
+ const trimmed = ignoredPath.replace(/\/+$/, "");
2488
+ if (known.has(trimmed) || known.has(ignoredPath)) continue;
2489
+ if (isToolOwned(trimmed)) continue;
2490
+ if (isSensitivePath(trimmed)) {
2491
+ skipped.push({
2492
+ path: trimmed,
2493
+ reason: "sensitive-file",
2494
+ detail: "credential-bearing file, ignored by .gitignore; enable includeSensitiveFiles to emit it"
2495
+ });
2496
+ } else {
2497
+ skipped.push({
2498
+ path: trimmed,
2499
+ reason: "gitignore",
2500
+ detail: ignoredPath.endsWith("/") ? "ignored by .gitignore (directory, not descended)" : "ignored by .gitignore"
2501
+ });
2502
+ }
2503
+ }
2504
+ } catch {
2505
+ const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), true);
2506
+ candidates = walked.files;
2507
+ skipped.push(...walked.skipped);
2508
+ }
2509
+ const files = candidates.filter((file) => {
2510
+ if (isToolOwned(file)) return false;
2511
+ const ignored = defaultIgnoreReason(file);
2512
+ if (ignored) {
2513
+ skipped.push({ path: file, reason: "default-ignore", detail: ignored });
2514
+ return false;
2515
+ }
2516
+ if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesExclude(file, pattern))) {
2517
+ skipped.push({
2518
+ path: file,
2519
+ reason: "excluded",
1890
2520
  detail: `does not match any include pattern (${includePatterns.join(", ")})`
1891
2521
  });
1892
2522
  return false;
1893
2523
  }
1894
2524
  for (const pattern of excludePatterns) {
1895
2525
  if (matchesExclude(file, pattern)) {
2526
+ const fromPreset = PRESET_IGNORES.has(pattern);
1896
2527
  skipped.push({
1897
2528
  path: file,
1898
- reason: "excluded",
1899
- detail: `matches exclude pattern "${pattern}"`
2529
+ reason: fromPreset ? "default-ignore" : "excluded",
2530
+ detail: fromPreset ? `preset ignore: "${pattern}"` : `matches exclude pattern "${pattern}"`
1900
2531
  });
1901
2532
  return false;
1902
2533
  }
@@ -1909,56 +2540,119 @@ function collectFiles(cwd, options = {}) {
1909
2540
  });
1910
2541
  return false;
1911
2542
  }
2543
+ const secret = findSecretInPath(file, pathRules);
2544
+ if (secret) {
2545
+ skipped.push({
2546
+ path: secret.masked,
2547
+ reason: "sensitive-path",
2548
+ 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`
2549
+ });
2550
+ return false;
2551
+ }
1912
2552
  return true;
1913
2553
  }).sort();
1914
- return { files, skipped };
2554
+ return { files, skipped: maskSkipPaths(skipped, pathRules) };
1915
2555
  }
1916
- function walkDirectory(dir, baseDir, visited) {
1917
- const results = { files: [], skipped: [] };
1918
- let realDir;
2556
+ function rootRealFor(root) {
1919
2557
  try {
1920
- realDir = fs9.realpathSync(dir);
2558
+ return fs9.realpathSync(root);
1921
2559
  } catch {
1922
- realDir = dir;
2560
+ return root;
1923
2561
  }
1924
- if (visited.has(realDir)) return results;
1925
- visited.add(realDir);
1926
- const ignoredByGitignore = dir === baseDir ? readGitignoreMatcher(baseDir) : () => false;
2562
+ }
2563
+ function walkDirectory(dir, baseDir, rootReal, visited, isRoot = false) {
2564
+ const results = { files: [], skipped: [] };
2565
+ if (visited.has(dir)) return results;
2566
+ visited.add(dir);
2567
+ const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
1927
2568
  let entries;
1928
2569
  try {
1929
2570
  entries = fs9.readdirSync(dir, { withFileTypes: true });
1930
2571
  } catch {
1931
- results.skipped.push({ path: dir, reason: "read-error", detail: "directory unreadable" });
2572
+ results.skipped.push({
2573
+ path: path10.relative(baseDir, dir).replace(/\\/g, "/") || ".",
2574
+ reason: "read-error",
2575
+ detail: "directory unreadable"
2576
+ });
1932
2577
  return results;
1933
2578
  }
1934
2579
  for (const entry of entries) {
1935
- if (DEFAULT_IGNORED_DIRS.includes(entry.name)) continue;
1936
2580
  const fullPath = path10.join(dir, entry.name);
1937
2581
  const relPath = path10.relative(baseDir, fullPath).replace(/\\/g, "/");
1938
- if (ignoredByGitignore(relPath)) continue;
2582
+ if (isToolOwnedDirName(entry.name)) continue;
2583
+ if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
2584
+ results.skipped.push({
2585
+ path: relPath,
2586
+ reason: "default-ignore",
2587
+ detail: `built-in ignore: directory "${entry.name}" (not descended)`
2588
+ });
2589
+ continue;
2590
+ }
2591
+ if (DEFAULT_IGNORED_FILES.includes(entry.name) || entry.name.startsWith("snapshot.backup-")) {
2592
+ results.skipped.push({
2593
+ path: relPath,
2594
+ reason: "default-ignore",
2595
+ detail: "built-in ignore: filename"
2596
+ });
2597
+ continue;
2598
+ }
2599
+ if (ignoredByGitignore(relPath)) {
2600
+ if (!hasSnapshotMarker(fullPath)) {
2601
+ results.skipped.push({
2602
+ path: relPath,
2603
+ reason: isSensitivePath(relPath) ? "sensitive-file" : "gitignore",
2604
+ detail: "ignored by .gitignore"
2605
+ });
2606
+ }
2607
+ continue;
2608
+ }
1939
2609
  let isDirectory = entry.isDirectory();
1940
2610
  let isFile = entry.isFile();
1941
2611
  if (entry.isSymbolicLink()) {
2612
+ let targetReal;
1942
2613
  try {
1943
- const stats = fs9.statSync(fullPath);
1944
- isDirectory = stats.isDirectory();
1945
- isFile = stats.isFile();
1946
- if (!isDirectory && !isFile) {
1947
- results.skipped.push({ path: relPath, reason: "symlink", detail: "not a file or directory" });
1948
- continue;
1949
- }
2614
+ targetReal = fs9.realpathSync(fullPath);
2615
+ } catch {
2616
+ results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
2617
+ continue;
2618
+ }
2619
+ let stats;
2620
+ try {
2621
+ stats = fs9.statSync(fullPath);
2622
+ } catch {
2623
+ results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
2624
+ continue;
2625
+ }
2626
+ const linkTarget = readLinkTarget(fullPath, targetReal);
2627
+ if (!isInside(rootReal, targetReal)) {
2628
+ results.skipped.push({
2629
+ path: relPath,
2630
+ reason: "outside-root",
2631
+ detail: `link target resolves outside the scanned root (target: ${linkTarget}); not followed`
2632
+ });
2633
+ continue;
2634
+ }
2635
+ if (stats.isDirectory()) {
1950
2636
  results.skipped.push({
1951
2637
  path: relPath,
1952
2638
  reason: "symlink",
1953
- detail: isDirectory ? "followed directory symlink" : "followed file symlink"
2639
+ detail: `directory link not followed (target: ${linkTarget})`
2640
+ });
2641
+ continue;
2642
+ }
2643
+ if (!stats.isFile()) {
2644
+ results.skipped.push({
2645
+ path: relPath,
2646
+ reason: "symlink",
2647
+ detail: `link to neither a file nor a directory (target: ${linkTarget})`
1954
2648
  });
1955
- } catch {
1956
- results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
1957
2649
  continue;
1958
2650
  }
2651
+ isDirectory = false;
2652
+ isFile = true;
1959
2653
  }
1960
2654
  if (isDirectory) {
1961
- const nested = walkDirectory(fullPath, baseDir, visited);
2655
+ const nested = walkDirectory(fullPath, baseDir, rootReal, visited);
1962
2656
  results.files.push(...nested.files);
1963
2657
  results.skipped.push(...nested.skipped);
1964
2658
  } else if (isFile) {
@@ -1968,195 +2662,6 @@ function walkDirectory(dir, baseDir, visited) {
1968
2662
  return results;
1969
2663
  }
1970
2664
 
1971
- // src/security/patterns.ts
1972
- var DEFAULT_SECRET_PATTERNS = [
1973
- {
1974
- rule: "private-key-header",
1975
- pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
1976
- },
1977
- {
1978
- rule: "jwt-token",
1979
- pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
1980
- },
1981
- {
1982
- rule: "aws-access-key-id",
1983
- pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
1984
- },
1985
- {
1986
- rule: "aws-secret-access-key",
1987
- pattern: /(?:aws_secret_access_key|aws_secret_key|secret_access_key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']?/i
1988
- },
1989
- {
1990
- rule: "github-token",
1991
- pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
1992
- },
1993
- {
1994
- rule: "slack-token",
1995
- pattern: /xox[baprs]-[0-9A-Za-z-]{10,250}/
1996
- },
1997
- {
1998
- rule: "slack-webhook-url",
1999
- pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
2000
- },
2001
- {
2002
- rule: "stripe-secret-key",
2003
- pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
2004
- },
2005
- {
2006
- rule: "google-api-key",
2007
- pattern: /AIza[0-9A-Za-z_-]{35}/
2008
- },
2009
- {
2010
- rule: "google-oauth-client-secret",
2011
- pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
2012
- },
2013
- {
2014
- rule: "openai-anthropic-key",
2015
- pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
2016
- },
2017
- {
2018
- rule: "npm-token",
2019
- pattern: /npm_[A-Za-z0-9]{36}/
2020
- },
2021
- {
2022
- rule: "pypi-token",
2023
- pattern: /pypi-[A-Za-z0-9_-]{50,180}/
2024
- },
2025
- {
2026
- rule: "sendgrid-key",
2027
- pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
2028
- },
2029
- {
2030
- rule: "twilio-api-key",
2031
- pattern: /SK[0-9a-fA-F]{32}/
2032
- },
2033
- {
2034
- rule: "digitalocean-token",
2035
- pattern: /dop_v1_[0-9a-f]{64}/
2036
- },
2037
- {
2038
- rule: "azure-storage-account-key",
2039
- pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
2040
- },
2041
- {
2042
- rule: "database-uri-credentials",
2043
- pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/
2044
- },
2045
- {
2046
- rule: "basic-auth-url",
2047
- pattern: /(?:https?|ftp|sftp|ssh|git):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/i
2048
- },
2049
- {
2050
- rule: "generic-secret-assignment",
2051
- pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["'][^"'\s]{8,256}["']/i
2052
- },
2053
- {
2054
- // Covers `.env` / shell assignment syntax, which never quotes its values and
2055
- // was therefore invisible to the rule above. Anchored to the line start and
2056
- // limited to secret-looking names to stay at zero false positives.
2057
- //
2058
- // The name is matched with BOUNDED quantifiers on purpose: an unbounded
2059
- // `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
2060
- // every offset of a long identifier, which made this rule quadratic
2061
- // (measured 5.6 s for a single 240 KB line before bounding).
2062
- rule: "env-secret-assignment",
2063
- pattern: /^\s{0,64}(?:export\s{1,8})?[A-Za-z0-9_]{0,96}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIAL)[A-Za-z0-9_]{0,96}\s{0,8}=\s{0,8}(?![\s$<{[])[^\s#]{6,512}$/i
2064
- }
2065
- ];
2066
- var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
2067
- var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
2068
-
2069
- // src/security/redact.ts
2070
- var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
2071
- var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
2072
- function compileSecretRules(rules) {
2073
- return rules.map((rule) => {
2074
- if (rule.pattern instanceof RegExp) {
2075
- return {
2076
- rule: rule.rule,
2077
- pattern: new RegExp(rule.pattern.source, rule.pattern.flags.replace(/[gy]/g, ""))
2078
- };
2079
- }
2080
- if (typeof rule.pattern === "string") {
2081
- try {
2082
- return { rule: rule.rule, pattern: new RegExp(rule.pattern, "i") };
2083
- } catch (err) {
2084
- throw new Error(
2085
- `Invalid regular expression for secret rule "${rule.rule}": ${err.message}`
2086
- );
2087
- }
2088
- }
2089
- throw new Error(
2090
- `Secret rule "${String(rule.rule)}" has a pattern that is neither a RegExp nor a string.`
2091
- );
2092
- });
2093
- }
2094
- function toGlobal(pattern) {
2095
- const flags = pattern.flags.replace(/[gy]/g, "");
2096
- return new RegExp(pattern.source, `${flags}g`);
2097
- }
2098
- function scanAndRedactSecrets(content, rules, filePath) {
2099
- const findings = [];
2100
- let redacted = content;
2101
- for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
2102
- blockPattern.lastIndex = 0;
2103
- let match;
2104
- while ((match = blockPattern.exec(redacted)) !== null) {
2105
- if (match[0].length === 0) {
2106
- blockPattern.lastIndex += 1;
2107
- continue;
2108
- }
2109
- const line = redacted.slice(0, match.index).split("\n").length;
2110
- const newlines = (match[0].match(/\n/g) ?? []).length;
2111
- findings.push({ rule: "private-key-block", line, path: filePath });
2112
- redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
2113
- blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
2114
- }
2115
- }
2116
- const compiled = rules.map((rule) => ({
2117
- rule: rule.rule,
2118
- pattern: toGlobal(rule.pattern)
2119
- }));
2120
- const lines = redacted.split("\n");
2121
- const redactedLines = lines.map((line, index) => {
2122
- if (line.length === 0) return line;
2123
- const spans = [];
2124
- const matchedRules = /* @__PURE__ */ new Set();
2125
- for (const { rule, pattern } of compiled) {
2126
- pattern.lastIndex = 0;
2127
- let match;
2128
- while ((match = pattern.exec(line)) !== null) {
2129
- if (match[0].length === 0) {
2130
- pattern.lastIndex += 1;
2131
- continue;
2132
- }
2133
- spans.push({ start: match.index, end: match.index + match[0].length, rule });
2134
- matchedRules.add(rule);
2135
- }
2136
- }
2137
- if (spans.length === 0) return line;
2138
- for (const rule of matchedRules) {
2139
- findings.push({ rule, line: index + 1, path: filePath });
2140
- }
2141
- return applySpans(line, spans);
2142
- });
2143
- return { redacted: redactedLines.join("\n"), findings };
2144
- }
2145
- function applySpans(line, spans) {
2146
- const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
2147
- let out = "";
2148
- let cursor = 0;
2149
- for (const span of ordered) {
2150
- if (span.end <= cursor) continue;
2151
- const start = Math.max(span.start, cursor);
2152
- out += line.slice(cursor, start);
2153
- out += REDACTION_MARKER;
2154
- cursor = Math.min(span.end, line.length);
2155
- }
2156
- out += line.slice(cursor);
2157
- return out;
2158
- }
2159
-
2160
2665
  // src/config.ts
2161
2666
  var CONFIG_FILENAMES = [
2162
2667
  "snapshot.config.ts",
@@ -2206,7 +2711,12 @@ function normalizeResolvedConfig(config) {
2206
2711
  ),
2207
2712
  include: resolveInclude(config.include),
2208
2713
  preset: typeof config.preset === "string" ? config.preset : "generic",
2209
- presetExclude: Array.isArray(config.presetExclude) ? config.presetExclude : [],
2714
+ presetExclude: [
2715
+ .../* @__PURE__ */ new Set([
2716
+ ...Array.isArray(config.presetExclude) ? config.presetExclude : [],
2717
+ ...outputExcludePatterns(config.cwd, config.outDir, config.tmpDir)
2718
+ ])
2719
+ ],
2210
2720
  layersMode: config.layersMode === "replace" ? "replace" : "merge",
2211
2721
  budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
2212
2722
  hooks: resolveHooks(config.hooks),
@@ -2243,7 +2753,68 @@ function supportsTypeScriptImports() {
2243
2753
  return major > 22 || major === 22 && minor >= 6;
2244
2754
  }
2245
2755
  function stripJsonComments2(raw) {
2246
- return raw.replace(/^\uFEFF/, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'\\])\/\/.*$/gm, "$1");
2756
+ const text2 = raw.replace(/^\uFEFF/, "");
2757
+ let out = "";
2758
+ let inString = false;
2759
+ let escaped = false;
2760
+ for (let index = 0; index < text2.length; index += 1) {
2761
+ const char = text2[index];
2762
+ if (inString) {
2763
+ out += char;
2764
+ if (escaped) escaped = false;
2765
+ else if (char === "\\") escaped = true;
2766
+ else if (char === '"') inString = false;
2767
+ continue;
2768
+ }
2769
+ if (char === '"') {
2770
+ inString = true;
2771
+ out += char;
2772
+ continue;
2773
+ }
2774
+ if (char === "/" && text2[index + 1] === "/") {
2775
+ while (index < text2.length && text2[index] !== "\n" && text2[index] !== "\r") index += 1;
2776
+ index -= 1;
2777
+ continue;
2778
+ }
2779
+ if (char === "/" && text2[index + 1] === "*") {
2780
+ index += 2;
2781
+ while (index < text2.length && !(text2[index] === "*" && text2[index + 1] === "/")) {
2782
+ if (text2[index] === "\n" || text2[index] === "\r") out += text2[index];
2783
+ index += 1;
2784
+ }
2785
+ index += 1;
2786
+ continue;
2787
+ }
2788
+ out += char;
2789
+ }
2790
+ return stripTrailingCommas(out);
2791
+ }
2792
+ function stripTrailingCommas(text2) {
2793
+ let out = "";
2794
+ let inString = false;
2795
+ let escaped = false;
2796
+ for (let index = 0; index < text2.length; index += 1) {
2797
+ const char = text2[index];
2798
+ if (inString) {
2799
+ out += char;
2800
+ if (escaped) escaped = false;
2801
+ else if (char === "\\") escaped = true;
2802
+ else if (char === '"') inString = false;
2803
+ continue;
2804
+ }
2805
+ if (char === '"') {
2806
+ inString = true;
2807
+ out += char;
2808
+ continue;
2809
+ }
2810
+ if (char === ",") {
2811
+ let next = index + 1;
2812
+ while (next < text2.length && /\s/.test(text2[next])) next += 1;
2813
+ if (text2[next] === "}" || text2[next] === "]") continue;
2814
+ }
2815
+ out += char;
2816
+ }
2817
+ return out;
2247
2818
  }
2248
2819
  function parseJsonish(raw, file) {
2249
2820
  try {
@@ -2252,6 +2823,47 @@ function parseJsonish(raw, file) {
2252
2823
  throw new Error(`Could not parse ${file}: ${err.message}`);
2253
2824
  }
2254
2825
  }
2826
+ function describeValue2(value) {
2827
+ if (value === null) return "null";
2828
+ if (Array.isArray(value)) return "an array";
2829
+ if (value instanceof RegExp) return value.toString();
2830
+ if (typeof value === "function") return "a function";
2831
+ if (typeof value === "string") return JSON.stringify(value);
2832
+ if (typeof value === "object") {
2833
+ const keys = Object.keys(value);
2834
+ return keys.length > 0 ? `an object with keys ${keys.join(", ")}` : "an empty object";
2835
+ }
2836
+ return `${typeof value} (${String(value)})`;
2837
+ }
2838
+ var isThenable = (value) => typeof value.then === "function";
2839
+ function readConfigExport(exported, file) {
2840
+ if (exported === void 0 || exported === null) {
2841
+ throw new Error(
2842
+ `${file} does not export a configuration object (received ${describeValue2(exported)}).`
2843
+ );
2844
+ }
2845
+ if (Array.isArray(exported)) {
2846
+ throw new Error(
2847
+ `${file} exports an array; expected a single configuration object such as \`export default { outDir: "snapshot" }\`.`
2848
+ );
2849
+ }
2850
+ if (typeof exported === "function") {
2851
+ throw new Error(
2852
+ `${file} exports a function; expected a configuration object. Call the function yourself and export its result.`
2853
+ );
2854
+ }
2855
+ if (typeof exported !== "object") {
2856
+ throw new Error(
2857
+ `${file} does not export a configuration object (received ${describeValue2(exported)}).`
2858
+ );
2859
+ }
2860
+ if (isThenable(exported)) {
2861
+ throw new Error(
2862
+ `${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;\`).`
2863
+ );
2864
+ }
2865
+ return exported;
2866
+ }
2255
2867
  async function loadConfigFileDetailed(cwd) {
2256
2868
  const warnings = [];
2257
2869
  const errors = [];
@@ -2281,13 +2893,7 @@ async function loadConfigFileDetailed(cwd) {
2281
2893
  }
2282
2894
  try {
2283
2895
  const imported = await import(pathToFileURL(fullPath).href);
2284
- const config = imported.default ?? imported;
2285
- if (!config || typeof config !== "object") {
2286
- const message = `${fullPath} does not export a configuration object.`;
2287
- warnings.push(message);
2288
- errors.push(message);
2289
- continue;
2290
- }
2896
+ const config = readConfigExport(imported.default ?? imported, fullPath);
2291
2897
  return { config, file: fullPath, warnings, errors };
2292
2898
  } catch (err) {
2293
2899
  const message = `Could not load ${fullPath}: ${err.message}`;
@@ -2341,7 +2947,7 @@ var DEFAULT_ARTIFACTS = {
2341
2947
  function isArtifactName(value) {
2342
2948
  return ARTIFACT_NAMES.includes(value);
2343
2949
  }
2344
- function resolveArtifacts(requested, skipped, includeMonolith) {
2950
+ function resolveArtifacts(requested, skipped, includeMonolith, monolithOverride) {
2345
2951
  const artifacts = {
2346
2952
  ...DEFAULT_ARTIFACTS,
2347
2953
  // `includeMonolith` predates this selection and stays the switch for it.
@@ -2363,6 +2969,9 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
2363
2969
  artifacts[name] = value;
2364
2970
  }
2365
2971
  }
2972
+ if (monolithOverride !== void 0) {
2973
+ artifacts.monolith = monolithOverride;
2974
+ }
2366
2975
  for (const name of skipped ?? []) {
2367
2976
  if (!isArtifactName(name)) {
2368
2977
  throw new Error(
@@ -2373,9 +2982,106 @@ function resolveArtifacts(requested, skipped, includeMonolith) {
2373
2982
  }
2374
2983
  return artifacts;
2375
2984
  }
2376
- function positiveInteger(value, fallback) {
2377
- if (!Number.isFinite(value) || value <= 0) return fallback;
2378
- return Math.floor(value);
2985
+ function strictPositiveInteger(value, fallback, label) {
2986
+ if (value === void 0) return fallback;
2987
+ if (typeof value !== "number" || !Number.isInteger(value) || value <= 0) {
2988
+ throw new Error(`${label} must be a positive integer (received ${describeValue2(value)}).`);
2989
+ }
2990
+ return value;
2991
+ }
2992
+ function strictBoolean(value, fallback, label) {
2993
+ if (value === void 0) return fallback;
2994
+ if (typeof value !== "boolean") {
2995
+ throw new Error(`${label} must be true or false (received ${describeValue2(value)}).`);
2996
+ }
2997
+ return value;
2998
+ }
2999
+ function strictString(value, fallback, label) {
3000
+ if (value === void 0) return fallback;
3001
+ if (typeof value !== "string" || value.trim() === "") {
3002
+ throw new Error(`${label} must be a non-empty string (received ${describeValue2(value)}).`);
3003
+ }
3004
+ return value;
3005
+ }
3006
+ function resolveExcludePatterns(value) {
3007
+ if (value === void 0) return [];
3008
+ if (!Array.isArray(value) || value.some((pattern) => typeof pattern !== "string")) {
3009
+ throw new Error(
3010
+ `\`excludePatterns\` must be an array of glob strings such as ["**/*.test.ts"] (received ${describeValue2(value)}).`
3011
+ );
3012
+ }
3013
+ return value.map((pattern) => pattern.trim()).filter(Boolean);
3014
+ }
3015
+ var AUTO_PRESET_ALIASES = /* @__PURE__ */ new Set(["auto", "detected"]);
3016
+ function resolvePresetName(value) {
3017
+ if (value === void 0) return void 0;
3018
+ if (typeof value !== "string" || value.trim() === "") {
3019
+ throw new Error(
3020
+ `\`preset\` must be a preset name (received ${describeValue2(value)}). Use "detected" (or "auto") to detect one, or one of: ${listPresetNames().join(", ")}.`
3021
+ );
3022
+ }
3023
+ const name = value.trim();
3024
+ return AUTO_PRESET_ALIASES.has(name.toLowerCase()) ? void 0 : name;
3025
+ }
3026
+ function resolveRuleList(value, kind) {
3027
+ const collection = kind === "layer" ? "layers" : "kinds";
3028
+ const example = kind === "layer" ? `{ name: "api", match: "src/api/**" }` : `{ kind: "route", match: "src/routes/**" }`;
3029
+ if (value === void 0) return [];
3030
+ if (!Array.isArray(value)) {
3031
+ throw new Error(
3032
+ `\`${collection}\` must be an array of ${kind} rules (received ${describeValue2(value)}). A ${kind} rule is ${example}.`
3033
+ );
3034
+ }
3035
+ return value.map((entry, index) => {
3036
+ const where = `${collection}[${index}]`;
3037
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
3038
+ throw new Error(`${where} must be an object (received ${describeValue2(entry)}). A ${kind} rule is ${example}.`);
3039
+ }
3040
+ const record = entry;
3041
+ const keys = Object.keys(record);
3042
+ const canonical = kind === "layer" ? "name" : "kind";
3043
+ const alias = kind === "layer" ? "layer" : "name";
3044
+ const rawName = typeof record[canonical] === "string" ? record[canonical] : typeof record[alias] === "string" ? record[alias] : void 0;
3045
+ if (rawName === void 0 || rawName.trim() === "") {
3046
+ throw new Error(
3047
+ `${where} is missing \`${canonical}\` (received keys: ${keys.join(", ") || "none"}). A ${kind} rule is ${example}; \`${alias}\` is accepted as an alias of \`${canonical}\`.`
3048
+ );
3049
+ }
3050
+ const match = record.match;
3051
+ if (match === void 0) {
3052
+ throw new Error(
3053
+ `${where}.match is required (received keys: ${keys.join(", ") || "none"}). Use a glob string ("src/api/**"), a RegExp (/\\\\.tsx$/) or a function.`
3054
+ );
3055
+ }
3056
+ if (!(typeof match === "string" || match instanceof RegExp || typeof match === "function")) {
3057
+ throw new Error(`${where}.match must be a string, a RegExp or a function (received ${describeValue2(match)}).`);
3058
+ }
3059
+ if (typeof match === "string" && match.trim() === "") {
3060
+ throw new Error(`${where}.match must not be an empty string.`);
3061
+ }
3062
+ return {
3063
+ name: rawName.trim(),
3064
+ match
3065
+ };
3066
+ });
3067
+ }
3068
+ function resolveLayerRules(value) {
3069
+ return resolveRuleList(value, "layer").map((rule) => ({ name: rule.name, match: rule.match }));
3070
+ }
3071
+ function resolveKindRules(value) {
3072
+ return resolveRuleList(value, "kind").map((rule) => ({ kind: rule.name, match: rule.match }));
3073
+ }
3074
+ function outputExcludePatterns(cwd, outDir, tmpDir) {
3075
+ const patterns = [];
3076
+ for (const dir of [outDir, tmpDir]) {
3077
+ const absolute = path10.resolve(cwd, dir);
3078
+ const relative4 = path10.relative(cwd, absolute);
3079
+ if (!relative4 || relative4 === ".." || relative4.startsWith(`..${path10.sep}`) || path10.isAbsolute(relative4)) {
3080
+ continue;
3081
+ }
3082
+ patterns.push(`${relative4.replace(/\\/g, "/")}/**`);
3083
+ }
3084
+ return patterns;
2379
3085
  }
2380
3086
  var BYTE_UNITS = {
2381
3087
  b: 1,
@@ -2444,11 +3150,13 @@ function resolveLayersMode(mode) {
2444
3150
  }
2445
3151
  return mode;
2446
3152
  }
2447
- function resolveBudgets(budgets, fallback) {
3153
+ function resolveBudgets(budgets, fallback, overrideDefault) {
2448
3154
  const byLayer = {};
2449
3155
  if (budgets === void 0) return { default: fallback, byLayer };
2450
3156
  if (typeof budgets !== "object" || budgets === null || Array.isArray(budgets)) {
2451
- throw new Error("`budgets` must be an object.");
3157
+ throw new Error(
3158
+ `\`budgets\` must be an object such as { default: "120k", byLayer: { api: "60k" } } (received ${describeValue2(budgets)}).`
3159
+ );
2452
3160
  }
2453
3161
  if (budgets.byLayer !== void 0) {
2454
3162
  if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
@@ -2463,10 +3171,12 @@ function resolveBudgets(budgets, fallback) {
2463
3171
  );
2464
3172
  }
2465
3173
  }
2466
- return {
2467
- default: boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default"),
2468
- byLayer
2469
- };
3174
+ const resolvedDefault = boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default");
3175
+ if (!overrideDefault) return { default: resolvedDefault, byLayer };
3176
+ for (const [layer2, value] of Object.entries(byLayer)) {
3177
+ byLayer[layer2] = Math.min(value, fallback);
3178
+ }
3179
+ return { default: fallback, byLayer };
2470
3180
  }
2471
3181
  function resolveHooks(hooks) {
2472
3182
  if (hooks === void 0) return {};
@@ -2479,6 +3189,11 @@ function resolveHooks(hooks) {
2479
3189
  return { ...hooks };
2480
3190
  }
2481
3191
  function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
3192
+ if (userConfig?.cwd !== void 0 && (typeof userConfig.cwd !== "string" || userConfig.cwd.trim() === "")) {
3193
+ throw new Error(
3194
+ `\`cwd\` must be a non-empty path string (received ${describeValue2(userConfig.cwd)}).`
3195
+ );
3196
+ }
2482
3197
  const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
2483
3198
  const resolved = path10.resolve(candidate);
2484
3199
  if (!fs9.existsSync(resolved)) {
@@ -2491,55 +3206,107 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
2491
3206
  }
2492
3207
  function resolveConfig(userConfig, cliOptions, baseDir) {
2493
3208
  const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
2494
- const forcedPreset = userConfig?.preset;
3209
+ const forcedPreset = resolvePresetName(userConfig?.preset);
2495
3210
  if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
2496
3211
  throw new Error(unknownPresetMessage(forcedPreset));
2497
3212
  }
2498
3213
  const preset = detectPreset(cwd, forcedPreset);
2499
3214
  const layersMode = resolveLayersMode(userConfig?.layersMode);
2500
- const layers = userConfig?.layers && userConfig.layers.length > 0 ? layersMode === "replace" ? userConfig.layers : [...userConfig.layers, ...preset.layers] : preset.layers;
2501
- const kinds = userConfig?.kinds && userConfig.kinds.length > 0 ? layersMode === "replace" ? userConfig.kinds : [...userConfig.kinds, ...preset.kinds] : preset.kinds;
2502
- const customRules = userConfig?.secrets?.customPatterns ?? [];
2503
- const rawRules = userConfig?.secrets?.disableDefaultPatterns ? customRules : [...DEFAULT_SECRET_PATTERNS, ...customRules];
2504
- const secretRules = compileSecretRules(rawRules);
2505
- const includeMonolith = cliOptions?.includeMonolith ?? userConfig?.includeMonolith ?? false;
3215
+ const userLayers = resolveLayerRules(userConfig?.layers);
3216
+ const userKinds = resolveKindRules(userConfig?.kinds);
3217
+ const layers = userLayers.length > 0 ? layersMode === "replace" ? userLayers : [...userLayers, ...preset.layers] : preset.layers;
3218
+ const kinds = userKinds.length > 0 ? layersMode === "replace" ? userKinds : [...userKinds, ...preset.kinds] : preset.kinds;
3219
+ const secrets = userConfig?.secrets;
3220
+ if (secrets !== void 0 && (typeof secrets !== "object" || Array.isArray(secrets))) {
3221
+ throw new Error(
3222
+ `\`secrets\` must be an object of { customPatterns, disableDefaultPatterns } (received ${describeValue2(secrets)}).`
3223
+ );
3224
+ }
3225
+ const customRules = secrets?.customPatterns ?? [];
3226
+ if (!Array.isArray(customRules)) {
3227
+ throw new Error(
3228
+ `\`secrets.customPatterns\` must be an array of secret rules (received ${describeValue2(customRules)}). Each entry is a RegExp, a string, or { rule, pattern }.`
3229
+ );
3230
+ }
3231
+ const disableDefaultPatterns = strictBoolean(
3232
+ secrets?.disableDefaultPatterns,
3233
+ false,
3234
+ "`secrets.disableDefaultPatterns`"
3235
+ );
3236
+ const aliasedRules = userConfig?.secretRules;
3237
+ if (aliasedRules !== void 0 && !Array.isArray(aliasedRules)) {
3238
+ throw new Error(
3239
+ `\`secretRules\` must be an array of { rule, pattern } entries (received ${describeValue2(aliasedRules)}).`
3240
+ );
3241
+ }
3242
+ const compiledCustomRules = compileSecretRules([
3243
+ ...customRules,
3244
+ ...aliasedRules ?? []
3245
+ ]);
3246
+ const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
3247
+ setActiveSecretRules(secretRules);
3248
+ const cliMonolith = cliOptions?.includeMonolith;
3249
+ const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
2506
3250
  const artifacts = resolveArtifacts(
2507
3251
  userConfig?.artifacts,
2508
3252
  cliOptions?.skipArtifacts,
2509
- includeMonolith
3253
+ includeMonolith,
3254
+ cliMonolith
2510
3255
  );
3256
+ const explicitMaxPartBytes = cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes;
2511
3257
  const maxPartBytes = boundedBytes(
2512
- cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes,
3258
+ explicitMaxPartBytes,
2513
3259
  300 * 1024,
2514
3260
  MIN_MAX_PART_BYTES,
2515
3261
  "maxPartBytes"
2516
3262
  );
2517
- const budgets = resolveBudgets(userConfig?.budgets, maxPartBytes);
3263
+ const budgets = resolveBudgets(
3264
+ userConfig?.budgets,
3265
+ maxPartBytes,
3266
+ explicitMaxPartBytes !== void 0
3267
+ );
3268
+ const configuredOutDir = strictString(userConfig?.outDir, "snapshot", "`outDir`");
3269
+ const outDir = cliOptions?.outDir || configuredOutDir;
3270
+ const tmpDir = strictString(userConfig?.tmpDir, "snapshot.tmp", "`tmpDir`");
3271
+ const branch = strictString(userConfig?.branch, "main", "`branch`");
3272
+ const configuredSensitiveFiles = strictBoolean(
3273
+ userConfig?.includeSensitiveFiles,
3274
+ false,
3275
+ "`includeSensitiveFiles`"
3276
+ );
3277
+ const includeSensitiveFiles = strictBoolean(
3278
+ cliOptions?.includeSensitiveFiles,
3279
+ configuredSensitiveFiles,
3280
+ "`includeSensitiveFiles`"
3281
+ );
3282
+ const presetExclude = [
3283
+ .../* @__PURE__ */ new Set([...preset.exclude, ...outputExcludePatterns(cwd, outDir, tmpDir)])
3284
+ ];
2518
3285
  return markResolvedConfig({
2519
3286
  cwd,
2520
- branch: userConfig?.branch || "main",
2521
- outDir: cliOptions?.outDir || userConfig?.outDir || "snapshot",
2522
- tmpDir: userConfig?.tmpDir || "snapshot.tmp",
3287
+ branch,
3288
+ outDir,
3289
+ tmpDir,
2523
3290
  maxPartBytes,
2524
3291
  largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
2525
- previewHeadLines: positiveInteger(userConfig?.previewHeadLines ?? 80, 80),
2526
- previewTailLines: positiveInteger(userConfig?.previewTailLines ?? 40, 40),
3292
+ previewHeadLines: strictPositiveInteger(userConfig?.previewHeadLines, 80, "`previewHeadLines`"),
3293
+ previewTailLines: strictPositiveInteger(userConfig?.previewTailLines, 40, "`previewTailLines`"),
2527
3294
  includeMonolith: artifacts.monolith,
2528
3295
  maxMonolithBytes: optionalBytes(
2529
3296
  cliOptions?.maxMonolithBytes !== void 0 ? cliOptions.maxMonolithBytes : userConfig?.maxMonolithBytes,
2530
3297
  "maxMonolithBytes"
2531
3298
  ),
2532
- includeSensitiveFiles: cliOptions?.includeSensitiveFiles ?? userConfig?.includeSensitiveFiles ?? false,
3299
+ includeSensitiveFiles,
2533
3300
  include: resolveInclude(userConfig?.include),
2534
3301
  preset: preset.presetName,
2535
- presetExclude: [...preset.exclude],
3302
+ presetExclude,
2536
3303
  artifacts,
2537
3304
  layersMode,
2538
3305
  budgets,
2539
3306
  hooks: resolveHooks(userConfig?.hooks),
2540
3307
  layers,
2541
3308
  kinds,
2542
- excludePatterns: userConfig?.excludePatterns ?? [],
3309
+ excludePatterns: resolveExcludePatterns(userConfig?.excludePatterns),
2543
3310
  secretRules
2544
3311
  });
2545
3312
  }
@@ -2585,7 +3352,8 @@ function defineConfig(config) {
2585
3352
  }
2586
3353
 
2587
3354
  // src/generator/audit.ts
2588
- function generateAuditReport(manifest, secretFindings) {
3355
+ function generateAuditReport(manifest, secretFindings, secretRules) {
3356
+ const mask = (value) => findSecretInPath(value, secretRules)?.masked ?? value;
2589
3357
  const lines = [
2590
3358
  "# Codebase Health & Security Audit",
2591
3359
  "",
@@ -2594,7 +3362,11 @@ function generateAuditReport(manifest, secretFindings) {
2594
3362
  `**Total Files:** ${manifest.counts.totalFiles} `,
2595
3363
  `**Total Lines:** ${manifest.counts.totalLines.toLocaleString()} `,
2596
3364
  `**Total Size:** ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB `,
2597
- `**Redacted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
3365
+ // This counts the bytes that were *emitted* after redaction, not the bytes
3366
+ // that were masked: calling it "redacted bytes" made a repository with zero
3367
+ // secrets report a non-zero redacted-byte figure directly above "no secrets
3368
+ // detected". The number of masked spans is reported in section 1 instead.
3369
+ `**Emitted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
2598
3370
  "",
2599
3371
  "## 1. Security Scan & Redaction",
2600
3372
  "",
@@ -2616,7 +3388,7 @@ function generateAuditReport(manifest, secretFindings) {
2616
3388
  "| :--- | :--- | :--- |"
2617
3389
  );
2618
3390
  for (const finding of secretFindings) {
2619
- lines.push(`| \`${finding.path ?? "unknown"}\` | ${finding.line} | \`${finding.rule}\` |`);
3391
+ lines.push(`| \`${mask(finding.path ?? "unknown")}\` | ${finding.line} | \`${finding.rule}\` |`);
2620
3392
  }
2621
3393
  lines.push("", "### Matches by rule", "", "| Rule | Matches |", "| :--- | :--- |");
2622
3394
  for (const [rule, count] of Array.from(byRule.entries()).sort((a, b) => b[1] - a[1])) {
@@ -2638,7 +3410,7 @@ function generateAuditReport(manifest, secretFindings) {
2638
3410
  for (const file of largeFiles) {
2639
3411
  const sizeKb = (file.sizeBytes / 1024).toFixed(1);
2640
3412
  const status = file.isPreviewTruncated ? "Truncated Head/Tail" : "Full";
2641
- lines.push(`| \`${file.relativePath}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
3413
+ lines.push(`| \`${mask(file.relativePath)}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
2642
3414
  }
2643
3415
  lines.push("");
2644
3416
  }
@@ -2680,7 +3452,7 @@ function generateAuditReport(manifest, secretFindings) {
2680
3452
  "| :--- | :--- | :--- |"
2681
3453
  );
2682
3454
  for (const skip of manifest.skipped) {
2683
- lines.push(`| \`${skip.path}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
3455
+ lines.push(`| \`${mask(skip.path)}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
2684
3456
  }
2685
3457
  lines.push("");
2686
3458
  }
@@ -2797,6 +3569,8 @@ function renumberParts(parts, contentFingerprint) {
2797
3569
  layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
2798
3570
  kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
2799
3571
  }
3572
+ const pathLines = included.map((file) => displayPath(file.relativePath));
3573
+ const listFence = fenceForText(pathLines.join("\n"), 3);
2800
3574
  const header = [
2801
3575
  `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
2802
3576
  "",
@@ -2809,9 +3583,9 @@ function renumberParts(parts, contentFingerprint) {
2809
3583
  "",
2810
3584
  "## Files",
2811
3585
  "",
2812
- "```text",
2813
- ...included.map((file) => displayPath(file.relativePath)),
2814
- "```",
3586
+ `${listFence}text`,
3587
+ ...pathLines,
3588
+ listFence,
2815
3589
  "",
2816
3590
  "## Contents",
2817
3591
  ""
@@ -2839,7 +3613,8 @@ function capValue(value, max) {
2839
3613
  return `${value.slice(0, half)}\u2026${value.slice(value.length - half)}`;
2840
3614
  }
2841
3615
  var capMetadata = (value) => capValue(value, METADATA_VALUE_MAX);
2842
- var displayPath = (value) => capValue(value, DISPLAY_PATH_MAX);
3616
+ var CONTROL_CHARS = /[\u0000-\u001f\u007f]/g;
3617
+ var displayPath = (value) => capValue(value.replace(CONTROL_CHARS, " "), DISPLAY_PATH_MAX);
2843
3618
  function topEntries(counts, limit) {
2844
3619
  return Object.entries(counts).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)).slice(0, limit).map(([key, value]) => `${capMetadata(key)} (${value})`).join(", ");
2845
3620
  }
@@ -2935,6 +3710,8 @@ function renderGroups(groups, contentFingerprint) {
2935
3710
  layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
2936
3711
  kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
2937
3712
  }
3713
+ const pathLines = group.map((s) => displayPath(s.file.relativePath));
3714
+ const listFence = fenceForText(pathLines.join("\n"), 3);
2938
3715
  const header = [
2939
3716
  `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
2940
3717
  "",
@@ -2947,9 +3724,9 @@ function renderGroups(groups, contentFingerprint) {
2947
3724
  "",
2948
3725
  "## Files",
2949
3726
  "",
2950
- "```text",
2951
- ...group.map((s) => displayPath(s.file.relativePath)),
2952
- "```",
3727
+ `${listFence}text`,
3728
+ ...pathLines,
3729
+ listFence,
2953
3730
  "",
2954
3731
  "## Contents",
2955
3732
  ""
@@ -3008,9 +3785,12 @@ function renderFileSection(file) {
3008
3785
  ].join("\n");
3009
3786
  }
3010
3787
  function fenceForContent(content) {
3788
+ return fenceForText(content, 4);
3789
+ }
3790
+ function fenceForText(content, minimum) {
3011
3791
  const matches = content.match(/`+/g);
3012
3792
  const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
3013
- return "`".repeat(Math.max(4, maxRun + 1));
3793
+ return "`".repeat(Math.max(minimum, maxRun + 1));
3014
3794
  }
3015
3795
  function renderSummary(summary) {
3016
3796
  if (!summary) return [];
@@ -3159,6 +3939,7 @@ function generateReadme(manifest) {
3159
3939
 
3160
3940
  // src/generator/tree.ts
3161
3941
  var MAX_TREE_LINES = 2e4;
3942
+ var CONTROL_CHARS2 = /[\u0000-\u001f\u007f]/g;
3162
3943
  function generateProjectTree(filePaths) {
3163
3944
  const root = { children: /* @__PURE__ */ new Map() };
3164
3945
  for (const filePath of filePaths) {
@@ -3173,7 +3954,7 @@ function generateProjectTree(filePaths) {
3173
3954
  current = child;
3174
3955
  }
3175
3956
  }
3176
- const lines = ["# Project Tree", "", "```text", "."];
3957
+ const lines = ["."];
3177
3958
  let truncatedAt = -1;
3178
3959
  function walk(node, prefix) {
3179
3960
  if (truncatedAt !== -1) return;
@@ -3188,7 +3969,8 @@ function generateProjectTree(filePaths) {
3188
3969
  const isLast = index === keys.length - 1;
3189
3970
  const child = node.children.get(key);
3190
3971
  const isDirectory = child.children.size > 0;
3191
- lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${key}${isDirectory ? "/" : ""}`);
3972
+ const label = key.replace(CONTROL_CHARS2, " ");
3973
+ lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${label}${isDirectory ? "/" : ""}`);
3192
3974
  if (lines.length >= MAX_TREE_LINES) {
3193
3975
  truncatedAt = lines.length;
3194
3976
  return;
@@ -3200,8 +3982,8 @@ function generateProjectTree(filePaths) {
3200
3982
  if (truncatedAt !== -1) {
3201
3983
  lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
3202
3984
  }
3203
- lines.push("```", "");
3204
- return lines.join("\n");
3985
+ const fence = fenceForText(lines.join("\n"), 3);
3986
+ return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
3205
3987
  }
3206
3988
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
3207
3989
  ".png",
@@ -3332,6 +4114,9 @@ function detectLanguage(filePath) {
3332
4114
  const ext = path10.posix.extname(base).toLowerCase();
3333
4115
  return LANGUAGE_BY_EXTENSION[ext] ?? "text";
3334
4116
  }
4117
+ function maskedPath(value, secretRules) {
4118
+ return findSecretInPath(value, secretRules)?.masked ?? value;
4119
+ }
3335
4120
  function emptyDrift() {
3336
4121
  return {
3337
4122
  missingInSnapshot: [],
@@ -3345,6 +4130,15 @@ function emptyDrift() {
3345
4130
  function sha256Hex(buffer) {
3346
4131
  return crypto.createHash("sha256").update(buffer).digest("hex");
3347
4132
  }
4133
+ function wouldNowBeIncluded(cwd, relativePath) {
4134
+ const absolutePath = path10.join(cwd, relativePath);
4135
+ try {
4136
+ if (!fs9.statSync(absolutePath).isFile()) return false;
4137
+ return !isBinaryFile(absolutePath);
4138
+ } catch {
4139
+ return false;
4140
+ }
4141
+ }
3348
4142
  function validateSnapshot(configInput) {
3349
4143
  assertUsableConfigInput(configInput);
3350
4144
  const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
@@ -3393,15 +4187,20 @@ function validateSnapshot(configInput) {
3393
4187
  const skippedSet = new Set(
3394
4188
  manifest.skipped.filter((entry) => entry && typeof entry.path === "string").map((entry) => entry.path)
3395
4189
  );
4190
+ const isSkippedOnDisk = (diskFile) => {
4191
+ if (skippedSet.has(diskFile)) return true;
4192
+ return skippedSet.has(maskedPath(diskFile, config.secretRules));
4193
+ };
3396
4194
  const collection = collectFiles(config.cwd, {
3397
4195
  includePatterns: config.include,
3398
4196
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
3399
- includeSensitiveFiles: config.includeSensitiveFiles
4197
+ includeSensitiveFiles: config.includeSensitiveFiles,
4198
+ secretRules: config.secretRules
3400
4199
  });
3401
4200
  for (const diskFile of collection.files) {
3402
- if (skippedSet.has(diskFile)) continue;
3403
4201
  const snapshotHash = manifestFileMap.get(diskFile);
3404
4202
  if (!snapshotHash) {
4203
+ if (isSkippedOnDisk(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
3405
4204
  drift.missingInSnapshot.push(diskFile);
3406
4205
  continue;
3407
4206
  }
@@ -3670,10 +4469,19 @@ function validateSnapshot(configInput) {
3670
4469
  `${drift.configMismatches.length} configuration mismatch(es) between the manifest and this run.`
3671
4470
  );
3672
4471
  }
4472
+ const maskedDrift = {
4473
+ ...drift,
4474
+ missingInSnapshot: drift.missingInSnapshot.map((value) => maskedPath(value, config.secretRules)),
4475
+ missingOnDisk: drift.missingOnDisk.map((value) => maskedPath(value, config.secretRules)),
4476
+ hashMismatches: drift.hashMismatches.map((mismatch) => ({
4477
+ ...mismatch,
4478
+ path: maskedPath(mismatch.path, config.secretRules)
4479
+ }))
4480
+ };
3673
4481
  return {
3674
4482
  valid: errors.length === 0,
3675
4483
  manifestPresent: true,
3676
- drift,
4484
+ drift: maskedDrift,
3677
4485
  errors,
3678
4486
  warnings
3679
4487
  };
@@ -3682,16 +4490,15 @@ function describeMatcher(matcher) {
3682
4490
  if (typeof matcher === "string") return matcher;
3683
4491
  if (matcher instanceof RegExp) return String(matcher);
3684
4492
  if (typeof matcher === "function") {
3685
- const source = String(matcher).replace(/\s+/g, " ").trim();
3686
- return source.length > 70 ? `${source.slice(0, 67)}...` : source;
4493
+ return "function (custom predicate)";
3687
4494
  }
3688
4495
  return String(matcher);
3689
4496
  }
3690
4497
  function normalizeTarget(cwd, target) {
3691
4498
  const absolute = path10.resolve(cwd, target);
3692
- const relative3 = path10.relative(cwd, absolute);
3693
- if (relative3.startsWith("..") || path10.isAbsolute(relative3)) return target;
3694
- return relative3.split(path10.sep).join("/");
4499
+ const relative4 = path10.relative(cwd, absolute);
4500
+ if (relative4.startsWith("..") || path10.isAbsolute(relative4)) return target;
4501
+ return relative4.split(path10.sep).join("/");
3695
4502
  }
3696
4503
  function findRuleMatch(rules, relativePath) {
3697
4504
  return rules.find(
@@ -3707,20 +4514,24 @@ function readManifest2(config) {
3707
4514
  }
3708
4515
  }
3709
4516
  function explainPath(config, target) {
3710
- const relative3 = normalizeTarget(config.cwd, target);
3711
- const absolute = path10.join(config.cwd, relative3);
4517
+ const relative4 = normalizeTarget(config.cwd, target);
4518
+ const absolute = path10.join(config.cwd, relative4);
4519
+ const maskedPath2 = findSecretInPath(relative4, config.secretRules)?.masked ?? relative4;
3712
4520
  const result = {
3713
- path: relative3,
4521
+ path: maskedPath2,
3714
4522
  exists: fs9.existsSync(absolute),
3715
4523
  included: false
3716
4524
  };
3717
4525
  const collection = collectFiles(config.cwd, {
3718
4526
  includePatterns: config.include,
3719
4527
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
3720
- includeSensitiveFiles: config.includeSensitiveFiles
4528
+ includeSensitiveFiles: config.includeSensitiveFiles,
4529
+ secretRules: config.secretRules
3721
4530
  });
3722
- if (!collection.files.includes(relative3)) {
3723
- const record = collection.skipped.find((entry2) => entry2.path === relative3);
4531
+ if (!collection.files.includes(relative4)) {
4532
+ const record = collection.skipped.find(
4533
+ (entry2) => entry2.path === relative4 || entry2.path === maskedPath2
4534
+ );
3724
4535
  if (record) {
3725
4536
  result.reason = record.reason;
3726
4537
  result.detail = record.detail;
@@ -3734,24 +4545,30 @@ function explainPath(config, target) {
3734
4545
  return result;
3735
4546
  }
3736
4547
  result.included = true;
3737
- result.layer = classifyLayer(relative3, config.layers);
3738
- result.kind = classifyKind(relative3, config.kinds);
3739
- result.language = detectLanguage(relative3);
3740
- const layerRule = findRuleMatch(config.layers, relative3);
4548
+ if (isBinaryFile(absolute)) {
4549
+ result.included = false;
4550
+ result.reason = "binary";
4551
+ result.detail = "binary content; generate skips it and records `binary` in manifest.skipped[]";
4552
+ return result;
4553
+ }
4554
+ result.layer = classifyLayer(relative4, config.layers);
4555
+ result.kind = classifyKind(relative4, config.kinds);
4556
+ result.language = detectLanguage(relative4);
4557
+ const layerRule = findRuleMatch(config.layers, relative4);
3741
4558
  if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
3742
- const kindRule = findRuleMatch(config.kinds, relative3);
4559
+ const kindRule = findRuleMatch(config.kinds, relative4);
3743
4560
  if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
3744
4561
  if (fs9.existsSync(absolute) && fs9.statSync(absolute).isFile()) {
3745
4562
  const buffer = fs9.readFileSync(absolute);
3746
4563
  const text2 = buffer.toString("utf8");
3747
4564
  result.bytes = buffer.length;
3748
4565
  result.lines = countLines(text2);
3749
- const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative3);
4566
+ const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative4);
3750
4567
  result.secretCount = findings.length;
3751
4568
  result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
3752
4569
  }
3753
4570
  const manifest = readManifest2(config);
3754
- const entry = manifest?.files.find((file) => file.relativePath === relative3);
4571
+ const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative4) : void 0;
3755
4572
  result.part = entry?.part ?? null;
3756
4573
  result.inclusionMode = entry?.inclusionMode;
3757
4574
  return result;
@@ -3806,7 +4623,40 @@ function hashMap(manifest) {
3806
4623
  }
3807
4624
  return map;
3808
4625
  }
4626
+ function manifestProblem(value) {
4627
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
4628
+ const candidate = value;
4629
+ if (!Array.isArray(candidate.files)) return 'no "files" array';
4630
+ for (const entry of candidate.files) {
4631
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
4632
+ return 'a "files" entry is not an object';
4633
+ }
4634
+ if (typeof entry.relativePath !== "string") {
4635
+ return 'a "files" entry has no "relativePath"';
4636
+ }
4637
+ }
4638
+ const counts = candidate.counts;
4639
+ if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
4640
+ return 'no "counts" object';
4641
+ }
4642
+ for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
4643
+ if (typeof counts[key] !== "number") {
4644
+ return `no numeric "counts.${key}"`;
4645
+ }
4646
+ }
4647
+ if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
4648
+ if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
4649
+ return null;
4650
+ }
4651
+ function assertUsableManifest(value, label) {
4652
+ const problem = manifestProblem(value);
4653
+ if (problem) {
4654
+ throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
4655
+ }
4656
+ }
3809
4657
  function compareManifests(before, after) {
4658
+ assertUsableManifest(before, "before");
4659
+ assertUsableManifest(after, "after");
3810
4660
  const left = hashMap(before);
3811
4661
  const right = hashMap(after);
3812
4662
  const added = [];
@@ -3823,12 +4673,13 @@ function compareManifests(before, after) {
3823
4673
  if (!right.has(filePath)) removed.push(filePath);
3824
4674
  }
3825
4675
  const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
4676
+ const mask = (values) => values.map((value) => findSecretInPath(value)?.masked ?? value);
3826
4677
  return {
3827
4678
  before: side(before),
3828
4679
  after: side(after),
3829
- added: sort(added),
3830
- removed: sort(removed),
3831
- changed: sort(changed),
4680
+ added: mask(sort(added)),
4681
+ removed: mask(sort(removed)),
4682
+ changed: mask(sort(changed)),
3832
4683
  unchanged
3833
4684
  };
3834
4685
  }
@@ -3852,11 +4703,11 @@ function loadManifest(target) {
3852
4703
  } catch (err) {
3853
4704
  throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
3854
4705
  }
3855
- if (!parsed || !Array.isArray(parsed.files)) {
3856
- throw new Error(`${manifestPath} is not a snapshot manifest (no "files" array).`);
3857
- }
3858
- if (!parsed.counts || typeof parsed.counts !== "object") {
3859
- throw new Error(`${manifestPath} is not a snapshot manifest (no "counts" object).`);
4706
+ const problem = manifestProblem(parsed);
4707
+ if (problem) {
4708
+ throw new Error(
4709
+ `${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
4710
+ );
3860
4711
  }
3861
4712
  return parsed;
3862
4713
  }
@@ -3968,7 +4819,7 @@ function renderConfig(answers) {
3968
4819
  secrets: {
3969
4820
  // [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
3970
4821
  customPatterns: [],
3971
- // Set true to drop the 21 built-in rules.
4822
+ // Set true to drop the built-in rules above and keep only customPatterns.
3972
4823
  disableDefaultPatterns: false,
3973
4824
  },
3974
4825
 
@@ -3984,6 +4835,10 @@ function renderConfig(answers) {
3984
4835
  },
3985
4836
  // Skip the monolith above this size. null = no limit.
3986
4837
  maxMonolithBytes: null,
4838
+ // Top-level switch for the monolith, equivalent to artifacts.monolith.
4839
+ // When both are present the artifact entry wins; this one exists so the
4840
+ // generated file lists every setting the tool accepts.
4841
+ includeMonolith: ${artifacts.monolith},
3987
4842
 
3988
4843
  // \u2500\u2500 Advanced \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
3989
4844
  // afterGenerate: (manifest) => { /* write your own artifact */ }
@@ -4305,6 +5160,14 @@ function existingConfig(cwd) {
4305
5160
  }
4306
5161
  return null;
4307
5162
  }
5163
+ function packageJsonHasConfig(cwd) {
5164
+ try {
5165
+ const parsed = JSON.parse(fs9.readFileSync(path10.join(cwd, "package.json"), "utf8"));
5166
+ return typeof parsed.snapshot === "object" && parsed.snapshot !== null;
5167
+ } catch {
5168
+ return false;
5169
+ }
5170
+ }
4308
5171
  function artifactAnswers(options) {
4309
5172
  const artifacts = { ...DEFAULT_ANSWERS.artifacts };
4310
5173
  for (const name of options.skipArtifacts ?? []) {
@@ -4517,6 +5380,11 @@ async function runInit(options) {
4517
5380
  `A configuration already exists (${present}). Re-run with --force to replace it.`
4518
5381
  );
4519
5382
  }
5383
+ if (packageJsonHasConfig(options.cwd) && !options.force) {
5384
+ throw new Error(
5385
+ '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.'
5386
+ );
5387
+ }
4520
5388
  if (fs9.existsSync(configPath) && !options.force) {
4521
5389
  throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
4522
5390
  }
@@ -4536,7 +5404,7 @@ async function runInit(options) {
4536
5404
 
4537
5405
  // src/index.ts
4538
5406
  var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
4539
- var GENERATOR_VERSION = "3.0.0";
5407
+ var GENERATOR_VERSION = "4.0.1";
4540
5408
  var SCHEMA_VERSION = "3.0.0";
4541
5409
  function sha256Hex2(input) {
4542
5410
  return crypto.createHash("sha256").update(input).digest("hex");
@@ -4573,7 +5441,6 @@ function bump(counts, key) {
4573
5441
  }
4574
5442
  var RELATIVE_SPECIFIER = /^\.{1,2}\//;
4575
5443
  var IMPORT_CANDIDATE_SUFFIXES = [
4576
- "",
4577
5444
  ".ts",
4578
5445
  ".tsx",
4579
5446
  ".mts",
@@ -4587,23 +5454,59 @@ var IMPORT_CANDIDATE_SUFFIXES = [
4587
5454
  "/index.js",
4588
5455
  "/index.mjs"
4589
5456
  ];
4590
- function resolveSpecifier(fromFile, specifier, knownFiles) {
5457
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
5458
+ ".ts",
5459
+ ".tsx",
5460
+ ".mts",
5461
+ ".cts",
5462
+ ".js",
5463
+ ".jsx",
5464
+ ".mjs",
5465
+ ".cjs"
5466
+ ]);
5467
+ function buildImportResolver(files) {
5468
+ const byPath = /* @__PURE__ */ new Map();
5469
+ const byPathFolded = /* @__PURE__ */ new Map();
5470
+ const byStem = /* @__PURE__ */ new Map();
5471
+ const byStemFolded = /* @__PURE__ */ new Map();
5472
+ const remember = (map, key, value) => {
5473
+ if (!map.has(key)) map.set(key, value);
5474
+ };
5475
+ for (const file of files) {
5476
+ const relativePath = file.relativePath;
5477
+ remember(byPath, relativePath, relativePath);
5478
+ remember(byPathFolded, relativePath.toLowerCase(), relativePath);
5479
+ const extension = path10.posix.extname(relativePath);
5480
+ if (!SOURCE_EXTENSIONS.has(extension)) continue;
5481
+ const stem = relativePath.slice(0, -extension.length);
5482
+ remember(byStem, stem, relativePath);
5483
+ remember(byStemFolded, stem.toLowerCase(), relativePath);
5484
+ if (path10.posix.basename(stem) === "index") {
5485
+ const directory = path10.posix.dirname(stem);
5486
+ remember(byStem, directory, relativePath);
5487
+ remember(byStemFolded, directory.toLowerCase(), relativePath);
5488
+ }
5489
+ }
5490
+ return { byPath, byPathFolded, byStem, byStemFolded };
5491
+ }
5492
+ function resolveSpecifier(fromFile, specifier, resolver) {
4591
5493
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
4592
- const baseDir = path10.posix.dirname(fromFile);
4593
- const joined = path10.posix.normalize(path10.posix.join(baseDir, specifier));
4594
- const candidate = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
5494
+ const joined = path10.posix.normalize(
5495
+ path10.posix.join(path10.posix.dirname(fromFile), specifier)
5496
+ );
5497
+ const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
5498
+ const direct = exact(joined);
5499
+ if (direct) return direct;
4595
5500
  for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
4596
- const withSuffix = `${joined}${suffix}`;
4597
- if (knownFiles.has(withSuffix)) return withSuffix;
5501
+ const hit = exact(`${joined}${suffix}`);
5502
+ if (hit) return hit;
4598
5503
  }
4599
- const extensionSwap = new RegExp(`${candidate}\\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$`);
4600
- for (const file of knownFiles) {
4601
- if (extensionSwap.test(file)) return file;
4602
- }
4603
- return null;
5504
+ const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
5505
+ const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
5506
+ return byStem(joined) ?? byStem(stem);
4604
5507
  }
4605
5508
  function buildIndexes(files) {
4606
- const knownFiles = new Set(files.map((file) => file.relativePath));
5509
+ const resolver = buildImportResolver(files);
4607
5510
  const importerCounts = /* @__PURE__ */ new Map();
4608
5511
  const byFile = files.filter((file) => isSourceSummary(file.summary)).map((file) => {
4609
5512
  const summary = file.summary;
@@ -4615,7 +5518,7 @@ function buildIndexes(files) {
4615
5518
  });
4616
5519
  for (const entry of byFile) {
4617
5520
  for (const specifier of entry.imports) {
4618
- const resolved = resolveSpecifier(entry.path, specifier, knownFiles);
5521
+ const resolved = resolveSpecifier(entry.path, specifier, resolver);
4619
5522
  if (resolved) importerCounts.set(resolved, (importerCounts.get(resolved) ?? 0) + 1);
4620
5523
  }
4621
5524
  }
@@ -4706,29 +5609,224 @@ function resolveSafeOutputDirs(config) {
4706
5609
  return { outDir, tmpDir };
4707
5610
  }
4708
5611
  var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
5612
+ var RUN_DIR_PREFIX = "run-";
5613
+ var STALE_RUN_MS = 60 * 60 * 1e3;
5614
+ function maskPathText(value, secretRules) {
5615
+ return findSecretInPath(value, secretRules)?.masked ?? value;
5616
+ }
5617
+ function maskSkipRecords(records, secretRules) {
5618
+ return maskSkipPaths(records, secretRules);
5619
+ }
5620
+ function isProcessAlive(pid) {
5621
+ try {
5622
+ process.kill(pid, 0);
5623
+ return true;
5624
+ } catch (err) {
5625
+ return err.code !== "ESRCH";
5626
+ }
5627
+ }
4709
5628
  var OWNERSHIP_MARKER = ".codebase-snapshot";
4710
- function writeOwnershipMarker(dir) {
5629
+ var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
5630
+ 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;
5631
+ function listEntries(dir) {
5632
+ const entries = [];
5633
+ const walk = (absolute, prefix) => {
5634
+ let children;
5635
+ try {
5636
+ children = fs9.readdirSync(absolute, { withFileTypes: true });
5637
+ } catch {
5638
+ return;
5639
+ }
5640
+ for (const child of children) {
5641
+ const relative4 = prefix === "" ? child.name : `${prefix}/${child.name}`;
5642
+ entries.push(relative4);
5643
+ if (child.isDirectory()) walk(path10.join(absolute, child.name), relative4);
5644
+ }
5645
+ };
5646
+ walk(dir, "");
5647
+ return entries.sort();
5648
+ }
5649
+ function markerPayload(dir, state, id, createdAt) {
4711
5650
  const marker2 = {
4712
5651
  generator: GENERATOR_NAME,
4713
5652
  version: GENERATOR_VERSION,
4714
- schemaVersion: SCHEMA_VERSION
5653
+ schemaVersion: SCHEMA_VERSION,
5654
+ createdAt,
5655
+ state,
5656
+ id,
5657
+ pid: process.pid,
5658
+ entries: listEntries(dir)
4715
5659
  };
4716
- fs9.writeFileSync(path10.join(dir, OWNERSHIP_MARKER), `${JSON.stringify(marker2, null, 2)}
4717
- `, "utf8");
4718
- }
4719
- function isOwnOutput(dir) {
4720
- if (fs9.existsSync(path10.join(dir, OWNERSHIP_MARKER))) return true;
4721
5660
  const manifestPath = path10.join(dir, "manifest.json");
4722
- if (!fs9.existsSync(manifestPath)) return false;
5661
+ if (fs9.existsSync(manifestPath)) {
5662
+ marker2.manifestSha256 = sha256Hex2(fs9.readFileSync(manifestPath));
5663
+ }
5664
+ return marker2;
5665
+ }
5666
+ function writeOwnershipMarker(dir, marker2) {
5667
+ fs9.writeFileSync(
5668
+ path10.join(dir, OWNERSHIP_MARKER),
5669
+ `${JSON.stringify(marker2, null, 2)}
5670
+ `,
5671
+ "utf8"
5672
+ );
5673
+ }
5674
+ function readOwnershipMarker(dir) {
5675
+ const markerPath = path10.join(dir, OWNERSHIP_MARKER);
5676
+ if (!fs9.existsSync(markerPath)) {
5677
+ return { marker: null, problem: "it holds no .codebase-snapshot marker" };
5678
+ }
5679
+ let raw;
4723
5680
  try {
4724
- const parsed = JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
4725
- const generator = parsed.generator;
4726
- return typeof parsed.schemaVersion === "string" && Array.isArray(parsed.files) && typeof generator === "object" && generator !== null && generator.name === GENERATOR_NAME;
5681
+ raw = fs9.readFileSync(markerPath, "utf8");
4727
5682
  } catch {
4728
- return false;
5683
+ return { marker: null, problem: "its .codebase-snapshot marker could not be read" };
5684
+ }
5685
+ if (raw.trim().length === 0) {
5686
+ return { marker: null, problem: "its .codebase-snapshot marker is empty" };
5687
+ }
5688
+ let parsed;
5689
+ try {
5690
+ parsed = JSON.parse(raw);
5691
+ } catch {
5692
+ return { marker: null, problem: "its .codebase-snapshot marker is not valid JSON" };
5693
+ }
5694
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
5695
+ return { marker: null, problem: "its .codebase-snapshot marker is not a JSON object" };
5696
+ }
5697
+ const candidate = parsed;
5698
+ if (candidate.generator !== GENERATOR_NAME) {
5699
+ return { marker: null, problem: "its .codebase-snapshot marker names a different generator" };
5700
+ }
5701
+ if (typeof candidate.schemaVersion !== "string" || !SUPPORTED_MARKER_SCHEMAS.has(candidate.schemaVersion)) {
5702
+ return {
5703
+ marker: null,
5704
+ problem: `its .codebase-snapshot marker has a schema version this build does not understand (${String(candidate.schemaVersion)})`
5705
+ };
5706
+ }
5707
+ if (typeof candidate.id !== "string" || !SNAPSHOT_ID_PATTERN.test(candidate.id)) {
5708
+ return { marker: null, problem: "its .codebase-snapshot marker carries no snapshot id" };
5709
+ }
5710
+ if (candidate.state !== "staging" && candidate.state !== "complete") {
5711
+ return { marker: null, problem: "its .codebase-snapshot marker records no run state" };
5712
+ }
5713
+ return {
5714
+ marker: {
5715
+ generator: GENERATOR_NAME,
5716
+ version: typeof candidate.version === "string" ? candidate.version : "unknown",
5717
+ schemaVersion: candidate.schemaVersion,
5718
+ createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
5719
+ state: candidate.state,
5720
+ id: candidate.id,
5721
+ pid: typeof candidate.pid === "number" ? candidate.pid : void 0,
5722
+ manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
5723
+ entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
5724
+ },
5725
+ problem: null
5726
+ };
5727
+ }
5728
+ function describeManifestProblem(value) {
5729
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "is not a JSON object";
5730
+ const candidate = value;
5731
+ const generator = candidate.generator;
5732
+ if (typeof generator !== "object" || generator === null || generator.name !== GENERATOR_NAME) {
5733
+ return "was not written by this generator";
5734
+ }
5735
+ if (!Array.isArray(candidate.files)) return "has no files array";
5736
+ return null;
5737
+ }
5738
+ function assessSnapshotDirectory(dir) {
5739
+ const { marker: marker2, problem } = readOwnershipMarker(dir);
5740
+ if (!marker2) return { owned: false, problem, foreignEntries: [], marker: null };
5741
+ if (marker2.state !== "complete") {
5742
+ return {
5743
+ owned: false,
5744
+ problem: "its .codebase-snapshot marker records a run that never finished writing it",
5745
+ foreignEntries: [],
5746
+ marker: marker2
5747
+ };
5748
+ }
5749
+ const manifestPath = path10.join(dir, "manifest.json");
5750
+ if (fs9.existsSync(manifestPath)) {
5751
+ let raw;
5752
+ try {
5753
+ raw = fs9.readFileSync(manifestPath, "utf8");
5754
+ } catch {
5755
+ return {
5756
+ owned: false,
5757
+ problem: "its manifest.json could not be read",
5758
+ foreignEntries: [],
5759
+ marker: marker2
5760
+ };
5761
+ }
5762
+ let parsed;
5763
+ try {
5764
+ parsed = JSON.parse(raw);
5765
+ } catch {
5766
+ return {
5767
+ owned: false,
5768
+ problem: "its manifest.json is not valid JSON",
5769
+ foreignEntries: [],
5770
+ marker: marker2
5771
+ };
5772
+ }
5773
+ const manifestProblem2 = describeManifestProblem(parsed);
5774
+ if (manifestProblem2) {
5775
+ return {
5776
+ owned: false,
5777
+ problem: `its manifest.json ${manifestProblem2}`,
5778
+ foreignEntries: [],
5779
+ marker: marker2
5780
+ };
5781
+ }
5782
+ if (!marker2.manifestSha256) {
5783
+ return {
5784
+ owned: false,
5785
+ problem: "its .codebase-snapshot marker records no manifest digest",
5786
+ foreignEntries: [],
5787
+ marker: marker2
5788
+ };
5789
+ }
5790
+ if (sha256Hex2(raw) !== marker2.manifestSha256) {
5791
+ return {
5792
+ owned: false,
5793
+ problem: "its manifest.json does not match the digest recorded in its .codebase-snapshot marker",
5794
+ foreignEntries: [],
5795
+ marker: marker2
5796
+ };
5797
+ }
5798
+ } else if (marker2.manifestSha256) {
5799
+ return {
5800
+ owned: false,
5801
+ problem: "its manifest.json is missing although its marker records one",
5802
+ foreignEntries: [],
5803
+ marker: marker2
5804
+ };
5805
+ }
5806
+ const recorded = new Set(marker2.entries ?? []);
5807
+ if (recorded.size === 0) {
5808
+ return {
5809
+ owned: false,
5810
+ problem: "its .codebase-snapshot marker records no contents",
5811
+ foreignEntries: [],
5812
+ marker: marker2
5813
+ };
4729
5814
  }
5815
+ const foreignEntries = listEntries(dir).filter(
5816
+ (entry) => !recorded.has(entry) && !PLACEHOLDER_ENTRIES.has(entry)
5817
+ );
5818
+ return {
5819
+ owned: foreignEntries.length === 0,
5820
+ problem: null,
5821
+ foreignEntries,
5822
+ marker: marker2
5823
+ };
5824
+ }
5825
+ function describeEntries(entries, limit = 5) {
5826
+ const shown = entries.slice(0, limit).join(", ");
5827
+ return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
4730
5828
  }
4731
- function assertReplaceable(target, label) {
5829
+ function assertReplaceable(target, label, secretRules) {
4732
5830
  if (!fs9.existsSync(target)) return;
4733
5831
  if (!fs9.statSync(target).isDirectory()) {
4734
5832
  throw new Error(
@@ -4736,26 +5834,210 @@ function assertReplaceable(target, label) {
4736
5834
  );
4737
5835
  }
4738
5836
  const entries = fs9.readdirSync(target);
4739
- const meaningful = entries.filter((name) => !PLACEHOLDER_ENTRIES.has(name));
4740
- if (meaningful.length === 0) return;
4741
- if (isOwnOutput(target)) return;
4742
- const preview = meaningful.slice(0, 5).join(", ");
4743
- const more = meaningful.length > 5 ? `, and ${meaningful.length - 5} more` : "";
5837
+ if (entries.length === 0) return;
5838
+ if (entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
5839
+ const { problem } = readOwnershipMarker(target);
5840
+ const verdict = assessSnapshotDirectory(target);
5841
+ if (verdict.owned) return;
5842
+ const shownForeign = verdict.foreignEntries.map((name) => maskPathText(name, secretRules));
5843
+ const shownEntries = entries.map((name) => maskPathText(name, secretRules));
5844
+ if (verdict.foreignEntries.length > 0) {
5845
+ throw new Error(
5846
+ `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.`
5847
+ );
5848
+ }
5849
+ const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
4744
5850
  throw new Error(
4745
- `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.`
5851
+ `Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(shownEntries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
4746
5852
  );
4747
5853
  }
4748
- function swapIntoPlace(tmpDir, outDir) {
5854
+ function assertStagingRootUsable(tmpDir, secretRules) {
5855
+ if (!fs9.existsSync(tmpDir)) return;
5856
+ if (!fs9.statSync(tmpDir).isDirectory()) {
5857
+ throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
5858
+ }
5859
+ const entries = fs9.readdirSync(tmpDir);
5860
+ if (entries.length === 0) return;
5861
+ const foreign = entries.filter((name) => {
5862
+ const full = path10.join(tmpDir, name);
5863
+ if (!name.startsWith(RUN_DIR_PREFIX)) return true;
5864
+ let stats;
5865
+ try {
5866
+ stats = fs9.statSync(full);
5867
+ } catch {
5868
+ return true;
5869
+ }
5870
+ if (!stats.isDirectory()) return true;
5871
+ const { marker: marker2 } = readOwnershipMarker(full);
5872
+ if (marker2) return false;
5873
+ try {
5874
+ return fs9.readdirSync(full).length > 0;
5875
+ } catch {
5876
+ return true;
5877
+ }
5878
+ });
5879
+ if (foreign.length > 0) {
5880
+ const shown = describeEntries(foreign.map((name) => maskPathText(name, secretRules)));
5881
+ throw new Error(
5882
+ `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.`
5883
+ );
5884
+ }
5885
+ }
5886
+ function cleanStaleRunDirs(tmpDir) {
5887
+ let entries;
5888
+ try {
5889
+ entries = fs9.readdirSync(tmpDir);
5890
+ } catch {
5891
+ return;
5892
+ }
5893
+ for (const name of entries) {
5894
+ if (!name.startsWith(RUN_DIR_PREFIX)) continue;
5895
+ const full = path10.join(tmpDir, name);
5896
+ const { marker: marker2 } = readOwnershipMarker(full);
5897
+ if (marker2) {
5898
+ const created = Date.parse(marker2.createdAt);
5899
+ const aged = !Number.isFinite(created) || Date.now() - created > STALE_RUN_MS;
5900
+ const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
5901
+ if (aged || abandoned) {
5902
+ try {
5903
+ fs9.rmSync(full, { recursive: true, force: true });
5904
+ } catch {
5905
+ }
5906
+ }
5907
+ continue;
5908
+ }
5909
+ try {
5910
+ if (fs9.readdirSync(full).length === 0) fs9.rmSync(full, { recursive: true, force: true });
5911
+ } catch {
5912
+ }
5913
+ }
5914
+ }
5915
+ function removeIfEmpty(dir) {
5916
+ try {
5917
+ if (fs9.readdirSync(dir).length === 0) fs9.rmSync(dir, { recursive: true, force: true });
5918
+ } catch {
5919
+ }
5920
+ }
5921
+ function ensureParentDirectory(target, label) {
5922
+ const parent = path10.dirname(target);
5923
+ if (fs9.existsSync(parent) && !fs9.statSync(parent).isDirectory()) {
5924
+ throw new Error(
5925
+ `Refusing to create ${label} ${target}: ${parent} exists but is not a directory.`
5926
+ );
5927
+ }
5928
+ try {
5929
+ fs9.mkdirSync(parent, { recursive: true });
5930
+ } catch (err) {
5931
+ throw new Error(
5932
+ `Refusing to create ${label} ${target}: its parent ${parent} could not be created (${err.message}).`
5933
+ );
5934
+ }
5935
+ }
5936
+ function acquireRunLock(stagingRoot) {
5937
+ const key = lockKeyFor(stagingRoot);
5938
+ const lockPath = path10.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
5939
+ const payload = `${JSON.stringify({
5940
+ pid: process.pid,
5941
+ stagingRoot,
5942
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
5943
+ })}
5944
+ `;
5945
+ for (let attempt = 0; attempt < 2; attempt += 1) {
5946
+ try {
5947
+ const handle = fs9.openSync(lockPath, "wx");
5948
+ try {
5949
+ fs9.writeSync(handle, payload);
5950
+ } finally {
5951
+ fs9.closeSync(handle);
5952
+ }
5953
+ return () => {
5954
+ try {
5955
+ fs9.rmSync(lockPath, { force: true });
5956
+ } catch {
5957
+ }
5958
+ };
5959
+ } catch (err) {
5960
+ if (err.code !== "EEXIST") throw err;
5961
+ if (attempt === 0 && lockOwnerIsGone(lockPath)) {
5962
+ fs9.rmSync(lockPath, { force: true });
5963
+ continue;
5964
+ }
5965
+ throw new Error(
5966
+ `Another generate run is already using the staging directory ${stagingRoot}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
5967
+ );
5968
+ }
5969
+ }
5970
+ throw new Error(`Could not acquire the generate lock for ${stagingRoot}.`);
5971
+ }
5972
+ function lockKeyFor(stagingRoot) {
5973
+ let key = stagingRoot;
5974
+ try {
5975
+ key = fs9.realpathSync(stagingRoot);
5976
+ } catch {
5977
+ }
5978
+ if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
5979
+ return sha256Hex2(key).slice(0, 16);
5980
+ }
5981
+ function lockOwnerIsGone(lockPath) {
5982
+ let raw;
5983
+ try {
5984
+ raw = fs9.readFileSync(lockPath, "utf8");
5985
+ } catch {
5986
+ return false;
5987
+ }
5988
+ let pid;
5989
+ try {
5990
+ pid = JSON.parse(raw).pid;
5991
+ } catch {
5992
+ return false;
5993
+ }
5994
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return false;
5995
+ try {
5996
+ process.kill(pid, 0);
5997
+ return false;
5998
+ } catch (err) {
5999
+ return err.code === "ESRCH";
6000
+ }
6001
+ }
6002
+ function writeManifestAtomically(manifestPath, manifest) {
6003
+ const tempPath = `${manifestPath}.${process.pid}.tmp`;
6004
+ fs9.writeFileSync(tempPath, JSON.stringify(manifest, null, 2), "utf8");
6005
+ try {
6006
+ fs9.renameSync(tempPath, manifestPath);
6007
+ } catch (err) {
6008
+ try {
6009
+ fs9.rmSync(tempPath, { force: true });
6010
+ } catch {
6011
+ }
6012
+ throw err;
6013
+ }
6014
+ }
6015
+ function swapIntoPlace(stagingDir, outDir) {
6016
+ ensureParentDirectory(outDir, "outDir");
6017
+ const deadline = Date.now() + 3e3;
6018
+ for (let attempt = 0; ; attempt += 1) {
6019
+ try {
6020
+ swapOnce(stagingDir, outDir);
6021
+ return;
6022
+ } catch (err) {
6023
+ const code = err.code;
6024
+ const racy = code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM";
6025
+ if (!racy || attempt >= 10 || Date.now() > deadline) throw err;
6026
+ sleepSync(50);
6027
+ }
6028
+ }
6029
+ }
6030
+ function swapOnce(stagingDir, outDir) {
4749
6031
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
4750
6032
  const hadPrevious = fs9.existsSync(outDir);
4751
6033
  if (hadPrevious) fs9.renameSync(outDir, backupDir);
4752
6034
  try {
4753
6035
  try {
4754
- fs9.renameSync(tmpDir, outDir);
6036
+ fs9.renameSync(stagingDir, outDir);
4755
6037
  } catch (err) {
4756
6038
  if (err.code === "EXDEV") {
4757
- fs9.cpSync(tmpDir, outDir, { recursive: true });
4758
- fs9.rmSync(tmpDir, { recursive: true, force: true });
6039
+ fs9.cpSync(stagingDir, outDir, { recursive: true });
6040
+ fs9.rmSync(stagingDir, { recursive: true, force: true });
4759
6041
  } else {
4760
6042
  throw err;
4761
6043
  }
@@ -4769,6 +6051,10 @@ function swapIntoPlace(tmpDir, outDir) {
4769
6051
  throw err;
4770
6052
  }
4771
6053
  }
6054
+ function sleepSync(milliseconds) {
6055
+ const buffer = new Int32Array(new SharedArrayBuffer(4));
6056
+ Atomics.wait(buffer, 0, 0, milliseconds);
6057
+ }
4772
6058
  function artifactWarnings(artifacts) {
4773
6059
  const messages = [];
4774
6060
  if (!artifacts.manifest) {
@@ -4831,10 +6117,12 @@ function generateSnapshot(configInput, options) {
4831
6117
  const collection = collectFiles(config.cwd, {
4832
6118
  includePatterns: config.include,
4833
6119
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
4834
- includeSensitiveFiles: config.includeSensitiveFiles
6120
+ includeSensitiveFiles: config.includeSensitiveFiles,
6121
+ secretRules: config.secretRules
4835
6122
  });
4836
6123
  const files = [];
4837
- const skipped = [...collection.skipped];
6124
+ const monolithBodies = /* @__PURE__ */ new Map();
6125
+ const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
4838
6126
  const secretFindings = [];
4839
6127
  const warnings = [];
4840
6128
  const layerCounts = {};
@@ -4897,6 +6185,9 @@ function generateSnapshot(configInput, options) {
4897
6185
  isPreviewTruncated = preview.isTruncated;
4898
6186
  includedLines = preview.includedLines;
4899
6187
  }
6188
+ if (config.includeMonolith && isPreviewTruncated) {
6189
+ monolithBodies.set(relativePath, redacted);
6190
+ }
4900
6191
  let summary;
4901
6192
  let summaryQuality;
4902
6193
  try {
@@ -4905,12 +6196,12 @@ function generateSnapshot(configInput, options) {
4905
6196
  summaryQuality = outcome?.quality;
4906
6197
  if (isSourceSummary(summary) && summary.parseErrors) {
4907
6198
  warnings.push(
4908
- `Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
6199
+ `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
4909
6200
  );
4910
6201
  }
4911
6202
  } catch (err) {
4912
6203
  warnings.push(
4913
- `Summary skipped for ${relativePath}: ${err.message}`.replace(/\s+/g, " ")
6204
+ `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(/\s+/g, " ")
4914
6205
  );
4915
6206
  }
4916
6207
  files.push({
@@ -4939,7 +6230,12 @@ function generateSnapshot(configInput, options) {
4939
6230
  ).join("\n")
4940
6231
  );
4941
6232
  const fullContents = /* @__PURE__ */ new Map();
4942
- for (const file of files) fullContents.set(file.relativePath, file.content ?? "");
6233
+ for (const file of files) {
6234
+ fullContents.set(
6235
+ file.relativePath,
6236
+ monolithBodies.get(file.relativePath) ?? file.content ?? ""
6237
+ );
6238
+ }
4943
6239
  const parts = partitionByBudget(files, config, fingerprint);
4944
6240
  const maxEffectiveBudget = Math.max(
4945
6241
  config.maxPartBytes,
@@ -5014,21 +6310,28 @@ function generateSnapshot(configInput, options) {
5014
6310
  const { content: _content, ...rest } = file;
5015
6311
  return rest;
5016
6312
  }),
5017
- skipped,
6313
+ skipped: maskSkipRecords(skipped, config.secretRules),
5018
6314
  warnings
5019
6315
  };
5020
6316
  manifest.warnings = [
5021
6317
  ...warnings,
5022
- ...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
6318
+ ...secretFindings.map((finding) => `${finding.rule} at ${maskPathText(finding.path ?? "unknown", config.secretRules)}:${finding.line}`)
5023
6319
  ];
5024
6320
  manifest.totals.warningCount = manifest.warnings.length;
5025
6321
  let monolithJson = null;
5026
6322
  let monolithMarkdown = null;
5027
6323
  if (config.includeMonolith) {
5028
- const fullFiles = files.map((file) => ({
5029
- ...file,
5030
- content: fullContents.get(file.relativePath) ?? ""
5031
- }));
6324
+ const fullFiles = files.map((file) => {
6325
+ const content = fullContents.get(file.relativePath) ?? "";
6326
+ return {
6327
+ ...file,
6328
+ content,
6329
+ inclusionMode: "full",
6330
+ isPreviewTruncated: false,
6331
+ includedBytes: Buffer.byteLength(content, "utf8"),
6332
+ includedLines: countLines(content)
6333
+ };
6334
+ });
5032
6335
  const json = generateMonolithJson(manifest, fullFiles);
5033
6336
  const markdown = generateMonolithMarkdown(manifest, fullFiles);
5034
6337
  const jsonBytes = Buffer.byteLength(json, "utf8");
@@ -5056,62 +6359,97 @@ function generateSnapshot(configInput, options) {
5056
6359
  }
5057
6360
  for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
5058
6361
  manifest.totals.warningCount = manifest.warnings.length;
6362
+ const { outDir, tmpDir } = resolveSafeOutputDirs(config);
6363
+ const { artifacts } = config;
6364
+ assertReplaceable(outDir, "outDir", config.secretRules);
6365
+ assertStagingRootUsable(tmpDir, config.secretRules);
5059
6366
  if (dryRun) {
5060
6367
  runAfterGenerate(config, manifest);
5061
6368
  return manifest;
5062
6369
  }
5063
- const { outDir, tmpDir } = resolveSafeOutputDirs(config);
5064
- assertReplaceable(tmpDir, "tmpDir");
5065
- assertReplaceable(outDir, "outDir");
5066
- const { artifacts } = config;
5067
- fs9.rmSync(tmpDir, { recursive: true, force: true });
5068
- fs9.mkdirSync(tmpDir, { recursive: true });
5069
- writeOwnershipMarker(tmpDir);
5070
- const partsDir = path10.join(tmpDir, "parts");
5071
- const reportsDir = path10.join(tmpDir, "reports");
5072
- if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
5073
- if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
5074
- fs9.mkdirSync(reportsDir, { recursive: true });
5075
- }
5076
- if (artifacts.parts) {
5077
- for (const part of parts) {
5078
- fs9.writeFileSync(path10.join(partsDir, part.filename), part.markdown, "utf8");
6370
+ const releaseLock = acquireRunLock(tmpDir);
6371
+ let stagingDir = null;
6372
+ try {
6373
+ ensureParentDirectory(outDir, "outDir");
6374
+ fs9.mkdirSync(tmpDir, { recursive: true });
6375
+ cleanStaleRunDirs(tmpDir);
6376
+ const snapshotId = crypto.randomUUID();
6377
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6378
+ stagingDir = path10.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
6379
+ fs9.mkdirSync(stagingDir);
6380
+ writeOwnershipMarker(stagingDir, {
6381
+ generator: GENERATOR_NAME,
6382
+ version: GENERATOR_VERSION,
6383
+ schemaVersion: SCHEMA_VERSION,
6384
+ createdAt,
6385
+ state: "staging",
6386
+ id: snapshotId,
6387
+ pid: process.pid
6388
+ });
6389
+ const partsDir = path10.join(stagingDir, "parts");
6390
+ const reportsDir = path10.join(stagingDir, "reports");
6391
+ if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
6392
+ if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
6393
+ fs9.mkdirSync(reportsDir, { recursive: true });
6394
+ }
6395
+ if (artifacts.parts) {
6396
+ for (const part of parts) {
6397
+ fs9.writeFileSync(path10.join(partsDir, part.filename), part.markdown, "utf8");
6398
+ }
5079
6399
  }
6400
+ if (artifacts.projectTree) {
6401
+ const projectTree = generateProjectTree(files.map((file) => file.relativePath));
6402
+ fs9.writeFileSync(path10.join(reportsDir, "project-tree.md"), projectTree, "utf8");
6403
+ }
6404
+ if (artifacts.navigation) {
6405
+ fs9.writeFileSync(path10.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
6406
+ }
6407
+ if (artifacts.audit) {
6408
+ fs9.writeFileSync(
6409
+ path10.join(reportsDir, "audit.md"),
6410
+ generateAuditReport(manifest, secretFindings, config.secretRules),
6411
+ "utf8"
6412
+ );
6413
+ }
6414
+ if (artifacts.readme) {
6415
+ fs9.writeFileSync(path10.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
6416
+ }
6417
+ if (artifacts.manifest) {
6418
+ fs9.writeFileSync(path10.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6419
+ }
6420
+ if (monolithJson !== null) {
6421
+ fs9.writeFileSync(path10.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
6422
+ }
6423
+ if (monolithMarkdown !== null) {
6424
+ fs9.writeFileSync(path10.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
6425
+ }
6426
+ writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
6427
+ swapIntoPlace(stagingDir, outDir);
6428
+ stagingDir = null;
6429
+ const warningsBeforeHook = manifest.warnings.length;
6430
+ runAfterGenerate(config, manifest);
6431
+ if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
6432
+ writeManifestAtomically(path10.join(outDir, "manifest.json"), manifest);
6433
+ }
6434
+ writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
6435
+ return manifest;
6436
+ } catch (err) {
6437
+ if (stagingDir !== null) {
6438
+ try {
6439
+ fs9.rmSync(stagingDir, { recursive: true, force: true });
6440
+ } catch {
6441
+ }
6442
+ }
6443
+ throw err;
6444
+ } finally {
6445
+ removeIfEmpty(tmpDir);
6446
+ releaseLock();
5080
6447
  }
5081
- if (artifacts.projectTree) {
5082
- const projectTree = generateProjectTree(files.map((file) => file.relativePath));
5083
- fs9.writeFileSync(path10.join(reportsDir, "project-tree.md"), projectTree, "utf8");
5084
- }
5085
- if (artifacts.navigation) {
5086
- fs9.writeFileSync(path10.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
5087
- }
5088
- if (artifacts.audit) {
5089
- fs9.writeFileSync(
5090
- path10.join(reportsDir, "audit.md"),
5091
- generateAuditReport(manifest, secretFindings),
5092
- "utf8"
5093
- );
5094
- }
5095
- if (artifacts.readme) {
5096
- fs9.writeFileSync(path10.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
5097
- }
5098
- if (artifacts.manifest) {
5099
- fs9.writeFileSync(path10.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
5100
- }
5101
- if (monolithJson !== null) {
5102
- fs9.writeFileSync(path10.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
5103
- }
5104
- if (monolithMarkdown !== null) {
5105
- fs9.writeFileSync(path10.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
5106
- }
5107
- swapIntoPlace(tmpDir, outDir);
5108
- runAfterGenerate(config, manifest);
5109
- return manifest;
5110
6448
  }
5111
6449
  async function generateSnapshotAsync(configInput, options) {
5112
6450
  return generateSnapshot(configInput, options);
5113
6451
  }
5114
6452
 
5115
- export { ARTIFACT_NAMES, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, GENERATOR_NAME, GENERATOR_VERSION, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, PRESETS, PromptCancelled, SCHEMA_VERSION, anchorForPath, assertReplaceable, assertUsableConfigInput, collectFiles, compareManifests, countLines, defineConfig, detectLanguage, detectPreset, explainPath, fenceForContent, generateProjectTree, generateSnapshot, generateSnapshotAsync, getPreset, globToRegExp, isBinaryBuffer, isBinaryExtension, isBinaryFile, isInteractive, isResolvedConfig, isSensitivePath, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, profileForPath, renderConfig, renderDiffText, renderExplainText, renderMinimalConfig, renumberParts, resolveConfig, resolveSafeOutputDirs, runInit, scanAndRedactSecrets, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, validateSnapshot };
5116
- //# sourceMappingURL=chunk-5B4UVQLR.js.map
5117
- //# sourceMappingURL=chunk-5B4UVQLR.js.map
6453
+ export { ARTIFACT_NAMES, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, GENERATOR_NAME, GENERATOR_VERSION, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, PRESETS, PromptCancelled, SCHEMA_VERSION, anchorForPath, assertReplaceable, assertUsableConfigInput, collectFiles, compareManifests, countLines, defineConfig, detectLanguage, detectPreset, explainPath, fenceForContent, fenceForText, generateProjectTree, generateSnapshot, generateSnapshotAsync, getPreset, globToRegExp, isBinaryBuffer, isBinaryExtension, isBinaryFile, isInteractive, isResolvedConfig, isSensitivePath, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, profileForPath, renderConfig, renderDiffText, renderExplainText, renderMinimalConfig, renumberParts, resolveConfig, resolveSafeOutputDirs, runInit, scanAndRedactSecrets, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, validateSnapshot };
6454
+ //# sourceMappingURL=chunk-RGIMMEAW.js.map
6455
+ //# sourceMappingURL=chunk-RGIMMEAW.js.map