@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/bin.js CHANGED
@@ -6,145 +6,17 @@ import * as path12 from "path";
6
6
  import { parseArgs } from "util";
7
7
 
8
8
  // src/diff.ts
9
- import * as fs from "fs";
10
- import * as path from "path";
11
- function side(manifest) {
12
- return {
13
- generatedAt: manifest.generatedAt,
14
- fingerprint: manifest.fingerprint,
15
- fileCount: manifest.counts.totalFiles,
16
- partsCount: manifest.counts.partsCount
17
- };
18
- }
19
- function hashMap(manifest) {
20
- const map = /* @__PURE__ */ new Map();
21
- for (const file of manifest.files) {
22
- if (file && typeof file.relativePath === "string") {
23
- map.set(file.relativePath, file.hashSha256);
24
- }
25
- }
26
- return map;
27
- }
28
- function manifestProblem(value) {
29
- if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
30
- const candidate = value;
31
- if (!Array.isArray(candidate.files)) return 'no "files" array';
32
- for (const entry of candidate.files) {
33
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
34
- return 'a "files" entry is not an object';
35
- }
36
- if (typeof entry.relativePath !== "string") {
37
- return 'a "files" entry has no "relativePath"';
38
- }
39
- }
40
- const counts = candidate.counts;
41
- if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
42
- return 'no "counts" object';
43
- }
44
- for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
45
- if (typeof counts[key] !== "number") {
46
- return `no numeric "counts.${key}"`;
47
- }
48
- }
49
- if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
50
- if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
51
- return null;
52
- }
53
- function assertUsableManifest(value, label) {
54
- const problem = manifestProblem(value);
55
- if (problem) {
56
- throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
57
- }
58
- }
59
- function compareManifests(before, after) {
60
- assertUsableManifest(before, "before");
61
- assertUsableManifest(after, "after");
62
- const left = hashMap(before);
63
- const right = hashMap(after);
64
- const added = [];
65
- const removed = [];
66
- const changed = [];
67
- let unchanged = 0;
68
- for (const [filePath, hash] of right) {
69
- const previous = left.get(filePath);
70
- if (previous === void 0) added.push(filePath);
71
- else if (previous !== hash) changed.push(filePath);
72
- else unchanged += 1;
73
- }
74
- for (const filePath of left.keys()) {
75
- if (!right.has(filePath)) removed.push(filePath);
76
- }
77
- const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
78
- return {
79
- before: side(before),
80
- after: side(after),
81
- added: sort(added),
82
- removed: sort(removed),
83
- changed: sort(changed),
84
- unchanged
85
- };
86
- }
87
- function loadManifest(target) {
88
- let isDirectory;
89
- try {
90
- isDirectory = fs.statSync(target).isDirectory();
91
- } catch {
92
- throw new Error(`No snapshot found at ${target}: the path does not exist.`);
93
- }
94
- const manifestPath = isDirectory ? path.join(target, "manifest.json") : target;
95
- let raw;
96
- try {
97
- raw = fs.readFileSync(manifestPath, "utf8");
98
- } catch {
99
- throw new Error(`Could not read a manifest at ${manifestPath}.`);
100
- }
101
- let parsed;
102
- try {
103
- parsed = JSON.parse(raw);
104
- } catch (err) {
105
- throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
106
- }
107
- const problem = manifestProblem(parsed);
108
- if (problem) {
109
- throw new Error(
110
- `${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
111
- );
112
- }
113
- return parsed;
114
- }
115
- function renderDiffText(diff) {
116
- const lines = [];
117
- lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
118
- lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
119
- lines.push("");
120
- lines.push(` + ${diff.added.length} added`);
121
- lines.push(` - ${diff.removed.length} removed`);
122
- lines.push(` ~ ${diff.changed.length} changed`);
123
- lines.push(` = ${diff.unchanged} unchanged`);
124
- const section = (title, values) => {
125
- if (values.length === 0) return;
126
- lines.push("", title);
127
- for (const value of values.slice(0, 200)) lines.push(` ${value}`);
128
- if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
129
- };
130
- section("added:", diff.added);
131
- section("removed:", diff.removed);
132
- section("changed:", diff.changed);
133
- return lines.join("\n");
134
- }
135
-
136
- // src/explain.ts
137
- import * as fs6 from "fs";
138
- import * as path7 from "path";
139
-
140
- // src/config.ts
141
- import * as fs4 from "fs";
142
- import * as path4 from "path";
143
- import { pathToFileURL } from "url";
9
+ import * as fs3 from "fs";
10
+ import * as path3 from "path";
144
11
 
145
- // src/presets/context.ts
12
+ // src/scanner/files.ts
13
+ import * as childProcess from "child_process";
146
14
  import * as fs2 from "fs";
147
15
  import * as path2 from "path";
16
+
17
+ // src/presets/context.ts
18
+ import * as fs from "fs";
19
+ import * as path from "path";
148
20
  var MANIFEST_FILES = [
149
21
  "package.json",
150
22
  "pyproject.toml",
@@ -160,11 +32,11 @@ var MANIFEST_FILES = [
160
32
  ];
161
33
  var MANIFEST_MAX_BYTES = 1e5;
162
34
  function readManifest(cwd, name) {
163
- const full = path2.join(cwd, name);
35
+ const full = path.join(cwd, name);
164
36
  try {
165
- const stats = fs2.statSync(full);
37
+ const stats = fs.statSync(full);
166
38
  if (!stats.isFile() || stats.size > MANIFEST_MAX_BYTES) return null;
167
- return fs2.readFileSync(full, "utf8");
39
+ return fs.readFileSync(full, "utf8");
168
40
  } catch {
169
41
  return null;
170
42
  }
@@ -189,7 +61,7 @@ function buildPresetContext(cwd) {
189
61
  const files = /* @__PURE__ */ new Set();
190
62
  const dirs = /* @__PURE__ */ new Set();
191
63
  try {
192
- for (const entry of fs2.readdirSync(cwd, { withFileTypes: true })) {
64
+ for (const entry of fs.readdirSync(cwd, { withFileTypes: true })) {
193
65
  if (entry.isDirectory()) dirs.add(entry.name);
194
66
  else if (entry.isFile()) files.add(entry.name);
195
67
  }
@@ -984,11 +856,6 @@ function detectPreset(cwd, forced) {
984
856
  return detectPresetFromContext(buildPresetContext(cwd), forced);
985
857
  }
986
858
 
987
- // src/scanner/files.ts
988
- import * as childProcess from "child_process";
989
- import * as fs3 from "fs";
990
- import * as path3 from "path";
991
-
992
859
  // src/security/patterns.ts
993
860
  var STRONG_CREDENTIAL_NAME = "(?:password|passwd|passphrase|pwd|api[_-]?key|api[_-]?secret|secret[_-]?access[_-]?key|secret[_-]?key|client[_-]?secret[_-]?key|client[_-]?secret|private[_-]?key|aws[_-]?secret[_-]?access[_-]?key|access[_-]?token|refresh[_-]?token|auth[_-]?token|session[_-]?token|secret|token)";
994
861
  var WEAK_CREDENTIAL_NAME = "(?:pass|id[_-]?token|credentials?)";
@@ -1389,14 +1256,14 @@ var PRESET_IGNORES = new Set(
1389
1256
  );
1390
1257
  function readLinkTarget(linkPath, fallback) {
1391
1258
  try {
1392
- return fs3.readlinkSync(linkPath);
1259
+ return fs2.readlinkSync(linkPath);
1393
1260
  } catch {
1394
1261
  return fallback;
1395
1262
  }
1396
1263
  }
1397
1264
  function isInside(root, candidate) {
1398
- const relative5 = path3.relative(root, candidate);
1399
- return relative5 === "" || !relative5.startsWith(`..${path3.sep}`) && relative5 !== ".." && !path3.isAbsolute(relative5);
1265
+ const relative5 = path2.relative(root, candidate);
1266
+ return relative5 === "" || !relative5.startsWith(`..${path2.sep}`) && relative5 !== ".." && !path2.isAbsolute(relative5);
1400
1267
  }
1401
1268
  function getGitInfo(cwd, fallbackBranch = "main") {
1402
1269
  try {
@@ -1467,11 +1334,15 @@ function isSensitivePath(relativePath) {
1467
1334
  }
1468
1335
  return matchesSensitiveList(posixPath, base);
1469
1336
  }
1470
- function findSecretInPath(relativePath) {
1337
+ function pathScanRules(extraRules) {
1338
+ const fallback = extraRules ?? [];
1339
+ return [...DEFAULT_SECRET_PATTERNS, ...fallback];
1340
+ }
1341
+ function findSecretInPath(relativePath, extraRules) {
1471
1342
  const posixPath = relativePath.replace(/\\/g, "/");
1472
1343
  const spans = [];
1473
1344
  const rules = [];
1474
- for (const { rule, pattern } of DEFAULT_SECRET_PATTERNS) {
1345
+ for (const { rule, pattern } of pathScanRules(extraRules)) {
1475
1346
  const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
1476
1347
  let match;
1477
1348
  while ((match = scan.exec(posixPath)) !== null) {
@@ -1479,24 +1350,39 @@ function findSecretInPath(relativePath) {
1479
1350
  scan.lastIndex += 1;
1480
1351
  continue;
1481
1352
  }
1482
- spans.push({ start: match.index, end: match.index + match[0].length, rule });
1353
+ spans.push({ start: match.index, end: match.index + match[0].length });
1483
1354
  if (!rules.includes(rule)) rules.push(rule);
1484
1355
  }
1485
1356
  }
1486
1357
  if (spans.length === 0) return null;
1487
1358
  spans.sort((a, b) => a.start - b.start || a.end - b.end);
1359
+ const merged = [];
1360
+ for (const span of spans) {
1361
+ const last = merged[merged.length - 1];
1362
+ if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
1363
+ else merged.push({ start: span.start, end: span.end });
1364
+ }
1488
1365
  let out = "";
1489
1366
  let cursor = 0;
1490
- for (const span of spans) {
1491
- if (span.end <= cursor) continue;
1492
- const start = Math.max(span.start, cursor);
1493
- out += posixPath.slice(cursor, start);
1367
+ for (const span of merged) {
1368
+ out += posixPath.slice(cursor, span.start);
1494
1369
  out += REDACTION_MARKER;
1495
- cursor = Math.min(span.end, posixPath.length);
1370
+ cursor = span.end;
1496
1371
  }
1497
1372
  out += posixPath.slice(cursor);
1498
1373
  return { rules, masked: out };
1499
1374
  }
1375
+ function maskSkipPaths(records, extraRules) {
1376
+ return records.map((record) => {
1377
+ const hit = findSecretInPath(record.path, extraRules);
1378
+ const detail = record.detail ? findSecretInPath(record.detail, extraRules) : null;
1379
+ return {
1380
+ ...record,
1381
+ path: hit ? hit.masked : record.path,
1382
+ ...detail ? { detail: detail.masked } : {}
1383
+ };
1384
+ });
1385
+ }
1500
1386
  function globToRegExp(glob) {
1501
1387
  const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1502
1388
  const anchored = normalized.startsWith("/");
@@ -1574,10 +1460,10 @@ function matchesExclude(relativePath, pattern) {
1574
1460
  return posixPath === cleaned || posixPath.startsWith(`${cleaned}/`) || posixPath.includes(`/${cleaned}/`) || posixPath.endsWith(`/${cleaned}`);
1575
1461
  }
1576
1462
  function readGitignoreMatcher(dir) {
1577
- const gitignorePath = path3.join(dir, ".gitignore");
1463
+ const gitignorePath = path2.join(dir, ".gitignore");
1578
1464
  let raw;
1579
1465
  try {
1580
- raw = fs3.readFileSync(gitignorePath, "utf8");
1466
+ raw = fs2.readFileSync(gitignorePath, "utf8");
1581
1467
  } catch {
1582
1468
  return () => false;
1583
1469
  }
@@ -1639,7 +1525,7 @@ function isToolOwnedDirName(name) {
1639
1525
  }
1640
1526
  function hasSnapshotMarker(target) {
1641
1527
  try {
1642
- return fs3.statSync(target).isDirectory() && fs3.existsSync(path3.join(target, ".codebase-snapshot"));
1528
+ return fs2.statSync(target).isDirectory() && fs2.existsSync(path2.join(target, ".codebase-snapshot"));
1643
1529
  } catch {
1644
1530
  return false;
1645
1531
  }
@@ -1653,7 +1539,7 @@ function makeToolOwnedDecider(cwd) {
1653
1539
  if (depth === 1 && isToolOwnedDirName(prefix)) return true;
1654
1540
  let hasMarker = markers.get(prefix);
1655
1541
  if (hasMarker === void 0) {
1656
- hasMarker = fs3.existsSync(path3.join(cwd, prefix, ".codebase-snapshot"));
1542
+ hasMarker = fs2.existsSync(path2.join(cwd, prefix, ".codebase-snapshot"));
1657
1543
  markers.set(prefix, hasMarker);
1658
1544
  }
1659
1545
  if (hasMarker) return true;
@@ -1667,7 +1553,7 @@ function makeLinkProbe() {
1667
1553
  let known = cache.get(absolutePath);
1668
1554
  if (known === void 0) {
1669
1555
  try {
1670
- known = fs3.lstatSync(absolutePath).isSymbolicLink();
1556
+ known = fs2.lstatSync(absolutePath).isSymbolicLink();
1671
1557
  } catch {
1672
1558
  known = false;
1673
1559
  }
@@ -1680,7 +1566,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
1680
1566
  const isLink = makeLinkProbe();
1681
1567
  let rootReal = cwd;
1682
1568
  try {
1683
- rootReal = fs3.realpathSync(cwd);
1569
+ rootReal = fs2.realpathSync(cwd);
1684
1570
  } catch {
1685
1571
  }
1686
1572
  const reported = /* @__PURE__ */ new Set();
@@ -1690,7 +1576,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
1690
1576
  let linkedDir = null;
1691
1577
  for (let depth = 1; depth <= parts.length; depth += 1) {
1692
1578
  const prefix = parts.slice(0, depth).join("/");
1693
- if (isLink(path3.join(cwd, prefix))) {
1579
+ if (isLink(path2.join(cwd, prefix))) {
1694
1580
  linkedDir = prefix;
1695
1581
  break;
1696
1582
  }
@@ -1701,10 +1587,10 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
1701
1587
  }
1702
1588
  if (reported.has(linkedDir)) continue;
1703
1589
  reported.add(linkedDir);
1704
- const absolute = path3.join(cwd, linkedDir);
1590
+ const absolute = path2.join(cwd, linkedDir);
1705
1591
  let targetReal = absolute;
1706
1592
  try {
1707
- targetReal = fs3.realpathSync(absolute);
1593
+ targetReal = fs2.realpathSync(absolute);
1708
1594
  } catch {
1709
1595
  }
1710
1596
  const target = readLinkTarget(absolute, targetReal);
@@ -1727,6 +1613,7 @@ function collectFiles(cwd, options = {}) {
1727
1613
  const includePatterns = options.includePatterns ?? [];
1728
1614
  const excludePatterns = options.excludePatterns ?? [];
1729
1615
  const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
1616
+ const pathRules = options.secretRules;
1730
1617
  const isToolOwned = makeToolOwnedDecider(cwd);
1731
1618
  let candidates;
1732
1619
  const skipped = [];
@@ -1762,7 +1649,7 @@ function collectFiles(cwd, options = {}) {
1762
1649
  }
1763
1650
  }
1764
1651
  } catch {
1765
- const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set(), true);
1652
+ const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), true);
1766
1653
  candidates = walked.files;
1767
1654
  skipped.push(...walked.skipped);
1768
1655
  }
@@ -1800,7 +1687,7 @@ function collectFiles(cwd, options = {}) {
1800
1687
  });
1801
1688
  return false;
1802
1689
  }
1803
- const secret = findSecretInPath(file);
1690
+ const secret = findSecretInPath(file, pathRules);
1804
1691
  if (secret) {
1805
1692
  skipped.push({
1806
1693
  path: secret.masked,
@@ -1811,35 +1698,34 @@ function collectFiles(cwd, options = {}) {
1811
1698
  }
1812
1699
  return true;
1813
1700
  }).sort();
1814
- return { files, skipped };
1701
+ return { files, skipped: maskSkipPaths(skipped, pathRules) };
1815
1702
  }
1816
- function walkDirectory(dir, baseDir, visited, isRoot = false) {
1817
- const results = { files: [], skipped: [] };
1818
- let realDir;
1819
- try {
1820
- realDir = fs3.realpathSync(dir);
1821
- } catch {
1822
- realDir = dir;
1823
- }
1824
- if (visited.has(realDir)) return results;
1825
- visited.add(realDir);
1826
- let rootReal;
1703
+ function rootRealFor(root) {
1827
1704
  try {
1828
- rootReal = fs3.realpathSync(baseDir);
1705
+ return fs2.realpathSync(root);
1829
1706
  } catch {
1830
- rootReal = baseDir;
1707
+ return root;
1831
1708
  }
1709
+ }
1710
+ function walkDirectory(dir, baseDir, rootReal, visited, isRoot = false) {
1711
+ const results = { files: [], skipped: [] };
1712
+ if (visited.has(dir)) return results;
1713
+ visited.add(dir);
1832
1714
  const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
1833
1715
  let entries;
1834
1716
  try {
1835
- entries = fs3.readdirSync(dir, { withFileTypes: true });
1717
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
1836
1718
  } catch {
1837
- results.skipped.push({ path: dir, reason: "read-error", detail: "directory unreadable" });
1719
+ results.skipped.push({
1720
+ path: path2.relative(baseDir, dir).replace(/\\/g, "/") || ".",
1721
+ reason: "read-error",
1722
+ detail: "directory unreadable"
1723
+ });
1838
1724
  return results;
1839
1725
  }
1840
1726
  for (const entry of entries) {
1841
- const fullPath = path3.join(dir, entry.name);
1842
- const relPath = path3.relative(baseDir, fullPath).replace(/\\/g, "/");
1727
+ const fullPath = path2.join(dir, entry.name);
1728
+ const relPath = path2.relative(baseDir, fullPath).replace(/\\/g, "/");
1843
1729
  if (isToolOwnedDirName(entry.name)) continue;
1844
1730
  if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
1845
1731
  results.skipped.push({
@@ -1872,14 +1758,14 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
1872
1758
  if (entry.isSymbolicLink()) {
1873
1759
  let targetReal;
1874
1760
  try {
1875
- targetReal = fs3.realpathSync(fullPath);
1761
+ targetReal = fs2.realpathSync(fullPath);
1876
1762
  } catch {
1877
1763
  results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
1878
1764
  continue;
1879
1765
  }
1880
1766
  let stats;
1881
1767
  try {
1882
- stats = fs3.statSync(fullPath);
1768
+ stats = fs2.statSync(fullPath);
1883
1769
  } catch {
1884
1770
  results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
1885
1771
  continue;
@@ -1913,7 +1799,7 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
1913
1799
  isFile = true;
1914
1800
  }
1915
1801
  if (isDirectory) {
1916
- const nested = walkDirectory(fullPath, baseDir, visited);
1802
+ const nested = walkDirectory(fullPath, baseDir, rootReal, visited);
1917
1803
  results.files.push(...nested.files);
1918
1804
  results.skipped.push(...nested.skipped);
1919
1805
  } else if (isFile) {
@@ -1923,7 +1809,141 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
1923
1809
  return results;
1924
1810
  }
1925
1811
 
1812
+ // src/diff.ts
1813
+ function side(manifest) {
1814
+ return {
1815
+ generatedAt: manifest.generatedAt,
1816
+ fingerprint: manifest.fingerprint,
1817
+ fileCount: manifest.counts.totalFiles,
1818
+ partsCount: manifest.counts.partsCount
1819
+ };
1820
+ }
1821
+ function hashMap(manifest) {
1822
+ const map = /* @__PURE__ */ new Map();
1823
+ for (const file of manifest.files) {
1824
+ if (file && typeof file.relativePath === "string") {
1825
+ map.set(file.relativePath, file.hashSha256);
1826
+ }
1827
+ }
1828
+ return map;
1829
+ }
1830
+ function manifestProblem(value) {
1831
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
1832
+ const candidate = value;
1833
+ if (!Array.isArray(candidate.files)) return 'no "files" array';
1834
+ for (const entry of candidate.files) {
1835
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1836
+ return 'a "files" entry is not an object';
1837
+ }
1838
+ if (typeof entry.relativePath !== "string") {
1839
+ return 'a "files" entry has no "relativePath"';
1840
+ }
1841
+ }
1842
+ const counts = candidate.counts;
1843
+ if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
1844
+ return 'no "counts" object';
1845
+ }
1846
+ for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
1847
+ if (typeof counts[key] !== "number") {
1848
+ return `no numeric "counts.${key}"`;
1849
+ }
1850
+ }
1851
+ if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
1852
+ if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
1853
+ return null;
1854
+ }
1855
+ function assertUsableManifest(value, label) {
1856
+ const problem = manifestProblem(value);
1857
+ if (problem) {
1858
+ throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
1859
+ }
1860
+ }
1861
+ function compareManifests(before, after) {
1862
+ assertUsableManifest(before, "before");
1863
+ assertUsableManifest(after, "after");
1864
+ const left = hashMap(before);
1865
+ const right = hashMap(after);
1866
+ const added = [];
1867
+ const removed = [];
1868
+ const changed = [];
1869
+ let unchanged = 0;
1870
+ for (const [filePath, hash] of right) {
1871
+ const previous = left.get(filePath);
1872
+ if (previous === void 0) added.push(filePath);
1873
+ else if (previous !== hash) changed.push(filePath);
1874
+ else unchanged += 1;
1875
+ }
1876
+ for (const filePath of left.keys()) {
1877
+ if (!right.has(filePath)) removed.push(filePath);
1878
+ }
1879
+ const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
1880
+ const mask = (values) => values.map((value) => findSecretInPath(value)?.masked ?? value);
1881
+ return {
1882
+ before: side(before),
1883
+ after: side(after),
1884
+ added: mask(sort(added)),
1885
+ removed: mask(sort(removed)),
1886
+ changed: mask(sort(changed)),
1887
+ unchanged
1888
+ };
1889
+ }
1890
+ function loadManifest(target) {
1891
+ let isDirectory;
1892
+ try {
1893
+ isDirectory = fs3.statSync(target).isDirectory();
1894
+ } catch {
1895
+ throw new Error(`No snapshot found at ${target}: the path does not exist.`);
1896
+ }
1897
+ const manifestPath = isDirectory ? path3.join(target, "manifest.json") : target;
1898
+ let raw;
1899
+ try {
1900
+ raw = fs3.readFileSync(manifestPath, "utf8");
1901
+ } catch {
1902
+ throw new Error(`Could not read a manifest at ${manifestPath}.`);
1903
+ }
1904
+ let parsed;
1905
+ try {
1906
+ parsed = JSON.parse(raw);
1907
+ } catch (err) {
1908
+ throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
1909
+ }
1910
+ const problem = manifestProblem(parsed);
1911
+ if (problem) {
1912
+ throw new Error(
1913
+ `${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
1914
+ );
1915
+ }
1916
+ return parsed;
1917
+ }
1918
+ function renderDiffText(diff) {
1919
+ const lines = [];
1920
+ lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
1921
+ lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
1922
+ lines.push("");
1923
+ lines.push(` + ${diff.added.length} added`);
1924
+ lines.push(` - ${diff.removed.length} removed`);
1925
+ lines.push(` ~ ${diff.changed.length} changed`);
1926
+ lines.push(` = ${diff.unchanged} unchanged`);
1927
+ const section = (title, values) => {
1928
+ if (values.length === 0) return;
1929
+ lines.push("", title);
1930
+ for (const value of values.slice(0, 200)) lines.push(` ${value}`);
1931
+ if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
1932
+ };
1933
+ section("added:", diff.added);
1934
+ section("removed:", diff.removed);
1935
+ section("changed:", diff.changed);
1936
+ return lines.join("\n");
1937
+ }
1938
+
1939
+ // src/explain.ts
1940
+ import * as fs6 from "fs";
1941
+ import * as path7 from "path";
1942
+
1926
1943
  // src/config.ts
1944
+ import * as fs4 from "fs";
1945
+ import * as path4 from "path";
1946
+ import { pathToFileURL } from "url";
1927
1947
  var CONFIG_FILENAMES = [
1928
1948
  "snapshot.config.ts",
1929
1949
  "snapshot.config.js",
@@ -2809,18 +2829,22 @@ function readManifest2(config) {
2809
2829
  function explainPath(config, target) {
2810
2830
  const relative5 = normalizeTarget(config.cwd, target);
2811
2831
  const absolute = path7.join(config.cwd, relative5);
2832
+ const maskedPath2 = findSecretInPath(relative5, config.secretRules)?.masked ?? relative5;
2812
2833
  const result = {
2813
- path: relative5,
2834
+ path: maskedPath2,
2814
2835
  exists: fs6.existsSync(absolute),
2815
2836
  included: false
2816
2837
  };
2817
2838
  const collection = collectFiles(config.cwd, {
2818
2839
  includePatterns: config.include,
2819
2840
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
2820
- includeSensitiveFiles: config.includeSensitiveFiles
2841
+ includeSensitiveFiles: config.includeSensitiveFiles,
2842
+ secretRules: config.secretRules
2821
2843
  });
2822
2844
  if (!collection.files.includes(relative5)) {
2823
- const record = collection.skipped.find((entry2) => entry2.path === relative5);
2845
+ const record = collection.skipped.find(
2846
+ (entry2) => entry2.path === relative5 || entry2.path === maskedPath2
2847
+ );
2824
2848
  if (record) {
2825
2849
  result.reason = record.reason;
2826
2850
  result.detail = record.detail;
@@ -2857,7 +2881,7 @@ function explainPath(config, target) {
2857
2881
  result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
2858
2882
  }
2859
2883
  const manifest = readManifest2(config);
2860
- const entry = manifest?.files.find((file) => file.relativePath === relative5);
2884
+ const entry = Array.isArray(manifest?.files) ? manifest.files.find((file) => file.relativePath === relative5) : void 0;
2861
2885
  result.part = entry?.part ?? null;
2862
2886
  result.inclusionMode = entry?.inclusionMode;
2863
2887
  return result;
@@ -4446,7 +4470,8 @@ function summarizeMarkdown(content) {
4446
4470
  }
4447
4471
 
4448
4472
  // src/generator/audit.ts
4449
- function generateAuditReport(manifest, secretFindings) {
4473
+ function generateAuditReport(manifest, secretFindings, secretRules) {
4474
+ const mask = (value) => findSecretInPath(value, secretRules)?.masked ?? value;
4450
4475
  const lines = [
4451
4476
  "# Codebase Health & Security Audit",
4452
4477
  "",
@@ -4481,7 +4506,7 @@ function generateAuditReport(manifest, secretFindings) {
4481
4506
  "| :--- | :--- | :--- |"
4482
4507
  );
4483
4508
  for (const finding of secretFindings) {
4484
- lines.push(`| \`${finding.path ?? "unknown"}\` | ${finding.line} | \`${finding.rule}\` |`);
4509
+ lines.push(`| \`${mask(finding.path ?? "unknown")}\` | ${finding.line} | \`${finding.rule}\` |`);
4485
4510
  }
4486
4511
  lines.push("", "### Matches by rule", "", "| Rule | Matches |", "| :--- | :--- |");
4487
4512
  for (const [rule, count] of Array.from(byRule.entries()).sort((a, b) => b[1] - a[1])) {
@@ -4503,7 +4528,7 @@ function generateAuditReport(manifest, secretFindings) {
4503
4528
  for (const file of largeFiles) {
4504
4529
  const sizeKb = (file.sizeBytes / 1024).toFixed(1);
4505
4530
  const status = file.isPreviewTruncated ? "Truncated Head/Tail" : "Full";
4506
- lines.push(`| \`${file.relativePath}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
4531
+ lines.push(`| \`${mask(file.relativePath)}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
4507
4532
  }
4508
4533
  lines.push("");
4509
4534
  }
@@ -4545,7 +4570,7 @@ function generateAuditReport(manifest, secretFindings) {
4545
4570
  "| :--- | :--- | :--- |"
4546
4571
  );
4547
4572
  for (const skip of manifest.skipped) {
4548
- lines.push(`| \`${skip.path}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
4573
+ lines.push(`| \`${mask(skip.path)}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
4549
4574
  }
4550
4575
  lines.push("");
4551
4576
  }
@@ -5044,6 +5069,9 @@ function generateProjectTree(filePaths) {
5044
5069
  import * as crypto2 from "crypto";
5045
5070
  import * as fs8 from "fs";
5046
5071
  import * as path9 from "path";
5072
+ function maskedPath(value, secretRules) {
5073
+ return findSecretInPath(value, secretRules)?.masked ?? value;
5074
+ }
5047
5075
  function emptyDrift() {
5048
5076
  return {
5049
5077
  missingInSnapshot: [],
@@ -5114,15 +5142,20 @@ function validateSnapshot(configInput) {
5114
5142
  const skippedSet = new Set(
5115
5143
  manifest.skipped.filter((entry) => entry && typeof entry.path === "string").map((entry) => entry.path)
5116
5144
  );
5145
+ const isSkippedOnDisk = (diskFile) => {
5146
+ if (skippedSet.has(diskFile)) return true;
5147
+ return skippedSet.has(maskedPath(diskFile, config.secretRules));
5148
+ };
5117
5149
  const collection = collectFiles(config.cwd, {
5118
5150
  includePatterns: config.include,
5119
5151
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
5120
- includeSensitiveFiles: config.includeSensitiveFiles
5152
+ includeSensitiveFiles: config.includeSensitiveFiles,
5153
+ secretRules: config.secretRules
5121
5154
  });
5122
5155
  for (const diskFile of collection.files) {
5123
5156
  const snapshotHash = manifestFileMap.get(diskFile);
5124
5157
  if (!snapshotHash) {
5125
- if (skippedSet.has(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
5158
+ if (isSkippedOnDisk(diskFile) && !wouldNowBeIncluded(config.cwd, diskFile)) continue;
5126
5159
  drift.missingInSnapshot.push(diskFile);
5127
5160
  continue;
5128
5161
  }
@@ -5391,10 +5424,19 @@ function validateSnapshot(configInput) {
5391
5424
  `${drift.configMismatches.length} configuration mismatch(es) between the manifest and this run.`
5392
5425
  );
5393
5426
  }
5427
+ const maskedDrift = {
5428
+ ...drift,
5429
+ missingInSnapshot: drift.missingInSnapshot.map((value) => maskedPath(value, config.secretRules)),
5430
+ missingOnDisk: drift.missingOnDisk.map((value) => maskedPath(value, config.secretRules)),
5431
+ hashMismatches: drift.hashMismatches.map((mismatch) => ({
5432
+ ...mismatch,
5433
+ path: maskedPath(mismatch.path, config.secretRules)
5434
+ }))
5435
+ };
5394
5436
  return {
5395
5437
  valid: errors.length === 0,
5396
5438
  manifestPresent: true,
5397
- drift,
5439
+ drift: maskedDrift,
5398
5440
  errors,
5399
5441
  warnings
5400
5442
  };
@@ -5402,7 +5444,7 @@ function validateSnapshot(configInput) {
5402
5444
 
5403
5445
  // src/index.ts
5404
5446
  var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
5405
- var GENERATOR_VERSION = "4.0.0";
5447
+ var GENERATOR_VERSION = "4.0.2";
5406
5448
  var SCHEMA_VERSION = "3.0.0";
5407
5449
  function sha256Hex2(input) {
5408
5450
  return crypto3.createHash("sha256").update(input).digest("hex");
@@ -5439,7 +5481,6 @@ function bump(counts, key) {
5439
5481
  }
5440
5482
  var RELATIVE_SPECIFIER = /^\.{1,2}\//;
5441
5483
  var IMPORT_CANDIDATE_SUFFIXES = [
5442
- "",
5443
5484
  ".ts",
5444
5485
  ".tsx",
5445
5486
  ".mts",
@@ -5453,23 +5494,59 @@ var IMPORT_CANDIDATE_SUFFIXES = [
5453
5494
  "/index.js",
5454
5495
  "/index.mjs"
5455
5496
  ];
5456
- function resolveSpecifier(fromFile, specifier, knownFiles) {
5497
+ var SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([
5498
+ ".ts",
5499
+ ".tsx",
5500
+ ".mts",
5501
+ ".cts",
5502
+ ".js",
5503
+ ".jsx",
5504
+ ".mjs",
5505
+ ".cjs"
5506
+ ]);
5507
+ function buildImportResolver(files) {
5508
+ const byPath = /* @__PURE__ */ new Map();
5509
+ const byPathFolded = /* @__PURE__ */ new Map();
5510
+ const byStem = /* @__PURE__ */ new Map();
5511
+ const byStemFolded = /* @__PURE__ */ new Map();
5512
+ const remember = (map, key, value) => {
5513
+ if (!map.has(key)) map.set(key, value);
5514
+ };
5515
+ for (const file of files) {
5516
+ const relativePath = file.relativePath;
5517
+ remember(byPath, relativePath, relativePath);
5518
+ remember(byPathFolded, relativePath.toLowerCase(), relativePath);
5519
+ const extension = path10.posix.extname(relativePath);
5520
+ if (!SOURCE_EXTENSIONS.has(extension)) continue;
5521
+ const stem = relativePath.slice(0, -extension.length);
5522
+ remember(byStem, stem, relativePath);
5523
+ remember(byStemFolded, stem.toLowerCase(), relativePath);
5524
+ if (path10.posix.basename(stem) === "index") {
5525
+ const directory = path10.posix.dirname(stem);
5526
+ remember(byStem, directory, relativePath);
5527
+ remember(byStemFolded, directory.toLowerCase(), relativePath);
5528
+ }
5529
+ }
5530
+ return { byPath, byPathFolded, byStem, byStemFolded };
5531
+ }
5532
+ function resolveSpecifier(fromFile, specifier, resolver) {
5457
5533
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
5458
- const baseDir = path10.posix.dirname(fromFile);
5459
- const joined = path10.posix.normalize(path10.posix.join(baseDir, specifier));
5460
- const candidate = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
5534
+ const joined = path10.posix.normalize(
5535
+ path10.posix.join(path10.posix.dirname(fromFile), specifier)
5536
+ );
5537
+ const exact = (key) => resolver.byPath.get(key) ?? resolver.byPathFolded.get(key.toLowerCase()) ?? null;
5538
+ const direct = exact(joined);
5539
+ if (direct) return direct;
5461
5540
  for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
5462
- const withSuffix = `${joined}${suffix}`;
5463
- if (knownFiles.has(withSuffix)) return withSuffix;
5464
- }
5465
- const extensionSwap = new RegExp(`${candidate}\\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$`);
5466
- for (const file of knownFiles) {
5467
- if (extensionSwap.test(file)) return file;
5541
+ const hit = exact(`${joined}${suffix}`);
5542
+ if (hit) return hit;
5468
5543
  }
5469
- return null;
5544
+ const stem = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
5545
+ const byStem = (key) => resolver.byStem.get(key) ?? resolver.byStemFolded.get(key.toLowerCase()) ?? null;
5546
+ return byStem(joined) ?? byStem(stem);
5470
5547
  }
5471
5548
  function buildIndexes(files) {
5472
- const knownFiles = new Set(files.map((file) => file.relativePath));
5549
+ const resolver = buildImportResolver(files);
5473
5550
  const importerCounts = /* @__PURE__ */ new Map();
5474
5551
  const byFile = files.filter((file) => isSourceSummary(file.summary)).map((file) => {
5475
5552
  const summary = file.summary;
@@ -5481,7 +5558,7 @@ function buildIndexes(files) {
5481
5558
  });
5482
5559
  for (const entry of byFile) {
5483
5560
  for (const specifier of entry.imports) {
5484
- const resolved = resolveSpecifier(entry.path, specifier, knownFiles);
5561
+ const resolved = resolveSpecifier(entry.path, specifier, resolver);
5485
5562
  if (resolved) importerCounts.set(resolved, (importerCounts.get(resolved) ?? 0) + 1);
5486
5563
  }
5487
5564
  }
@@ -5572,15 +5649,22 @@ function resolveSafeOutputDirs(config) {
5572
5649
  return { outDir, tmpDir };
5573
5650
  }
5574
5651
  var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
5575
- var STAGING_ENTRIES = /* @__PURE__ */ new Set([
5576
- ".codebase-snapshot",
5577
- "manifest.json",
5578
- "README.md",
5579
- "parts",
5580
- "reports",
5581
- "full-codebase.json",
5582
- "full-codebase.md"
5583
- ]);
5652
+ var RUN_DIR_PREFIX = "run-";
5653
+ var STALE_RUN_MS = 60 * 60 * 1e3;
5654
+ function maskPathText(value, secretRules) {
5655
+ return findSecretInPath(value, secretRules)?.masked ?? value;
5656
+ }
5657
+ function maskSkipRecords(records, secretRules) {
5658
+ return maskSkipPaths(records, secretRules);
5659
+ }
5660
+ function isProcessAlive(pid) {
5661
+ try {
5662
+ process.kill(pid, 0);
5663
+ return true;
5664
+ } catch (err) {
5665
+ return err.code !== "ESRCH";
5666
+ }
5667
+ }
5584
5668
  var OWNERSHIP_MARKER = ".codebase-snapshot";
5585
5669
  var SUPPORTED_MARKER_SCHEMAS = /* @__PURE__ */ new Set([SCHEMA_VERSION]);
5586
5670
  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;
@@ -5610,6 +5694,7 @@ function markerPayload(dir, state, id, createdAt) {
5610
5694
  createdAt,
5611
5695
  state,
5612
5696
  id,
5697
+ pid: process.pid,
5613
5698
  entries: listEntries(dir)
5614
5699
  };
5615
5700
  const manifestPath = path10.join(dir, "manifest.json");
@@ -5673,6 +5758,7 @@ function readOwnershipMarker(dir) {
5673
5758
  createdAt: typeof candidate.createdAt === "string" ? candidate.createdAt : "unknown",
5674
5759
  state: candidate.state,
5675
5760
  id: candidate.id,
5761
+ pid: typeof candidate.pid === "number" ? candidate.pid : void 0,
5676
5762
  manifestSha256: typeof candidate.manifestSha256 === "string" ? candidate.manifestSha256 : void 0,
5677
5763
  entries: Array.isArray(candidate.entries) && candidate.entries.every((entry) => typeof entry === "string") ? candidate.entries : void 0
5678
5764
  },
@@ -5780,7 +5866,7 @@ function describeEntries(entries, limit = 5) {
5780
5866
  const shown = entries.slice(0, limit).join(", ");
5781
5867
  return entries.length > limit ? `${shown}, and ${entries.length - limit} more` : shown;
5782
5868
  }
5783
- function assertReplaceable(target, label) {
5869
+ function assertReplaceable(target, label, secretRules) {
5784
5870
  if (!fs9.existsSync(target)) return;
5785
5871
  if (!fs9.statSync(target).isDirectory()) {
5786
5872
  throw new Error(
@@ -5789,34 +5875,89 @@ function assertReplaceable(target, label) {
5789
5875
  }
5790
5876
  const entries = fs9.readdirSync(target);
5791
5877
  if (entries.length === 0) return;
5792
- if (label === "outDir" && entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
5793
- const { marker: marker2, problem } = readOwnershipMarker(target);
5794
- if (label === "tmpDir") {
5795
- if (!marker2) {
5796
- throw new Error(
5797
- `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.`
5798
- );
5799
- }
5800
- const unexpected = entries.filter((name) => !STAGING_ENTRIES.has(name));
5801
- if (unexpected.length > 0) {
5802
- throw new Error(
5803
- `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.`
5804
- );
5805
- }
5806
- return;
5807
- }
5878
+ if (entries.every((name) => PLACEHOLDER_ENTRIES.has(name))) return;
5879
+ const { problem } = readOwnershipMarker(target);
5808
5880
  const verdict = assessSnapshotDirectory(target);
5809
5881
  if (verdict.owned) return;
5882
+ const shownForeign = verdict.foreignEntries.map((name) => maskPathText(name, secretRules));
5883
+ const shownEntries = entries.map((name) => maskPathText(name, secretRules));
5810
5884
  if (verdict.foreignEntries.length > 0) {
5811
5885
  throw new Error(
5812
- `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.`
5886
+ `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.`
5813
5887
  );
5814
5888
  }
5815
5889
  const reason = verdict.problem ?? problem ?? "it does not look like a snapshot this generator wrote";
5816
5890
  throw new Error(
5817
- `Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(entries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
5891
+ `Refusing to replace ${label} ${target}: ${reason} (found ${describeEntries(shownEntries)}). Remove the directory yourself, or point ${label} at a new or empty directory.`
5818
5892
  );
5819
5893
  }
5894
+ function assertStagingRootUsable(tmpDir, secretRules) {
5895
+ if (!fs9.existsSync(tmpDir)) return;
5896
+ if (!fs9.statSync(tmpDir).isDirectory()) {
5897
+ throw new Error(`Refusing to write into tmpDir ${tmpDir}: it exists but is not a directory.`);
5898
+ }
5899
+ const entries = fs9.readdirSync(tmpDir);
5900
+ if (entries.length === 0) return;
5901
+ const foreign = entries.filter((name) => {
5902
+ const full = path10.join(tmpDir, name);
5903
+ if (!name.startsWith(RUN_DIR_PREFIX)) return true;
5904
+ let stats;
5905
+ try {
5906
+ stats = fs9.statSync(full);
5907
+ } catch {
5908
+ return true;
5909
+ }
5910
+ if (!stats.isDirectory()) return true;
5911
+ const { marker: marker2 } = readOwnershipMarker(full);
5912
+ if (marker2) return false;
5913
+ try {
5914
+ return fs9.readdirSync(full).length > 0;
5915
+ } catch {
5916
+ return true;
5917
+ }
5918
+ });
5919
+ if (foreign.length > 0) {
5920
+ const shown = describeEntries(foreign.map((name) => maskPathText(name, secretRules)));
5921
+ throw new Error(
5922
+ `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.`
5923
+ );
5924
+ }
5925
+ }
5926
+ function cleanStaleRunDirs(tmpDir) {
5927
+ let entries;
5928
+ try {
5929
+ entries = fs9.readdirSync(tmpDir);
5930
+ } catch {
5931
+ return;
5932
+ }
5933
+ for (const name of entries) {
5934
+ if (!name.startsWith(RUN_DIR_PREFIX)) continue;
5935
+ const full = path10.join(tmpDir, name);
5936
+ const { marker: marker2 } = readOwnershipMarker(full);
5937
+ if (marker2) {
5938
+ const created = Date.parse(marker2.createdAt);
5939
+ const aged = !Number.isFinite(created) || Date.now() - created > STALE_RUN_MS;
5940
+ const abandoned = typeof marker2.pid === "number" && !isProcessAlive(marker2.pid);
5941
+ if (aged || abandoned) {
5942
+ try {
5943
+ fs9.rmSync(full, { recursive: true, force: true });
5944
+ } catch {
5945
+ }
5946
+ }
5947
+ continue;
5948
+ }
5949
+ try {
5950
+ if (fs9.readdirSync(full).length === 0) fs9.rmSync(full, { recursive: true, force: true });
5951
+ } catch {
5952
+ }
5953
+ }
5954
+ }
5955
+ function removeIfEmpty(dir) {
5956
+ try {
5957
+ if (fs9.readdirSync(dir).length === 0) fs9.rmSync(dir, { recursive: true, force: true });
5958
+ } catch {
5959
+ }
5960
+ }
5820
5961
  function ensureParentDirectory(target, label) {
5821
5962
  const parent = path10.dirname(target);
5822
5963
  if (fs9.existsSync(parent) && !fs9.statSync(parent).isDirectory()) {
@@ -5832,14 +5973,12 @@ function ensureParentDirectory(target, label) {
5832
5973
  );
5833
5974
  }
5834
5975
  }
5835
- function acquireRunLock(outDir) {
5836
- const lockPath = path10.join(
5837
- os.tmpdir(),
5838
- `codebase-snapshot-${sha256Hex2(outDir).slice(0, 16)}.lock`
5839
- );
5976
+ function acquireRunLock(stagingRoot) {
5977
+ const key = lockKeyFor(stagingRoot);
5978
+ const lockPath = path10.join(os.tmpdir(), `codebase-snapshot-${key}.lock`);
5840
5979
  const payload = `${JSON.stringify({
5841
5980
  pid: process.pid,
5842
- outDir,
5981
+ stagingRoot,
5843
5982
  startedAt: (/* @__PURE__ */ new Date()).toISOString()
5844
5983
  })}
5845
5984
  `;
@@ -5864,11 +6003,20 @@ function acquireRunLock(outDir) {
5864
6003
  continue;
5865
6004
  }
5866
6005
  throw new Error(
5867
- `Another generate run is already writing ${outDir}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
6006
+ `Another generate run is already using the staging directory ${stagingRoot}. Wait for it to finish, or delete ${lockPath} if no other run is active.`
5868
6007
  );
5869
6008
  }
5870
6009
  }
5871
- throw new Error(`Could not acquire the generate lock for ${outDir}.`);
6010
+ throw new Error(`Could not acquire the generate lock for ${stagingRoot}.`);
6011
+ }
6012
+ function lockKeyFor(stagingRoot) {
6013
+ let key = stagingRoot;
6014
+ try {
6015
+ key = fs9.realpathSync(stagingRoot);
6016
+ } catch {
6017
+ }
6018
+ if (process.platform === "win32" || process.platform === "darwin") key = key.toLowerCase();
6019
+ return sha256Hex2(key).slice(0, 16);
5872
6020
  }
5873
6021
  function lockOwnerIsGone(lockPath) {
5874
6022
  let raw;
@@ -5904,18 +6052,32 @@ function writeManifestAtomically(manifestPath, manifest) {
5904
6052
  throw err;
5905
6053
  }
5906
6054
  }
5907
- function swapIntoPlace(tmpDir, outDir) {
6055
+ function swapIntoPlace(stagingDir, outDir) {
5908
6056
  ensureParentDirectory(outDir, "outDir");
6057
+ const deadline = Date.now() + 3e3;
6058
+ for (let attempt = 0; ; attempt += 1) {
6059
+ try {
6060
+ swapOnce(stagingDir, outDir);
6061
+ return;
6062
+ } catch (err) {
6063
+ const code = err.code;
6064
+ const racy = code === "ENOENT" || code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM";
6065
+ if (!racy || attempt >= 10 || Date.now() > deadline) throw err;
6066
+ sleepSync(50);
6067
+ }
6068
+ }
6069
+ }
6070
+ function swapOnce(stagingDir, outDir) {
5909
6071
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
5910
6072
  const hadPrevious = fs9.existsSync(outDir);
5911
6073
  if (hadPrevious) fs9.renameSync(outDir, backupDir);
5912
6074
  try {
5913
6075
  try {
5914
- fs9.renameSync(tmpDir, outDir);
6076
+ fs9.renameSync(stagingDir, outDir);
5915
6077
  } catch (err) {
5916
6078
  if (err.code === "EXDEV") {
5917
- fs9.cpSync(tmpDir, outDir, { recursive: true });
5918
- fs9.rmSync(tmpDir, { recursive: true, force: true });
6079
+ fs9.cpSync(stagingDir, outDir, { recursive: true });
6080
+ fs9.rmSync(stagingDir, { recursive: true, force: true });
5919
6081
  } else {
5920
6082
  throw err;
5921
6083
  }
@@ -5929,6 +6091,10 @@ function swapIntoPlace(tmpDir, outDir) {
5929
6091
  throw err;
5930
6092
  }
5931
6093
  }
6094
+ function sleepSync(milliseconds) {
6095
+ const buffer = new Int32Array(new SharedArrayBuffer(4));
6096
+ Atomics.wait(buffer, 0, 0, milliseconds);
6097
+ }
5932
6098
  function artifactWarnings(artifacts) {
5933
6099
  const messages = [];
5934
6100
  if (!artifacts.manifest) {
@@ -5991,11 +6157,12 @@ function generateSnapshot(configInput, options) {
5991
6157
  const collection = collectFiles(config.cwd, {
5992
6158
  includePatterns: config.include,
5993
6159
  excludePatterns: [...config.excludePatterns, ...config.presetExclude],
5994
- includeSensitiveFiles: config.includeSensitiveFiles
6160
+ includeSensitiveFiles: config.includeSensitiveFiles,
6161
+ secretRules: config.secretRules
5995
6162
  });
5996
6163
  const files = [];
5997
6164
  const monolithBodies = /* @__PURE__ */ new Map();
5998
- const skipped = [...collection.skipped];
6165
+ const skipped = maskSkipRecords([...collection.skipped], config.secretRules);
5999
6166
  const secretFindings = [];
6000
6167
  const warnings = [];
6001
6168
  const layerCounts = {};
@@ -6069,12 +6236,12 @@ function generateSnapshot(configInput, options) {
6069
6236
  summaryQuality = outcome?.quality;
6070
6237
  if (isSourceSummary(summary) && summary.parseErrors) {
6071
6238
  warnings.push(
6072
- `Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
6239
+ `Parse errors in ${maskPathText(relativePath, config.secretRules)} (${summary.parseErrors}); its summary is incomplete.`
6073
6240
  );
6074
6241
  }
6075
6242
  } catch (err) {
6076
6243
  warnings.push(
6077
- `Summary skipped for ${relativePath}: ${err.message}`.replace(/\s+/g, " ")
6244
+ `Summary skipped for ${maskPathText(relativePath, config.secretRules)}: ${err.message}`.replace(/\s+/g, " ")
6078
6245
  );
6079
6246
  }
6080
6247
  files.push({
@@ -6118,6 +6285,7 @@ function generateSnapshot(configInput, options) {
6118
6285
  const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
6119
6286
  const validationErrors = verifyInMemory(files, parts, maxEffectiveBudget);
6120
6287
  const totalContentBytes = files.reduce((total, file) => total + file.includedBytes, 0);
6288
+ const maskName = (value) => maskPathText(value, config.secretRules);
6121
6289
  const manifest = {
6122
6290
  schemaVersion: SCHEMA_VERSION,
6123
6291
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
@@ -6138,13 +6306,13 @@ function generateSnapshot(configInput, options) {
6138
6306
  includeMonolith: config.includeMonolith,
6139
6307
  includeSensitiveFiles: config.includeSensitiveFiles,
6140
6308
  artifacts: { ...config.artifacts },
6141
- include: [...config.include],
6309
+ include: [...config.include].map(maskName),
6142
6310
  preset: config.preset,
6143
6311
  layersMode: config.layersMode,
6144
6312
  budgets: { default: config.budgets.default, byLayer: { ...config.budgets.byLayer } },
6145
- excludePatterns: [...config.excludePatterns],
6146
- layers: config.layers.map((layer2) => layer2.name),
6147
- kinds: config.kinds.map((kind) => kind.kind)
6313
+ excludePatterns: [...config.excludePatterns].map(maskName),
6314
+ layers: config.layers.map((layer2) => maskName(layer2.name)),
6315
+ kinds: config.kinds.map((kind) => maskName(kind.kind))
6148
6316
  },
6149
6317
  counts: {
6150
6318
  totalFiles: files.length,
@@ -6183,12 +6351,12 @@ function generateSnapshot(configInput, options) {
6183
6351
  const { content: _content, ...rest } = file;
6184
6352
  return rest;
6185
6353
  }),
6186
- skipped,
6354
+ skipped: maskSkipRecords(skipped, config.secretRules),
6187
6355
  warnings
6188
6356
  };
6189
6357
  manifest.warnings = [
6190
6358
  ...warnings,
6191
- ...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
6359
+ ...secretFindings.map((finding) => `${finding.rule} at ${maskPathText(finding.path ?? "unknown", config.secretRules)}:${finding.line}`)
6192
6360
  ];
6193
6361
  manifest.totals.warningCount = manifest.warnings.length;
6194
6362
  let monolithJson = null;
@@ -6234,31 +6402,33 @@ function generateSnapshot(configInput, options) {
6234
6402
  manifest.totals.warningCount = manifest.warnings.length;
6235
6403
  const { outDir, tmpDir } = resolveSafeOutputDirs(config);
6236
6404
  const { artifacts } = config;
6237
- assertReplaceable(tmpDir, "tmpDir");
6238
- assertReplaceable(outDir, "outDir");
6405
+ assertReplaceable(outDir, "outDir", config.secretRules);
6406
+ assertStagingRootUsable(tmpDir, config.secretRules);
6239
6407
  if (dryRun) {
6240
6408
  runAfterGenerate(config, manifest);
6241
6409
  return manifest;
6242
6410
  }
6243
- const releaseLock = acquireRunLock(outDir);
6244
- let stagingStarted = false;
6411
+ const releaseLock = acquireRunLock(tmpDir);
6412
+ let stagingDir = null;
6245
6413
  try {
6246
6414
  ensureParentDirectory(outDir, "outDir");
6247
- fs9.rmSync(tmpDir, { recursive: true, force: true });
6248
6415
  fs9.mkdirSync(tmpDir, { recursive: true });
6249
- stagingStarted = true;
6416
+ cleanStaleRunDirs(tmpDir);
6250
6417
  const snapshotId = crypto3.randomUUID();
6251
6418
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6252
- writeOwnershipMarker(tmpDir, {
6419
+ stagingDir = path10.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
6420
+ fs9.mkdirSync(stagingDir);
6421
+ writeOwnershipMarker(stagingDir, {
6253
6422
  generator: GENERATOR_NAME,
6254
6423
  version: GENERATOR_VERSION,
6255
6424
  schemaVersion: SCHEMA_VERSION,
6256
6425
  createdAt,
6257
6426
  state: "staging",
6258
- id: snapshotId
6427
+ id: snapshotId,
6428
+ pid: process.pid
6259
6429
  });
6260
- const partsDir = path10.join(tmpDir, "parts");
6261
- const reportsDir = path10.join(tmpDir, "reports");
6430
+ const partsDir = path10.join(stagingDir, "parts");
6431
+ const reportsDir = path10.join(stagingDir, "reports");
6262
6432
  if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
6263
6433
  if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
6264
6434
  fs9.mkdirSync(reportsDir, { recursive: true });
@@ -6278,25 +6448,25 @@ function generateSnapshot(configInput, options) {
6278
6448
  if (artifacts.audit) {
6279
6449
  fs9.writeFileSync(
6280
6450
  path10.join(reportsDir, "audit.md"),
6281
- generateAuditReport(manifest, secretFindings),
6451
+ generateAuditReport(manifest, secretFindings, config.secretRules),
6282
6452
  "utf8"
6283
6453
  );
6284
6454
  }
6285
6455
  if (artifacts.readme) {
6286
- fs9.writeFileSync(path10.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
6456
+ fs9.writeFileSync(path10.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
6287
6457
  }
6288
6458
  if (artifacts.manifest) {
6289
- fs9.writeFileSync(path10.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6459
+ fs9.writeFileSync(path10.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6290
6460
  }
6291
6461
  if (monolithJson !== null) {
6292
- fs9.writeFileSync(path10.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
6462
+ fs9.writeFileSync(path10.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
6293
6463
  }
6294
6464
  if (monolithMarkdown !== null) {
6295
- fs9.writeFileSync(path10.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
6465
+ fs9.writeFileSync(path10.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
6296
6466
  }
6297
- writeOwnershipMarker(tmpDir, markerPayload(tmpDir, "complete", snapshotId, createdAt));
6298
- swapIntoPlace(tmpDir, outDir);
6299
- stagingStarted = false;
6467
+ writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
6468
+ swapIntoPlace(stagingDir, outDir);
6469
+ stagingDir = null;
6300
6470
  const warningsBeforeHook = manifest.warnings.length;
6301
6471
  runAfterGenerate(config, manifest);
6302
6472
  if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
@@ -6305,14 +6475,15 @@ function generateSnapshot(configInput, options) {
6305
6475
  writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
6306
6476
  return manifest;
6307
6477
  } catch (err) {
6308
- if (stagingStarted) {
6478
+ if (stagingDir !== null) {
6309
6479
  try {
6310
- fs9.rmSync(tmpDir, { recursive: true, force: true });
6480
+ fs9.rmSync(stagingDir, { recursive: true, force: true });
6311
6481
  } catch {
6312
6482
  }
6313
6483
  }
6314
6484
  throw err;
6315
6485
  } finally {
6486
+ removeIfEmpty(tmpDir);
6316
6487
  releaseLock();
6317
6488
  }
6318
6489
  }
@@ -6797,8 +6968,8 @@ function buildAgentContract() {
6797
6968
  "snapshot.config.mjs",
6798
6969
  "snapshot.config.cjs",
6799
6970
  "snapshot.config.json",
6800
- ".snapshotrc",
6801
6971
  ".snapshotrc.json",
6972
+ ".snapshotrc",
6802
6973
  'package.json \u2192 "snapshot" key'
6803
6974
  ],
6804
6975
  settings: [
@@ -7601,8 +7772,7 @@ ${USAGE}
7601
7772
  warn(`\u274C ${manifestPath} is not a snapshot manifest: ${shapeError}`);
7602
7773
  return 1;
7603
7774
  }
7604
- const validation = validateSnapshot(resolved);
7605
- emit(invocation.format, { manifest, validation }, () => {
7775
+ emit(invocation.format, { manifest, validation: null }, () => {
7606
7776
  info(`
7607
7777
  \u{1F4CA} Codebase Snapshot Report (${resolved.cwd})`);
7608
7778
  info(` - Generated at: ${manifest.generatedAt}`);
@@ -7614,14 +7784,14 @@ ${USAGE}
7614
7784
  ` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
7615
7785
  );
7616
7786
  info(` - Total parts: ${manifest.counts.partsCount}`);
7617
- info(` - Integrity: ${validation.valid ? "\u2705 Clean (no drift)" : "\u274C Drift detected"}`);
7787
+ info(" - Integrity: not checked (run `codebase-snapshot validate`)");
7618
7788
  info(` - Warnings: ${manifest.warnings?.length ?? 0}`);
7619
7789
  info("\n Layers:");
7620
7790
  for (const [layer2, count] of Object.entries(manifest.layers)) {
7621
7791
  info(` - ${layer2.padEnd(16)}: ${count} files`);
7622
7792
  }
7623
7793
  });
7624
- return validation.valid ? 0 : 1;
7794
+ return 0;
7625
7795
  }
7626
7796
  if (invocation.command === "explain") {
7627
7797
  const result = explainPath(resolved, invocation.args[0]);
@@ -7740,6 +7910,8 @@ async function runWatchLoop(invocation, resolved) {
7740
7910
  (segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`) || VCS_SEGMENTS.has(segment)
7741
7911
  );
7742
7912
  let running = false;
7913
+ let quietUntil = 0;
7914
+ const QUIET_MS = 1500;
7743
7915
  const regenerate = async () => {
7744
7916
  if (running) return;
7745
7917
  running = true;
@@ -7777,6 +7949,7 @@ async function runWatchLoop(invocation, resolved) {
7777
7949
  } catch (err) {
7778
7950
  warn(`\u274C ${err.message}`);
7779
7951
  } finally {
7952
+ quietUntil = performance.now() + QUIET_MS;
7780
7953
  running = false;
7781
7954
  }
7782
7955
  };
@@ -7784,6 +7957,7 @@ async function runWatchLoop(invocation, resolved) {
7784
7957
  void regenerate();
7785
7958
  let timer = null;
7786
7959
  const watcher = fs11.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
7960
+ if (performance.now() < quietUntil) return;
7787
7961
  if (filename) {
7788
7962
  const changed = path12.resolve(resolved.cwd, filename.toString());
7789
7963
  if (isGenerated(changed)) return;