@ankurhalder/codebase-snapshot 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import * as fs9 from "fs";
5
- import * as path10 from "path";
4
+ import * as fs10 from "fs";
5
+ import * as path11 from "path";
6
6
  import { parseArgs } from "util";
7
7
 
8
8
  // src/diff.ts
@@ -100,9 +100,141 @@ import * as fs4 from "fs";
100
100
  import * as path4 from "path";
101
101
  import { pathToFileURL } from "url";
102
102
 
103
- // src/presets/index.ts
103
+ // src/presets/context.ts
104
104
  import * as fs2 from "fs";
105
105
  import * as path2 from "path";
106
+ var MANIFEST_FILES = [
107
+ "package.json",
108
+ "pyproject.toml",
109
+ "requirements.txt",
110
+ "setup.py",
111
+ "go.mod",
112
+ "Cargo.toml",
113
+ "pom.xml",
114
+ "build.gradle",
115
+ "build.gradle.kts",
116
+ "composer.json",
117
+ "Gemfile"
118
+ ];
119
+ var MANIFEST_MAX_BYTES = 1e5;
120
+ function readManifest(cwd, name) {
121
+ const full = path2.join(cwd, name);
122
+ try {
123
+ const stats = fs2.statSync(full);
124
+ if (!stats.isFile() || stats.size > MANIFEST_MAX_BYTES) return null;
125
+ return fs2.readFileSync(full, "utf8");
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ function collectDependencies(manifests) {
131
+ const names = /* @__PURE__ */ new Set();
132
+ const raw = manifests["package.json"];
133
+ if (!raw) return names;
134
+ try {
135
+ const parsed = JSON.parse(raw);
136
+ for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
137
+ const block = parsed[section];
138
+ if (block && typeof block === "object") {
139
+ for (const name of Object.keys(block)) names.add(name);
140
+ }
141
+ }
142
+ } catch {
143
+ }
144
+ return names;
145
+ }
146
+ function buildPresetContext(cwd) {
147
+ const files = /* @__PURE__ */ new Set();
148
+ const dirs = /* @__PURE__ */ new Set();
149
+ try {
150
+ for (const entry of fs2.readdirSync(cwd, { withFileTypes: true })) {
151
+ if (entry.isDirectory()) dirs.add(entry.name);
152
+ else if (entry.isFile()) files.add(entry.name);
153
+ }
154
+ } catch {
155
+ }
156
+ const manifests = {};
157
+ for (const name of MANIFEST_FILES) {
158
+ const content = readManifest(cwd, name);
159
+ if (content !== null) manifests[name] = content;
160
+ }
161
+ return {
162
+ cwd,
163
+ files,
164
+ dirs,
165
+ dependencies: collectDependencies(manifests),
166
+ manifests
167
+ };
168
+ }
169
+ function hasFile(context, name) {
170
+ return context.files.has(name);
171
+ }
172
+ function hasFileStartingWith(context, prefix) {
173
+ for (const file of context.files) {
174
+ if (file.startsWith(prefix)) return true;
175
+ }
176
+ return false;
177
+ }
178
+ function manifestContains(context, manifest, needle) {
179
+ const content = context.manifests[manifest];
180
+ if (!content) return false;
181
+ return content.toLowerCase().includes(needle.toLowerCase());
182
+ }
183
+ function hasDependency(context, ...names) {
184
+ return names.some((name) => context.dependencies.has(name));
185
+ }
186
+
187
+ // src/presets/helpers.ts
188
+ function layer(name, ...prefixes) {
189
+ return {
190
+ name,
191
+ match: (filePath) => prefixes.some((prefix) => filePath.startsWith(prefix))
192
+ };
193
+ }
194
+ function kindOf(name, ...needles) {
195
+ return {
196
+ kind: name,
197
+ match: (filePath) => needles.some((needle) => filePath.includes(needle))
198
+ };
199
+ }
200
+ function suffixKind(name, ...suffixes) {
201
+ return {
202
+ kind: name,
203
+ match: (filePath) => suffixes.some((suffix) => filePath.endsWith(suffix))
204
+ };
205
+ }
206
+ function commonLayers() {
207
+ return [
208
+ layer("scripts", "scripts/", "tools/"),
209
+ layer("docs", "docs/", "documentation/"),
210
+ {
211
+ name: "config",
212
+ match: (filePath) => filePath.startsWith(".github/") || filePath.startsWith(".husky/") || filePath.startsWith(".vscode/") || filePath.startsWith(".") || filePath.includes("config")
213
+ },
214
+ { name: "root", match: (filePath) => !filePath.includes("/") }
215
+ ];
216
+ }
217
+ function commonKinds() {
218
+ return [
219
+ {
220
+ kind: "test",
221
+ match: (filePath) => {
222
+ const lower = filePath.toLowerCase();
223
+ return lower.includes(".test.") || lower.includes(".spec.") || lower.startsWith("test/") || lower.startsWith("tests/") || lower.includes("_test.") || lower.endsWith("_test.go") || lower.includes("__tests__");
224
+ }
225
+ },
226
+ suffixKind("doc", ".md", ".mdx", ".rst", ".txt"),
227
+ suffixKind("data", ".json", ".yaml", ".yml", ".csv", ".toml"),
228
+ {
229
+ kind: "script",
230
+ match: (filePath) => filePath.startsWith("scripts/") || /\.(sh|ps1|bat)$/i.test(filePath)
231
+ },
232
+ {
233
+ kind: "config",
234
+ match: (filePath) => filePath.startsWith(".") || filePath.includes("config") || filePath === "package.json" || filePath === "tsconfig.json"
235
+ }
236
+ ];
237
+ }
106
238
 
107
239
  // src/presets/nextjs.ts
108
240
  var NEXTJS_LAYERS = [
@@ -159,6 +291,47 @@ var NEXTJS_KINDS = [
159
291
  }
160
292
  ];
161
293
 
294
+ // src/presets/node.ts
295
+ var NODE_LAYERS = [
296
+ { name: "core", match: (p) => p.startsWith("src/core/") || p.startsWith("core/") },
297
+ { name: "protocols", match: (p) => p.startsWith("src/protocols/") || p.startsWith("protocols/") },
298
+ { name: "providers", match: (p) => p.startsWith("src/providers/") || p.startsWith("providers/") },
299
+ { name: "security", match: (p) => p.startsWith("src/security/") || p.startsWith("security/") },
300
+ { name: "db", match: (p) => p.startsWith("src/db/") || p.startsWith("db/") || p.startsWith("prisma/") },
301
+ { name: "api", match: (p) => p.startsWith("src/api/") || p.startsWith("api/") || p.startsWith("routes/") },
302
+ { name: "services", match: (p) => p.startsWith("src/services/") || p.startsWith("services/") },
303
+ { name: "controllers", match: (p) => p.startsWith("src/controllers/") || p.startsWith("controllers/") },
304
+ { name: "scripts", match: (p) => p.startsWith("scripts/") },
305
+ { name: "docs", match: (p) => p.startsWith("docs/") },
306
+ {
307
+ name: "config",
308
+ match: (p) => p.startsWith(".github/") || p.startsWith(".husky/") || p.startsWith(".") || p.includes("config")
309
+ },
310
+ { name: "root", match: (p) => !p.includes("/") }
311
+ ];
312
+ var NODE_KINDS = [
313
+ { kind: "test", match: (p) => /\.(test|spec)\.[jt]sx?$/.test(p.toLowerCase()) },
314
+ { kind: "route", match: (p) => p.includes("/routes/") || p.includes("route.") || p.includes("/api/") },
315
+ { kind: "controller", match: (p) => p.includes("controller") },
316
+ { kind: "service", match: (p) => p.includes("service") },
317
+ { kind: "provider", match: (p) => p.includes("provider") },
318
+ { kind: "model", match: (p) => p.includes("model") || p.includes("entity") },
319
+ { kind: "schema", match: (p) => p.includes("schema") || p.includes("zod") || p.includes("dto") },
320
+ { kind: "type", match: (p) => p.endsWith(".d.ts") || p.includes("types.") || p.includes("type.") },
321
+ { kind: "security", match: (p) => p.includes("auth") || p.includes("security") || p.includes("guard") },
322
+ { kind: "db", match: (p) => p.includes("db") || p.includes("database") || p.includes("migration") },
323
+ { kind: "data", match: (p) => p.endsWith(".json") || p.endsWith(".csv") },
324
+ { kind: "doc", match: (p) => p.endsWith(".md") || p.endsWith(".mdx") },
325
+ { kind: "script", match: (p) => p.startsWith("scripts/") || p.endsWith(".sh") || p.endsWith(".ps1") },
326
+ {
327
+ kind: "config",
328
+ match: (p) => {
329
+ const base = p.split("/").pop() || "";
330
+ return ["package.json", "tsconfig.json", "Dockerfile"].includes(base) || base.startsWith(".");
331
+ }
332
+ }
333
+ ];
334
+
162
335
  // src/presets/vite.ts
163
336
  var VITE_LAYERS = [
164
337
  { name: "pages", match: (p) => p.startsWith("src/pages/") || p.startsWith("src/views/") },
@@ -206,44 +379,419 @@ var VITE_KINDS = [
206
379
  }
207
380
  ];
208
381
 
209
- // src/presets/node.ts
210
- var NODE_LAYERS = [
211
- { name: "core", match: (p) => p.startsWith("src/core/") || p.startsWith("core/") },
212
- { name: "protocols", match: (p) => p.startsWith("src/protocols/") || p.startsWith("protocols/") },
213
- { name: "providers", match: (p) => p.startsWith("src/providers/") || p.startsWith("providers/") },
214
- { name: "security", match: (p) => p.startsWith("src/security/") || p.startsWith("security/") },
215
- { name: "db", match: (p) => p.startsWith("src/db/") || p.startsWith("db/") || p.startsWith("prisma/") },
216
- { name: "api", match: (p) => p.startsWith("src/api/") || p.startsWith("api/") || p.startsWith("routes/") },
217
- { name: "services", match: (p) => p.startsWith("src/services/") || p.startsWith("services/") },
218
- { name: "controllers", match: (p) => p.startsWith("src/controllers/") || p.startsWith("controllers/") },
219
- { name: "scripts", match: (p) => p.startsWith("scripts/") },
220
- { name: "docs", match: (p) => p.startsWith("docs/") },
382
+ // src/presets/js-presets.ts
383
+ var NEXT_LIKE_EXCLUDES = [".svelte-kit", ".astro", ".vite", ".output"];
384
+ var JS_PRESETS = [
221
385
  {
222
- name: "config",
223
- match: (p) => p.startsWith(".github/") || p.startsWith(".husky/") || p.startsWith(".") || p.includes("config")
386
+ name: "nextjs",
387
+ priority: 100,
388
+ detect: (context) => hasFileStartingWith(context, "next.config") || hasDependency(context, "next"),
389
+ layers: NEXTJS_LAYERS,
390
+ kinds: NEXTJS_KINDS,
391
+ exclude: NEXT_LIKE_EXCLUDES,
392
+ recommendedBudget: "300k"
224
393
  },
225
- { name: "root", match: (p) => !p.includes("/") }
394
+ {
395
+ name: "nuxt",
396
+ priority: 98,
397
+ detect: (context) => hasFileStartingWith(context, "nuxt.config") || hasDependency(context, "nuxt", "nuxt3"),
398
+ layers: [
399
+ layer("pages", "pages/", "src/pages/"),
400
+ layer("components", "components/", "src/components/"),
401
+ layer("composables", "composables/", "src/composables/"),
402
+ layer("server", "server/", "src/server/", "api/"),
403
+ layer("stores", "stores/", "src/stores/"),
404
+ layer("layouts", "layouts/", "src/layouts/"),
405
+ ...commonLayers()
406
+ ],
407
+ kinds: [
408
+ kindOf("api-route", "server/api/", "server/routes/"),
409
+ suffixKind("component", ".vue"),
410
+ kindOf("composable", "composables/"),
411
+ kindOf("store", "stores/", "store/"),
412
+ kindOf("layout", "layouts/"),
413
+ ...commonKinds()
414
+ ],
415
+ exclude: [".nuxt", ".output"],
416
+ recommendedBudget: "200k"
417
+ },
418
+ {
419
+ name: "sveltekit",
420
+ priority: 96,
421
+ detect: (context) => hasFileStartingWith(context, "svelte.config") || hasDependency(context, "@sveltejs/kit", "svelte"),
422
+ layers: [
423
+ layer("routes", "src/routes/", "routes/"),
424
+ layer("lib", "src/lib/", "lib/"),
425
+ layer("components", "src/lib/components/", "src/components/"),
426
+ layer("stores", "src/lib/stores/", "src/stores/"),
427
+ ...commonLayers()
428
+ ],
429
+ kinds: [
430
+ kindOf("route", "+page.", "+server.", "+layout."),
431
+ suffixKind("component", ".svelte"),
432
+ kindOf("store", "stores/"),
433
+ kindOf("params", "params/"),
434
+ ...commonKinds()
435
+ ],
436
+ exclude: [".svelte-kit", ".vercel"],
437
+ recommendedBudget: "200k"
438
+ },
439
+ {
440
+ name: "remix",
441
+ priority: 94,
442
+ detect: (context) => hasDependency(context, "@remix-run/react", "@remix-run/node", "@remix-run/dev"),
443
+ layers: [
444
+ layer("routes", "app/routes/", "routes/"),
445
+ layer("components", "app/components/"),
446
+ layer("lib", "app/lib/", "app/utils/"),
447
+ ...commonLayers()
448
+ ],
449
+ kinds: [kindOf("route", "routes/"), ...commonKinds()],
450
+ exclude: [".cache", "public/build"],
451
+ recommendedBudget: "200k"
452
+ },
453
+ {
454
+ name: "astro",
455
+ priority: 92,
456
+ detect: (context) => hasFileStartingWith(context, "astro.config") || hasDependency(context, "astro"),
457
+ layers: [
458
+ layer("pages", "src/pages/"),
459
+ layer("layouts", "src/layouts/"),
460
+ layer("components", "src/components/"),
461
+ layer("content", "src/content/"),
462
+ ...commonLayers()
463
+ ],
464
+ kinds: [suffixKind("page", ".astro"), kindOf("layout", "layouts/"), ...commonKinds()],
465
+ exclude: [".astro", "dist"],
466
+ recommendedBudget: "150k"
467
+ },
468
+ {
469
+ name: "angular",
470
+ priority: 90,
471
+ detect: (context) => hasFile(context, "angular.json") || hasDependency(context, "@angular/core"),
472
+ layers: [
473
+ layer("app", "src/app/"),
474
+ layer("features", "src/app/features/"),
475
+ layer("shared", "src/app/shared/"),
476
+ layer("core", "src/app/core/"),
477
+ layer("assets", "src/assets/"),
478
+ ...commonLayers()
479
+ ],
480
+ kinds: [
481
+ suffixKind("component", ".component.ts"),
482
+ suffixKind("service", ".service.ts"),
483
+ suffixKind("module", ".module.ts"),
484
+ suffixKind("guard", ".guard.ts"),
485
+ suffixKind("pipe", ".pipe.ts"),
486
+ suffixKind("directive", ".directive.ts"),
487
+ ...commonKinds()
488
+ ],
489
+ exclude: [".angular", "dist"],
490
+ recommendedBudget: "200k"
491
+ },
492
+ {
493
+ name: "solid",
494
+ priority: 88,
495
+ detect: (context) => hasDependency(context, "solid-js", "solid-start"),
496
+ layers: [
497
+ layer("pages", "src/routes/", "src/pages/"),
498
+ layer("components", "src/components/"),
499
+ ...commonLayers()
500
+ ],
501
+ kinds: [kindOf("route", "routes/"), ...commonKinds()],
502
+ exclude: [".solid", "dist"],
503
+ recommendedBudget: "120k"
504
+ },
505
+ {
506
+ name: "vue-vite",
507
+ priority: 86,
508
+ detect: (context) => hasDependency(context, "vue") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
509
+ layers: [
510
+ layer("pages", "src/pages/", "src/views/"),
511
+ layer("components", "src/components/"),
512
+ layer("composables", "src/composables/", "src/hooks/"),
513
+ layer("stores", "src/stores/", "src/store/"),
514
+ layer("services", "src/services/", "src/api/"),
515
+ layer("shared", "src/shared/", "src/common/"),
516
+ layer("styles", "src/styles/", "src/assets/"),
517
+ ...commonLayers()
518
+ ],
519
+ kinds: [
520
+ suffixKind("component", ".vue"),
521
+ kindOf("composable", "composables/", "use"),
522
+ kindOf("store", "stores/", "store/"),
523
+ kindOf("service", "services/", "api/"),
524
+ suffixKind("style", ".css", ".scss", ".sass", ".less"),
525
+ ...commonKinds()
526
+ ],
527
+ exclude: [".vite", "dist"],
528
+ recommendedBudget: "120k"
529
+ },
530
+ {
531
+ name: "react-vite",
532
+ priority: 84,
533
+ detect: (context) => hasDependency(context, "react") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
534
+ layers: VITE_LAYERS,
535
+ kinds: VITE_KINDS,
536
+ exclude: [".vite", "dist"],
537
+ recommendedBudget: "120k"
538
+ },
539
+ {
540
+ name: "nest",
541
+ priority: 80,
542
+ detect: (context) => hasDependency(context, "@nestjs/core"),
543
+ layers: [
544
+ layer("modules", "src/modules/", "modules/"),
545
+ layer("controllers", "src/controllers/", "controllers/"),
546
+ layer("providers", "src/providers/", "providers/"),
547
+ layer("core", "src/core/"),
548
+ layer("security", "src/security/", "src/auth/"),
549
+ layer("db", "src/db/", "src/database/", "prisma/"),
550
+ ...commonLayers()
551
+ ],
552
+ kinds: [
553
+ suffixKind("controller", ".controller.ts"),
554
+ suffixKind("service", ".service.ts"),
555
+ suffixKind("module", ".module.ts"),
556
+ suffixKind("guard", ".guard.ts"),
557
+ suffixKind("dto", ".dto.ts"),
558
+ suffixKind("entity", ".entity.ts"),
559
+ kindOf("migration", "migrations/"),
560
+ ...commonKinds()
561
+ ],
562
+ exclude: ["dist", "node_modules"],
563
+ recommendedBudget: "200k"
564
+ },
565
+ {
566
+ name: "node-express",
567
+ priority: 78,
568
+ detect: (context) => hasDependency(context, "express", "fastify", "koa", "@hapi/hapi"),
569
+ layers: NODE_LAYERS,
570
+ kinds: NODE_KINDS,
571
+ exclude: ["dist"],
572
+ recommendedBudget: "200k"
573
+ },
574
+ {
575
+ name: "node-lib",
576
+ // A bare package.json is often just tooling around a non-JS project, so it
577
+ // ranks below every definitive language manifest.
578
+ priority: 45,
579
+ detect: (context) => hasFile(context, "package.json"),
580
+ layers: [
581
+ layer("src", "src/"),
582
+ layer("lib", "lib/"),
583
+ layer("test", "test/", "tests/", "__tests__/"),
584
+ ...commonLayers()
585
+ ],
586
+ kinds: [suffixKind("type", ".d.ts"), ...commonKinds()],
587
+ exclude: ["dist"],
588
+ recommendedBudget: "120k"
589
+ }
226
590
  ];
227
- var NODE_KINDS = [
228
- { kind: "test", match: (p) => /\.(test|spec)\.[jt]sx?$/.test(p.toLowerCase()) },
229
- { kind: "route", match: (p) => p.includes("/routes/") || p.includes("route.") || p.includes("/api/") },
230
- { kind: "controller", match: (p) => p.includes("controller") },
231
- { kind: "service", match: (p) => p.includes("service") },
232
- { kind: "provider", match: (p) => p.includes("provider") },
233
- { kind: "model", match: (p) => p.includes("model") || p.includes("entity") },
234
- { kind: "schema", match: (p) => p.includes("schema") || p.includes("zod") || p.includes("dto") },
235
- { kind: "type", match: (p) => p.endsWith(".d.ts") || p.includes("types.") || p.includes("type.") },
236
- { kind: "security", match: (p) => p.includes("auth") || p.includes("security") || p.includes("guard") },
237
- { kind: "db", match: (p) => p.includes("db") || p.includes("database") || p.includes("migration") },
238
- { kind: "data", match: (p) => p.endsWith(".json") || p.endsWith(".csv") },
239
- { kind: "doc", match: (p) => p.endsWith(".md") || p.endsWith(".mdx") },
240
- { kind: "script", match: (p) => p.startsWith("scripts/") || p.endsWith(".sh") || p.endsWith(".ps1") },
591
+
592
+ // src/presets/lang-presets.ts
593
+ var anyFileEndingWith = (context, suffix) => Array.from(context.files).some((file) => file.endsWith(suffix));
594
+ var DIR = (name) => `${name}/`;
595
+ var LANGUAGE_PRESETS = [
241
596
  {
242
- kind: "config",
243
- match: (p) => {
244
- const base = p.split("/").pop() || "";
245
- return ["package.json", "tsconfig.json", "Dockerfile"].includes(base) || base.startsWith(".");
246
- }
597
+ name: "python-django",
598
+ priority: 56,
599
+ detect: (context) => hasFile(context, "manage.py") || manifestContains(context, "pyproject.toml", "django") || manifestContains(context, "requirements.txt", "django"),
600
+ layers: [
601
+ layer("views", DIR("views"), DIR("app/views"), DIR("apps/views")),
602
+ layer("models", DIR("models"), DIR("app/models"), DIR("apps/models")),
603
+ layer("serializers", DIR("serializers")),
604
+ layer("urls", "urls.py"),
605
+ layer("migrations", DIR("migrations")),
606
+ layer("settings", "settings.py", "settings/"),
607
+ layer("templates", DIR("templates")),
608
+ ...commonLayers()
609
+ ],
610
+ kinds: [
611
+ suffixKind("model", "models.py"),
612
+ suffixKind("view", "views.py"),
613
+ suffixKind("serializer", "serializers.py"),
614
+ suffixKind("urlconf", "urls.py"),
615
+ kindOf("migration", "migrations/"),
616
+ suffixKind("settings", "settings.py"),
617
+ ...commonKinds()
618
+ ],
619
+ exclude: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".tox", ".eggs", "staticfiles", "media"],
620
+ recommendedBudget: "150k"
621
+ },
622
+ {
623
+ name: "python-fastapi",
624
+ priority: 54,
625
+ detect: (context) => manifestContains(context, "requirements.txt", "fastapi") || manifestContains(context, "requirements.txt", "flask") || manifestContains(context, "pyproject.toml", "fastapi") || manifestContains(context, "pyproject.toml", "flask"),
626
+ layers: [
627
+ layer("routers", DIR("routers"), DIR("routes"), DIR("api")),
628
+ layer("models", DIR("models"), DIR("db")),
629
+ layer("schemas", DIR("schemas")),
630
+ layer("services", DIR("services")),
631
+ layer("core", DIR("core")),
632
+ layer("tests", DIR("tests")),
633
+ ...commonLayers()
634
+ ],
635
+ kinds: [
636
+ kindOf("router", "routers/", "routes/", "api/"),
637
+ kindOf("model", "models/"),
638
+ kindOf("schema", "schemas/"),
639
+ kindOf("service", "services/"),
640
+ ...commonKinds()
641
+ ],
642
+ exclude: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".tox", ".eggs"],
643
+ recommendedBudget: "150k"
644
+ },
645
+ {
646
+ name: "python-lib",
647
+ // Below node-lib: a Node repo with a stray .py file is still a Node repo.
648
+ priority: 30,
649
+ detect: (context) => hasFile(context, "pyproject.toml") || hasFile(context, "setup.py") || hasFile(context, "requirements.txt") || anyFileEndingWith(context, ".py"),
650
+ layers: [
651
+ layer("package", DIR("src"), DIR("lib")),
652
+ layer("tests", DIR("tests"), DIR("test")),
653
+ ...commonLayers()
654
+ ],
655
+ kinds: [suffixKind("module", ".py"), ...commonKinds()],
656
+ exclude: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".tox", ".eggs"],
657
+ recommendedBudget: "120k"
658
+ },
659
+ {
660
+ name: "go",
661
+ priority: 50,
662
+ detect: (context) => hasFile(context, "go.mod"),
663
+ layers: [
664
+ layer("cmd", "cmd/"),
665
+ layer("internal", "internal/"),
666
+ layer("pkg", "pkg/"),
667
+ layer("api", "api/", "proto/"),
668
+ layer("migrations", "migrations/"),
669
+ ...commonLayers()
670
+ ],
671
+ kinds: [
672
+ kindOf("main", "cmd/"),
673
+ kindOf("handler", "handler", "server", "http"),
674
+ kindOf("service", "service"),
675
+ kindOf("repository", "repository", "store", "repo"),
676
+ kindOf("model", "model", "entity"),
677
+ suffixKind("test", "_test.go"),
678
+ ...commonKinds()
679
+ ],
680
+ exclude: ["vendor", "testdata"],
681
+ recommendedBudget: "200k"
682
+ },
683
+ {
684
+ name: "rust",
685
+ priority: 48,
686
+ detect: (context) => hasFile(context, "Cargo.toml"),
687
+ layers: [
688
+ layer("src", "src/"),
689
+ layer("crates", "crates/"),
690
+ layer("bins", "src/bin/"),
691
+ layer("tests", "tests/"),
692
+ layer("benches", "benches/"),
693
+ ...commonLayers()
694
+ ],
695
+ kinds: [suffixKind("module", ".rs"), suffixKind("test", "_test.rs"), ...commonKinds()],
696
+ exclude: ["target"],
697
+ recommendedBudget: "200k"
698
+ },
699
+ {
700
+ name: "java-spring",
701
+ priority: 46,
702
+ detect: (context) => hasFile(context, "pom.xml") || hasFile(context, "build.gradle") || hasFile(context, "build.gradle.kts"),
703
+ layers: [
704
+ layer("api", "src/main/java/"),
705
+ layer("test", "src/test/java/"),
706
+ layer("resources", "src/main/resources/"),
707
+ layer("migrations", "src/main/resources/db/"),
708
+ ...commonLayers()
709
+ ],
710
+ kinds: [
711
+ kindOf("controller", "controller"),
712
+ kindOf("service", "service"),
713
+ kindOf("repository", "repository", "dao"),
714
+ kindOf("entity", "entity", "model"),
715
+ kindOf("dto", "dto", "request", "response"),
716
+ kindOf("migration", "migration", "db/migration"),
717
+ ...commonKinds()
718
+ ],
719
+ exclude: [".gradle", "out", "target"],
720
+ recommendedBudget: "200k"
721
+ },
722
+ {
723
+ name: "dotnet",
724
+ priority: 44,
725
+ detect: (context) => anyFileEndingWith(context, ".csproj") || anyFileEndingWith(context, ".sln") || hasFile(context, "global.json"),
726
+ layers: [
727
+ layer("web", "src/", "Web/"),
728
+ layer("domain", "Domain/", "Core/"),
729
+ layer("infrastructure", "Infrastructure/", "Data/"),
730
+ layer("tests", "Tests/", "test/"),
731
+ ...commonLayers()
732
+ ],
733
+ kinds: [
734
+ kindOf("controller", "Controller"),
735
+ kindOf("service", "Service"),
736
+ kindOf("repository", "Repository"),
737
+ kindOf("model", "Model", "Entity"),
738
+ kindOf("dto", "Dto", "Request", "Response"),
739
+ suffixKind("test", "Tests.cs"),
740
+ ...commonKinds()
741
+ ],
742
+ exclude: ["bin", "obj", ".vs"],
743
+ recommendedBudget: "200k"
744
+ },
745
+ {
746
+ name: "php-laravel",
747
+ priority: 42,
748
+ detect: (context) => hasFile(context, "artisan") || manifestContains(context, "composer.json", "laravel") || hasFile(context, "composer.json"),
749
+ layers: [
750
+ layer("app", "app/"),
751
+ layer("routes", "routes/"),
752
+ layer("http", "app/Http/"),
753
+ layer("models", "app/Models/"),
754
+ layer("providers", "app/Providers/"),
755
+ layer("database", "database/"),
756
+ layer("tests", "tests/"),
757
+ ...commonLayers()
758
+ ],
759
+ kinds: [
760
+ kindOf("controller", "Http/Controllers/"),
761
+ kindOf("model", "Models/"),
762
+ kindOf("migration", "migrations/"),
763
+ kindOf("middleware", "Middleware/"),
764
+ kindOf("request", "Requests/"),
765
+ kindOf("provider", "Providers/"),
766
+ ...commonKinds()
767
+ ],
768
+ exclude: ["vendor", "storage", "bootstrap/cache", "public/build"],
769
+ recommendedBudget: "150k"
770
+ },
771
+ {
772
+ name: "ruby-rails",
773
+ priority: 40,
774
+ detect: (context) => hasFile(context, "Gemfile") || hasFile(context, "config.ru") || hasFile(context, "Rakefile"),
775
+ layers: [
776
+ layer("app", "app/"),
777
+ layer("controllers", "app/controllers/"),
778
+ layer("models", "app/models/"),
779
+ layer("config", "config/"),
780
+ layer("db", "db/"),
781
+ layer("spec", "spec/", "test/"),
782
+ ...commonLayers()
783
+ ],
784
+ kinds: [
785
+ kindOf("controller", "controllers/"),
786
+ kindOf("model", "models/"),
787
+ kindOf("migration", "migrate/"),
788
+ kindOf("job", "jobs/"),
789
+ kindOf("mailer", "mailers/"),
790
+ suffixKind("test", "_spec.rb", "_test.rb"),
791
+ ...commonKinds()
792
+ ],
793
+ exclude: [".bundle", "log", "tmp", "vendor/bundle", "public/assets"],
794
+ recommendedBudget: "150k"
247
795
  }
248
796
  ];
