@ankurhalder/codebase-snapshot 3.0.0 → 4.0.0

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