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