@ankurhalder/codebase-snapshot 0.0.0-stage → 2.0.0

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 ADDED
@@ -0,0 +1,3725 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import * as fs9 from "fs";
5
+ import * as path10 from "path";
6
+ import { parseArgs } from "util";
7
+
8
+ // src/diff.ts
9
+ import * as fs from "fs";
10
+ import * as path from "path";
11
+ function side(manifest) {
12
+ return {
13
+ generatedAt: manifest.generatedAt,
14
+ fingerprint: manifest.fingerprint,
15
+ fileCount: manifest.counts.totalFiles,
16
+ partsCount: manifest.counts.partsCount
17
+ };
18
+ }
19
+ function hashMap(manifest) {
20
+ const map = /* @__PURE__ */ new Map();
21
+ for (const file of manifest.files) {
22
+ if (file && typeof file.relativePath === "string") {
23
+ map.set(file.relativePath, file.hashSha256);
24
+ }
25
+ }
26
+ return map;
27
+ }
28
+ function compareManifests(before, after) {
29
+ const left = hashMap(before);
30
+ const right = hashMap(after);
31
+ const added = [];
32
+ const removed = [];
33
+ const changed = [];
34
+ let unchanged = 0;
35
+ for (const [filePath, hash] of right) {
36
+ const previous = left.get(filePath);
37
+ if (previous === void 0) added.push(filePath);
38
+ else if (previous !== hash) changed.push(filePath);
39
+ else unchanged += 1;
40
+ }
41
+ for (const filePath of left.keys()) {
42
+ if (!right.has(filePath)) removed.push(filePath);
43
+ }
44
+ const sort = (values) => values.sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
45
+ return {
46
+ before: side(before),
47
+ after: side(after),
48
+ added: sort(added),
49
+ removed: sort(removed),
50
+ changed: sort(changed),
51
+ unchanged
52
+ };
53
+ }
54
+ function loadManifest(target) {
55
+ const manifestPath = fs.statSync(target).isDirectory() ? path.join(target, "manifest.json") : target;
56
+ let raw;
57
+ try {
58
+ raw = fs.readFileSync(manifestPath, "utf8");
59
+ } catch {
60
+ throw new Error(`Could not read a manifest at ${manifestPath}.`);
61
+ }
62
+ let parsed;
63
+ try {
64
+ parsed = JSON.parse(raw);
65
+ } catch (err) {
66
+ throw new Error(`Could not parse ${manifestPath}: ${err.message}`);
67
+ }
68
+ if (!parsed || !Array.isArray(parsed.files)) {
69
+ throw new Error(`${manifestPath} is not a snapshot manifest (no "files" array).`);
70
+ }
71
+ return parsed;
72
+ }
73
+ function renderDiffText(diff) {
74
+ const lines = [];
75
+ lines.push(`before ${diff.before.generatedAt} ${diff.before.fileCount} file(s), ${diff.before.partsCount} part(s)`);
76
+ lines.push(`after ${diff.after.generatedAt} ${diff.after.fileCount} file(s), ${diff.after.partsCount} part(s)`);
77
+ lines.push("");
78
+ lines.push(` + ${diff.added.length} added`);
79
+ lines.push(` - ${diff.removed.length} removed`);
80
+ lines.push(` ~ ${diff.changed.length} changed`);
81
+ lines.push(` = ${diff.unchanged} unchanged`);
82
+ const section = (title, values) => {
83
+ if (values.length === 0) return;
84
+ lines.push("", title);
85
+ for (const value of values.slice(0, 200)) lines.push(` ${value}`);
86
+ if (values.length > 200) lines.push(` ... and ${values.length - 200} more`);
87
+ };
88
+ section("added:", diff.added);
89
+ section("removed:", diff.removed);
90
+ section("changed:", diff.changed);
91
+ return lines.join("\n");
92
+ }
93
+
94
+ // src/explain.ts
95
+ import * as fs5 from "fs";
96
+ import * as path6 from "path";
97
+
98
+ // src/config.ts
99
+ import * as fs4 from "fs";
100
+ import * as path4 from "path";
101
+ import { pathToFileURL } from "url";
102
+
103
+ // src/presets/index.ts
104
+ import * as fs2 from "fs";
105
+ import * as path2 from "path";
106
+
107
+ // src/presets/nextjs.ts
108
+ var NEXTJS_LAYERS = [
109
+ { name: "app", match: (p) => p.startsWith("src/app/") || p.startsWith("app/") },
110
+ { name: "features", match: (p) => p.startsWith("src/features/") || p.startsWith("features/") },
111
+ { name: "shared", match: (p) => p.startsWith("src/shared/") || p.startsWith("shared/") },
112
+ { name: "layout", match: (p) => p.startsWith("src/layout/") || p.startsWith("layout/") },
113
+ { name: "scripts", match: (p) => p.startsWith("scripts/") },
114
+ { name: "docs", match: (p) => p.startsWith("docs/") },
115
+ {
116
+ name: "config",
117
+ match: (p) => p.startsWith(".github/") || p.startsWith(".husky/") || p.startsWith(".vscode/") || p.startsWith(".") || p.includes("config")
118
+ },
119
+ { name: "root", match: (p) => !p.includes("/") }
120
+ ];
121
+ var NEXTJS_KINDS = [
122
+ { kind: "test", match: (p) => /\.(test|spec)\.[jt]sx?$/.test(p.toLowerCase()) },
123
+ {
124
+ kind: "route",
125
+ match: (p) => {
126
+ const l = p.toLowerCase();
127
+ return l.endsWith("/route.ts") || l.endsWith("/route.tsx") || l.startsWith("src/app/api/");
128
+ }
129
+ },
130
+ { kind: "handler", match: (p) => p.includes(".handler.") || p.endsWith(".handler.ts") },
131
+ { kind: "repository", match: (p) => p.includes("repositories/") || p.includes("repository") },
132
+ { kind: "service", match: (p) => p.includes("service") || p.includes(".factory.") },
133
+ { kind: "schema", match: (p) => p.includes("schema") || p.includes("zod") },
134
+ { kind: "model", match: (p) => p.includes("model") || p.includes("types/") },
135
+ { kind: "type", match: (p) => p.endsWith(".d.ts") || p.includes("types.ts") },
136
+ {
137
+ kind: "hook",
138
+ match: (p) => {
139
+ const base = p.split("/").pop() || "";
140
+ return p.includes("/hooks/") || /^use[A-Z]/.test(base);
141
+ }
142
+ },
143
+ {
144
+ kind: "component",
145
+ match: (p) => {
146
+ const base = p.split("/").pop() || "";
147
+ return p.includes("/components/") || base.endsWith(".tsx") || base.endsWith(".jsx");
148
+ }
149
+ },
150
+ { kind: "data", match: (p) => p.endsWith(".json") || p.endsWith(".csv") },
151
+ { kind: "doc", match: (p) => p.endsWith(".md") || p.endsWith(".mdx") },
152
+ { kind: "script", match: (p) => p.startsWith("scripts/") || p.endsWith(".sh") || p.endsWith(".ps1") },
153
+ {
154
+ kind: "config",
155
+ match: (p) => {
156
+ const base = p.split("/").pop() || "";
157
+ return ["package.json", "tsconfig.json", "next.config.ts", "next.config.js", "eslint.config.mjs"].includes(base) || base.startsWith(".");
158
+ }
159
+ }
160
+ ];
161
+
162
+ // src/presets/vite.ts
163
+ var VITE_LAYERS = [
164
+ { name: "pages", match: (p) => p.startsWith("src/pages/") || p.startsWith("src/views/") },
165
+ { name: "components", match: (p) => p.startsWith("src/components/") },
166
+ { name: "hooks", match: (p) => p.startsWith("src/hooks/") },
167
+ { name: "services", match: (p) => p.startsWith("src/services/") || p.startsWith("src/api/") },
168
+ { name: "shared", match: (p) => p.startsWith("src/shared/") || p.startsWith("src/common/") },
169
+ { name: "styles", match: (p) => p.startsWith("src/styles/") || p.startsWith("src/assets/") },
170
+ { name: "scripts", match: (p) => p.startsWith("scripts/") },
171
+ { name: "docs", match: (p) => p.startsWith("docs/") },
172
+ {
173
+ name: "config",
174
+ match: (p) => p.startsWith(".github/") || p.startsWith(".husky/") || p.startsWith(".") || p.includes("config")
175
+ },
176
+ { name: "root", match: (p) => !p.includes("/") }
177
+ ];
178
+ var VITE_KINDS = [
179
+ { kind: "test", match: (p) => /\.(test|spec)\.[jt]sx?$/.test(p.toLowerCase()) },
180
+ {
181
+ kind: "component",
182
+ match: (p) => {
183
+ const base = p.split("/").pop() || "";
184
+ return p.includes("/components/") || base.endsWith(".tsx") || base.endsWith(".jsx") || base.endsWith(".vue");
185
+ }
186
+ },
187
+ {
188
+ kind: "hook",
189
+ match: (p) => {
190
+ const base = p.split("/").pop() || "";
191
+ return p.includes("/hooks/") || /^use[A-Z]/.test(base);
192
+ }
193
+ },
194
+ { kind: "service", match: (p) => p.includes("service") || p.includes("api") },
195
+ { kind: "type", match: (p) => p.endsWith(".d.ts") || p.includes("types") },
196
+ { kind: "style", match: (p) => p.endsWith(".css") || p.endsWith(".scss") },
197
+ { kind: "data", match: (p) => p.endsWith(".json") || p.endsWith(".csv") },
198
+ { kind: "doc", match: (p) => p.endsWith(".md") || p.endsWith(".mdx") },
199
+ { kind: "script", match: (p) => p.startsWith("scripts/") || p.endsWith(".sh") || p.endsWith(".ps1") },
200
+ {
201
+ kind: "config",
202
+ match: (p) => {
203
+ const base = p.split("/").pop() || "";
204
+ return ["package.json", "tsconfig.json", "vite.config.ts", "vite.config.js"].includes(base) || base.startsWith(".");
205
+ }
206
+ }
207
+ ];
208
+
209
+ // src/presets/node.ts
210
+ var NODE_LAYERS = [
211
+ { name: "core", match: (p) => p.startsWith("src/core/") || p.startsWith("core/") },
212
+ { name: "protocols", match: (p) => p.startsWith("src/protocols/") || p.startsWith("protocols/") },
213
+ { name: "providers", match: (p) => p.startsWith("src/providers/") || p.startsWith("providers/") },
214
+ { name: "security", match: (p) => p.startsWith("src/security/") || p.startsWith("security/") },
215
+ { name: "db", match: (p) => p.startsWith("src/db/") || p.startsWith("db/") || p.startsWith("prisma/") },
216
+ { name: "api", match: (p) => p.startsWith("src/api/") || p.startsWith("api/") || p.startsWith("routes/") },
217
+ { name: "services", match: (p) => p.startsWith("src/services/") || p.startsWith("services/") },
218
+ { name: "controllers", match: (p) => p.startsWith("src/controllers/") || p.startsWith("controllers/") },
219
+ { name: "scripts", match: (p) => p.startsWith("scripts/") },
220
+ { name: "docs", match: (p) => p.startsWith("docs/") },
221
+ {
222
+ name: "config",
223
+ match: (p) => p.startsWith(".github/") || p.startsWith(".husky/") || p.startsWith(".") || p.includes("config")
224
+ },
225
+ { name: "root", match: (p) => !p.includes("/") }
226
+ ];
227
+ var NODE_KINDS = [
228
+ { kind: "test", match: (p) => /\.(test|spec)\.[jt]sx?$/.test(p.toLowerCase()) },
229
+ { kind: "route", match: (p) => p.includes("/routes/") || p.includes("route.") || p.includes("/api/") },
230
+ { kind: "controller", match: (p) => p.includes("controller") },
231
+ { kind: "service", match: (p) => p.includes("service") },
232
+ { kind: "provider", match: (p) => p.includes("provider") },
233
+ { kind: "model", match: (p) => p.includes("model") || p.includes("entity") },
234
+ { kind: "schema", match: (p) => p.includes("schema") || p.includes("zod") || p.includes("dto") },
235
+ { kind: "type", match: (p) => p.endsWith(".d.ts") || p.includes("types.") || p.includes("type.") },
236
+ { kind: "security", match: (p) => p.includes("auth") || p.includes("security") || p.includes("guard") },
237
+ { kind: "db", match: (p) => p.includes("db") || p.includes("database") || p.includes("migration") },
238
+ { kind: "data", match: (p) => p.endsWith(".json") || p.endsWith(".csv") },
239
+ { kind: "doc", match: (p) => p.endsWith(".md") || p.endsWith(".mdx") },
240
+ { kind: "script", match: (p) => p.startsWith("scripts/") || p.endsWith(".sh") || p.endsWith(".ps1") },
241
+ {
242
+ kind: "config",
243
+ match: (p) => {
244
+ const base = p.split("/").pop() || "";
245
+ return ["package.json", "tsconfig.json", "Dockerfile"].includes(base) || base.startsWith(".");
246
+ }
247
+ }
248
+ ];
249
+
250
+ // src/presets/default.ts
251
+ var DEFAULT_LAYERS = [
252
+ { name: "src", match: (p) => p.startsWith("src/") },
253
+ { name: "packages", match: (p) => p.startsWith("packages/") },
254
+ { name: "scripts", match: (p) => p.startsWith("scripts/") },
255
+ { name: "docs", match: (p) => p.startsWith("docs/") },
256
+ { name: "test", match: (p) => p.startsWith("test/") || p.startsWith("tests/") },
257
+ {
258
+ name: "config",
259
+ match: (p) => p.startsWith(".github/") || p.startsWith(".husky/") || p.startsWith(".") || p.includes("config")
260
+ },
261
+ { name: "root", match: (p) => !p.includes("/") }
262
+ ];
263
+ var DEFAULT_KINDS = [
264
+ { kind: "test", match: (p) => /\.(test|spec)\.[jt]sx?$/.test(p.toLowerCase()) },
265
+ {
266
+ kind: "component",
267
+ match: (p) => {
268
+ const base = p.split("/").pop() || "";
269
+ return base.endsWith(".tsx") || base.endsWith(".jsx") || p.includes("component");
270
+ }
271
+ },
272
+ {
273
+ kind: "hook",
274
+ match: (p) => {
275
+ const base = p.split("/").pop() || "";
276
+ return /^use[A-Z]/.test(base) || p.includes("/hooks/");
277
+ }
278
+ },
279
+ { kind: "type", match: (p) => p.endsWith(".d.ts") || p.includes("type") },
280
+ { kind: "data", match: (p) => p.endsWith(".json") || p.endsWith(".csv") || p.endsWith(".yaml") || p.endsWith(".yml") },
281
+ { kind: "doc", match: (p) => p.endsWith(".md") || p.endsWith(".mdx") || p.endsWith(".txt") },
282
+ { kind: "script", match: (p) => p.startsWith("scripts/") || p.endsWith(".sh") || p.endsWith(".ps1") },
283
+ {
284
+ kind: "config",
285
+ match: (p) => {
286
+ const base = p.split("/").pop() || "";
287
+ return base.startsWith(".") || base.includes("config") || base === "package.json";
288
+ }
289
+ }
290
+ ];
291
+
292
+ // src/presets/index.ts
293
+ function detectPreset(cwd) {
294
+ const hasNextConfig = fs2.existsSync(path2.join(cwd, "next.config.ts")) || fs2.existsSync(path2.join(cwd, "next.config.js")) || fs2.existsSync(path2.join(cwd, "next.config.mjs"));
295
+ const hasViteConfig = fs2.existsSync(path2.join(cwd, "vite.config.ts")) || fs2.existsSync(path2.join(cwd, "vite.config.js")) || fs2.existsSync(path2.join(cwd, "vite.config.mjs"));
296
+ const pkgJsonPath = path2.join(cwd, "package.json");
297
+ let pkgDependencies = {};
298
+ if (fs2.existsSync(pkgJsonPath)) {
299
+ try {
300
+ const parsed = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf8"));
301
+ pkgDependencies = {
302
+ ...parsed.dependencies || {},
303
+ ...parsed.devDependencies || {}
304
+ };
305
+ } catch {
306
+ pkgDependencies = {};
307
+ }
308
+ }
309
+ if (hasNextConfig || pkgDependencies["next"]) {
310
+ return {
311
+ presetName: "nextjs",
312
+ layers: NEXTJS_LAYERS,
313
+ kinds: NEXTJS_KINDS
314
+ };
315
+ }
316
+ if (hasViteConfig || pkgDependencies["vite"]) {
317
+ return {
318
+ presetName: "vite",
319
+ layers: VITE_LAYERS,
320
+ kinds: VITE_KINDS
321
+ };
322
+ }
323
+ if (pkgDependencies["express"] || pkgDependencies["fastify"] || pkgDependencies["@nestjs/core"] || fs2.existsSync(path2.join(cwd, "src/core")) || fs2.existsSync(path2.join(cwd, "src/protocols"))) {
324
+ return {
325
+ presetName: "node",
326
+ layers: NODE_LAYERS,
327
+ kinds: NODE_KINDS
328
+ };
329
+ }
330
+ return {
331
+ presetName: "default",
332
+ layers: DEFAULT_LAYERS,
333
+ kinds: DEFAULT_KINDS
334
+ };
335
+ }
336
+
337
+ // src/scanner/files.ts
338
+ import * as childProcess from "child_process";
339
+ import * as fs3 from "fs";
340
+ import * as path3 from "path";
341
+ function getGitInfo(cwd, fallbackBranch = "main") {
342
+ try {
343
+ const branch = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
344
+ cwd,
345
+ stdio: ["ignore", "pipe", "ignore"],
346
+ encoding: "utf8"
347
+ }).trim() || fallbackBranch;
348
+ const commit = childProcess.execSync("git rev-parse HEAD", {
349
+ cwd,
350
+ stdio: ["ignore", "pipe", "ignore"],
351
+ encoding: "utf8"
352
+ }).trim() || "unknown";
353
+ const status = childProcess.execSync("git status --porcelain", {
354
+ cwd,
355
+ stdio: ["ignore", "pipe", "ignore"],
356
+ encoding: "utf8"
357
+ }).trim();
358
+ return { branch, commit, isDirty: status.length > 0 };
359
+ } catch {
360
+ return { branch: fallbackBranch, commit: "unknown", isDirty: false };
361
+ }
362
+ }
363
+ var DEFAULT_IGNORED_DIRS = [
364
+ "node_modules",
365
+ ".git",
366
+ "dist",
367
+ "build",
368
+ ".next",
369
+ ".nuxt",
370
+ ".turbo",
371
+ ".cache",
372
+ "coverage",
373
+ "out",
374
+ "snapshot",
375
+ "snapshot.tmp"
376
+ ];
377
+ var DEFAULT_IGNORED_FILES = [
378
+ "package-lock.json",
379
+ "yarn.lock",
380
+ "pnpm-lock.yaml",
381
+ "bun.lockb",
382
+ "composer.lock"
383
+ ];
384
+ var SENSITIVE_BASENAME = /^(?:\.env(?:[._-][A-Za-z0-9_-]*)*|\.env(?:backup|bak|old|orig|copy|save)|\.envrc|\.npmrc(?:\.[A-Za-z0-9_-]+)?|\.yarnrc(?:\.yml)?|\.netrc(?:\.[A-Za-z0-9_-]+)?|\.git-credentials|\.htpasswd|\.pgpass|id_[a-z0-9_]+(?:\.[A-Za-z0-9_-]+)?|credentials(?:\.json)?|service[-_]?account(?:\.[a-z0-9_-]+)*\.json|secrets?\.(?:json|ya?ml))$/i;
385
+ var SENSITIVE_PATH = /(?:^|\/)\.(?:aws|ssh|gnupg)\/|(?:^|\/)\.docker\/config\.json$|(?:^|\/)\.kube\/config$/i;
386
+ var SENSITIVE_EXT = /\.(?:pem|key|p12|pfx|jks|keystore|ppk|asc|kdbx|tfstate|tfvars)$/i;
387
+ var TEMPLATE_SUFFIX = /\.(?:example|sample|template|dist)(?=$|[.])/i;
388
+ var matchesSensitiveList = (posixPath, base) => SENSITIVE_BASENAME.test(base) || SENSITIVE_PATH.test(posixPath) || SENSITIVE_EXT.test(base);
389
+ function isSensitivePath(relativePath) {
390
+ const posixPath = relativePath.replace(/\\/g, "/");
391
+ const base = posixPath.split("/").pop() ?? posixPath;
392
+ if (TEMPLATE_SUFFIX.test(base)) {
393
+ const stripped = base.replace(new RegExp(TEMPLATE_SUFFIX.source, "gi"), "");
394
+ if (matchesSensitiveList(posixPath, stripped)) return false;
395
+ }
396
+ return matchesSensitiveList(posixPath, base);
397
+ }
398
+ function globToRegExp(glob) {
399
+ const normalized = glob.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
400
+ const anchored = normalized.startsWith("/");
401
+ const body = anchored ? normalized.slice(1) : normalized;
402
+ const prefix = anchored || body.includes("/") ? "^" : "(?:^|.*/)";
403
+ return new RegExp(`${prefix}${globBodyToRegExp(body, true)}(?:/.*)?$`);
404
+ }
405
+ function globBodyToRegExp(body, allowBraces) {
406
+ let out = "";
407
+ for (let i = 0; i < body.length; i++) {
408
+ const char = body[i];
409
+ if (char === "*") {
410
+ if (body[i + 1] === "*") {
411
+ i += 1;
412
+ if (body[i + 1] === "/") {
413
+ i += 1;
414
+ out += "(?:.*/)?";
415
+ } else {
416
+ out += ".*";
417
+ }
418
+ } else {
419
+ out += "[^/]*";
420
+ }
421
+ continue;
422
+ }
423
+ if (char === "?") {
424
+ out += "[^/]";
425
+ continue;
426
+ }
427
+ if (char === "{" && allowBraces) {
428
+ const close = findClosing(body, i, "{", "}");
429
+ if (close !== -1) {
430
+ const alternatives = body.slice(i + 1, close).split(",").map((part) => globBodyToRegExp(part.trim(), false));
431
+ if (alternatives.length > 1) {
432
+ out += `(?:${alternatives.join("|")})`;
433
+ i = close;
434
+ continue;
435
+ }
436
+ }
437
+ }
438
+ if (char === "[") {
439
+ const close = body.indexOf("]", i + 1);
440
+ if (close !== -1 && close > i + 1) {
441
+ const inner = body.slice(i + 1, close);
442
+ const negated = inner.startsWith("!") || inner.startsWith("^");
443
+ const chars = negated ? inner.slice(1) : inner;
444
+ const safe = chars.replace(/[\\^\]]/g, (value) => `\\${value}`);
445
+ out += negated ? `[^/${safe}]` : `[${safe}]`;
446
+ i = close;
447
+ continue;
448
+ }
449
+ }
450
+ out += "\\^$.|+()[]{}".includes(char) ? `\\${char}` : char;
451
+ }
452
+ return out;
453
+ }
454
+ function findClosing(body, start, open, close) {
455
+ let depth = 0;
456
+ for (let i = start; i < body.length; i++) {
457
+ if (body[i] === open) depth += 1;
458
+ else if (body[i] === close) {
459
+ depth -= 1;
460
+ if (depth === 0) return i;
461
+ }
462
+ }
463
+ return -1;
464
+ }
465
+ function matchesExclude(relativePath, pattern) {
466
+ const posixPath = relativePath.replace(/\\/g, "/");
467
+ const cleaned = pattern.replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "").trim();
468
+ if (!cleaned) return false;
469
+ if (/[*?{[]/.test(cleaned)) {
470
+ return globToRegExp(cleaned).test(posixPath);
471
+ }
472
+ return posixPath === cleaned || posixPath.startsWith(`${cleaned}/`) || posixPath.includes(`/${cleaned}/`) || posixPath.endsWith(`/${cleaned}`);
473
+ }
474
+ function readGitignoreMatcher(dir) {
475
+ const gitignorePath = path3.join(dir, ".gitignore");
476
+ let raw;
477
+ try {
478
+ raw = fs3.readFileSync(gitignorePath, "utf8");
479
+ } catch {
480
+ return () => false;
481
+ }
482
+ const rules = [];
483
+ for (const line of raw.split("\n")) {
484
+ const trimmed = line.trim();
485
+ if (!trimmed || trimmed.startsWith("#")) continue;
486
+ const negated = trimmed.startsWith("!");
487
+ let pattern = negated ? trimmed.slice(1) : trimmed;
488
+ const dirOnly = pattern.endsWith("/");
489
+ if (dirOnly) pattern = pattern.slice(0, -1);
490
+ if (pattern) rules.push({ pattern, negated, dirOnly });
491
+ }
492
+ return (relativePath) => {
493
+ let ignored = false;
494
+ for (const rule of rules) {
495
+ const target = rule.dirOnly ? relativePath.replace(/\/.*$/, "") : relativePath;
496
+ if (matchesExclude(target, rule.pattern)) ignored = !rule.negated;
497
+ }
498
+ return ignored;
499
+ };
500
+ }
501
+ function defaultIgnoreReason(relativePath) {
502
+ const posixPath = relativePath.replace(/\\/g, "/");
503
+ const parts = posixPath.split("/");
504
+ for (let i = 0; i < parts.length - 1; i++) {
505
+ if (DEFAULT_IGNORED_DIRS.includes(parts[i])) {
506
+ return `built-in ignore: directory "${parts[i]}"`;
507
+ }
508
+ if (parts[i].startsWith("snapshot.backup-")) {
509
+ return `built-in ignore: leftover snapshot backup "${parts[i]}"`;
510
+ }
511
+ }
512
+ const base = parts[parts.length - 1];
513
+ if (DEFAULT_IGNORED_FILES.includes(base)) {
514
+ return `built-in ignore: filename "${base}"`;
515
+ }
516
+ return null;
517
+ }
518
+ function collectFiles(cwd, options = {}) {
519
+ const includePatterns = options.includePatterns ?? [];
520
+ const excludePatterns = options.excludePatterns ?? [];
521
+ const includeSensitiveFiles = options.includeSensitiveFiles ?? false;
522
+ let candidates;
523
+ let walkSkips = [];
524
+ try {
525
+ const stdout = childProcess.execSync(
526
+ "git ls-files -z --cached --others --exclude-standard",
527
+ {
528
+ cwd,
529
+ stdio: ["ignore", "pipe", "ignore"],
530
+ encoding: "utf8",
531
+ maxBuffer: 50 * 1024 * 1024
532
+ }
533
+ );
534
+ candidates = stdout.split("\0").filter((entry) => entry.length > 0).map((entry) => entry.replace(/\\/g, "/"));
535
+ } catch {
536
+ const walked = walkDirectory(cwd, cwd, /* @__PURE__ */ new Set());
537
+ candidates = walked.files;
538
+ walkSkips = walked.skipped;
539
+ }
540
+ const skipped = [...walkSkips];
541
+ const files = candidates.filter((file) => {
542
+ const ignored = defaultIgnoreReason(file);
543
+ if (ignored) {
544
+ skipped.push({ path: file, reason: "default-ignore", detail: ignored });
545
+ return false;
546
+ }
547
+ if (includePatterns.length > 0 && !includePatterns.some((pattern) => matchesExclude(file, pattern))) {
548
+ skipped.push({
549
+ path: file,
550
+ reason: "excluded",
551
+ detail: `does not match any include pattern (${includePatterns.join(", ")})`
552
+ });
553
+ return false;
554
+ }
555
+ for (const pattern of excludePatterns) {
556
+ if (matchesExclude(file, pattern)) {
557
+ skipped.push({
558
+ path: file,
559
+ reason: "excluded",
560
+ detail: `matches exclude pattern "${pattern}"`
561
+ });
562
+ return false;
563
+ }
564
+ }
565
+ if (!includeSensitiveFiles && isSensitivePath(file)) {
566
+ skipped.push({
567
+ path: file,
568
+ reason: "sensitive-file",
569
+ detail: "credential-bearing file; enable includeSensitiveFiles to emit it"
570
+ });
571
+ return false;
572
+ }
573
+ return true;
574
+ }).sort();
575
+ return { files, skipped };
576
+ }
577
+ function walkDirectory(dir, baseDir, visited) {
578
+ const results = { files: [], skipped: [] };
579
+ let realDir;
580
+ try {
581
+ realDir = fs3.realpathSync(dir);
582
+ } catch {
583
+ realDir = dir;
584
+ }
585
+ if (visited.has(realDir)) return results;
586
+ visited.add(realDir);
587
+ const ignoredByGitignore = dir === baseDir ? readGitignoreMatcher(baseDir) : () => false;
588
+ let entries;
589
+ try {
590
+ entries = fs3.readdirSync(dir, { withFileTypes: true });
591
+ } catch {
592
+ results.skipped.push({ path: dir, reason: "read-error", detail: "directory unreadable" });
593
+ return results;
594
+ }
595
+ for (const entry of entries) {
596
+ if (DEFAULT_IGNORED_DIRS.includes(entry.name)) continue;
597
+ const fullPath = path3.join(dir, entry.name);
598
+ const relPath = path3.relative(baseDir, fullPath).replace(/\\/g, "/");
599
+ if (ignoredByGitignore(relPath)) continue;
600
+ let isDirectory = entry.isDirectory();
601
+ let isFile = entry.isFile();
602
+ if (entry.isSymbolicLink()) {
603
+ try {
604
+ const stats = fs3.statSync(fullPath);
605
+ isDirectory = stats.isDirectory();
606
+ isFile = stats.isFile();
607
+ if (!isDirectory && !isFile) {
608
+ results.skipped.push({ path: relPath, reason: "symlink", detail: "not a file or directory" });
609
+ continue;
610
+ }
611
+ results.skipped.push({
612
+ path: relPath,
613
+ reason: "symlink",
614
+ detail: isDirectory ? "followed directory symlink" : "followed file symlink"
615
+ });
616
+ } catch {
617
+ results.skipped.push({ path: relPath, reason: "read-error", detail: "broken symlink" });
618
+ continue;
619
+ }
620
+ }
621
+ if (isDirectory) {
622
+ const nested = walkDirectory(fullPath, baseDir, visited);
623
+ results.files.push(...nested.files);
624
+ results.skipped.push(...nested.skipped);
625
+ } else if (isFile) {
626
+ results.files.push(relPath);
627
+ }
628
+ }
629
+ return results;
630
+ }
631
+
632
+ // src/security/patterns.ts
633
+ var DEFAULT_SECRET_PATTERNS = [
634
+ {
635
+ rule: "private-key-header",
636
+ pattern: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----|-----BEGIN (?:RSA |OPENSSH |EC |PGP |DSA |ENCRYPTED )?PRIVATE KEY-----/
637
+ },
638
+ {
639
+ rule: "jwt-token",
640
+ pattern: /eyJ[A-Za-z0-9_-]{10,1024}\.eyJ[A-Za-z0-9_-]{10,1024}\.[A-Za-z0-9_-]{10,1024}/
641
+ },
642
+ {
643
+ rule: "aws-access-key-id",
644
+ pattern: /(?:AKIA|ASIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASCA)[0-9A-Z]{16}/
645
+ },
646
+ {
647
+ rule: "aws-secret-access-key",
648
+ pattern: /(?:aws_secret_access_key|aws_secret_key|secret_access_key)["']?\s*[:=]\s*["']?[A-Za-z0-9/+=]{40}["']?/i
649
+ },
650
+ {
651
+ rule: "github-token",
652
+ pattern: /gh[pousr]_[A-Za-z0-9]{36,251}|github_pat_[A-Za-z0-9_]{22,251}|ghs_[A-Za-z0-9]{36,251}/
653
+ },
654
+ {
655
+ rule: "slack-token",
656
+ pattern: /xox[baprs]-[0-9A-Za-z-]{10,250}/
657
+ },
658
+ {
659
+ rule: "slack-webhook-url",
660
+ pattern: /hooks\.slack\.com\/services\/T[A-Z0-9]{6,12}\/B[A-Z0-9]{6,12}\/[A-Za-z0-9]{20,32}/
661
+ },
662
+ {
663
+ rule: "stripe-secret-key",
664
+ pattern: /[sr]k_live_[0-9a-zA-Z]{20,247}/
665
+ },
666
+ {
667
+ rule: "google-api-key",
668
+ pattern: /AIza[0-9A-Za-z_-]{35}/
669
+ },
670
+ {
671
+ rule: "google-oauth-client-secret",
672
+ pattern: /GOCSPX-[A-Za-z0-9_-]{20,64}/
673
+ },
674
+ {
675
+ rule: "openai-anthropic-key",
676
+ pattern: /\bsk-(?:ant-[a-z0-9]+-|proj-|svcacct-|admin-)?[A-Za-z0-9_-]{20,}_?[A-Za-z0-9_-]{0,20}\b/
677
+ },
678
+ {
679
+ rule: "npm-token",
680
+ pattern: /npm_[A-Za-z0-9]{36}/
681
+ },
682
+ {
683
+ rule: "pypi-token",
684
+ pattern: /pypi-[A-Za-z0-9_-]{50,180}/
685
+ },
686
+ {
687
+ rule: "sendgrid-key",
688
+ pattern: /SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/
689
+ },
690
+ {
691
+ rule: "twilio-api-key",
692
+ pattern: /SK[0-9a-fA-F]{32}/
693
+ },
694
+ {
695
+ rule: "digitalocean-token",
696
+ pattern: /dop_v1_[0-9a-f]{64}/
697
+ },
698
+ {
699
+ rule: "azure-storage-account-key",
700
+ pattern: /AccountKey=[A-Za-z0-9+/=]{80,100}/
701
+ },
702
+ {
703
+ rule: "database-uri-credentials",
704
+ pattern: /(?:mongodb(?:\+srv)?|postgres(?:ql)?|mysql|maria(?:db)?|redis(?:s)?|amqp(?:s)?|clickhouse):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/
705
+ },
706
+ {
707
+ rule: "basic-auth-url",
708
+ pattern: /(?:https?|ftp|sftp|ssh|git):\/\/[^\s:@/]{1,128}:[^\s@/]{1,256}@[^\s/]{1,256}/i
709
+ },
710
+ {
711
+ rule: "generic-secret-assignment",
712
+ pattern: /(?:api[_-]?key|secret[_-]?key|auth[_-]?token|client[_-]?secret|access[_-]?token|refresh[_-]?token|private[_-]?key|password|passwd|pwd)["']?\s*[:=]\s*["'][^"'\s]{8,256}["']/i
713
+ },
714
+ {
715
+ // Covers `.env` / shell assignment syntax, which never quotes its values and
716
+ // was therefore invisible to the rule above. Anchored to the line start and
717
+ // limited to secret-looking names to stay at zero false positives.
718
+ //
719
+ // The name is matched with BOUNDED quantifiers on purpose: an unbounded
720
+ // `[A-Za-z0-9_]*` on both sides of the keyword alternation backtracks at
721
+ // every offset of a long identifier, which made this rule quadratic
722
+ // (measured 5.6 s for a single 240 KB line before bounding).
723
+ rule: "env-secret-assignment",
724
+ pattern: /^\s{0,64}(?:export\s{1,8})?[A-Za-z0-9_]{0,96}(?:KEY|TOKEN|SECRET|PASSWORD|PASSWD|PWD|CREDENTIAL)[A-Za-z0-9_]{0,96}\s{0,8}=\s{0,8}(?![\s$<{[])[^\s#]{6,512}$/i
725
+ }
726
+ ];
727
+ var PEM_BLOCK_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g;
728
+ var PEM_UNTERMINATED_PATTERN = /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----(?:\r?\n[A-Za-z0-9+/=]{4,}[ A-Za-z0-9+/=]*)+/g;
729
+
730
+ // src/security/redact.ts
731
+ var REDACTION_MARKER = "[REDACTED: potential secret-like value omitted from snapshot]";
732
+ var PEM_REDACTION_MARKER = "[REDACTED: private key block omitted from snapshot]";
733
+ function compileSecretRules(rules) {
734
+ return rules.map((rule) => {
735
+ if (rule.pattern instanceof RegExp) {
736
+ return {
737
+ rule: rule.rule,
738
+ pattern: new RegExp(rule.pattern.source, rule.pattern.flags.replace(/[gy]/g, ""))
739
+ };
740
+ }
741
+ if (typeof rule.pattern === "string") {
742
+ try {
743
+ return { rule: rule.rule, pattern: new RegExp(rule.pattern, "i") };
744
+ } catch (err) {
745
+ throw new Error(
746
+ `Invalid regular expression for secret rule "${rule.rule}": ${err.message}`
747
+ );
748
+ }
749
+ }
750
+ throw new Error(
751
+ `Secret rule "${String(rule.rule)}" has a pattern that is neither a RegExp nor a string.`
752
+ );
753
+ });
754
+ }
755
+ function toGlobal(pattern) {
756
+ const flags = pattern.flags.replace(/[gy]/g, "");
757
+ return new RegExp(pattern.source, `${flags}g`);
758
+ }
759
+ function scanAndRedactSecrets(content, rules, filePath) {
760
+ const findings = [];
761
+ let redacted = content;
762
+ for (const blockPattern of [PEM_BLOCK_PATTERN, PEM_UNTERMINATED_PATTERN]) {
763
+ blockPattern.lastIndex = 0;
764
+ let match;
765
+ while ((match = blockPattern.exec(redacted)) !== null) {
766
+ if (match[0].length === 0) {
767
+ blockPattern.lastIndex += 1;
768
+ continue;
769
+ }
770
+ const line = redacted.slice(0, match.index).split("\n").length;
771
+ const newlines = (match[0].match(/\n/g) ?? []).length;
772
+ findings.push({ rule: "private-key-block", line, path: filePath });
773
+ redacted = redacted.slice(0, match.index) + PEM_REDACTION_MARKER + "\n".repeat(newlines) + redacted.slice(match.index + match[0].length);
774
+ blockPattern.lastIndex = match.index + PEM_REDACTION_MARKER.length + newlines;
775
+ }
776
+ }
777
+ const compiled = rules.map((rule) => ({
778
+ rule: rule.rule,
779
+ pattern: toGlobal(rule.pattern)
780
+ }));
781
+ const lines = redacted.split("\n");
782
+ const redactedLines = lines.map((line, index) => {
783
+ if (line.length === 0) return line;
784
+ const spans = [];
785
+ const matchedRules = /* @__PURE__ */ new Set();
786
+ for (const { rule, pattern } of compiled) {
787
+ pattern.lastIndex = 0;
788
+ let match;
789
+ while ((match = pattern.exec(line)) !== null) {
790
+ if (match[0].length === 0) {
791
+ pattern.lastIndex += 1;
792
+ continue;
793
+ }
794
+ spans.push({ start: match.index, end: match.index + match[0].length, rule });
795
+ matchedRules.add(rule);
796
+ }
797
+ }
798
+ if (spans.length === 0) return line;
799
+ for (const rule of matchedRules) {
800
+ findings.push({ rule, line: index + 1, path: filePath });
801
+ }
802
+ return applySpans(line, spans);
803
+ });
804
+ return { redacted: redactedLines.join("\n"), findings };
805
+ }
806
+ function applySpans(line, spans) {
807
+ const ordered = [...spans].sort((a, b) => a.start - b.start || a.end - b.end);
808
+ let out = "";
809
+ let cursor = 0;
810
+ for (const span of ordered) {
811
+ if (span.end <= cursor) continue;
812
+ const start = Math.max(span.start, cursor);
813
+ out += line.slice(cursor, start);
814
+ out += REDACTION_MARKER;
815
+ cursor = Math.min(span.end, line.length);
816
+ }
817
+ out += line.slice(cursor);
818
+ return out;
819
+ }
820
+
821
+ // src/config.ts
822
+ var CONFIG_FILENAMES = [
823
+ "snapshot.config.ts",
824
+ "snapshot.config.js",
825
+ "snapshot.config.mjs",
826
+ "snapshot.config.cjs",
827
+ "snapshot.config.json",
828
+ ".snapshotrc.json",
829
+ ".snapshotrc"
830
+ ];
831
+ var RESOLVED_CONFIG_BRAND = /* @__PURE__ */ Symbol.for(
832
+ "@ankurhalder/codebase-snapshot/resolved-config"
833
+ );
834
+ function markResolvedConfig(config) {
835
+ Object.defineProperty(config, RESOLVED_CONFIG_BRAND, {
836
+ value: true,
837
+ enumerable: false,
838
+ configurable: false,
839
+ writable: false
840
+ });
841
+ return config;
842
+ }
843
+ function isResolvedConfig(value) {
844
+ if (typeof value !== "object" || value === null) return false;
845
+ const candidate = value;
846
+ if (candidate[RESOLVED_CONFIG_BRAND] === true) {
847
+ return true;
848
+ }
849
+ const hasResolvedNumbers = typeof candidate.maxPartBytes === "number" && typeof candidate.largeFileBytes === "number" && typeof candidate.previewHeadLines === "number" && typeof candidate.previewTailLines === "number";
850
+ const hasResolvedStrings = typeof candidate.cwd === "string" && typeof candidate.outDir === "string" && typeof candidate.tmpDir === "string";
851
+ const hasResolvedFlags = typeof candidate.includeMonolith === "boolean" && typeof candidate.includeSensitiveFiles === "boolean";
852
+ const hasResolvedRules = Array.isArray(candidate.layers) && Array.isArray(candidate.kinds) && Array.isArray(candidate.excludePatterns);
853
+ const secretRules = candidate.secretRules;
854
+ const hasCompiledRules = Array.isArray(secretRules) && secretRules.every(
855
+ (rule) => typeof rule === "object" && rule !== null && typeof rule.rule === "string" && (typeof rule.pattern === "string" || rule.pattern instanceof RegExp)
856
+ );
857
+ return hasResolvedNumbers && hasResolvedStrings && hasResolvedFlags && hasResolvedRules && hasCompiledRules;
858
+ }
859
+ function normalizeResolvedConfig(config) {
860
+ return markResolvedConfig({
861
+ ...config,
862
+ secretRules: compileSecretRules(config.secretRules),
863
+ artifacts: resolveArtifacts(
864
+ config.artifacts,
865
+ void 0,
866
+ config.includeMonolith === true
867
+ ),
868
+ include: resolveInclude(config.include),
869
+ layersMode: config.layersMode === "replace" ? "replace" : "merge",
870
+ budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
871
+ hooks: resolveHooks(config.hooks),
872
+ excludePatterns: Array.isArray(config.excludePatterns) ? config.excludePatterns : [],
873
+ layers: Array.isArray(config.layers) ? config.layers : [],
874
+ kinds: Array.isArray(config.kinds) ? config.kinds : []
875
+ });
876
+ }
877
+ function assertUsableConfigInput(value) {
878
+ if (typeof value !== "object" || value === null) return;
879
+ const candidate = value;
880
+ if (!Array.isArray(candidate.secretRules) || candidate.secretRules.length === 0) {
881
+ return;
882
+ }
883
+ const unusable = candidate.secretRules.filter((rule) => {
884
+ if (typeof rule !== "object" || rule === null) return true;
885
+ const pattern = rule.pattern;
886
+ if (pattern instanceof RegExp || typeof pattern === "string") return false;
887
+ return typeof pattern === "object" && pattern !== null;
888
+ });
889
+ if (unusable.length > 0) {
890
+ throw new Error(
891
+ `This configuration carries ${unusable.length} unusable secret rule pattern(s). A RegExp cannot survive JSON serialization, and silently dropping these rules would let those secrets through redaction. Pass the resolved config object itself (or express rules via \`secrets.customPatterns\`) instead of a JSON clone.`
892
+ );
893
+ }
894
+ }
895
+ function supportsTypeScriptImports() {
896
+ const features = process.features;
897
+ if (features && "typescript" in features) {
898
+ return features.typescript !== false && features.typescript !== void 0;
899
+ }
900
+ const [major, minor] = process.versions.node.split(".").map((value) => Number(value));
901
+ if (!Number.isFinite(major)) return false;
902
+ return major > 22 || major === 22 && minor >= 6;
903
+ }
904
+ function stripJsonComments(raw) {
905
+ return raw.replace(/^\uFEFF/, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:"'\\])\/\/.*$/gm, "$1");
906
+ }
907
+ function parseJsonish(raw, file) {
908
+ try {
909
+ return JSON.parse(stripJsonComments(raw));
910
+ } catch (err) {
911
+ throw new Error(`Could not parse ${file}: ${err.message}`);
912
+ }
913
+ }
914
+ async function loadConfigFileDetailed(cwd) {
915
+ const warnings = [];
916
+ for (const filename of CONFIG_FILENAMES) {
917
+ const fullPath = path4.join(cwd, filename);
918
+ if (!fs4.existsSync(fullPath)) continue;
919
+ if (filename.endsWith(".json") || filename === ".snapshotrc") {
920
+ try {
921
+ return {
922
+ config: parseJsonish(fs4.readFileSync(fullPath, "utf8"), fullPath),
923
+ file: fullPath,
924
+ warnings
925
+ };
926
+ } catch (err) {
927
+ warnings.push(err.message);
928
+ continue;
929
+ }
930
+ }
931
+ if (filename.endsWith(".ts") && !supportsTypeScriptImports()) {
932
+ warnings.push(
933
+ `${fullPath} was ignored: loading a .ts config needs Node >= 22.6 (running ${process.versions.node}). Use snapshot.config.mjs, .cjs or .json instead.`
934
+ );
935
+ continue;
936
+ }
937
+ try {
938
+ const imported = await import(pathToFileURL(fullPath).href);
939
+ const config = imported.default ?? imported;
940
+ if (!config || typeof config !== "object") {
941
+ warnings.push(`${fullPath} does not export a configuration object.`);
942
+ continue;
943
+ }
944
+ return { config, file: fullPath, warnings };
945
+ } catch (err) {
946
+ warnings.push(`Could not load ${fullPath}: ${err.message}`);
947
+ continue;
948
+ }
949
+ }
950
+ const pkgJsonPath = path4.join(cwd, "package.json");
951
+ if (fs4.existsSync(pkgJsonPath)) {
952
+ try {
953
+ const parsed = parseJsonish(fs4.readFileSync(pkgJsonPath, "utf8"), pkgJsonPath);
954
+ const snapshotKey = parsed.snapshot;
955
+ if (snapshotKey && typeof snapshotKey === "object") {
956
+ return { config: snapshotKey, file: pkgJsonPath, warnings };
957
+ }
958
+ } catch (err) {
959
+ warnings.push(err.message);
960
+ }
961
+ }
962
+ return { config: null, file: null, warnings };
963
+ }
964
+ var MIN_MAX_PART_BYTES = 4096;
965
+ var ARTIFACT_NAMES = [
966
+ "readme",
967
+ "manifest",
968
+ "parts",
969
+ "navigation",
970
+ "projectTree",
971
+ "audit",
972
+ "monolith"
973
+ ];
974
+ var DEFAULT_ARTIFACTS = {
975
+ readme: true,
976
+ manifest: true,
977
+ parts: true,
978
+ navigation: true,
979
+ projectTree: true,
980
+ audit: true,
981
+ monolith: false
982
+ };
983
+ function isArtifactName(value) {
984
+ return ARTIFACT_NAMES.includes(value);
985
+ }
986
+ function resolveArtifacts(requested, skipped, includeMonolith) {
987
+ const artifacts = {
988
+ ...DEFAULT_ARTIFACTS,
989
+ // `includeMonolith` predates this selection and stays the switch for it.
990
+ monolith: includeMonolith
991
+ };
992
+ if (requested !== void 0) {
993
+ if (typeof requested !== "object" || requested === null || Array.isArray(requested)) {
994
+ throw new Error("`artifacts` must be an object of boolean flags.");
995
+ }
996
+ for (const [name, value] of Object.entries(requested)) {
997
+ if (!isArtifactName(name)) {
998
+ throw new Error(
999
+ `Unknown artifact "${name}". Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
1000
+ );
1001
+ }
1002
+ if (typeof value !== "boolean") {
1003
+ throw new Error(`\`artifacts.${name}\` must be a boolean.`);
1004
+ }
1005
+ artifacts[name] = value;
1006
+ }
1007
+ }
1008
+ for (const name of skipped ?? []) {
1009
+ if (!isArtifactName(name)) {
1010
+ throw new Error(
1011
+ `Unknown artifact "${name}". Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
1012
+ );
1013
+ }
1014
+ artifacts[name] = false;
1015
+ }
1016
+ return artifacts;
1017
+ }
1018
+ function positiveInteger(value, fallback) {
1019
+ if (!Number.isFinite(value) || value <= 0) return fallback;
1020
+ return Math.floor(value);
1021
+ }
1022
+ var BYTE_UNITS = {
1023
+ b: 1,
1024
+ k: 1024,
1025
+ kb: 1024,
1026
+ kib: 1024,
1027
+ m: 1024 * 1024,
1028
+ mb: 1024 * 1024,
1029
+ mib: 1024 * 1024,
1030
+ g: 1024 * 1024 * 1024,
1031
+ gb: 1024 * 1024 * 1024,
1032
+ gib: 1024 * 1024 * 1024
1033
+ };
1034
+ function parseByteSize(value) {
1035
+ if (value === null || value === void 0) return void 0;
1036
+ if (typeof value === "number") {
1037
+ return Number.isFinite(value) ? Math.floor(value) : void 0;
1038
+ }
1039
+ const match = /^\s*(\d+(?:\.\d+)?)\s*(b|k|kb|kib|m|mb|mib|g|gb|gib)?\s*$/i.exec(value);
1040
+ if (!match) return void 0;
1041
+ const amount = Number(match[1]);
1042
+ if (!Number.isFinite(amount)) return void 0;
1043
+ const multiplier = BYTE_UNITS[(match[2] ?? "b").toLowerCase()] ?? 1;
1044
+ return Math.floor(amount * multiplier);
1045
+ }
1046
+ function describeBytes(value) {
1047
+ return typeof value === "number" ? String(value) : `"${value}"`;
1048
+ }
1049
+ function boundedBytes(value, fallback, minimum, label) {
1050
+ if (value === void 0) return fallback;
1051
+ const parsed = parseByteSize(value);
1052
+ if (parsed === void 0) {
1053
+ throw new Error(
1054
+ `${label} must be a byte count such as 120000, "120k" or "1.5mb" (received ${describeBytes(value)}).`
1055
+ );
1056
+ }
1057
+ if (parsed <= 0) return fallback;
1058
+ if (parsed < minimum) {
1059
+ throw new Error(`${label} must be at least ${minimum} bytes (received ${describeBytes(value)}).`);
1060
+ }
1061
+ return parsed;
1062
+ }
1063
+ function optionalBytes(value, label) {
1064
+ if (value === void 0 || value === null) return null;
1065
+ const parsed = parseByteSize(value);
1066
+ if (parsed === void 0 || parsed <= 0) {
1067
+ throw new Error(
1068
+ `${label} must be a positive byte count such as 1000000 or "1mb" (received ${describeBytes(value)}).`
1069
+ );
1070
+ }
1071
+ return parsed;
1072
+ }
1073
+ function resolveInclude(include) {
1074
+ if (include === void 0) return [];
1075
+ if (!Array.isArray(include) || include.some((pattern) => typeof pattern !== "string")) {
1076
+ throw new Error("`include` must be an array of glob strings.");
1077
+ }
1078
+ return include.map((pattern) => pattern.trim()).filter(Boolean);
1079
+ }
1080
+ function resolveLayersMode(mode) {
1081
+ if (mode === void 0) return "merge";
1082
+ if (mode !== "merge" && mode !== "replace") {
1083
+ throw new Error(`\`layersMode\` must be "merge" or "replace" (received ${JSON.stringify(mode)}).`);
1084
+ }
1085
+ return mode;
1086
+ }
1087
+ function resolveBudgets(budgets, fallback) {
1088
+ const byLayer = {};
1089
+ if (budgets === void 0) return { default: fallback, byLayer };
1090
+ if (typeof budgets !== "object" || budgets === null || Array.isArray(budgets)) {
1091
+ throw new Error("`budgets` must be an object.");
1092
+ }
1093
+ if (budgets.byLayer !== void 0) {
1094
+ if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
1095
+ throw new Error("`budgets.byLayer` must be an object of layer name to byte size.");
1096
+ }
1097
+ for (const [layer, value] of Object.entries(budgets.byLayer)) {
1098
+ byLayer[layer] = boundedBytes(
1099
+ value,
1100
+ fallback,
1101
+ MIN_MAX_PART_BYTES,
1102
+ `budgets.byLayer["${layer}"]`
1103
+ );
1104
+ }
1105
+ }
1106
+ return {
1107
+ default: boundedBytes(budgets.default, fallback, MIN_MAX_PART_BYTES, "budgets.default"),
1108
+ byLayer
1109
+ };
1110
+ }
1111
+ function resolveHooks(hooks) {
1112
+ if (hooks === void 0) return {};
1113
+ if (typeof hooks !== "object" || hooks === null || Array.isArray(hooks)) {
1114
+ throw new Error("`hooks` must be an object.");
1115
+ }
1116
+ if (hooks.afterGenerate !== void 0 && typeof hooks.afterGenerate !== "function") {
1117
+ throw new Error("`hooks.afterGenerate` must be a function.");
1118
+ }
1119
+ return { ...hooks };
1120
+ }
1121
+ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
1122
+ const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
1123
+ const resolved = path4.resolve(candidate);
1124
+ if (!fs4.existsSync(resolved)) {
1125
+ throw new Error(`Working directory does not exist: ${resolved}`);
1126
+ }
1127
+ if (!fs4.statSync(resolved).isDirectory()) {
1128
+ throw new Error(`Working directory is not a directory: ${resolved}`);
1129
+ }
1130
+ return resolved;
1131
+ }
1132
+ function resolveConfig(userConfig, cliOptions, baseDir) {
1133
+ const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
1134
+ const preset = detectPreset(cwd);
1135
+ const layersMode = resolveLayersMode(userConfig?.layersMode);
1136
+ const layers = userConfig?.layers && userConfig.layers.length > 0 ? layersMode === "replace" ? userConfig.layers : [...userConfig.layers, ...preset.layers] : preset.layers;
1137
+ const kinds = userConfig?.kinds && userConfig.kinds.length > 0 ? layersMode === "replace" ? userConfig.kinds : [...userConfig.kinds, ...preset.kinds] : preset.kinds;
1138
+ const customRules = userConfig?.secrets?.customPatterns ?? [];
1139
+ const rawRules = userConfig?.secrets?.disableDefaultPatterns ? customRules : [...DEFAULT_SECRET_PATTERNS, ...customRules];
1140
+ const secretRules = compileSecretRules(rawRules);
1141
+ const includeMonolith = cliOptions?.includeMonolith ?? userConfig?.includeMonolith ?? false;
1142
+ const artifacts = resolveArtifacts(
1143
+ userConfig?.artifacts,
1144
+ cliOptions?.skipArtifacts,
1145
+ includeMonolith
1146
+ );
1147
+ const maxPartBytes = boundedBytes(
1148
+ cliOptions?.maxPartBytes ?? userConfig?.maxPartBytes,
1149
+ 300 * 1024,
1150
+ MIN_MAX_PART_BYTES,
1151
+ "maxPartBytes"
1152
+ );
1153
+ const budgets = resolveBudgets(userConfig?.budgets, maxPartBytes);
1154
+ return markResolvedConfig({
1155
+ cwd,
1156
+ branch: userConfig?.branch || "main",
1157
+ outDir: cliOptions?.outDir || userConfig?.outDir || "snapshot",
1158
+ tmpDir: userConfig?.tmpDir || "snapshot.tmp",
1159
+ maxPartBytes,
1160
+ largeFileBytes: boundedBytes(cliOptions?.largeFileBytes ?? userConfig?.largeFileBytes, 200 * 1024, 1, "largeFileBytes"),
1161
+ previewHeadLines: positiveInteger(userConfig?.previewHeadLines ?? 80, 80),
1162
+ previewTailLines: positiveInteger(userConfig?.previewTailLines ?? 40, 40),
1163
+ includeMonolith: artifacts.monolith,
1164
+ maxMonolithBytes: optionalBytes(
1165
+ cliOptions?.maxMonolithBytes !== void 0 ? cliOptions.maxMonolithBytes : userConfig?.maxMonolithBytes,
1166
+ "maxMonolithBytes"
1167
+ ),
1168
+ includeSensitiveFiles: cliOptions?.includeSensitiveFiles ?? userConfig?.includeSensitiveFiles ?? false,
1169
+ include: resolveInclude(userConfig?.include),
1170
+ artifacts,
1171
+ layersMode,
1172
+ budgets,
1173
+ hooks: resolveHooks(userConfig?.hooks),
1174
+ layers,
1175
+ kinds,
1176
+ excludePatterns: userConfig?.excludePatterns ?? [],
1177
+ secretRules
1178
+ });
1179
+ }
1180
+ function matchRule(filePath, matcher) {
1181
+ if (typeof matcher === "function") return matcher(filePath);
1182
+ if (matcher instanceof RegExp) {
1183
+ matcher.lastIndex = 0;
1184
+ return matcher.test(filePath);
1185
+ }
1186
+ if (typeof matcher === "string") {
1187
+ const pattern = matcher.replace(/\\/g, "/").trim();
1188
+ if (!pattern) return false;
1189
+ if (pattern.endsWith("/**")) {
1190
+ const prefix = pattern.slice(0, -3).replace(/\/+$/, "");
1191
+ if (!prefix) return true;
1192
+ if (!/[*?]/.test(prefix)) {
1193
+ return filePath === prefix || filePath.startsWith(`${prefix}/`);
1194
+ }
1195
+ }
1196
+ if (pattern.includes("*") || pattern.includes("?")) {
1197
+ return globToRegExp(pattern).test(filePath);
1198
+ }
1199
+ return filePath.includes(pattern);
1200
+ }
1201
+ return false;
1202
+ }
1203
+ function classifyLayer(filePath, layers) {
1204
+ for (const layer of layers) {
1205
+ if (matchRule(filePath, layer.match)) return layer.name;
1206
+ }
1207
+ return "other";
1208
+ }
1209
+ function classifyKind(filePath, kinds) {
1210
+ for (const kind of kinds) {
1211
+ if (matchRule(filePath, kind.match)) return kind.kind;
1212
+ }
1213
+ return "other";
1214
+ }
1215
+
1216
+ // src/partitioner/preview.ts
1217
+ function countLines(content) {
1218
+ if (content.length === 0) return 0;
1219
+ return content.split("\n").length - (content.endsWith("\n") ? 1 : 0);
1220
+ }
1221
+ function truncateToBytes(content, maxBytes) {
1222
+ if (maxBytes <= 0) return { content: "", isTruncated: content.length > 0 };
1223
+ const buffer = Buffer.from(content, "utf8");
1224
+ if (buffer.length <= maxBytes) return { content, isTruncated: false };
1225
+ let slice = buffer.subarray(0, maxBytes).toString("utf8");
1226
+ while (slice.endsWith("\uFFFD")) slice = slice.slice(0, -1);
1227
+ return { content: slice, isTruncated: true };
1228
+ }
1229
+ function truncatePreview(content, headLines, tailLines) {
1230
+ const lines = content.split("\n");
1231
+ const total = countLines(content);
1232
+ if (total <= headLines + tailLines) {
1233
+ return {
1234
+ content,
1235
+ isTruncated: false,
1236
+ originalLines: total,
1237
+ includedLines: total
1238
+ };
1239
+ }
1240
+ const head = lines.slice(0, headLines);
1241
+ const tail = tailLines > 0 ? lines.slice(lines.length - tailLines) : [];
1242
+ const omitted = total - headLines - tailLines;
1243
+ const result = [
1244
+ ...head,
1245
+ `... [${omitted} lines omitted: file exceeds large file size budget] ...`,
1246
+ ...tail
1247
+ ].join("\n");
1248
+ return {
1249
+ content: result,
1250
+ isTruncated: true,
1251
+ originalLines: total,
1252
+ includedLines: headLines + tailLines + 1
1253
+ };
1254
+ }
1255
+
1256
+ // src/scanner/language.ts
1257
+ import * as path5 from "path";
1258
+ var LANGUAGE_BY_EXTENSION = {
1259
+ ".ts": "typescript",
1260
+ ".mts": "typescript",
1261
+ ".cts": "typescript",
1262
+ ".tsx": "tsx",
1263
+ ".js": "javascript",
1264
+ ".mjs": "javascript",
1265
+ ".cjs": "javascript",
1266
+ ".jsx": "jsx",
1267
+ ".json": "json",
1268
+ ".jsonc": "json",
1269
+ ".md": "markdown",
1270
+ ".mdx": "mdx",
1271
+ ".css": "css",
1272
+ ".scss": "scss",
1273
+ ".sass": "scss",
1274
+ ".less": "less",
1275
+ ".html": "html",
1276
+ ".vue": "vue",
1277
+ ".svelte": "svelte",
1278
+ ".sql": "sql",
1279
+ ".yaml": "yaml",
1280
+ ".yml": "yaml",
1281
+ ".sh": "bash",
1282
+ ".bash": "bash",
1283
+ ".zsh": "bash",
1284
+ ".ps1": "powershell",
1285
+ ".py": "python",
1286
+ ".go": "go",
1287
+ ".rs": "rust",
1288
+ ".java": "java",
1289
+ ".rb": "ruby",
1290
+ ".php": "php",
1291
+ ".toml": "toml",
1292
+ ".ini": "ini",
1293
+ ".env": "dotenv",
1294
+ ".xml": "xml",
1295
+ ".svg": "xml",
1296
+ ".graphql": "graphql",
1297
+ ".gql": "graphql",
1298
+ ".proto": "protobuf",
1299
+ ".tf": "hcl"
1300
+ };
1301
+ var LANGUAGE_BY_BASENAME = {
1302
+ Dockerfile: "dockerfile",
1303
+ Containerfile: "dockerfile",
1304
+ Makefile: "makefile",
1305
+ ".gitignore": "gitignore",
1306
+ ".npmignore": "gitignore",
1307
+ ".dockerignore": "gitignore",
1308
+ ".editorconfig": "editorconfig"
1309
+ };
1310
+ function detectLanguage(filePath) {
1311
+ const normalized = filePath.replace(/\\/g, "/");
1312
+ const base = path5.posix.basename(normalized);
1313
+ if (LANGUAGE_BY_BASENAME[base]) return LANGUAGE_BY_BASENAME[base];
1314
+ if (base === ".env" || base.startsWith(".env.")) return "dotenv";
1315
+ const ext = path5.posix.extname(base).toLowerCase();
1316
+ return LANGUAGE_BY_EXTENSION[ext] ?? "text";
1317
+ }
1318
+
1319
+ // src/explain.ts
1320
+ function describeMatcher(matcher) {
1321
+ if (typeof matcher === "string") return matcher;
1322
+ if (matcher instanceof RegExp) return String(matcher);
1323
+ if (typeof matcher === "function") {
1324
+ const source = String(matcher).replace(/\s+/g, " ").trim();
1325
+ return source.length > 70 ? `${source.slice(0, 67)}...` : source;
1326
+ }
1327
+ return String(matcher);
1328
+ }
1329
+ function normalizeTarget(cwd, target) {
1330
+ const absolute = path6.resolve(cwd, target);
1331
+ const relative4 = path6.relative(cwd, absolute);
1332
+ if (relative4.startsWith("..") || path6.isAbsolute(relative4)) return target;
1333
+ return relative4.split(path6.sep).join("/");
1334
+ }
1335
+ function findRuleMatch(rules, relativePath) {
1336
+ return rules.find(
1337
+ (rule) => matchRule(relativePath, rule.match)
1338
+ );
1339
+ }
1340
+ function readManifest(config) {
1341
+ const manifestPath = path6.join(config.cwd, config.outDir, "manifest.json");
1342
+ try {
1343
+ return JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
1344
+ } catch {
1345
+ return null;
1346
+ }
1347
+ }
1348
+ function explainPath(config, target) {
1349
+ const relative4 = normalizeTarget(config.cwd, target);
1350
+ const absolute = path6.join(config.cwd, relative4);
1351
+ const result = {
1352
+ path: relative4,
1353
+ exists: fs5.existsSync(absolute),
1354
+ included: false
1355
+ };
1356
+ const collection = collectFiles(config.cwd, {
1357
+ includePatterns: config.include,
1358
+ excludePatterns: config.excludePatterns,
1359
+ includeSensitiveFiles: config.includeSensitiveFiles
1360
+ });
1361
+ if (!collection.files.includes(relative4)) {
1362
+ const record = collection.skipped.find((entry2) => entry2.path === relative4);
1363
+ if (record) {
1364
+ result.reason = record.reason;
1365
+ result.detail = record.detail;
1366
+ } else if (!result.exists) {
1367
+ result.reason = "missing";
1368
+ result.detail = "no such file or directory, relative to the configured cwd";
1369
+ } else {
1370
+ result.reason = "not-discovered";
1371
+ result.detail = "exists but is not tracked or is otherwise not a candidate";
1372
+ }
1373
+ return result;
1374
+ }
1375
+ result.included = true;
1376
+ result.layer = classifyLayer(relative4, config.layers);
1377
+ result.kind = classifyKind(relative4, config.kinds);
1378
+ result.language = detectLanguage(relative4);
1379
+ const layerRule = findRuleMatch(config.layers, relative4);
1380
+ if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
1381
+ const kindRule = findRuleMatch(config.kinds, relative4);
1382
+ if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
1383
+ if (fs5.existsSync(absolute) && fs5.statSync(absolute).isFile()) {
1384
+ const buffer = fs5.readFileSync(absolute);
1385
+ const text = buffer.toString("utf8");
1386
+ result.bytes = buffer.length;
1387
+ result.lines = countLines(text);
1388
+ const { findings } = scanAndRedactSecrets(text, config.secretRules, relative4);
1389
+ result.secretCount = findings.length;
1390
+ result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
1391
+ }
1392
+ const manifest = readManifest(config);
1393
+ const entry = manifest?.files.find((file) => file.relativePath === relative4);
1394
+ result.part = entry?.part ?? null;
1395
+ result.inclusionMode = entry?.inclusionMode;
1396
+ return result;
1397
+ }
1398
+ function renderExplainText(result) {
1399
+ const lines = [result.path, ""];
1400
+ const row = (label, value) => ` ${label.padEnd(11)}${value}`;
1401
+ if (!result.included) {
1402
+ lines.push(row("included", "no"));
1403
+ lines.push(row("reason", result.reason ?? "unknown"));
1404
+ if (result.detail) lines.push(row("detail", result.detail));
1405
+ return lines.join("\n");
1406
+ }
1407
+ lines.push(row("included", "yes"));
1408
+ lines.push(row("layer", `${result.layer}${result.layerMatch ? ` (rule ${result.layerMatch})` : ""}`));
1409
+ lines.push(row("kind", `${result.kind}${result.kindMatch ? ` (rule ${result.kindMatch})` : ""}`));
1410
+ lines.push(row("language", result.language ?? "text"));
1411
+ if (typeof result.lines === "number") {
1412
+ lines.push(row("size", `${result.bytes} B, ${result.lines} line(s)`));
1413
+ }
1414
+ lines.push(
1415
+ row(
1416
+ "part",
1417
+ result.part ?? "(not in the last snapshot; run generate)"
1418
+ )
1419
+ );
1420
+ if (result.inclusionMode) lines.push(row("inclusion", result.inclusionMode));
1421
+ lines.push(
1422
+ row(
1423
+ "secrets",
1424
+ result.secretCount ? `${result.secretCount} finding(s): ${Array.from(
1425
+ new Set((result.secrets ?? []).map((secret) => secret.rule))
1426
+ ).join(", ")}` : "none"
1427
+ )
1428
+ );
1429
+ return lines.join("\n");
1430
+ }
1431
+
1432
+ // src/index.ts
1433
+ import * as crypto3 from "crypto";
1434
+ import * as fs8 from "fs";
1435
+ import * as path9 from "path";
1436
+
1437
+ // src/ast/structured.ts
1438
+ function stripJsonComments2(content) {
1439
+ const text = content.replace(/^\uFEFF/, "");
1440
+ let out = "";
1441
+ let inString = false;
1442
+ let inLineComment = false;
1443
+ let inBlockComment = false;
1444
+ for (let i = 0; i < text.length; i++) {
1445
+ const char = text[i];
1446
+ const next = text[i + 1];
1447
+ if (inLineComment) {
1448
+ if (char === "\n") {
1449
+ inLineComment = false;
1450
+ out += char;
1451
+ }
1452
+ continue;
1453
+ }
1454
+ if (inBlockComment) {
1455
+ if (char === "*" && next === "/") {
1456
+ inBlockComment = false;
1457
+ i += 1;
1458
+ }
1459
+ continue;
1460
+ }
1461
+ if (inString) {
1462
+ out += char;
1463
+ if (char === "\\") {
1464
+ if (next !== void 0) {
1465
+ out += next;
1466
+ i += 1;
1467
+ }
1468
+ } else if (char === '"') {
1469
+ inString = false;
1470
+ }
1471
+ continue;
1472
+ }
1473
+ if (char === '"') {
1474
+ inString = true;
1475
+ out += char;
1476
+ continue;
1477
+ }
1478
+ if (char === "/" && next === "/") {
1479
+ inLineComment = true;
1480
+ i += 1;
1481
+ continue;
1482
+ }
1483
+ if (char === "/" && next === "*") {
1484
+ inBlockComment = true;
1485
+ i += 1;
1486
+ continue;
1487
+ }
1488
+ out += char;
1489
+ }
1490
+ return out;
1491
+ }
1492
+ function summarizeJson(content) {
1493
+ try {
1494
+ const parsed = JSON.parse(stripJsonComments2(content));
1495
+ if (Array.isArray(parsed)) {
1496
+ const sample = parsed[0];
1497
+ const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
1498
+ return {
1499
+ shape: "array",
1500
+ itemCount: parsed.length,
1501
+ keys: [],
1502
+ sampleKeys
1503
+ };
1504
+ }
1505
+ if (parsed && typeof parsed === "object") {
1506
+ const keys = Object.keys(parsed).sort();
1507
+ return {
1508
+ shape: "object",
1509
+ itemCount: keys.length,
1510
+ keys: keys.slice(0, 30),
1511
+ sampleKeys: []
1512
+ };
1513
+ }
1514
+ return {
1515
+ shape: typeof parsed,
1516
+ itemCount: null,
1517
+ keys: [],
1518
+ sampleKeys: []
1519
+ };
1520
+ } catch {
1521
+ return {
1522
+ shape: "invalid-json",
1523
+ itemCount: null,
1524
+ keys: [],
1525
+ sampleKeys: []
1526
+ };
1527
+ }
1528
+ }
1529
+ function summarizeMarkdown(content) {
1530
+ const headings = [];
1531
+ const frontmatterKeys = [];
1532
+ const lines = content.replace(/^\uFEFF/, "").split("\n");
1533
+ let inFrontmatter = false;
1534
+ for (let index = 0; index < lines.length; index++) {
1535
+ const line = lines[index];
1536
+ if (index === 0 && line.trim() === "---") {
1537
+ inFrontmatter = true;
1538
+ continue;
1539
+ }
1540
+ if (inFrontmatter) {
1541
+ if (line.trim() === "---") {
1542
+ inFrontmatter = false;
1543
+ continue;
1544
+ }
1545
+ const match = /^([a-zA-Z0-9_-]+):/.exec(line);
1546
+ if (match) frontmatterKeys.push(match[1]);
1547
+ continue;
1548
+ }
1549
+ const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line.trim());
1550
+ if (headingMatch) {
1551
+ headings.push(`${headingMatch[1]} ${headingMatch[2].trim()}`);
1552
+ }
1553
+ }
1554
+ return {
1555
+ headings: headings.slice(0, 40),
1556
+ frontmatterKeys: Array.from(new Set(frontmatterKeys)).sort()
1557
+ };
1558
+ }
1559
+
1560
+ // src/ast/ts-parser.ts
1561
+ import ts from "typescript";
1562
+ var ROUTE_HANDLER_NAMES = /* @__PURE__ */ new Set([
1563
+ "GET",
1564
+ "POST",
1565
+ "PUT",
1566
+ "DELETE",
1567
+ "PATCH",
1568
+ "HEAD",
1569
+ "OPTIONS"
1570
+ ]);
1571
+ function summarizeTypeScriptSource(relativePath, content) {
1572
+ const summary = {
1573
+ directives: [],
1574
+ imports: [],
1575
+ exports: [],
1576
+ reExports: [],
1577
+ functions: [],
1578
+ classes: [],
1579
+ interfaces: [],
1580
+ types: [],
1581
+ enums: [],
1582
+ variables: [],
1583
+ reactComponents: [],
1584
+ hooks: [],
1585
+ routeHandlers: []
1586
+ };
1587
+ const lowerPath = relativePath.toLowerCase();
1588
+ const isJsx = lowerPath.endsWith(".tsx") || lowerPath.endsWith(".jsx");
1589
+ const sourceFile = ts.createSourceFile(
1590
+ relativePath,
1591
+ content,
1592
+ ts.ScriptTarget.Latest,
1593
+ /* setParentNodes */
1594
+ false,
1595
+ isJsx ? ts.ScriptKind.TSX : ts.ScriptKind.TS
1596
+ );
1597
+ const parseErrors = sourceFile.parseDiagnostics?.length ?? 0;
1598
+ if (parseErrors > 0) summary.parseErrors = parseErrors;
1599
+ for (const statement of sourceFile.statements) {
1600
+ if (ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)) {
1601
+ const text = statement.expression.text;
1602
+ if (text === "use client" || text === "use server") {
1603
+ summary.directives.push(text);
1604
+ }
1605
+ continue;
1606
+ }
1607
+ break;
1608
+ }
1609
+ const isExported = (node) => {
1610
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
1611
+ return Boolean(modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword));
1612
+ };
1613
+ const isDefault = (node) => {
1614
+ const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
1615
+ return Boolean(modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword));
1616
+ };
1617
+ const recordFunctionLike = (name, exported) => {
1618
+ summary.functions.push(name);
1619
+ if (exported) {
1620
+ summary.exports.push(name);
1621
+ if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
1622
+ }
1623
+ if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
1624
+ if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
1625
+ };
1626
+ const stack = [sourceFile];
1627
+ while (stack.length > 0) {
1628
+ const node = stack.pop();
1629
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
1630
+ summary.imports.push(node.moduleSpecifier.text);
1631
+ }
1632
+ if (ts.isExportDeclaration(node)) {
1633
+ if (node.moduleSpecifier && ts.isStringLiteral(node.moduleSpecifier)) {
1634
+ summary.imports.push(node.moduleSpecifier.text);
1635
+ if (node.exportClause && ts.isNamespaceExport(node.exportClause)) {
1636
+ summary.exports.push(node.exportClause.name.text);
1637
+ } else if (!node.exportClause) {
1638
+ summary.reExports.push(node.moduleSpecifier.text);
1639
+ }
1640
+ }
1641
+ if (node.exportClause && ts.isNamedExports(node.exportClause)) {
1642
+ for (const element of node.exportClause.elements) {
1643
+ summary.exports.push(element.name.text);
1644
+ }
1645
+ }
1646
+ }
1647
+ if (ts.isExportAssignment(node)) {
1648
+ summary.exports.push("default");
1649
+ }
1650
+ if (ts.isFunctionDeclaration(node)) {
1651
+ const exported = isExported(node);
1652
+ if (node.name) {
1653
+ recordFunctionLike(node.name.text, exported);
1654
+ if (isDefault(node)) summary.exports.push("default");
1655
+ } else if (isDefault(node)) {
1656
+ summary.exports.push("default");
1657
+ }
1658
+ }
1659
+ if (ts.isClassDeclaration(node)) {
1660
+ if (node.name) {
1661
+ summary.classes.push(node.name.text);
1662
+ if (isExported(node)) summary.exports.push(node.name.text);
1663
+ if (isDefault(node)) summary.exports.push("default");
1664
+ if (/^[A-Z]/.test(node.name.text)) summary.reactComponents.push(node.name.text);
1665
+ } else if (isDefault(node)) {
1666
+ summary.exports.push("default");
1667
+ }
1668
+ }
1669
+ if (ts.isInterfaceDeclaration(node)) {
1670
+ summary.interfaces.push(node.name.text);
1671
+ if (isExported(node)) summary.exports.push(node.name.text);
1672
+ }
1673
+ if (ts.isTypeAliasDeclaration(node)) {
1674
+ summary.types.push(node.name.text);
1675
+ if (isExported(node)) summary.exports.push(node.name.text);
1676
+ }
1677
+ if (ts.isEnumDeclaration(node)) {
1678
+ summary.enums.push(node.name.text);
1679
+ if (isExported(node)) summary.exports.push(node.name.text);
1680
+ }
1681
+ if (ts.isVariableStatement(node)) {
1682
+ const exported = isExported(node);
1683
+ for (const declaration of node.declarationList.declarations) {
1684
+ if (!ts.isIdentifier(declaration.name)) continue;
1685
+ const name = declaration.name.text;
1686
+ summary.variables.push(name);
1687
+ if (exported) {
1688
+ summary.exports.push(name);
1689
+ if (ROUTE_HANDLER_NAMES.has(name)) summary.routeHandlers.push(name);
1690
+ }
1691
+ if (/^use[A-Z0-9]/.test(name)) summary.hooks.push(name);
1692
+ if (/^[A-Z]/.test(name)) summary.reactComponents.push(name);
1693
+ }
1694
+ }
1695
+ const children = [];
1696
+ ts.forEachChild(node, (child) => {
1697
+ children.push(child);
1698
+ });
1699
+ for (let index = children.length - 1; index >= 0; index--) {
1700
+ stack.push(children[index]);
1701
+ }
1702
+ }
1703
+ const dedupeSorted = (values) => Array.from(new Set(values)).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
1704
+ summary.directives = dedupeSorted(summary.directives);
1705
+ summary.imports = dedupeSorted(summary.imports);
1706
+ summary.exports = dedupeSorted(summary.exports);
1707
+ summary.reExports = dedupeSorted(summary.reExports);
1708
+ summary.functions = dedupeSorted(summary.functions);
1709
+ summary.classes = dedupeSorted(summary.classes);
1710
+ summary.interfaces = dedupeSorted(summary.interfaces);
1711
+ summary.types = dedupeSorted(summary.types);
1712
+ summary.enums = dedupeSorted(summary.enums);
1713
+ summary.variables = dedupeSorted(summary.variables);
1714
+ summary.reactComponents = dedupeSorted(summary.reactComponents);
1715
+ summary.hooks = dedupeSorted(summary.hooks);
1716
+ summary.routeHandlers = dedupeSorted(summary.routeHandlers);
1717
+ return summary;
1718
+ }
1719
+
1720
+ // src/generator/audit.ts
1721
+ function generateAuditReport(manifest, secretFindings) {
1722
+ const lines = [
1723
+ "# Codebase Health & Security Audit",
1724
+ "",
1725
+ `**Generated:** ${manifest.generatedAt} `,
1726
+ `**Git Branch:** \`${manifest.git.branch}\` (\`${manifest.git.commit.slice(0, 8)}\`${manifest.git.isDirty ? " - dirty" : ""}) `,
1727
+ `**Total Files:** ${manifest.counts.totalFiles} `,
1728
+ `**Total Lines:** ${manifest.counts.totalLines.toLocaleString()} `,
1729
+ `**Total Size:** ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB `,
1730
+ `**Redacted Content Bytes:** ${manifest.totals.totalContentBytes.toLocaleString()} `,
1731
+ "",
1732
+ "## 1. Security Scan & Redaction",
1733
+ "",
1734
+ "Matched values are replaced in place in `parts/*.md`; no excerpt of a matched",
1735
+ "line is reproduced in this report or anywhere else in the snapshot.",
1736
+ ""
1737
+ ];
1738
+ if (secretFindings.length === 0) {
1739
+ lines.push("\u2705 **No secret-like patterns detected.** Clean security scan.", "");
1740
+ } else {
1741
+ const byRule = /* @__PURE__ */ new Map();
1742
+ for (const finding of secretFindings) {
1743
+ byRule.set(finding.rule, (byRule.get(finding.rule) ?? 0) + 1);
1744
+ }
1745
+ lines.push(
1746
+ `\u26A0\uFE0F **${secretFindings.length} secret-like pattern(s) identified and redacted:**`,
1747
+ "",
1748
+ "| File | Line | Rule |",
1749
+ "| :--- | :--- | :--- |"
1750
+ );
1751
+ for (const finding of secretFindings) {
1752
+ lines.push(`| \`${finding.path ?? "unknown"}\` | ${finding.line} | \`${finding.rule}\` |`);
1753
+ }
1754
+ lines.push("", "### Matches by rule", "", "| Rule | Matches |", "| :--- | :--- |");
1755
+ for (const [rule, count] of Array.from(byRule.entries()).sort((a, b) => b[1] - a[1])) {
1756
+ lines.push(`| \`${rule}\` | ${count} |`);
1757
+ }
1758
+ lines.push("");
1759
+ }
1760
+ lines.push("## 2. Large Files Analysis", "");
1761
+ const largeFiles = manifest.files.filter((file) => file.isLarge);
1762
+ if (largeFiles.length === 0) {
1763
+ lines.push("\u2705 No files exceeded the large file budget.", "");
1764
+ } else {
1765
+ lines.push(
1766
+ `Found ${largeFiles.length} file(s) exceeding the large file budget (${(manifest.budgets.largeFileBytes / 1024).toFixed(0)} KB):`,
1767
+ "",
1768
+ "| File | Size (KB) | Lines | Preview Status |",
1769
+ "| :--- | :--- | :--- | :--- |"
1770
+ );
1771
+ for (const file of largeFiles) {
1772
+ const sizeKb = (file.sizeBytes / 1024).toFixed(1);
1773
+ const status = file.isPreviewTruncated ? "Truncated Head/Tail" : "Full";
1774
+ lines.push(`| \`${file.relativePath}\` | ${sizeKb} KB | ${file.lines} | ${status} |`);
1775
+ }
1776
+ lines.push("");
1777
+ }
1778
+ lines.push(
1779
+ "## 3. Architecture Layer Distribution",
1780
+ "",
1781
+ "| Layer | File Count | Percentage |",
1782
+ "| :--- | :--- | :--- |"
1783
+ );
1784
+ const totalFiles = manifest.counts.totalFiles || 1;
1785
+ for (const [layer, count] of Object.entries(manifest.layers).sort(
1786
+ (a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
1787
+ )) {
1788
+ const pct = (count / totalFiles * 100).toFixed(1);
1789
+ lines.push(`| \`${layer}\` | ${count} | ${pct}% |`);
1790
+ }
1791
+ lines.push("");
1792
+ lines.push(
1793
+ "## 4. File Kind Distribution",
1794
+ "",
1795
+ "| Kind | File Count | Percentage |",
1796
+ "| :--- | :--- | :--- |"
1797
+ );
1798
+ for (const [kind, count] of Object.entries(manifest.kinds).sort(
1799
+ (a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
1800
+ )) {
1801
+ const pct = (count / totalFiles * 100).toFixed(1);
1802
+ lines.push(`| \`${kind}\` | ${count} | ${pct}% |`);
1803
+ }
1804
+ lines.push("");
1805
+ lines.push("## 5. Skipped / Non-Text Files", "");
1806
+ if (manifest.skipped.length === 0) {
1807
+ lines.push("No files were skipped.", "");
1808
+ } else {
1809
+ lines.push(
1810
+ `Total skipped: ${manifest.skipped.length}`,
1811
+ "",
1812
+ "| File | Reason | Detail |",
1813
+ "| :--- | :--- | :--- |"
1814
+ );
1815
+ for (const skip of manifest.skipped) {
1816
+ lines.push(`| \`${skip.path}\` | \`${skip.reason}\` | ${skip.detail ?? "-"} |`);
1817
+ }
1818
+ lines.push("");
1819
+ }
1820
+ return lines.join("\n");
1821
+ }
1822
+
1823
+ // src/partitioner/chunker.ts
1824
+ import * as crypto from "crypto";
1825
+ var HEADER_RESERVE_BYTES = 2048;
1826
+ var MIN_SECTION_BUDGET_BYTES = 512;
1827
+ var MAX_SPLIT_PASSES = 64;
1828
+ function partitionFiles(files, maxPartBytes, contentFingerprint) {
1829
+ if (files.length === 0) return [];
1830
+ const budget = Math.max(MIN_SECTION_BUDGET_BYTES + HEADER_RESERVE_BYTES, maxPartBytes);
1831
+ const sectionBudget = budget - HEADER_RESERVE_BYTES;
1832
+ const prepared = files.map((file) => prepareSection(file, sectionBudget));
1833
+ let groups = greedyGroups(prepared, budget);
1834
+ for (let pass = 0; pass < MAX_SPLIT_PASSES; pass++) {
1835
+ const rendered = renderGroups(groups, contentFingerprint);
1836
+ const oversized = rendered.findIndex((part) => part.metadata.byteSize > budget);
1837
+ if (oversized === -1) return rendered;
1838
+ const group = groups[oversized];
1839
+ if (group.length > 1) {
1840
+ const moved = group.pop();
1841
+ groups.splice(oversized + 1, 0, [moved]);
1842
+ continue;
1843
+ }
1844
+ const [section] = group;
1845
+ const overflow = rendered[oversized].metadata.byteSize - budget;
1846
+ const currentContent = section.file.content ?? "";
1847
+ const shrinkTo = Math.max(
1848
+ 0,
1849
+ Buffer.byteLength(currentContent, "utf8") - overflow - 64
1850
+ );
1851
+ const shrunk = truncateToBytes(currentContent, shrinkTo);
1852
+ if (shrunk.isTruncated) {
1853
+ applyTruncatedContent(section.file, shrunk.content);
1854
+ section.text = renderFileSection(section.file);
1855
+ section.byteSize = Buffer.byteLength(section.text, "utf8");
1856
+ continue;
1857
+ }
1858
+ const marker = "\n... [part truncated: cannot fit budget] ...\n";
1859
+ const target = Math.max(0, budget - Buffer.byteLength(marker, "utf8"));
1860
+ const clipped = truncateToBytes(rendered[oversized].markdown, target);
1861
+ rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker);
1862
+ return rendered;
1863
+ }
1864
+ return enforceBudget(renderGroups(groups, contentFingerprint), budget);
1865
+ }
1866
+ function enforceBudget(parts, budget) {
1867
+ const marker = "\n... [part truncated: cannot fit budget] ...\n";
1868
+ const target = Math.max(0, budget - Buffer.byteLength(marker, "utf8"));
1869
+ return parts.map((part) => {
1870
+ if (part.metadata.byteSize <= budget) return part;
1871
+ const clipped = truncateToBytes(part.markdown, target);
1872
+ return rehashPart(part, clipped.content + marker);
1873
+ });
1874
+ }
1875
+ function renumberParts(parts, contentFingerprint) {
1876
+ const total = parts.length;
1877
+ if (total === 0) return [];
1878
+ const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
1879
+ const nameFor = (position) => `part-${zeroPad(position)}.md`;
1880
+ const marker = "## Contents\n";
1881
+ return parts.map((part, index) => {
1882
+ const markerIndex = part.markdown.indexOf(marker);
1883
+ const body = markerIndex === -1 ? "" : part.markdown.slice(markerIndex + marker.length);
1884
+ const included = part.metadata.includedFiles;
1885
+ if (markerIndex === -1) {
1886
+ throw new Error(`${part.filename} has no "## Contents" section to re-number.`);
1887
+ }
1888
+ const layerCounts = {};
1889
+ const kindCounts = {};
1890
+ for (const file of included) {
1891
+ layerCounts[file.layer] = (layerCounts[file.layer] || 0) + 1;
1892
+ kindCounts[file.kind] = (kindCounts[file.kind] || 0) + 1;
1893
+ }
1894
+ const header = [
1895
+ `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
1896
+ "",
1897
+ `- Previous: ${index === 0 ? "none" : nameFor(index)}`,
1898
+ `- Next: ${index === total - 1 ? "none" : nameFor(index + 2)}`,
1899
+ `- Files: ${included.length}`,
1900
+ `- Content Fingerprint: ${contentFingerprint || "none"}`,
1901
+ `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
1902
+ `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
1903
+ "",
1904
+ "## Files",
1905
+ "",
1906
+ "```text",
1907
+ ...included.map((file) => displayPath(file.relativePath)),
1908
+ "```",
1909
+ "",
1910
+ "## Contents",
1911
+ ""
1912
+ ].join("\n");
1913
+ const markdown = (header + body).replace(/\r\n/g, "\n");
1914
+ const metadata = {
1915
+ ...part.metadata,
1916
+ index: index + 1,
1917
+ filename: nameFor(index + 1),
1918
+ byteSize: Buffer.byteLength(markdown, "utf8"),
1919
+ sha256: crypto.createHash("sha256").update(markdown).digest("hex"),
1920
+ lines: countLines(markdown),
1921
+ fileCount: included.length,
1922
+ layersCovered: Object.keys(layerCounts).sort()
1923
+ };
1924
+ return { filename: metadata.filename, markdown, metadata };
1925
+ });
1926
+ }
1927
+ var SECTION_TRUNCATION_NOTICE = "\n... [section truncated: metadata alone exceeds the part budget] ...\n";
1928
+ var METADATA_VALUE_MAX = 200;
1929
+ var DISPLAY_PATH_MAX = 300;
1930
+ function capValue(value, max) {
1931
+ if (value.length <= max) return value;
1932
+ const half = Math.floor((max - 1) / 2);
1933
+ return `${value.slice(0, half)}\u2026${value.slice(value.length - half)}`;
1934
+ }
1935
+ var capMetadata = (value) => capValue(value, METADATA_VALUE_MAX);
1936
+ var displayPath = (value) => capValue(value, DISPLAY_PATH_MAX);
1937
+ function topEntries(counts, limit) {
1938
+ return Object.entries(counts).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)).slice(0, limit).map(([key, value]) => `${capMetadata(key)} (${value})`).join(", ");
1939
+ }
1940
+ function prepareSection(file, sectionBudget) {
1941
+ let text = renderFileSection(file);
1942
+ let byteSize = Buffer.byteLength(text, "utf8");
1943
+ if (byteSize <= sectionBudget) return { file, text, byteSize };
1944
+ const shrinkBody = () => {
1945
+ const content = file.content ?? "";
1946
+ const overhead = byteSize - Buffer.byteLength(content, "utf8");
1947
+ const allowed = sectionBudget - overhead - 96;
1948
+ if (allowed <= 0) return;
1949
+ const truncated = truncateToBytes(content, allowed);
1950
+ if (!truncated.isTruncated) return;
1951
+ applyTruncatedContent(file, truncated.content);
1952
+ text = renderFileSection(file);
1953
+ byteSize = Buffer.byteLength(text, "utf8");
1954
+ };
1955
+ shrinkBody();
1956
+ if (byteSize > sectionBudget && file.summary) {
1957
+ file.summary = void 0;
1958
+ file.summaryOmitted = true;
1959
+ text = renderFileSection(file);
1960
+ byteSize = Buffer.byteLength(text, "utf8");
1961
+ shrinkBody();
1962
+ }
1963
+ if (byteSize > sectionBudget) {
1964
+ const target = sectionBudget - Buffer.byteLength(SECTION_TRUNCATION_NOTICE, "utf8");
1965
+ const truncated = truncateToBytes(text, Math.max(0, target));
1966
+ text = truncated.content + SECTION_TRUNCATION_NOTICE;
1967
+ byteSize = Buffer.byteLength(text, "utf8");
1968
+ }
1969
+ return { file, text, byteSize };
1970
+ }
1971
+ function applyTruncatedContent(file, content) {
1972
+ file.content = content;
1973
+ file.isPreviewTruncated = true;
1974
+ file.inclusionMode = "preview";
1975
+ file.includedBytes = Buffer.byteLength(content, "utf8");
1976
+ file.includedLines = countLines(content);
1977
+ }
1978
+ function rehashPart(part, markdown) {
1979
+ return {
1980
+ filename: part.filename,
1981
+ markdown,
1982
+ metadata: {
1983
+ ...part.metadata,
1984
+ byteSize: Buffer.byteLength(markdown, "utf8"),
1985
+ sha256: crypto.createHash("sha256").update(markdown).digest("hex"),
1986
+ lines: countLines(markdown)
1987
+ }
1988
+ };
1989
+ }
1990
+ function greedyGroups(sections, budget) {
1991
+ const groups = [];
1992
+ let current = [];
1993
+ let bodyBytes = 0;
1994
+ let pathBytes = 0;
1995
+ for (const section of sections) {
1996
+ const sectionPathBytes = Buffer.byteLength(
1997
+ `${displayPath(section.file.relativePath)}
1998
+ `,
1999
+ "utf8"
2000
+ );
2001
+ const projected = bodyBytes + section.byteSize + pathBytes + sectionPathBytes + HEADER_RESERVE_BYTES;
2002
+ if (current.length > 0 && projected > budget) {
2003
+ groups.push(current);
2004
+ current = [];
2005
+ bodyBytes = 0;
2006
+ pathBytes = 0;
2007
+ }
2008
+ current.push(section);
2009
+ bodyBytes += section.byteSize;
2010
+ pathBytes += sectionPathBytes;
2011
+ }
2012
+ if (current.length > 0) groups.push(current);
2013
+ return groups;
2014
+ }
2015
+ function renderGroups(groups, contentFingerprint) {
2016
+ const total = groups.length;
2017
+ const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
2018
+ return groups.map((group, index) => {
2019
+ const filename = `part-${zeroPad(index + 1)}.md`;
2020
+ const prev = index === 0 ? "none" : `part-${zeroPad(index)}.md`;
2021
+ const next = index === total - 1 ? "none" : `part-${zeroPad(index + 2)}.md`;
2022
+ for (const section of group) {
2023
+ section.file.part = filename;
2024
+ }
2025
+ const layersCovered = Array.from(new Set(group.map((s) => s.file.layer))).sort();
2026
+ const layerCounts = {};
2027
+ const kindCounts = {};
2028
+ for (const section of group) {
2029
+ layerCounts[section.file.layer] = (layerCounts[section.file.layer] || 0) + 1;
2030
+ kindCounts[section.file.kind] = (kindCounts[section.file.kind] || 0) + 1;
2031
+ }
2032
+ const header = [
2033
+ `# Codebase Snapshot Part ${zeroPad(index + 1)} of ${zeroPad(total)}`,
2034
+ "",
2035
+ `- Previous: ${prev}`,
2036
+ `- Next: ${next}`,
2037
+ `- Files: ${group.length}`,
2038
+ `- Content Fingerprint: ${contentFingerprint || "none"}`,
2039
+ `- Layers: ${topEntries(layerCounts, 6) || "none"}`,
2040
+ `- Kinds: ${topEntries(kindCounts, 8) || "none"}`,
2041
+ "",
2042
+ "## Files",
2043
+ "",
2044
+ "```text",
2045
+ ...group.map((s) => displayPath(s.file.relativePath)),
2046
+ "```",
2047
+ "",
2048
+ "## Contents",
2049
+ ""
2050
+ ].join("\n");
2051
+ const content = (header + group.map((s) => s.text).join("")).replace(/\r\n/g, "\n");
2052
+ const byteSize = Buffer.byteLength(content, "utf8");
2053
+ const sha256 = crypto.createHash("sha256").update(content).digest("hex");
2054
+ const includedFiles = group.map(({ file }) => ({
2055
+ relativePath: file.relativePath,
2056
+ sourceBytes: file.sizeBytes,
2057
+ includedBytes: file.includedBytes,
2058
+ lines: file.includedLines,
2059
+ layer: file.layer,
2060
+ kind: file.kind
2061
+ }));
2062
+ const metadata = {
2063
+ index: index + 1,
2064
+ filename,
2065
+ byteSize,
2066
+ sha256,
2067
+ lines: countLines(content),
2068
+ fileCount: group.length,
2069
+ layersCovered,
2070
+ includedFiles
2071
+ };
2072
+ return { filename, markdown: content, metadata };
2073
+ });
2074
+ }
2075
+ function renderFileSection(file) {
2076
+ const content = file.content ?? "";
2077
+ const fence = fenceForContent(content);
2078
+ const anchor = anchorForPath(file.relativePath);
2079
+ return [
2080
+ `<a id="${anchor}"></a>`,
2081
+ `### FILE: ${displayPath(file.relativePath)}`,
2082
+ "",
2083
+ `- Layer: ${capMetadata(file.layer)}`,
2084
+ `- Kind: ${capMetadata(file.kind)}`,
2085
+ `- Language: ${capMetadata(file.language)}`,
2086
+ `- Inclusion: ${file.inclusionMode}`,
2087
+ `- Source Bytes: ${file.sizeBytes}`,
2088
+ `- Source Lines: ${file.originalLines}`,
2089
+ `- Included Lines: ${file.includedLines}`,
2090
+ `- SHA-256: ${file.hashSha256}`,
2091
+ ...renderSummary(file.summary),
2092
+ ...file.summaryOmitted ? ["- Summary: (omitted: exceeds the part budget)"] : [],
2093
+ "",
2094
+ file.inclusionMode === "preview" ? "#### Bounded Preview" : "#### Content",
2095
+ "",
2096
+ `${fence}${capMetadata(file.language)}`,
2097
+ content.endsWith("\n") ? content.slice(0, -1) : content,
2098
+ fence,
2099
+ "",
2100
+ ""
2101
+ ].join("\n");
2102
+ }
2103
+ function fenceForContent(content) {
2104
+ const matches = content.match(/`+/g);
2105
+ const maxRun = matches?.reduce((max, run) => Math.max(max, run.length), 0) ?? 0;
2106
+ return "`".repeat(Math.max(4, maxRun + 1));
2107
+ }
2108
+ function renderSummary(summary) {
2109
+ if (!summary) return [];
2110
+ if (typeof summary === "string") return [`- Summary: ${summary}`];
2111
+ return ["- Summary:", "```json", JSON.stringify(summary, null, 2), "```"];
2112
+ }
2113
+ function anchorForPath(filePath) {
2114
+ const slug = filePath.toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
2115
+ const digest = crypto.createHash("sha256").update(filePath).digest("hex").slice(0, 8);
2116
+ return `${slug || "file"}-${digest}`;
2117
+ }
2118
+
2119
+ // src/generator/monolith.ts
2120
+ function generateMonolithJson(manifest, filesWithContent) {
2121
+ return JSON.stringify({ manifest, files: filesWithContent }, null, 2);
2122
+ }
2123
+ function generateMonolithMarkdown(manifest, filesWithContent) {
2124
+ const sections = filesWithContent.map((file) => {
2125
+ const content = (file.content ?? "").replace(/\r\n/g, "\n");
2126
+ const fence = fenceForContent(content);
2127
+ return [
2128
+ `## File: ${file.relativePath}`,
2129
+ "",
2130
+ `- **Layer:** \`${file.layer}\``,
2131
+ `- **Kind:** \`${file.kind}\``,
2132
+ `- **Lines:** \`${file.originalLines}\``,
2133
+ `- **Size:** \`${file.sizeBytes} B\``,
2134
+ `- **Inclusion:** \`${file.inclusionMode}\``,
2135
+ "",
2136
+ `${fence}${file.language}`,
2137
+ content.endsWith("\n") ? content.slice(0, -1) : content,
2138
+ fence,
2139
+ "",
2140
+ "---",
2141
+ ""
2142
+ ].join("\n");
2143
+ });
2144
+ return [
2145
+ `# Complete Codebase Monolith`,
2146
+ "",
2147
+ `**Generated:** ${manifest.generatedAt} `,
2148
+ `**Total Files:** ${manifest.counts.totalFiles} `,
2149
+ `**Total Lines:** ${manifest.counts.totalLines} `,
2150
+ "",
2151
+ "---",
2152
+ "",
2153
+ ...sections
2154
+ ].join("\n");
2155
+ }
2156
+
2157
+ // src/generator/navigation.ts
2158
+ function generateNavigation(manifest) {
2159
+ const lines = [
2160
+ "# Codebase Navigation Index",
2161
+ "",
2162
+ "Fast lookup index mapping every file to its snapshot chunk.",
2163
+ "",
2164
+ "## Parts Overview",
2165
+ "",
2166
+ "| Part | File Count | Size (KB) | Layers Covered |",
2167
+ "| :--- | :--- | :--- | :--- |"
2168
+ ];
2169
+ for (const part of manifest.parts) {
2170
+ const sizeKb = (part.byteSize / 1024).toFixed(1);
2171
+ lines.push(
2172
+ `| [${part.filename}](../parts/${part.filename}) | ${part.fileCount} | ${sizeKb} KB | ${part.layersCovered.join(", ")} |`
2173
+ );
2174
+ }
2175
+ lines.push(
2176
+ "",
2177
+ "## File Directory",
2178
+ "",
2179
+ "| File | Part | Layer | Kind | Lines | Size |",
2180
+ "| :--- | :--- | :--- | :--- | :--- | :--- |"
2181
+ );
2182
+ const filePartMap = /* @__PURE__ */ new Map();
2183
+ for (const part of manifest.parts) {
2184
+ for (const file of part.includedFiles) {
2185
+ filePartMap.set(file.relativePath, part.filename);
2186
+ }
2187
+ }
2188
+ for (const file of manifest.files) {
2189
+ const partFilename = filePartMap.get(file.relativePath) || file.part || "none";
2190
+ const anchor = anchorForPath(file.relativePath);
2191
+ const link = `[${file.relativePath}](../parts/${partFilename}#${anchor})`;
2192
+ const sizeStr = `${(file.sizeBytes / 1024).toFixed(1)} KB`;
2193
+ lines.push(
2194
+ `| ${link} | \`${partFilename}\` | \`${file.layer}\` | \`${file.kind}\` | ${file.lines} | ${sizeStr} |`
2195
+ );
2196
+ }
2197
+ lines.push("");
2198
+ return lines.join("\n");
2199
+ }
2200
+
2201
+ // src/generator/readme.ts
2202
+ function generateReadme(manifest) {
2203
+ const lines = [
2204
+ "# Codebase Snapshot",
2205
+ "",
2206
+ "This directory contains a complete, partitioned, and audited snapshot of this codebase, optimized for Large Language Models (LLMs) and automated agents.",
2207
+ "",
2208
+ "## Snapshot Metrics",
2209
+ "",
2210
+ `- **Generated At:** \`${manifest.generatedAt}\``,
2211
+ `- **Schema Version:** \`${manifest.schemaVersion}\``,
2212
+ `- **Branch:** \`${manifest.git.branch}\` (\`${manifest.git.commit.slice(0, 8)}\`${manifest.git.isDirty ? " - dirty" : ""})`,
2213
+ `- **Total Tracked Files:** \`${manifest.counts.totalFiles}\``,
2214
+ `- **Total Code Lines:** \`${manifest.counts.totalLines.toLocaleString()}\``,
2215
+ `- **Total Size:** \`${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB\``,
2216
+ `- **Emitted Content:** \`${(manifest.totals.totalContentBytes / (1024 * 1024)).toFixed(2)} MB\``,
2217
+ `- **Part Count:** \`${manifest.counts.partsCount}\` (budget \`${(manifest.budgets.maxPartBytes / 1024).toFixed(0)} KB\` per part)`,
2218
+ `- **Generator:** \`${manifest.generator.name}@${manifest.generator.version}\``,
2219
+ "",
2220
+ "## Documentation & Reports",
2221
+ "",
2222
+ "- [reports/navigation.md](reports/navigation.md) \u2014 Comprehensive file directory mapping files to chunk files.",
2223
+ "- [reports/project-tree.md](reports/project-tree.md) \u2014 Full visual ASCII directory tree of the repository.",
2224
+ "- [reports/audit.md](reports/audit.md) \u2014 Security scan counts, large files report, and architectural breakdown.",
2225
+ "- [manifest.json](manifest.json) \u2014 Complete machine-readable metadata and AST symbol registry.",
2226
+ "",
2227
+ "## Snapshot Parts",
2228
+ "",
2229
+ "The codebase is partitioned into budget-safe chunks under `parts/` to avoid context window degradation:",
2230
+ "",
2231
+ "| Part | File Count | Size (KB) | Layers Covered |",
2232
+ "| :--- | :--- | :--- | :--- |"
2233
+ ];
2234
+ for (const part of manifest.parts) {
2235
+ const sizeKb = (part.byteSize / 1024).toFixed(1);
2236
+ lines.push(
2237
+ `| [${part.filename}](parts/${part.filename}) | ${part.fileCount} | ${sizeKb} KB | ${part.layersCovered.join(", ")} |`
2238
+ );
2239
+ }
2240
+ lines.push(
2241
+ "",
2242
+ "## AI Agent Ingestion Guide",
2243
+ "",
2244
+ "1. Read `reports/navigation.md` or `reports/project-tree.md` first to locate target features or files.",
2245
+ "2. Open only the specific `parts/part-XXX.md` file holding the target modules.",
2246
+ "3. Inspect `manifest.json` for precise dependency and AST symbol maps.",
2247
+ "4. Check `manifest.validation.status` before trusting the snapshot; `snapshot validate` re-checks it against disk.",
2248
+ ""
2249
+ );
2250
+ return lines.join("\n");
2251
+ }
2252
+
2253
+ // src/generator/tree.ts
2254
+ var MAX_TREE_LINES = 2e4;
2255
+ function generateProjectTree(filePaths) {
2256
+ const root = { children: /* @__PURE__ */ new Map() };
2257
+ for (const filePath of filePaths) {
2258
+ const parts = filePath.split("/").filter((part) => part.length > 0);
2259
+ let current = root;
2260
+ for (const part of parts) {
2261
+ let child = current.children.get(part);
2262
+ if (!child) {
2263
+ child = { children: /* @__PURE__ */ new Map() };
2264
+ current.children.set(part, child);
2265
+ }
2266
+ current = child;
2267
+ }
2268
+ }
2269
+ const lines = ["# Project Tree", "", "```text", "."];
2270
+ let truncatedAt = -1;
2271
+ function walk(node, prefix) {
2272
+ if (truncatedAt !== -1) return;
2273
+ const keys = Array.from(node.children.keys()).sort((a, b) => {
2274
+ const aIsDir = node.children.get(a).children.size > 0;
2275
+ const bIsDir = node.children.get(b).children.size > 0;
2276
+ if (aIsDir !== bIsDir) return aIsDir ? -1 : 1;
2277
+ return a < b ? -1 : a > b ? 1 : 0;
2278
+ });
2279
+ keys.forEach((key, index) => {
2280
+ if (truncatedAt !== -1) return;
2281
+ const isLast = index === keys.length - 1;
2282
+ const child = node.children.get(key);
2283
+ const isDirectory = child.children.size > 0;
2284
+ lines.push(`${prefix}${isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 "}${key}${isDirectory ? "/" : ""}`);
2285
+ if (lines.length >= MAX_TREE_LINES) {
2286
+ truncatedAt = lines.length;
2287
+ return;
2288
+ }
2289
+ walk(child, prefix + (isLast ? " " : "\u2502 "));
2290
+ });
2291
+ }
2292
+ walk(root, "");
2293
+ if (truncatedAt !== -1) {
2294
+ lines.push(`... [tree truncated at ${MAX_TREE_LINES} lines] ...`);
2295
+ }
2296
+ lines.push("```", "");
2297
+ return lines.join("\n");
2298
+ }
2299
+
2300
+ // src/scanner/binary.ts
2301
+ import * as fs6 from "fs";
2302
+ import * as path7 from "path";
2303
+ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2304
+ ".png",
2305
+ ".jpg",
2306
+ ".jpeg",
2307
+ ".gif",
2308
+ ".webp",
2309
+ ".avif",
2310
+ ".ico",
2311
+ ".bmp",
2312
+ ".tiff",
2313
+ ".pdf",
2314
+ ".zip",
2315
+ ".tar",
2316
+ ".gz",
2317
+ ".tgz",
2318
+ ".bz2",
2319
+ ".xz",
2320
+ ".7z",
2321
+ ".rar",
2322
+ ".mp3",
2323
+ ".mp4",
2324
+ ".wav",
2325
+ ".ogg",
2326
+ ".webm",
2327
+ ".mov",
2328
+ ".avi",
2329
+ ".woff",
2330
+ ".woff2",
2331
+ ".ttf",
2332
+ ".otf",
2333
+ ".eot",
2334
+ ".wasm",
2335
+ ".node",
2336
+ ".exe",
2337
+ ".dll",
2338
+ ".dylib",
2339
+ ".so"
2340
+ ]);
2341
+ function isBinaryExtension(filePath) {
2342
+ const ext = path7.extname(filePath).toLowerCase();
2343
+ return BINARY_EXTENSIONS.has(ext);
2344
+ }
2345
+ function isBinaryBuffer(buffer) {
2346
+ const checkLength = Math.min(buffer.length, 8192);
2347
+ for (let index = 0; index < checkLength; index++) {
2348
+ if (buffer[index] === 0) return true;
2349
+ }
2350
+ return false;
2351
+ }
2352
+ function isBinaryFile(absolutePath) {
2353
+ if (isBinaryExtension(absolutePath)) return true;
2354
+ let fd;
2355
+ try {
2356
+ fd = fs6.openSync(absolutePath, "r");
2357
+ const buffer = Buffer.alloc(8192);
2358
+ const bytesRead = fs6.readSync(fd, buffer, 0, 8192, 0);
2359
+ if (bytesRead === 0) return false;
2360
+ return isBinaryBuffer(buffer.subarray(0, bytesRead));
2361
+ } catch {
2362
+ return false;
2363
+ } finally {
2364
+ if (fd !== void 0) {
2365
+ try {
2366
+ fs6.closeSync(fd);
2367
+ } catch {
2368
+ }
2369
+ }
2370
+ }
2371
+ }
2372
+
2373
+ // src/validator/validate.ts
2374
+ import * as crypto2 from "crypto";
2375
+ import * as fs7 from "fs";
2376
+ import * as path8 from "path";
2377
+ function emptyDrift() {
2378
+ return {
2379
+ missingInSnapshot: [],
2380
+ missingOnDisk: [],
2381
+ hashMismatches: [],
2382
+ partIssues: [],
2383
+ totalsMismatches: [],
2384
+ configMismatches: []
2385
+ };
2386
+ }
2387
+ function sha256Hex(buffer) {
2388
+ return crypto2.createHash("sha256").update(buffer).digest("hex");
2389
+ }
2390
+ function validateSnapshot(configInput) {
2391
+ assertUsableConfigInput(configInput);
2392
+ const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
2393
+ const manifestPath = path8.join(config.cwd, config.outDir, "manifest.json");
2394
+ if (!fs7.existsSync(manifestPath)) {
2395
+ return {
2396
+ valid: false,
2397
+ manifestPresent: false,
2398
+ drift: emptyDrift(),
2399
+ errors: [`Snapshot manifest not found at: ${manifestPath}`],
2400
+ warnings: []
2401
+ };
2402
+ }
2403
+ let manifest;
2404
+ try {
2405
+ manifest = JSON.parse(fs7.readFileSync(manifestPath, "utf8"));
2406
+ } catch (err) {
2407
+ return {
2408
+ valid: false,
2409
+ manifestPresent: true,
2410
+ drift: emptyDrift(),
2411
+ errors: [`Failed to parse snapshot manifest: ${err.message}`],
2412
+ warnings: []
2413
+ };
2414
+ }
2415
+ if (!manifest || typeof manifest !== "object" || !Array.isArray(manifest.files) || !Array.isArray(manifest.skipped) || !Array.isArray(manifest.parts)) {
2416
+ return {
2417
+ valid: false,
2418
+ manifestPresent: true,
2419
+ drift: emptyDrift(),
2420
+ errors: [
2421
+ `${manifestPath} is not a snapshot manifest v3: expected arrays for "files", "skipped" and "parts".`
2422
+ ],
2423
+ warnings: []
2424
+ };
2425
+ }
2426
+ const drift = emptyDrift();
2427
+ const warnings = [];
2428
+ const snapshotDir = path8.join(config.cwd, config.outDir);
2429
+ const manifestFileMap = /* @__PURE__ */ new Map();
2430
+ for (const file of manifest.files) {
2431
+ if (file && typeof file.relativePath === "string") {
2432
+ manifestFileMap.set(file.relativePath, file.hashSha256);
2433
+ }
2434
+ }
2435
+ const skippedSet = new Set(
2436
+ manifest.skipped.filter((entry) => entry && typeof entry.path === "string").map((entry) => entry.path)
2437
+ );
2438
+ const collection = collectFiles(config.cwd, {
2439
+ includePatterns: config.include,
2440
+ excludePatterns: config.excludePatterns,
2441
+ includeSensitiveFiles: config.includeSensitiveFiles
2442
+ });
2443
+ for (const diskFile of collection.files) {
2444
+ if (skippedSet.has(diskFile)) continue;
2445
+ const snapshotHash = manifestFileMap.get(diskFile);
2446
+ if (!snapshotHash) {
2447
+ drift.missingInSnapshot.push(diskFile);
2448
+ continue;
2449
+ }
2450
+ let diskHash;
2451
+ try {
2452
+ diskHash = sha256Hex(fs7.readFileSync(path8.join(config.cwd, diskFile)));
2453
+ } catch {
2454
+ drift.missingOnDisk.push(diskFile);
2455
+ continue;
2456
+ }
2457
+ if (diskHash !== snapshotHash) {
2458
+ drift.hashMismatches.push({ path: diskFile, diskHash, snapshotHash });
2459
+ }
2460
+ }
2461
+ for (const snapshotFile of manifestFileMap.keys()) {
2462
+ if (!fs7.existsSync(path8.join(config.cwd, snapshotFile))) {
2463
+ drift.missingOnDisk.push(snapshotFile);
2464
+ }
2465
+ }
2466
+ const maxEffectiveBudget = Math.max(
2467
+ config.maxPartBytes,
2468
+ config.budgets.default,
2469
+ ...Object.values(config.budgets.byLayer)
2470
+ );
2471
+ const partNames = /* @__PURE__ */ new Set();
2472
+ for (const part of manifest.parts) {
2473
+ if (!part || typeof part.filename !== "string") continue;
2474
+ partNames.add(part.filename);
2475
+ if (!config.artifacts.parts) continue;
2476
+ const partPath = path8.join(snapshotDir, "parts", part.filename);
2477
+ let buffer;
2478
+ try {
2479
+ buffer = fs7.readFileSync(partPath);
2480
+ } catch {
2481
+ drift.partIssues.push({ part: part.filename, issue: "missing from snapshot/parts" });
2482
+ continue;
2483
+ }
2484
+ if (buffer.length !== part.byteSize) {
2485
+ drift.partIssues.push({
2486
+ part: part.filename,
2487
+ issue: `byteSize is ${part.byteSize} in the manifest but ${buffer.length} on disk`
2488
+ });
2489
+ }
2490
+ if (sha256Hex(buffer) !== part.sha256) {
2491
+ drift.partIssues.push({ part: part.filename, issue: "sha256 does not match the manifest" });
2492
+ }
2493
+ if (buffer.length > maxEffectiveBudget) {
2494
+ drift.partIssues.push({
2495
+ part: part.filename,
2496
+ issue: `${buffer.length} B exceeds the effective part budget ${maxEffectiveBudget} B`
2497
+ });
2498
+ }
2499
+ }
2500
+ const filesInParts = manifest.parts.reduce(
2501
+ (total, part) => total + (typeof part?.fileCount === "number" ? part.fileCount : 0),
2502
+ 0
2503
+ );
2504
+ if (filesInParts !== manifest.files.length) {
2505
+ drift.partIssues.push({
2506
+ part: "(totals)",
2507
+ issue: `parts list ${filesInParts} file(s) but the manifest lists ${manifest.files.length}`
2508
+ });
2509
+ }
2510
+ for (const file of manifest.files) {
2511
+ if (file?.part && !partNames.has(file.part)) {
2512
+ drift.partIssues.push({
2513
+ part: file.part,
2514
+ issue: `${file.relativePath} points at a part that is not in the manifest`
2515
+ });
2516
+ }
2517
+ }
2518
+ const recorded = manifest.config;
2519
+ if (recorded && typeof recorded === "object") {
2520
+ if (recorded.maxPartBytes !== config.maxPartBytes) {
2521
+ drift.configMismatches.push(
2522
+ `maxPartBytes: manifest ${recorded.maxPartBytes} vs current ${config.maxPartBytes}`
2523
+ );
2524
+ }
2525
+ if (recorded.largeFileBytes !== config.largeFileBytes) {
2526
+ drift.configMismatches.push(
2527
+ `largeFileBytes: manifest ${recorded.largeFileBytes} vs current ${config.largeFileBytes}`
2528
+ );
2529
+ }
2530
+ if (recorded.previewHeadLines !== config.previewHeadLines) {
2531
+ drift.configMismatches.push(
2532
+ `previewHeadLines: manifest ${recorded.previewHeadLines} vs current ${config.previewHeadLines}`
2533
+ );
2534
+ }
2535
+ if (recorded.previewTailLines !== config.previewTailLines) {
2536
+ drift.configMismatches.push(
2537
+ `previewTailLines: manifest ${recorded.previewTailLines} vs current ${config.previewTailLines}`
2538
+ );
2539
+ }
2540
+ if (recorded.includeSensitiveFiles !== config.includeSensitiveFiles) {
2541
+ drift.configMismatches.push(
2542
+ `includeSensitiveFiles: manifest ${recorded.includeSensitiveFiles} vs current ${config.includeSensitiveFiles}`
2543
+ );
2544
+ }
2545
+ const recordedExcludes = Array.isArray(recorded.excludePatterns) ? recorded.excludePatterns : [];
2546
+ if (recordedExcludes.join("\0") !== config.excludePatterns.join("\0")) {
2547
+ drift.configMismatches.push(
2548
+ `excludePatterns: manifest [${recordedExcludes.join(", ")}] vs current [${config.excludePatterns.join(", ")}]`
2549
+ );
2550
+ }
2551
+ const recordedInclude = Array.isArray(recorded.include) ? recorded.include : [];
2552
+ if (recordedInclude.join("\0") !== config.include.join("\0")) {
2553
+ drift.configMismatches.push(
2554
+ `include: manifest [${recordedInclude.join(", ")}] vs current [${config.include.join(", ")}]`
2555
+ );
2556
+ }
2557
+ if (recorded?.artifacts && typeof recorded.artifacts === "object") {
2558
+ for (const name of ARTIFACT_NAMES) {
2559
+ if (recorded.artifacts[name] !== config.artifacts[name]) {
2560
+ drift.configMismatches.push(
2561
+ `artifacts.${name}: manifest ${recorded.artifacts[name]} vs current ${config.artifacts[name]}`
2562
+ );
2563
+ }
2564
+ }
2565
+ }
2566
+ if (recorded?.layersMode !== void 0 && recorded.layersMode !== config.layersMode) {
2567
+ drift.configMismatches.push(
2568
+ `layersMode: manifest ${recorded.layersMode} vs current ${config.layersMode}`
2569
+ );
2570
+ }
2571
+ const recordedBudgets = recorded?.budgets;
2572
+ if (recordedBudgets && typeof recordedBudgets === "object") {
2573
+ if (recordedBudgets.default !== config.budgets.default) {
2574
+ drift.configMismatches.push(
2575
+ `budgets.default: manifest ${recordedBudgets.default} vs current ${config.budgets.default}`
2576
+ );
2577
+ }
2578
+ const recordedByLayer = recordedBudgets.byLayer ?? {};
2579
+ const layerNames = /* @__PURE__ */ new Set([
2580
+ ...Object.keys(recordedByLayer),
2581
+ ...Object.keys(config.budgets.byLayer)
2582
+ ]);
2583
+ for (const layer of Array.from(layerNames).sort()) {
2584
+ if (recordedByLayer[layer] !== config.budgets.byLayer[layer]) {
2585
+ drift.configMismatches.push(
2586
+ `budgets.byLayer.${layer}: manifest ${recordedByLayer[layer]} vs current ${config.budgets.byLayer[layer]}`
2587
+ );
2588
+ }
2589
+ }
2590
+ }
2591
+ } else {
2592
+ drift.configMismatches.push("manifest does not record the effective configuration");
2593
+ }
2594
+ const totalsMismatches = drift.totalsMismatches;
2595
+ const expectEqual = (label, recordedValue, actual) => {
2596
+ if (recordedValue !== actual) {
2597
+ totalsMismatches.push(`${label}: manifest ${String(recordedValue)} vs actual ${actual}`);
2598
+ }
2599
+ };
2600
+ const counts = manifest.counts ?? {};
2601
+ const totals = manifest.totals ?? {};
2602
+ const sumSourceBytes = manifest.files.reduce(
2603
+ (total, file) => total + (typeof file?.sizeBytes === "number" ? file.sizeBytes : 0),
2604
+ 0
2605
+ );
2606
+ const sumIncludedBytes = manifest.parts.reduce(
2607
+ (total, part) => total + (Array.isArray(part?.includedFiles) ? part.includedFiles.reduce(
2608
+ (inner, entry) => inner + (typeof entry?.includedBytes === "number" ? entry.includedBytes : 0),
2609
+ 0
2610
+ ) : 0),
2611
+ 0
2612
+ );
2613
+ expectEqual("counts.totalFiles", counts.totalFiles, manifest.files.length);
2614
+ expectEqual("counts.partsCount", counts.partsCount, manifest.parts.length);
2615
+ expectEqual("counts.skippedCount", counts.skippedCount, manifest.skipped.length);
2616
+ expectEqual("counts.totalSizeBytes", counts.totalSizeBytes, sumSourceBytes);
2617
+ expectEqual("totals.includedFiles", totals.includedFiles, manifest.files.length);
2618
+ expectEqual("totals.partCount", totals.partCount, manifest.parts.length);
2619
+ expectEqual("totals.skippedFiles", totals.skippedFiles, manifest.skipped.length);
2620
+ expectEqual("totals.totalSourceBytes", totals.totalSourceBytes, sumSourceBytes);
2621
+ expectEqual("totals.totalContentBytes", totals.totalContentBytes, sumIncludedBytes);
2622
+ expectEqual(
2623
+ "totals.candidateFiles",
2624
+ totals.candidateFiles,
2625
+ manifest.files.length + manifest.skipped.length
2626
+ );
2627
+ expectEqual(
2628
+ "totals.warningCount",
2629
+ totals.warningCount,
2630
+ Array.isArray(manifest.warnings) ? manifest.warnings.length : 0
2631
+ );
2632
+ if (typeof totals.fullFiles === "number" && typeof totals.previewFiles === "number" && totals.fullFiles + totals.previewFiles !== manifest.files.length) {
2633
+ totalsMismatches.push(
2634
+ `totals.fullFiles + previewFiles (${totals.fullFiles} + ${totals.previewFiles}) != ${manifest.files.length} files`
2635
+ );
2636
+ }
2637
+ if (manifest.budgets && manifest.budgets.maxPartBytes !== config.maxPartBytes) {
2638
+ totalsMismatches.push(
2639
+ `budgets.maxPartBytes: manifest ${manifest.budgets.maxPartBytes} vs current ${config.maxPartBytes}`
2640
+ );
2641
+ }
2642
+ if (manifest.budgets && manifest.budgets.largeFileBytes !== config.largeFileBytes) {
2643
+ totalsMismatches.push(
2644
+ `budgets.largeFileBytes: manifest ${manifest.budgets.largeFileBytes} vs current ${config.largeFileBytes}`
2645
+ );
2646
+ }
2647
+ if (manifest.git && typeof manifest.git.commit === "string") {
2648
+ const current = getGitInfo(config.cwd, config.branch);
2649
+ if (manifest.git.commit !== "unknown" && current.commit !== "unknown" && manifest.git.commit !== current.commit) {
2650
+ warnings.push(
2651
+ `Snapshot was generated at commit ${manifest.git.commit.slice(0, 8)} but HEAD is ${current.commit.slice(0, 8)}.`
2652
+ );
2653
+ }
2654
+ if (current.isDirty) {
2655
+ warnings.push("Working tree has uncommitted changes.");
2656
+ }
2657
+ }
2658
+ if (manifest.validation && manifest.validation.status === "failed") {
2659
+ const declared = Array.isArray(manifest.validation.errors) ? manifest.validation.errors : [];
2660
+ for (const error of declared) {
2661
+ drift.configMismatches.push(`manifest declared itself invalid: ${error}`);
2662
+ }
2663
+ }
2664
+ for (const [key, artifact] of [
2665
+ ["full-codebase.json", manifest.artifacts?.fullJson],
2666
+ ["full-codebase.md", manifest.artifacts?.fullMarkdown]
2667
+ ]) {
2668
+ if (!artifact) continue;
2669
+ const artifactPath = path8.join(snapshotDir, key);
2670
+ try {
2671
+ const buffer = fs7.readFileSync(artifactPath);
2672
+ if (sha256Hex(buffer) !== artifact.sha256 || buffer.length !== artifact.byteSize) {
2673
+ drift.partIssues.push({ part: key, issue: "artifact does not match its recorded digest" });
2674
+ }
2675
+ } catch {
2676
+ drift.partIssues.push({ part: key, issue: "declared artifact is missing" });
2677
+ }
2678
+ }
2679
+ if (manifest.git && manifest.git.branch && manifest.git.branch !== config.branch) {
2680
+ warnings.push(
2681
+ `Snapshot was generated on branch "${manifest.git.branch}" but the configured branch is "${config.branch}".`
2682
+ );
2683
+ }
2684
+ const errors = [];
2685
+ if (drift.missingInSnapshot.length > 0) {
2686
+ errors.push(`${drift.missingInSnapshot.length} file(s) on disk are missing from the snapshot.`);
2687
+ }
2688
+ if (drift.missingOnDisk.length > 0) {
2689
+ errors.push(`${drift.missingOnDisk.length} file(s) in snapshot no longer exist on disk.`);
2690
+ }
2691
+ if (drift.hashMismatches.length > 0) {
2692
+ errors.push(
2693
+ `${drift.hashMismatches.length} file(s) have been modified since the snapshot was generated.`
2694
+ );
2695
+ }
2696
+ if (drift.partIssues.length > 0) {
2697
+ errors.push(`${drift.partIssues.length} snapshot artefact problem(s) detected.`);
2698
+ }
2699
+ if (drift.totalsMismatches.length > 0) {
2700
+ errors.push(
2701
+ `${drift.totalsMismatches.length} manifest bookkeeping value(s) disagree with the snapshot contents.`
2702
+ );
2703
+ }
2704
+ if (drift.configMismatches.length > 0) {
2705
+ errors.push(
2706
+ `${drift.configMismatches.length} configuration mismatch(es) between the manifest and this run.`
2707
+ );
2708
+ }
2709
+ return {
2710
+ valid: errors.length === 0,
2711
+ manifestPresent: true,
2712
+ drift,
2713
+ errors,
2714
+ warnings
2715
+ };
2716
+ }
2717
+
2718
+ // src/index.ts
2719
+ var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
2720
+ var GENERATOR_VERSION = "2.0.0";
2721
+ var SCHEMA_VERSION = "3.0.0";
2722
+ function sha256Hex2(input) {
2723
+ return crypto3.createHash("sha256").update(input).digest("hex");
2724
+ }
2725
+ var SUMMARY_EXTENSIONS = [
2726
+ ".ts",
2727
+ ".mts",
2728
+ ".cts",
2729
+ ".tsx",
2730
+ ".js",
2731
+ ".mjs",
2732
+ ".cjs",
2733
+ ".jsx",
2734
+ ".json",
2735
+ ".jsonc",
2736
+ ".md",
2737
+ ".mdx"
2738
+ ];
2739
+ function summarizeFile(relativePath, redactedContent) {
2740
+ const lower = relativePath.toLowerCase();
2741
+ if (!SUMMARY_EXTENSIONS.some((extension) => lower.endsWith(extension))) {
2742
+ return void 0;
2743
+ }
2744
+ if (lower.endsWith(".json") || lower.endsWith(".jsonc")) {
2745
+ return summarizeJson(redactedContent);
2746
+ }
2747
+ if (lower.endsWith(".md") || lower.endsWith(".mdx")) {
2748
+ return summarizeMarkdown(redactedContent);
2749
+ }
2750
+ return summarizeTypeScriptSource(relativePath, redactedContent);
2751
+ }
2752
+ function isSourceSummary(summary) {
2753
+ return Boolean(summary && typeof summary === "object" && "imports" in summary);
2754
+ }
2755
+ function bump(counts, key) {
2756
+ counts[key] = (counts[key] ?? 0) + 1;
2757
+ }
2758
+ var RELATIVE_SPECIFIER = /^\.{1,2}\//;
2759
+ var IMPORT_CANDIDATE_SUFFIXES = [
2760
+ "",
2761
+ ".ts",
2762
+ ".tsx",
2763
+ ".mts",
2764
+ ".cts",
2765
+ ".js",
2766
+ ".jsx",
2767
+ ".mjs",
2768
+ ".cjs",
2769
+ "/index.ts",
2770
+ "/index.tsx",
2771
+ "/index.js",
2772
+ "/index.mjs"
2773
+ ];
2774
+ function resolveSpecifier(fromFile, specifier, knownFiles) {
2775
+ if (!RELATIVE_SPECIFIER.test(specifier)) return null;
2776
+ const baseDir = path9.posix.dirname(fromFile);
2777
+ const joined = path9.posix.normalize(path9.posix.join(baseDir, specifier));
2778
+ const candidate = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
2779
+ for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
2780
+ const withSuffix = `${joined}${suffix}`;
2781
+ if (knownFiles.has(withSuffix)) return withSuffix;
2782
+ }
2783
+ const extensionSwap = new RegExp(`${candidate}\\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs)$`);
2784
+ for (const file of knownFiles) {
2785
+ if (extensionSwap.test(file)) return file;
2786
+ }
2787
+ return null;
2788
+ }
2789
+ function buildIndexes(files) {
2790
+ const knownFiles = new Set(files.map((file) => file.relativePath));
2791
+ const importerCounts = /* @__PURE__ */ new Map();
2792
+ const byFile = files.filter((file) => isSourceSummary(file.summary)).map((file) => {
2793
+ const summary = file.summary;
2794
+ return {
2795
+ path: file.relativePath,
2796
+ imports: summary.imports,
2797
+ exports: summary.exports
2798
+ };
2799
+ });
2800
+ for (const entry of byFile) {
2801
+ for (const specifier of entry.imports) {
2802
+ const resolved = resolveSpecifier(entry.path, specifier, knownFiles);
2803
+ if (resolved) importerCounts.set(resolved, (importerCounts.get(resolved) ?? 0) + 1);
2804
+ }
2805
+ }
2806
+ const featureCounts = /* @__PURE__ */ new Map();
2807
+ for (const file of files) {
2808
+ const match = /(?:^|\/)features\/([^/]+)\//.exec(file.relativePath);
2809
+ if (match) featureCounts.set(match[1], (featureCounts.get(match[1]) ?? 0) + 1);
2810
+ }
2811
+ return {
2812
+ routes: files.filter((file) => file.kind === "route").map((file) => ({
2813
+ path: file.relativePath,
2814
+ methods: isSourceSummary(file.summary) ? file.summary.routeHandlers : [],
2815
+ part: file.part
2816
+ })),
2817
+ components: files.filter((file) => file.kind === "component").map((file) => ({
2818
+ path: file.relativePath,
2819
+ symbols: isSourceSummary(file.summary) ? file.summary.reactComponents : [],
2820
+ part: file.part,
2821
+ layer: file.layer
2822
+ })),
2823
+ handlers: files.filter((file) => file.kind === "handler").map((file) => ({
2824
+ path: file.relativePath,
2825
+ exports: isSourceSummary(file.summary) ? file.summary.exports : [],
2826
+ part: file.part
2827
+ })),
2828
+ features: Array.from(featureCounts.entries()).map(([name, count]) => ({ name, files: count })).sort((a, b) => b.files - a.files || (a.name < b.name ? -1 : 1)),
2829
+ imports: {
2830
+ byFile,
2831
+ centralFiles: Array.from(importerCounts.entries()).map(([filePath, importedByCount]) => ({ path: filePath, importedByCount })).sort((a, b) => b.importedByCount - a.importedByCount || (a.path < b.path ? -1 : 1)).slice(0, 50)
2832
+ },
2833
+ directives: files.filter((file) => isSourceSummary(file.summary) && file.summary.directives.length > 0).map((file) => ({
2834
+ path: file.relativePath,
2835
+ directives: file.summary.directives
2836
+ }))
2837
+ };
2838
+ }
2839
+ function verifyInMemory(files, parts, maxPartBytes) {
2840
+ const errors = [];
2841
+ const partNames = new Set(parts.map((part) => part.filename));
2842
+ for (const file of files) {
2843
+ if (!file.part || !partNames.has(file.part)) {
2844
+ errors.push(`${file.relativePath} was not assigned to a snapshot part.`);
2845
+ }
2846
+ }
2847
+ for (const part of parts) {
2848
+ if (sha256Hex2(part.markdown) !== part.metadata.sha256) {
2849
+ errors.push(`${part.filename} content does not match its recorded sha256.`);
2850
+ }
2851
+ const actualBytes = Buffer.byteLength(part.markdown, "utf8");
2852
+ if (actualBytes !== part.metadata.byteSize) {
2853
+ errors.push(`${part.filename} byteSize ${part.metadata.byteSize} != actual ${actualBytes}.`);
2854
+ }
2855
+ if (actualBytes > maxPartBytes) {
2856
+ errors.push(`${part.filename} (${actualBytes} B) exceeds the part budget of ${maxPartBytes} B.`);
2857
+ }
2858
+ if (part.metadata.includedFiles.length !== part.metadata.fileCount) {
2859
+ errors.push(`${part.filename} fileCount does not match its included files.`);
2860
+ }
2861
+ }
2862
+ const filesInParts = parts.reduce((total, part) => total + part.metadata.fileCount, 0);
2863
+ if (filesInParts !== files.length) {
2864
+ errors.push(`Parts contain ${filesInParts} file(s) but ${files.length} were scanned.`);
2865
+ }
2866
+ return errors;
2867
+ }
2868
+ function resolveSafeOutputDirs(config) {
2869
+ const cwd = path9.resolve(config.cwd);
2870
+ const outDir = path9.resolve(cwd, config.outDir);
2871
+ const tmpDir = path9.resolve(cwd, config.tmpDir);
2872
+ const root = path9.parse(cwd).root;
2873
+ const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path9.sep}`);
2874
+ const reject = (reason) => {
2875
+ throw new Error(
2876
+ `Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
2877
+ );
2878
+ };
2879
+ if (outDir === root) reject("outDir resolves to the filesystem root");
2880
+ if (outDir === cwd || contains(outDir, cwd)) {
2881
+ reject("outDir is the working directory or one of its ancestors");
2882
+ }
2883
+ if (tmpDir === root) reject("tmpDir resolves to the filesystem root");
2884
+ if (tmpDir === cwd || contains(tmpDir, cwd)) {
2885
+ reject("tmpDir is the working directory or one of its ancestors");
2886
+ }
2887
+ if (tmpDir === outDir) reject("outDir and tmpDir are the same directory");
2888
+ if (contains(outDir, tmpDir)) reject("tmpDir is inside outDir");
2889
+ if (contains(tmpDir, outDir)) reject("outDir is inside tmpDir");
2890
+ return { outDir, tmpDir };
2891
+ }
2892
+ function swapIntoPlace(tmpDir, outDir) {
2893
+ const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
2894
+ const hadPrevious = fs8.existsSync(outDir);
2895
+ if (hadPrevious) fs8.renameSync(outDir, backupDir);
2896
+ try {
2897
+ try {
2898
+ fs8.renameSync(tmpDir, outDir);
2899
+ } catch (err) {
2900
+ if (err.code === "EXDEV") {
2901
+ fs8.cpSync(tmpDir, outDir, { recursive: true });
2902
+ fs8.rmSync(tmpDir, { recursive: true, force: true });
2903
+ } else {
2904
+ throw err;
2905
+ }
2906
+ }
2907
+ if (hadPrevious) fs8.rmSync(backupDir, { recursive: true, force: true });
2908
+ } catch (err) {
2909
+ if (hadPrevious && fs8.existsSync(backupDir)) {
2910
+ if (fs8.existsSync(outDir)) fs8.rmSync(outDir, { recursive: true, force: true });
2911
+ fs8.renameSync(backupDir, outDir);
2912
+ }
2913
+ throw err;
2914
+ }
2915
+ }
2916
+ function artifactWarnings(artifacts) {
2917
+ const messages = [];
2918
+ if (!artifacts.manifest) {
2919
+ messages.push("manifest.json is disabled: `validate` and `report` cannot inspect this snapshot.");
2920
+ }
2921
+ if (!artifacts.parts && artifacts.navigation) {
2922
+ messages.push(
2923
+ "parts are disabled but navigation.md is enabled: its links point at part files that were not written."
2924
+ );
2925
+ }
2926
+ if (ARTIFACT_NAMES.every((name) => artifacts[name] === false)) {
2927
+ messages.push("every artifact is disabled: the snapshot directory will be empty.");
2928
+ }
2929
+ return messages;
2930
+ }
2931
+ function runAfterGenerate(config, manifest) {
2932
+ const hook = config.hooks.afterGenerate;
2933
+ if (!hook) return;
2934
+ try {
2935
+ hook(manifest);
2936
+ } catch (err) {
2937
+ manifest.warnings.push(`hooks.afterGenerate failed: ${err.message}`);
2938
+ manifest.totals.warningCount = manifest.warnings.length;
2939
+ }
2940
+ }
2941
+ function partitionByBudget(files, config, fingerprint) {
2942
+ const budgetFor = (file) => config.budgets.byLayer[file.layer] ?? config.budgets.default;
2943
+ const buckets = /* @__PURE__ */ new Map();
2944
+ for (const file of files) {
2945
+ const budget = budgetFor(file);
2946
+ const bucket = buckets.get(budget);
2947
+ if (bucket) bucket.push(file);
2948
+ else buckets.set(budget, [file]);
2949
+ }
2950
+ if (buckets.size <= 1) {
2951
+ const only = buckets.size === 1 ? Array.from(buckets.keys())[0] : config.maxPartBytes;
2952
+ return partitionFiles(files, only, fingerprint);
2953
+ }
2954
+ const parts = [];
2955
+ for (const budget of Array.from(buckets.keys()).sort((a, b) => a - b)) {
2956
+ parts.push(...partitionFiles(buckets.get(budget) ?? [], budget, fingerprint));
2957
+ }
2958
+ const merged = renumberParts(parts, fingerprint);
2959
+ const byPath = new Map(files.map((file) => [file.relativePath, file]));
2960
+ for (const part of merged) {
2961
+ for (const included of part.metadata.includedFiles) {
2962
+ const file = byPath.get(included.relativePath);
2963
+ if (file) file.part = part.filename;
2964
+ }
2965
+ }
2966
+ return merged;
2967
+ }
2968
+ function generateSnapshot(configInput, options) {
2969
+ assertUsableConfigInput(configInput);
2970
+ const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput ?? null);
2971
+ const dryRun = options?.dryRun ?? false;
2972
+ const gitInfo = getGitInfo(config.cwd, config.branch);
2973
+ const collection = collectFiles(config.cwd, {
2974
+ includePatterns: config.include,
2975
+ excludePatterns: config.excludePatterns,
2976
+ includeSensitiveFiles: config.includeSensitiveFiles
2977
+ });
2978
+ const files = [];
2979
+ const skipped = [...collection.skipped];
2980
+ const secretFindings = [];
2981
+ const warnings = [];
2982
+ const layerCounts = {};
2983
+ const kindCounts = {};
2984
+ let totalLines = 0;
2985
+ let totalSizeBytes = 0;
2986
+ let largeFileCount = 0;
2987
+ for (const relativePath of collection.files) {
2988
+ const absolutePath = path9.join(config.cwd, relativePath);
2989
+ let stats;
2990
+ try {
2991
+ stats = fs8.statSync(absolutePath);
2992
+ if (!stats.isFile()) {
2993
+ skipped.push({ path: relativePath, reason: "not-file" });
2994
+ continue;
2995
+ }
2996
+ } catch {
2997
+ skipped.push({ path: relativePath, reason: "read-error" });
2998
+ continue;
2999
+ }
3000
+ if (isBinaryFile(absolutePath)) {
3001
+ skipped.push({ path: relativePath, reason: "binary", detail: `Size: ${stats.size} B` });
3002
+ continue;
3003
+ }
3004
+ let buffer;
3005
+ try {
3006
+ buffer = fs8.readFileSync(absolutePath);
3007
+ } catch {
3008
+ skipped.push({ path: relativePath, reason: "read-error" });
3009
+ continue;
3010
+ }
3011
+ const hashSha256 = sha256Hex2(buffer);
3012
+ const rawContent = buffer.toString("utf8");
3013
+ const lineCount = countLines(rawContent);
3014
+ totalLines += lineCount;
3015
+ totalSizeBytes += stats.size;
3016
+ const layer = classifyLayer(relativePath, config.layers);
3017
+ const kind = classifyKind(relativePath, config.kinds);
3018
+ const language = detectLanguage(relativePath);
3019
+ bump(layerCounts, layer);
3020
+ bump(kindCounts, kind);
3021
+ const { redacted, findings } = scanAndRedactSecrets(
3022
+ rawContent,
3023
+ config.secretRules,
3024
+ relativePath
3025
+ );
3026
+ secretFindings.push(...findings);
3027
+ const isLarge = stats.size > config.largeFileBytes;
3028
+ if (isLarge) largeFileCount += 1;
3029
+ let finalContent = redacted;
3030
+ let isPreviewTruncated = false;
3031
+ let includedLines = lineCount;
3032
+ if (isLarge) {
3033
+ const preview = truncatePreview(
3034
+ redacted,
3035
+ config.previewHeadLines,
3036
+ config.previewTailLines
3037
+ );
3038
+ finalContent = preview.content;
3039
+ isPreviewTruncated = preview.isTruncated;
3040
+ includedLines = preview.includedLines;
3041
+ }
3042
+ let summary;
3043
+ try {
3044
+ summary = summarizeFile(relativePath, redacted);
3045
+ if (isSourceSummary(summary) && summary.parseErrors) {
3046
+ warnings.push(
3047
+ `Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
3048
+ );
3049
+ }
3050
+ } catch (err) {
3051
+ warnings.push(
3052
+ `Summary skipped for ${relativePath}: ${err.message}`.replace(/\s+/g, " ")
3053
+ );
3054
+ }
3055
+ files.push({
3056
+ relativePath,
3057
+ sizeBytes: stats.size,
3058
+ lines: lineCount,
3059
+ hashSha256,
3060
+ part: "",
3061
+ layer,
3062
+ kind,
3063
+ language,
3064
+ inclusionMode: isPreviewTruncated ? "preview" : "full",
3065
+ isLarge,
3066
+ isPreviewTruncated,
3067
+ originalLines: lineCount,
3068
+ includedLines,
3069
+ includedBytes: Buffer.byteLength(finalContent, "utf8"),
3070
+ summary,
3071
+ content: finalContent
3072
+ });
3073
+ }
3074
+ const fingerprint = sha256Hex2(
3075
+ files.map(
3076
+ (file) => `${file.relativePath}:${file.hashSha256}:${file.sizeBytes}:${file.inclusionMode}`
3077
+ ).join("\n")
3078
+ );
3079
+ const fullContents = /* @__PURE__ */ new Map();
3080
+ for (const file of files) fullContents.set(file.relativePath, file.content ?? "");
3081
+ const parts = partitionByBudget(files, config, fingerprint);
3082
+ const maxEffectiveBudget = Math.max(
3083
+ config.maxPartBytes,
3084
+ config.budgets.default,
3085
+ ...Object.values(config.budgets.byLayer)
3086
+ );
3087
+ const truncatedPreviewCount = files.filter((file) => file.isPreviewTruncated).length;
3088
+ const validationErrors = verifyInMemory(files, parts, maxEffectiveBudget);
3089
+ const totalContentBytes = files.reduce((total, file) => total + file.includedBytes, 0);
3090
+ const manifest = {
3091
+ schemaVersion: SCHEMA_VERSION,
3092
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3093
+ generator: {
3094
+ name: GENERATOR_NAME,
3095
+ version: GENERATOR_VERSION
3096
+ },
3097
+ git: gitInfo,
3098
+ budgets: {
3099
+ maxPartBytes: config.maxPartBytes,
3100
+ largeFileBytes: config.largeFileBytes
3101
+ },
3102
+ config: {
3103
+ maxPartBytes: config.maxPartBytes,
3104
+ largeFileBytes: config.largeFileBytes,
3105
+ previewHeadLines: config.previewHeadLines,
3106
+ previewTailLines: config.previewTailLines,
3107
+ includeMonolith: config.includeMonolith,
3108
+ includeSensitiveFiles: config.includeSensitiveFiles,
3109
+ artifacts: { ...config.artifacts },
3110
+ include: [...config.include],
3111
+ layersMode: config.layersMode,
3112
+ budgets: { default: config.budgets.default, byLayer: { ...config.budgets.byLayer } },
3113
+ excludePatterns: [...config.excludePatterns],
3114
+ layers: config.layers.map((layer) => layer.name),
3115
+ kinds: config.kinds.map((kind) => kind.kind)
3116
+ },
3117
+ counts: {
3118
+ totalFiles: files.length,
3119
+ totalSizeBytes,
3120
+ totalLines,
3121
+ partsCount: parts.length,
3122
+ skippedCount: skipped.length,
3123
+ largeFileCount,
3124
+ truncatedPreviewCount
3125
+ },
3126
+ totals: {
3127
+ candidateFiles: files.length + skipped.length,
3128
+ includedFiles: files.length,
3129
+ fullFiles: files.filter((file) => !file.isPreviewTruncated).length,
3130
+ previewFiles: files.filter((file) => file.isPreviewTruncated).length,
3131
+ skippedFiles: skipped.length,
3132
+ totalSourceBytes: totalSizeBytes,
3133
+ totalContentBytes,
3134
+ partCount: parts.length,
3135
+ warningCount: 0
3136
+ },
3137
+ fingerprint,
3138
+ validation: {
3139
+ status: validationErrors.length === 0 ? "passed" : "failed",
3140
+ errors: validationErrors
3141
+ },
3142
+ artifacts: {
3143
+ fullJson: null,
3144
+ fullMarkdown: null
3145
+ },
3146
+ layers: layerCounts,
3147
+ kinds: kindCounts,
3148
+ indexes: buildIndexes(files),
3149
+ parts: parts.map((part) => part.metadata),
3150
+ files: files.map((file) => {
3151
+ const { content: _content, ...rest } = file;
3152
+ return rest;
3153
+ }),
3154
+ skipped,
3155
+ warnings
3156
+ };
3157
+ manifest.warnings = [
3158
+ ...warnings,
3159
+ ...secretFindings.map((finding) => `${finding.rule} at ${finding.path ?? "unknown"}:${finding.line}`)
3160
+ ];
3161
+ manifest.totals.warningCount = manifest.warnings.length;
3162
+ let monolithJson = null;
3163
+ let monolithMarkdown = null;
3164
+ if (config.includeMonolith) {
3165
+ const fullFiles = files.map((file) => ({
3166
+ ...file,
3167
+ content: fullContents.get(file.relativePath) ?? ""
3168
+ }));
3169
+ const json = generateMonolithJson(manifest, fullFiles);
3170
+ const markdown = generateMonolithMarkdown(manifest, fullFiles);
3171
+ const jsonBytes = Buffer.byteLength(json, "utf8");
3172
+ const markdownBytes = Buffer.byteLength(markdown, "utf8");
3173
+ if (!config.maxMonolithBytes || jsonBytes <= config.maxMonolithBytes) {
3174
+ monolithJson = json;
3175
+ manifest.artifacts.fullJson = { byteSize: jsonBytes, sha256: sha256Hex2(json) };
3176
+ } else {
3177
+ manifest.warnings.push(
3178
+ `full-codebase.json skipped: ${jsonBytes} B exceeds maxMonolithBytes ${config.maxMonolithBytes} B`
3179
+ );
3180
+ }
3181
+ if (!config.maxMonolithBytes || markdownBytes <= config.maxMonolithBytes) {
3182
+ monolithMarkdown = markdown;
3183
+ manifest.artifacts.fullMarkdown = {
3184
+ byteSize: markdownBytes,
3185
+ sha256: sha256Hex2(markdown)
3186
+ };
3187
+ } else {
3188
+ manifest.warnings.push(
3189
+ `full-codebase.md skipped: ${markdownBytes} B exceeds maxMonolithBytes ${config.maxMonolithBytes} B`
3190
+ );
3191
+ }
3192
+ manifest.totals.warningCount = manifest.warnings.length;
3193
+ }
3194
+ for (const message of artifactWarnings(config.artifacts)) manifest.warnings.push(message);
3195
+ manifest.totals.warningCount = manifest.warnings.length;
3196
+ if (dryRun) {
3197
+ runAfterGenerate(config, manifest);
3198
+ return manifest;
3199
+ }
3200
+ const { outDir, tmpDir } = resolveSafeOutputDirs(config);
3201
+ const { artifacts } = config;
3202
+ fs8.rmSync(tmpDir, { recursive: true, force: true });
3203
+ fs8.mkdirSync(tmpDir, { recursive: true });
3204
+ const partsDir = path9.join(tmpDir, "parts");
3205
+ const reportsDir = path9.join(tmpDir, "reports");
3206
+ if (artifacts.parts) fs8.mkdirSync(partsDir, { recursive: true });
3207
+ if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
3208
+ fs8.mkdirSync(reportsDir, { recursive: true });
3209
+ }
3210
+ if (artifacts.parts) {
3211
+ for (const part of parts) {
3212
+ fs8.writeFileSync(path9.join(partsDir, part.filename), part.markdown, "utf8");
3213
+ }
3214
+ }
3215
+ if (artifacts.projectTree) {
3216
+ const projectTree = generateProjectTree(files.map((file) => file.relativePath));
3217
+ fs8.writeFileSync(path9.join(reportsDir, "project-tree.md"), projectTree, "utf8");
3218
+ }
3219
+ if (artifacts.navigation) {
3220
+ fs8.writeFileSync(path9.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
3221
+ }
3222
+ if (artifacts.audit) {
3223
+ fs8.writeFileSync(
3224
+ path9.join(reportsDir, "audit.md"),
3225
+ generateAuditReport(manifest, secretFindings),
3226
+ "utf8"
3227
+ );
3228
+ }
3229
+ if (artifacts.readme) {
3230
+ fs8.writeFileSync(path9.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
3231
+ }
3232
+ if (artifacts.manifest) {
3233
+ fs8.writeFileSync(path9.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
3234
+ }
3235
+ if (monolithJson !== null) {
3236
+ fs8.writeFileSync(path9.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
3237
+ }
3238
+ if (monolithMarkdown !== null) {
3239
+ fs8.writeFileSync(path9.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
3240
+ }
3241
+ swapIntoPlace(tmpDir, outDir);
3242
+ runAfterGenerate(config, manifest);
3243
+ return manifest;
3244
+ }
3245
+
3246
+ // src/cli.ts
3247
+ var VERSION = GENERATOR_VERSION;
3248
+ var COMMAND_ALIASES = /* @__PURE__ */ new Map([
3249
+ ["generate", "generate"],
3250
+ ["validate", "validate"],
3251
+ ["check", "validate"],
3252
+ ["report", "report"],
3253
+ ["init", "init"],
3254
+ ["explain", "explain"],
3255
+ ["diff", "diff"]
3256
+ ]);
3257
+ var COMMAND_ARITY = {
3258
+ generate: 0,
3259
+ validate: 0,
3260
+ report: 0,
3261
+ init: 0,
3262
+ explain: 1,
3263
+ diff: 2
3264
+ };
3265
+ var CLI_OPTIONS = {
3266
+ cwd: { type: "string" },
3267
+ "out-dir": { type: "string" },
3268
+ "max-part-bytes": { type: "string" },
3269
+ "large-file-bytes": { type: "string" },
3270
+ "max-monolith-bytes": { type: "string" },
3271
+ "include-monolith": { type: "boolean" },
3272
+ "include-sensitive-files": { type: "boolean" },
3273
+ "skip-artifacts": { type: "string" },
3274
+ "only-artifacts": { type: "string" },
3275
+ format: { type: "string" },
3276
+ mode: { type: "string" },
3277
+ "dry-run": { type: "boolean" },
3278
+ watch: { type: "boolean" },
3279
+ help: { type: "boolean", short: "h" },
3280
+ version: { type: "boolean", short: "v" }
3281
+ };
3282
+ var UsageError = class extends Error {
3283
+ };
3284
+ var USAGE = `
3285
+ ${GENERATOR_NAME} v${VERSION}
3286
+ Production-grade LLM codebase snapshot generator, auditor, and AST partitioner
3287
+
3288
+ Usage:
3289
+ codebase-snapshot [command] [options]
3290
+ snap [command] [options]
3291
+
3292
+ Commands:
3293
+ generate (default) Generate a complete partitioned codebase snapshot
3294
+ validate, check Validate snapshot integrity and check for drift
3295
+ init Generate a starter snapshot.config file
3296
+ report Print codebase health, layer metrics, and secret audit
3297
+ explain <path> Show how one path would be treated: layer, kind, part, skips
3298
+ diff <a> <b> Compare two snapshots (directories or manifest.json files)
3299
+
3300
+ Options:
3301
+ --cwd <path> Target repository directory (default: current directory)
3302
+ --out-dir <path> Snapshot output directory (default: snapshot)
3303
+ --include-monolith Also generate full-codebase.json and full-codebase.md
3304
+ --max-monolith-bytes <n> Skip monolith artefacts larger than n bytes
3305
+ --max-part-bytes <n> Maximum byte budget per markdown part (default: 307200).
3306
+ Accepts units: 120k, 1.5mb
3307
+ --large-file-bytes <n> Threshold for head/tail truncation (default: 204800)
3308
+ --include-sensitive-files Emit .env / *.pem / .npmrc / credential files
3309
+ --skip-artifacts <names> Comma-separated artifacts to omit
3310
+ (readme, manifest, parts, navigation,
3311
+ projectTree, audit, monolith)
3312
+ --only-artifacts <names> Write *only* these artifacts
3313
+ --format <text|json> CLI output format (default: text)
3314
+ --dry-run Compute the snapshot without writing anything
3315
+ --watch Re-generate whenever the repository changes
3316
+ -h, --help Display this help message
3317
+ -v, --version Display version
3318
+
3319
+ All options also accept the --option=value form.
3320
+ Human-readable progress is written to stderr; payloads (--format json) go to stdout.
3321
+ `.trim();
3322
+ function parseByteOption(raw, optionName, minimum) {
3323
+ if (raw === void 0) return void 0;
3324
+ const value = parseByteSize(raw);
3325
+ if (value === void 0 || !Number.isSafeInteger(value)) {
3326
+ throw new UsageError(
3327
+ `--${optionName} expects a byte count such as 120000 or "120k", received "${raw}".`
3328
+ );
3329
+ }
3330
+ if (value < minimum) {
3331
+ throw new UsageError(`--${optionName} must be at least ${minimum}, received "${raw}".`);
3332
+ }
3333
+ return value;
3334
+ }
3335
+ function parseArtifactList(raw, flag) {
3336
+ const names = raw.split(",").map((name) => name.trim()).filter(Boolean);
3337
+ if (names.length === 0) {
3338
+ throw new UsageError(
3339
+ `${flag} expects at least one artifact name (${ARTIFACT_NAMES.join(", ")}).`
3340
+ );
3341
+ }
3342
+ const unknown = names.filter((name) => !ARTIFACT_NAMES.includes(name));
3343
+ if (unknown.length > 0) {
3344
+ throw new UsageError(
3345
+ `Unknown artifact(s): ${unknown.join(", ")}. Expected one of: ${ARTIFACT_NAMES.join(", ")}.`
3346
+ );
3347
+ }
3348
+ return names;
3349
+ }
3350
+ function parseCliArgs(argv) {
3351
+ let parsed;
3352
+ try {
3353
+ parsed = parseArgs({
3354
+ args: argv,
3355
+ options: CLI_OPTIONS,
3356
+ allowPositionals: true,
3357
+ strict: true
3358
+ });
3359
+ } catch (err) {
3360
+ throw new UsageError(err.message);
3361
+ }
3362
+ const values = parsed.values;
3363
+ if (values.help) {
3364
+ process.stdout.write(`${USAGE}
3365
+ `);
3366
+ throw new HelpRequested();
3367
+ }
3368
+ if (values.version) {
3369
+ process.stdout.write(`v${VERSION}
3370
+ `);
3371
+ throw new HelpRequested();
3372
+ }
3373
+ const positionals = parsed.positionals;
3374
+ let command = "generate";
3375
+ let args = [];
3376
+ const explicitMode = values.mode ? String(values.mode) : void 0;
3377
+ const firstPositional = positionals[0];
3378
+ if (firstPositional !== void 0) {
3379
+ const resolved = COMMAND_ALIASES.get(firstPositional);
3380
+ if (!resolved) {
3381
+ throw new UsageError(
3382
+ `Unknown command "${firstPositional}". Expected one of: ${Array.from(COMMAND_ALIASES.keys()).join(", ")}.`
3383
+ );
3384
+ }
3385
+ command = resolved;
3386
+ args = positionals.slice(1);
3387
+ } else if (explicitMode !== void 0) {
3388
+ const resolved = COMMAND_ALIASES.get(explicitMode);
3389
+ if (!resolved) {
3390
+ throw new UsageError(
3391
+ `Unknown --mode "${explicitMode}". Expected one of: ${Array.from(COMMAND_ALIASES.keys()).join(", ")}.`
3392
+ );
3393
+ }
3394
+ command = resolved;
3395
+ }
3396
+ const expectedArgs = COMMAND_ARITY[command];
3397
+ if (args.length !== expectedArgs) {
3398
+ if (command === "explain") {
3399
+ throw new UsageError("explain expects exactly one path.");
3400
+ }
3401
+ if (command === "diff") {
3402
+ throw new UsageError(
3403
+ "diff expects exactly two arguments: two snapshot directories or manifest.json files."
3404
+ );
3405
+ }
3406
+ throw new UsageError(
3407
+ `Unexpected argument(s): ${args.join(" ")}. ${command} takes no positional arguments.`
3408
+ );
3409
+ }
3410
+ const rawFormat = values.format === void 0 ? "text" : String(values.format);
3411
+ if (rawFormat !== "text" && rawFormat !== "json") {
3412
+ throw new UsageError(`--format must be "text" or "json", received "${rawFormat}".`);
3413
+ }
3414
+ const rawSkip = values["skip-artifacts"];
3415
+ const rawOnly = values["only-artifacts"];
3416
+ if (rawSkip !== void 0 && rawOnly !== void 0) {
3417
+ throw new UsageError("--skip-artifacts and --only-artifacts cannot be combined.");
3418
+ }
3419
+ let skipArtifacts;
3420
+ if (rawOnly !== void 0) {
3421
+ const keep = parseArtifactList(String(rawOnly), "--only-artifacts");
3422
+ skipArtifacts = ARTIFACT_NAMES.filter(
3423
+ (name) => !keep.includes(name)
3424
+ );
3425
+ } else if (rawSkip !== void 0) {
3426
+ skipArtifacts = parseArtifactList(String(rawSkip), "--skip-artifacts");
3427
+ }
3428
+ return {
3429
+ command,
3430
+ args,
3431
+ format: rawFormat,
3432
+ dryRun: values["dry-run"] === true,
3433
+ watch: values.watch === true,
3434
+ cwd: values.cwd === void 0 ? process.cwd() : path10.resolve(String(values.cwd)),
3435
+ outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
3436
+ includeMonolith: values["include-monolith"] === true ? true : void 0,
3437
+ includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
3438
+ maxPartBytes: parseByteOption(
3439
+ values["max-part-bytes"],
3440
+ "max-part-bytes",
3441
+ MIN_MAX_PART_BYTES
3442
+ ),
3443
+ largeFileBytes: parseByteOption(
3444
+ values["large-file-bytes"],
3445
+ "large-file-bytes",
3446
+ 1
3447
+ ),
3448
+ maxMonolithBytes: parseByteOption(
3449
+ values["max-monolith-bytes"],
3450
+ "max-monolith-bytes",
3451
+ 1
3452
+ ),
3453
+ skipArtifacts
3454
+ };
3455
+ }
3456
+ var HelpRequested = class extends Error {
3457
+ };
3458
+ function info(message) {
3459
+ process.stderr.write(`${message}
3460
+ `);
3461
+ }
3462
+ function warn(message) {
3463
+ process.stderr.write(`${message}
3464
+ `);
3465
+ }
3466
+ function emit(format, payload, textRenderer) {
3467
+ if (format === "json") {
3468
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}
3469
+ `);
3470
+ return;
3471
+ }
3472
+ textRenderer();
3473
+ }
3474
+ function runInit(cwd) {
3475
+ const targetName = supportsTypeScriptImports() ? "snapshot.config.ts" : "snapshot.config.mjs";
3476
+ const targetPath = path10.join(cwd, targetName);
3477
+ if (fs9.existsSync(targetPath)) {
3478
+ warn(`\u274C ${targetName} already exists in ${cwd}.`);
3479
+ return 1;
3480
+ }
3481
+ const importPath = JSON.stringify(GENERATOR_NAME);
3482
+ const starter = `import { defineConfig } from ${importPath};
3483
+
3484
+ export default defineConfig({
3485
+ branch: "main",
3486
+ outDir: "snapshot",
3487
+ maxPartBytes: 300 * 1024,
3488
+ largeFileBytes: 200 * 1024,
3489
+ excludePatterns: [],
3490
+ });
3491
+ `;
3492
+ fs9.writeFileSync(targetPath, starter, "utf8");
3493
+ info(`\u2705 Created ${targetName} in ${cwd}`);
3494
+ if (targetName.endsWith(".mjs")) {
3495
+ info(
3496
+ " (a .ts config needs Node >= 22.6 to import natively; this Node writes a .mjs instead)"
3497
+ );
3498
+ }
3499
+ return 0;
3500
+ }
3501
+ function renderValidateText(result) {
3502
+ if (result.valid) {
3503
+ info("\u2705 Snapshot is valid and up to date with disk.");
3504
+ } else {
3505
+ warn("\u274C Snapshot validation failed:");
3506
+ for (const error of result.errors) warn(` - ${error}`);
3507
+ for (const drift of result.drift.hashMismatches.slice(0, 20)) {
3508
+ warn(` - modified: ${drift.path}`);
3509
+ }
3510
+ for (const issue of result.drift.partIssues.slice(0, 20)) {
3511
+ warn(` - part: ${issue.part} (${issue.issue})`);
3512
+ }
3513
+ for (const mismatch of result.drift.configMismatches.slice(0, 20)) {
3514
+ warn(` - config: ${mismatch}`);
3515
+ }
3516
+ for (const missing of result.drift.missingInSnapshot.slice(0, 20)) {
3517
+ warn(` - new file: ${missing}`);
3518
+ }
3519
+ for (const missing of result.drift.missingOnDisk.slice(0, 20)) {
3520
+ warn(` - deleted: ${missing}`);
3521
+ }
3522
+ }
3523
+ for (const warning of result.warnings) warn(` \u26A0 ${warning}`);
3524
+ }
3525
+ async function main(argv = process.argv.slice(2)) {
3526
+ let invocation;
3527
+ try {
3528
+ invocation = parseCliArgs(argv);
3529
+ } catch (err) {
3530
+ if (err instanceof HelpRequested) return 0;
3531
+ if (err instanceof UsageError) {
3532
+ process.stderr.write(`\u274C ${err.message}
3533
+
3534
+ ${USAGE}
3535
+ `);
3536
+ return 2;
3537
+ }
3538
+ throw err;
3539
+ }
3540
+ if (invocation.command === "init") {
3541
+ return runInit(invocation.cwd);
3542
+ }
3543
+ const loaded = await loadConfigFileDetailed(invocation.cwd);
3544
+ for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
3545
+ if (loaded.file) info(`\u2699 Using config ${path10.relative(process.cwd(), loaded.file) || loaded.file}`);
3546
+ const resolved = resolveConfig(
3547
+ loaded.config,
3548
+ {
3549
+ mode: invocation.command,
3550
+ format: invocation.format,
3551
+ dryRun: invocation.dryRun,
3552
+ cwd: invocation.cwd,
3553
+ outDir: invocation.outDir,
3554
+ includeMonolith: invocation.includeMonolith,
3555
+ includeSensitiveFiles: invocation.includeSensitiveFiles,
3556
+ maxPartBytes: invocation.maxPartBytes,
3557
+ largeFileBytes: invocation.largeFileBytes,
3558
+ maxMonolithBytes: invocation.maxMonolithBytes,
3559
+ skipArtifacts: invocation.skipArtifacts
3560
+ },
3561
+ invocation.cwd
3562
+ );
3563
+ if (invocation.command === "validate") {
3564
+ const result = validateSnapshot(resolved);
3565
+ emit(invocation.format, result, () => renderValidateText(result));
3566
+ return result.valid ? 0 : 1;
3567
+ }
3568
+ if (invocation.command === "report") {
3569
+ const manifestPath = path10.join(resolved.cwd, resolved.outDir, "manifest.json");
3570
+ if (!fs9.existsSync(manifestPath)) {
3571
+ warn(`\u274C Manifest not found at ${manifestPath}. Run generate first.`);
3572
+ return 1;
3573
+ }
3574
+ let manifest;
3575
+ try {
3576
+ manifest = JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
3577
+ } catch (err) {
3578
+ warn(`\u274C Could not read ${manifestPath}: ${err.message}`);
3579
+ return 1;
3580
+ }
3581
+ const validation = validateSnapshot(resolved);
3582
+ emit(invocation.format, { manifest, validation }, () => {
3583
+ info(`
3584
+ \u{1F4CA} Codebase Snapshot Report (${resolved.cwd})`);
3585
+ info(` - Generated at: ${manifest.generatedAt}`);
3586
+ info(` - Schema: ${manifest.schemaVersion}`);
3587
+ info(` - Fingerprint: ${manifest.fingerprint}`);
3588
+ info(` - Files included: ${manifest.counts.totalFiles}`);
3589
+ info(` - Total lines: ${manifest.counts.totalLines.toLocaleString()}`);
3590
+ info(
3591
+ ` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
3592
+ );
3593
+ info(` - Total parts: ${manifest.counts.partsCount}`);
3594
+ info(` - Integrity: ${validation.valid ? "\u2705 Clean (no drift)" : "\u274C Drift detected"}`);
3595
+ info(` - Warnings: ${manifest.warnings?.length ?? 0}`);
3596
+ info("\n Layers:");
3597
+ for (const [layer, count] of Object.entries(manifest.layers)) {
3598
+ info(` - ${layer.padEnd(16)}: ${count} files`);
3599
+ }
3600
+ });
3601
+ return validation.valid ? 0 : 1;
3602
+ }
3603
+ if (invocation.command === "explain") {
3604
+ const result = explainPath(resolved, invocation.args[0]);
3605
+ emit(invocation.format, result, () => info(renderExplainText(result)));
3606
+ return result.included ? 0 : 1;
3607
+ }
3608
+ if (invocation.command === "diff") {
3609
+ try {
3610
+ const [beforePath, afterPath] = invocation.args;
3611
+ const diff = compareManifests(loadManifest(beforePath), loadManifest(afterPath));
3612
+ emit(invocation.format, diff, () => info(renderDiffText(diff)));
3613
+ return 0;
3614
+ } catch (err) {
3615
+ warn(`\u274C ${err.message}`);
3616
+ return 1;
3617
+ }
3618
+ }
3619
+ const started = performance.now();
3620
+ info(`\u{1F4F8} ${invocation.dryRun ? "Planning snapshot (dry run)" : "Generating snapshot"} for: ${resolved.cwd}`);
3621
+ info(
3622
+ ` Branch: ${resolved.branch} | Part budget: ${(resolved.maxPartBytes / 1024).toFixed(0)} KB`
3623
+ );
3624
+ try {
3625
+ const manifest = generateSnapshot(resolved, { dryRun: invocation.dryRun });
3626
+ const elapsed = ((performance.now() - started) / 1e3).toFixed(2);
3627
+ if (invocation.dryRun) {
3628
+ info("\n\u{1F9EA} Dry run \u2014 nothing was written:");
3629
+ info(` - Would track: ${manifest.counts.totalFiles} files`);
3630
+ info(` - Would write: ${manifest.counts.partsCount} part(s) under ${resolved.outDir}/parts/`);
3631
+ info(` - Skipped: ${manifest.counts.skippedCount} file(s)`);
3632
+ info(` - Warnings: ${manifest.warnings.length}`);
3633
+ info(` - Integrity: ${manifest.validation.status}`);
3634
+ for (const error of manifest.validation.errors.slice(0, 10)) info(` - ${error}`);
3635
+ } else {
3636
+ info(`
3637
+ \u2705 Snapshot generated in ${elapsed}s:`);
3638
+ info(` - Tracked files: ${manifest.counts.totalFiles}`);
3639
+ info(` - Total lines: ${manifest.counts.totalLines.toLocaleString()}`);
3640
+ info(
3641
+ ` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
3642
+ );
3643
+ info(` - Parts written: ${manifest.counts.partsCount} under ${resolved.outDir}/parts/`);
3644
+ info(` - Reports: ${resolved.outDir}/reports/ (project-tree, navigation, audit)`);
3645
+ info(` - Manifest: ${resolved.outDir}/manifest.json`);
3646
+ info(` - Fingerprint: ${manifest.fingerprint}`);
3647
+ info(` - Integrity: ${manifest.validation.status}`);
3648
+ if (manifest.warnings.length > 0) {
3649
+ info(` - Warnings: ${manifest.warnings.length}`);
3650
+ for (const warning of manifest.warnings.slice(0, 10)) info(` - ${warning}`);
3651
+ }
3652
+ }
3653
+ if (invocation.format === "json") {
3654
+ process.stdout.write(`${JSON.stringify(manifest, null, 2)}
3655
+ `);
3656
+ }
3657
+ if (invocation.watch) {
3658
+ return await runWatchLoop(invocation, resolved);
3659
+ }
3660
+ return manifest.validation.status === "passed" ? 0 : 1;
3661
+ } catch (err) {
3662
+ warn(`\u274C Failed to generate snapshot: ${err.message}`);
3663
+ return 1;
3664
+ }
3665
+ }
3666
+ async function runWatchLoop(invocation, resolved) {
3667
+ const outDir = path10.resolve(resolved.cwd, resolved.outDir);
3668
+ const tmpDir = path10.resolve(resolved.cwd, resolved.tmpDir);
3669
+ const outName = path10.basename(outDir).toLowerCase();
3670
+ const tmpName = path10.basename(tmpDir).toLowerCase();
3671
+ const isGenerated = (candidate) => candidate.toLowerCase().split(/[\\/]/).some(
3672
+ (segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`)
3673
+ );
3674
+ let running = false;
3675
+ const regenerate = () => {
3676
+ if (running) return;
3677
+ running = true;
3678
+ const started = performance.now();
3679
+ try {
3680
+ const manifest = generateSnapshot(resolved, { dryRun: invocation.dryRun });
3681
+ const elapsed = ((performance.now() - started) / 1e3).toFixed(2);
3682
+ info(
3683
+ `\u267B\uFE0F ${(/* @__PURE__ */ new Date()).toISOString()} ${manifest.counts.totalFiles} file(s), ${manifest.counts.partsCount} part(s), ${manifest.validation.status} (${elapsed}s)`
3684
+ );
3685
+ } catch (err) {
3686
+ warn(`\u274C ${err.message}`);
3687
+ } finally {
3688
+ running = false;
3689
+ }
3690
+ };
3691
+ info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
3692
+ regenerate();
3693
+ let timer = null;
3694
+ const watcher = fs9.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
3695
+ if (filename) {
3696
+ const changed = path10.resolve(resolved.cwd, filename.toString());
3697
+ if (isGenerated(changed)) return;
3698
+ }
3699
+ if (timer) clearTimeout(timer);
3700
+ timer = setTimeout(regenerate, 250);
3701
+ });
3702
+ await new Promise((resolve5) => {
3703
+ const stop = () => {
3704
+ if (timer) clearTimeout(timer);
3705
+ watcher.close();
3706
+ resolve5();
3707
+ };
3708
+ process.once("SIGINT", stop);
3709
+ process.once("SIGTERM", stop);
3710
+ });
3711
+ return 0;
3712
+ }
3713
+
3714
+ // src/bin.ts
3715
+ main().then(
3716
+ (code) => {
3717
+ process.exitCode = code;
3718
+ },
3719
+ (err) => {
3720
+ process.stderr.write(`\u274C ${err.message}
3721
+ `);
3722
+ process.exitCode = 1;
3723
+ }
3724
+ );
3725
+ //# sourceMappingURL=bin.js.map