249
797
 
@@ -289,50 +837,110 @@ var DEFAULT_KINDS = [
289
837
  }
290
838
  ];
291
839
 
292
- // src/presets/index.ts
293
- function detectPreset(cwd) {
294
- const hasNextConfig = fs2.existsSync(path2.join(cwd, "next.config.ts")) || fs2.existsSync(path2.join(cwd, "next.config.js")) || fs2.existsSync(path2.join(cwd, "next.config.mjs"));
295
- const hasViteConfig = fs2.existsSync(path2.join(cwd, "vite.config.ts")) || fs2.existsSync(path2.join(cwd, "vite.config.js")) || fs2.existsSync(path2.join(cwd, "vite.config.mjs"));
296
- const pkgJsonPath = path2.join(cwd, "package.json");
297
- let pkgDependencies = {};
298
- if (fs2.existsSync(pkgJsonPath)) {
299
- try {
300
- const parsed = JSON.parse(fs2.readFileSync(pkgJsonPath, "utf8"));
301
- pkgDependencies = {
302
- ...parsed.dependencies || {},
303
- ...parsed.devDependencies || {}
304
- };
305
- } catch {
306
- pkgDependencies = {};
307
- }
308
- }
309
- if (hasNextConfig || pkgDependencies["next"]) {
310
- return {
311
- presetName: "nextjs",
312
- layers: NEXTJS_LAYERS,
313
- kinds: NEXTJS_KINDS
314
- };
840
+ // src/presets/shape-presets.ts
841
+ var SHAPE_PRESETS = [
842
+ {
843
+ name: "monorepo",
844
+ priority: 70,
845
+ detect: (context) => hasFile(context, "pnpm-workspace.yaml") || hasFile(context, "nx.json") || hasFile(context, "turbo.json") || hasFile(context, "lerna.json") || manifestContains(context, "package.json", '"workspaces"'),
846
+ layers: [
847
+ layer("apps", "apps/"),
848
+ layer("packages", "packages/"),
849
+ layer("tooling", "tooling/", "tools/", "scripts/"),
850
+ ...commonLayers()
851
+ ],
852
+ kinds: commonKinds(),
853
+ exclude: [".turbo", ".nx"],
854
+ recommendedBudget: "200k"
855
+ },
856
+ {
857
+ name: "infra",
858
+ priority: 24,
859
+ detect: (context) => Array.from(context.files).some((file) => file.endsWith(".tf")) || hasFile(context, "Chart.yaml") || hasFile(context, "kustomization.yaml") || hasFile(context, "docker-compose.yml") || hasFile(context, "docker-compose.yaml"),
860
+ layers: [
861
+ layer("modules", "modules/"),
862
+ layer("environments", "env/", "environments/"),
863
+ layer("manifests", "k8s/", "helm/", "charts/", "deploy/"),
864
+ ...commonLayers()
865
+ ],
866
+ kinds: [
867
+ { kind: "terraform", match: (filePath) => filePath.endsWith(".tf") },
868
+ { kind: "kubernetes", match: (filePath) => /\.(ya?ml)$/i.test(filePath) && /k8s|helm|charts|deploy/.test(filePath) },
869
+ ...commonKinds()
870
+ ],
871
+ exclude: [".terraform", ".terragrunt-cache"],
872
+ recommendedBudget: "100k"
873
+ },
874
+ {
875
+ name: "docs",
876
+ priority: 22,
877
+ detect: (context) => {
878
+ const markdown = Array.from(context.files).filter((file) => /\.mdx?$/i.test(file)).length;
879
+ return markdown >= 3 && !hasFile(context, "package.json");
880
+ },
881
+ layers: [
882
+ layer("content", "content/", "docs/"),
883
+ layer("assets", "assets/", "static/", "public/"),
884
+ ...commonLayers()
885
+ ],
886
+ kinds: commonKinds(),
887
+ exclude: [".docusaurus", ".vitepress/dist", "site"],
888
+ recommendedBudget: "100k"
889
+ },
890
+ {
891
+ name: "generic",
892
+ priority: 0,
893
+ detect: () => true,
894
+ layers: DEFAULT_LAYERS,
895
+ kinds: DEFAULT_KINDS,
896
+ exclude: [],
897
+ recommendedBudget: "120k"
315
898
  }
316
- if (hasViteConfig || pkgDependencies["vite"]) {
317
- return {
318
- presetName: "vite",
319
- layers: VITE_LAYERS,
320
- kinds: VITE_KINDS
321
- };
899
+ ];
900
+
901
+ // src/presets/registry.ts
902
+ function compare(a, b) {
903
+ if (b.priority !== a.priority) return b.priority - a.priority;
904
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
905
+ }
906
+ var PRESETS = [
907
+ ...JS_PRESETS,
908
+ ...LANGUAGE_PRESETS,
909
+ ...SHAPE_PRESETS
910
+ ].sort(compare);
911
+ var BY_NAME = new Map(PRESETS.map((preset) => [preset.name, preset]));
912
+ function listPresetNames() {
913
+ return PRESETS.map((preset) => preset.name);
914
+ }
915
+ function getPreset(name) {
916
+ return BY_NAME.get(name);
917
+ }
918
+ function unknownPresetMessage(name) {
919
+ return `Unknown preset "${name}". Expected one of: ${listPresetNames().join(", ")}.`;
920
+ }
921
+ function detectPresetFromContext(context, forced) {
922
+ if (forced !== void 0) {
923
+ const preset = BY_NAME.get(forced);
924
+ if (!preset) throw new Error(unknownPresetMessage(forced));
925
+ return toDetected(preset);
322
926
  }
323
- if (pkgDependencies["express"] || pkgDependencies["fastify"] || pkgDependencies["@nestjs/core"] || fs2.existsSync(path2.join(cwd, "src/core")) || fs2.existsSync(path2.join(cwd, "src/protocols"))) {
324
- return {
325
- presetName: "node",
326
- layers: NODE_LAYERS,
327
- kinds: NODE_KINDS
328
- };
927
+ for (const preset of PRESETS) {
928
+ if (preset.detect(context)) return toDetected(preset);
329
929
  }
930
+ return toDetected(PRESETS[PRESETS.length - 1]);
931
+ }
932
+ function toDetected(preset) {
330
933
  return {
331
- presetName: "default",
332
- layers: DEFAULT_LAYERS,
333
- kinds: DEFAULT_KINDS
934
+ presetName: preset.name,
935
+ layers: preset.layers,
936
+ kinds: preset.kinds,
937
+ exclude: preset.exclude ? [...preset.exclude] : [],
938
+ recommendedBudget: preset.recommendedBudget ?? null
334
939
  };
335
940
  }
941
+ function detectPreset(cwd, forced) {
942
+ return detectPresetFromContext(buildPresetContext(cwd), forced);
943
+ }
336
944
 
337
945
  // src/scanner/files.ts
338
946
  import * as childProcess from "child_process";
@@ -866,6 +1474,8 @@ function normalizeResolvedConfig(config) {
866
1474
  config.includeMonolith === true
867
1475
  ),
868
1476
  include: resolveInclude(config.include),
1477
+ preset: typeof config.preset === "string" ? config.preset : "generic",
1478
+ presetExclude: Array.isArray(config.presetExclude) ? config.presetExclude : [],
869
1479
  layersMode: config.layersMode === "replace" ? "replace" : "merge",
870
1480
  budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
871
1481
  hooks: resolveHooks(config.hooks),
@@ -913,6 +1523,7 @@ function parseJsonish(raw, file) {
913
1523
  }
914
1524
  async function loadConfigFileDetailed(cwd) {
915
1525
  const warnings = [];
1526
+ const errors = [];
916
1527
  for (const filename of CONFIG_FILENAMES) {
917
1528
  const fullPath = path4.join(cwd, filename);
918
1529
  if (!fs4.existsSync(fullPath)) continue;
@@ -921,10 +1532,13 @@ async function loadConfigFileDetailed(cwd) {
921
1532
  return {
922
1533
  config: parseJsonish(fs4.readFileSync(fullPath, "utf8"), fullPath),
923
1534
  file: fullPath,
924
- warnings
1535
+ warnings,
1536
+ errors
925
1537
  };
926
1538
  } catch (err) {
927
- warnings.push(err.message);
1539
+ const message = err.message;
1540
+ warnings.push(message);
1541
+ errors.push(message);
928
1542
  continue;
929
1543
  }
930
1544
  }
@@ -938,12 +1552,16 @@ async function loadConfigFileDetailed(cwd) {
938
1552
  const imported = await import(pathToFileURL(fullPath).href);
939
1553
  const config = imported.default ?? imported;
940
1554
  if (!config || typeof config !== "object") {
941
- warnings.push(`${fullPath} does not export a configuration object.`);
1555
+ const message = `${fullPath} does not export a configuration object.`;
1556
+ warnings.push(message);
1557
+ errors.push(message);
942
1558
  continue;
943
1559
  }
944
- return { config, file: fullPath, warnings };
1560
+ return { config, file: fullPath, warnings, errors };
945
1561
  } catch (err) {
946
- warnings.push(`Could not load ${fullPath}: ${err.message}`);
1562
+ const message = `Could not load ${fullPath}: ${err.message}`;
1563
+ warnings.push(message);
1564
+ errors.push(message);
947
1565
  continue;
948
1566
  }
949
1567
  }
@@ -953,13 +1571,15 @@ async function loadConfigFileDetailed(cwd) {
953
1571
  const parsed = parseJsonish(fs4.readFileSync(pkgJsonPath, "utf8"), pkgJsonPath);
954
1572
  const snapshotKey = parsed.snapshot;
955
1573
  if (snapshotKey && typeof snapshotKey === "object") {
956
- return { config: snapshotKey, file: pkgJsonPath, warnings };
1574
+ return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
957
1575
  }
958
1576
  } catch (err) {
959
- warnings.push(err.message);
1577
+ const message = err.message;
1578
+ warnings.push(message);
1579
+ errors.push(message);
960
1580
  }
961
1581
  }
962
- return { config: null, file: null, warnings };
1582
+ return { config: null, file: null, warnings, errors };
963
1583
  }
964
1584
  var MIN_MAX_PART_BYTES = 4096;
965
1585
  var ARTIFACT_NAMES = [
@@ -1054,7 +1674,9 @@ function boundedBytes(value, fallback, minimum, label) {
1054
1674
  `${label} must be a byte count such as 120000, "120k" or "1.5mb" (received ${describeBytes(value)}).`
1055
1675
  );
1056
1676
  }
1057
- if (parsed <= 0) return fallback;
1677
+ if (parsed <= 0) {
1678
+ throw new Error(`${label} must be a positive byte count (received ${describeBytes(value)}).`);
1679
+ }
1058
1680
  if (parsed < minimum) {
1059
1681
  throw new Error(`${label} must be at least ${minimum} bytes (received ${describeBytes(value)}).`);
1060
1682
  }
@@ -1094,12 +1716,12 @@ function resolveBudgets(budgets, fallback) {
1094
1716
  if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
1095
1717
  throw new Error("`budgets.byLayer` must be an object of layer name to byte size.");
1096
1718
  }
1097
- for (const [layer, value] of Object.entries(budgets.byLayer)) {
1098
- byLayer[layer] = boundedBytes(
1719
+ for (const [layer2, value] of Object.entries(budgets.byLayer)) {
1720
+ byLayer[layer2] = boundedBytes(
1099
1721
  value,
1100
1722
  fallback,
1101
1723
  MIN_MAX_PART_BYTES,
1102
- `budgets.byLayer["${layer}"]`
1724
+ `budgets.byLayer["${layer2}"]`
1103
1725
  );
1104
1726
  }
1105
1727
  }
@@ -1131,7 +1753,11 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
1131
1753
  }
1132
1754
  function resolveConfig(userConfig, cliOptions, baseDir) {
1133
1755
  const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
1134
- const preset = detectPreset(cwd);
1756
+ const forcedPreset = userConfig?.preset;
1757
+ if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
1758
+ throw new Error(unknownPresetMessage(forcedPreset));
1759
+ }
1760
+ const preset = detectPreset(cwd, forcedPreset);
1135
1761
  const layersMode = resolveLayersMode(userConfig?.layersMode);
1136
1762
  const layers = userConfig?.layers && userConfig.layers.length > 0 ? layersMode === "replace" ? userConfig.layers : [...userConfig.layers, ...preset.layers] : preset.layers;
1137
1763
  const kinds = userConfig?.kinds && userConfig.kinds.length > 0 ? layersMode === "replace" ? userConfig.kinds : [...userConfig.kinds, ...preset.kinds] : preset.kinds;
@@ -1167,6 +1793,8 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
1167
1793
  ),
1168
1794
  includeSensitiveFiles: cliOptions?.includeSensitiveFiles ?? userConfig?.includeSensitiveFiles ?? false,
1169
1795
  include: resolveInclude(userConfig?.include),
1796
+ preset: preset.presetName,
1797
+ presetExclude: [...preset.exclude],
1170
1798
  artifacts,
1171
1799
  layersMode,
1172
1800
  budgets,
@@ -1201,8 +1829,8 @@ function matchRule(filePath, matcher) {
1201
1829
  return false;
1202
1830
  }
1203
1831
  function classifyLayer(filePath, layers) {
1204
- for (const layer of layers) {
1205
- if (matchRule(filePath, layer.match)) return layer.name;
1832
+ for (const layer2 of layers) {
1833
+ if (matchRule(filePath, layer2.match)) return layer2.name;
1206
1834
  }
1207
1835
  return "other";
1208
1836
  }
@@ -1337,8 +1965,8 @@ function findRuleMatch(rules, relativePath) {
1337
1965
  (rule) => matchRule(relativePath, rule.match)
1338
1966
  );
1339
1967
  }
1340
- function readManifest(config) {
1341
- const manifestPath = path6.join(config.cwd, config.outDir, "manifest.json");
1968
+ function readManifest2(config) {
1969
+ const manifestPath = path6.resolve(config.cwd, config.outDir, "manifest.json");
1342
1970
  try {
1343
1971
  return JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
1344
1972
  } catch {
@@ -1355,7 +1983,7 @@ function explainPath(config, target) {
1355
1983
  };
1356
1984
  const collection = collectFiles(config.cwd, {
1357
1985
  includePatterns: config.include,
1358
- excludePatterns: config.excludePatterns,
1986
+ excludePatterns: [...config.excludePatterns, ...config.presetExclude],
1359
1987
  includeSensitiveFiles: config.includeSensitiveFiles
1360
1988
  });
1361
1989
  if (!collection.files.includes(relative4)) {
@@ -1382,14 +2010,14 @@ function explainPath(config, target) {
1382
2010
  if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
1383
2011
  if (fs5.existsSync(absolute) && fs5.statSync(absolute).isFile()) {
1384
2012
  const buffer = fs5.readFileSync(absolute);
1385
- const text = buffer.toString("utf8");
2013
+ const text2 = buffer.toString("utf8");
1386
2014
  result.bytes = buffer.length;
1387
- result.lines = countLines(text);
1388
- const { findings } = scanAndRedactSecrets(text, config.secretRules, relative4);
2015
+ result.lines = countLines(text2);
2016
+ const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative4);
1389
2017
  result.secretCount = findings.length;
1390
2018
  result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
1391
2019
  }
1392
- const manifest = readManifest(config);
2020
+ const manifest = readManifest2(config);
1393
2021
  const entry = manifest?.files.find((file) => file.relativePath === relative4);
1394
2022
  result.part = entry?.part ?? null;
1395
2023
  result.inclusionMode = entry?.inclusionMode;
@@ -1429,133 +2057,1147 @@ function renderExplainText(result) {
1429
2057
  return lines.join("\n");
1430
2058
  }
1431
2059
 
1432
- // src/index.ts
1433
- import * as crypto3 from "crypto";
1434
- import * as fs8 from "fs";
1435
- import * as path9 from "path";
2060
+ // src/wizard/init.ts
2061
+ import * as fs6 from "fs";
2062
+ import * as path7 from "path";
1436
2063
 
1437
- // src/ast/structured.ts
1438
- function stripJsonComments2(content) {
1439
- const text = content.replace(/^\uFEFF/, "");
1440
- let out = "";
1441
- let inString = false;
1442
- let inLineComment = false;
1443
- let inBlockComment = false;
1444
- for (let i = 0; i < text.length; i++) {
1445
- const char = text[i];
1446
- const next = text[i + 1];
1447
- if (inLineComment) {
1448
- if (char === "\n") {
1449
- inLineComment = false;
1450
- out += char;
1451
- }
1452
- continue;
1453
- }
1454
- if (inBlockComment) {
1455
- if (char === "*" && next === "/") {
1456
- inBlockComment = false;
1457
- i += 1;
1458
- }
1459
- continue;
1460
- }
1461
- if (inString) {
1462
- out += char;
1463
- if (char === "\\") {
1464
- if (next !== void 0) {
1465
- out += next;
1466
- i += 1;
1467
- }
1468
- } else if (char === '"') {
1469
- inString = false;
1470
- }
1471
- continue;
1472
- }
1473
- if (char === '"') {
1474
- inString = true;
1475
- out += char;
1476
- continue;
1477
- }
1478
- if (char === "/" && next === "/") {
1479
- inLineComment = true;
1480
- i += 1;
1481
- continue;
1482
- }
1483
- if (char === "/" && next === "*") {
1484
- inBlockComment = true;
1485
- i += 1;
1486
- continue;
1487
- }
1488
- out += char;
1489
- }
1490
- return out;
2064
+ // src/wizard/config-template.ts
2065
+ var DEFAULT_ANSWERS = {
2066
+ preset: "generic",
2067
+ maxPartBytes: "300k",
2068
+ largeFileBytes: "200k",
2069
+ include: [],
2070
+ excludePatterns: [],
2071
+ artifacts: {
2072
+ readme: true,
2073
+ manifest: true,
2074
+ parts: true,
2075
+ navigation: true,
2076
+ projectTree: true,
2077
+ audit: true,
2078
+ monolith: false
2079
+ },
2080
+ includeSensitiveFiles: false,
2081
+ outDir: "snapshot"
2082
+ };
2083
+ function list(values) {
2084
+ if (values.length === 0) return "[]";
2085
+ return `[${values.map((value) => JSON.stringify(value)).join(", ")}]`;
1491
2086
  }
1492
- function summarizeJson(content) {
2087
+ function size(value) {
2088
+ return typeof value === "string" ? JSON.stringify(value) : String(value);
2089
+ }
2090
+ var PRESET_NAMES = [
2091
+ "nextjs \xB7 react-vite \xB7 vue-vite \xB7 nuxt \xB7 sveltekit \xB7 angular \xB7 astro",
2092
+ "remix \xB7 solid \xB7 nest \xB7 node-express \xB7 node-lib",
2093
+ "python-django \xB7 python-fastapi \xB7 python-lib \xB7 go \xB7 rust",
2094
+ "java-spring \xB7 dotnet \xB7 php-laravel \xB7 ruby-rails",
2095
+ "monorepo \xB7 docs \xB7 infra \xB7 generic"
2096
+ ];
2097
+ function renderConfig(answers) {
2098
+ const { artifacts } = answers;
2099
+ return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
2100
+
2101
+ export default defineConfig({
2102
+ // \u2500\u2500 Project \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2103
+ // Which project shape to use. Detected from the repo when omitted.
2104
+ // ${PRESET_NAMES.join("\n // ")}
2105
+ preset: ${JSON.stringify(answers.preset)},
2106
+
2107
+ // Recorded in the manifest when git cannot report a branch.
2108
+ branch: "main",
2109
+
2110
+ // \u2500\u2500 Output \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2111
+ // Where the snapshot is written.
2112
+ outDir: ${JSON.stringify(answers.outDir)},
2113
+ // Scratch directory; the new snapshot is renamed into place atomically.
2114
+ tmpDir: "snapshot.tmp",
2115
+
2116
+ // \u2500\u2500 Size \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2117
+ // Hard ceiling per part. "300k", 307200 and "1.5mb" are all accepted.
2118
+ maxPartBytes: ${size(answers.maxPartBytes)},
2119
+ // Files above this size get a head/tail preview instead of the full body.
2120
+ largeFileBytes: ${size(answers.largeFileBytes)},
2121
+ previewHeadLines: 80,
2122
+ previewTailLines: 40,
2123
+ // Different ceilings per layer. Unlisted layers use \`default\`.
2124
+ budgets: {
2125
+ default: ${size(answers.maxPartBytes)},
2126
+ byLayer: {},
2127
+ },
2128
+
2129
+ // \u2500\u2500 What goes in \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2130
+ // Allow-list. Empty means "everything that is not ignored".
2131
+ // Example: ["src/**", "packages/*/src/**"]
2132
+ include: ${list(answers.include)},
2133
+ // Globs or path segments to drop. Example: ["**/*.test.ts", "docs"]
2134
+ excludePatterns: ${list(answers.excludePatterns)},
2135
+
2136
+ // \u2500\u2500 Classification \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2137
+ // "merge" = the preset's rules plus yours (yours win). "replace" = yours only.
2138
+ layersMode: "merge",
2139
+ // [{ name: "billing", match: "src/features/billing/**" }]
2140
+ layers: [],
2141
+ // [{ kind: "route", match: "src/app/api/**" }]
2142
+ kinds: [],
2143
+
2144
+ // \u2500\u2500 Safety \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2145
+ // .env, *.pem, .npmrc, .ssh/ and friends are skipped unless this is true.
2146
+ includeSensitiveFiles: ${answers.includeSensitiveFiles},
2147
+ secrets: {
2148
+ // [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
2149
+ customPatterns: [],
2150
+ // Set true to drop the 21 built-in rules.
2151
+ disableDefaultPatterns: false,
2152
+ },
2153
+
2154
+ // \u2500\u2500 Artifacts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2155
+ artifacts: {
2156
+ readme: ${artifacts.readme}, // README.md
2157
+ manifest: ${artifacts.manifest}, // manifest.json \u2014 required by validate and report
2158
+ parts: ${artifacts.parts}, // parts/part-XXX.md
2159
+ navigation: ${artifacts.navigation}, // reports/navigation.md
2160
+ projectTree: ${artifacts.projectTree}, // reports/project-tree.md
2161
+ audit: ${artifacts.audit}, // reports/audit.md
2162
+ monolith: ${artifacts.monolith}, // full-codebase.json + .md (roughly doubles output)
2163
+ },
2164
+ // Skip the monolith above this size. null = no limit.
2165
+ maxMonolithBytes: null,
2166
+
2167
+ // \u2500\u2500 Advanced \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
2168
+ // afterGenerate: (manifest) => { /* write your own artifact */ }
2169
+ hooks: {},
2170
+ });
2171
+ `;
2172
+ }
2173
+ function renderMinimalConfig(answers) {
2174
+ const lines = [];
2175
+ if (answers.preset !== DEFAULT_ANSWERS.preset) lines.push(` preset: ${JSON.stringify(answers.preset)},`);
2176
+ if (answers.maxPartBytes !== DEFAULT_ANSWERS.maxPartBytes) {
2177
+ lines.push(` maxPartBytes: ${size(answers.maxPartBytes)},`);
2178
+ }
2179
+ if (answers.largeFileBytes !== DEFAULT_ANSWERS.largeFileBytes) {
2180
+ lines.push(` largeFileBytes: ${size(answers.largeFileBytes)},`);
2181
+ }
2182
+ if (answers.include.length > 0) lines.push(` include: ${list(answers.include)},`);
2183
+ if (answers.excludePatterns.length > 0) {
2184
+ lines.push(` excludePatterns: ${list(answers.excludePatterns)},`);
2185
+ }
2186
+ if (answers.includeSensitiveFiles) lines.push(" includeSensitiveFiles: true,");
2187
+ if (answers.outDir !== DEFAULT_ANSWERS.outDir) lines.push(` outDir: ${JSON.stringify(answers.outDir)},`);
2188
+ const changed = Object.keys(answers.artifacts).filter(
2189
+ (name) => answers.artifacts[name] !== DEFAULT_ANSWERS.artifacts[name]
2190
+ );
2191
+ if (changed.length > 0) {
2192
+ const entries = changed.map((name) => ` ${name}: ${answers.artifacts[name]}`).join(",\n");
2193
+ lines.push(` artifacts: {
2194
+ ${entries},
2195
+ },`);
2196
+ }
2197
+ if (lines.length === 0) {
2198
+ return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
2199
+
2200
+ export default defineConfig({});
2201
+ `;
2202
+ }
2203
+ return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
2204
+
2205
+ export default defineConfig({
2206
+ ${lines.join("\n")}
2207
+ });
2208
+ `;
2209
+ }
2210
+
2211
+ // src/wizard/prompts.ts
2212
+ import * as readline from "readline";
2213
+ var PromptCancelled = class extends Error {
2214
+ constructor() {
2215
+ super("Prompt cancelled");
2216
+ this.name = "PromptCancelled";
2217
+ }
2218
+ };
2219
+ function defaultIO() {
2220
+ return { input: process.stdin, output: process.stdout };
2221
+ }
2222
+ function isInteractive(io) {
2223
+ return Boolean(io.input.isTTY && io.output.isTTY);
2224
+ }
2225
+ function moveSelection(current, delta, count) {
2226
+ if (count <= 0) return 0;
2227
+ return ((current + delta) % count + count) % count;
2228
+ }
2229
+ function toggleIndex(selected, index) {
2230
+ const next = new Set(selected);
2231
+ if (next.has(index)) next.delete(index);
2232
+ else next.add(index);
2233
+ return next;
2234
+ }
2235
+ function marker(active) {
2236
+ return active ? "\u276F" : " ";
2237
+ }
2238
+ function choiceLine(choice, active) {
2239
+ const hint = choice.hint ? ` ${choice.hint}` : "";
2240
+ return ` ${marker(active)} ${choice.label}${hint}`;
2241
+ }
2242
+ function renderSelect(message, choices, index) {
2243
+ const lines = [`\u25C6 ${message}`, ""];
2244
+ choices.forEach((choice, position) => lines.push(choiceLine(choice, position === index)));
2245
+ lines.push("", " \u2191\u2193 move \xB7 enter select");
2246
+ return lines.join("\n");
2247
+ }
2248
+ function renderMultiSelect(message, choices, index, selected) {
2249
+ const lines = [`\u25C6 ${message}`, ""];
2250
+ choices.forEach((choice, position) => {
2251
+ const box = selected.has(position) ? "\u25FC" : "\u25FB";
2252
+ const hint = choice.hint ? ` ${choice.hint}` : "";
2253
+ lines.push(` ${marker(position === index)} ${box} ${choice.label}${hint}`);
2254
+ });
2255
+ lines.push("", " \u2191\u2193 move \xB7 space toggle \xB7 enter confirm");
2256
+ return lines.join("\n");
2257
+ }
2258
+ function renderConfirm(message, value) {
2259
+ return [
2260
+ `\u25C6 ${message}`,
2261
+ "",
2262
+ ` ${marker(value)} Yes`,
2263
+ ` ${marker(!value)} No`,
2264
+ "",
2265
+ " \u2190\u2192 or y/n \xB7 enter confirm"
2266
+ ].join("\n");
2267
+ }
2268
+ function renderText(message, value, error) {
2269
+ const lines = [`\u25C6 ${message}`, "", ` ${value || "\u258F"}`];
2270
+ if (error) lines.push("", ` \u2716 ${error}`);
2271
+ return lines.join("\n");
2272
+ }
2273
+ function keyReader(input) {
2274
+ const queue = [];
2275
+ const waiters = [];
2276
+ const wasRaw = input.isRaw;
2277
+ const onKey = (_text, key) => {
2278
+ const waiter = waiters.shift();
2279
+ if (waiter) waiter(key);
2280
+ else queue.push(key);
2281
+ };
2282
+ readline.emitKeypressEvents(input);
2283
+ if (input.isTTY) input.setRawMode(true);
2284
+ input.resume();
2285
+ input.on("keypress", onKey);
2286
+ return {
2287
+ next() {
2288
+ const queued = queue.shift();
2289
+ if (queued) return Promise.resolve(queued);
2290
+ return new Promise((resolve6) => waiters.push(resolve6));
2291
+ },
2292
+ close() {
2293
+ input.off("keypress", onKey);
2294
+ if (input.isTTY) input.setRawMode(wasRaw ?? false);
2295
+ input.pause();
2296
+ }
2297
+ };
2298
+ }
2299
+ var Frame = class {
2300
+ constructor(output) {
2301
+ this.output = output;
2302
+ }
2303
+ output;
2304
+ drawn = 0;
2305
+ draw(text2) {
2306
+ this.erase();
2307
+ this.output.write(`${text2}
2308
+ `);
2309
+ this.drawn = text2.split("\n").length;
2310
+ }
2311
+ erase() {
2312
+ if (this.drawn > 0) this.output.write(`\x1B[${this.drawn}A\x1B[0J`);
2313
+ this.drawn = 0;
2314
+ }
2315
+ };
2316
+ function isCancel(key) {
2317
+ return key.ctrl === true && key.name === "c" || key.ctrl === true && key.name === "d";
2318
+ }
2319
+ async function select(io, message, choices, options = {}) {
2320
+ if (choices.length === 0) throw new Error("select() requires at least one choice.");
2321
+ const fallbackIndex = Math.max(
2322
+ 0,
2323
+ choices.findIndex((choice) => choice.value === options.defaultValue)
2324
+ );
2325
+ if (!isInteractive(io)) {
2326
+ io.output.write(`${renderSelect(message, choices, fallbackIndex)}
2327
+ `);
2328
+ return choices[fallbackIndex].value;
2329
+ }
2330
+ const frame = new Frame(io.output);
2331
+ const reader = keyReader(io.input);
2332
+ let index = fallbackIndex;
1493
2333
  try {
1494
- const parsed = JSON.parse(stripJsonComments2(content));
1495
- if (Array.isArray(parsed)) {
1496
- const sample = parsed[0];
1497
- const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
1498
- return {
1499
- shape: "array",
1500
- itemCount: parsed.length,
1501
- keys: [],
1502
- sampleKeys
1503
- };
2334
+ for (; ; ) {
2335
+ frame.draw(renderSelect(message, choices, index));
2336
+ const key = await reader.next();
2337
+ if (isCancel(key)) throw new PromptCancelled();
2338
+ if (key.name === "up") index = moveSelection(index, -1, choices.length);
2339
+ else if (key.name === "down") index = moveSelection(index, 1, choices.length);
2340
+ else if (key.name === "return" || key.name === "enter") {
2341
+ frame.erase();
2342
+ return choices[index].value;
2343
+ } else if (key.name === "j") index = moveSelection(index, 1, choices.length);
2344
+ else if (key.name === "k") index = moveSelection(index, -1, choices.length);
1504
2345
  }
1505
- if (parsed && typeof parsed === "object") {
1506
- const keys = Object.keys(parsed).sort();
1507
- return {
1508
- shape: "object",
1509
- itemCount: keys.length,
1510
- keys: keys.slice(0, 30),
1511
- sampleKeys: []
1512
- };
2346
+ } finally {
2347
+ reader.close();
2348
+ }
2349
+ }
2350
+ async function multiSelect(io, message, choices, options = {}) {
2351
+ const selected = /* @__PURE__ */ new Set();
2352
+ choices.forEach((choice, position) => {
2353
+ if (options.defaults?.includes(choice.value)) selected.add(position);
2354
+ });
2355
+ const result = () => choices.filter((_, position) => selected.has(position)).map((choice) => choice.value);
2356
+ if (!isInteractive(io)) {
2357
+ io.output.write(`${renderMultiSelect(message, choices, 0, selected)}
2358
+ `);
2359
+ return result();
2360
+ }
2361
+ const frame = new Frame(io.output);
2362
+ const reader = keyReader(io.input);
2363
+ let index = 0;
2364
+ try {
2365
+ for (; ; ) {
2366
+ frame.draw(renderMultiSelect(message, choices, index, selected));
2367
+ const key = await reader.next();
2368
+ if (isCancel(key)) throw new PromptCancelled();
2369
+ if (key.name === "up") index = moveSelection(index, -1, choices.length);
2370
+ else if (key.name === "down") index = moveSelection(index, 1, choices.length);
2371
+ else if (key.name === "space") {
2372
+ const next = toggleIndex(selected, index);
2373
+ selected.clear();
2374
+ for (const position of next) selected.add(position);
2375
+ } else if (key.name === "a") {
2376
+ if (selected.size === choices.length) selected.clear();
2377
+ else choices.forEach((_, position) => selected.add(position));
2378
+ } else if (key.name === "return" || key.name === "enter") {
2379
+ frame.erase();
2380
+ return result();
2381
+ }
1513
2382
  }
1514
- return {
1515
- shape: typeof parsed,
1516
- itemCount: null,
1517
- keys: [],
1518
- sampleKeys: []
1519
- };
2383
+ } finally {
2384
+ reader.close();
2385
+ }
2386
+ }
2387
+ async function confirm(io, message, options = {}) {
2388
+ let value = options.defaultValue ?? true;
2389
+ if (!isInteractive(io)) {
2390
+ io.output.write(`${renderConfirm(message, value)}
2391
+ `);
2392
+ return value;
2393
+ }
2394
+ const frame = new Frame(io.output);
2395
+ const reader = keyReader(io.input);
2396
+ try {
2397
+ for (; ; ) {
2398
+ frame.draw(renderConfirm(message, value));
2399
+ const key = await reader.next();
2400
+ if (isCancel(key)) throw new PromptCancelled();
2401
+ if (key.name === "up" || key.name === "left") value = true;
2402
+ else if (key.name === "down" || key.name === "right") value = false;
2403
+ else if (key.name === "y") value = true;
2404
+ else if (key.name === "n") value = false;
2405
+ else if (key.name === "return" || key.name === "enter") {
2406
+ frame.erase();
2407
+ return value;
2408
+ }
2409
+ }
2410
+ } finally {
2411
+ reader.close();
2412
+ }
2413
+ }
2414
+ async function text(io, message, options = {}) {
2415
+ const fallback = options.defaultValue ?? "";
2416
+ if (!isInteractive(io)) {
2417
+ io.output.write(`${renderText(message, fallback)}
2418
+ `);
2419
+ return fallback;
2420
+ }
2421
+ const frame = new Frame(io.output);
2422
+ const reader = keyReader(io.input);
2423
+ let value = fallback;
2424
+ let error;
2425
+ try {
2426
+ for (; ; ) {
2427
+ frame.draw(renderText(message, value, error));
2428
+ const key = await reader.next();
2429
+ if (isCancel(key)) throw new PromptCancelled();
2430
+ error = void 0;
2431
+ if (key.name === "return" || key.name === "enter") {
2432
+ const problem = options.validate ? options.validate(value) : null;
2433
+ if (problem) {
2434
+ error = problem;
2435
+ continue;
2436
+ }
2437
+ frame.erase();
2438
+ return value;
2439
+ }
2440
+ if (key.name === "backspace") {
2441
+ value = value.slice(0, -1);
2442
+ continue;
2443
+ }
2444
+ if (key.name === "escape") {
2445
+ value = "";
2446
+ continue;
2447
+ }
2448
+ if (_isPrintable(key)) value += _keyText(key);
2449
+ }
2450
+ } finally {
2451
+ reader.close();
2452
+ }
2453
+ }
2454
+ function _isPrintable(key) {
2455
+ return typeof key.sequence === "string" && key.sequence.length === 1 && key.sequence >= " ";
2456
+ }
2457
+ function _keyText(key) {
2458
+ return key.sequence ?? "";
2459
+ }
2460
+
2461
+ // src/wizard/init.ts
2462
+ var CONFIG_CANDIDATES = [
2463
+ "snapshot.config.ts",
2464
+ "snapshot.config.js",
2465
+ "snapshot.config.mjs",
2466
+ "snapshot.config.cjs",
2467
+ "snapshot.config.json",
2468
+ ".snapshotrc.json",
2469
+ ".snapshotrc"
2470
+ ];
2471
+ var SIZE_CHOICES = [
2472
+ { value: "60k", label: "Small", hint: "60 KB \xB7 ~15k tokens \xB7 tight context windows" },
2473
+ { value: "120k", label: "Medium", hint: "120 KB \xB7 ~30k tokens \xB7 recommended" },
2474
+ { value: "300k", label: "Large", hint: "300 KB \xB7 ~75k tokens \xB7 the default" },
2475
+ { value: "custom", label: "Custom\u2026", hint: "type a size like 150k or 1.5mb" }
2476
+ ];
2477
+ function existingConfig(cwd) {
2478
+ for (const name of CONFIG_CANDIDATES) {
2479
+ if (fs6.existsSync(path7.join(cwd, name))) return name;
2480
+ }
2481
+ return null;
2482
+ }
2483
+ function artifactAnswers(options) {
2484
+ const artifacts = { ...DEFAULT_ANSWERS.artifacts };
2485
+ for (const name of options.skipArtifacts ?? []) {
2486
+ if (ARTIFACT_NAMES.includes(name)) {
2487
+ artifacts[name] = false;
2488
+ }
2489
+ }
2490
+ if (options.onlyArtifacts && options.onlyArtifacts.length > 0) {
2491
+ const keep = new Set(options.onlyArtifacts);
2492
+ for (const name of ARTIFACT_NAMES) artifacts[name] = keep.has(name);
2493
+ }
2494
+ return artifacts;
2495
+ }
2496
+ function topLevelDirs(cwd) {
2497
+ try {
2498
+ return fs6.readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort();
1520
2499
  } catch {
2500
+ return [];
2501
+ }
2502
+ }
2503
+ function addScripts(cwd, notes) {
2504
+ const packagePath = path7.join(cwd, "package.json");
2505
+ if (!fs6.existsSync(packagePath)) {
2506
+ notes.push("No package.json found, so npm scripts were not added.");
2507
+ return [];
2508
+ }
2509
+ let parsed;
2510
+ try {
2511
+ parsed = JSON.parse(fs6.readFileSync(packagePath, "utf8"));
2512
+ } catch {
2513
+ notes.push("Could not parse package.json; npm scripts were not added.");
2514
+ return [];
2515
+ }
2516
+ const scripts = parsed.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {};
2517
+ const added = [];
2518
+ if (!scripts.snapshot) {
2519
+ scripts.snapshot = "codebase-snapshot generate";
2520
+ added.push("snapshot");
2521
+ }
2522
+ if (!scripts["snapshot:validate"]) {
2523
+ scripts["snapshot:validate"] = "codebase-snapshot validate";
2524
+ added.push("snapshot:validate");
2525
+ }
2526
+ if (added.length === 0) return [];
2527
+ parsed.scripts = scripts;
2528
+ fs6.writeFileSync(packagePath, `${JSON.stringify(parsed, null, 2)}
2529
+ `, "utf8");
2530
+ return added;
2531
+ }
2532
+ function addGitignore(cwd) {
2533
+ const ignorePath = path7.join(cwd, ".gitignore");
2534
+ const existing = fs6.existsSync(ignorePath) ? fs6.readFileSync(ignorePath, "utf8") : "";
2535
+ const wanted = ["snapshot/", "snapshot.tmp/"];
2536
+ const added = wanted.filter((entry) => !existing.split(/\r?\n/).includes(entry));
2537
+ if (added.length === 0) return [];
2538
+ const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
2539
+ fs6.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
2540
+ `, "utf8");
2541
+ return added;
2542
+ }
2543
+ function resolvePresetChoice(answers, presetName) {
2544
+ return {
2545
+ value: presetName,
2546
+ label: presetName,
2547
+ hint: presetName === answers.preset ? "detected" : void 0
2548
+ };
2549
+ }
2550
+ async function runInit(options) {
2551
+ const io = options.io ?? defaultIO();
2552
+ const detected = detectPreset(options.cwd, options.preset);
2553
+ const notes = [];
2554
+ const answers = {
2555
+ preset: detected.presetName,
2556
+ maxPartBytes: options.maxPartBytes ?? detected.recommendedBudget ?? DEFAULT_ANSWERS.maxPartBytes,
2557
+ largeFileBytes: DEFAULT_ANSWERS.largeFileBytes,
2558
+ include: options.include ?? [],
2559
+ excludePatterns: options.exclude ?? [],
2560
+ artifacts: artifactAnswers(options),
2561
+ includeSensitiveFiles: options.includeSensitiveFiles ?? DEFAULT_ANSWERS.includeSensitiveFiles,
2562
+ outDir: options.outDir ?? DEFAULT_ANSWERS.outDir
2563
+ };
2564
+ const interactive = isInteractive(io) && options.yes !== true;
2565
+ if (interactive) {
2566
+ io.output.write("\n @ankurhalder/codebase-snapshot\n");
2567
+ io.output.write(" Turn this repository into an LLM-ready, redacted, byte-bounded snapshot.\n\n");
2568
+ io.output.write(` Detected ${detected.presetName}
2569
+ `);
2570
+ io.output.write(` Directory ${options.cwd}
2571
+
2572
+ `);
2573
+ const useDetected = await confirm(io, `Set up for ${detected.presetName}?`, {
2574
+ defaultValue: true
2575
+ });
2576
+ if (!useDetected) {
2577
+ answers.preset = await select(
2578
+ io,
2579
+ "What kind of project is this?",
2580
+ listPresetNames().map((name) => resolvePresetChoice(answers, name))
2581
+ );
2582
+ const chosen = detectPreset(options.cwd, answers.preset);
2583
+ answers.preset = chosen.presetName;
2584
+ }
2585
+ const scope = await select(
2586
+ io,
2587
+ "What should go into the snapshot?",
2588
+ [
2589
+ { value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
2590
+ { value: "folders", label: "Only specific folders\u2026" },
2591
+ { value: "no-tests", label: "Everything, but skip tests" },
2592
+ { value: "custom", label: "Let me write glob patterns" }
2593
+ ],
2594
+ { defaultValue: "all" }
2595
+ );
2596
+ if (scope === "folders") {
2597
+ const dirs = topLevelDirs(options.cwd).slice(0, 40);
2598
+ if (dirs.length === 0) {
2599
+ notes.push("No top-level directories found; the snapshot will include everything.");
2600
+ } else {
2601
+ const chosen = await multiSelect(
2602
+ io,
2603
+ "Select folders",
2604
+ dirs.map((name) => ({ value: name, label: `${name}/` })),
2605
+ { defaults: dirs }
2606
+ );
2607
+ answers.include = chosen.map((name) => `${name}/**`);
2608
+ }
2609
+ } else if (scope === "no-tests") {
2610
+ answers.excludePatterns = [
2611
+ ...answers.excludePatterns,
2612
+ "**/*.test.*",
2613
+ "**/*.spec.*",
2614
+ "tests",
2615
+ "test"
2616
+ ];
2617
+ } else if (scope === "custom") {
2618
+ const raw = await text(io, "Glob patterns to include (comma separated)", {
2619
+ defaultValue: ""
2620
+ });
2621
+ answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
2622
+ }
2623
+ const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
2624
+ defaultValue: "120k"
2625
+ });
2626
+ if (sizeChoice === "custom") {
2627
+ const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
2628
+ defaultValue: String(answers.maxPartBytes),
2629
+ validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null
2630
+ });
2631
+ answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
2632
+ } else {
2633
+ answers.maxPartBytes = sizeChoice;
2634
+ }
2635
+ const artifactChoices = [
2636
+ { value: "readme", label: "README.md", hint: "orientation page" },
2637
+ { value: "manifest", label: "manifest.json", hint: "required by validate and report" },
2638
+ { value: "parts", label: "parts/", hint: "the code payload" },
2639
+ { value: "navigation", label: "reports/navigation.md", hint: "file \u2192 part index" },
2640
+ { value: "projectTree", label: "reports/project-tree.md", hint: "ASCII tree" },
2641
+ { value: "audit", label: "reports/audit.md", hint: "security and health findings" },
2642
+ { value: "monolith", label: "full-codebase.json + .md", hint: "roughly doubles output" }
2643
+ ];
2644
+ const presetEnabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
2645
+ const selectedArtifacts = await multiSelect(
2646
+ io,
2647
+ "Which artifacts should be generated?",
2648
+ artifactChoices,
2649
+ { defaults: presetEnabled }
2650
+ );
2651
+ for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
2652
+ const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
2653
+ answers.includeSensitiveFiles = await confirm(
2654
+ io,
2655
+ "Include credential files (.env, *.pem, .npmrc, .ssh/)?",
2656
+ { defaultValue: false }
2657
+ );
2658
+ io.output.write("\n Review\n\n");
2659
+ io.output.write(` project ${answers.preset}
2660
+ `);
2661
+ io.output.write(` include ${answers.include.length ? answers.include.join(", ") : "everything"}
2662
+ `);
2663
+ io.output.write(` part size ${String(answers.maxPartBytes)}
2664
+ `);
2665
+ io.output.write(` artifacts ${enabledCount} of ${ARTIFACT_NAMES.length}
2666
+ `);
2667
+ io.output.write(` credentials ${answers.includeSensitiveFiles ? "included" : "skipped"}
2668
+
2669
+ `);
2670
+ io.output.write(" Will write snapshot.config with your answers, plus every other setting\n");
2671
+ io.output.write(" at its default \u2014 so nothing is hidden behind the docs.\n\n");
2672
+ const proceed = await confirm(io, "Write this and continue?", { defaultValue: true });
2673
+ if (!proceed) throw new PromptCancelled();
2674
+ }
2675
+ const configText = options.minimal ? renderMinimalConfig(answers) : renderConfig(answers);
2676
+ const configName = supportsTypeScriptImports() ? "snapshot.config.ts" : "snapshot.config.mjs";
2677
+ const configPath = path7.join(options.cwd, configName);
2678
+ if (options.dryRun) {
1521
2679
  return {
1522
- shape: "invalid-json",
1523
- itemCount: null,
1524
- keys: [],
1525
- sampleKeys: []
2680
+ configPath,
2681
+ configText,
2682
+ written: false,
2683
+ preset: answers.preset,
2684
+ scripts: [],
2685
+ gitignoreEntries: [],
2686
+ notes
1526
2687
  };
1527
2688
  }
1528
- }
1529
- function summarizeMarkdown(content) {
1530
- const headings = [];
1531
- const frontmatterKeys = [];
1532
- const lines = content.replace(/^\uFEFF/, "").split("\n");
1533
- let inFrontmatter = false;
1534
- for (let index = 0; index < lines.length; index++) {
1535
- const line = lines[index];
1536
- if (index === 0 && line.trim() === "---") {
1537
- inFrontmatter = true;
1538
- continue;
1539
- }
1540
- if (inFrontmatter) {
1541
- if (line.trim() === "---") {
1542
- inFrontmatter = false;
1543
- continue;
2689
+ const present = existingConfig(options.cwd);
2690
+ if (present && present !== configName && !options.force) {
2691
+ throw new Error(
2692
+ `A configuration already exists (${present}). Re-run with --force to replace it.`
2693
+ );
2694
+ }
2695
+ if (fs6.existsSync(configPath) && !options.force) {
2696
+ throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
2697
+ }
2698
+ fs6.writeFileSync(configPath, configText, "utf8");
2699
+ const scripts = addScripts(options.cwd, notes);
2700
+ const gitignoreEntries = addGitignore(options.cwd);
2701
+ return {
2702
+ configPath,
2703
+ configText,
2704
+ written: true,
2705
+ preset: answers.preset,
2706
+ scripts,
2707
+ gitignoreEntries,
2708
+ notes
2709
+ };
2710
+ }
2711
+
2712
+ // src/index.ts
2713
+ import * as crypto3 from "crypto";
2714
+ import * as fs9 from "fs";
2715
+ import * as path10 from "path";
2716
+
2717
+ // src/ast/profiles/shared.ts
2718
+ var MAX_SUMMARY_CHARS = 1e6;
2719
+ function emptySummary() {
2720
+ return {
2721
+ directives: [],
2722
+ imports: [],
2723
+ exports: [],
2724
+ reExports: [],
2725
+ functions: [],
2726
+ classes: [],
2727
+ interfaces: [],
2728
+ types: [],
2729
+ enums: [],
2730
+ variables: [],
2731
+ reactComponents: [],
2732
+ hooks: [],
2733
+ routeHandlers: []
2734
+ };
2735
+ }
2736
+ function dedupeSorted(values) {
2737
+ return Array.from(new Set(values.filter(Boolean))).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
2738
+ }
2739
+ function finalizeSummary(summary) {
2740
+ return {
2741
+ directives: dedupeSorted(summary.directives),
2742
+ imports: dedupeSorted(summary.imports),
2743
+ exports: dedupeSorted(summary.exports),
2744
+ reExports: dedupeSorted(summary.reExports),
2745
+ functions: dedupeSorted(summary.functions),
2746
+ classes: dedupeSorted(summary.classes),
2747
+ interfaces: dedupeSorted(summary.interfaces),
2748
+ types: dedupeSorted(summary.types),
2749
+ enums: dedupeSorted(summary.enums),
2750
+ variables: dedupeSorted(summary.variables),
2751
+ reactComponents: dedupeSorted(summary.reactComponents),
2752
+ hooks: dedupeSorted(summary.hooks),
2753
+ routeHandlers: dedupeSorted(summary.routeHandlers)
2754
+ };
2755
+ }
2756
+ function eachLine(content, visit) {
2757
+ const text2 = content.length > MAX_SUMMARY_CHARS ? content.slice(0, MAX_SUMMARY_CHARS) : content;
2758
+ for (const line of text2.split(/\r?\n/)) visit(line);
2759
+ }
2760
+ function isTopLevel(line) {
2761
+ return !/^[\s]/.test(line);
2762
+ }
2763
+ function isPublicName(name) {
2764
+ return name.length > 0 && !name.startsWith("_") && /^[A-Za-z]/.test(name);
2765
+ }
2766
+ function isCapitalizedName(name) {
2767
+ return /^[A-Z]/.test(name);
2768
+ }
2769
+
2770
+ // src/ast/profiles/cfamily.ts
2771
+ var INCLUDE = /^#\s*include\s*[<"]([^>"]+)[>"]/;
2772
+ var DEFINE = /^#\s*define\s+([A-Za-z_]\w*)/;
2773
+ var USING = /^using\s+(?:namespace\s+)?([\w:]+)\s*;/;
2774
+ var TYPE_DECL = /^(?:typedef\s+)?(?:struct|union|class|enum)\s+([A-Za-z_]\w*)/;
2775
+ var TYPEDEF = /^typedef\b[^;{}]*?\b([A-Za-z_]\w*)\s*;/;
2776
+ var FUNCTION = /^[A-Za-z_][\w \t*&:<>,]*?\b([A-Za-z_]\w*)\s*\([^;]*\)\s*\{?\s*$/;
2777
+ var NOT_A_FUNCTION = /^(?:if|for|while|switch|return|else|do|case|sizeof|catch|throw)\b/;
2778
+ var cfamilyProfile = {
2779
+ id: "cfamily",
2780
+ extensions: [".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hxx", ".m", ".mm"],
2781
+ summarize(_path, content) {
2782
+ const summary = emptySummary();
2783
+ eachLine(content, (line) => {
2784
+ const trimmed = line.trim();
2785
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
2786
+ return;
2787
+ }
2788
+ const included = INCLUDE.exec(trimmed);
2789
+ if (included) {
2790
+ summary.imports.push(included[1]);
2791
+ return;
2792
+ }
2793
+ const used = USING.exec(trimmed);
2794
+ if (used) {
2795
+ summary.imports.push(used[1]);
2796
+ return;
2797
+ }
2798
+ const macro = DEFINE.exec(trimmed);
2799
+ if (macro) {
2800
+ summary.variables.push(macro[1]);
2801
+ return;
2802
+ }
2803
+ if (!isTopLevel(line)) return;
2804
+ const declared = TYPE_DECL.exec(line);
2805
+ if (declared) {
2806
+ const kind = /^typedef\s+enum/.test(line) || /^enum\b/.test(trimmed) ? "enum" : "type";
2807
+ if (kind === "enum") summary.enums.push(declared[1]);
2808
+ else summary.classes.push(declared[1]);
2809
+ summary.exports.push(declared[1]);
2810
+ return;
2811
+ }
2812
+ const aliased = TYPEDEF.exec(line);
2813
+ if (aliased) {
2814
+ summary.types.push(aliased[1]);
2815
+ summary.exports.push(aliased[1]);
2816
+ return;
2817
+ }
2818
+ if (NOT_A_FUNCTION.test(trimmed)) return;
2819
+ const fn = FUNCTION.exec(line);
2820
+ if (fn) {
2821
+ summary.functions.push(fn[1]);
2822
+ summary.exports.push(fn[1]);
2823
+ }
2824
+ });
2825
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
2826
+ }
2827
+ };
2828
+
2829
+ // src/ast/profiles/dotnet.ts
2830
+ var USING2 = /^using\s+([\w.]+)\s*;/;
2831
+ var TYPE_DECL2 = /^\s*(?:public\s+|internal\s+|private\s+|protected\s+|sealed\s+|abstract\s+|static\s+|partial\s+|readonly\s+)*(class|interface|struct|record|enum)\s+([A-Za-z_]\w*)/;
2832
+ var METHOD = /^\s*(?:public|private|protected|internal)\s+(?:static\s+)?(?:async\s+)?(?:virtual\s+|override\s+|sealed\s+)?[\w<>[\],?]+\s+([A-Za-z_]\w*)\s*[<(]/;
2833
+ var dotnetProfile = {
2834
+ id: "dotnet",
2835
+ extensions: [".cs"],
2836
+ summarize(_path, content) {
2837
+ const summary = emptySummary();
2838
+ eachLine(content, (line) => {
2839
+ const trimmed = line.trim();
2840
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
2841
+ return;
2842
+ }
2843
+ const used = USING2.exec(trimmed);
2844
+ if (used) {
2845
+ summary.imports.push(used[1]);
2846
+ return;
2847
+ }
2848
+ const declaration = TYPE_DECL2.exec(line);
2849
+ if (declaration) {
2850
+ const kind = declaration[1];
2851
+ const name = declaration[2];
2852
+ if (kind === "interface") summary.interfaces.push(name);
2853
+ else if (kind === "enum") summary.enums.push(name);
2854
+ else summary.classes.push(name);
2855
+ if (/^\s*public\b/.test(line)) summary.exports.push(name);
2856
+ return;
2857
+ }
2858
+ const method = METHOD.exec(line);
2859
+ if (method) {
2860
+ summary.functions.push(method[1]);
2861
+ if (/^\s*public\b/.test(line)) summary.exports.push(method[1]);
2862
+ }
2863
+ });
2864
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
2865
+ }
2866
+ };
2867
+
2868
+ // src/ast/profiles/go.ts
2869
+ var FUNC = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/;
2870
+ var TYPE = /^type\s+([A-Za-z_]\w*)\s+(.*)$/;
2871
+ var VAR_CONST = /^(?:var|const)\s+([A-Za-z_]\w*)/;
2872
+ var IMPORT_SINGLE = /^import\s+(?:[\w.]+\s+)?"([^"]+)"/;
2873
+ var IMPORT_OPEN = /^import\s*\(/;
2874
+ var IMPORT_ENTRY = /^\s*(?:[\w.]+\s+)?"([^"]+)"/;
2875
+ var goProfile = {
2876
+ id: "go",
2877
+ extensions: [".go"],
2878
+ summarize(_path, content) {
2879
+ const summary = emptySummary();
2880
+ let inImportBlock = false;
2881
+ eachLine(content, (line) => {
2882
+ const trimmed = line.trim();
2883
+ if (!trimmed || trimmed.startsWith("//")) return;
2884
+ if (inImportBlock) {
2885
+ if (trimmed.startsWith(")")) {
2886
+ inImportBlock = false;
2887
+ return;
2888
+ }
2889
+ const entry = IMPORT_ENTRY.exec(line);
2890
+ if (entry) summary.imports.push(entry[1]);
2891
+ return;
2892
+ }
2893
+ if (IMPORT_OPEN.test(trimmed)) {
2894
+ inImportBlock = true;
2895
+ return;
2896
+ }
2897
+ const single = IMPORT_SINGLE.exec(trimmed);
2898
+ if (single) {
2899
+ summary.imports.push(single[1]);
2900
+ return;
2901
+ }
2902
+ if (!isTopLevel(line)) return;
2903
+ const method = FUNC.exec(line);
2904
+ if (method) {
2905
+ const name = method[1];
2906
+ summary.functions.push(name);
2907
+ if (isCapitalizedName(name)) summary.exports.push(name);
2908
+ return;
2909
+ }
2910
+ const declared = TYPE.exec(line);
2911
+ if (declared) {
2912
+ const name = declared[1];
2913
+ const body = declared[2].trim();
2914
+ if (body.startsWith("struct")) summary.classes.push(name);
2915
+ else if (body.startsWith("interface")) summary.interfaces.push(name);
2916
+ else summary.types.push(name);
2917
+ if (isCapitalizedName(name)) summary.exports.push(name);
2918
+ return;
2919
+ }
2920
+ const value = VAR_CONST.exec(line);
2921
+ if (value) {
2922
+ summary.variables.push(value[1]);
2923
+ if (isCapitalizedName(value[1])) summary.exports.push(value[1]);
2924
+ }
2925
+ });
2926
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
2927
+ }
2928
+ };
2929
+
2930
+ // src/ast/profiles/jvm.ts
2931
+ var IMPORT = /^import\s+(?:static\s+)?([\w.]+?)(?:\.\*)?;?\s*$/;
2932
+ var TYPE_DECL3 = /^\s*(?:public\s+|private\s+|protected\s+|internal\s+)?(?:final\s+|abstract\s+|open\s+|sealed\s+|data\s+|static\s+|partial\s+)*(class|interface|enum|record|object|trait)\s+(?:class\s+)?([A-Za-z_]\w*)/;
2933
+ var KOTLIN_FUN = /^\s*(?:public\s+|private\s+|protected\s+|internal\s+|override\s+|suspend\s+|inline\s+|open\s+|operator\s+)*fun\s+(?:<[^>]*>\s*)?(?:[\w.]+\.)?([A-Za-z_]\w*)/;
2934
+ var SCALA_DEF = /^\s*def\s+([A-Za-z_]\w*)/;
2935
+ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:abstract\s+)?[\w<>[\],.?]+\s+([A-Za-z_]\w*)\s*\(/;
2936
+ var jvmProfile = {
2937
+ id: "jvm",
2938
+ extensions: [".java", ".kt", ".kts", ".scala"],
2939
+ summarize(path12, content) {
2940
+ const summary = emptySummary();
2941
+ const lower = path12.toLowerCase();
2942
+ const isJava = lower.endsWith(".java");
2943
+ const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
2944
+ eachLine(content, (line) => {
2945
+ const trimmed = line.trim();
2946
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
2947
+ return;
2948
+ }
2949
+ const imported = IMPORT.exec(trimmed);
2950
+ if (imported) {
2951
+ summary.imports.push(imported[1]);
2952
+ return;
2953
+ }
2954
+ const declaration = TYPE_DECL3.exec(line);
2955
+ if (declaration) {
2956
+ const kind = declaration[1];
2957
+ const name = declaration[2];
2958
+ if (kind === "interface" || kind === "trait") summary.interfaces.push(name);
2959
+ else if (kind === "enum") summary.enums.push(name);
2960
+ else summary.classes.push(name);
2961
+ const hidden = /^\s*(?:private|protected|internal)\b/.test(line);
2962
+ if (isJava ? /^\s*public\b/.test(line) : !hidden) summary.exports.push(name);
2963
+ return;
2964
+ }
2965
+ const kotlinFunction = KOTLIN_FUN.exec(line);
2966
+ if (kotlinFunction && isKotlin) {
2967
+ summary.functions.push(kotlinFunction[1]);
2968
+ const hidden = /^\s*(?:private|protected|internal)\b/.test(line);
2969
+ if (!hidden) summary.exports.push(kotlinFunction[1]);
2970
+ return;
2971
+ }
2972
+ const scalaFunction = SCALA_DEF.exec(line);
2973
+ if (scalaFunction && !isJava && !isKotlin) {
2974
+ summary.functions.push(scalaFunction[1]);
2975
+ const hidden = /^\s*(?:private|protected)\b/.test(line);
2976
+ if (!hidden) summary.exports.push(scalaFunction[1]);
2977
+ return;
2978
+ }
2979
+ if (isJava) {
2980
+ const method = JAVA_METHOD.exec(line);
2981
+ if (method) {
2982
+ summary.functions.push(method[1]);
2983
+ if (/^\s*public\b/.test(line)) summary.exports.push(method[1]);
2984
+ }
2985
+ }
2986
+ });
2987
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
2988
+ }
2989
+ };
2990
+
2991
+ // src/ast/profiles/php.ts
2992
+ var USE = /^use\s+([\w\\]+)\s*(?:as\s+\w+)?;/;
2993
+ var DECLARE = /^(?:abstract\s+|final\s+|readonly\s+)*(class|interface|trait|enum)\s+([A-Za-z_]\w*)/;
2994
+ var FUNCTION2 = /^(?:public\s+|protected\s+|private\s+|static\s+|final\s+|abstract\s+)*function\s+([A-Za-z_]\w*)/;
2995
+ var phpProfile = {
2996
+ id: "php",
2997
+ extensions: [".php"],
2998
+ summarize(_path, content) {
2999
+ const summary = emptySummary();
3000
+ eachLine(content, (line) => {
3001
+ const trimmed = line.trim();
3002
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("/*")) {
3003
+ return;
3004
+ }
3005
+ const used = USE.exec(trimmed);
3006
+ if (used) {
3007
+ summary.imports.push(used[1].replace(/\\\\/g, "\\"));
3008
+ return;
3009
+ }
3010
+ const declaration = DECLARE.exec(trimmed);
3011
+ if (declaration) {
3012
+ const kind = declaration[1];
3013
+ const name = declaration[2];
3014
+ if (kind === "interface" || kind === "trait") summary.interfaces.push(name);
3015
+ else if (kind === "enum") summary.enums.push(name);
3016
+ else summary.classes.push(name);
3017
+ summary.exports.push(name);
3018
+ return;
3019
+ }
3020
+ const fn = FUNCTION2.exec(trimmed);
3021
+ if (fn) {
3022
+ summary.functions.push(fn[1]);
3023
+ if (!/^private\b/.test(trimmed)) summary.exports.push(fn[1]);
3024
+ }
3025
+ });
3026
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
3027
+ }
3028
+ };
3029
+
3030
+ // src/ast/profiles/python.ts
3031
+ var IMPORT2 = /^import\s+(.+)$/;
3032
+ var FROM_IMPORT = /^from\s+([\w.]+)\s+import\s+(.+)$/;
3033
+ var CLASS = /^class\s+([A-Za-z_]\w*)/;
3034
+ var DEF = /^(?:async\s+)?def\s+([A-Za-z_]\w*)/;
3035
+ var CONSTANT = /^([A-Z_][A-Z0-9_]*)\s*(?::[^=]+)?=/;
3036
+ var ALL_DUNDER = /^__all__\s*=\s*\[([^\]]*)\]/;
3037
+ var ROUTE_DECORATOR = /^\s*@[\w.]*\b(?:get|post|put|patch|delete|head|options|route)\s*\(/i;
3038
+ var pythonProfile = {
3039
+ id: "python",
3040
+ extensions: [".py", ".pyi"],
3041
+ summarize(_path, content) {
3042
+ const summary = emptySummary();
3043
+ let routePending = false;
3044
+ eachLine(content, (line) => {
3045
+ const trimmed = line.trim();
3046
+ if (!trimmed || trimmed.startsWith("#")) return;
3047
+ if (ROUTE_DECORATOR.test(line)) {
3048
+ routePending = true;
3049
+ return;
3050
+ }
3051
+ const from = FROM_IMPORT.exec(trimmed);
3052
+ if (from) {
3053
+ summary.imports.push(from[1]);
3054
+ routePending = false;
3055
+ return;
3056
+ }
3057
+ const imported = IMPORT2.exec(trimmed);
3058
+ if (imported) {
3059
+ for (const part of imported[1].split(",")) {
3060
+ const name = part.trim().split(/\s+as\s+/)[0].trim();
3061
+ if (name) summary.imports.push(name);
3062
+ }
3063
+ routePending = false;
3064
+ return;
3065
+ }
3066
+ if (isTopLevel(line)) {
3067
+ const klass = CLASS.exec(line);
3068
+ if (klass) {
3069
+ summary.classes.push(klass[1]);
3070
+ if (isPublicName(klass[1])) summary.exports.push(klass[1]);
3071
+ routePending = false;
3072
+ return;
3073
+ }
3074
+ const definition = DEF.exec(line);
3075
+ if (definition) {
3076
+ const name = definition[1];
3077
+ summary.functions.push(name);
3078
+ if (routePending) {
3079
+ summary.routeHandlers.push(name);
3080
+ routePending = false;
3081
+ }
3082
+ if (isPublicName(name)) summary.exports.push(name);
3083
+ return;
3084
+ }
3085
+ const all = ALL_DUNDER.exec(trimmed);
3086
+ if (all) {
3087
+ for (const entry of all[1].split(",")) {
3088
+ const name = entry.trim().replace(/['"]/g, "").trim();
3089
+ if (name) summary.exports.push(name);
3090
+ }
3091
+ return;
3092
+ }
3093
+ const constant = CONSTANT.exec(line);
3094
+ if (constant) summary.variables.push(constant[1]);
3095
+ }
3096
+ });
3097
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
3098
+ }
3099
+ };
3100
+
3101
+ // src/ast/profiles/ruby.ts
3102
+ var REQUIRE = /^require(?:_relative)?\s+["']([^"']+)["']/;
3103
+ var CLASS_OR_MODULE = /^(?:class|module)\s+([A-Z][\w:]*)/;
3104
+ var METHOD2 = /^def\s+(?:self\.)?([A-Za-z_]\w*[?!=]?)/;
3105
+ var rubyProfile = {
3106
+ id: "ruby",
3107
+ extensions: [".rb", ".rake", ".gemspec"],
3108
+ summarize(_path, content) {
3109
+ const summary = emptySummary();
3110
+ eachLine(content, (line) => {
3111
+ const trimmed = line.trim();
3112
+ if (!trimmed || trimmed.startsWith("#")) return;
3113
+ const required = REQUIRE.exec(trimmed);
3114
+ if (required) {
3115
+ summary.imports.push(required[1]);
3116
+ return;
1544
3117
  }
1545
- const match = /^([a-zA-Z0-9_-]+):/.exec(line);
1546
- if (match) frontmatterKeys.push(match[1]);
1547
- continue;
1548
- }
1549
- const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line.trim());
1550
- if (headingMatch) {
1551
- headings.push(`${headingMatch[1]} ${headingMatch[2].trim()}`);
1552
- }
3118
+ if (!isTopLevel(line)) return;
3119
+ const declaration = CLASS_OR_MODULE.exec(line);
3120
+ if (declaration) {
3121
+ summary.classes.push(declaration[1]);
3122
+ summary.exports.push(declaration[1]);
3123
+ return;
3124
+ }
3125
+ const method = METHOD2.exec(line);
3126
+ if (method) {
3127
+ summary.functions.push(method[1]);
3128
+ summary.exports.push(method[1]);
3129
+ }
3130
+ });
3131
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
1553
3132
  }
1554
- return {
1555
- headings: headings.slice(0, 40),
1556
- frontmatterKeys: Array.from(new Set(frontmatterKeys)).sort()
1557
- };
1558
- }
3133
+ };
3134
+
3135
+ // src/ast/profiles/rust.ts
3136
+ var USE2 = /^use\s+([^;]+);/;
3137
+ var FN = /^(pub(?:\([^)]*\))?\s+)?(?:const\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+([A-Za-z_]\w*)/;
3138
+ var STRUCT = /^(pub(?:\([^)]*\))?\s+)?struct\s+([A-Za-z_]\w*)/;
3139
+ var TRAIT = /^(pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?trait\s+([A-Za-z_]\w*)/;
3140
+ var ENUM = /^(pub(?:\([^)]*\))?\s+)?enum\s+([A-Za-z_]\w*)/;
3141
+ var TYPE2 = /^(pub(?:\([^)]*\))?\s+)?type\s+([A-Za-z_]\w*)/;
3142
+ var MOD = /^(pub(?:\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)/;
3143
+ var IMPL = /^impl(?:<[^>]*>)?\s+(?:[\w:<>]+\s+for\s+)?([A-Za-z_]\w*)/;
3144
+ var rustProfile = {
3145
+ id: "rust",
3146
+ extensions: [".rs"],
3147
+ summarize(_path, content) {
3148
+ const summary = emptySummary();
3149
+ eachLine(content, (line) => {
3150
+ const trimmed = line.trim();
3151
+ if (!trimmed || trimmed.startsWith("//")) return;
3152
+ const used = USE2.exec(trimmed);
3153
+ if (used) {
3154
+ summary.imports.push(used[1].trim());
3155
+ return;
3156
+ }
3157
+ if (!isTopLevel(line)) return;
3158
+ const isPublic = /^pub\b/.test(trimmed);
3159
+ const fn = FN.exec(line);
3160
+ if (fn) {
3161
+ summary.functions.push(fn[2]);
3162
+ if (isPublic) summary.exports.push(fn[2]);
3163
+ return;
3164
+ }
3165
+ const struct = STRUCT.exec(line);
3166
+ if (struct) {
3167
+ summary.classes.push(struct[2]);
3168
+ if (isPublic) summary.exports.push(struct[2]);
3169
+ return;
3170
+ }
3171
+ const trait = TRAIT.exec(line);
3172
+ if (trait) {
3173
+ summary.interfaces.push(trait[2]);
3174
+ if (isPublic) summary.exports.push(trait[2]);
3175
+ return;
3176
+ }
3177
+ const enumeration = ENUM.exec(line);
3178
+ if (enumeration) {
3179
+ summary.enums.push(enumeration[2]);
3180
+ if (isPublic) summary.exports.push(enumeration[2]);
3181
+ return;
3182
+ }
3183
+ const alias = TYPE2.exec(line);
3184
+ if (alias) {
3185
+ summary.types.push(alias[2]);
3186
+ if (isPublic) summary.exports.push(alias[2]);
3187
+ return;
3188
+ }
3189
+ const module = MOD.exec(line);
3190
+ if (module) {
3191
+ summary.variables.push(module[2]);
3192
+ if (isPublic) summary.exports.push(module[2]);
3193
+ return;
3194
+ }
3195
+ const implementation = IMPL.exec(line);
3196
+ if (implementation) summary.classes.push(implementation[1]);
3197
+ });
3198
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
3199
+ }
3200
+ };
1559
3201
 
1560
3202
  // src/ast/ts-parser.ts
1561
3203
  import ts from "typescript";
@@ -1598,9 +3240,9 @@ function summarizeTypeScriptSource(relativePath, content) {
1598
3240
  if (parseErrors > 0) summary.parseErrors = parseErrors;
1599
3241
  for (const statement of sourceFile.statements) {
1600
3242
  if (ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)) {
1601
- const text = statement.expression.text;
1602
- if (text === "use client" || text === "use server") {
1603
- summary.directives.push(text);
3243
+ const text2 = statement.expression.text;
3244
+ if (text2 === "use client" || text2 === "use server") {
3245
+ summary.directives.push(text2);
1604
3246
  }
1605
3247
  continue;
1606
3248
  }
@@ -1700,23 +3342,212 @@ function summarizeTypeScriptSource(relativePath, content) {
1700
3342
  stack.push(children[index]);
1701
3343
  }
1702
3344
  }
1703
- const dedupeSorted = (values) => Array.from(new Set(values)).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
1704
- summary.directives = dedupeSorted(summary.directives);
1705
- summary.imports = dedupeSorted(summary.imports);
1706
- summary.exports = dedupeSorted(summary.exports);
1707
- summary.reExports = dedupeSorted(summary.reExports);
1708
- summary.functions = dedupeSorted(summary.functions);
1709
- summary.classes = dedupeSorted(summary.classes);
1710
- summary.interfaces = dedupeSorted(summary.interfaces);
1711
- summary.types = dedupeSorted(summary.types);
1712
- summary.enums = dedupeSorted(summary.enums);
1713
- summary.variables = dedupeSorted(summary.variables);
1714
- summary.reactComponents = dedupeSorted(summary.reactComponents);
1715
- summary.hooks = dedupeSorted(summary.hooks);
1716
- summary.routeHandlers = dedupeSorted(summary.routeHandlers);
3345
+ const dedupeSorted2 = (values) => Array.from(new Set(values)).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
3346
+ summary.directives = dedupeSorted2(summary.directives);
3347
+ summary.imports = dedupeSorted2(summary.imports);
3348
+ summary.exports = dedupeSorted2(summary.exports);
3349
+ summary.reExports = dedupeSorted2(summary.reExports);
3350
+ summary.functions = dedupeSorted2(summary.functions);
3351
+ summary.classes = dedupeSorted2(summary.classes);
3352
+ summary.interfaces = dedupeSorted2(summary.interfaces);
3353
+ summary.types = dedupeSorted2(summary.types);
3354
+ summary.enums = dedupeSorted2(summary.enums);
3355
+ summary.variables = dedupeSorted2(summary.variables);
3356
+ summary.reactComponents = dedupeSorted2(summary.reactComponents);
3357
+ summary.hooks = dedupeSorted2(summary.hooks);
3358
+ summary.routeHandlers = dedupeSorted2(summary.routeHandlers);
1717
3359
  return summary;
1718
3360
  }
1719
3361
 
3362
+ // src/ast/profiles/sfc.ts
3363
+ var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
3364
+ var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
3365
+ function componentName(path12) {
3366
+ const base = path12.split("/").pop() ?? path12;
3367
+ return base.replace(/\.[^.]+$/, "");
3368
+ }
3369
+ var sfcProfile = {
3370
+ id: "sfc",
3371
+ extensions: [".vue", ".svelte", ".astro"],
3372
+ summarize(path12, content) {
3373
+ let script = "";
3374
+ if (path12.toLowerCase().endsWith(".astro")) {
3375
+ const frontmatter = ASTRO_FRONTMATTER.exec(content);
3376
+ if (frontmatter) script += `${frontmatter[1]}
3377
+ `;
3378
+ }
3379
+ for (const match of content.matchAll(SCRIPT_BLOCK)) {
3380
+ script += `${match[1]}
3381
+ `;
3382
+ }
3383
+ const name = componentName(path12);
3384
+ if (!script.trim()) {
3385
+ const summary2 = emptySummary();
3386
+ if (name) {
3387
+ summary2.exports.push(name);
3388
+ summary2.reactComponents.push(name);
3389
+ }
3390
+ return { summary: finalizeSummary(summary2), quality: "heuristic" };
3391
+ }
3392
+ const summary = summarizeTypeScriptSource(`${name}.ts`, script);
3393
+ summary.reactComponents.push(name);
3394
+ summary.exports.push(name);
3395
+ return { summary, quality: "ast" };
3396
+ }
3397
+ };
3398
+
3399
+ // src/ast/profiles/index.ts
3400
+ var LANGUAGE_PROFILES = [
3401
+ pythonProfile,
3402
+ goProfile,
3403
+ rustProfile,
3404
+ jvmProfile,
3405
+ dotnetProfile,
3406
+ phpProfile,
3407
+ rubyProfile,
3408
+ cfamilyProfile,
3409
+ sfcProfile
3410
+ ];
3411
+ var BY_EXTENSION = /* @__PURE__ */ new Map();
3412
+ for (const profile of LANGUAGE_PROFILES) {
3413
+ for (const extension of profile.extensions) {
3414
+ BY_EXTENSION.set(extension.toLowerCase(), profile);
3415
+ }
3416
+ }
3417
+ function profileForPath(path12) {
3418
+ const lower = path12.toLowerCase();
3419
+ const dot = lower.lastIndexOf(".");
3420
+ if (dot === -1) return null;
3421
+ return BY_EXTENSION.get(lower.slice(dot)) ?? null;
3422
+ }
3423
+ function summarizeWithProfile(path12, content) {
3424
+ const profile = profileForPath(path12);
3425
+ return profile ? profile.summarize(path12, content) : null;
3426
+ }
3427
+
3428
+ // src/ast/structured.ts
3429
+ function stripJsonComments2(content) {
3430
+ const text2 = content.replace(/^\uFEFF/, "");
3431
+ let out = "";
3432
+ let inString = false;
3433
+ let inLineComment = false;
3434
+ let inBlockComment = false;
3435
+ for (let i = 0; i < text2.length; i++) {
3436
+ const char = text2[i];
3437
+ const next = text2[i + 1];
3438
+ if (inLineComment) {
3439
+ if (char === "\n") {
3440
+ inLineComment = false;
3441
+ out += char;
3442
+ }
3443
+ continue;
3444
+ }
3445
+ if (inBlockComment) {
3446
+ if (char === "*" && next === "/") {
3447
+ inBlockComment = false;
3448
+ i += 1;
3449
+ }
3450
+ continue;
3451
+ }
3452
+ if (inString) {
3453
+ out += char;
3454
+ if (char === "\\") {
3455
+ if (next !== void 0) {
3456
+ out += next;
3457
+ i += 1;
3458
+ }
3459
+ } else if (char === '"') {
3460
+ inString = false;
3461
+ }
3462
+ continue;
3463
+ }
3464
+ if (char === '"') {
3465
+ inString = true;
3466
+ out += char;
3467
+ continue;
3468
+ }
3469
+ if (char === "/" && next === "/") {
3470
+ inLineComment = true;
3471
+ i += 1;
3472
+ continue;
3473
+ }
3474
+ if (char === "/" && next === "*") {
3475
+ inBlockComment = true;
3476
+ i += 1;
3477
+ continue;
3478
+ }
3479
+ out += char;
3480
+ }
3481
+ return out;
3482
+ }
3483
+ function summarizeJson(content) {
3484
+ try {
3485
+ const parsed = JSON.parse(stripJsonComments2(content));
3486
+ if (Array.isArray(parsed)) {
3487
+ const sample = parsed[0];
3488
+ const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
3489
+ return {
3490
+ shape: "array",
3491
+ itemCount: parsed.length,
3492
+ keys: [],
3493
+ sampleKeys
3494
+ };
3495
+ }
3496
+ if (parsed && typeof parsed === "object") {
3497
+ const keys = Object.keys(parsed).sort();
3498
+ return {
3499
+ shape: "object",
3500
+ itemCount: keys.length,
3501
+ keys: keys.slice(0, 30),
3502
+ sampleKeys: []
3503
+ };
3504
+ }
3505
+ return {
3506
+ shape: typeof parsed,
3507
+ itemCount: null,
3508
+ keys: [],
3509
+ sampleKeys: []
3510
+ };
3511
+ } catch {
3512
+ return {
3513
+ shape: "invalid-json",
3514
+ itemCount: null,
3515
+ keys: [],
3516
+ sampleKeys: []
3517
+ };
3518
+ }
3519
+ }
3520
+ function summarizeMarkdown(content) {
3521
+ const headings = [];
3522
+ const frontmatterKeys = [];
3523
+ const lines = content.replace(/^\uFEFF/, "").split("\n");
3524
+ let inFrontmatter = false;
3525
+ for (let index = 0; index < lines.length; index++) {
3526
+ const line = lines[index];
3527
+ if (index === 0 && line.trim() === "---") {
3528
+ inFrontmatter = true;
3529
+ continue;
3530
+ }
3531
+ if (inFrontmatter) {
3532
+ if (line.trim() === "---") {
3533
+ inFrontmatter = false;
3534
+ continue;
3535
+ }
3536
+ const match = /^([a-zA-Z0-9_-]+):/.exec(line);
3537
+ if (match) frontmatterKeys.push(match[1]);
3538
+ continue;
3539
+ }
3540
+ const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line.trim());
3541
+ if (headingMatch) {
3542
+ headings.push(`${headingMatch[1]} ${headingMatch[2].trim()}`);
3543
+ }
3544
+ }
3545
+ return {
3546
+ headings: headings.slice(0, 40),
3547
+ frontmatterKeys: Array.from(new Set(frontmatterKeys)).sort()
3548
+ };
3549
+ }
3550
+
1720
3551
  // src/generator/audit.ts
