@ankurhalder/codebase-snapshot 4.0.0 → 4.0.2

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,15 @@ function isSensitivePath(relativePath) {
1480
1356
  }
1481
1357
  return matchesSensitiveList(posixPath, base);
1482
1358
  }
1483
- function findSecretInPath(relativePath) {
1359
+ function pathScanRules(extraRules) {
1360
+ const fallback = extraRules ?? [];
1361
+ return [...DEFAULT_SECRET_PATTERNS, ...fallback];
1362
+ }
1363
+ function findSecretInPath(relativePath, extraRules) {
1484
1364
  const posixPath = relativePath.replace(/\\/g, "/");
1485
1365
  const spans = [];
1486
1366
  const rules = [];
1487
- for (const { rule, pattern } of DEFAULT_SECRET_PATTERNS) {
1367
+ for (const { rule, pattern } of pathScanRules(extraRules)) {
1488
1368
  const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
1489
1369
  let match;
1490
1370
  while ((match = scan.exec(posixPath)) !== null) {
@@ -1492,24 +1372,39 @@ function findSecretInPath(relativePath) {
1492
1372
  scan.lastIndex += 1;
1493
1373
  continue;
1494
1374
  }
1495
- spans.push({ start: match.index, end: match.index + match[0].length, rule });
1375
+ spans.push({ start: match.index, end: match.index + match[0].length });
1496
1376
  if (!rules.includes(rule)) rules.push(rule);
1497
1377
  }
1498
1378
  }
1499
1379
  if (spans.length === 0) return null;
1500
1380
  spans.sort((a, b) => a.start - b.start || a.end - b.end);
1381
+ const merged = [];
1382
+ for (const span of spans) {
1383
+ const last = merged[merged.length - 1];
1384
+ if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
1385
+ else merged.push({ start: span.start, end: span.end });
1386
+ }
1501
1387
  let out = "";
1502
1388
  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);
1389
+ for (const span of merged) {
1390
+ out += posixPath.slice(cursor, span.start);
1507
1391
  out += REDACTION_MARKER;
1508
- cursor = Math.min(span.end, posixPath.length);
1392
+ cursor = span.end;
1509
1393
  }
1510
1394
  out += posixPath.slice(cursor);
1511
1395
  return { rules, masked: out };
1512
1396
  }
1397
+ function maskSkipPaths(records, extraRules) {
1398
+ return records.map((record) => {
1399
+ const hit = findSecretInPath(record.path, extraRules);
1400
+ const detail = record.detail ? findSecretInPath(record.detail, extraRules) : null;
1401
+ return {
1402
+ ...record,
1403
+ path: hit ? hit.masked : record.path,
1404
+ ...detail ? { detail: detail.masked } : {}
1405
+ };
1406
+ });
1407
+ }
1513
1408
  function globToRegExp(glob) {
1514
1409
  const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1515
1410
  const anchored = normalized.startsWith("/");
@@ -1740,6 +1635,7 @@ function collectFiles(cwd, options = {}) {
1740
1635
  const includePatterns = options.includePatterns ?? [];
1741
1636
  const excludePatterns = options.excludePatterns ?? [];
1742
1637
  const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
1638
+ const pathRules = options.secretRules;
1743
1639
  const isToolOwned = makeToolOwnedDecider(cwd);
1744
1640
  let candidates;
1745
1641
  const skipped = [];
@@ -1775,7 +1671,7 @@ function collectFiles(cwd, options = {}) {
1775
1671
  }
1776
1672
  }
1777
1673
  } catch {
1778
- const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set(), true);
1674
+ const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), true);
1779
1675
  candidates = walked.files;
1780
1676
  skipped.push(...walked.skipped);
1781
1677
  }
@@ -1813,7 +1709,7 @@ function collectFiles(cwd, options = {}) {
1813
1709
  });
1814
1710
  return false;
1815
1711
  }
1816
- const secret = findSecretInPath(file);
1712
+ const secret = findSecretInPath(file, pathRules);
1817
1713
  if (secret) {
1818
1714
  skipped.push({
1819
1715
  path: secret.masked,
@@ -1824,30 +1720,29 @@ function collectFiles(cwd, options = {}) {
1824
1720
  }
1825
1721
  return true;
1826
1722
  }).sort();
1827
- return { files, skipped };
1723
+ return { files, skipped: maskSkipPaths(skipped, pathRules) };
1828
1724
  }
