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