1721
3552
  function generateAuditReport(manifest, secretFindings) {
1722
3553
  const lines = [
@@ -1782,11 +3613,11 @@ function generateAuditReport(manifest, secretFindings) {
1782
3613
  "| :--- | :--- | :--- |"
1783
3614
  );
1784
3615
  const totalFiles = manifest.counts.totalFiles || 1;
1785
- for (const [layer, count] of Object.entries(manifest.layers).sort(
3616
+ for (const [layer2, count] of Object.entries(manifest.layers).sort(
1786
3617
  (a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
1787
3618
  )) {
1788
3619
  const pct = (count / totalFiles * 100).toFixed(1);
1789
- lines.push(`| \`${layer}\` | ${count} | ${pct}% |`);
3620
+ lines.push(`| \`${layer2}\` | ${count} | ${pct}% |`);
1790
3621
  }
1791
3622
  lines.push("");
1792
3623
  lines.push(
@@ -1855,21 +3686,21 @@ function partitionFiles(files, maxPartBytes, contentFingerprint) {
1855
3686
  section.byteSize = Buffer.byteLength(section.text, "utf8");
1856
3687
  continue;
1857
3688
  }
1858
- const marker = "\n... [part truncated: cannot fit budget] ...\n";
1859
- const target = Math.max(0, budget - Buffer.byteLength(marker, "utf8"));
3689
+ const marker2 = "\n... [part truncated: cannot fit budget] ...\n";
3690
+ const target = Math.max(0, budget - Buffer.byteLength(marker2, "utf8"));
1860
3691
  const clipped = truncateToBytes(rendered[oversized].markdown, target);
1861
- rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker);
3692
+ rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker2);
1862
3693
  return rendered;
1863
3694
  }
1864
3695
  return enforceBudget(renderGroups(groups, contentFingerprint), budget);
1865
3696
  }
1866
3697
  function enforceBudget(parts, budget) {
1867
- const marker = "\n... [part truncated: cannot fit budget] ...\n";
1868
- const target = Math.max(0, budget - Buffer.byteLength(marker, "utf8"));
3698
+ const marker2 = "\n... [part truncated: cannot fit budget] ...\n";
3699
+ const target = Math.max(0, budget - Buffer.byteLength(marker2, "utf8"));
1869
3700
  return parts.map((part) => {
1870
3701
  if (part.metadata.byteSize <= budget) return part;
1871
3702
  const clipped = truncateToBytes(part.markdown, target);
1872
- return rehashPart(part, clipped.content + marker);
3703
+ return rehashPart(part, clipped.content + marker2);
1873
3704
  });
1874
3705
  }
1875
3706
  function renumberParts(parts, contentFingerprint) {
@@ -1877,10 +3708,10 @@ function renumberParts(parts, contentFingerprint) {
1877
3708
  if (total === 0) return [];
1878
3709
  const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
1879
3710
  const nameFor = (position) => `part-${zeroPad(position)}.md`;
1880
- const marker = "## Contents\n";
3711
+ const marker2 = "## Contents\n";
1881
3712
  return parts.map((part, index) => {
1882
- const markerIndex = part.markdown.indexOf(marker);
1883
- const body = markerIndex === -1 ? "" : part.markdown.slice(markerIndex + marker.length);
3713
+ const markerIndex = part.markdown.indexOf(marker2);
3714
+ const body = markerIndex === -1 ? "" : part.markdown.slice(markerIndex + marker2.length);
1884
3715
  const included = part.metadata.includedFiles;
1885
3716
  if (markerIndex === -1) {
1886
3717
  throw new Error(`${part.filename} has no "## Contents" section to re-number.`);
@@ -1938,9 +3769,9 @@ function topEntries(counts, limit) {
1938
3769
  return Object.entries(counts).sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)).slice(0, limit).map(([key, value]) => `${capMetadata(key)} (${value})`).join(", ");
1939
3770
  }
1940
3771
  function prepareSection(file, sectionBudget) {
1941
- let text = renderFileSection(file);
1942
- let byteSize = Buffer.byteLength(text, "utf8");
1943
- if (byteSize <= sectionBudget) return { file, text, byteSize };
3772
+ let text2 = renderFileSection(file);
3773
+ let byteSize = Buffer.byteLength(text2, "utf8");
3774
+ if (byteSize <= sectionBudget) return { file, text: text2, byteSize };
1944
3775
  const shrinkBody = () => {
1945
3776
  const content = file.content ?? "";
1946
3777
  const overhead = byteSize - Buffer.byteLength(content, "utf8");
@@ -1949,24 +3780,24 @@ function prepareSection(file, sectionBudget) {
1949
3780
  const truncated = truncateToBytes(content, allowed);
1950
3781
  if (!truncated.isTruncated) return;
1951
3782
  applyTruncatedContent(file, truncated.content);
1952
- text = renderFileSection(file);
1953
- byteSize = Buffer.byteLength(text, "utf8");
3783
+ text2 = renderFileSection(file);
3784
+ byteSize = Buffer.byteLength(text2, "utf8");
1954
3785
  };
1955
3786
  shrinkBody();
1956
3787
  if (byteSize > sectionBudget && file.summary) {
1957
3788
  file.summary = void 0;
1958
3789
  file.summaryOmitted = true;
1959
- text = renderFileSection(file);
1960
- byteSize = Buffer.byteLength(text, "utf8");
3790
+ text2 = renderFileSection(file);
3791
+ byteSize = Buffer.byteLength(text2, "utf8");
1961
3792
  shrinkBody();
1962
3793
  }
1963
3794
  if (byteSize > sectionBudget) {
1964
3795
  const target = sectionBudget - Buffer.byteLength(SECTION_TRUNCATION_NOTICE, "utf8");
1965
- const truncated = truncateToBytes(text, Math.max(0, target));
1966
- text = truncated.content + SECTION_TRUNCATION_NOTICE;
1967
- byteSize = Buffer.byteLength(text, "utf8");
3796
+ const truncated = truncateToBytes(text2, Math.max(0, target));
3797
+ text2 = truncated.content + SECTION_TRUNCATION_NOTICE;
3798
+ byteSize = Buffer.byteLength(text2, "utf8");
1968
3799
  }
1969
- return { file, text, byteSize };
3800
+ return { file, text: text2, byteSize };
1970
3801
  }
1971
3802
  function applyTruncatedContent(file, content) {
1972
3803
  file.content = content;
@@ -2088,6 +3919,7 @@ function renderFileSection(file) {
2088
3919
  `- Source Lines: ${file.originalLines}`,
2089
3920
  `- Included Lines: ${file.includedLines}`,
2090
3921
  `- SHA-256: ${file.hashSha256}`,
3922
+ ...file.summaryQuality ? [`- Summary Quality: ${file.summaryQuality}`] : [],
2091
3923
  ...renderSummary(file.summary),
2092
3924
  ...file.summaryOmitted ? ["- Summary: (omitted: exceeds the part budget)"] : [],
2093
3925
  "",
@@ -2298,8 +4130,8 @@ function generateProjectTree(filePaths) {
2298
4130
  }
2299
4131
 
2300
4132
  // src/scanner/binary.ts
2301
- import * as fs6 from "fs";
2302
- import * as path7 from "path";
4133
+ import * as fs7 from "fs";
4134
+ import * as path8 from "path";
2303
4135
  var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2304
4136
  ".png",
2305
4137
  ".jpg",
@@ -2339,7 +4171,7 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2339
4171
  ".so"
2340
4172
  ]);
2341
4173
  function isBinaryExtension(filePath) {
2342
- const ext = path7.extname(filePath).toLowerCase();
4174
+ const ext = path8.extname(filePath).toLowerCase();
2343
4175
  return BINARY_EXTENSIONS.has(ext);
2344
4176
  }
2345
4177
  function isBinaryBuffer(buffer) {
@@ -2353,9 +4185,9 @@ function isBinaryFile(absolutePath) {
2353
4185
  if (isBinaryExtension(absolutePath)) return true;
2354
4186
  let fd;
2355
4187
  try {
2356
- fd = fs6.openSync(absolutePath, "r");
4188
+ fd = fs7.openSync(absolutePath, "r");
2357
4189
  const buffer = Buffer.alloc(8192);
2358
- const bytesRead = fs6.readSync(fd, buffer, 0, 8192, 0);
4190
+ const bytesRead = fs7.readSync(fd, buffer, 0, 8192, 0);
2359
4191
  if (bytesRead === 0) return false;
2360
4192
  return isBinaryBuffer(buffer.subarray(0, bytesRead));
2361
4193
  } catch {
@@ -2363,7 +4195,7 @@ function isBinaryFile(absolutePath) {
2363
4195
  } finally {
2364
4196
  if (fd !== void 0) {
2365
4197
  try {
2366
- fs6.closeSync(fd);
4198
+ fs7.closeSync(fd);
2367
4199
  } catch {
2368
4200
  }
2369
4201
  }
@@ -2372,8 +4204,8 @@ function isBinaryFile(absolutePath) {
2372
4204
 
2373
4205
  // src/validator/validate.ts
2374
4206
  import * as crypto2 from "crypto";
2375
- import * as fs7 from "fs";
2376
- import * as path8 from "path";
4207
+ import * as fs8 from "fs";
4208
+ import * as path9 from "path";
2377
4209
  function emptyDrift() {
2378
4210
  return {
2379
4211
  missingInSnapshot: [],
@@ -2390,8 +4222,8 @@ function sha256Hex(buffer) {
2390
4222
  function validateSnapshot(configInput) {
2391
4223
  assertUsableConfigInput(configInput);
2392
4224
  const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
2393
- const manifestPath = path8.join(config.cwd, config.outDir, "manifest.json");
2394
- if (!fs7.existsSync(manifestPath)) {
4225
+ const manifestPath = path9.resolve(config.cwd, config.outDir, "manifest.json");
4226
+ if (!fs8.existsSync(manifestPath)) {
2395
4227
  return {
2396
4228
  valid: false,
2397
4229
  manifestPresent: false,
@@ -2402,7 +4234,7 @@ function validateSnapshot(configInput) {
2402
4234
  }
2403
4235
  let manifest;
2404
4236
  try {
2405
- manifest = JSON.parse(fs7.readFileSync(manifestPath, "utf8"));
4237
+ manifest = JSON.parse(fs8.readFileSync(manifestPath, "utf8"));
2406
4238
  } catch (err) {
2407
4239
  return {
2408
4240
  valid: false,
@@ -2425,7 +4257,7 @@ function validateSnapshot(configInput) {
2425
4257
  }
2426
4258
  const drift = emptyDrift();
2427
4259
  const warnings = [];
2428
- const snapshotDir = path8.join(config.cwd, config.outDir);
4260
+ const snapshotDir = path9.resolve(config.cwd, config.outDir);
2429
4261
  const manifestFileMap = /* @__PURE__ */ new Map();
2430
4262
  for (const file of manifest.files) {
2431
4263
  if (file && typeof file.relativePath === "string") {
@@ -2437,7 +4269,7 @@ function validateSnapshot(configInput) {
2437
4269
  );
2438
4270
  const collection = collectFiles(config.cwd, {
2439
4271
  includePatterns: config.include,
2440
- excludePatterns: config.excludePatterns,
4272
+ excludePatterns: [...config.excludePatterns, ...config.presetExclude],
2441
4273
  includeSensitiveFiles: config.includeSensitiveFiles
2442
4274
  });
2443
4275
  for (const diskFile of collection.files) {
@@ -2449,7 +4281,7 @@ function validateSnapshot(configInput) {
2449
4281
  }
2450
4282
  let diskHash;
2451
4283
  try {
2452
- diskHash = sha256Hex(fs7.readFileSync(path8.join(config.cwd, diskFile)));
4284
+ diskHash = sha256Hex(fs8.readFileSync(path9.join(config.cwd, diskFile)));
2453
4285
  } catch {
2454
4286
  drift.missingOnDisk.push(diskFile);
2455
4287
  continue;
@@ -2459,7 +4291,7 @@ function validateSnapshot(configInput) {
2459
4291
  }
2460
4292
  }
2461
4293
  for (const snapshotFile of manifestFileMap.keys()) {
2462
- if (!fs7.existsSync(path8.join(config.cwd, snapshotFile))) {
4294
+ if (!fs8.existsSync(path9.join(config.cwd, snapshotFile))) {
2463
4295
  drift.missingOnDisk.push(snapshotFile);
2464
4296
  }
2465
4297
  }
@@ -2473,10 +4305,10 @@ function validateSnapshot(configInput) {
2473
4305
  if (!part || typeof part.filename !== "string") continue;
2474
4306
  partNames.add(part.filename);
2475
4307
  if (!config.artifacts.parts) continue;
2476
- const partPath = path8.join(snapshotDir, "parts", part.filename);
4308
+ const partPath = path9.join(snapshotDir, "parts", part.filename);
2477
4309
  let buffer;
2478
4310
  try {
2479
- buffer = fs7.readFileSync(partPath);
4311
+ buffer = fs8.readFileSync(partPath);
2480
4312
  } catch {
2481
4313
  drift.partIssues.push({ part: part.filename, issue: "missing from snapshot/parts" });
2482
4314
  continue;
@@ -2563,6 +4395,11 @@ function validateSnapshot(configInput) {
2563
4395
  }
2564
4396
  }
2565
4397
  }
4398
+ if (recorded?.preset !== void 0 && recorded.preset !== config.preset) {
4399
+ drift.configMismatches.push(
4400
+ `preset: manifest ${recorded.preset} vs current ${config.preset}`
4401
+ );
4402
+ }
2566
4403
  if (recorded?.layersMode !== void 0 && recorded.layersMode !== config.layersMode) {
2567
4404
  drift.configMismatches.push(
2568
4405
  `layersMode: manifest ${recorded.layersMode} vs current ${config.layersMode}`
@@ -2580,10 +4417,10 @@ function validateSnapshot(configInput) {
2580
4417
  ...Object.keys(recordedByLayer),
2581
4418
  ...Object.keys(config.budgets.byLayer)
2582
4419
  ]);
2583
- for (const layer of Array.from(layerNames).sort()) {
2584
- if (recordedByLayer[layer] !== config.budgets.byLayer[layer]) {
4420
+ for (const layer2 of Array.from(layerNames).sort()) {
4421
+ if (recordedByLayer[layer2] !== config.budgets.byLayer[layer2]) {
2585
4422
  drift.configMismatches.push(
2586
- `budgets.byLayer.${layer}: manifest ${recordedByLayer[layer]} vs current ${config.budgets.byLayer[layer]}`
4423
+ `budgets.byLayer.${layer2}: manifest ${recordedByLayer[layer2]} vs current ${config.budgets.byLayer[layer2]}`
2587
4424
  );
2588
4425
  }
2589
4426
  }
@@ -2666,9 +4503,9 @@ function validateSnapshot(configInput) {
2666
4503
  ["full-codebase.md", manifest.artifacts?.fullMarkdown]
2667
4504
  ]) {
2668
4505
  if (!artifact) continue;
2669
- const artifactPath = path8.join(snapshotDir, key);
4506
+ const artifactPath = path9.join(snapshotDir, key);
2670
4507
  try {
2671
- const buffer = fs7.readFileSync(artifactPath);
4508
+ const buffer = fs8.readFileSync(artifactPath);
2672
4509
  if (sha256Hex(buffer) !== artifact.sha256 || buffer.length !== artifact.byteSize) {
2673
4510
  drift.partIssues.push({ part: key, issue: "artifact does not match its recorded digest" });
2674
4511
  }
@@ -2717,12 +4554,12 @@ function validateSnapshot(configInput) {
2717
4554
 
2718
4555
  // src/index.ts
2719
4556
  var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
2720
- var GENERATOR_VERSION = "2.0.0";
4557
+ var GENERATOR_VERSION = "2.1.0";
2721
4558
  var SCHEMA_VERSION = "3.0.0";
2722
4559
  function sha256Hex2(input) {
2723
4560
  return crypto3.createHash("sha256").update(input).digest("hex");
2724
4561
  }
2725
- var SUMMARY_EXTENSIONS = [
4562
+ var TS_EXTENSIONS = [
2726
4563
  ".ts",
2727
4564
  ".mts",
2728
4565
  ".cts",
@@ -2730,24 +4567,21 @@ var SUMMARY_EXTENSIONS = [
2730
4567
  ".js",
2731
4568
  ".mjs",
2732
4569
  ".cjs",
2733
- ".jsx",
2734
- ".json",
2735
- ".jsonc",
2736
- ".md",
2737
- ".mdx"
4570
+ ".jsx"
2738
4571
  ];
2739
4572
  function summarizeFile(relativePath, redactedContent) {
2740
4573
  const lower = relativePath.toLowerCase();
2741
- if (!SUMMARY_EXTENSIONS.some((extension) => lower.endsWith(extension))) {
2742
- return void 0;
2743
- }
2744
4574
  if (lower.endsWith(".json") || lower.endsWith(".jsonc")) {
2745
- return summarizeJson(redactedContent);
4575
+ return { summary: summarizeJson(redactedContent), quality: "structural" };
2746
4576
  }
2747
4577
  if (lower.endsWith(".md") || lower.endsWith(".mdx")) {
2748
- return summarizeMarkdown(redactedContent);
4578
+ return { summary: summarizeMarkdown(redactedContent), quality: "structural" };
4579
+ }
4580
+ if (TS_EXTENSIONS.some((extension) => lower.endsWith(extension))) {
4581
+ return { summary: summarizeTypeScriptSource(relativePath, redactedContent), quality: "ast" };
2749
4582
  }
2750
- return summarizeTypeScriptSource(relativePath, redactedContent);
4583
+ const profiled = summarizeWithProfile(relativePath, redactedContent);
4584
+ return profiled ? { summary: profiled.summary, quality: profiled.quality } : void 0;
2751
4585
  }
2752
4586
  function isSourceSummary(summary) {
2753
4587
  return Boolean(summary && typeof summary === "object" && "imports" in summary);
@@ -2773,8 +4607,8 @@ var IMPORT_CANDIDATE_SUFFIXES = [
2773
4607
  ];
2774
4608
  function resolveSpecifier(fromFile, specifier, knownFiles) {
2775
4609
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
2776
- const baseDir = path9.posix.dirname(fromFile);
2777
- const joined = path9.posix.normalize(path9.posix.join(baseDir, specifier));
4610
+ const baseDir = path10.posix.dirname(fromFile);
4611
+ const joined = path10.posix.normalize(path10.posix.join(baseDir, specifier));
2778
4612
  const candidate = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
2779
4613
  for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
2780
4614
  const withSuffix = `${joined}${suffix}`;
@@ -2866,11 +4700,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
2866
4700
  return errors;
2867
4701
  }
2868
4702
  function resolveSafeOutputDirs(config) {
2869
- const cwd = path9.resolve(config.cwd);
2870
- const outDir = path9.resolve(cwd, config.outDir);
2871
- const tmpDir = path9.resolve(cwd, config.tmpDir);
2872
- const root = path9.parse(cwd).root;
2873
- const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path9.sep}`);
4703
+ const cwd = path10.resolve(config.cwd);
4704
+ const outDir = path10.resolve(cwd, config.outDir);
4705
+ const tmpDir = path10.resolve(cwd, config.tmpDir);
4706
+ const root = path10.parse(cwd).root;
4707
+ const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path10.sep}`);
2874
4708
  const reject = (reason) => {
2875
4709
  throw new Error(
2876
4710
  `Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
@@ -2889,26 +4723,66 @@ function resolveSafeOutputDirs(config) {
2889
4723
  if (contains(tmpDir, outDir)) reject("outDir is inside tmpDir");
2890
4724
  return { outDir, tmpDir };
2891
4725
  }
4726
+ var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
4727
+ var OWNERSHIP_MARKER = ".codebase-snapshot";
4728
+ function writeOwnershipMarker(dir) {
4729
+ const marker2 = {
4730
+ generator: GENERATOR_NAME,
4731
+ version: GENERATOR_VERSION,
4732
+ schemaVersion: SCHEMA_VERSION
4733
+ };
4734
+ fs9.writeFileSync(path10.join(dir, OWNERSHIP_MARKER), `${JSON.stringify(marker2, null, 2)}
4735
+ `, "utf8");
4736
+ }
4737
+ function isOwnOutput(dir) {
4738
+ if (fs9.existsSync(path10.join(dir, OWNERSHIP_MARKER))) return true;
4739
+ const manifestPath = path10.join(dir, "manifest.json");
4740
+ if (!fs9.existsSync(manifestPath)) return false;
4741
+ try {
4742
+ const parsed = JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
4743
+ const generator = parsed.generator;
4744
+ return typeof parsed.schemaVersion === "string" && Array.isArray(parsed.files) && typeof generator === "object" && generator !== null && generator.name === GENERATOR_NAME;
4745
+ } catch {
4746
+ return false;
4747
+ }
4748
+ }
4749
+ function assertReplaceable(target, label) {
4750
+ if (!fs9.existsSync(target)) return;
4751
+ if (!fs9.statSync(target).isDirectory()) {
4752
+ throw new Error(
4753
+ `Refusing to write into ${label} ${target}: it exists but is not a directory.`
4754
+ );
4755
+ }
4756
+ const entries = fs9.readdirSync(target);
4757
+ const meaningful = entries.filter((name) => !PLACEHOLDER_ENTRIES.has(name));
4758
+ if (meaningful.length === 0) return;
4759
+ if (isOwnOutput(target)) return;
4760
+ const preview = meaningful.slice(0, 5).join(", ");
4761
+ const more = meaningful.length > 5 ? `, and ${meaningful.length - 5} more` : "";
4762
+ throw new Error(
4763
+ `Refusing to replace ${label} ${target}: it exists, is not empty, and does not look like a snapshot this generator wrote (found ${preview}${more}). Point ${label} at a new or empty directory, or delete it yourself first.`
4764
+ );
4765
+ }
2892
4766
  function swapIntoPlace(tmpDir, outDir) {
2893
4767
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
2894
- const hadPrevious = fs8.existsSync(outDir);
2895
- if (hadPrevious) fs8.renameSync(outDir, backupDir);
4768
+ const hadPrevious = fs9.existsSync(outDir);
4769
+ if (hadPrevious) fs9.renameSync(outDir, backupDir);
2896
4770
  try {
2897
4771
  try {
2898
- fs8.renameSync(tmpDir, outDir);
4772
+ fs9.renameSync(tmpDir, outDir);
2899
4773
  } catch (err) {
2900
4774
  if (err.code === "EXDEV") {
2901
- fs8.cpSync(tmpDir, outDir, { recursive: true });
2902
- fs8.rmSync(tmpDir, { recursive: true, force: true });
4775
+ fs9.cpSync(tmpDir, outDir, { recursive: true });
4776
+ fs9.rmSync(tmpDir, { recursive: true, force: true });
2903
4777
  } else {
2904
4778
  throw err;
2905
4779
  }
2906
4780
  }
2907
- if (hadPrevious) fs8.rmSync(backupDir, { recursive: true, force: true });
4781
+ if (hadPrevious) fs9.rmSync(backupDir, { recursive: true, force: true });
2908
4782
  } catch (err) {
2909
- if (hadPrevious && fs8.existsSync(backupDir)) {
2910
- if (fs8.existsSync(outDir)) fs8.rmSync(outDir, { recursive: true, force: true });
2911
- fs8.renameSync(backupDir, outDir);
4783
+ if (hadPrevious && fs9.existsSync(backupDir)) {
4784
+ if (fs9.existsSync(outDir)) fs9.rmSync(outDir, { recursive: true, force: true });
4785
+ fs9.renameSync(backupDir, outDir);
2912
4786
  }
2913
4787
  throw err;
2914
4788
  }
@@ -2972,7 +4846,7 @@ function generateSnapshot(configInput, options) {
2972
4846
  const gitInfo = getGitInfo(config.cwd, config.branch);
2973
4847
  const collection = collectFiles(config.cwd, {
2974
4848
  includePatterns: config.include,
2975
- excludePatterns: config.excludePatterns,
4849
+ excludePatterns: [...config.excludePatterns, ...config.presetExclude],
2976
4850
  includeSensitiveFiles: config.includeSensitiveFiles
2977
4851
  });
2978
4852
  const files = [];
@@ -2985,10 +4859,10 @@ function generateSnapshot(configInput, options) {
2985
4859
  let totalSizeBytes = 0;
2986
4860
  let largeFileCount = 0;
2987
4861
  for (const relativePath of collection.files) {
2988
- const absolutePath = path9.join(config.cwd, relativePath);
4862
+ const absolutePath = path10.join(config.cwd, relativePath);
2989
4863
  let stats;
2990
4864
  try {
2991
- stats = fs8.statSync(absolutePath);
4865
+ stats = fs9.statSync(absolutePath);
2992
4866
  if (!stats.isFile()) {
2993
4867
  skipped.push({ path: relativePath, reason: "not-file" });
2994
4868
  continue;
@@ -3003,7 +4877,7 @@ function generateSnapshot(configInput, options) {
3003
4877
  }
3004
4878
  let buffer;
3005
4879
  try {
3006
- buffer = fs8.readFileSync(absolutePath);
4880
+ buffer = fs9.readFileSync(absolutePath);
3007
4881
  } catch {
3008
4882
  skipped.push({ path: relativePath, reason: "read-error" });
3009
4883
  continue;
@@ -3013,10 +4887,10 @@ function generateSnapshot(configInput, options) {
3013
4887
  const lineCount = countLines(rawContent);
3014
4888
  totalLines += lineCount;
3015
4889
  totalSizeBytes += stats.size;
3016
- const layer = classifyLayer(relativePath, config.layers);
4890
+ const layer2 = classifyLayer(relativePath, config.layers);
3017
4891
  const kind = classifyKind(relativePath, config.kinds);
3018
4892
  const language = detectLanguage(relativePath);
3019
- bump(layerCounts, layer);
4893
+ bump(layerCounts, layer2);
3020
4894
  bump(kindCounts, kind);
3021
4895
  const { redacted, findings } = scanAndRedactSecrets(
3022
4896
  rawContent,
@@ -3040,8 +4914,11 @@ function generateSnapshot(configInput, options) {
3040
4914
  includedLines = preview.includedLines;
3041
4915
  }
3042
4916
  let summary;
4917
+ let summaryQuality;
3043
4918
  try {
3044
- summary = summarizeFile(relativePath, redacted);
4919
+ const outcome = summarizeFile(relativePath, redacted);
4920
+ summary = outcome?.summary;
4921
+ summaryQuality = outcome?.quality;
3045
4922
  if (isSourceSummary(summary) && summary.parseErrors) {
3046
4923
  warnings.push(
3047
4924
  `Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
@@ -3058,7 +4935,7 @@ function generateSnapshot(configInput, options) {
3058
4935
  lines: lineCount,
3059
4936
  hashSha256,
3060
4937
  part: "",
3061
- layer,
4938
+ layer: layer2,
3062
4939
  kind,
3063
4940
  language,
3064
4941
  inclusionMode: isPreviewTruncated ? "preview" : "full",
@@ -3068,6 +4945,7 @@ function generateSnapshot(configInput, options) {
3068
4945
  includedLines,
3069
4946
  includedBytes: Buffer.byteLength(finalContent, "utf8"),
3070
4947
  summary,
4948
+ summaryQuality,
3071
4949
  content: finalContent
3072
4950
  });
3073
4951
  }
@@ -3108,10 +4986,11 @@ function generateSnapshot(configInput, options) {
3108
4986
  includeSensitiveFiles: config.includeSensitiveFiles,
3109
4987
  artifacts: { ...config.artifacts },
3110
4988
  include: [...config.include],
4989
+ preset: config.preset,
3111
4990
  layersMode: config.layersMode,
3112
4991
  budgets: { default: config.budgets.default, byLayer: { ...config.budgets.byLayer } },
3113
4992
  excludePatterns: [...config.excludePatterns],
3114
- layers: config.layers.map((layer) => layer.name),
4993
+ layers: config.layers.map((layer2) => layer2.name),
3115
4994
  kinds: config.kinds.map((kind) => kind.kind)
3116
4995
  },
3117
4996
  counts: {
@@ -3198,45 +5077,48 @@ function generateSnapshot(configInput, options) {
3198
5077
  return manifest;
3199
5078
  }
3200
5079
  const { outDir, tmpDir } = resolveSafeOutputDirs(config);
5080
+ assertReplaceable(tmpDir, "tmpDir");
5081
+ assertReplaceable(outDir, "outDir");
3201
5082
  const { artifacts } = config;
3202
- fs8.rmSync(tmpDir, { recursive: true, force: true });
3203
- fs8.mkdirSync(tmpDir, { recursive: true });
3204
- const partsDir = path9.join(tmpDir, "parts");
3205
- const reportsDir = path9.join(tmpDir, "reports");
3206
- if (artifacts.parts) fs8.mkdirSync(partsDir, { recursive: true });
5083
+ fs9.rmSync(tmpDir, { recursive: true, force: true });
5084
+ fs9.mkdirSync(tmpDir, { recursive: true });
5085
+ writeOwnershipMarker(tmpDir);
5086
+ const partsDir = path10.join(tmpDir, "parts");
5087
+ const reportsDir = path10.join(tmpDir, "reports");
5088
+ if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
3207
5089
  if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
3208
- fs8.mkdirSync(reportsDir, { recursive: true });
5090
+ fs9.mkdirSync(reportsDir, { recursive: true });
3209
5091
  }
3210
5092
  if (artifacts.parts) {
3211
5093
  for (const part of parts) {
3212
- fs8.writeFileSync(path9.join(partsDir, part.filename), part.markdown, "utf8");
5094
+ fs9.writeFileSync(path10.join(partsDir, part.filename), part.markdown, "utf8");
3213
5095
  }
3214
5096
  }
3215
5097
  if (artifacts.projectTree) {
3216
5098
  const projectTree = generateProjectTree(files.map((file) => file.relativePath));
3217
- fs8.writeFileSync(path9.join(reportsDir, "project-tree.md"), projectTree, "utf8");
5099
+ fs9.writeFileSync(path10.join(reportsDir, "project-tree.md"), projectTree, "utf8");
3218
5100
  }
3219
5101
  if (artifacts.navigation) {
3220
- fs8.writeFileSync(path9.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
5102
+ fs9.writeFileSync(path10.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
3221
5103
  }
3222
5104
  if (artifacts.audit) {
3223
- fs8.writeFileSync(
3224
- path9.join(reportsDir, "audit.md"),
5105
+ fs9.writeFileSync(
5106
+ path10.join(reportsDir, "audit.md"),
3225
5107
  generateAuditReport(manifest, secretFindings),
3226
5108
  "utf8"
3227
5109
  );
3228
5110
  }
3229
5111
  if (artifacts.readme) {
3230
- fs8.writeFileSync(path9.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
5112
+ fs9.writeFileSync(path10.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
3231
5113
  }
3232
5114
  if (artifacts.manifest) {
3233
- fs8.writeFileSync(path9.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
5115
+ fs9.writeFileSync(path10.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
3234
5116
  }
3235
5117
  if (monolithJson !== null) {
3236
- fs8.writeFileSync(path9.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
5118
+ fs9.writeFileSync(path10.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
3237
5119
  }
3238
5120
  if (monolithMarkdown !== null) {
3239
- fs8.writeFileSync(path9.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
5121
+ fs9.writeFileSync(path10.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
3240
5122
  }
3241
5123
  swapIntoPlace(tmpDir, outDir);
3242
5124
  runAfterGenerate(config, manifest);
@@ -3276,6 +5158,12 @@ var CLI_OPTIONS = {
3276
5158
  mode: { type: "string" },
3277
5159
  "dry-run": { type: "boolean" },
3278
5160
  watch: { type: "boolean" },
5161
+ preset: { type: "string" },
5162
+ include: { type: "string" },
5163
+ exclude: { type: "string" },
5164
+ yes: { type: "boolean" },
5165
+ minimal: { type: "boolean" },
5166
+ force: { type: "boolean" },
3279
5167
  help: { type: "boolean", short: "h" },
3280
5168
  version: { type: "boolean", short: "v" }
3281
5169
  };
@@ -3313,6 +5201,14 @@ Options:
3313
5201
  --format <text|json> CLI output format (default: text)
3314
5202
  --dry-run Compute the snapshot without writing anything
3315
5203
  --watch Re-generate whenever the repository changes
5204
+
5205
+ init only:
5206
+ --preset <name> Force a project preset instead of detecting one
5207
+ --include <globs> Comma-separated allow-list
5208
+ --exclude <globs> Comma-separated exclude patterns
5209
+ --minimal Write only the settings that differ from defaults
5210
+ --yes Accept every default without prompting
5211
+ --force Overwrite an existing config
3316
5212
  -h, --help Display this help message
3317
5213
  -v, --version Display version
3318
5214
 
@@ -3425,13 +5321,38 @@ function parseCliArgs(argv) {
3425
5321
  } else if (rawSkip !== void 0) {
3426
5322
  skipArtifacts = parseArtifactList(String(rawSkip), "--skip-artifacts");
3427
5323
  }
5324
+ if (command !== "init") {
5325
+ const initOnly = [];
5326
+ if (values.preset !== void 0) initOnly.push("--preset");
5327
+ if (values.include !== void 0) initOnly.push("--include");
5328
+ if (values.exclude !== void 0) initOnly.push("--exclude");
5329
+ if (values.yes === true) initOnly.push("--yes");
5330
+ if (values.minimal === true) initOnly.push("--minimal");
5331
+ if (values.force === true) initOnly.push("--force");
5332
+ if (initOnly.length > 0) {
5333
+ throw new UsageError(
5334
+ `${initOnly.join(", ")} ${initOnly.length === 1 ? "is" : "are"} only valid with \`init\`. To scope \`${command}\`, set include/excludePatterns/preset in snapshot.config.ts (or run \`snap init\` to generate one).`
5335
+ );
5336
+ }
5337
+ }
5338
+ if (command === "init" && values.preset !== void 0) {
5339
+ const requested = String(values.preset);
5340
+ if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
5341
+ }
3428
5342
  return {
3429
5343
  command,
3430
5344
  args,
3431
5345
  format: rawFormat,
3432
5346
  dryRun: values["dry-run"] === true,
3433
5347
  watch: values.watch === true,
3434
- cwd: values.cwd === void 0 ? process.cwd() : path10.resolve(String(values.cwd)),
5348
+ preset: values.preset === void 0 ? void 0 : String(values.preset),
5349
+ maxPartBytesRaw: values["max-part-bytes"] === void 0 ? void 0 : String(values["max-part-bytes"]),
5350
+ include: values.include === void 0 ? void 0 : String(values.include),
5351
+ exclude: values.exclude === void 0 ? void 0 : String(values.exclude),
5352
+ yes: values.yes === true,
5353
+ minimal: values.minimal === true,
5354
+ force: values.force === true,
5355
+ cwd: values.cwd === void 0 ? process.cwd() : path11.resolve(String(values.cwd)),
3435
5356
  outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
3436
5357
  includeMonolith: values["include-monolith"] === true ? true : void 0,
3437
5358
  includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
@@ -3471,32 +5392,65 @@ function emit(format, payload, textRenderer) {
3471
5392
  }
3472
5393
  textRenderer();
3473
5394
  }
3474
- function runInit(cwd) {
3475
- const targetName = supportsTypeScriptImports() ? "snapshot.config.ts" : "snapshot.config.mjs";
3476
- const targetPath = path10.join(cwd, targetName);
3477
- if (fs9.existsSync(targetPath)) {
3478
- warn(`\u274C ${targetName} already exists in ${cwd}.`);
5395
+ function splitList(value) {
5396
+ if (value === void 0) return void 0;
5397
+ return value.split(",").map((entry) => entry.trim()).filter(Boolean);
5398
+ }
5399
+ function describeManifestShapeProblem(manifest) {
5400
+ if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
5401
+ return "not a JSON object";
5402
+ }
5403
+ const candidate = manifest;
5404
+ const counts = candidate.counts;
5405
+ if (!counts || typeof counts !== "object") return "missing `counts`";
5406
+ for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
5407
+ if (typeof counts[key] !== "number") return `missing numeric \`counts.${key}\``;
5408
+ }
5409
+ if (!candidate.layers || typeof candidate.layers !== "object") return "missing `layers`";
5410
+ if (!Array.isArray(candidate.files)) return "missing `files` array";
5411
+ return null;
5412
+ }
5413
+ async function runInitCommand(invocation) {
5414
+ try {
5415
+ const result = await runInit({
5416
+ cwd: invocation.cwd,
5417
+ preset: invocation.preset,
5418
+ maxPartBytes: invocation.maxPartBytesRaw ?? invocation.maxPartBytes,
5419
+ include: splitList(invocation.include),
5420
+ exclude: splitList(invocation.exclude),
5421
+ skipArtifacts: invocation.skipArtifacts,
5422
+ includeSensitiveFiles: invocation.includeSensitiveFiles,
5423
+ yes: invocation.yes,
5424
+ minimal: invocation.minimal,
5425
+ force: invocation.force,
5426
+ dryRun: invocation.dryRun
5427
+ });
5428
+ if (invocation.format === "json") {
5429
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
5430
+ `);
5431
+ return 0;
5432
+ }
5433
+ if (!result.written) {
5434
+ info(result.configText);
5435
+ return 0;
5436
+ }
5437
+ info(`\u2705 Created ${path11.basename(result.configPath)} (preset: ${result.preset})`);
5438
+ if (result.scripts.length > 0) info(` Added npm scripts: ${result.scripts.join(", ")}`);
5439
+ if (result.gitignoreEntries.length > 0) {
5440
+ info(` Added to .gitignore: ${result.gitignoreEntries.join(", ")}`);
5441
+ }
5442
+ for (const note of result.notes) info(` \u26A0 ${note}`);
5443
+ info("");
5444
+ info(" Next: npm run snapshot");
5445
+ return 0;
5446
+ } catch (err) {
5447
+ if (err instanceof PromptCancelled) {
5448
+ warn("\u274C Cancelled.");
5449
+ return 1;
5450
+ }
5451
+ warn(`\u274C ${err.message}`);
3479
5452
  return 1;
3480
5453
  }
3481
- const importPath = JSON.stringify(GENERATOR_NAME);
3482
- const starter = `import { defineConfig } from ${importPath};
3483
-
3484
- export default defineConfig({
3485
- branch: "main",
3486
- outDir: "snapshot",
3487
- maxPartBytes: 300 * 1024,
3488
- largeFileBytes: 200 * 1024,
3489
- excludePatterns: [],
3490
- });
3491
- `;
3492
- fs9.writeFileSync(targetPath, starter, "utf8");
3493
- info(`\u2705 Created ${targetName} in ${cwd}`);
3494
- if (targetName.endsWith(".mjs")) {
3495
- info(
3496
- " (a .ts config needs Node >= 22.6 to import natively; this Node writes a .mjs instead)"
3497
- );
3498
- }
3499
- return 0;
3500
5454
  }
3501
5455
  function renderValidateText(result) {
3502
5456
  if (result.valid) {
@@ -3538,11 +5492,15 @@ ${USAGE}
3538
5492
  throw err;
3539
5493
  }
3540
5494
  if (invocation.command === "init") {
3541
- return runInit(invocation.cwd);
5495
+ return await runInitCommand(invocation);
3542
5496
  }
3543
5497
  const loaded = await loadConfigFileDetailed(invocation.cwd);
3544
5498
  for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
3545
- if (loaded.file) info(`\u2699 Using config ${path10.relative(process.cwd(), loaded.file) || loaded.file}`);
5499
+ if (loaded.errors.length > 0) {
5500
+ warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
5501
+ return 1;
5502
+ }
5503
+ if (loaded.file) info(`\u2699 Using config ${path11.relative(process.cwd(), loaded.file) || loaded.file}`);
3546
5504
  const resolved = resolveConfig(
3547
5505
  loaded.config,
3548
5506
  {
@@ -3566,18 +5524,23 @@ ${USAGE}
3566
5524
  return result.valid ? 0 : 1;
3567
5525
  }
3568
5526
  if (invocation.command === "report") {
3569
- const manifestPath = path10.join(resolved.cwd, resolved.outDir, "manifest.json");
3570
- if (!fs9.existsSync(manifestPath)) {
5527
+ const manifestPath = path11.resolve(resolved.cwd, resolved.outDir, "manifest.json");
5528
+ if (!fs10.existsSync(manifestPath)) {
3571
5529
  warn(`\u274C Manifest not found at ${manifestPath}. Run generate first.`);
3572
5530
  return 1;
3573
5531
  }
3574
5532
  let manifest;
3575
5533
  try {
3576
- manifest = JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
5534
+ manifest = JSON.parse(fs10.readFileSync(manifestPath, "utf8"));
3577
5535
  } catch (err) {
3578
5536
  warn(`\u274C Could not read ${manifestPath}: ${err.message}`);
3579
5537
  return 1;
3580
5538
  }
5539
+ const shapeError = describeManifestShapeProblem(manifest);
5540
+ if (shapeError) {
5541
+ warn(`\u274C ${manifestPath} is not a snapshot manifest: ${shapeError}`);
5542
+ return 1;
5543
+ }
3581
5544
  const validation = validateSnapshot(resolved);
3582
5545
  emit(invocation.format, { manifest, validation }, () => {
3583
5546
  info(`
@@ -3594,8 +5557,8 @@ ${USAGE}
3594
5557
  info(` - Integrity: ${validation.valid ? "\u2705 Clean (no drift)" : "\u274C Drift detected"}`);
3595
5558
  info(` - Warnings: ${manifest.warnings?.length ?? 0}`);
3596
5559
  info("\n Layers:");
3597
- for (const [layer, count] of Object.entries(manifest.layers)) {
3598
- info(` - ${layer.padEnd(16)}: ${count} files`);
5560
+ for (const [layer2, count] of Object.entries(manifest.layers)) {
5561
+ info(` - ${layer2.padEnd(16)}: ${count} files`);
3599
5562
  }
3600
5563
  });
3601
5564
  return validation.valid ? 0 : 1;
@@ -3664,10 +5627,10 @@ ${USAGE}
3664
5627
  }
3665
5628
  }
3666
5629
  async function runWatchLoop(invocation, resolved) {
3667
- const outDir = path10.resolve(resolved.cwd, resolved.outDir);
3668
- const tmpDir = path10.resolve(resolved.cwd, resolved.tmpDir);
3669
- const outName = path10.basename(outDir).toLowerCase();
3670
- const tmpName = path10.basename(tmpDir).toLowerCase();
5630
+ const outDir = path11.resolve(resolved.cwd, resolved.outDir);
5631
+ const tmpDir = path11.resolve(resolved.cwd, resolved.tmpDir);
5632
+ const outName = path11.basename(outDir).toLowerCase();
5633
+ const tmpName = path11.basename(tmpDir).toLowerCase();
3671
5634
  const isGenerated = (candidate) => candidate.toLowerCase().split(/[\\/]/).some(
3672
5635
  (segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`)
3673
5636
  );
@@ -3691,19 +5654,19 @@ async function runWatchLoop(invocation, resolved) {
3691
5654
  info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
3692
5655
  regenerate();
3693
5656
  let timer = null;
3694
- const watcher = fs9.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
5657
+ const watcher = fs10.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
3695
5658
  if (filename) {
3696
- const changed = path10.resolve(resolved.cwd, filename.toString());
5659
+ const changed = path11.resolve(resolved.cwd, filename.toString());
3697
5660
  if (isGenerated(changed)) return;
3698
5661
  }
3699
5662
  if (timer) clearTimeout(timer);
3700
5663
  timer = setTimeout(regenerate, 250);
3701
5664
  });
3702
- await new Promise((resolve5) => {
5665
+ await new Promise((resolve6) => {
3703
5666
  const stop = () => {
3704
5667
  if (timer) clearTimeout(timer);
3705
5668
  watcher.close();
3706
- resolve5();
5669
+ resolve6();
3707
5670
  };
3708
5671
  process.once("SIGINT", stop);
3709
5672
  process.once("SIGTERM", stop);