1829
- function walkDirectory(dir, baseDir, visited, isRoot = false) {
1830
- const results = { files: [], skipped: [] };
1831
- let realDir;
1725
+ function rootRealFor(root) {
1832
1726
  try {
1833
- realDir = fs9__namespace.realpathSync(dir);
1727
+ return fs9__namespace.realpathSync(root);
1834
1728
  } 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;
1729
+ return root;
1844
1730
  }
1731
+ }
1732
+ function walkDirectory(dir, baseDir, rootReal, visited, isRoot = false) {
1733
+ const results = { files: [], skipped: [] };
1734
+ if (visited.has(dir)) return results;
1735
+ visited.add(dir);
1845
1736
  const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
1846
1737
  let entries;
1847
1738
  try {
1848
1739
  entries = fs9__namespace.readdirSync(dir, { withFileTypes: true });
1849
1740
  } catch {
1850
- results.skipped.push({ path: dir, reason: "read-error", detail: "directory unreadable" });
1741
+ results.skipped.push({
1742
+ path: path10__namespace.relative(baseDir, dir).replace(/\\/g, "/") || ".",
1743
+ reason: "read-error",
1744
+ detail: "directory unreadable"
1745
+ });
1851
1746
  return results;
1852
1747
  }
1853
1748
  for (const entry of entries) {
@@ -1926,7 +1821,7 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
1926
1821
  isFile = true;
1927
1822
  }
1928
1823
  if (isDirectory) {
1929
- const nested = walkDirectory(fullPath, baseDir, visited);
1824
+ const nested = walkDirectory(fullPath, baseDir, rootReal, visited);
1930
1825
  results.files.push(...nested.files);
1931
1826
  results.skipped.push(...nested.skipped);
1932
1827
  } else if (isFile) {
@@ -1936,7 +1831,132 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
1936
1831
  return results;
1937
1832
  }
1938
1833
 
1939
- // src/config.ts
1834
+ // src/diff.ts
1835
+ function side(manifest) {
1836
+ return {
1837
+ generatedAt: manifest.generatedAt,
1838
+ fingerprint: manifest.fingerprint,
1839
+ fileCount: manifest.counts.totalFiles,
1840
+ partsCount: manifest.counts.partsCount
1841
+ };
1842
+ }
1843
+ function hashMap(manifest) {
1844
+ const map = /* @__PURE__ */ new Map();
1845
+ for (const file of manifest.files) {
1846
+ if (file && typeof file.relativePath === "string") {
1847
+ map.set(file.relativePath, file.hashSha256);
1848
+ }
1849
+ }
1850
+ return map;
1851
+ }
1852
+ function manifestProblem(value) {
1853
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
1854
+ const candidate = value;
1855
+ if (!Array.isArray(candidate.files)) return 'no "files" array';
1856
+ for (const entry of candidate.files) {
1857
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1858
+ return 'a "files" entry is not an object';
1859
+ }
1860
+ if (typeof entry.relativePath !== "string") {
1861
+ return 'a "files" entry has no "relativePath"';
1862
+ }
1863
+ }
1864
+ const counts = candidate.counts;
1865
+ if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
1866
+ return 'no "counts" object';
1867
+ }
1868
+ for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
1869
+ if (typeof counts[key] !== "number") {
1870
+ return `no numeric "counts.${key}"`;
1871
+ }
1872
+ }
1873
+ if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
1874
+ if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
1875
+ return null;
1876
+ }
1877
+ function assertUsableManifest(value, label) {
1878
+ const problem = manifestProblem(value);
1879
+ if (problem) {
1880
+ throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
1881
+ }
1882
+ }
1883
+ function compareManifests(before, after) {
1884
+ assertUsableManifest(before, "before");
1885
+ assertUsableManifest(after, "after");
1886
+ const left = hashMap(before);
1887
+ const right = hashMap(after);
1888
+ const added = [];
1889
+ const removed = [];
1890
+ const changed = [];
1891
+ let unchanged = 0;
1892
+ for (const [filePath, hash] of right) {
1893
+ const previous = left.get(filePath);
1894
+ if (previous === void 0) added.push(filePath);
1895
+ else if (previous !== hash) changed.push(filePath);
1896
+ else unchanged += 1;
1897
+ }
1898
+ for (const filePath of left.keys()) {
1899
+ if (!right.has(filePath)) removed.push(filePath);
1900
+ }
1901
+ const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
1902
+ const mask = (values) => values.map((value) => findSecretInPath(value)?.masked ?? value);
1903
+ return {
1904
+ before: side(before),
1905
+ after: side(after),
1906
+ added: mask(sort(added)),
1907
+ removed: mask(sort(removed)),
1908
+ changed: mask(sort(changed)),
1909
+ unchanged
1910
+ };
1911
+ }
1912
+ function loadManifest(target) {
1913
+ let isDirectory;
1914
+ try {
1915
+ isDirectory = fs9__namespace.statSync(target).isDirectory();
1916
+ } catch {
1917
+ throw new Error(`No snapshot found at ${target}: the path does not exist.`);
1918
+ }
1919
+ const manifestPath = isDirectory ? path10__namespace.join(target, "manifest.json") : target;
1920
+ let raw;
1921
+ try {
1922
+ raw = fs9__namespace.readFileSync(manifestPath, "utf8");
1923
+ } catch {
1924
+ throw new Error(`Could not read a manifest at ${manifestPath}.`);
1925
+ }
1926
+ let parsed;
1927
+ try {
1928
+ parsed = JSON.parse(raw);
1929
+ } catch (err) {
1930
+ throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
1931
+ }
1932
+ const problem = manifestProblem(parsed);
1933
+ if (problem) {
1934
+ throw new Error(
1935
+ `${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
1936
+ );
1937
+ }
1938
+ return parsed;
1939
+ }
1940
+ function renderDiffText(diff) {
1941
+ const lines = [];
1942
+ lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
1943
+ lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
1944
+ lines.push("");
1945
+ lines.push(` + ${diff.added.length} added`);
1946
+ lines.push(` - ${diff.removed.length} removed`);
1947
+ lines.push(` ~ ${diff.changed.length} changed`);
1948
+ lines.push(` = ${diff.unchanged} unchanged`);
1949
+ const section = (title, values) => {
1950
+ if (values.length === 0) return;
1951
+ lines.push("", title);
1952
+ for (const value of values.slice(0, 200)) lines.push(` ${value}`);
1953
+ if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
1954
+ };
1955
+ section("added:", diff.added);
1956
+ section("removed:", diff.removed);
1957
+ section("changed:", diff.changed);
1958
+ return lines.join("\n");
1959
+ }
1940
1960
  var CONFIG_FILENAMES = [
1941
1961
  "snapshot.config.ts",
1942
1962
  "snapshot.config.js",
@@ -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.2";
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;
5444
- }
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;
5521
+ const hit = exact(`${joined}${suffix}`);
5522
+ if (hit) return hit;
5448
5523
  }
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({
@@ -6098,6 +6265,7 @@ function generateSnapshot(configInput, options) {
6098
6265
  const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
6099
6266
  const validationErrors = verifyInMemory(files, parts, maxEffectiveBudget);
6100
6267
  const totalContentBytes = files.reduce((total, file) => total + file.includedBytes, 0);
6268
+ const maskName = (value) => maskPathText(value, config.secretRules);
6101
6269
  const manifest = {
6102
6270
  schemaVersion: SCHEMA_VERSION,
6103
6271
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -6118,13 +6286,13 @@ function generateSnapshot(configInput, options) {
6118
6286
  includeMonolith: config.includeMonolith,
6119
6287
  includeSensitiveFiles: config.includeSensitiveFiles,
6120
6288
  artifacts: { ...config.artifacts },
6121
- include: [...config.include],
6289
+ include: [...config.include].map(maskName),
6122
6290
  preset: config.preset,
6123
6291
  layersMode: config.layersMode,
6124
6292
  budgets: { default: config.budgets.default, byLayer: { ...config.budgets.byLayer } },
6125
- excludePatterns: [...config.excludePatterns],
6126
- layers: config.layers.map((layer2) => layer2.name),
6127
- kinds: config.kinds.map((kind) => kind.kind)
6293
+ excludePatterns: [...config.excludePatterns].map(maskName),
6294
+ layers: config.layers.map((layer2) => maskName(layer2.name)),
6295
+ kinds: config.kinds.map((kind) => maskName(kind.kind))
6128
6296
  },
6129
6297
  counts: {
6130
6298
  totalFiles: files.length,
@@ -6163,12 +6331,12 @@ function generateSnapshot(configInput, options) {
6163
6331
  const { content: _content, ...rest } = file;
6164
6332
  return rest;
6165
6333
  }),
6166
- skipped,
6334
+ skipped: maskSkipRecords(skipped, config.secretRules),
6167
6335
  warnings
6168
6336
  };
6169
6337
  manifest.warnings = [
6170
6338
  ...warnings,
6171
- ...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
6339
+ ...secretFindings.map((finding) => `${finding.rule} at ${maskPathText(finding.path ?? "unknown", config.secretRules)}:${finding.line}`)
6172
6340
  ];
6173
6341
  manifest.totals.warningCount = manifest.warnings.length;
6174
6342
  let monolithJson = null;
@@ -6214,31 +6382,33 @@ function generateSnapshot(configInput, options) {
6214
6382
  manifest.totals.warningCount = manifest.warnings.length;
6215
6383
  const { outDir, tmpDir } = resolveSafeOutputDirs(config);
6216
6384
  const { artifacts } = config;
6217
- assertReplaceable(tmpDir, "tmpDir");
6218
- assertReplaceable(outDir, "outDir");
6385
+ assertReplaceable(outDir, "outDir", config.secretRules);
6386
+ assertStagingRootUsable(tmpDir, config.secretRules);
6219
6387
  if (dryRun) {
6220
6388
  runAfterGenerate(config, manifest);
6221
6389
  return manifest;
6222
6390
  }
6223
- const releaseLock = acquireRunLock(outDir);
6224
- let stagingStarted = false;
6391
+ const releaseLock = acquireRunLock(tmpDir);
6392
+ let stagingDir = null;
6225
6393
  try {
6226
6394
  ensureParentDirectory(outDir, "outDir");
6227
- fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
6228
6395
  fs9__namespace.mkdirSync(tmpDir, { recursive: true });
6229
- stagingStarted = true;
6396
+ cleanStaleRunDirs(tmpDir);
6230
6397
  const snapshotId = crypto__namespace.randomUUID();
6231
6398
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6232
- writeOwnershipMarker(tmpDir, {
6399
+ stagingDir = path10__namespace.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
6400
+ fs9__namespace.mkdirSync(stagingDir);
6401
+ writeOwnershipMarker(stagingDir, {
6233
6402
  generator: GENERATOR_NAME,
6234
6403
  version: GENERATOR_VERSION,
6235
6404
  schemaVersion: SCHEMA_VERSION,
6236
6405
  createdAt,
6237
6406
  state: "staging",
6238
- id: snapshotId
6407
+ id: snapshotId,
6408
+ pid: process.pid
6239
6409
  });
6240
- const partsDir = path10__namespace.join(tmpDir, "parts");
6241
- const reportsDir = path10__namespace.join(tmpDir, "reports");
6410
+ const partsDir = path10__namespace.join(stagingDir, "parts");
6411
+ const reportsDir = path10__namespace.join(stagingDir, "reports");
6242
6412
  if (artifacts.parts) fs9__namespace.mkdirSync(partsDir, { recursive: true });
6243
6413
  if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
6244
6414
  fs9__namespace.mkdirSync(reportsDir, { recursive: true });
@@ -6258,25 +6428,25 @@ function generateSnapshot(configInput, options) {
6258
6428
  if (artifacts.audit) {
6259
6429
  fs9__namespace.writeFileSync(
6260
6430
  path10__namespace.join(reportsDir, "audit.md"),
6261
- generateAuditReport(manifest, secretFindings),
6431
+ generateAuditReport(manifest, secretFindings, config.secretRules),
6262
6432
  "utf8"
6263
6433
  );
6264
6434
  }
6265
6435
  if (artifacts.readme) {
6266
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
6436
+ fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
6267
6437
  }
6268
6438
  if (artifacts.manifest) {
6269
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6439
+ fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6270
6440
  }
6271
6441
  if (monolithJson !== null) {
6272
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
6442
+ fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
6273
6443
  }
6274
6444
  if (monolithMarkdown !== null) {
6275
- fs9__namespace.writeFileSync(path10__namespace.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
6445
+ fs9__namespace.writeFileSync(path10__namespace.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
6276
6446
  }
6277
- writeOwnershipMarker(tmpDir, markerPayload(tmpDir, "complete", snapshotId, createdAt));
6278
- swapIntoPlace(tmpDir, outDir);
6279
- stagingStarted = false;
6447
+ writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
6448
+ swapIntoPlace(stagingDir, outDir);
6449
+ stagingDir = null;
6280
6450
  const warningsBeforeHook = manifest.warnings.length;
6281
6451
  runAfterGenerate(config, manifest);
6282
6452
  if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
@@ -6285,14 +6455,15 @@ function generateSnapshot(configInput, options) {
6285
6455
  writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
6286
6456
  return manifest;
6287
6457
  } catch (err) {
6288
- if (stagingStarted) {
6458
+ if (stagingDir !== null) {
6289
6459
  try {
6290
- fs9__namespace.rmSync(tmpDir, { recursive: true, force: true });
6460
+ fs9__namespace.rmSync(stagingDir, { recursive: true, force: true });
6291
6461
  } catch {
6292
6462
  }
6293
6463
  }
6294
6464
  throw err;
6295
6465
  } finally {
6466
+ removeIfEmpty(tmpDir);
6296
6467
  releaseLock();
6297
6468
  }
6298
6469
  }
@@ -6777,8 +6948,8 @@ function buildAgentContract() {
6777
6948
  "snapshot.config.mjs",
6778
6949
  "snapshot.config.cjs",
6779
6950
  "snapshot.config.json",
6780
- ".snapshotrc",
6781
6951
  ".snapshotrc.json",
6952
+ ".snapshotrc",
6782
6953
  'package.json \u2192 "snapshot" key'
6783
6954
  ],
6784
6955
  settings: [
@@ -7589,8 +7760,7 @@ ${USAGE}
7589
7760
  warn(`\u274C ${manifestPath} is not a snapshot manifest: ${shapeError}`);
7590
7761
  return 1;
7591
7762
  }
7592
- const validation = validateSnapshot(resolved);
7593
- emit(invocation.format, { manifest, validation }, () => {
7763
+ emit(invocation.format, { manifest, validation: null }, () => {
7594
7764
  info(`
7595
7765
  \u{1F4CA} Codebase Snapshot Report (${resolved.cwd})`);
7596
7766
  info(` - Generated at: ${manifest.generatedAt}`);
@@ -7602,14 +7772,14 @@ ${USAGE}
7602
7772
  ` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
7603
7773
  );
7604
7774
  info(` - Total parts: ${manifest.counts.partsCount}`);
7605
- info(` - Integrity: ${validation.valid ? "\u2705 Clean (no drift)" : "\u274C Drift detected"}`);
7775
+ info(" - Integrity: not checked (run `codebase-snapshot validate`)");
7606
7776
  info(` - Warnings: ${manifest.warnings?.length ?? 0}`);
7607
7777
  info("\n Layers:");
7608
7778
  for (const [layer2, count] of Object.entries(manifest.layers)) {
7609
7779
  info(` - ${layer2.padEnd(16)}: ${count} files`);
7610
7780
  }
7611
7781
  });
