@ankurhalder/codebase-snapshot 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -3,8 +3,8 @@
3
3
  var fs9 = require('fs');
4
4
  var path10 = require('path');
5
5
  var util = require('util');
6
- var url = require('url');
7
6
  var childProcess = require('child_process');
7
+ var url = require('url');
8
8
  var readline = require('readline');
9
9
  var crypto = require('crypto');
10
10
  var os = require('os');
@@ -39,130 +39,6 @@ var os__namespace = /*#__PURE__*/_interopNamespace(os);
39
39
  var ts__default = /*#__PURE__*/_interopDefault(ts);
40
40
 
41
41
  // src/cli.ts
42
- function side(manifest) {
43
- return {
44
- generatedAt: manifest.generatedAt,
45
- fingerprint: manifest.fingerprint,
46
- fileCount: manifest.counts.totalFiles,
47
- partsCount: manifest.counts.partsCount
48
- };
49
- }
50
- function hashMap(manifest) {
51
- const map = /* @__PURE__ */ new Map();
52
- for (const file of manifest.files) {
53
- if (file && typeof file.relativePath === "string") {
54
- map.set(file.relativePath, file.hashSha256);
55
- }
56
- }
57
- return map;
58
- }
59
- function manifestProblem(value) {
60
- if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
61
- const candidate = value;
62
- if (!Array.isArray(candidate.files)) return 'no "files" array';
63
- for (const entry of candidate.files) {
64
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
65
- return 'a "files" entry is not an object';
66
- }
67
- if (typeof entry.relativePath !== "string") {
68
- return 'a "files" entry has no "relativePath"';
69
- }
70
- }
71
- const counts = candidate.counts;
72
- if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
73
- return 'no "counts" object';
74
- }
75
- for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
76
- if (typeof counts[key] !== "number") {
77
- return `no numeric "counts.${key}"`;
78
- }
79
- }
80
- if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
81
- if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
82
- return null;
83
- }
84
- function assertUsableManifest(value, label) {
85
- const problem = manifestProblem(value);
86
- if (problem) {
87
- throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
88
- }
89
- }
90
- function compareManifests(before, after) {
91
- assertUsableManifest(before, "before");
92
- assertUsableManifest(after, "after");
93
- const left = hashMap(before);
94
- const right = hashMap(after);
95
- const added = [];
96
- const removed = [];
97
- const changed = [];
98
- let unchanged = 0;
99
- for (const [filePath, hash] of right) {
100
- const previous = left.get(filePath);
101
- if (previous === void 0) added.push(filePath);
102
- else if (previous !== hash) changed.push(filePath);
103
- else unchanged += 1;
104
- }
105
- for (const filePath of left.keys()) {
106
- if (!right.has(filePath)) removed.push(filePath);
107
- }
108
- const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
109
- return {
110
- before: side(before),
111
- after: side(after),
112
- added: sort(added),
113
- removed: sort(removed),
114
- changed: sort(changed),
115
- unchanged
116
- };
117
- }
118
- function loadManifest(target) {
119
- let isDirectory;
120
- try {
121
- isDirectory = fs9__namespace.statSync(target).isDirectory();
122
- } catch {
123
- throw new Error(`No snapshot found at ${target}: the path does not exist.`);
124
- }
125
- const manifestPath = isDirectory ? path10__namespace.join(target, "manifest.json") : target;
126
- let raw;
127
- try {
128
- raw = fs9__namespace.readFileSync(manifestPath, "utf8");
129
- } catch {
130
- throw new Error(`Could not read a manifest at ${manifestPath}.`);
131
- }
132
- let parsed;
133
- try {
134
- parsed = JSON.parse(raw);
135
- } catch (err) {
136
- throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
137
- }
138
- const problem = manifestProblem(parsed);
139
- if (problem) {
140
- throw new Error(
141
- `${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
142
- );
143
- }
144
- return parsed;
145
- }
146
- function renderDiffText(diff) {
147
- const lines = [];
148
- lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
149
- lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
150
- lines.push("");
151
- lines.push(` + ${diff.added.length} added`);
152
- lines.push(` - ${diff.removed.length} removed`);
153
- lines.push(` ~ ${diff.changed.length} changed`);
154
- lines.push(` = ${diff.unchanged} unchanged`);
155
- const section = (title, values) => {
156
- if (values.length === 0) return;
157
- lines.push("", title);
158
- for (const value of values.slice(0, 200)) lines.push(` ${value}`);
159
- if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
160
- };
161
- section("added:", diff.added);
162
- section("removed:", diff.removed);
163
- section("changed:", diff.changed);
164
- return lines.join("\n");
165
- }
166
42
  var MANIFEST_FILES = [
167
43
  "package.json",
168
44
  "pyproject.toml",
@@ -1480,11 +1356,19 @@ function isSensitivePath(relativePath) {
1480
1356
  }
1481
1357
  return matchesSensitiveList(posixPath, base);
1482
1358
  }
1483
- function findSecretInPath(relativePath) {
1359
+ var activeSecretRules = null;
1360
+ function setActiveSecretRules(rules) {
1361
+ activeSecretRules = rules === null ? null : [...rules];
1362
+ }
1363
+ function pathScanRules(extraRules) {
1364
+ const fallback = extraRules ?? activeSecretRules ?? [];
1365
+ return [...DEFAULT_SECRET_PATTERNS, ...fallback];
1366
+ }
1367
+ function findSecretInPath(relativePath, extraRules) {
1484
1368
  const posixPath = relativePath.replace(/\\/g, "/");
1485
1369
  const spans = [];
1486
1370
  const rules = [];
1487
- for (const { rule, pattern } of DEFAULT_SECRET_PATTERNS) {
1371
+ for (const { rule, pattern } of pathScanRules(extraRules)) {
1488
1372
  const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
1489
1373
  let match;
1490
1374
  while ((match = scan.exec(posixPath)) !== null) {
@@ -1492,24 +1376,34 @@ function findSecretInPath(relativePath) {
1492
1376
  scan.lastIndex += 1;
1493
1377
  continue;
1494
1378
  }
1495
- spans.push({ start: match.index, end: match.index + match[0].length, rule });
1379
+ spans.push({ start: match.index, end: match.index + match[0].length });
1496
1380
  if (!rules.includes(rule)) rules.push(rule);
1497
1381
  }
1498
1382
  }
1499
1383
  if (spans.length === 0) return null;
1500
1384
  spans.sort((a, b) => a.start - b.start || a.end - b.end);
1385
+ const merged = [];
1386
+ for (const span of spans) {
1387
+ const last = merged[merged.length - 1];
1388
+ if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
1389
+ else merged.push({ start: span.start, end: span.end });
1390
+ }
1501
1391
  let out = "";
1502
1392
  let cursor = 0;
1503
- for (const span of spans) {
1504
- if (span.end <= cursor) continue;
1505
- const start = Math.max(span.start, cursor);
1506
- out += posixPath.slice(cursor, start);
1393
+ for (const span of merged) {
1394
+ out += posixPath.slice(cursor, span.start);
1507
1395
  out += REDACTION_MARKER;
1508
- cursor = Math.min(span.end, posixPath.length);
1396
+ cursor = span.end;
1509
1397
  }
1510
1398
  out += posixPath.slice(cursor);
1511
1399
  return { rules, masked: out };
1512
1400
  }
1401
+ function maskSkipPaths(records, extraRules) {
1402
+ return records.map((record) => {
1403
+ const hit = findSecretInPath(record.path, extraRules);
1404
+ return hit ? { ...record, path: hit.masked } : record;
1405
+ });
1406
+ }
1513
1407
  function globToRegExp(glob) {
1514
1408
  const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1515
1409
  const anchored = normalized.startsWith("/");
@@ -1740,6 +1634,7 @@ function collectFiles(cwd, options = {}) {
1740
1634
  const includePatterns = options.includePatterns ?? [];
1741
1635
  const excludePatterns = options.excludePatterns ?? [];
1742
1636
  const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
1637
+ const pathRules = options.secretRules;
1743
1638
  const isToolOwned = makeToolOwnedDecider(cwd);
1744
1639
  let candidates;
1745
1640
  const skipped = [];
@@ -1775,7 +1670,7 @@ function collectFiles(cwd, options = {}) {
1775
1670
  }
1776
1671
  }
1777
1672
  } catch {
1778
- const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set(), true);
1673
+ const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), true);
1779
1674
  candidates = walked.files;
1780
1675
  skipped.push(...walked.skipped);
1781
1676
  }
@@ -1813,7 +1708,7 @@ function collectFiles(cwd, options = {}) {
1813
1708
  });
1814
1709
  return false;
1815
1710
  }
1816
- const secret = findSecretInPath(file);
1711
+ const secret = findSecretInPath(file, pathRules);
1817
1712
  if (secret) {
1818
1713
  skipped.push({
1819
1714
  path: secret.masked,
@@ -1824,30 +1719,29 @@ function collectFiles(cwd, options = {}) {
1824
1719
  }
1825
1720
  return true;
1826
1721
  }).sort();
1827
- return { files, skipped };
1722
+ return { files, skipped: maskSkipPaths(skipped, pathRules) };
1828
1723
  }
1829
- function walkDirectory(dir, baseDir, visited, isRoot = false) {
1830
- const results = { files: [], skipped: [] };
1831
- let realDir;
1724
+ function rootRealFor(root) {
1832
1725
  try {
1833
- realDir = fs9__namespace.realpathSync(dir);
1726
+ return fs9__namespace.realpathSync(root);
1834
1727
  } catch {
1835
- realDir = dir;
1836
- }
1837
- if (visited.has(realDir)) return results;
1838
- visited.add(realDir);
1839
- let rootReal;
1840
- try {
1841
- rootReal = fs9__namespace.realpathSync(baseDir);
1842
- } catch {
1843
- rootReal = baseDir;
1728
+ return root;
1844
1729
  }
1730
+ }
1731
+ function walkDirectory(dir, baseDir, rootReal, visited, isRoot = false) {
1732
+ const results = { files: [], skipped: [] };
1733
+ if (visited.has(dir)) return results;
1734
+ visited.add(dir);
1845
1735
  const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
1846
1736
  let entries;
1847
1737
  try {
1848
1738
  entries = fs9__namespace.readdirSync(dir, { withFileTypes: true });
1849
1739
  } catch {
1850
- results.skipped.push({ path: dir, reason: "read-error", detail: "directory unreadable" });
1740
+ results.skipped.push({
1741
+ path: path10__namespace.relative(baseDir, dir).replace(/\\/g, "/") || ".",
1742
+ reason: "read-error",
1743
+ detail: "directory unreadable"
1744
+ });
1851
1745
  return results;
1852
1746
  }
1853
1747
  for (const entry of entries) {
@@ -1926,7 +1820,7 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
1926
1820
  isFile = true;
1927
1821
  }
1928
1822
  if (isDirectory) {
1929
- const nested = walkDirectory(fullPath, baseDir, visited);
1823
+ const nested = walkDirectory(fullPath, baseDir, rootReal, visited);
1930
1824
  results.files.push(...nested.files);
1931
1825
  results.skipped.push(...nested.skipped);
1932
1826
  } else if (isFile) {
@@ -1936,7 +1830,132 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
1936
1830
  return results;
1937
1831
  }
1938
1832
 
1939
- // src/config.ts
1833
+ // src/diff.ts
1834
+ function side(manifest) {
1835
+ return {
1836
+ generatedAt: manifest.generatedAt,
1837
+ fingerprint: manifest.fingerprint,
1838
+ fileCount: manifest.counts.totalFiles,
1839
+ partsCount: manifest.counts.partsCount
1840
+ };
1841
+ }
1842
+ function hashMap(manifest) {
1843
+ const map = /* @__PURE__ */ new Map();
1844
+ for (const file of manifest.files) {
1845
+ if (file && typeof file.relativePath === "string") {
1846
+ map.set(file.relativePath, file.hashSha256);
1847
+ }
1848
+ }
1849
+ return map;
1850
+ }
1851
+ function manifestProblem(value) {
1852
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
1853
+ const candidate = value;
1854
+ if (!Array.isArray(candidate.files)) return 'no "files" array';
1855
+ for (const entry of candidate.files) {
1856
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1857
+ return 'a "files" entry is not an object';
1858
+ }
1859
+ if (typeof entry.relativePath !== "string") {
1860
+ return 'a "files" entry has no "relativePath"';
1861
+ }
1862
+ }
1863
+ const counts = candidate.counts;
1864
+ if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
1865
+ return 'no "counts" object';
1866
+ }
1867
+ for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
1868
+ if (typeof counts[key] !== "number") {
1869
+ return `no numeric "counts.${key}"`;
1870
+ }
1871
+ }
1872
+ if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
1873
+ if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
1874
+ return null;
1875
+ }
1876
+ function assertUsableManifest(value, label) {
1877
+ const problem = manifestProblem(value);
1878
+ if (problem) {
1879
+ throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
1880
+ }
1881
+ }
1882
+ function compareManifests(before, after) {
1883
+ assertUsableManifest(before, "before");
1884
+ assertUsableManifest(after, "after");
1885
+ const left = hashMap(before);
1886
+ const right = hashMap(after);
1887
+ const added = [];
1888
+ const removed = [];
1889
+ const changed = [];
1890
+ let unchanged = 0;
1891
+ for (const [filePath, hash] of right) {
1892
+ const previous = left.get(filePath);
1893
+ if (previous === void 0) added.push(filePath);
1894
+ else if (previous !== hash) changed.push(filePath);
1895
+ else unchanged += 1;
1896
+ }
1897
+ for (const filePath of left.keys()) {
1898
+ if (!right.has(filePath)) removed.push(filePath);
1899
+ }
1900
+ const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
1901
+ const mask = (values) => values.map((value) => findSecretInPath(value)?.masked ?? value);
1902
+ return {
1903
+ before: side(before),
1904
+ after: side(after),
1905
+ added: mask(sort(added)),
1906
+ removed: mask(sort(removed)),
1907
+ changed: mask(sort(changed)),
1908
+ unchanged
1909
+ };
1910
+ }
1911
+ function loadManifest(target) {
1912
+ let isDirectory;
1913
+ try {
1914
+ isDirectory = fs9__namespace.statSync(target).isDirectory();
1915
+ } catch {
1916
+ throw new Error(`No snapshot found at ${target}: the path does not exist.`);
1917
+ }
1918
+ const manifestPath = isDirectory ? path10__namespace.join(target, "manifest.json") : target;
1919
+ let raw;
1920
+ try {
1921
+ raw = fs9__namespace.readFileSync(manifestPath, "utf8");
1922
+ } catch {
1923
+ throw new Error(`Could not read a manifest at ${manifestPath}.`);
1924
+ }
1925
+ let parsed;
1926
+ try {
1927
+ parsed = JSON.parse(raw);
1928
+ } catch (err) {
1929
+ throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
1930
+ }
1931
+ const problem = manifestProblem(parsed);
1932
+ if (problem) {
1933
+ throw new Error(
1934
+ `${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
1935
+ );
1936
+ }
1937
+ return parsed;
1938
+ }
1939
+ function renderDiffText(diff) {
1940
+ const lines = [];
1941
+ lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
1942
+ lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
1943
+ lines.push("");
1944
+ lines.push(` + ${diff.added.length} added`);
1945
+ lines.push(` - ${diff.removed.length} removed`);
1946
+ lines.push(` ~ ${diff.changed.length} changed`);
1947
+ lines.push(` = ${diff.unchanged} unchanged`);
1948
+ const section = (title, values) => {
1949
+ if (values.length === 0) return;
1950
+ lines.push("", title);
1951
+ for (const value of values.slice(0, 200)) lines.push(` ${value}`);
1952
+ if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
1953
+ };
1954
+ section("added:", diff.added);
1955
+ section("removed:", diff.removed);
1956
+ section("changed:", diff.changed);
1957
+ return lines.join("\n");
1958
+ }
1940
1959
  var CONFIG_FILENAMES = [
1941
1960
  "snapshot.config.ts",
1942
1961
  "snapshot.config.js",
@@ -2514,6 +2533,7 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
2514
2533
  ...aliasedRules ?? []
2515
2534
  ]);
