@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/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
|
|
10
|
-
import * as
|
|
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/
|
|
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 =
|
|
35
|
+
const full = path.join(cwd, name);
|
|
164
36
|
try {
|
|
165
|
-
const stats =
|
|
37
|
+
const stats = fs.statSync(full);
|
|
166
38
|
if (!stats.isFile() || stats.size > MANIFEST_MAX_BYTES) return null;
|
|
167
|
-
return
|
|
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
|
|
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
|
|
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 =
|
|
1399
|
-
return relative5 === "" || !relative5.startsWith(`..${
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1491
|
-
|
|
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 =
|
|
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 =
|
|
1462
|
+
const gitignorePath = path2.join(dir, ".gitignore");
|
|
1578
1463
|
let raw;
|
|
1579
1464
|
try {
|
|
1580
|
-
raw =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 =
|
|
1589
|
+
const absolute = path2.join(cwd, linkedDir);
|
|
1705
1590
|
let targetReal = absolute;
|
|
1706
1591
|
try {
|
|
1707
|
-
targetReal =
|
|
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
|
|
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
|
-
|
|
1704
|
+
return fs2.realpathSync(root);
|
|
1829
1705
|
} catch {
|
|
1830
|
-
|
|
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 =
|
|
1716
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
1836
1717
|
} catch {
|
|
1837
|
-
results.skipped.push({
|
|
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 =
|
|
1842
|
-
const relPath =
|
|
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 =
|
|
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 =
|
|
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:
|
|
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(
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
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
|
|
5459
|
-
|
|
5460
|
-
|
|
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
|
|
5463
|
-
if (
|
|
5541
|
+
const hit = exact(`${joined}${suffix}`);
|
|
5542
|
+
if (hit) return hit;
|
|
5464
5543
|
}
|
|
5465
|
-
const
|
|
5466
|
-
|
|
5467
|
-
|
|
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
|
|
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,
|
|
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
|
|
5576
|
-
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
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 (
|
|
5793
|
-
const {
|
|
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(
|
|
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(
|
|
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(
|
|
5836
|
-
const
|
|
5837
|
-
|
|
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
|
-
|
|
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
|
|
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 ${
|
|
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(
|
|
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(
|
|
6076
|
+
fs9.renameSync(stagingDir, outDir);
|
|
5915
6077
|
} catch (err) {
|
|
5916
6078
|
if (err.code === "EXDEV") {
|
|
5917
|
-
fs9.cpSync(
|
|
5918
|
-
fs9.rmSync(
|
|
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(
|
|
6238
|
-
|
|
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(
|
|
6244
|
-
let
|
|
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
|
-
|
|
6415
|
+
cleanStaleRunDirs(tmpDir);
|
|
6250
6416
|
const snapshotId = crypto3.randomUUID();
|
|
6251
6417
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6252
|
-
|
|
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(
|
|
6261
|
-
const reportsDir = path10.join(
|
|
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(
|
|
6455
|
+
fs9.writeFileSync(path10.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
|
|
6287
6456
|
}
|
|
6288
6457
|
if (artifacts.manifest) {
|
|
6289
|
-
fs9.writeFileSync(path10.join(
|
|
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(
|
|
6461
|
+
fs9.writeFileSync(path10.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
|
|
6293
6462
|
}
|
|
6294
6463
|
if (monolithMarkdown !== null) {
|
|
6295
|
-
fs9.writeFileSync(path10.join(
|
|
6464
|
+
fs9.writeFileSync(path10.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
6296
6465
|
}
|
|
6297
|
-
writeOwnershipMarker(
|
|
6298
|
-
swapIntoPlace(
|
|
6299
|
-
|
|
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 (
|
|
6477
|
+
if (stagingDir !== null) {
|
|
6309
6478
|
try {
|
|
6310
|
-
fs9.rmSync(
|
|
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
|
-
|
|
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(
|
|
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
|
|
7793
|
+
return 0;
|
|
7625
7794
|
}
|
|
7626
7795
|
if (invocation.command === "explain") {
|
|
7627
7796
|
const result = explainPath(resolved, invocation.args[0]);
|