@ankurhalder/codebase-snapshot 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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,19 @@ function isSensitivePath(relativePath) {
1467
1334
  }
1468
1335
  return matchesSensitiveList(posixPath, base);
1469
1336
  }
1470
- function findSecretInPath(relativePath) {
1337
+ var activeSecretRules = null;
1338
+ function setActiveSecretRules(rules) {
1339
+ activeSecretRules = rules === null ? null : [...rules];
1340
+ }
1341
+ function pathScanRules(extraRules) {
1342
+ const fallback = extraRules ?? activeSecretRules ?? [];
1343
+ return [...DEFAULT_SECRET_PATTERNS, ...fallback];
1344
+ }
1345
+ function findSecretInPath(relativePath, extraRules) {
1471
1346
  const posixPath = relativePath.replace(/\\/g, "/");
1472
1347
  const spans = [];
1473
1348
  const rules = [];
1474
- for (const { rule, pattern } of DEFAULT_SECRET_PATTERNS) {
1349
+ for (const { rule, pattern } of pathScanRules(extraRules)) {
1475
1350
  const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
1476
1351
  let match;
1477
1352
  while ((match = scan.exec(posixPath)) !== null) {
@@ -1479,24 +1354,34 @@ function findSecretInPath(relativePath) {
1479
1354
  scan.lastIndex += 1;
1480
1355
  continue;
1481
1356
  }
1482
- spans.push({ start: match.index, end: match.index + match[0].length, rule });
1357
+ spans.push({ start: match.index, end: match.index + match[0].length });
1483
1358
  if (!rules.includes(rule)) rules.push(rule);
1484
1359
  }
1485
1360
  }
1486
1361
  if (spans.length === 0) return null;
1487
1362
  spans.sort((a, b) => a.start - b.start || a.end - b.end);
1363
+ const merged = [];
1364
+ for (const span of spans) {
1365
+ const last = merged[merged.length - 1];
1366
+ if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
1367
+ else merged.push({ start: span.start, end: span.end });
1368
+ }
1488
1369
  let out = "";
1489
1370
  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);
1371
+ for (const span of merged) {
1372
+ out += posixPath.slice(cursor, span.start);
1494
1373
  out += REDACTION_MARKER;
1495
- cursor = Math.min(span.end, posixPath.length);
1374
+ cursor = span.end;
1496
1375
  }
1497
1376
  out += posixPath.slice(cursor);
1498
1377
  return { rules, masked: out };
1499
1378
  }
1379
+ function maskSkipPaths(records, extraRules) {
1380
+ return records.map((record) => {
1381
+ const hit = findSecretInPath(record.path, extraRules);
1382
+ return hit ? { ...record, path: hit.masked } : record;
1383
+ });
1384
+ }
1500
1385
  function globToRegExp(glob) {
1501
1386
  const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1502
1387
  const anchored = normalized.startsWith("/");
@@ -1574,10 +1459,10 @@ function matchesExclude(relativePath, pattern) {
1574
1459
  return posixPath === cleaned || posixPath.startsWith(`${cleaned}/`) || posixPath.includes(`/${cleaned}/`) || posixPath.endsWith(`/${cleaned}`);
1575
1460
  }
1576
1461
  function readGitignoreMatcher(dir) {
1577
- const gitignorePath = path3.join(dir, ".gitignore");
1462
+ const gitignorePath = path2.join(dir, ".gitignore");
1578
1463
  let raw;
1579
1464
  try {
1580
- raw = fs3.readFileSync(gitignorePath, "utf8");
1465
+ raw = fs2.readFileSync(gitignorePath, "utf8");
1581
1466
  } catch {
1582
1467
  return () => false;
1583
1468
  }
@@ -1639,7 +1524,7 @@ function isToolOwnedDirName(name) {
1639
1524
  }
1640
1525
  function hasSnapshotMarker(target) {
1641
1526
  try {
1642
- return fs3.statSync(target).isDirectory() && fs3.existsSync(path3.join(target, ".codebase-snapshot"));
1527
+ return fs2.statSync(target).isDirectory() && fs2.existsSync(path2.join(target, ".codebase-snapshot"));
1643
1528
  } catch {
1644
1529
  return false;
1645
1530
  }
@@ -1653,7 +1538,7 @@ function makeToolOwnedDecider(cwd) {
1653
1538
  if (depth === 1 && isToolOwnedDirName(prefix)) return true;
1654
1539
  let hasMarker = markers.get(prefix);
1655
1540
  if (hasMarker === void 0) {
1656
- hasMarker = fs3.existsSync(path3.join(cwd, prefix, ".codebase-snapshot"));
1541
+ hasMarker = fs2.existsSync(path2.join(cwd, prefix, ".codebase-snapshot"));
1657
1542
  markers.set(prefix, hasMarker);
1658
1543
  }
1659
1544
  if (hasMarker) return true;
@@ -1667,7 +1552,7 @@ function makeLinkProbe() {
1667
1552
  let known = cache.get(absolutePath);
1668
1553
  if (known === void 0) {
1669
1554
  try {
1670
- known = fs3.lstatSync(absolutePath).isSymbolicLink();
1555
+ known = fs2.lstatSync(absolutePath).isSymbolicLink();
1671
1556
  } catch {
1672
1557
  known = false;
1673
1558
  }
@@ -1680,7 +1565,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
1680
1565
  const isLink = makeLinkProbe();
1681
1566
  let rootReal = cwd;
1682
1567
  try {
1683
- rootReal = fs3.realpathSync(cwd);
1568
+ rootReal = fs2.realpathSync(cwd);
1684
1569
  } catch {
1685
1570
  }
1686
1571
  const reported = /* @__PURE__ */ new Set();
@@ -1690,7 +1575,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
1690
1575
  let linkedDir = null;
1691
1576
  for (let depth = 1; depth <= parts.length; depth += 1) {
1692
1577
  const prefix = parts.slice(0, depth).join("/");
1693
- if (isLink(path3.join(cwd, prefix))) {
1578
+ if (isLink(path2.join(cwd, prefix))) {
1694
1579
  linkedDir = prefix;
1695
1580
  break;
1696
1581
  }
@@ -1701,10 +1586,10 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
1701
1586
  }
1702
1587
  if (reported.has(linkedDir)) continue;
1703
1588
  reported.add(linkedDir);
1704
- const absolute = path3.join(cwd, linkedDir);
1589
+ const absolute = path2.join(cwd, linkedDir);
1705
1590
  let targetReal = absolute;
1706
1591
  try {
1707
- targetReal = fs3.realpathSync(absolute);
1592
+ targetReal = fs2.realpathSync(absolute);
1708
1593
  } catch {
1709
1594
  }
1710
1595
  const target = readLinkTarget(absolute, targetReal);
@@ -1727,6 +1612,7 @@ function collectFiles(cwd, options = {}) {
1727
1612
  const includePatterns = options.includePatterns ?? [];
1728
1613
  const excludePatterns = options.excludePatterns ?? [];
1729
1614
  const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
1615
+ const pathRules = options.secretRules;
1730
1616
  const isToolOwned = makeToolOwnedDecider(cwd);
1731
1617
  let candidates;
1732
1618
  const skipped = [];
@@ -1762,7 +1648,7 @@ function collectFiles(cwd, options = {}) {
1762
1648
  }
1763
1649
  }
1764
1650
  } catch {
1765
- const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set(), true);
1651
+ const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), true);
1766
1652
  candidates = walked.files;
1767
1653
  skipped.push(...walked.skipped);
1768
1654
  }
@@ -1800,7 +1686,7 @@ function collectFiles(cwd, options = {}) {
1800
1686
  });
1801
1687
  return false;
1802
1688
  }
1803
- const secret = findSecretInPath(file);
1689
+ const secret = findSecretInPath(file, pathRules);
1804
1690
  if (secret) {
1805
1691
  skipped.push({
1806
1692
  path: secret.masked,
@@ -1811,35 +1697,34 @@ function collectFiles(cwd, options = {}) {
1811
1697
  }
1812
1698
  return true;
1813
1699
  }).sort();
1814
- return { files, skipped };
1700
+ return { files, skipped: maskSkipPaths(skipped, pathRules) };
1815
1701
  }
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;
1702
+ function rootRealFor(root) {
1827
1703
  try {
1828
- rootReal = fs3.realpathSync(baseDir);
1704
+ return fs2.realpathSync(root);
1829
1705
  } catch {
1830
- rootReal = baseDir;
1706
+ return root;
1831
1707
  }
1708
+ }
1709
+ function walkDirectory(dir, baseDir, rootReal, visited, isRoot = false) {
1710
+ const results = { files: [], skipped: [] };
1711
+ if (visited.has(dir)) return results;
1712
+ visited.add(dir);
1832
1713
  const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
1833
1714
  let entries;
1834
1715
  try {
1835
- entries = fs3.readdirSync(dir, { withFileTypes: true });
1716
+ entries = fs2.readdirSync(dir, { withFileTypes: true });
1836
1717
  } catch {
1837
- results.skipped.push({ path: dir, reason: "read-error", detail: "directory unreadable" });
1718
+ results.skipped.push({
1719
+ path: path2.relative(baseDir, dir).replace(/\\/g, "/") || ".",
1720
+ reason: "read-error",
1721
+ detail: "directory unreadable"
1722
+ });
1838
1723
  return results;
1839
1724
  }
1840
1725
  for (const entry of entries) {
1841
- const fullPath = path3.join(dir, entry.name);
1842
- const relPath = path3.relative(baseDir, fullPath).replace(/\\/g, "/");
1726
+ const fullPath = path2.join(dir, entry.name);
1727
+ const relPath = path2.relative(baseDir, fullPath).replace(/\\/g, "/");
1843
1728
  if (isToolOwnedDirName(entry.name)) continue;
1844
1729
  if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
1845
1730
  results.skipped.push({
@@ -1872,14 +1757,14 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
1872
1757
  if (entry.isSymbolicLink()) {
1873
1758
  let targetReal;
1874
1759
  try {
1875
- targetReal = fs3.realpathSync(fullPath);
1760
+ targetReal = fs2.realpathSync(fullPath);
1876
1761
  } catch {
1877
1762
  results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
1878
1763
  continue;
1879
1764
  }
1880
1765
  let stats;
1881
1766
  try {
1882
- stats = fs3.statSync(fullPath);
1767
+ stats = fs2.statSync(fullPath);
1883
1768
  } catch {
1884
1769
  results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
1885
1770
  continue;
@@ -1913,7 +1798,7 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
1913
1798
  isFile = true;
1914
1799
  }
1915
1800
  if (isDirectory) {
1916
- const nested = walkDirectory(fullPath, baseDir, visited);
1801
+ const nested = walkDirectory(fullPath, baseDir, rootReal, visited);
1917
1802
  results.files.push(...nested.files);
1918
1803
  results.skipped.push(...nested.skipped);
1919
1804
  } else if (isFile) {
@@ -1923,7 +1808,141 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
1923
1808
  return results;
1924
1809
  }
1925
1810
 
1811
+ // src/diff.ts
1812
+ function side(manifest) {
1813
+ return {
1814
+ generatedAt: manifest.generatedAt,
1815
+ fingerprint: manifest.fingerprint,
1816
+ fileCount: manifest.counts.totalFiles,
1817
+ partsCount: manifest.counts.partsCount
1818
+ };
1819
+ }
1820
+ function hashMap(manifest) {
1821
+ const map = /* @__PURE__ */ new Map();
1822
+ for (const file of manifest.files) {
1823
+ if (file && typeof file.relativePath === "string") {
1824
+ map.set(file.relativePath, file.hashSha256);
1825
+ }
1826
+ }
1827
+ return map;
1828
+ }
1829
+ function manifestProblem(value) {
1830
+ if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
1831
+ const candidate = value;
1832
+ if (!Array.isArray(candidate.files)) return 'no "files" array';
1833
+ for (const entry of candidate.files) {
1834
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1835
+ return 'a "files" entry is not an object';
1836
+ }
1837
+ if (typeof entry.relativePath !== "string") {
1838
+ return 'a "files" entry has no "relativePath"';
1839
+ }
1840
+ }
1841
+ const counts = candidate.counts;
1842
+ if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
1843
+ return 'no "counts" object';
1844
+ }
1845
+ for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
1846
+ if (typeof counts[key] !== "number") {
1847
+ return `no numeric "counts.${key}"`;
1848
+ }
1849
+ }
1850
+ if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
1851
+ if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
1852
+ return null;
1853
+ }
1854
+ function assertUsableManifest(value, label) {
1855
+ const problem = manifestProblem(value);
1856
+ if (problem) {
1857
+ throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
1858
+ }
1859
+ }
1860
+ function compareManifests(before, after) {
1861
+ assertUsableManifest(before, "before");
1862
+ assertUsableManifest(after, "after");
1863
+ const left = hashMap(before);
1864
+ const right = hashMap(after);
1865
+ const added = [];
1866
+ const removed = [];
1867
+ const changed = [];
1868
+ let unchanged = 0;
1869
+ for (const [filePath, hash] of right) {
1870
+ const previous = left.get(filePath);
1871
+ if (previous === void 0) added.push(filePath);
1872
+ else if (previous !== hash) changed.push(filePath);
1873
+ else unchanged += 1;
1874
+ }
1875
+ for (const filePath of left.keys()) {
1876
+ if (!right.has(filePath)) removed.push(filePath);
1877
+ }
1878
+ const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
1879
+ const mask = (values) => values.map((value) => findSecretInPath(value)?.masked ?? value);
1880
+ return {
1881
+ before: side(before),
1882
+ after: side(after),
1883
+ added: mask(sort(added)),
1884
+ removed: mask(sort(removed)),
1885
+ changed: mask(sort(changed)),
1886
+ unchanged
1887
+ };
1888
+ }
1889
+ function loadManifest(target) {
1890
+ let isDirectory;
1891
+ try {
1892
+ isDirectory = fs3.statSync(target).isDirectory();
1893
+ } catch {
1894
+ throw new Error(`No snapshot found at ${target}: the path does not exist.`);
1895
+ }
1896
+ const manifestPath = isDirectory ? path3.join(target, "manifest.json") : target;
1897
+ let raw;
1898
+ try {
1899
+ raw = fs3.readFileSync(manifestPath, "utf8");
1900
+ } catch {
1901
+ throw new Error(`Could not read a manifest at ${manifestPath}.`);
1902
+ }
1903
+ let parsed;
1904
+ try {
1905
+ parsed = JSON.parse(raw);
1906
+ } catch (err) {
1907
+ throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
1908
+ }
1909
+ const problem = manifestProblem(parsed);
1910
+ if (problem) {
1911
+ throw new Error(
1912
+ `${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
1913
+ );
1914
+ }
1915
+ return parsed;
1916
+ }
1917
+ function renderDiffText(diff) {
1918
+ const lines = [];
1919
+ lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
1920
+ lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
1921
+ lines.push("");
1922
+ lines.push(` + ${diff.added.length} added`);
1923
+ lines.push(` - ${diff.removed.length} removed`);
1924
+ lines.push(` ~ ${diff.changed.length} changed`);
1925
+ lines.push(` = ${diff.unchanged} unchanged`);
1926
+ const section = (title, values) => {
1927
+ if (values.length === 0) return;
1928
+ lines.push("", title);
1929
+ for (const value of values.slice(0, 200)) lines.push(` ${value}`);
1930
+ if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
1931
+ };
1932
+ section("added:", diff.added);
1933
+ section("removed:", diff.removed);
1934
+ section("changed:", diff.changed);
1935
+ return lines.join("\n");
1936
+ }
1937
+
1938
+ // src/explain.ts
1939
+ import * as fs6 from "fs";
1940
+ import * as path7 from "path";
1941
+
1926
1942
  // src/config.ts
1943
+ import * as fs4 from "fs";
1944
+ import * as path4 from "path";
1945
+ import { pathToFileURL } from "url";
1927
1946
  var CONFIG_FILENAMES = [
1928
1947
  "snapshot.config.ts",
1929
1948
  "snapshot.config.js",
@@ -2501,6 +2520,7 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
2501
2520
  ...aliasedRules ?? []
2502
2521
  ]);
2503
2522
  const secretRules = disableDefaultPatterns ? compiledCustomRules : [...compileSecretRules(DEFAULT_SECRET_PATTERNS), ...compiledCustomRules];
2523
+ setActiveSecretRules(secretRules);
2504
2524
  const cliMonolith = cliOptions?.includeMonolith;
2505
2525
  const includeMonolith = cliMonolith ?? strictBoolean(userConfig?.includeMonolith, false, "`includeMonolith`");
2506
2526
  const artifacts = resolveArtifacts(
@@ -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.1";
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;
5541
+ const hit = exact(`${joined}${suffix}`);
5542
+ if (hit) return hit;
5464
5543
  }
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;
5468
- }
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({
@@ -6183,12 +6350,12 @@ function generateSnapshot(configInput, options) {
6183
6350
  const { content: _content, ...rest } = file;
6184
6351
  return rest;
6185
6352
  }),
6186
- skipped,
6353
+ skipped: maskSkipRecords(skipped, config.secretRules),
6187
6354
  warnings
6188
6355
  };
6189
6356
  manifest.warnings = [
6190
6357
  ...warnings,
6191
- ...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
6358
+ ...secretFindings.map((finding) => `${finding.rule} at ${maskPathText(finding.path ?? "unknown", config.secretRules)}:${finding.line}`)
6192
6359
  ];
6193
6360
  manifest.totals.warningCount = manifest.warnings.length;
6194
6361
  let monolithJson = null;
@@ -6234,31 +6401,33 @@ function generateSnapshot(configInput, options) {
6234
6401
  manifest.totals.warningCount = manifest.warnings.length;
6235
6402
  const { outDir, tmpDir } = resolveSafeOutputDirs(config);
6236
6403
  const { artifacts } = config;
6237
- assertReplaceable(tmpDir, "tmpDir");
6238
- assertReplaceable(outDir, "outDir");
6404
+ assertReplaceable(outDir, "outDir", config.secretRules);
6405
+ assertStagingRootUsable(tmpDir, config.secretRules);
6239
6406
  if (dryRun) {
6240
6407
  runAfterGenerate(config, manifest);
6241
6408
  return manifest;
6242
6409
  }
6243
- const releaseLock = acquireRunLock(outDir);
6244
- let stagingStarted = false;
6410
+ const releaseLock = acquireRunLock(tmpDir);
6411
+ let stagingDir = null;
6245
6412
  try {
6246
6413
  ensureParentDirectory(outDir, "outDir");
6247
- fs9.rmSync(tmpDir, { recursive: true, force: true });
6248
6414
  fs9.mkdirSync(tmpDir, { recursive: true });
6249
- stagingStarted = true;
6415
+ cleanStaleRunDirs(tmpDir);
6250
6416
  const snapshotId = crypto3.randomUUID();
6251
6417
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
6252
- writeOwnershipMarker(tmpDir, {
6418
+ stagingDir = path10.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
6419
+ fs9.mkdirSync(stagingDir);
6420
+ writeOwnershipMarker(stagingDir, {
6253
6421
  generator: GENERATOR_NAME,
6254
6422
  version: GENERATOR_VERSION,
6255
6423
  schemaVersion: SCHEMA_VERSION,
6256
6424
  createdAt,
6257
6425
  state: "staging",
6258
- id: snapshotId
6426
+ id: snapshotId,
6427
+ pid: process.pid
6259
6428
  });
6260
- const partsDir = path10.join(tmpDir, "parts");
6261
- const reportsDir = path10.join(tmpDir, "reports");
6429
+ const partsDir = path10.join(stagingDir, "parts");
6430
+ const reportsDir = path10.join(stagingDir, "reports");
6262
6431
  if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
6263
6432
  if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
6264
6433
  fs9.mkdirSync(reportsDir, { recursive: true });
@@ -6278,25 +6447,25 @@ function generateSnapshot(configInput, options) {
6278
6447
  if (artifacts.audit) {
6279
6448
  fs9.writeFileSync(
6280
6449
  path10.join(reportsDir, "audit.md"),
6281
- generateAuditReport(manifest, secretFindings),
6450
+ generateAuditReport(manifest, secretFindings, config.secretRules),
6282
6451
  "utf8"
6283
6452
  );
6284
6453
  }
6285
6454
  if (artifacts.readme) {
6286
- fs9.writeFileSync(path10.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
6455
+ fs9.writeFileSync(path10.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
6287
6456
  }
6288
6457
  if (artifacts.manifest) {
6289
- fs9.writeFileSync(path10.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6458
+ fs9.writeFileSync(path10.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
6290
6459
  }
6291
6460
  if (monolithJson !== null) {
6292
- fs9.writeFileSync(path10.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
6461
+ fs9.writeFileSync(path10.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
6293
6462
  }
6294
6463
  if (monolithMarkdown !== null) {
6295
- fs9.writeFileSync(path10.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
6464
+ fs9.writeFileSync(path10.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
6296
6465
  }
6297
- writeOwnershipMarker(tmpDir, markerPayload(tmpDir, "complete", snapshotId, createdAt));
6298
- swapIntoPlace(tmpDir, outDir);
6299
- stagingStarted = false;
6466
+ writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
6467
+ swapIntoPlace(stagingDir, outDir);
6468
+ stagingDir = null;
6300
6469
  const warningsBeforeHook = manifest.warnings.length;
6301
6470
  runAfterGenerate(config, manifest);
6302
6471
  if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
@@ -6305,14 +6474,15 @@ function generateSnapshot(configInput, options) {
6305
6474
  writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
6306
6475
  return manifest;
6307
6476
  } catch (err) {
6308
- if (stagingStarted) {
6477
+ if (stagingDir !== null) {
6309
6478
  try {
6310
- fs9.rmSync(tmpDir, { recursive: true, force: true });
6479
+ fs9.rmSync(stagingDir, { recursive: true, force: true });
6311
6480
  } catch {
6312
6481
  }
6313
6482
  }
6314
6483
  throw err;
6315
6484
  } finally {
6485
+ removeIfEmpty(tmpDir);
6316
6486
  releaseLock();
6317
6487
  }
6318
6488
  }
@@ -7601,8 +7771,7 @@ ${USAGE}
7601
7771
  warn(`\u274C ${manifestPath} is not a snapshot manifest: ${shapeError}`);
7602
7772
  return 1;
7603
7773
  }
7604
- const validation = validateSnapshot(resolved);
7605
- emit(invocation.format, { manifest, validation }, () => {
7774
+ emit(invocation.format, { manifest, validation: null }, () => {
7606
7775
  info(`
7607
7776
  \u{1F4CA} Codebase Snapshot Report (${resolved.cwd})`);
7608
7777
  info(` - Generated at: ${manifest.generatedAt}`);
@@ -7614,14 +7783,14 @@ ${USAGE}
7614
7783
  ` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
7615
7784
  );
7616
7785
  info(` - Total parts: ${manifest.counts.partsCount}`);
7617
- info(` - Integrity: ${validation.valid ? "\u2705 Clean (no drift)" : "\u274C Drift detected"}`);
7786
+ info(" - Integrity: not checked (run `codebase-snapshot validate`)");
7618
7787
  info(` - Warnings: ${manifest.warnings?.length ?? 0}`);
7619
7788
  info("\n Layers:");
7620
7789
  for (const [layer2, count] of Object.entries(manifest.layers)) {
7621
7790
  info(` - ${layer2.padEnd(16)}: ${count} files`);
7622
7791
  }
7623
7792
  });
7624
- return validation.valid ? 0 : 1;
7793
+ return 0;
7625
7794
  }
7626
7795
  if (invocation.command === "explain") {
7627
7796
  const result = explainPath(resolved, invocation.args[0]);