@ankurhalder/codebase-snapshot 4.0.0 → 4.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +463 -289
- package/dist/{chunk-FM4OOQPF.js → chunk-CY3XU5TS.js} +301 -127
- package/dist/cli.cjs +427 -253
- package/dist/cli.d.cts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +9 -6
- package/dist/index.cjs +299 -125
- 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,15 @@ function isSensitivePath(relativePath) {
|
|
|
1467
1334
|
}
|
|
1468
1335
|
return matchesSensitiveList(posixPath, base);
|
|
1469
1336
|
}
|
|
1470
|
-
function
|
|
1337
|
+
function pathScanRules(extraRules) {
|
|
1338
|
+
const fallback = extraRules ?? [];
|
|
1339
|
+
return [...DEFAULT_SECRET_PATTERNS, ...fallback];
|
|
1340
|
+
}
|
|
1341
|
+
function findSecretInPath(relativePath, extraRules) {
|
|
1471
1342
|
const posixPath = relativePath.replace(/\\/g, "/");
|
|
1472
1343
|
const spans = [];
|
|
1473
1344
|
const rules = [];
|
|
1474
|
-
for (const { rule, pattern } of
|
|
1345
|
+
for (const { rule, pattern } of pathScanRules(extraRules)) {
|
|
1475
1346
|
const scan = new RegExp(pattern.source, `${pattern.flags.replace(/[gy]/g, "")}g`);
|
|
1476
1347
|
let match;
|
|
1477
1348
|
while ((match = scan.exec(posixPath)) !== null) {
|
|
@@ -1479,24 +1350,39 @@ function findSecretInPath(relativePath) {
|
|
|
1479
1350
|
scan.lastIndex += 1;
|
|
1480
1351
|
continue;
|
|
1481
1352
|
}
|
|
1482
|
-
spans.push({ start: match.index, end: match.index + match[0].length
|
|
1353
|
+
spans.push({ start: match.index, end: match.index + match[0].length });
|
|
1483
1354
|
if (!rules.includes(rule)) rules.push(rule);
|
|
1484
1355
|
}
|
|
1485
1356
|
}
|
|
1486
1357
|
if (spans.length === 0) return null;
|
|
1487
1358
|
spans.sort((a, b) => a.start - b.start || a.end - b.end);
|
|
1359
|
+
const merged = [];
|
|
1360
|
+
for (const span of spans) {
|
|
1361
|
+
const last = merged[merged.length - 1];
|
|
1362
|
+
if (last && span.start <= last.end) last.end = Math.max(last.end, span.end);
|
|
1363
|
+
else merged.push({ start: span.start, end: span.end });
|
|
1364
|
+
}
|
|
1488
1365
|
let out = "";
|
|
1489
1366
|
let cursor = 0;
|
|
1490
|
-
for (const span of
|
|
1491
|
-
|
|
1492
|
-
const start = Math.max(span.start, cursor);
|
|
1493
|
-
out += posixPath.slice(cursor, start);
|
|
1367
|
+
for (const span of merged) {
|
|
1368
|
+
out += posixPath.slice(cursor, span.start);
|
|
1494
1369
|
out += REDACTION_MARKER;
|
|
1495
|
-
cursor =
|
|
1370
|
+
cursor = span.end;
|
|
1496
1371
|
}
|
|
1497
1372
|
out += posixPath.slice(cursor);
|
|
1498
1373
|
return { rules, masked: out };
|
|
1499
1374
|
}
|
|
1375
|
+
function maskSkipPaths(records, extraRules) {
|
|
1376
|
+
return records.map((record) => {
|
|
1377
|
+
const hit = findSecretInPath(record.path, extraRules);
|
|
1378
|
+
const detail = record.detail ? findSecretInPath(record.detail, extraRules) : null;
|
|
1379
|
+
return {
|
|
1380
|
+
...record,
|
|
1381
|
+
path: hit ? hit.masked : record.path,
|
|
1382
|
+
...detail ? { detail: detail.masked } : {}
|
|
1383
|
+
};
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1500
1386
|
function globToRegExp(glob) {
|
|
1501
1387
|
const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
|
|
1502
1388
|
const anchored = normalized.startsWith("/");
|
|
@@ -1574,10 +1460,10 @@ function matchesExclude(relativePath, pattern) {
|
|
|
1574
1460
|
return posixPath === cleaned || posixPath.startsWith(`${cleaned}/`) || posixPath.includes(`/${cleaned}/`) || posixPath.endsWith(`/${cleaned}`);
|
|
1575
1461
|
}
|
|
1576
1462
|
function readGitignoreMatcher(dir) {
|
|
1577
|
-
const gitignorePath =
|
|
1463
|
+
const gitignorePath = path2.join(dir, ".gitignore");
|
|
1578
1464
|
let raw;
|
|
1579
1465
|
try {
|
|
1580
|
-
raw =
|
|
1466
|
+
raw = fs2.readFileSync(gitignorePath, "utf8");
|
|
1581
1467
|
} catch {
|
|
1582
1468
|
return () => false;
|
|
1583
1469
|
}
|
|
@@ -1639,7 +1525,7 @@ function isToolOwnedDirName(name) {
|
|
|
1639
1525
|
}
|
|
1640
1526
|
function hasSnapshotMarker(target) {
|
|
1641
1527
|
try {
|
|
1642
|
-
return
|
|
1528
|
+
return fs2.statSync(target).isDirectory() && fs2.existsSync(path2.join(target, ".codebase-snapshot"));
|
|
1643
1529
|
} catch {
|
|
1644
1530
|
return false;
|
|
1645
1531
|
}
|
|
@@ -1653,7 +1539,7 @@ function makeToolOwnedDecider(cwd) {
|
|
|
1653
1539
|
if (depth === 1 && isToolOwnedDirName(prefix)) return true;
|
|
1654
1540
|
let hasMarker = markers.get(prefix);
|
|
1655
1541
|
if (hasMarker === void 0) {
|
|
1656
|
-
hasMarker =
|
|
1542
|
+
hasMarker = fs2.existsSync(path2.join(cwd, prefix, ".codebase-snapshot"));
|
|
1657
1543
|
markers.set(prefix, hasMarker);
|
|
1658
1544
|
}
|
|
1659
1545
|
if (hasMarker) return true;
|
|
@@ -1667,7 +1553,7 @@ function makeLinkProbe() {
|
|
|
1667
1553
|
let known = cache.get(absolutePath);
|
|
1668
1554
|
if (known === void 0) {
|
|
1669
1555
|
try {
|
|
1670
|
-
known =
|
|
1556
|
+
known = fs2.lstatSync(absolutePath).isSymbolicLink();
|
|
1671
1557
|
} catch {
|
|
1672
1558
|
known = false;
|
|
1673
1559
|
}
|
|
@@ -1680,7 +1566,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
|
|
|
1680
1566
|
const isLink = makeLinkProbe();
|
|
1681
1567
|
let rootReal = cwd;
|
|
1682
1568
|
try {
|
|
1683
|
-
rootReal =
|
|
1569
|
+
rootReal = fs2.realpathSync(cwd);
|
|
1684
1570
|
} catch {
|
|
1685
1571
|
}
|
|
1686
1572
|
const reported = /* @__PURE__ */ new Set();
|
|
@@ -1690,7 +1576,7 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
|
|
|
1690
1576
|
let linkedDir = null;
|
|
1691
1577
|
for (let depth = 1; depth <= parts.length; depth += 1) {
|
|
1692
1578
|
const prefix = parts.slice(0, depth).join("/");
|
|
1693
|
-
if (isLink(
|
|
1579
|
+
if (isLink(path2.join(cwd, prefix))) {
|
|
1694
1580
|
linkedDir = prefix;
|
|
1695
1581
|
break;
|
|
1696
1582
|
}
|
|
@@ -1701,10 +1587,10 @@ function dropLinkedDirectories(cwd, candidates, skipped) {
|
|
|
1701
1587
|
}
|
|
1702
1588
|
if (reported.has(linkedDir)) continue;
|
|
1703
1589
|
reported.add(linkedDir);
|
|
1704
|
-
const absolute =
|
|
1590
|
+
const absolute = path2.join(cwd, linkedDir);
|
|
1705
1591
|
let targetReal = absolute;
|
|
1706
1592
|
try {
|
|
1707
|
-
targetReal =
|
|
1593
|
+
targetReal = fs2.realpathSync(absolute);
|
|
1708
1594
|
} catch {
|
|
1709
1595
|
}
|
|
1710
1596
|
const target = readLinkTarget(absolute, targetReal);
|
|
@@ -1727,6 +1613,7 @@ function collectFiles(cwd, options = {}) {
|
|
|
1727
1613
|
const includePatterns = options.includePatterns ?? [];
|
|
1728
1614
|
const excludePatterns = options.excludePatterns ?? [];
|
|
1729
1615
|
const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
|
|
1616
|
+
const pathRules = options.secretRules;
|
|
1730
1617
|
const isToolOwned = makeToolOwnedDecider(cwd);
|
|
1731
1618
|
let candidates;
|
|
1732
1619
|
const skipped = [];
|
|
@@ -1762,7 +1649,7 @@ function collectFiles(cwd, options = {}) {
|
|
|
1762
1649
|
}
|
|
1763
1650
|
}
|
|
1764
1651
|
} catch {
|
|
1765
|
-
const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set(), true);
|
|
1652
|
+
const walked = walkDirectory(cwd, cwd, rootRealFor(cwd), /* @__PURE__ */ new Set(), true);
|
|
1766
1653
|
candidates = walked.files;
|
|
1767
1654
|
skipped.push(...walked.skipped);
|
|
1768
1655
|
}
|
|
@@ -1800,7 +1687,7 @@ function collectFiles(cwd, options = {}) {
|
|
|
1800
1687
|
});
|
|
1801
1688
|
return false;
|
|
1802
1689
|
}
|
|
1803
|
-
const secret = findSecretInPath(file);
|
|
1690
|
+
const secret = findSecretInPath(file, pathRules);
|
|
1804
1691
|
if (secret) {
|
|
1805
1692
|
skipped.push({
|
|
1806
1693
|
path: secret.masked,
|
|
@@ -1811,35 +1698,34 @@ function collectFiles(cwd, options = {}) {
|
|
|
1811
1698
|
}
|
|
1812
1699
|
return true;
|
|
1813
1700
|
}).sort();
|
|
1814
|
-
return { files, skipped };
|
|
1701
|
+
return { files, skipped: maskSkipPaths(skipped, pathRules) };
|
|
1815
1702
|
}
|
|
1816
|
-
function
|
|
1817
|
-
const results = { files: [], skipped: [] };
|
|
1818
|
-
let realDir;
|
|
1819
|
-
try {
|
|
1820
|
-
realDir = fs3.realpathSync(dir);
|
|
1821
|
-
} catch {
|
|
1822
|
-
realDir = dir;
|
|
1823
|
-
}
|
|
1824
|
-
if (visited.has(realDir)) return results;
|
|
1825
|
-
visited.add(realDir);
|
|
1826
|
-
let rootReal;
|
|
1703
|
+
function rootRealFor(root) {
|
|
1827
1704
|
try {
|
|
1828
|
-
|
|
1705
|
+
return fs2.realpathSync(root);
|
|
1829
1706
|
} catch {
|
|
1830
|
-
|
|
1707
|
+
return root;
|
|
1831
1708
|
}
|
|
1709
|
+
}
|
|
1710
|
+
function walkDirectory(dir, baseDir, rootReal, visited, isRoot = false) {
|
|
1711
|
+
const results = { files: [], skipped: [] };
|
|
1712
|
+
if (visited.has(dir)) return results;
|
|
1713
|
+
visited.add(dir);
|
|
1832
1714
|
const ignoredByGitignore = isRoot ? readGitignoreMatcher(baseDir) : () => false;
|
|
1833
1715
|
let entries;
|
|
1834
1716
|
try {
|
|
1835
|
-
entries =
|
|
1717
|
+
entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
1836
1718
|
} catch {
|
|
1837
|
-
results.skipped.push({
|
|
1719
|
+
results.skipped.push({
|
|
1720
|
+
path: path2.relative(baseDir, dir).replace(/\\/g, "/") || ".",
|
|
1721
|
+
reason: "read-error",
|
|
1722
|
+
detail: "directory unreadable"
|
|
1723
|
+
});
|
|
1838
1724
|
return results;
|
|
1839
1725
|
}
|
|
1840
1726
|
for (const entry of entries) {
|
|
1841
|
-
const fullPath =
|
|
1842
|
-
const relPath =
|
|
1727
|
+
const fullPath = path2.join(dir, entry.name);
|
|
1728
|
+
const relPath = path2.relative(baseDir, fullPath).replace(/\\/g, "/");
|
|
1843
1729
|
if (isToolOwnedDirName(entry.name)) continue;
|
|
1844
1730
|
if (DEFAULT_IGNORED_DIRS.includes(entry.name)) {
|
|
1845
1731
|
results.skipped.push({
|
|
@@ -1872,14 +1758,14 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
|
|
|
1872
1758
|
if (entry.isSymbolicLink()) {
|
|
1873
1759
|
let targetReal;
|
|
1874
1760
|
try {
|
|
1875
|
-
targetReal =
|
|
1761
|
+
targetReal = fs2.realpathSync(fullPath);
|
|
1876
1762
|
} catch {
|
|
1877
1763
|
results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
|
|
1878
1764
|
continue;
|
|
1879
1765
|
}
|
|
1880
1766
|
let stats;
|
|
1881
1767
|
try {
|
|
1882
|
-
stats =
|
|
1768
|
+
stats = fs2.statSync(fullPath);
|
|
1883
1769
|
} catch {
|
|
1884
1770
|
results.skipped.push({ path: relPath, reason: "read-error", detail: "unreadable symlink" });
|
|
1885
1771
|
continue;
|
|
@@ -1913,7 +1799,7 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
|
|
|
1913
1799
|
isFile = true;
|
|
1914
1800
|
}
|
|
1915
1801
|
if (isDirectory) {
|
|
1916
|
-
const nested = walkDirectory(fullPath, baseDir, visited);
|
|
1802
|
+
const nested = walkDirectory(fullPath, baseDir, rootReal, visited);
|
|
1917
1803
|
results.files.push(...nested.files);
|
|
1918
1804
|
results.skipped.push(...nested.skipped);
|
|
1919
1805
|
} else if (isFile) {
|
|
@@ -1923,7 +1809,141 @@ function walkDirectory(dir, baseDir, visited, isRoot = false) {
|
|
|
1923
1809
|
return results;
|
|
1924
1810
|
}
|
|
1925
1811
|
|
|
1812
|
+
// src/diff.ts
|
|
1813
|
+
function side(manifest) {
|
|
1814
|
+
return {
|
|
1815
|
+
generatedAt: manifest.generatedAt,
|
|
1816
|
+
fingerprint: manifest.fingerprint,
|
|
1817
|
+
fileCount: manifest.counts.totalFiles,
|
|
1818
|
+
partsCount: manifest.counts.partsCount
|
|
1819
|
+
};
|
|
1820
|
+
}
|
|
1821
|
+
function hashMap(manifest) {
|
|
1822
|
+
const map = /* @__PURE__ */ new Map();
|
|
1823
|
+
for (const file of manifest.files) {
|
|
1824
|
+
if (file && typeof file.relativePath === "string") {
|
|
1825
|
+
map.set(file.relativePath, file.hashSha256);
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
return map;
|
|
1829
|
+
}
|
|
1830
|
+
function manifestProblem(value) {
|
|
1831
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "not a JSON object";
|
|
1832
|
+
const candidate = value;
|
|
1833
|
+
if (!Array.isArray(candidate.files)) return 'no "files" array';
|
|
1834
|
+
for (const entry of candidate.files) {
|
|
1835
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
1836
|
+
return 'a "files" entry is not an object';
|
|
1837
|
+
}
|
|
1838
|
+
if (typeof entry.relativePath !== "string") {
|
|
1839
|
+
return 'a "files" entry has no "relativePath"';
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
const counts = candidate.counts;
|
|
1843
|
+
if (!counts || typeof counts !== "object" || Array.isArray(counts)) {
|
|
1844
|
+
return 'no "counts" object';
|
|
1845
|
+
}
|
|
1846
|
+
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
1847
|
+
if (typeof counts[key] !== "number") {
|
|
1848
|
+
return `no numeric "counts.${key}"`;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
if (typeof candidate.generatedAt !== "string") return 'no "generatedAt"';
|
|
1852
|
+
if (typeof candidate.fingerprint !== "string") return 'no "fingerprint"';
|
|
1853
|
+
return null;
|
|
1854
|
+
}
|
|
1855
|
+
function assertUsableManifest(value, label) {
|
|
1856
|
+
const problem = manifestProblem(value);
|
|
1857
|
+
if (problem) {
|
|
1858
|
+
throw new Error(`The ${label} manifest is not a snapshot manifest this tool wrote: ${problem}.`);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
function compareManifests(before, after) {
|
|
1862
|
+
assertUsableManifest(before, "before");
|
|
1863
|
+
assertUsableManifest(after, "after");
|
|
1864
|
+
const left = hashMap(before);
|
|
1865
|
+
const right = hashMap(after);
|
|
1866
|
+
const added = [];
|
|
1867
|
+
const removed = [];
|
|
1868
|
+
const changed = [];
|
|
1869
|
+
let unchanged = 0;
|
|
1870
|
+
for (const [filePath, hash] of right) {
|
|
1871
|
+
const previous = left.get(filePath);
|
|
1872
|
+
if (previous === void 0) added.push(filePath);
|
|
1873
|
+
else if (previous !== hash) changed.push(filePath);
|
|
1874
|
+
else unchanged += 1;
|
|
1875
|
+
}
|
|
1876
|
+
for (const filePath of left.keys()) {
|
|
1877
|
+
if (!right.has(filePath)) removed.push(filePath);
|
|
1878
|
+
}
|
|
1879
|
+
const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
1880
|
+
const mask = (values) => values.map((value) => findSecretInPath(value)?.masked ?? value);
|
|
1881
|
+
return {
|
|
1882
|
+
before: side(before),
|
|
1883
|
+
after: side(after),
|
|
1884
|
+
added: mask(sort(added)),
|
|
1885
|
+
removed: mask(sort(removed)),
|
|
1886
|
+
changed: mask(sort(changed)),
|
|
1887
|
+
unchanged
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
function loadManifest(target) {
|
|
1891
|
+
let isDirectory;
|
|
1892
|
+
try {
|
|
1893
|
+
isDirectory = fs3.statSync(target).isDirectory();
|
|
1894
|
+
} catch {
|
|
1895
|
+
throw new Error(`No snapshot found at ${target}: the path does not exist.`);
|
|
1896
|
+
}
|
|
1897
|
+
const manifestPath = isDirectory ? path3.join(target, "manifest.json") : target;
|
|
1898
|
+
let raw;
|
|
1899
|
+
try {
|
|
1900
|
+
raw = fs3.readFileSync(manifestPath, "utf8");
|
|
1901
|
+
} catch {
|
|
1902
|
+
throw new Error(`Could not read a manifest at ${manifestPath}.`);
|
|
1903
|
+
}
|
|
1904
|
+
let parsed;
|
|
1905
|
+
try {
|
|
1906
|
+
parsed = JSON.parse(raw);
|
|
1907
|
+
} catch (err) {
|
|
1908
|
+
throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
|
|
1909
|
+
}
|
|
1910
|
+
const problem = manifestProblem(parsed);
|
|
1911
|
+
if (problem) {
|
|
1912
|
+
throw new Error(
|
|
1913
|
+
`${manifestPath} is not a usable snapshot manifest: ${problem}. Point diff at a manifest.json written by \`generate\` (or at the directory that holds it).`
|
|
1914
|
+
);
|
|
1915
|
+
}
|
|
1916
|
+
return parsed;
|
|
1917
|
+
}
|
|
1918
|
+
function renderDiffText(diff) {
|
|
1919
|
+
const lines = [];
|
|
1920
|
+
lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
|
|
1921
|
+
lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
|
|
1922
|
+
lines.push("");
|
|
1923
|
+
lines.push(` + ${diff.added.length} added`);
|
|
1924
|
+
lines.push(` - ${diff.removed.length} removed`);
|
|
1925
|
+
lines.push(` ~ ${diff.changed.length} changed`);
|
|
1926
|
+
lines.push(` = ${diff.unchanged} unchanged`);
|
|
1927
|
+
const section = (title, values) => {
|
|
1928
|
+
if (values.length === 0) return;
|
|
1929
|
+
lines.push("", title);
|
|
1930
|
+
for (const value of values.slice(0, 200)) lines.push(` ${value}`);
|
|
1931
|
+
if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
|
|
1932
|
+
};
|
|
1933
|
+
section("added:", diff.added);
|
|
1934
|
+
section("removed:", diff.removed);
|
|
1935
|
+
section("changed:", diff.changed);
|
|
1936
|
+
return lines.join("\n");
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
// src/explain.ts
|
|
1940
|
+
import * as fs6 from "fs";
|
|
1941
|
+
import * as path7 from "path";
|
|
1942
|
+
|
|
1926
1943
|
// src/config.ts
|
|
1944
|
+
import * as fs4 from "fs";
|
|
1945
|
+
import * as path4 from "path";
|
|
1946
|
+
import { pathToFileURL } from "url";
|
|
1927
1947
|
var CONFIG_FILENAMES = [
|
|
1928
1948
|
"snapshot.config.ts",
|
|
1929
1949
|
"snapshot.config.js",
|
|
@@ -2809,18 +2829,22 @@ function readManifest2(config) {
|
|
|
2809
2829
|
function explainPath(config, target) {
|
|
2810
2830
|
const relative5 = normalizeTarget(config.cwd, target);
|
|
2811
2831
|
const absolute = path7.join(config.cwd, relative5);
|
|
2832
|
+
const maskedPath2 = findSecretInPath(relative5, config.secretRules)?.masked ?? relative5;
|
|
2812
2833
|
const result = {
|
|
2813
|
-
path:
|
|
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.2";
|
|
5406
5448
|
var SCHEMA_VERSION = "3.0.0";
|
|
5407
5449
|
function sha256Hex2(input) {
|
|
5408
5450
|
return crypto3.createHash("sha256").update(input).digest("hex");
|
|
@@ -5439,7 +5481,6 @@ function bump(counts, key) {
|
|
|
5439
5481
|
}
|
|
5440
5482
|
var RELATIVE_SPECIFIER = /^\.{1,2}\//;
|
|
5441
5483
|
var IMPORT_CANDIDATE_SUFFIXES = [
|
|
5442
|
-
"",
|
|
5443
5484
|
".ts",
|
|
5444
5485
|
".tsx",
|
|
5445
5486
|
".mts",
|
|
@@ -5453,23 +5494,59 @@ var IMPORT_CANDIDATE_SUFFIXES = [
|
|
|
5453
5494
|
"/index.js",
|
|
5454
5495
|
"/index.mjs"
|
|
5455
5496
|
];
|
|
5456
|
-
|
|
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 (
|
|
5464
|
-
}
|
|
5465
|
-
const extensionSwap = new RegExp(`${candidate}\\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$`);
|
|
5466
|
-
for (const file of knownFiles) {
|
|
5467
|
-
if (extensionSwap.test(file)) return file;
|
|
5541
|
+
const hit = exact(`${joined}${suffix}`);
|
|
5542
|
+
if (hit) return hit;
|
|
5468
5543
|
}
|
|
5469
|
-
|
|
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({
|
|
@@ -6118,6 +6285,7 @@ function generateSnapshot(configInput, options) {
|
|
|
6118
6285
|
const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
|
|
6119
6286
|
const validationErrors = verifyInMemory(files, parts, maxEffectiveBudget);
|
|
6120
6287
|
const totalContentBytes = files.reduce((total, file) => total + file.includedBytes, 0);
|
|
6288
|
+
const maskName = (value) => maskPathText(value, config.secretRules);
|
|
6121
6289
|
const manifest = {
|
|
6122
6290
|
schemaVersion: SCHEMA_VERSION,
|
|
6123
6291
|
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -6138,13 +6306,13 @@ function generateSnapshot(configInput, options) {
|
|
|
6138
6306
|
includeMonolith: config.includeMonolith,
|
|
6139
6307
|
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
6140
6308
|
artifacts: { ...config.artifacts },
|
|
6141
|
-
include: [...config.include],
|
|
6309
|
+
include: [...config.include].map(maskName),
|
|
6142
6310
|
preset: config.preset,
|
|
6143
6311
|
layersMode: config.layersMode,
|
|
6144
6312
|
budgets: { default: config.budgets.default, byLayer: { ...config.budgets.byLayer } },
|
|
6145
|
-
excludePatterns: [...config.excludePatterns],
|
|
6146
|
-
layers: config.layers.map((layer2) => layer2.name),
|
|
6147
|
-
kinds: config.kinds.map((kind) => kind.kind)
|
|
6313
|
+
excludePatterns: [...config.excludePatterns].map(maskName),
|
|
6314
|
+
layers: config.layers.map((layer2) => maskName(layer2.name)),
|
|
6315
|
+
kinds: config.kinds.map((kind) => maskName(kind.kind))
|
|
6148
6316
|
},
|
|
6149
6317
|
counts: {
|
|
6150
6318
|
totalFiles: files.length,
|
|
@@ -6183,12 +6351,12 @@ function generateSnapshot(configInput, options) {
|
|
|
6183
6351
|
const { content: _content, ...rest } = file;
|
|
6184
6352
|
return rest;
|
|
6185
6353
|
}),
|
|
6186
|
-
skipped,
|
|
6354
|
+
skipped: maskSkipRecords(skipped, config.secretRules),
|
|
6187
6355
|
warnings
|
|
6188
6356
|
};
|
|
6189
6357
|
manifest.warnings = [
|
|
6190
6358
|
...warnings,
|
|
6191
|
-
...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
|
|
6359
|
+
...secretFindings.map((finding) => `${finding.rule} at ${maskPathText(finding.path ?? "unknown", config.secretRules)}:${finding.line}`)
|
|
6192
6360
|
];
|
|
6193
6361
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
6194
6362
|
let monolithJson = null;
|
|
@@ -6234,31 +6402,33 @@ function generateSnapshot(configInput, options) {
|
|
|
6234
6402
|
manifest.totals.warningCount = manifest.warnings.length;
|
|
6235
6403
|
const { outDir, tmpDir } = resolveSafeOutputDirs(config);
|
|
6236
6404
|
const { artifacts } = config;
|
|
6237
|
-
assertReplaceable(
|
|
6238
|
-
|
|
6405
|
+
assertReplaceable(outDir, "outDir", config.secretRules);
|
|
6406
|
+
assertStagingRootUsable(tmpDir, config.secretRules);
|
|
6239
6407
|
if (dryRun) {
|
|
6240
6408
|
runAfterGenerate(config, manifest);
|
|
6241
6409
|
return manifest;
|
|
6242
6410
|
}
|
|
6243
|
-
const releaseLock = acquireRunLock(
|
|
6244
|
-
let
|
|
6411
|
+
const releaseLock = acquireRunLock(tmpDir);
|
|
6412
|
+
let stagingDir = null;
|
|
6245
6413
|
try {
|
|
6246
6414
|
ensureParentDirectory(outDir, "outDir");
|
|
6247
|
-
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
6248
6415
|
fs9.mkdirSync(tmpDir, { recursive: true });
|
|
6249
|
-
|
|
6416
|
+
cleanStaleRunDirs(tmpDir);
|
|
6250
6417
|
const snapshotId = crypto3.randomUUID();
|
|
6251
6418
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6252
|
-
|
|
6419
|
+
stagingDir = path10.join(tmpDir, `${RUN_DIR_PREFIX}${process.pid}-${snapshotId.slice(0, 8)}`);
|
|
6420
|
+
fs9.mkdirSync(stagingDir);
|
|
6421
|
+
writeOwnershipMarker(stagingDir, {
|
|
6253
6422
|
generator: GENERATOR_NAME,
|
|
6254
6423
|
version: GENERATOR_VERSION,
|
|
6255
6424
|
schemaVersion: SCHEMA_VERSION,
|
|
6256
6425
|
createdAt,
|
|
6257
6426
|
state: "staging",
|
|
6258
|
-
id: snapshotId
|
|
6427
|
+
id: snapshotId,
|
|
6428
|
+
pid: process.pid
|
|
6259
6429
|
});
|
|
6260
|
-
const partsDir = path10.join(
|
|
6261
|
-
const reportsDir = path10.join(
|
|
6430
|
+
const partsDir = path10.join(stagingDir, "parts");
|
|
6431
|
+
const reportsDir = path10.join(stagingDir, "reports");
|
|
6262
6432
|
if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
|
|
6263
6433
|
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
6264
6434
|
fs9.mkdirSync(reportsDir, { recursive: true });
|
|
@@ -6278,25 +6448,25 @@ function generateSnapshot(configInput, options) {
|
|
|
6278
6448
|
if (artifacts.audit) {
|
|
6279
6449
|
fs9.writeFileSync(
|
|
6280
6450
|
path10.join(reportsDir, "audit.md"),
|
|
6281
|
-
generateAuditReport(manifest, secretFindings),
|
|
6451
|
+
generateAuditReport(manifest, secretFindings, config.secretRules),
|
|
6282
6452
|
"utf8"
|
|
6283
6453
|
);
|
|
6284
6454
|
}
|
|
6285
6455
|
if (artifacts.readme) {
|
|
6286
|
-
fs9.writeFileSync(path10.join(
|
|
6456
|
+
fs9.writeFileSync(path10.join(stagingDir, "README.md"), generateReadme(manifest), "utf8");
|
|
6287
6457
|
}
|
|
6288
6458
|
if (artifacts.manifest) {
|
|
6289
|
-
fs9.writeFileSync(path10.join(
|
|
6459
|
+
fs9.writeFileSync(path10.join(stagingDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
6290
6460
|
}
|
|
6291
6461
|
if (monolithJson !== null) {
|
|
6292
|
-
fs9.writeFileSync(path10.join(
|
|
6462
|
+
fs9.writeFileSync(path10.join(stagingDir, "full-codebase.json"), monolithJson, "utf8");
|
|
6293
6463
|
}
|
|
6294
6464
|
if (monolithMarkdown !== null) {
|
|
6295
|
-
fs9.writeFileSync(path10.join(
|
|
6465
|
+
fs9.writeFileSync(path10.join(stagingDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
6296
6466
|
}
|
|
6297
|
-
writeOwnershipMarker(
|
|
6298
|
-
swapIntoPlace(
|
|
6299
|
-
|
|
6467
|
+
writeOwnershipMarker(stagingDir, markerPayload(stagingDir, "complete", snapshotId, createdAt));
|
|
6468
|
+
swapIntoPlace(stagingDir, outDir);
|
|
6469
|
+
stagingDir = null;
|
|
6300
6470
|
const warningsBeforeHook = manifest.warnings.length;
|
|
6301
6471
|
runAfterGenerate(config, manifest);
|
|
6302
6472
|
if (artifacts.manifest && manifest.warnings.length !== warningsBeforeHook) {
|
|
@@ -6305,14 +6475,15 @@ function generateSnapshot(configInput, options) {
|
|
|
6305
6475
|
writeOwnershipMarker(outDir, markerPayload(outDir, "complete", snapshotId, createdAt));
|
|
6306
6476
|
return manifest;
|
|
6307
6477
|
} catch (err) {
|
|
6308
|
-
if (
|
|
6478
|
+
if (stagingDir !== null) {
|
|
6309
6479
|
try {
|
|
6310
|
-
fs9.rmSync(
|
|
6480
|
+
fs9.rmSync(stagingDir, { recursive: true, force: true });
|
|
6311
6481
|
} catch {
|
|
6312
6482
|
}
|
|
6313
6483
|
}
|
|
6314
6484
|
throw err;
|
|
6315
6485
|
} finally {
|
|
6486
|
+
removeIfEmpty(tmpDir);
|
|
6316
6487
|
releaseLock();
|
|
6317
6488
|
}
|
|
6318
6489
|
}
|
|
@@ -6797,8 +6968,8 @@ function buildAgentContract() {
|
|
|
6797
6968
|
"snapshot.config.mjs",
|
|
6798
6969
|
"snapshot.config.cjs",
|
|
6799
6970
|
"snapshot.config.json",
|
|
6800
|
-
".snapshotrc",
|
|
6801
6971
|
".snapshotrc.json",
|
|
6972
|
+
".snapshotrc",
|
|
6802
6973
|
'package.json \u2192 "snapshot" key'
|
|
6803
6974
|
],
|
|
6804
6975
|
settings: [
|
|
@@ -7601,8 +7772,7 @@ ${USAGE}
|
|
|
7601
7772
|
warn(`\u274C ${manifestPath} is not a snapshot manifest: ${shapeError}`);
|
|
7602
7773
|
return 1;
|
|
7603
7774
|
}
|
|
7604
|
-
|
|
7605
|
-
emit(invocation.format, { manifest, validation }, () => {
|
|
7775
|
+
emit(invocation.format, { manifest, validation: null }, () => {
|
|
7606
7776
|
info(`
|
|
7607
7777
|
\u{1F4CA} Codebase Snapshot Report (${resolved.cwd})`);
|
|
7608
7778
|
info(` - Generated at: ${manifest.generatedAt}`);
|
|
@@ -7614,14 +7784,14 @@ ${USAGE}
|
|
|
7614
7784
|
` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
|
|
7615
7785
|
);
|
|
7616
7786
|
info(` - Total parts: ${manifest.counts.partsCount}`);
|
|
7617
|
-
info(
|
|
7787
|
+
info(" - Integrity: not checked (run `codebase-snapshot validate`)");
|
|
7618
7788
|
info(` - Warnings: ${manifest.warnings?.length ?? 0}`);
|
|
7619
7789
|
info("\n Layers:");
|
|
7620
7790
|
for (const [layer2, count] of Object.entries(manifest.layers)) {
|
|
7621
7791
|
info(` - ${layer2.padEnd(16)}: ${count} files`);
|
|
7622
7792
|
}
|
|
7623
7793
|
});
|
|
7624
|
-
return
|
|
7794
|
+
return 0;
|
|
7625
7795
|
}
|
|
7626
7796
|
if (invocation.command === "explain") {
|
|
7627
7797
|
const result = explainPath(resolved, invocation.args[0]);
|
|
@@ -7740,6 +7910,8 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
7740
7910
|
(segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`) || VCS_SEGMENTS.has(segment)
|
|
7741
7911
|
);
|
|
7742
7912
|
let running = false;
|
|
7913
|
+
let quietUntil = 0;
|
|
7914
|
+
const QUIET_MS = 1500;
|
|
7743
7915
|
const regenerate = async () => {
|
|
7744
7916
|
if (running) return;
|
|
7745
7917
|
running = true;
|
|
@@ -7777,6 +7949,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
7777
7949
|
} catch (err) {
|
|
7778
7950
|
warn(`\u274C ${err.message}`);
|
|
7779
7951
|
} finally {
|
|
7952
|
+
quietUntil = performance.now() + QUIET_MS;
|
|
7780
7953
|
running = false;
|
|
7781
7954
|
}
|
|
7782
7955
|
};
|
|
@@ -7784,6 +7957,7 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
7784
7957
|
void regenerate();
|
|
7785
7958
|
let timer = null;
|
|
7786
7959
|
const watcher = fs11.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
|
|
7960
|
+
if (performance.now() < quietUntil) return;
|
|
7787
7961
|
if (filename) {
|
|
7788
7962
|
const changed = path12.resolve(resolved.cwd, filename.toString());
|
|
7789
7963
|
if (isGenerated(changed)) return;
|