7612
- return validation.valid ? 0 : 1;
7782
+ return 0;
7613
7783
  }
7614
7784
  if (invocation.command === "explain") {
7615
7785
  const result = explainPath(resolved, invocation.args[0]);
@@ -7728,6 +7898,8 @@ async function runWatchLoop(invocation, resolved) {
7728
7898
  (segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`) || VCS_SEGMENTS.has(segment)
7729
7899
  );
7730
7900
  let running = false;
7901
+ let quietUntil = 0;
7902
+ const QUIET_MS = 1500;
7731
7903
  const regenerate = async () => {
7732
7904
  if (running) return;
7733
7905
  running = true;
@@ -7765,6 +7937,7 @@ async function runWatchLoop(invocation, resolved) {
7765
7937
  } catch (err) {
7766
7938
  warn(`\u274C ${err.message}`);
7767
7939
  } finally {
7940
+ quietUntil = performance.now() + QUIET_MS;
7768
7941
  running = false;
7769
7942
  }
7770
7943
  };
@@ -7772,6 +7945,7 @@ async function runWatchLoop(invocation, resolved) {
7772
7945
  void regenerate();
7773
7946
  let timer = null;
7774
7947
  const watcher = fs9__namespace.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
7948
+ if (performance.now() < quietUntil) return;
7775
7949
  if (filename) {
7776
7950
  const changed = path10__namespace.resolve(resolved.cwd, filename.toString());
7777
7951
  if (isGenerated(changed)) return;