2516
2535
  const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
2536
+ setActiveSecretRules(secretRules);
2517
2537
  const cliMonolith = cliOptions?.includeMonolith;
2518
2538
  const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
2519
2539
  const artifacts = resolveArtifacts(
@@ -2815,18 +2835,22 @@ function readManifest2(config) {
2815
2835
  function explainPath(config, target) {
2816
2836
  const relative5 = normalizeTarget(config.cwd, target);
2817
2837
  const absolute = path10__namespace.join(config.cwd, relative5);
2838
+ const maskedPath2 = findSecretInPath(relative5, config.secretRules)?.masked ?? relative5;
2818
2839
  const result = {
2819
- path: relative5,
2840
+ path: maskedPath2,
2820
2841
  exists: fs9__namespace.existsSync(absolute),
2821
2842
  included: false
2822
2843
  };
2823
2844
  const collection = collectFiles(config.cwd, {
2824
2845
  includePatterns: config.include,
2825
2846
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
2826
- includeSensitiveFiles: config.includeSensitiveFiles
2847
+ includeSensitiveFiles: config.includeSensitiveFiles,
2848
+ secretRules: config.secretRules
2827
2849
  });
2828
2850
  if (!collection.files.includes(relative5)) {
2829
- const record = collection.skipped.find((entry2) => entry2.path === relative5);
2851
+ const record = collection.skipped.find(
2852
+ (entry2) => entry2.path === relative5 || entry2.path === maskedPath2
2853
+ );
2830
2854
  if (record) {
2831
2855
  result.reason = record.reason;
2832
2856
  result.detail = record.detail;
@@ -2863,7 +2887,7 @@ function explainPath(config, target) {
2863
2887
  result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
2864
2888
  }
2865
2889
  const manifest = readManifest2(config);
2866
- const entry = manifest?.files.find((file) => file.relativePath === relative5);
2890
+ const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative5) : void 0;
2867
2891
  result.part = entry?.part ?? null;
2868
2892
  result.inclusionMode = entry?.inclusionMode;
2869
2893
  return result;
@@ -4434,7 +4458,8 @@ function summarizeMarkdown(content) {
4434
4458
  }
4435
4459
 
4436
4460
  // src/generator/audit.ts
4437
- function generateAuditReport(manifest, secretFindings) {
4461
+ function generateAuditReport(manifest, secretFindings, secretRules) {
4462
+ const mask = (value) => findSecretInPath(value, secretRules)?.masked ?? value;
4438
4463
  const lines = [
4439
4464
  "# Codebase Health & Security Audit",
4440
4465
  "",
@@ -4469,7 +4494,7 @@ function generateAuditReport(manifest, secretFindings) {
4469
4494
  "| :--- | :--- | :--- |"
4470
4495
  );
4471
4496
  for (const finding of secretFindings) {
4472
- lines.push(`| \`${finding.path ?? "unknown"}\` | ${finding.line} | \`${finding.rule}\` |`);
4497
+ lines.push(`| \`${mask(finding.path ?? "unknown")}\` | ${finding.line} | \`${finding.rule}\` |`);
4473
4498
  }
4474
4499
  lines.push("", "### Matches by rule", "", "| Rule | Matches |", "| :--- | :--- |");
4475
4500
  for (const [rule, count] of Array.from(byRule.entries()).sort((a, b) => b[1] - a[1])) {
@@ -4491,7 +4516,7 @@ function generateAuditReport(manifest, secretFindings) {
4491
4516
  for (const file of largeFiles) {
4492
4517
  const sizeKb = (file.sizeBytes / 1024).toFixed(1);
4493
4518
  const status = file.isPreviewTruncated ? "Truncated Head/Tail" : "Full";
4494
- lines.push(`| \`${file.relativePath}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
4519
+ lines.push(`| \`${mask(file.relativePath)}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
4495
4520
  }
4496
4521
  lines.push("");
4497
4522
  }
@@ -4533,7 +4558,7 @@ function generateAuditReport(manifest, secretFindings) {
4533
4558
  "| :--- | :--- | :--- |"
4534
4559
  );
4535
4560
  for (const skip of manifest.skipped) {
4536
- lines.push(`| \`${skip.path}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
4561
+ lines.push(`| \`${mask(skip.path)}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
4537
4562
  }
4538
4563
  lines.push("");
4539
4564
  }
@@ -5024,6 +5049,9 @@ function generateProjectTree(filePaths) {
5024
5049
  const fence = fenceForText(lines.join("\n"), 3);
5025
5050
  return ["# Project Tree", "", `${fence}text`, ...lines, fence, ""].join("\n");
5026
5051
  }
5052
+ function maskedPath(value, secretRules) {
5053
+ return findSecretInPath(value, secretRules)?.masked ?? value;
5054
+ }
5027
5055
  function emptyDrift() {
5028
5056
  return {
5029
5057
  missingInSnapshot: [],
@@ -5094,15 +5122,20 @@ function validateSnapshot(configInput) {
5094
5122
  const skippedSet = new Set(
5095
5123
  manifest.skipped.filter((entry) => entry && typeof entry.path === "string").map((entry) => entry.path)
5096
5124
  );
5125
+ const isSkippedOnDisk = (diskFile) => {
5126
+ if (skippedSet.has(diskFile)) return true;
5127
+ return skippedSet.has(maskedPath(diskFile, config.secretRules));
5128
+ };
5097
5129
  const collection = collectFiles(config.cwd, {
5098
5130
  includePatterns: config.include,
5099
5131
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
5100
- includeSensitiveFiles: config.includeSensitiveFiles
5132
+ includeSensitiveFiles: config.includeSensitiveFiles,
5133
+ secretRules: config.secretRules
5101
5134
  });
5102
5135
  for (const diskFile of collection.files) {
5103
5136
  const snapshotHash = manifestFileMap.get(diskFile);
5104
5137
  if (!snapshotHash) {
5105
- if (skippedSet.has(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
5138
+ if (isSkippedOnDisk(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
5106
5139
  drift.missingInSnapshot.push(diskFile);
5107
5140
  continue;
5108
5141
  }
@@ -5371,10 +5404,19 @@ function validateSnapshot(configInput) {
5371
5404
  `${drift.configMismatches.length} configuration mismatch(es) between the manifest and this run.`
5372
5405
  );
5373
5406
  }
5407
+ const maskedDrift = {
5408
+ ...drift,
5409
+ missingInSnapshot: drift.missingInSnapshot.map((value) => maskedPath(value, config.secretRules)),
5410
+ missingOnDisk: drift.missingOnDisk.map((value) => maskedPath(value, config.secretRules)),
5411
+ hashMismatches: drift.hashMismatches.map((mismatch) => ({
5412
+ ...mismatch,
5413
+ path: maskedPath(mismatch.path, config.secretRules)
5414
+ }))
5415
+ };
5374
5416
  return {
5375
5417
  valid: errors.length === 0,
5376
5418
  manifestPresent: true,
5377
- drift,
5419
+ drift: maskedDrift,
5378
5420
  errors,
5379
5421
  warnings
5380
5422
  };
@@ -5382,7 +5424,7 @@ function validateSnapshot(configInput) {
5382
5424
 
5383
5425
  // src/index.ts
5384
5426
  var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
5385
- var GENERATOR_VERSION = "4.0.0";
5427
+ var GENERATOR_VERSION = "4.0.1";
5386
5428
  var SCHEMA_VERSION = "3.0.0";
5387
5429
  function sha256Hex2(input) {
5388
5430
  return crypto__namespace.createHash("sha256").update(input).digest("hex");
@@ -5419,7 +5461,6 @@ function bump(counts, key) {
5419
5461
  }
5420
5462
  var RELATIVE_SPECIFIER = /^\.{1,2}\//;
5421
5463
  var IMPORT_CANDIDATE_SUFFIXES = [
5422
- "",
5423
5464
  ".ts",
5424
5465
  ".tsx",
5425
5466
  ".mts",
@@ -5433,23 +5474,59 @@ var IMPORT_CANDIDATE_SUFFIXES = [
5433
5474
  "/index.js",
5434
5475
  "/index.mjs"
5435
5476
  ];
5436
- function resolveSpecifier(fromFile, specifier, knownFiles) {
5477
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
5478
+ ".ts",
5479
+ ".tsx",
5480
+ ".mts",
5481
+ ".cts",
5482
+ ".js",
5483
+ ".jsx",
5484
+ ".mjs",
5485
+ ".cjs"
5486
+ ]);
5487
+ function buildImportResolver(files) {
5488
+ const byPath = /* @__PURE__ */ new Map();
5489
+ const byPathFolded = /* @__PURE__ */ new Map();
5490
+ const byStem = /* @__PURE__ */ new Map();
5491
+ const byStemFolded = /* @__PURE__ */ new Map();
5492
+ const remember = (map, key, value) => {
5493
+ if (!map.has(key)) map.set(key, value);
5494
+ };
5495
+ for (const file of files) {
5496
+ const relativePath = file.relativePath;
5497
+ remember(byPath, relativePath, relativePath);
5498
+ remember(byPathFolded, relativePath.toLowerCase(), relativePath);
5499
+ const extension = path10__namespace.posix.extname(relativePath);
5500
+ if (!SOURCE_EXTENSIONS.has(extension)) continue;
5501
+ const stem = relativePath.slice(0, -extension.length);
5502
+ remember(byStem, stem, relativePath);
5503
+ remember(byStemFolded, stem.toLowerCase(), relativePath);
5504
+ if (path10__namespace.posix.basename(stem) === "index") {
5505
+ const directory = path10__namespace.posix.dirname(stem);
5506
+ remember(byStem, directory, relativePath);
5507
+ remember(byStemFolded, directory.toLowerCase(), relativePath);
5508
+ }
5509
+ }
5510
+ return { byPath, byPathFolded, byStem, byStemFolded };
5511
+ }
5512
+ function resolveSpecifier(fromFile, specifier, resolver) {
5437
5513
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
5438
- const baseDir = path10__namespace.posix.dirname(fromFile);
5439
- const joined = path10__namespace.posix.normalize(path10__namespace.posix.join(baseDir, specifier));
5440
- const candidate = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
5514
+ const joined = path10__namespace.posix.normalize(
5515
+ path10__namespace.posix.join(path10__namespace.posix.dirname(fromFile), specifier)
5516
+ );
5517
+ const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
5518
+ const direct = exact(joined);
5519
+ if (direct) return direct;
5441
5520
  for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
5442
- const withSuffix = `${joined}${suffix}`;
5443
- if (knownFiles.has(withSuffix)) return withSuffix;
5521
+ const hit = exact(`${joined}${suffix}`);
5522
+ if (hit) return hit;
5444
5523
  }
5445
- const extensionSwap = new RegExp(`${candidate}\\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$`);
5446
- for (const file of knownFiles) {
5447
- if (extensionSwap.test(file)) return file;
5448
- }
5449
- return null;
5524
+ const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
5525
+ const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
5526
+ return byStem(joined) ?? byStem(stem);
5450
5527
  }
5451
5528
  function buildIndexes(files) {
5452
- const knownFiles = new Set(files.map((file) => file.relativePath));
5529
+ const resolver = buildImportResolver(files);
5453
5530
  const importerCounts = /* @__PURE__ */ new Map();
5454
5531
  const byFile = files.filter((file) => isSourceSummary(file.summary)).map((file) => {
5455
5532
  const summary = file.summary;
@@ -5461,7 +5538,7 @@ function buildIndexes(files) {
5461
5538
  });
5462
5539
  for (const entry of byFile) {
5463
5540
  for (const specifier of entry.imports) {
5464
- const resolved = resolveSpecifier(entry.path, specifier, knownFiles);
5541
+ const resolved = resolveSpecifier(entry.path, specifier, resolver);
5465
5542
  if (resolved) importerCounts.set(resolved, (importerCounts.get(resolved) ?? 0) + 1);
5466
5543
  }
5467
5544
  }
@@ -5552,15 +5629,22 @@ function resolveSafeOutputDirs(config) {
5552
5629
  return { outDir, tmpDir };
5553
5630
  }
5554
5631
  var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
5555
- var STAGING_ENTRIES = /* @__PURE__ */ new Set([
5556
- ".codebase-snapshot",
5557
- "manifest.json",
5558
- "README.md",
5559
- "parts",
5560
- "reports",
5561
- "full-codebase.json",
5562
- "full-codebase.md"
5563
- ]);
5632
+ var RUN_DIR_PREFIX = "run-";
5633
+ var STALE_RUN_MS = 60 * 60 * 1e3;
5634
+ function maskPathText(value, secretRules) {
5635
+ return findSecretInPath(value, secretRules)?.masked ?? value;
5636
+ }
5637
+ function maskSkipRecords(records, secretRules) {
5638
+ return maskSkipPaths(records, secretRules);
5639
+ }
5640
+ function isProcessAlive(pid) {
5641
+ try {
5642
+ process.kill(pid, 0);
5643
+ return true;
5644
+ } catch (err) {
5645
+ return err.code !== "ESRCH";
5646
+ }
5647
+ }
5564
5648
  var OWNERSHIP_MARKER = ".codebase-snapshot";
5565
5649
  var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
5566
5650
  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;
@@ -5590,6 +5674,7 @@ function markerPayload(dir, state, id, createdAt) {
5590
5674
  createdAt,
5591
5675
  state,
5592
5676
  id,
5677
+ pid: process.pid,
5593
5678
  entries: listEntries(dir)
5594
5679
  };
5595
5680
  const manifestPath = path10__namespace.join(dir, "manifest.json");
@@ -5653,6 +5738,7 @@ function readOwnershipMarker(dir) {
5653
5738
  createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
5654
5739
  state: candidate.state,
5655
5740
  id: candidate.id,
5741
+ pid: typeof candidate.pid === "number" ? candidate.pid : void 0,
5656
5742
  manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
5657
5743
  entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
5658
5744
  },
@@ -5760,7 +5846,7 @@ function describeEntries(entries, limit = 5) {
5760
5846
  const shown = entries.slice(0, limit).join(", ");
5761
5847
  return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
5762
5848
  }
5763
- function assertReplaceable(target, label) {
5849
+ function assertReplaceable(target, label, secretRules) {
5764
5850
  if (!fs9__namespace.existsSync(target)) return;
5765
5851
  if (!fs9__namespace.statSync(target).isDirectory()) {
5766
5852
  throw new Error(
@@ -5769,34 +5855,89 @@ function assertReplaceable(target, label) {
5769
5855
  }
5770
5856
  const entries = fs9__namespace.readdirSync(target);
5771
5857
  if (entries.length === 0) return;
5772
- if (label === "outDir" && entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
5773
- const { marker: marker2, problem } = readOwnershipMarker(target);
5774
- if (label === "tmpDir") {
5775
- if (!marker2) {
5776
- throw new Error(
5777
- `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.`
5778
- );
5779
- }
5780
- const unexpected = entries.filter((name) => !STAGING_ENTRIES.has(name));
5781
- if (unexpected.length > 0) {
5782
- throw new Error(
5783
- `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.`
5784
- );
5785
- }
5786
- return;
5787
- }
5858
+ if (entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
5859
+ const { problem } = readOwnershipMarker(target);
5788
5860
  const verdict = assessSnapshotDirectory(target);
5789
5861
  if (verdict.owned) return;
5862
+ const shownForeign = verdict.foreignEntries.map((name) => maskPathText(name, secretRules));
5863
+ const shownEntries = entries.map((name) => maskPathText(name, secretRules));
5790
5864
  if (verdict.foreignEntries.length > 0) {
5791
5865
  throw new Error(
5792
- `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.`
5866
+ `Refusing to replace ${label} ${target}: it carries this generator's ownership marker, but these entries are not part of the snapshot it recorded: ${describeEntries(shownForeign)}. Remove them yourself, or point ${label} at a new or empty directory.`
5793
5867
  );
5794
5868
  }
5795
5869
  const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
5796
5870
  throw new Error(
5797
- `Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(entries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
5871
+ `Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(shownEntries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
5798
5872
  );
5799
5873
  }
5874
+ function assertStagingRootUsable(tmpDir, secretRules) {
5875
+ if (!fs9__namespace.existsSync(tmpDir)) return;
5876
+ if (!fs9__namespace.statSync(tmpDir).isDirectory()) {
5877
+ throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
5878
+ }
5879
+ const entries = fs9__namespace.readdirSync(tmpDir);
5880
+ if (entries.length === 0) return;
5881
+ const foreign = entries.filter((name) => {
5882
+ const full = path10__namespace.join(tmpDir, name);
5883
+ if (!name.startsWith(RUN_DIR_PREFIX)) return true;
5884
+ let stats;
5885
+ try {
5886
+ stats = fs9__namespace.statSync(full);
5887
+ } catch {
5888
+ return true;
5889
+ }
5890
+ if (!stats.isDirectory()) return true;
5891
+ const { marker: marker2 } = readOwnershipMarker(full);
5892
+ if (marker2) return false;
5893
+ try {
5894
+ return fs9__namespace.readdirSync(full).length > 0;
5895
+ } catch {
5896
+ return true;
5897
+ }
5898
+ });
5899
+ if (foreign.length > 0) {
5900
+ const shown = describeEntries(foreign.map((name) => maskPathText(name, secretRules)));
5901
+ throw new Error(
5902
+ `Refusing to replace tmpDir ${tmpDir}: it holds ${shown}, which this generator did not write. Point tmpDir at a new or empty directory, or remove those files yourself.`
5903
+ );
5904
+ }
5905
+ }
5906
+ function cleanStaleRunDirs(tmpDir) {
5907
+ let entries;
5908
+ try {
5909
+ entries = fs9__namespace.readdirSync(tmpDir);
5910
+ } catch {
5911
+ return;
5912
+ }
5913
+ for (const name of entries) {
5914
+ if (!name.startsWith(RUN_DIR_PREFIX)) continue;
5915
+ const full = path10__namespace.join(tmpDir, name);
5916
+ const { marker: marker2 } = readOwnershipMarker(full);
5917
+ if (marker2) {
5918
+ const created = Date.parse(marker2.createdAt);
5919
+ const aged = !Number.isFinite(created) || Date.now() - created > STALE_RUN_MS;
5920
+ const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
5921
+ if (aged || abandoned) {
5922
+ try {
5923
+ fs9__namespace.rmSync(full, { recursive: true, force: true });
5924
+ } catch {
5925
+ }
5926
+ }
5927
+ continue;
5928
+ }
5929
+ try {
5930
+ if (fs9__namespace.readdirSync(full).length === 0) fs9__namespace.rmSync(full, { recursive: true, force: true });
5931
+ } catch {
5932
+ }
5933
+ }
5934
+ }
5935
+ function removeIfEmpty(dir) {
5936
+ try {
5937
+ if (fs9__namespace.readdirSync(dir).length === 0) fs9__namespace.rmSync(dir, { recursive: true, force: true });
5938
+ } catch {
5939
+ }
5940
+ }
5800
5941
  function ensureParentDirectory(target, label) {
5801
5942
  const parent = path10__namespace.dirname(target);
5802
5943
  if (fs9__namespace.existsSync(parent) && !fs9__namespace.statSync(parent).isDirectory()) {
@@ -5812,14 +5953,12 @@ function ensureParentDirectory(target, label) {
5812
5953
  );
5813
5954
  }
5814
5955
  }
5815
- function acquireRunLock(outDir) {
5816
- const lockPath = path10__namespace.join(
5817
- os__namespace.tmpdir(),
5818
- `codebase-snapshot-${sha256Hex2(outDir).slice(0, 16)}.lock`
5819
- );
5956
+ function acquireRunLock(stagingRoot) {
5957
+ const key = lockKeyFor(stagingRoot);
5958
+ const lockPath = path10__namespace.join(os__namespace.tmpdir(), `codebase-snapshot-${key}.lock`);
5820
5959
  const payload = `${JSON.stringify({
5821
5960
  pid: process.pid,
5822
- outDir,
5961
+ stagingRoot,
5823
5962
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
5824
5963
  })}
5825
5964
  `;
@@ -5844,11 +5983,20 @@ function acquireRunLock(outDir) {
5844
5983
  continue;
5845
5984
  }
5846
5985
  throw new Error(
5847
- `Another generate run is already writing ${outDir}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
5986
+ `Another generate run is already using the staging directory ${stagingRoot}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
5848
5987
  );
5849
5988
  }
5850
5989
  }
5851
- throw new Error(`Could not acquire the generate lock for ${outDir}.`);
5990
+ throw new Error(`Could not acquire the generate lock for ${stagingRoot}.`);
5991
+ }
5992
+ function lockKeyFor(stagingRoot) {
5993
+ let key = stagingRoot;
5994
+ try {
5995
+ key = fs9__namespace.realpathSync(stagingRoot);
5996
+ } catch {
5997
+ }
5998
+ if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
5999
+ return sha256Hex2(key).slice(0, 16);
5852
6000
  }
5853
6001
  function lockOwnerIsGone(lockPath) {
5854
6002
  let raw;
@@ -5884,18 +6032,32 @@ function writeManifestAtomically(manifestPath, manifest) {
5884
6032
  throw err;
5885
6033
  }
5886
6034
  }
5887
- function swapIntoPlace(tmpDir, outDir) {
6035
+ function swapIntoPlace(stagingDir, outDir) {
5888
6036
  ensureParentDirectory(outDir, "outDir");
6037
+ const deadline = Date.now() + 3e3;
6038
+ for (let attempt = 0; ; attempt += 1) {
6039
+ try {
6040
+ swapOnce(stagingDir, outDir);
6041
+ return;
6042
+ } catch (err) {
6043
+ const code = err.code;
6044
+ const racy = code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM";
6045
+ if (!racy || attempt >= 10 || Date.now() > deadline) throw err;
6046
+ sleepSync(50);
6047
+ }
6048
+ }
6049
+ }
6050
+ function swapOnce(stagingDir, outDir) {
5889
6051
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
5890
6052
  const hadPrevious = fs9__namespace.existsSync(outDir);
5891
6053
  if (hadPrevious) fs9__namespace.renameSync(outDir, backupDir);
5892
6054
  try {
5893
6055
  try {
5894
- fs9__namespace.renameSync(tmpDir, outDir);
6056
+ fs9__namespace.renameSync(stagingDir, outDir);
5895
6057
  } catch (err) {
5896
6058
  if (err.code === "EXDEV") {
5897
- fs9__namespace.cpSync(tmpDir, outDir, { recursive: true });
5898
- fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
6059
+ fs9__namespace.cpSync(stagingDir, outDir, { recursive: true });
6060
+ fs9__namespace.rmSync(stagingDir, { recursive: true, force: true });
5899
6061
  } else {
5900
6062
  throw err;
5901
6063
  }
@@ -5909,6 +6071,10 @@ function swapIntoPlace(tmpDir, outDir) {
5909
6071
  throw err;
5910
6072
  }
5911
6073
  }
6074
+ function sleepSync(milliseconds) {
6075
+ const buffer = new Int32Array(new SharedArrayBuffer(4));
6076
+ Atomics.wait(buffer, 0, 0, milliseconds);
6077
+ }
5912
6078
  function artifactWarnings(artifacts) {
5913
6079
  const messages = [];
5914
6080
  if (!artifacts.manifest) {
@@ -5971,11 +6137,12 @@ function generateSnapshot(configInput, options) {
5971
6137
  const collection = collectFiles(config.cwd, {
5972
6138
  includePatterns: config.include,
5973
6139
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
5974
- includeSensitiveFiles: config.includeSensitiveFiles
6140
+ includeSensitiveFiles: config.includeSensitiveFiles,
6141
+ secretRules: config.secretRules
5975
6142
  });
5976
6143
  const files = [];
5977
6144
  const monolithBodies = /* @__PURE__ */ new Map();
5978
- const skipped = [...collection.skipped];
6145
+ const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
5979
6146
  const secretFindings = [];
5980
6147
  const warnings = [];
5981
6148
  const layerCounts = {};
@@ -6049,12 +6216,12 @@ function generateSnapshot(configInput, options) {
6049
6216
  summaryQuality = outcome?.quality;
6050
6217
  if (isSourceSummary(summary) && summary.parseErrors) {
6051
6218
  warnings.push(
6052
- `Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
6219
+ `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
6053
6220
  );
6054
6221
  }
6055
6222
  } catch (err) {
6056
6223
  warnings.push(
6057
- `Summary skipped for ${relativePath}: ${err.message}`.replace(/\s+/g, " ")
6224
+ `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(/\s+/g, " ")
6058
6225
  );
6059
6226
  }
6060
6227
  files.push({
@@ -6163,12 +6330,12 @@ function generateSnapshot(configInput, options) {
6163
6330
  const { content: _content, ...rest } = file;
6164
6331
  return rest;
6165
6332
  }),
6166
- skipped,
6333
+ skipped: maskSkipRecords(skipped, config.secretRules),
6167
6334
  warnings
6168
6335
  };
6169
6336
  manifest.warnings = [
6170
6337
  ...warnings,
6171
- ...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
6338
+ ...secretFindings.map((finding) => `${finding.rule} at ${maskPathText(finding.path ?? "unknown", config.secretRules)}:${finding.line}`)
6172
6339
  ];
6173
6340
  manifest.totals.warningCount = manifest.warnings.length;
6174
6341
  let monolithJson = null;
@@ -6214,31 +6381,33 @@ function generateSnapshot(configInput, options) {
6214
6381
  manifest.totals.warningCount = manifest.warnings.length;
6215
6382
  const { outDir, tmpDir } = resolveSafeOutputDirs(config);
6216
6383
  const { artifacts } = config;
6217
- assertReplaceable(tmpDir, "tmpDir");
6218
- assertReplaceable(outDir, "outDir");
6384
+ assertReplaceable(outDir, "outDir", config.secretRules);
6385
+ assertStagingRootUsable(tmpDir, config.secretRules);
6219
6386
  if (dryRun) {
6220
6387
  runAfterGenerate(config, manifest);
6221
6388
  return manifest;
6222
6389
  }
6223
- const releaseLock = acquireRunLock(outDir);
6224
- let stagingStarted = false;
6390
+ const releaseLock = acquireRunLock(tmpDir);
6391
+ let stagingDir = null;
6225
6392
  try {
6226
6393
  ensureParentDirectory(outDir, "outDir");
6227
- fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
6228
6394
  fs9__namespace.mkdirSync(tmpDir, { recursive: true });
6229
- stagingStarted = true;
6395
+ cleanStaleRunDirs(tmpDir);
6230
6396
  const snapshotId = crypto__namespace.randomUUID();
6231
6397
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6232
- writeOwnershipMarker(tmpDir, {
6398
+ stagingDir = path10__namespace.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
6399
+ fs9__namespace.mkdirSync(stagingDir);
6400
+ writeOwnershipMarker(stagingDir, {
6233
6401
  generator: GENERATOR_NAME,
6234
6402
  version: GENERATOR_VERSION,
6235
6403
  schemaVersion: SCHEMA_VERSION,
6236
6404
  createdAt,
6237
6405
  state: "staging",
6238
- id: snapshotId
6406
+ id: snapshotId,
6407
+ pid: process.pid
6239
6408
  });
6240
- const partsDir = path10__namespace.join(tmpDir, "parts");
6241
- const reportsDir = path10__namespace.join(tmpDir, "reports");
6409
+ const partsDir = path10__namespace.join(stagingDir, "parts");
6410
+ const reportsDir = path10__namespace.join(stagingDir, "reports");
6242
6411
  if (artifacts.parts) fs9__namespace.mkdirSync(partsDir, { recursive: true });
6243
6412
  if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
6244
6413
  fs9__namespace.mkdirSync(reportsDir, { recursive: true });
@@ -6258,25 +6427,25 @@ function generateSnapshot(configInput, options) {
6258
6427
  if (artifacts.audit) {
6259
6428
  fs9__namespace.writeFileSync(
6260
6429
  path10__namespace.join(reportsDir, "audit.md"),
6261
- generateAuditReport(manifest, secretFindings),
6430
+ generateAuditReport(manifest, secretFindings, config.secretRules),
6262
6431
  "utf8"
6263
6432
  );
6264
6433
  }
6265
6434
  if (artifacts.readme) {
6266
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
6435
+ fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
6267
6436
  }
6268
6437
  if (artifacts.manifest) {
6269
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6438
+ fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6270
6439
  }
6271
6440
  if (monolithJson !== null) {
6272
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
6441
+ fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
6273
6442
  }
6274
6443
  if (monolithMarkdown !== null) {
6275
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
6444
+ fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
6276
6445
  }
6277
- writeOwnershipMarker(tmpDir, markerPayload(tmpDir, "complete", snapshotId, createdAt));
6278
- swapIntoPlace(tmpDir, outDir);
6279
- stagingStarted = false;
6446
+ writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
6447
+ swapIntoPlace(stagingDir, outDir);
6448
+ stagingDir = null;
6280
6449
  const warningsBeforeHook = manifest.warnings.length;
6281
6450
  runAfterGenerate(config, manifest);
6282
6451
  if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
@@ -6285,14 +6454,15 @@ function generateSnapshot(configInput, options) {
6285
6454
  writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
6286
6455
  return manifest;
6287
6456
  } catch (err) {
6288
- if (stagingStarted) {
6457
+ if (stagingDir !== null) {
6289
6458
  try {
6290
- fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
6459
+ fs9__namespace.rmSync(stagingDir, { recursive: true, force: true });
6291
6460
  } catch {
6292
6461
  }
6293
6462
  }
6294
6463
  throw err;
6295
6464
  } finally {
6465
+ removeIfEmpty(tmpDir);
6296
6466
  releaseLock();
6297
6467
  }
6298
6468
  }
@@ -7589,8 +7759,7 @@ ${USAGE}
7589
7759
  warn(`\u274C ${manifestPath} is not a snapshot manifest: ${shapeError}`);
7590
7760
  return 1;
7591
7761
  }
7592
- const validation = validateSnapshot(resolved);
7593
- emit(invocation.format, { manifest, validation }, () => {
7762
+ emit(invocation.format, { manifest, validation: null }, () => {
7594
7763
  info(`
7595
7764
  \u{1F4CA} Codebase Snapshot Report (${resolved.cwd})`);
7596
7765
  info(` - Generated at: ${manifest.generatedAt}`);
@@ -7602,14 +7771,14 @@ ${USAGE}
7602
7771
  ` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
7603
7772
  );
7604
7773
  info(` - Total parts: ${manifest.counts.partsCount}`);
7605
- info(` - Integrity: ${validation.valid ? "\u2705 Clean (no drift)" : "\u274C Drift detected"}`);
7774
+ info(" - Integrity: not checked (run `codebase-snapshot validate`)");
7606
7775
  info(` - Warnings: ${manifest.warnings?.length ?? 0}`);
7607
7776
  info("\n Layers:");
7608
7777
  for (const [layer2, count] of Object.entries(manifest.layers)) {
7609
7778
  info(` - ${layer2.padEnd(16)}: ${count} files`);
7610
7779
  }
7611
7780
  });
7612
- return validation.valid ? 0 : 1;
7781
+ return 0;
7613
7782
  }
7614
7783
  if (invocation.command === "explain") {
7615
7784
  const result = explainPath(resolved, invocation.args[0]);