@ankurhalder/codebase-snapshot 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,134 +1,497 @@
1
1
  import * as crypto from 'crypto';
2
- import * as fs8 from 'fs';
3
- import * as path9 from 'path';
2
+ import * as fs9 from 'fs';
3
+ import * as path10 from 'path';
4
4
  import ts from 'typescript';
5
5
  import { pathToFileURL } from 'url';
6
6
  import * as childProcess from 'child_process';
7
+ import * as readline from 'readline';
7
8
 
8
9
  // src/index.ts
9
10
 
10
- // src/ast/structured.ts
11
- function stripJsonComments(content) {
12
- const text = content.replace(/^\uFEFF/, "");
13
- let out = "";
14
- let inString = false;
15
- let inLineComment = false;
16
- let inBlockComment = false;
17
- for (let i = 0; i < text.length; i++) {
18
- const char = text[i];
19
- const next = text[i + 1];
20
- if (inLineComment) {
21
- if (char === "\n") {
22
- inLineComment = false;
23
- out += char;
11
+ // src/ast/profiles/shared.ts
12
+ var MAX_SUMMARY_CHARS = 1e6;
13
+ function emptySummary() {
14
+ return {
15
+ directives: [],
16
+ imports: [],
17
+ exports: [],
18
+ reExports: [],
19
+ functions: [],
20
+ classes: [],
21
+ interfaces: [],
22
+ types: [],
23
+ enums: [],
24
+ variables: [],
25
+ reactComponents: [],
26
+ hooks: [],
27
+ routeHandlers: []
28
+ };
29
+ }
30
+ function dedupeSorted(values) {
31
+ return Array.from(new Set(values.filter(Boolean))).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
32
+ }
33
+ function finalizeSummary(summary) {
34
+ return {
35
+ directives: dedupeSorted(summary.directives),
36
+ imports: dedupeSorted(summary.imports),
37
+ exports: dedupeSorted(summary.exports),
38
+ reExports: dedupeSorted(summary.reExports),
39
+ functions: dedupeSorted(summary.functions),
40
+ classes: dedupeSorted(summary.classes),
41
+ interfaces: dedupeSorted(summary.interfaces),
42
+ types: dedupeSorted(summary.types),
43
+ enums: dedupeSorted(summary.enums),
44
+ variables: dedupeSorted(summary.variables),
45
+ reactComponents: dedupeSorted(summary.reactComponents),
46
+ hooks: dedupeSorted(summary.hooks),
47
+ routeHandlers: dedupeSorted(summary.routeHandlers)
48
+ };
49
+ }
50
+ function eachLine(content, visit) {
51
+ const text2 = content.length > MAX_SUMMARY_CHARS ? content.slice(0, MAX_SUMMARY_CHARS) : content;
52
+ for (const line of text2.split(/\r?\n/)) visit(line);
53
+ }
54
+ function isTopLevel(line) {
55
+ return !/^[\s]/.test(line);
56
+ }
57
+ function isPublicName(name) {
58
+ return name.length > 0 && !name.startsWith("_") && /^[A-Za-z]/.test(name);
59
+ }
60
+ function isCapitalizedName(name) {
61
+ return /^[A-Z]/.test(name);
62
+ }
63
+
64
+ // src/ast/profiles/cfamily.ts
65
+ var INCLUDE = /^#\s*include\s*[<"]([^>"]+)[>"]/;
66
+ var DEFINE = /^#\s*define\s+([A-Za-z_]\w*)/;
67
+ var USING = /^using\s+(?:namespace\s+)?([\w:]+)\s*;/;
68
+ var TYPE_DECL = /^(?:typedef\s+)?(?:struct|union|class|enum)\s+([A-Za-z_]\w*)/;
69
+ var TYPEDEF = /^typedef\b[^;{}]*?\b([A-Za-z_]\w*)\s*;/;
70
+ var FUNCTION = /^[A-Za-z_][\w \t*&:<>,]*?\b([A-Za-z_]\w*)\s*\([^;]*\)\s*\{?\s*$/;
71
+ var NOT_A_FUNCTION = /^(?:if|for|while|switch|return|else|do|case|sizeof|catch|throw)\b/;
72
+ var cfamilyProfile = {
73
+ id: "cfamily",
74
+ extensions: [".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hxx", ".m", ".mm"],
75
+ summarize(_path, content) {
76
+ const summary = emptySummary();
77
+ eachLine(content, (line) => {
78
+ const trimmed = line.trim();
79
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
80
+ return;
24
81
  }
25
- continue;
26
- }
27
- if (inBlockComment) {
28
- if (char === "*" && next === "/") {
29
- inBlockComment = false;
30
- i += 1;
82
+ const included = INCLUDE.exec(trimmed);
83
+ if (included) {
84
+ summary.imports.push(included[1]);
85
+ return;
31
86
  }
32
- continue;
33
- }
34
- if (inString) {
35
- out += char;
36
- if (char === "\\") {
37
- if (next !== void 0) {
38
- out += next;
39
- i += 1;
87
+ const used = USING.exec(trimmed);
88
+ if (used) {
89
+ summary.imports.push(used[1]);
90
+ return;
91
+ }
92
+ const macro = DEFINE.exec(trimmed);
93
+ if (macro) {
94
+ summary.variables.push(macro[1]);
95
+ return;
96
+ }
97
+ if (!isTopLevel(line)) return;
98
+ const declared = TYPE_DECL.exec(line);
99
+ if (declared) {
100
+ const kind = /^typedef\s+enum/.test(line) || /^enum\b/.test(trimmed) ? "enum" : "type";
101
+ if (kind === "enum") summary.enums.push(declared[1]);
102
+ else summary.classes.push(declared[1]);
103
+ summary.exports.push(declared[1]);
104
+ return;
105
+ }
106
+ const aliased = TYPEDEF.exec(line);
107
+ if (aliased) {
108
+ summary.types.push(aliased[1]);
109
+ summary.exports.push(aliased[1]);
110
+ return;
111
+ }
112
+ if (NOT_A_FUNCTION.test(trimmed)) return;
113
+ const fn = FUNCTION.exec(line);
114
+ if (fn) {
115
+ summary.functions.push(fn[1]);
116
+ summary.exports.push(fn[1]);
117
+ }
118
+ });
119
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
120
+ }
121
+ };
122
+
123
+ // src/ast/profiles/dotnet.ts
124
+ var USING2 = /^using\s+([\w.]+)\s*;/;
125
+ 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*)/;
126
+ 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*[<(]/;
127
+ var dotnetProfile = {
128
+ id: "dotnet",
129
+ extensions: [".cs"],
130
+ summarize(_path, content) {
131
+ const summary = emptySummary();
132
+ eachLine(content, (line) => {
133
+ const trimmed = line.trim();
134
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
135
+ return;
136
+ }
137
+ const used = USING2.exec(trimmed);
138
+ if (used) {
139
+ summary.imports.push(used[1]);
140
+ return;
141
+ }
142
+ const declaration = TYPE_DECL2.exec(line);
143
+ if (declaration) {
144
+ const kind = declaration[1];
145
+ const name = declaration[2];
146
+ if (kind === "interface") summary.interfaces.push(name);
147
+ else if (kind === "enum") summary.enums.push(name);
148
+ else summary.classes.push(name);
149
+ if (/^\s*public\b/.test(line)) summary.exports.push(name);
150
+ return;
151
+ }
152
+ const method = METHOD.exec(line);
153
+ if (method) {
154
+ summary.functions.push(method[1]);
155
+ if (/^\s*public\b/.test(line)) summary.exports.push(method[1]);
156
+ }
157
+ });
158
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
159
+ }
160
+ };
161
+
162
+ // src/ast/profiles/go.ts
163
+ var FUNC = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/;
164
+ var TYPE = /^type\s+([A-Za-z_]\w*)\s+(.*)$/;
165
+ var VAR_CONST = /^(?:var|const)\s+([A-Za-z_]\w*)/;
166
+ var IMPORT_SINGLE = /^import\s+(?:[\w.]+\s+)?"([^"]+)"/;
167
+ var IMPORT_OPEN = /^import\s*\(/;
168
+ var IMPORT_ENTRY = /^\s*(?:[\w.]+\s+)?"([^"]+)"/;
169
+ var goProfile = {
170
+ id: "go",
171
+ extensions: [".go"],
172
+ summarize(_path, content) {
173
+ const summary = emptySummary();
174
+ let inImportBlock = false;
175
+ eachLine(content, (line) => {
176
+ const trimmed = line.trim();
177
+ if (!trimmed || trimmed.startsWith("//")) return;
178
+ if (inImportBlock) {
179
+ if (trimmed.startsWith(")")) {
180
+ inImportBlock = false;
181
+ return;
40
182
  }
41
- } else if (char === '"') {
42
- inString = false;
183
+ const entry = IMPORT_ENTRY.exec(line);
184
+ if (entry) summary.imports.push(entry[1]);
185
+ return;
43
186
  }
44
- continue;
45
- }
46
- if (char === '"') {
47
- inString = true;
48
- out += char;
49
- continue;
50
- }
51
- if (char === "/" && next === "/") {
52
- inLineComment = true;
53
- i += 1;
54
- continue;
55
- }
56
- if (char === "/" && next === "*") {
57
- inBlockComment = true;
58
- i += 1;
59
- continue;
60
- }
61
- out += char;
187
+ if (IMPORT_OPEN.test(trimmed)) {
188
+ inImportBlock = true;
189
+ return;
190
+ }
191
+ const single = IMPORT_SINGLE.exec(trimmed);
192
+ if (single) {
193
+ summary.imports.push(single[1]);
194
+ return;
195
+ }
196
+ if (!isTopLevel(line)) return;
197
+ const method = FUNC.exec(line);
198
+ if (method) {
199
+ const name = method[1];
200
+ summary.functions.push(name);
201
+ if (isCapitalizedName(name)) summary.exports.push(name);
202
+ return;
203
+ }
204
+ const declared = TYPE.exec(line);
205
+ if (declared) {
206
+ const name = declared[1];
207
+ const body = declared[2].trim();
208
+ if (body.startsWith("struct")) summary.classes.push(name);
209
+ else if (body.startsWith("interface")) summary.interfaces.push(name);
210
+ else summary.types.push(name);
211
+ if (isCapitalizedName(name)) summary.exports.push(name);
212
+ return;
213
+ }
214
+ const value = VAR_CONST.exec(line);
215
+ if (value) {
216
+ summary.variables.push(value[1]);
217
+ if (isCapitalizedName(value[1])) summary.exports.push(value[1]);
218
+ }
219
+ });
220
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
62
221
  }
63
- return out;
64
- }
65
- function summarizeJson(content) {
66
- try {
67
- const parsed = JSON.parse(stripJsonComments(content));
68
- if (Array.isArray(parsed)) {
69
- const sample = parsed[0];
70
- const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
71
- return {
72
- shape: "array",
73
- itemCount: parsed.length,
74
- keys: [],
75
- sampleKeys
76
- };
77
- }
78
- if (parsed && typeof parsed === "object") {
79
- const keys = Object.keys(parsed).sort();
80
- return {
81
- shape: "object",
82
- itemCount: keys.length,
83
- keys: keys.slice(0, 30),
84
- sampleKeys: []
85
- };
86
- }
87
- return {
88
- shape: typeof parsed,
89
- itemCount: null,
90
- keys: [],
91
- sampleKeys: []
92
- };
93
- } catch {
94
- return {
95
- shape: "invalid-json",
96
- itemCount: null,
97
- keys: [],
98
- sampleKeys: []
99
- };
222
+ };
223
+
224
+ // src/ast/profiles/jvm.ts
225
+ var IMPORT = /^import\s+(?:static\s+)?([\w.]+?)(?:\.\*)?;?\s*$/;
226
+ 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*)/;
227
+ 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*)/;
228
+ var SCALA_DEF = /^\s*def\s+([A-Za-z_]\w*)/;
229
+ var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:abstract\s+)?[\w<>[\],.?]+\s+([A-Za-z_]\w*)\s*\(/;
230
+ var jvmProfile = {
231
+ id: "jvm",
232
+ extensions: [".java", ".kt", ".kts", ".scala"],
233
+ summarize(path11, content) {
234
+ const summary = emptySummary();
235
+ const lower = path11.toLowerCase();
236
+ const isJava = lower.endsWith(".java");
237
+ const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
238
+ eachLine(content, (line) => {
239
+ const trimmed = line.trim();
240
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
241
+ return;
242
+ }
243
+ const imported = IMPORT.exec(trimmed);
244
+ if (imported) {
245
+ summary.imports.push(imported[1]);
246
+ return;
247
+ }
248
+ const declaration = TYPE_DECL3.exec(line);
249
+ if (declaration) {
250
+ const kind = declaration[1];
251
+ const name = declaration[2];
252
+ if (kind === "interface" || kind === "trait") summary.interfaces.push(name);
253
+ else if (kind === "enum") summary.enums.push(name);
254
+ else summary.classes.push(name);
255
+ const hidden = /^\s*(?:private|protected|internal)\b/.test(line);
256
+ if (isJava ? /^\s*public\b/.test(line) : !hidden) summary.exports.push(name);
257
+ return;
258
+ }
259
+ const kotlinFunction = KOTLIN_FUN.exec(line);
260
+ if (kotlinFunction && isKotlin) {
261
+ summary.functions.push(kotlinFunction[1]);
262
+ const hidden = /^\s*(?:private|protected|internal)\b/.test(line);
263
+ if (!hidden) summary.exports.push(kotlinFunction[1]);
264
+ return;
265
+ }
266
+ const scalaFunction = SCALA_DEF.exec(line);
267
+ if (scalaFunction && !isJava && !isKotlin) {
268
+ summary.functions.push(scalaFunction[1]);
269
+ const hidden = /^\s*(?:private|protected)\b/.test(line);
270
+ if (!hidden) summary.exports.push(scalaFunction[1]);
271
+ return;
272
+ }
273
+ if (isJava) {
274
+ const method = JAVA_METHOD.exec(line);
275
+ if (method) {
276
+ summary.functions.push(method[1]);
277
+ if (/^\s*public\b/.test(line)) summary.exports.push(method[1]);
278
+ }
279
+ }
280
+ });
281
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
100
282
  }
101
- }
102
- function summarizeMarkdown(content) {
103
- const headings = [];
104
- const frontmatterKeys = [];
105
- const lines = content.replace(/^\uFEFF/, "").split("\n");
106
- let inFrontmatter = false;
107
- for (let index = 0; index < lines.length; index++) {
108
- const line = lines[index];
109
- if (index === 0 && line.trim() === "---") {
110
- inFrontmatter = true;
111
- continue;
112
- }
113
- if (inFrontmatter) {
114
- if (line.trim() === "---") {
115
- inFrontmatter = false;
116
- continue;
283
+ };
284
+
285
+ // src/ast/profiles/php.ts
286
+ var USE = /^use\s+([\w\\]+)\s*(?:as\s+\w+)?;/;
287
+ var DECLARE = /^(?:abstract\s+|final\s+|readonly\s+)*(class|interface|trait|enum)\s+([A-Za-z_]\w*)/;
288
+ var FUNCTION2 = /^(?:public\s+|protected\s+|private\s+|static\s+|final\s+|abstract\s+)*function\s+([A-Za-z_]\w*)/;
289
+ var phpProfile = {
290
+ id: "php",
291
+ extensions: [".php"],
292
+ summarize(_path, content) {
293
+ const summary = emptySummary();
294
+ eachLine(content, (line) => {
295
+ const trimmed = line.trim();
296
+ if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("/*")) {
297
+ return;
117
298
  }
118
- const match = /^([a-zA-Z0-9_-]+):/.exec(line);
119
- if (match) frontmatterKeys.push(match[1]);
120
- continue;
121
- }
122
- const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line.trim());
123
- if (headingMatch) {
124
- headings.push(`${headingMatch[1]} ${headingMatch[2].trim()}`);
125
- }
299
+ const used = USE.exec(trimmed);
300
+ if (used) {
301
+ summary.imports.push(used[1].replace(/\\\\/g, "\\"));
302
+ return;
303
+ }
304
+ const declaration = DECLARE.exec(trimmed);
305
+ if (declaration) {
306
+ const kind = declaration[1];
307
+ const name = declaration[2];
308
+ if (kind === "interface" || kind === "trait") summary.interfaces.push(name);
309
+ else if (kind === "enum") summary.enums.push(name);
310
+ else summary.classes.push(name);
311
+ summary.exports.push(name);
312
+ return;
313
+ }
314
+ const fn = FUNCTION2.exec(trimmed);
315
+ if (fn) {
316
+ summary.functions.push(fn[1]);
317
+ if (!/^private\b/.test(trimmed)) summary.exports.push(fn[1]);
318
+ }
319
+ });
320
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
126
321
  }
127
- return {
128
- headings: headings.slice(0, 40),
129
- frontmatterKeys: Array.from(new Set(frontmatterKeys)).sort()
130
- };
131
- }
322
+ };
323
+
324
+ // src/ast/profiles/python.ts
325
+ var IMPORT2 = /^import\s+(.+)$/;
326
+ var FROM_IMPORT = /^from\s+([\w.]+)\s+import\s+(.+)$/;
327
+ var CLASS = /^class\s+([A-Za-z_]\w*)/;
328
+ var DEF = /^(?:async\s+)?def\s+([A-Za-z_]\w*)/;
329
+ var CONSTANT = /^([A-Z_][A-Z0-9_]*)\s*(?::[^=]+)?=/;
330
+ var ALL_DUNDER = /^__all__\s*=\s*\[([^\]]*)\]/;
331
+ var ROUTE_DECORATOR = /^\s*@[\w.]*\b(?:get|post|put|patch|delete|head|options|route)\s*\(/i;
332
+ var pythonProfile = {
333
+ id: "python",
334
+ extensions: [".py", ".pyi"],
335
+ summarize(_path, content) {
336
+ const summary = emptySummary();
337
+ let routePending = false;
338
+ eachLine(content, (line) => {
339
+ const trimmed = line.trim();
340
+ if (!trimmed || trimmed.startsWith("#")) return;
341
+ if (ROUTE_DECORATOR.test(line)) {
342
+ routePending = true;
343
+ return;
344
+ }
345
+ const from = FROM_IMPORT.exec(trimmed);
346
+ if (from) {
347
+ summary.imports.push(from[1]);
348
+ routePending = false;
349
+ return;
350
+ }
351
+ const imported = IMPORT2.exec(trimmed);
352
+ if (imported) {
353
+ for (const part of imported[1].split(",")) {
354
+ const name = part.trim().split(/\s+as\s+/)[0].trim();
355
+ if (name) summary.imports.push(name);
356
+ }
357
+ routePending = false;
358
+ return;
359
+ }
360
+ if (isTopLevel(line)) {
361
+ const klass = CLASS.exec(line);
362
+ if (klass) {
363
+ summary.classes.push(klass[1]);
364
+ if (isPublicName(klass[1])) summary.exports.push(klass[1]);
365
+ routePending = false;
366
+ return;
367
+ }
368
+ const definition = DEF.exec(line);
369
+ if (definition) {
370
+ const name = definition[1];
371
+ summary.functions.push(name);
372
+ if (routePending) {
373
+ summary.routeHandlers.push(name);
374
+ routePending = false;
375
+ }
376
+ if (isPublicName(name)) summary.exports.push(name);
377
+ return;
378
+ }
379
+ const all = ALL_DUNDER.exec(trimmed);
380
+ if (all) {
381
+ for (const entry of all[1].split(",")) {
382
+ const name = entry.trim().replace(/['"]/g, "").trim();
383
+ if (name) summary.exports.push(name);
384
+ }
385
+ return;
386
+ }
387
+ const constant = CONSTANT.exec(line);
388
+ if (constant) summary.variables.push(constant[1]);
389
+ }
390
+ });
391
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
392
+ }
393
+ };
394
+
395
+ // src/ast/profiles/ruby.ts
396
+ var REQUIRE = /^require(?:_relative)?\s+["']([^"']+)["']/;
397
+ var CLASS_OR_MODULE = /^(?:class|module)\s+([A-Z][\w:]*)/;
398
+ var METHOD2 = /^def\s+(?:self\.)?([A-Za-z_]\w*[?!=]?)/;
399
+ var rubyProfile = {
400
+ id: "ruby",
401
+ extensions: [".rb", ".rake", ".gemspec"],
402
+ summarize(_path, content) {
403
+ const summary = emptySummary();
404
+ eachLine(content, (line) => {
405
+ const trimmed = line.trim();
406
+ if (!trimmed || trimmed.startsWith("#")) return;
407
+ const required = REQUIRE.exec(trimmed);
408
+ if (required) {
409
+ summary.imports.push(required[1]);
410
+ return;
411
+ }
412
+ if (!isTopLevel(line)) return;
413
+ const declaration = CLASS_OR_MODULE.exec(line);
414
+ if (declaration) {
415
+ summary.classes.push(declaration[1]);
416
+ summary.exports.push(declaration[1]);
417
+ return;
418
+ }
419
+ const method = METHOD2.exec(line);
420
+ if (method) {
421
+ summary.functions.push(method[1]);
422
+ summary.exports.push(method[1]);
423
+ }
424
+ });
425
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
426
+ }
427
+ };
428
+
429
+ // src/ast/profiles/rust.ts
430
+ var USE2 = /^use\s+([^;]+);/;
431
+ var FN = /^(pub(?:\([^)]*\))?\s+)?(?:const\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+([A-Za-z_]\w*)/;
432
+ var STRUCT = /^(pub(?:\([^)]*\))?\s+)?struct\s+([A-Za-z_]\w*)/;
433
+ var TRAIT = /^(pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?trait\s+([A-Za-z_]\w*)/;
434
+ var ENUM = /^(pub(?:\([^)]*\))?\s+)?enum\s+([A-Za-z_]\w*)/;
435
+ var TYPE2 = /^(pub(?:\([^)]*\))?\s+)?type\s+([A-Za-z_]\w*)/;
436
+ var MOD = /^(pub(?:\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)/;
437
+ var IMPL = /^impl(?:<[^>]*>)?\s+(?:[\w:<>]+\s+for\s+)?([A-Za-z_]\w*)/;
438
+ var rustProfile = {
439
+ id: "rust",
440
+ extensions: [".rs"],
441
+ summarize(_path, content) {
442
+ const summary = emptySummary();
443
+ eachLine(content, (line) => {
444
+ const trimmed = line.trim();
445
+ if (!trimmed || trimmed.startsWith("//")) return;
446
+ const used = USE2.exec(trimmed);
447
+ if (used) {
448
+ summary.imports.push(used[1].trim());
449
+ return;
450
+ }
451
+ if (!isTopLevel(line)) return;
452
+ const isPublic = /^pub\b/.test(trimmed);
453
+ const fn = FN.exec(line);
454
+ if (fn) {
455
+ summary.functions.push(fn[2]);
456
+ if (isPublic) summary.exports.push(fn[2]);
457
+ return;
458
+ }
459
+ const struct = STRUCT.exec(line);
460
+ if (struct) {
461
+ summary.classes.push(struct[2]);
462
+ if (isPublic) summary.exports.push(struct[2]);
463
+ return;
464
+ }
465
+ const trait = TRAIT.exec(line);
466
+ if (trait) {
467
+ summary.interfaces.push(trait[2]);
468
+ if (isPublic) summary.exports.push(trait[2]);
469
+ return;
470
+ }
471
+ const enumeration = ENUM.exec(line);
472
+ if (enumeration) {
473
+ summary.enums.push(enumeration[2]);
474
+ if (isPublic) summary.exports.push(enumeration[2]);
475
+ return;
476
+ }
477
+ const alias = TYPE2.exec(line);
478
+ if (alias) {
479
+ summary.types.push(alias[2]);
480
+ if (isPublic) summary.exports.push(alias[2]);
481
+ return;
482
+ }
483
+ const module = MOD.exec(line);
484
+ if (module) {
485
+ summary.variables.push(module[2]);
486
+ if (isPublic) summary.exports.push(module[2]);
487
+ return;
488
+ }
489
+ const implementation = IMPL.exec(line);
490
+ if (implementation) summary.classes.push(implementation[1]);
491
+ });
492
+ return { summary: finalizeSummary(summary), quality: "heuristic" };
493
+ }
494
+ };
132
495
  var ROUTE_HANDLER_NAMES = /* @__PURE__ */ new Set([
133
496
  "GET",
134
497
  "POST",
@@ -168,9 +531,9 @@ function summarizeTypeScriptSource(relativePath, content) {
168
531
  if (parseErrors > 0) summary.parseErrors = parseErrors;
169
532
  for (const statement of sourceFile.statements) {
170
533
  if (ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)) {
171
- const text = statement.expression.text;
172
- if (text === "use client" || text === "use server") {
173
- summary.directives.push(text);
534
+ const text2 = statement.expression.text;
535
+ if (text2 === "use client" || text2 === "use server") {
536
+ summary.directives.push(text2);
174
537
  }
175
538
  continue;
176
539
  }
@@ -270,23 +633,344 @@ function summarizeTypeScriptSource(relativePath, content) {
270
633
  stack.push(children[index]);
271
634
  }
272
635
  }
273
- const dedupeSorted = (values) => Array.from(new Set(values)).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
274
- summary.directives = dedupeSorted(summary.directives);
275
- summary.imports = dedupeSorted(summary.imports);
276
- summary.exports = dedupeSorted(summary.exports);
277
- summary.reExports = dedupeSorted(summary.reExports);
278
- summary.functions = dedupeSorted(summary.functions);
279
- summary.classes = dedupeSorted(summary.classes);
280
- summary.interfaces = dedupeSorted(summary.interfaces);
281
- summary.types = dedupeSorted(summary.types);
282
- summary.enums = dedupeSorted(summary.enums);
283
- summary.variables = dedupeSorted(summary.variables);
284
- summary.reactComponents = dedupeSorted(summary.reactComponents);
285
- summary.hooks = dedupeSorted(summary.hooks);
286
- summary.routeHandlers = dedupeSorted(summary.routeHandlers);
636
+ const dedupeSorted2 = (values) => Array.from(new Set(values)).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
637
+ summary.directives = dedupeSorted2(summary.directives);
638
+ summary.imports = dedupeSorted2(summary.imports);
639
+ summary.exports = dedupeSorted2(summary.exports);
640
+ summary.reExports = dedupeSorted2(summary.reExports);
641
+ summary.functions = dedupeSorted2(summary.functions);
642
+ summary.classes = dedupeSorted2(summary.classes);
643
+ summary.interfaces = dedupeSorted2(summary.interfaces);
644
+ summary.types = dedupeSorted2(summary.types);
645
+ summary.enums = dedupeSorted2(summary.enums);
646
+ summary.variables = dedupeSorted2(summary.variables);
647
+ summary.reactComponents = dedupeSorted2(summary.reactComponents);
648
+ summary.hooks = dedupeSorted2(summary.hooks);
649
+ summary.routeHandlers = dedupeSorted2(summary.routeHandlers);
287
650
  return summary;
288
651
  }
289
652
 
653
+ // src/ast/profiles/sfc.ts
654
+ var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
655
+ var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
656
+ function componentName(path11) {
657
+ const base = path11.split("/").pop() ?? path11;
658
+ return base.replace(/\.[^.]+$/, "");
659
+ }
660
+ var sfcProfile = {
661
+ id: "sfc",
662
+ extensions: [".vue", ".svelte", ".astro"],
663
+ summarize(path11, content) {
664
+ let script = "";
665
+ if (path11.toLowerCase().endsWith(".astro")) {
666
+ const frontmatter = ASTRO_FRONTMATTER.exec(content);
667
+ if (frontmatter) script += `${frontmatter[1]}
668
+ `;
669
+ }
670
+ for (const match of content.matchAll(SCRIPT_BLOCK)) {
671
+ script += `${match[1]}
672
+ `;
673
+ }
674
+ const name = componentName(path11);
675
+ if (!script.trim()) {
676
+ const summary2 = emptySummary();
677
+ if (name) {
678
+ summary2.exports.push(name);
679
+ summary2.reactComponents.push(name);
680
+ }
681
+ return { summary: finalizeSummary(summary2), quality: "heuristic" };
682
+ }
683
+ const summary = summarizeTypeScriptSource(`${name}.ts`, script);
684
+ summary.reactComponents.push(name);
685
+ summary.exports.push(name);
686
+ return { summary, quality: "ast" };
687
+ }
688
+ };
689
+
690
+ // src/ast/profiles/index.ts
691
+ var LANGUAGE_PROFILES = [
692
+ pythonProfile,
693
+ goProfile,
694
+ rustProfile,
695
+ jvmProfile,
696
+ dotnetProfile,
697
+ phpProfile,
698
+ rubyProfile,
699
+ cfamilyProfile,
700
+ sfcProfile
701
+ ];
702
+ var BY_EXTENSION = /* @__PURE__ */ new Map();
703
+ for (const profile of LANGUAGE_PROFILES) {
704
+ for (const extension of profile.extensions) {
705
+ BY_EXTENSION.set(extension.toLowerCase(), profile);
706
+ }
707
+ }
708
+ function profileForPath(path11) {
709
+ const lower = path11.toLowerCase();
710
+ const dot = lower.lastIndexOf(".");
711
+ if (dot === -1) return null;
712
+ return BY_EXTENSION.get(lower.slice(dot)) ?? null;
713
+ }
714
+ function summarizeWithProfile(path11, content) {
715
+ const profile = profileForPath(path11);
716
+ return profile ? profile.summarize(path11, content) : null;
717
+ }
718
+
719
+ // src/ast/structured.ts
720
+ function stripJsonComments(content) {
721
+ const text2 = content.replace(/^\uFEFF/, "");
722
+ let out = "";
723
+ let inString = false;
724
+ let inLineComment = false;
725
+ let inBlockComment = false;
726
+ for (let i = 0; i < text2.length; i++) {
727
+ const char = text2[i];
728
+ const next = text2[i + 1];
729
+ if (inLineComment) {
730
+ if (char === "\n") {
731
+ inLineComment = false;
732
+ out += char;
733
+ }
734
+ continue;
735
+ }
736
+ if (inBlockComment) {
737
+ if (char === "*" && next === "/") {
738
+ inBlockComment = false;
739
+ i += 1;
740
+ }
741
+ continue;
742
+ }
743
+ if (inString) {
744
+ out += char;
745
+ if (char === "\\") {
746
+ if (next !== void 0) {
747
+ out += next;
748
+ i += 1;
749
+ }
750
+ } else if (char === '"') {
751
+ inString = false;
752
+ }
753
+ continue;
754
+ }
755
+ if (char === '"') {
756
+ inString = true;
757
+ out += char;
758
+ continue;
759
+ }
760
+ if (char === "/" && next === "/") {
761
+ inLineComment = true;
762
+ i += 1;
763
+ continue;
764
+ }
765
+ if (char === "/" && next === "*") {
766
+ inBlockComment = true;
767
+ i += 1;
768
+ continue;
769
+ }
770
+ out += char;
771
+ }
772
+ return out;
773
+ }
774
+ function summarizeJson(content) {
775
+ try {
776
+ const parsed = JSON.parse(stripJsonComments(content));
777
+ if (Array.isArray(parsed)) {
778
+ const sample = parsed[0];
779
+ const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
780
+ return {
781
+ shape: "array",
782
+ itemCount: parsed.length,
783
+ keys: [],
784
+ sampleKeys
785
+ };
786
+ }
787
+ if (parsed && typeof parsed === "object") {
788
+ const keys = Object.keys(parsed).sort();
789
+ return {
790
+ shape: "object",
791
+ itemCount: keys.length,
792
+ keys: keys.slice(0, 30),
793
+ sampleKeys: []
794
+ };
795
+ }
796
+ return {
797
+ shape: typeof parsed,
798
+ itemCount: null,
799
+ keys: [],
800
+ sampleKeys: []
801
+ };
802
+ } catch {
803
+ return {
804
+ shape: "invalid-json",
805
+ itemCount: null,
806
+ keys: [],
807
+ sampleKeys: []
808
+ };
809
+ }
810
+ }
811
+ function summarizeMarkdown(content) {
812
+ const headings = [];
813
+ const frontmatterKeys = [];
814
+ const lines = content.replace(/^\uFEFF/, "").split("\n");
815
+ let inFrontmatter = false;
816
+ for (let index = 0; index < lines.length; index++) {
817
+ const line = lines[index];
818
+ if (index === 0 && line.trim() === "---") {
819
+ inFrontmatter = true;
820
+ continue;
821
+ }
822
+ if (inFrontmatter) {
823
+ if (line.trim() === "---") {
824
+ inFrontmatter = false;
825
+ continue;
826
+ }
827
+ const match = /^([a-zA-Z0-9_-]+):/.exec(line);
828
+ if (match) frontmatterKeys.push(match[1]);
829
+ continue;
830
+ }
831
+ const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line.trim());
832
+ if (headingMatch) {
833
+ headings.push(`${headingMatch[1]} ${headingMatch[2].trim()}`);
834
+ }
835
+ }
836
+ return {
837
+ headings: headings.slice(0, 40),
838
+ frontmatterKeys: Array.from(new Set(frontmatterKeys)).sort()
839
+ };
840
+ }
841
+ var MANIFEST_FILES = [
842
+ "package.json",
843
+ "pyproject.toml",
844
+ "requirements.txt",
845
+ "setup.py",
846
+ "go.mod",
847
+ "Cargo.toml",
848
+ "pom.xml",
849
+ "build.gradle",
850
+ "build.gradle.kts",
851
+ "composer.json",
852
+ "Gemfile"
853
+ ];
854
+ var MANIFEST_MAX_BYTES = 1e5;
855
+ function readManifest(cwd, name) {
856
+ const full = path10.join(cwd, name);
857
+ try {
858
+ const stats = fs9.statSync(full);
859
+ if (!stats.isFile() || stats.size > MANIFEST_MAX_BYTES) return null;
860
+ return fs9.readFileSync(full, "utf8");
861
+ } catch {
862
+ return null;
863
+ }
864
+ }
865
+ function collectDependencies(manifests) {
866
+ const names = /* @__PURE__ */ new Set();
867
+ const raw = manifests["package.json"];
868
+ if (!raw) return names;
869
+ try {
870
+ const parsed = JSON.parse(raw);
871
+ for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
872
+ const block = parsed[section];
873
+ if (block && typeof block === "object") {
874
+ for (const name of Object.keys(block)) names.add(name);
875
+ }
876
+ }
877
+ } catch {
878
+ }
879
+ return names;
880
+ }
881
+ function buildPresetContext(cwd) {
882
+ const files = /* @__PURE__ */ new Set();
883
+ const dirs = /* @__PURE__ */ new Set();
884
+ try {
885
+ for (const entry of fs9.readdirSync(cwd, { withFileTypes: true })) {
886
+ if (entry.isDirectory()) dirs.add(entry.name);
887
+ else if (entry.isFile()) files.add(entry.name);
888
+ }
889
+ } catch {
890
+ }
891
+ const manifests = {};
892
+ for (const name of MANIFEST_FILES) {
893
+ const content = readManifest(cwd, name);
894
+ if (content !== null) manifests[name] = content;
895
+ }
896
+ return {
897
+ cwd,
898
+ files,
899
+ dirs,
900
+ dependencies: collectDependencies(manifests),
901
+ manifests
902
+ };
903
+ }
904
+ function hasFile(context, name) {
905
+ return context.files.has(name);
906
+ }
907
+ function hasFileStartingWith(context, prefix) {
908
+ for (const file of context.files) {
909
+ if (file.startsWith(prefix)) return true;
910
+ }
911
+ return false;
912
+ }
913
+ function manifestContains(context, manifest, needle) {
914
+ const content = context.manifests[manifest];
915
+ if (!content) return false;
916
+ return content.toLowerCase().includes(needle.toLowerCase());
917
+ }
918
+ function hasDependency(context, ...names) {
919
+ return names.some((name) => context.dependencies.has(name));
920
+ }
921
+
922
+ // src/presets/helpers.ts
923
+ function layer(name, ...prefixes) {
924
+ return {
925
+ name,
926
+ match: (filePath) => prefixes.some((prefix) => filePath.startsWith(prefix))
927
+ };
928
+ }
929
+ function kindOf(name, ...needles) {
930
+ return {
931
+ kind: name,
932
+ match: (filePath) => needles.some((needle) => filePath.includes(needle))
933
+ };
934
+ }
935
+ function suffixKind(name, ...suffixes) {
936
+ return {
937
+ kind: name,
938
+ match: (filePath) => suffixes.some((suffix) => filePath.endsWith(suffix))
939
+ };
940
+ }
941
+ function commonLayers() {
942
+ return [
943
+ layer("scripts", "scripts/", "tools/"),
944
+ layer("docs", "docs/", "documentation/"),
945
+ {
946
+ name: "config",
947
+ match: (filePath) => filePath.startsWith(".github/") || filePath.startsWith(".husky/") || filePath.startsWith(".vscode/") || filePath.startsWith(".") || filePath.includes("config")
948
+ },
949
+ { name: "root", match: (filePath) => !filePath.includes("/") }
950
+ ];
951
+ }
952
+ function commonKinds() {
953
+ return [
954
+ {
955
+ kind: "test",
956
+ match: (filePath) => {
957
+ const lower = filePath.toLowerCase();
958
+ return lower.includes(".test.") || lower.includes(".spec.") || lower.startsWith("test/") || lower.startsWith("tests/") || lower.includes("_test.") || lower.endsWith("_test.go") || lower.includes("__tests__");
959
+ }
960
+ },
961
+ suffixKind("doc", ".md", ".mdx", ".rst", ".txt"),
962
+ suffixKind("data", ".json", ".yaml", ".yml", ".csv", ".toml"),
963
+ {
964
+ kind: "script",
965
+ match: (filePath) => filePath.startsWith("scripts/") || /\.(sh|ps1|bat)$/i.test(filePath)
966
+ },
967
+ {
968
+ kind: "config",
969
+ match: (filePath) => filePath.startsWith(".") || filePath.includes("config") || filePath === "package.json" || filePath === "tsconfig.json"
970
+ }
971
+ ];
972
+ }
973
+
290
974
  // src/presets/nextjs.ts
291
975
  var NEXTJS_LAYERS = [
292
976
  { name: "app", match: (p) => p.startsWith("src/app/") || p.startsWith("app/") },
@@ -342,6 +1026,47 @@ var NEXTJS_KINDS = [
342
1026
  }
343
1027
  ];
344
1028
 
1029
+ // src/presets/node.ts
1030
+ var NODE_LAYERS = [
1031
+ { name: "core", match: (p) => p.startsWith("src/core/") || p.startsWith("core/") },
1032
+ { name: "protocols", match: (p) => p.startsWith("src/protocols/") || p.startsWith("protocols/") },
1033
+ { name: "providers", match: (p) => p.startsWith("src/providers/") || p.startsWith("providers/") },
1034
+ { name: "security", match: (p) => p.startsWith("src/security/") || p.startsWith("security/") },
1035
+ { name: "db", match: (p) => p.startsWith("src/db/") || p.startsWith("db/") || p.startsWith("prisma/") },
1036
+ { name: "api", match: (p) => p.startsWith("src/api/") || p.startsWith("api/") || p.startsWith("routes/") },
1037
+ { name: "services", match: (p) => p.startsWith("src/services/") || p.startsWith("services/") },
1038
+ { name: "controllers", match: (p) => p.startsWith("src/controllers/") || p.startsWith("controllers/") },
1039
+ { name: "scripts", match: (p) => p.startsWith("scripts/") },
1040
+ { name: "docs", match: (p) => p.startsWith("docs/") },
1041
+ {
1042
+ name: "config",
1043
+ match: (p) => p.startsWith(".github/") || p.startsWith(".husky/") || p.startsWith(".") || p.includes("config")
1044
+ },
1045
+ { name: "root", match: (p) => !p.includes("/") }
1046
+ ];
1047
+ var NODE_KINDS = [
1048
+ { kind: "test", match: (p) => /\.(test|spec)\.[jt]sx?$/.test(p.toLowerCase()) },
1049
+ { kind: "route", match: (p) => p.includes("/routes/") || p.includes("route.") || p.includes("/api/") },
1050
+ { kind: "controller", match: (p) => p.includes("controller") },
1051
+ { kind: "service", match: (p) => p.includes("service") },
1052
+ { kind: "provider", match: (p) => p.includes("provider") },
1053
+ { kind: "model", match: (p) => p.includes("model") || p.includes("entity") },
1054
+ { kind: "schema", match: (p) => p.includes("schema") || p.includes("zod") || p.includes("dto") },
1055
+ { kind: "type", match: (p) => p.endsWith(".d.ts") || p.includes("types.") || p.includes("type.") },
1056
+ { kind: "security", match: (p) => p.includes("auth") || p.includes("security") || p.includes("guard") },
1057
+ { kind: "db", match: (p) => p.includes("db") || p.includes("database") || p.includes("migration") },
1058
+ { kind: "data", match: (p) => p.endsWith(".json") || p.endsWith(".csv") },
1059
+ { kind: "doc", match: (p) => p.endsWith(".md") || p.endsWith(".mdx") },
1060
+ { kind: "script", match: (p) => p.startsWith("scripts/") || p.endsWith(".sh") || p.endsWith(".ps1") },
1061
+ {
1062
+ kind: "config",
1063
+ match: (p) => {
1064
+ const base = p.split("/").pop() || "";
1065
+ return ["package.json", "tsconfig.json", "Dockerfile"].includes(base) || base.startsWith(".");
1066
+ }
1067
+ }
1068
+ ];
1069
+
345
1070
  // src/presets/vite.ts
346
1071
  var VITE_LAYERS = [
347
1072
  { name: "pages", match: (p) => p.startsWith("src/pages/") || p.startsWith("src/views/") },
@@ -389,44 +1114,419 @@ var VITE_KINDS = [
389
1114
  }
390
1115
  ];
391
1116
 
392
- // src/presets/node.ts
393
- var NODE_LAYERS = [
394
- { name: "core", match: (p) => p.startsWith("src/core/") || p.startsWith("core/") },
395
- { name: "protocols", match: (p) => p.startsWith("src/protocols/") || p.startsWith("protocols/") },
396
- { name: "providers", match: (p) => p.startsWith("src/providers/") || p.startsWith("providers/") },
397
- { name: "security", match: (p) => p.startsWith("src/security/") || p.startsWith("security/") },
398
- { name: "db", match: (p) => p.startsWith("src/db/") || p.startsWith("db/") || p.startsWith("prisma/") },
399
- { name: "api", match: (p) => p.startsWith("src/api/") || p.startsWith("api/") || p.startsWith("routes/") },
400
- { name: "services", match: (p) => p.startsWith("src/services/") || p.startsWith("services/") },
401
- { name: "controllers", match: (p) => p.startsWith("src/controllers/") || p.startsWith("controllers/") },
402
- { name: "scripts", match: (p) => p.startsWith("scripts/") },
403
- { name: "docs", match: (p) => p.startsWith("docs/") },
1117
+ // src/presets/js-presets.ts
1118
+ var NEXT_LIKE_EXCLUDES = [".svelte-kit", ".astro", ".vite", ".output"];
1119
+ var JS_PRESETS = [
1120
+ {
1121
+ name: "nextjs",
1122
+ priority: 100,
1123
+ detect: (context) => hasFileStartingWith(context, "next.config") || hasDependency(context, "next"),
1124
+ layers: NEXTJS_LAYERS,
1125
+ kinds: NEXTJS_KINDS,
1126
+ exclude: NEXT_LIKE_EXCLUDES,
1127
+ recommendedBudget: "300k"
1128
+ },
1129
+ {
1130
+ name: "nuxt",
1131
+ priority: 98,
1132
+ detect: (context) => hasFileStartingWith(context, "nuxt.config") || hasDependency(context, "nuxt", "nuxt3"),
1133
+ layers: [
1134
+ layer("pages", "pages/", "src/pages/"),
1135
+ layer("components", "components/", "src/components/"),
1136
+ layer("composables", "composables/", "src/composables/"),
1137
+ layer("server", "server/", "src/server/", "api/"),
1138
+ layer("stores", "stores/", "src/stores/"),
1139
+ layer("layouts", "layouts/", "src/layouts/"),
1140
+ ...commonLayers()
1141
+ ],
1142
+ kinds: [
1143
+ kindOf("api-route", "server/api/", "server/routes/"),
1144
+ suffixKind("component", ".vue"),
1145
+ kindOf("composable", "composables/"),
1146
+ kindOf("store", "stores/", "store/"),
1147
+ kindOf("layout", "layouts/"),
1148
+ ...commonKinds()
1149
+ ],
1150
+ exclude: [".nuxt", ".output"],
1151
+ recommendedBudget: "200k"
1152
+ },
1153
+ {
1154
+ name: "sveltekit",
1155
+ priority: 96,
1156
+ detect: (context) => hasFileStartingWith(context, "svelte.config") || hasDependency(context, "@sveltejs/kit", "svelte"),
1157
+ layers: [
1158
+ layer("routes", "src/routes/", "routes/"),
1159
+ layer("lib", "src/lib/", "lib/"),
1160
+ layer("components", "src/lib/components/", "src/components/"),
1161
+ layer("stores", "src/lib/stores/", "src/stores/"),
1162
+ ...commonLayers()
1163
+ ],
1164
+ kinds: [
1165
+ kindOf("route", "+page.", "+server.", "+layout."),
1166
+ suffixKind("component", ".svelte"),
1167
+ kindOf("store", "stores/"),
1168
+ kindOf("params", "params/"),
1169
+ ...commonKinds()
1170
+ ],
1171
+ exclude: [".svelte-kit", ".vercel"],
1172
+ recommendedBudget: "200k"
1173
+ },
1174
+ {
1175
+ name: "remix",
1176
+ priority: 94,
1177
+ detect: (context) => hasDependency(context, "@remix-run/react", "@remix-run/node", "@remix-run/dev"),
1178
+ layers: [
1179
+ layer("routes", "app/routes/", "routes/"),
1180
+ layer("components", "app/components/"),
1181
+ layer("lib", "app/lib/", "app/utils/"),
1182
+ ...commonLayers()
1183
+ ],
1184
+ kinds: [kindOf("route", "routes/"), ...commonKinds()],
1185
+ exclude: [".cache", "public/build"],
1186
+ recommendedBudget: "200k"
1187
+ },
1188
+ {
1189
+ name: "astro",
1190
+ priority: 92,
1191
+ detect: (context) => hasFileStartingWith(context, "astro.config") || hasDependency(context, "astro"),
1192
+ layers: [
1193
+ layer("pages", "src/pages/"),
1194
+ layer("layouts", "src/layouts/"),
1195
+ layer("components", "src/components/"),
1196
+ layer("content", "src/content/"),
1197
+ ...commonLayers()
1198
+ ],
1199
+ kinds: [suffixKind("page", ".astro"), kindOf("layout", "layouts/"), ...commonKinds()],
1200
+ exclude: [".astro", "dist"],
1201
+ recommendedBudget: "150k"
1202
+ },
1203
+ {
1204
+ name: "angular",
1205
+ priority: 90,
1206
+ detect: (context) => hasFile(context, "angular.json") || hasDependency(context, "@angular/core"),
1207
+ layers: [
1208
+ layer("app", "src/app/"),
1209
+ layer("features", "src/app/features/"),
1210
+ layer("shared", "src/app/shared/"),
1211
+ layer("core", "src/app/core/"),
1212
+ layer("assets", "src/assets/"),
1213
+ ...commonLayers()
1214
+ ],
1215
+ kinds: [
1216
+ suffixKind("component", ".component.ts"),
1217
+ suffixKind("service", ".service.ts"),
1218
+ suffixKind("module", ".module.ts"),
1219
+ suffixKind("guard", ".guard.ts"),
1220
+ suffixKind("pipe", ".pipe.ts"),
1221
+ suffixKind("directive", ".directive.ts"),
1222
+ ...commonKinds()
1223
+ ],
1224
+ exclude: [".angular", "dist"],
1225
+ recommendedBudget: "200k"
1226
+ },
1227
+ {
1228
+ name: "solid",
1229
+ priority: 88,
1230
+ detect: (context) => hasDependency(context, "solid-js", "solid-start"),
1231
+ layers: [
1232
+ layer("pages", "src/routes/", "src/pages/"),
1233
+ layer("components", "src/components/"),
1234
+ ...commonLayers()
1235
+ ],
1236
+ kinds: [kindOf("route", "routes/"), ...commonKinds()],
1237
+ exclude: [".solid", "dist"],
1238
+ recommendedBudget: "120k"
1239
+ },
1240
+ {
1241
+ name: "vue-vite",
1242
+ priority: 86,
1243
+ detect: (context) => hasDependency(context, "vue") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
1244
+ layers: [
1245
+ layer("pages", "src/pages/", "src/views/"),
1246
+ layer("components", "src/components/"),
1247
+ layer("composables", "src/composables/", "src/hooks/"),
1248
+ layer("stores", "src/stores/", "src/store/"),
1249
+ layer("services", "src/services/", "src/api/"),
1250
+ layer("shared", "src/shared/", "src/common/"),
1251
+ layer("styles", "src/styles/", "src/assets/"),
1252
+ ...commonLayers()
1253
+ ],
1254
+ kinds: [
1255
+ suffixKind("component", ".vue"),
1256
+ kindOf("composable", "composables/", "use"),
1257
+ kindOf("store", "stores/", "store/"),
1258
+ kindOf("service", "services/", "api/"),
1259
+ suffixKind("style", ".css", ".scss", ".sass", ".less"),
1260
+ ...commonKinds()
1261
+ ],
1262
+ exclude: [".vite", "dist"],
1263
+ recommendedBudget: "120k"
1264
+ },
404
1265
  {
405
- name: "config",
406
- match: (p) => p.startsWith(".github/") || p.startsWith(".husky/") || p.startsWith(".") || p.includes("config")
1266
+ name: "react-vite",
1267
+ priority: 84,
1268
+ detect: (context) => hasDependency(context, "react") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
1269
+ layers: VITE_LAYERS,
1270
+ kinds: VITE_KINDS,
1271
+ exclude: [".vite", "dist"],
1272
+ recommendedBudget: "120k"
407
1273
  },
408
- { name: "root", match: (p) => !p.includes("/") }
1274
+ {
1275
+ name: "nest",
1276
+ priority: 80,
1277
+ detect: (context) => hasDependency(context, "@nestjs/core"),
1278
+ layers: [
1279
+ layer("modules", "src/modules/", "modules/"),
1280
+ layer("controllers", "src/controllers/", "controllers/"),
1281
+ layer("providers", "src/providers/", "providers/"),
1282
+ layer("core", "src/core/"),
1283
+ layer("security", "src/security/", "src/auth/"),
1284
+ layer("db", "src/db/", "src/database/", "prisma/"),
1285
+ ...commonLayers()
1286
+ ],
1287
+ kinds: [
1288
+ suffixKind("controller", ".controller.ts"),
1289
+ suffixKind("service", ".service.ts"),
1290
+ suffixKind("module", ".module.ts"),
1291
+ suffixKind("guard", ".guard.ts"),
1292
+ suffixKind("dto", ".dto.ts"),
1293
+ suffixKind("entity", ".entity.ts"),
1294
+ kindOf("migration", "migrations/"),
1295
+ ...commonKinds()
1296
+ ],
1297
+ exclude: ["dist", "node_modules"],
1298
+ recommendedBudget: "200k"
1299
+ },
1300
+ {
1301
+ name: "node-express",
1302
+ priority: 78,
1303
+ detect: (context) => hasDependency(context, "express", "fastify", "koa", "@hapi/hapi"),
1304
+ layers: NODE_LAYERS,
1305
+ kinds: NODE_KINDS,
1306
+ exclude: ["dist"],
1307
+ recommendedBudget: "200k"
1308
+ },
1309
+ {
1310
+ name: "node-lib",
1311
+ // A bare package.json is often just tooling around a non-JS project, so it
1312
+ // ranks below every definitive language manifest.
1313
+ priority: 45,
1314
+ detect: (context) => hasFile(context, "package.json"),
1315
+ layers: [
1316
+ layer("src", "src/"),
1317
+ layer("lib", "lib/"),
1318
+ layer("test", "test/", "tests/", "__tests__/"),
1319
+ ...commonLayers()
1320
+ ],
1321
+ kinds: [suffixKind("type", ".d.ts"), ...commonKinds()],
1322
+ exclude: ["dist"],
1323
+ recommendedBudget: "120k"
1324
+ }
409
1325
  ];
410
- var NODE_KINDS = [
411
- { kind: "test", match: (p) => /\.(test|spec)\.[jt]sx?$/.test(p.toLowerCase()) },
412
- { kind: "route", match: (p) => p.includes("/routes/") || p.includes("route.") || p.includes("/api/") },
413
- { kind: "controller", match: (p) => p.includes("controller") },
414
- { kind: "service", match: (p) => p.includes("service") },
415
- { kind: "provider", match: (p) => p.includes("provider") },
416
- { kind: "model", match: (p) => p.includes("model") || p.includes("entity") },
417
- { kind: "schema", match: (p) => p.includes("schema") || p.includes("zod") || p.includes("dto") },
418
- { kind: "type", match: (p) => p.endsWith(".d.ts") || p.includes("types.") || p.includes("type.") },
419
- { kind: "security", match: (p) => p.includes("auth") || p.includes("security") || p.includes("guard") },
420
- { kind: "db", match: (p) => p.includes("db") || p.includes("database") || p.includes("migration") },
421
- { kind: "data", match: (p) => p.endsWith(".json") || p.endsWith(".csv") },
422
- { kind: "doc", match: (p) => p.endsWith(".md") || p.endsWith(".mdx") },
423
- { kind: "script", match: (p) => p.startsWith("scripts/") || p.endsWith(".sh") || p.endsWith(".ps1") },
1326
+
1327
+ // src/presets/lang-presets.ts
1328
+ var anyFileEndingWith = (context, suffix) => Array.from(context.files).some((file) => file.endsWith(suffix));
1329
+ var DIR = (name) => `${name}/`;
1330
+ var LANGUAGE_PRESETS = [
424
1331
  {
425
- kind: "config",
426
- match: (p) => {
427
- const base = p.split("/").pop() || "";
428
- return ["package.json", "tsconfig.json", "Dockerfile"].includes(base) || base.startsWith(".");
429
- }
1332
+ name: "python-django",
1333
+ priority: 56,
1334
+ detect: (context) => hasFile(context, "manage.py") || manifestContains(context, "pyproject.toml", "django") || manifestContains(context, "requirements.txt", "django"),
1335
+ layers: [
1336
+ layer("views", DIR("views"), DIR("app/views"), DIR("apps/views")),
1337
+ layer("models", DIR("models"), DIR("app/models"), DIR("apps/models")),
1338
+ layer("serializers", DIR("serializers")),
1339
+ layer("urls", "urls.py"),
1340
+ layer("migrations", DIR("migrations")),
1341
+ layer("settings", "settings.py", "settings/"),
1342
+ layer("templates", DIR("templates")),
1343
+ ...commonLayers()
1344
+ ],
1345
+ kinds: [
1346
+ suffixKind("model", "models.py"),
1347
+ suffixKind("view", "views.py"),
1348
+ suffixKind("serializer", "serializers.py"),
1349
+ suffixKind("urlconf", "urls.py"),
1350
+ kindOf("migration", "migrations/"),
1351
+ suffixKind("settings", "settings.py"),
1352
+ ...commonKinds()
1353
+ ],
1354
+ exclude: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".tox", ".eggs", "staticfiles", "media"],
1355
+ recommendedBudget: "150k"
1356
+ },
1357
+ {
1358
+ name: "python-fastapi",
1359
+ priority: 54,
1360
+ detect: (context) => manifestContains(context, "requirements.txt", "fastapi") || manifestContains(context, "requirements.txt", "flask") || manifestContains(context, "pyproject.toml", "fastapi") || manifestContains(context, "pyproject.toml", "flask"),
1361
+ layers: [
1362
+ layer("routers", DIR("routers"), DIR("routes"), DIR("api")),
1363
+ layer("models", DIR("models"), DIR("db")),
1364
+ layer("schemas", DIR("schemas")),
1365
+ layer("services", DIR("services")),
1366
+ layer("core", DIR("core")),
1367
+ layer("tests", DIR("tests")),
1368
+ ...commonLayers()
1369
+ ],
1370
+ kinds: [
1371
+ kindOf("router", "routers/", "routes/", "api/"),
1372
+ kindOf("model", "models/"),
1373
+ kindOf("schema", "schemas/"),
1374
+ kindOf("service", "services/"),
1375
+ ...commonKinds()
1376
+ ],
1377
+ exclude: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".tox", ".eggs"],
1378
+ recommendedBudget: "150k"
1379
+ },
1380
+ {
1381
+ name: "python-lib",
1382
+ // Below node-lib: a Node repo with a stray .py file is still a Node repo.
1383
+ priority: 30,
1384
+ detect: (context) => hasFile(context, "pyproject.toml") || hasFile(context, "setup.py") || hasFile(context, "requirements.txt") || anyFileEndingWith(context, ".py"),
1385
+ layers: [
1386
+ layer("package", DIR("src"), DIR("lib")),
1387
+ layer("tests", DIR("tests"), DIR("test")),
1388
+ ...commonLayers()
1389
+ ],
1390
+ kinds: [suffixKind("module", ".py"), ...commonKinds()],
1391
+ exclude: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".tox", ".eggs"],
1392
+ recommendedBudget: "120k"
1393
+ },
1394
+ {
1395
+ name: "go",
1396
+ priority: 50,
1397
+ detect: (context) => hasFile(context, "go.mod"),
1398
+ layers: [
1399
+ layer("cmd", "cmd/"),
1400
+ layer("internal", "internal/"),
1401
+ layer("pkg", "pkg/"),
1402
+ layer("api", "api/", "proto/"),
1403
+ layer("migrations", "migrations/"),
1404
+ ...commonLayers()
1405
+ ],
1406
+ kinds: [
1407
+ kindOf("main", "cmd/"),
1408
+ kindOf("handler", "handler", "server", "http"),
1409
+ kindOf("service", "service"),
1410
+ kindOf("repository", "repository", "store", "repo"),
1411
+ kindOf("model", "model", "entity"),
1412
+ suffixKind("test", "_test.go"),
1413
+ ...commonKinds()
1414
+ ],
1415
+ exclude: ["vendor", "testdata"],
1416
+ recommendedBudget: "200k"
1417
+ },
1418
+ {
1419
+ name: "rust",
1420
+ priority: 48,
1421
+ detect: (context) => hasFile(context, "Cargo.toml"),
1422
+ layers: [
1423
+ layer("src", "src/"),
1424
+ layer("crates", "crates/"),
1425
+ layer("bins", "src/bin/"),
1426
+ layer("tests", "tests/"),
1427
+ layer("benches", "benches/"),
1428
+ ...commonLayers()
1429
+ ],
1430
+ kinds: [suffixKind("module", ".rs"), suffixKind("test", "_test.rs"), ...commonKinds()],
1431
+ exclude: ["target"],
1432
+ recommendedBudget: "200k"
1433
+ },
1434
+ {
1435
+ name: "java-spring",
1436
+ priority: 46,
1437
+ detect: (context) => hasFile(context, "pom.xml") || hasFile(context, "build.gradle") || hasFile(context, "build.gradle.kts"),
1438
+ layers: [
1439
+ layer("api", "src/main/java/"),
1440
+ layer("test", "src/test/java/"),
1441
+ layer("resources", "src/main/resources/"),
1442
+ layer("migrations", "src/main/resources/db/"),
1443
+ ...commonLayers()
1444
+ ],
1445
+ kinds: [
1446
+ kindOf("controller", "controller"),
1447
+ kindOf("service", "service"),
1448
+ kindOf("repository", "repository", "dao"),
1449
+ kindOf("entity", "entity", "model"),
1450
+ kindOf("dto", "dto", "request", "response"),
1451
+ kindOf("migration", "migration", "db/migration"),
1452
+ ...commonKinds()
1453
+ ],
1454
+ exclude: [".gradle", "out", "target"],
1455
+ recommendedBudget: "200k"
1456
+ },
1457
+ {
1458
+ name: "dotnet",
1459
+ priority: 44,
1460
+ detect: (context) => anyFileEndingWith(context, ".csproj") || anyFileEndingWith(context, ".sln") || hasFile(context, "global.json"),
1461
+ layers: [
1462
+ layer("web", "src/", "Web/"),
1463
+ layer("domain", "Domain/", "Core/"),
1464
+ layer("infrastructure", "Infrastructure/", "Data/"),
1465
+ layer("tests", "Tests/", "test/"),
1466
+ ...commonLayers()
1467
+ ],
1468
+ kinds: [
1469
+ kindOf("controller", "Controller"),
1470
+ kindOf("service", "Service"),
1471
+ kindOf("repository", "Repository"),
1472
+ kindOf("model", "Model", "Entity"),
1473
+ kindOf("dto", "Dto", "Request", "Response"),
1474
+ suffixKind("test", "Tests.cs"),
1475
+ ...commonKinds()
1476
+ ],
1477
+ exclude: ["bin", "obj", ".vs"],
1478
+ recommendedBudget: "200k"
1479
+ },
1480
+ {
1481
+ name: "php-laravel",
1482
+ priority: 42,
1483
+ detect: (context) => hasFile(context, "artisan") || manifestContains(context, "composer.json", "laravel") || hasFile(context, "composer.json"),
1484
+ layers: [
1485
+ layer("app", "app/"),
1486
+ layer("routes", "routes/"),
1487
+ layer("http", "app/Http/"),
1488
+ layer("models", "app/Models/"),
1489
+ layer("providers", "app/Providers/"),
1490
+ layer("database", "database/"),
1491
+ layer("tests", "tests/"),
1492
+ ...commonLayers()
1493
+ ],
1494
+ kinds: [
1495
+ kindOf("controller", "Http/Controllers/"),
1496
+ kindOf("model", "Models/"),
1497
+ kindOf("migration", "migrations/"),
1498
+ kindOf("middleware", "Middleware/"),
1499
+ kindOf("request", "Requests/"),
1500
+ kindOf("provider", "Providers/"),
1501
+ ...commonKinds()
1502
+ ],
1503
+ exclude: ["vendor", "storage", "bootstrap/cache", "public/build"],
1504
+ recommendedBudget: "150k"
1505
+ },
1506
+ {
1507
+ name: "ruby-rails",
1508
+ priority: 40,
1509
+ detect: (context) => hasFile(context, "Gemfile") || hasFile(context, "config.ru") || hasFile(context, "Rakefile"),
1510
+ layers: [
1511
+ layer("app", "app/"),
1512
+ layer("controllers", "app/controllers/"),
1513
+ layer("models", "app/models/"),
1514
+ layer("config", "config/"),
1515
+ layer("db", "db/"),
1516
+ layer("spec", "spec/", "test/"),
1517
+ ...commonLayers()
1518
+ ],
1519
+ kinds: [
1520
+ kindOf("controller", "controllers/"),
1521
+ kindOf("model", "models/"),
1522
+ kindOf("migration", "migrate/"),
1523
+ kindOf("job", "jobs/"),
1524
+ kindOf("mailer", "mailers/"),
1525
+ suffixKind("test", "_spec.rb", "_test.rb"),
1526
+ ...commonKinds()
1527
+ ],
1528
+ exclude: [".bundle", "log", "tmp", "vendor/bundle", "public/assets"],
1529
+ recommendedBudget: "150k"
430
1530
  }
431
1531
  ];
432
1532
 
@@ -472,57 +1572,118 @@ var DEFAULT_KINDS = [
472
1572
  }
473
1573
  ];
474
1574
 
475
- // src/presets/index.ts
476
- function detectPreset(cwd) {
477
- const hasNextConfig = fs8.existsSync(path9.join(cwd, "next.config.ts")) || fs8.existsSync(path9.join(cwd, "next.config.js")) || fs8.existsSync(path9.join(cwd, "next.config.mjs"));
478
- const hasViteConfig = fs8.existsSync(path9.join(cwd, "vite.config.ts")) || fs8.existsSync(path9.join(cwd, "vite.config.js")) || fs8.existsSync(path9.join(cwd, "vite.config.mjs"));
479
- const pkgJsonPath = path9.join(cwd, "package.json");
480
- let pkgDependencies = {};
481
- if (fs8.existsSync(pkgJsonPath)) {
482
- try {
483
- const parsed = JSON.parse(fs8.readFileSync(pkgJsonPath, "utf8"));
484
- pkgDependencies = {
485
- ...parsed.dependencies || {},
486
- ...parsed.devDependencies || {}
487
- };
488
- } catch {
489
- pkgDependencies = {};
490
- }
491
- }
492
- if (hasNextConfig || pkgDependencies["next"]) {
493
- return {
494
- presetName: "nextjs",
495
- layers: NEXTJS_LAYERS,
496
- kinds: NEXTJS_KINDS
497
- };
1575
+ // src/presets/shape-presets.ts
1576
+ var SHAPE_PRESETS = [
1577
+ {
1578
+ name: "monorepo",
1579
+ priority: 70,
1580
+ detect: (context) => hasFile(context, "pnpm-workspace.yaml") || hasFile(context, "nx.json") || hasFile(context, "turbo.json") || hasFile(context, "lerna.json") || manifestContains(context, "package.json", '"workspaces"'),
1581
+ layers: [
1582
+ layer("apps", "apps/"),
1583
+ layer("packages", "packages/"),
1584
+ layer("tooling", "tooling/", "tools/", "scripts/"),
1585
+ ...commonLayers()
1586
+ ],
1587
+ kinds: commonKinds(),
1588
+ exclude: [".turbo", ".nx"],
1589
+ recommendedBudget: "200k"
1590
+ },
1591
+ {
1592
+ name: "infra",
1593
+ priority: 24,
1594
+ 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"),
1595
+ layers: [
1596
+ layer("modules", "modules/"),
1597
+ layer("environments", "env/", "environments/"),
1598
+ layer("manifests", "k8s/", "helm/", "charts/", "deploy/"),
1599
+ ...commonLayers()
1600
+ ],
1601
+ kinds: [
1602
+ { kind: "terraform", match: (filePath) => filePath.endsWith(".tf") },
1603
+ { kind: "kubernetes", match: (filePath) => /\.(ya?ml)$/i.test(filePath) && /k8s|helm|charts|deploy/.test(filePath) },
1604
+ ...commonKinds()
1605
+ ],
1606
+ exclude: [".terraform", ".terragrunt-cache"],
1607
+ recommendedBudget: "100k"
1608
+ },
1609
+ {
1610
+ name: "docs",
1611
+ priority: 22,
1612
+ detect: (context) => {
1613
+ const markdown = Array.from(context.files).filter((file) => /\.mdx?$/i.test(file)).length;
1614
+ return markdown >= 3 && !hasFile(context, "package.json");
1615
+ },
1616
+ layers: [
1617
+ layer("content", "content/", "docs/"),
1618
+ layer("assets", "assets/", "static/", "public/"),
1619
+ ...commonLayers()
1620
+ ],
1621
+ kinds: commonKinds(),
1622
+ exclude: [".docusaurus", ".vitepress/dist", "site"],
1623
+ recommendedBudget: "100k"
1624
+ },
1625
+ {
1626
+ name: "generic",
1627
+ priority: 0,
1628
+ detect: () => true,
1629
+ layers: DEFAULT_LAYERS,
1630
+ kinds: DEFAULT_KINDS,
1631
+ exclude: [],
1632
+ recommendedBudget: "120k"
498
1633
  }
499
- if (hasViteConfig || pkgDependencies["vite"]) {
500
- return {
501
- presetName: "vite",
502
- layers: VITE_LAYERS,
503
- kinds: VITE_KINDS
504
- };
1634
+ ];
1635
+
1636
+ // src/presets/registry.ts
1637
+ function compare(a, b) {
1638
+ if (b.priority !== a.priority) return b.priority - a.priority;
1639
+ return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
1640
+ }
1641
+ var PRESETS = [
1642
+ ...JS_PRESETS,
1643
+ ...LANGUAGE_PRESETS,
1644
+ ...SHAPE_PRESETS
1645
+ ].sort(compare);
1646
+ var BY_NAME = new Map(PRESETS.map((preset) => [preset.name, preset]));
1647
+ function listPresetNames() {
1648
+ return PRESETS.map((preset) => preset.name);
1649
+ }
1650
+ function getPreset(name) {
1651
+ return BY_NAME.get(name);
1652
+ }
1653
+ function unknownPresetMessage(name) {
1654
+ return `Unknown preset "${name}". Expected one of: ${listPresetNames().join(", ")}.`;
1655
+ }
1656
+ function detectPresetFromContext(context, forced) {
1657
+ if (forced !== void 0) {
1658
+ const preset = BY_NAME.get(forced);
1659
+ if (!preset) throw new Error(unknownPresetMessage(forced));
1660
+ return toDetected(preset);
505
1661
  }
506
- if (pkgDependencies["express"] || pkgDependencies["fastify"] || pkgDependencies["@nestjs/core"] || fs8.existsSync(path9.join(cwd, "src/core")) || fs8.existsSync(path9.join(cwd, "src/protocols"))) {
507
- return {
508
- presetName: "node",
509
- layers: NODE_LAYERS,
510
- kinds: NODE_KINDS
511
- };
1662
+ for (const preset of PRESETS) {
1663
+ if (preset.detect(context)) return toDetected(preset);
512
1664
  }
1665
+ return toDetected(PRESETS[PRESETS.length - 1]);
1666
+ }
1667
+ function toDetected(preset) {
513
1668
  return {
514
- presetName: "default",
515
- layers: DEFAULT_LAYERS,
516
- kinds: DEFAULT_KINDS
1669
+ presetName: preset.name,
1670
+ layers: preset.layers,
1671
+ kinds: preset.kinds,
1672
+ exclude: preset.exclude ? [...preset.exclude] : [],
1673
+ recommendedBudget: preset.recommendedBudget ?? null
517
1674
  };
518
1675
  }
1676
+ function detectPreset(cwd, forced) {
1677
+ return detectPresetFromContext(buildPresetContext(cwd), forced);
1678
+ }
519
1679
  function getGitInfo(cwd, fallbackBranch = "main") {
520
1680
  try {
521
- const branch = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
1681
+ const reported = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
522
1682
  cwd,
523
1683
  stdio: ["ignore", "pipe", "ignore"],
524
1684
  encoding: "utf8"
525
- }).trim() || fallbackBranch;
1685
+ }).trim();
1686
+ const branch = !reported || reported === "HEAD" ? fallbackBranch : reported;
526
1687
  const commit = childProcess.execSync("git rev-parse HEAD", {
527
1688
  cwd,
528
1689
  stdio: ["ignore", "pipe", "ignore"],
@@ -650,10 +1811,10 @@ function matchesExclude(relativePath, pattern) {
650
1811
  return posixPath === cleaned || posixPath.startsWith(`${cleaned}/`) || posixPath.includes(`/${cleaned}/`) || posixPath.endsWith(`/${cleaned}`);
651
1812
  }
652
1813
  function readGitignoreMatcher(dir) {
653
- const gitignorePath = path9.join(dir, ".gitignore");
1814
+ const gitignorePath = path10.join(dir, ".gitignore");
654
1815
  let raw;
655
1816
  try {
656
- raw = fs8.readFileSync(gitignorePath, "utf8");
1817
+ raw = fs9.readFileSync(gitignorePath, "utf8");
657
1818
  } catch {
658
1819
  return () => false;
659
1820
  }
@@ -756,7 +1917,7 @@ function walkDirectory(dir, baseDir, visited) {
756
1917
  const results = { files: [], skipped: [] };
757
1918
  let realDir;
758
1919
  try {
759
- realDir = fs8.realpathSync(dir);
1920
+ realDir = fs9.realpathSync(dir);
760
1921
  } catch {
761
1922
  realDir = dir;
762
1923
  }
@@ -765,21 +1926,21 @@ function walkDirectory(dir, baseDir, visited) {
765
1926
  const ignoredByGitignore = dir === baseDir ? readGitignoreMatcher(baseDir) : () => false;
766
1927
  let entries;
767
1928
  try {
768
- entries = fs8.readdirSync(dir, { withFileTypes: true });
1929
+ entries = fs9.readdirSync(dir, { withFileTypes: true });
769
1930
  } catch {
770
1931
  results.skipped.push({ path: dir, reason: "read-error", detail: "directory unreadable" });
771
1932
  return results;
772
1933
  }
773
1934
  for (const entry of entries) {
774
1935
  if (DEFAULT_IGNORED_DIRS.includes(entry.name)) continue;
775
- const fullPath = path9.join(dir, entry.name);
776
- const relPath = path9.relative(baseDir, fullPath).replace(/\\/g, "/");
1936
+ const fullPath = path10.join(dir, entry.name);
1937
+ const relPath = path10.relative(baseDir, fullPath).replace(/\\/g, "/");
777
1938
  if (ignoredByGitignore(relPath)) continue;
778
1939
  let isDirectory = entry.isDirectory();
779
1940
  let isFile = entry.isFile();
780
1941
  if (entry.isSymbolicLink()) {
781
1942
  try {
782
- const stats = fs8.statSync(fullPath);
1943
+ const stats = fs9.statSync(fullPath);
783
1944
  isDirectory = stats.isDirectory();
784
1945
  isFile = stats.isFile();
785
1946
  if (!isDirectory && !isFile) {
@@ -1044,6 +2205,8 @@ function normalizeResolvedConfig(config) {
1044
2205
  config.includeMonolith === true
1045
2206
  ),
1046
2207
  include: resolveInclude(config.include),
2208
+ preset: typeof config.preset === "string" ? config.preset : "generic",
2209
+ presetExclude: Array.isArray(config.presetExclude) ? config.presetExclude : [],
1047
2210
  layersMode: config.layersMode === "replace" ? "replace" : "merge",
1048
2211
  budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
1049
2212
  hooks: resolveHooks(config.hooks),
@@ -1091,18 +2254,22 @@ function parseJsonish(raw, file) {
1091
2254
  }
1092
2255
  async function loadConfigFileDetailed(cwd) {
1093
2256
  const warnings = [];
2257
+ const errors = [];
1094
2258
  for (const filename of CONFIG_FILENAMES) {
1095
- const fullPath = path9.join(cwd, filename);
1096
- if (!fs8.existsSync(fullPath)) continue;
2259
+ const fullPath = path10.join(cwd, filename);
2260
+ if (!fs9.existsSync(fullPath)) continue;
1097
2261
  if (filename.endsWith(".json") || filename === ".snapshotrc") {
1098
2262
  try {
1099
2263
  return {
1100
- config: parseJsonish(fs8.readFileSync(fullPath, "utf8"), fullPath),
2264
+ config: parseJsonish(fs9.readFileSync(fullPath, "utf8"), fullPath),
1101
2265
  file: fullPath,
1102
- warnings
2266
+ warnings,
2267
+ errors
1103
2268
  };
1104
2269
  } catch (err) {
1105
- warnings.push(err.message);
2270
+ const message = err.message;
2271
+ warnings.push(message);
2272
+ errors.push(message);
1106
2273
  continue;
1107
2274
  }
1108
2275
  }
@@ -1116,28 +2283,37 @@ async function loadConfigFileDetailed(cwd) {
1116
2283
  const imported = await import(pathToFileURL(fullPath).href);
1117
2284
  const config = imported.default ?? imported;
1118
2285
  if (!config || typeof config !== "object") {
1119
- warnings.push(`${fullPath} does not export a configuration object.`);
2286
+ const message = `${fullPath} does not export a configuration object.`;
2287
+ warnings.push(message);
2288
+ errors.push(message);
1120
2289
  continue;
1121
2290
  }
1122
- return { config, file: fullPath, warnings };
2291
+ return { config, file: fullPath, warnings, errors };
1123
2292
  } catch (err) {
1124
- warnings.push(`Could not load ${fullPath}: ${err.message}`);
2293
+ const message = `Could not load ${fullPath}: ${err.message}`;
2294
+ warnings.push(message);
2295
+ errors.push(message);
1125
2296
  continue;
1126
2297
  }
1127
2298
  }
1128
- const pkgJsonPath = path9.join(cwd, "package.json");
1129
- if (fs8.existsSync(pkgJsonPath)) {
2299
+ const pkgJsonPath = path10.join(cwd, "package.json");
2300
+ if (fs9.existsSync(pkgJsonPath)) {
1130
2301
  try {
1131
- const parsed = parseJsonish(fs8.readFileSync(pkgJsonPath, "utf8"), pkgJsonPath);
2302
+ const parsed = parseJsonish(fs9.readFileSync(pkgJsonPath, "utf8"), pkgJsonPath);
1132
2303
  const snapshotKey = parsed.snapshot;
1133
2304
  if (snapshotKey && typeof snapshotKey === "object") {
1134
- return { config: snapshotKey, file: pkgJsonPath, warnings };
2305
+ return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
2306
+ }
2307
+ if (snapshotKey !== void 0) {
2308
+ warnings.push(`${pkgJsonPath}: the "snapshot" key must be an object; it was ignored.`);
1135
2309
  }
1136
2310
  } catch (err) {
1137
- warnings.push(err.message);
2311
+ const message = err.message;
2312
+ warnings.push(message);
2313
+ errors.push(message);
1138
2314
  }
1139
2315
  }
1140
- return { config: null, file: null, warnings };
2316
+ return { config: null, file: null, warnings, errors };
1141
2317
  }
1142
2318
  async function loadConfigFile(cwd) {
1143
2319
  const loaded = await loadConfigFileDetailed(cwd);
@@ -1236,7 +2412,9 @@ function boundedBytes(value, fallback, minimum, label) {
1236
2412
  `${label} must be a byte count such as 120000, "120k" or "1.5mb" (received ${describeBytes(value)}).`
1237
2413
  );
1238
2414
  }
1239
- if (parsed <= 0) return fallback;
2415
+ if (parsed <= 0) {
2416
+ throw new Error(`${label} must be a positive byte count (received ${describeBytes(value)}).`);
2417
+ }
1240
2418
  if (parsed < minimum) {
1241
2419
  throw new Error(`${label} must be at least ${minimum} bytes (received ${describeBytes(value)}).`);
1242
2420
  }
@@ -1276,12 +2454,12 @@ function resolveBudgets(budgets, fallback) {
1276
2454
  if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
1277
2455
  throw new Error("`budgets.byLayer` must be an object of layer name to byte size.");
1278
2456
  }
1279
- for (const [layer, value] of Object.entries(budgets.byLayer)) {
1280
- byLayer[layer] = boundedBytes(
2457
+ for (const [layer2, value] of Object.entries(budgets.byLayer)) {
2458
+ byLayer[layer2] = boundedBytes(
1281
2459
  value,
1282
2460
  fallback,
1283
2461
  MIN_MAX_PART_BYTES,
1284
- `budgets.byLayer["${layer}"]`
2462
+ `budgets.byLayer["${layer2}"]`
1285
2463
  );
1286
2464
  }
1287
2465
  }
@@ -1302,18 +2480,22 @@ function resolveHooks(hooks) {
1302
2480
  }
1303
2481
  function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
1304
2482
  const candidate = cliOptions?.cwd || userConfig?.cwd || baseDir || process.cwd();
1305
- const resolved = path9.resolve(candidate);
1306
- if (!fs8.existsSync(resolved)) {
2483
+ const resolved = path10.resolve(candidate);
2484
+ if (!fs9.existsSync(resolved)) {
1307
2485
  throw new Error(`Working directory does not exist: ${resolved}`);
1308
2486
  }
1309
- if (!fs8.statSync(resolved).isDirectory()) {
2487
+ if (!fs9.statSync(resolved).isDirectory()) {
1310
2488
  throw new Error(`Working directory is not a directory: ${resolved}`);
1311
2489
  }
1312
2490
  return resolved;
1313
2491
  }
1314
2492
  function resolveConfig(userConfig, cliOptions, baseDir) {
1315
2493
  const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
1316
- const preset = detectPreset(cwd);
2494
+ const forcedPreset = userConfig?.preset;
2495
+ if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
2496
+ throw new Error(unknownPresetMessage(forcedPreset));
2497
+ }
2498
+ const preset = detectPreset(cwd, forcedPreset);
1317
2499
  const layersMode = resolveLayersMode(userConfig?.layersMode);
1318
2500
  const layers = userConfig?.layers && userConfig.layers.length > 0 ? layersMode === "replace" ? userConfig.layers : [...userConfig.layers, ...preset.layers] : preset.layers;
1319
2501
  const kinds = userConfig?.kinds && userConfig.kinds.length > 0 ? layersMode === "replace" ? userConfig.kinds : [...userConfig.kinds, ...preset.kinds] : preset.kinds;
@@ -1349,6 +2531,8 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
1349
2531
  ),
1350
2532
  includeSensitiveFiles: cliOptions?.includeSensitiveFiles ?? userConfig?.includeSensitiveFiles ?? false,
1351
2533
  include: resolveInclude(userConfig?.include),
2534
+ preset: preset.presetName,
2535
+ presetExclude: [...preset.exclude],
1352
2536
  artifacts,
1353
2537
  layersMode,
1354
2538
  budgets,
@@ -1383,8 +2567,8 @@ function matchRule(filePath, matcher) {
1383
2567
  return false;
1384
2568
  }
1385
2569
  function classifyLayer(filePath, layers) {
1386
- for (const layer of layers) {
1387
- if (matchRule(filePath, layer.match)) return layer.name;
2570
+ for (const layer2 of layers) {
2571
+ if (matchRule(filePath, layer2.match)) return layer2.name;
1388
2572
  }
1389
2573
  return "other";
1390
2574
  }
@@ -1465,11 +2649,11 @@ function generateAuditReport(manifest, secretFindings) {
1465
2649
  "| :--- | :--- | :--- |"
1466
2650
  );
1467
2651
  const totalFiles = manifest.counts.totalFiles || 1;
1468
- for (const [layer, count] of Object.entries(manifest.layers).sort(
2652
+ for (const [layer2, count] of Object.entries(manifest.layers).sort(
1469
2653
  (a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
1470
2654
  )) {
1471
2655
  const pct = (count / totalFiles * 100).toFixed(1);
1472
- lines.push(`| \`${layer}\` | ${count} | ${pct}% |`);
2656
+ lines.push(`| \`${layer2}\` | ${count} | ${pct}% |`);
1473
2657
  }
1474
2658
  lines.push("");
1475
2659
  lines.push(
@@ -1577,21 +2761,21 @@ function partitionFiles(files, maxPartBytes, contentFingerprint) {
1577
2761
  section.byteSize = Buffer.byteLength(section.text, "utf8");
1578
2762
  continue;
1579
2763
  }
1580
- const marker = "\n... [part truncated: cannot fit budget] ...\n";
1581
- const target = Math.max(0, budget - Buffer.byteLength(marker, "utf8"));
2764
+ const marker2 = "\n... [part truncated: cannot fit budget] ...\n";
2765
+ const target = Math.max(0, budget - Buffer.byteLength(marker2, "utf8"));
1582
2766
  const clipped = truncateToBytes(rendered[oversized].markdown, target);
1583
- rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker);
2767
+ rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker2);
1584
2768
  return rendered;
1585
2769
  }
1586
2770
  return enforceBudget(renderGroups(groups, contentFingerprint), budget);
1587
2771
  }
1588
2772
  function enforceBudget(parts, budget) {
1589
- const marker = "\n... [part truncated: cannot fit budget] ...\n";
1590
- const target = Math.max(0, budget - Buffer.byteLength(marker, "utf8"));
2773
+ const marker2 = "\n... [part truncated: cannot fit budget] ...\n";
2774
+ const target = Math.max(0, budget - Buffer.byteLength(marker2, "utf8"));
1591
2775
  return parts.map((part) => {
1592
2776
  if (part.metadata.byteSize <= budget) return part;
1593
2777
  const clipped = truncateToBytes(part.markdown, target);
1594
- return rehashPart(part, clipped.content + marker);
2778
+ return rehashPart(part, clipped.content + marker2);
1595
2779
  });
1596
2780
  }
1597
2781
  function renumberParts(parts, contentFingerprint) {
@@ -1599,10 +2783,10 @@ function renumberParts(parts, contentFingerprint) {
1599
2783
  if (total === 0) return [];
1600
2784
  const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
1601
2785
  const nameFor = (position) => `part-${zeroPad(position)}.md`;
1602
- const marker = "## Contents\n";
2786
+ const marker2 = "## Contents\n";
1603
2787
  return parts.map((part, index) => {
1604
- const markerIndex = part.markdown.indexOf(marker);
1605
- const body = markerIndex === -1 ? "" : part.markdown.slice(markerIndex + marker.length);
2788
+ const markerIndex = part.markdown.indexOf(marker2);
2789
+ const body = markerIndex === -1 ? "" : part.markdown.slice(markerIndex + marker2.length);
1606
2790
  const included = part.metadata.includedFiles;
1607
2791
  if (markerIndex === -1) {
1608
2792
  throw new Error(`${part.filename} has no "## Contents" section to re-number.`);
@@ -1660,9 +2844,9 @@ function topEntries(counts, limit) {
1660
2844
  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(", ");
1661
2845
  }
1662
2846
  function prepareSection(file, sectionBudget) {
1663
- let text = renderFileSection(file);
1664
- let byteSize = Buffer.byteLength(text, "utf8");
1665
- if (byteSize <= sectionBudget) return { file, text, byteSize };
2847
+ let text2 = renderFileSection(file);
2848
+ let byteSize = Buffer.byteLength(text2, "utf8");
2849
+ if (byteSize <= sectionBudget) return { file, text: text2, byteSize };
1666
2850
  const shrinkBody = () => {
1667
2851
  const content = file.content ?? "";
1668
2852
  const overhead = byteSize - Buffer.byteLength(content, "utf8");
@@ -1671,24 +2855,24 @@ function prepareSection(file, sectionBudget) {
1671
2855
  const truncated = truncateToBytes(content, allowed);
1672
2856
  if (!truncated.isTruncated) return;
1673
2857
  applyTruncatedContent(file, truncated.content);
1674
- text = renderFileSection(file);
1675
- byteSize = Buffer.byteLength(text, "utf8");
2858
+ text2 = renderFileSection(file);
2859
+ byteSize = Buffer.byteLength(text2, "utf8");
1676
2860
  };
1677
2861
  shrinkBody();
1678
2862
  if (byteSize > sectionBudget && file.summary) {
1679
2863
  file.summary = void 0;
1680
2864
  file.summaryOmitted = true;
1681
- text = renderFileSection(file);
1682
- byteSize = Buffer.byteLength(text, "utf8");
2865
+ text2 = renderFileSection(file);
2866
+ byteSize = Buffer.byteLength(text2, "utf8");
1683
2867
  shrinkBody();
1684
2868
  }
1685
2869
  if (byteSize > sectionBudget) {
1686
2870
  const target = sectionBudget - Buffer.byteLength(SECTION_TRUNCATION_NOTICE, "utf8");
1687
- const truncated = truncateToBytes(text, Math.max(0, target));
1688
- text = truncated.content + SECTION_TRUNCATION_NOTICE;
1689
- byteSize = Buffer.byteLength(text, "utf8");
2871
+ const truncated = truncateToBytes(text2, Math.max(0, target));
2872
+ text2 = truncated.content + SECTION_TRUNCATION_NOTICE;
2873
+ byteSize = Buffer.byteLength(text2, "utf8");
1690
2874
  }
1691
- return { file, text, byteSize };
2875
+ return { file, text: text2, byteSize };
1692
2876
  }
1693
2877
  function applyTruncatedContent(file, content) {
1694
2878
  file.content = content;
@@ -1810,6 +2994,7 @@ function renderFileSection(file) {
1810
2994
  `- Source Lines: ${file.originalLines}`,
1811
2995
  `- Included Lines: ${file.includedLines}`,
1812
2996
  `- SHA-256: ${file.hashSha256}`,
2997
+ ...file.summaryQuality ? [`- Summary Quality: ${file.summaryQuality}`] : [],
1813
2998
  ...renderSummary(file.summary),
1814
2999
  ...file.summaryOmitted ? ["- Summary: (omitted: exceeds the part budget)"] : [],
1815
3000
  "",
@@ -2057,7 +3242,7 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
2057
3242
  ".so"
2058
3243
  ]);
2059
3244
  function isBinaryExtension(filePath) {
2060
- const ext = path9.extname(filePath).toLowerCase();
3245
+ const ext = path10.extname(filePath).toLowerCase();
2061
3246
  return BINARY_EXTENSIONS.has(ext);
2062
3247
  }
2063
3248
  function isBinaryBuffer(buffer) {
@@ -2071,9 +3256,9 @@ function isBinaryFile(absolutePath) {
2071
3256
  if (isBinaryExtension(absolutePath)) return true;
2072
3257
  let fd;
2073
3258
  try {
2074
- fd = fs8.openSync(absolutePath, "r");
3259
+ fd = fs9.openSync(absolutePath, "r");
2075
3260
  const buffer = Buffer.alloc(8192);
2076
- const bytesRead = fs8.readSync(fd, buffer, 0, 8192, 0);
3261
+ const bytesRead = fs9.readSync(fd, buffer, 0, 8192, 0);
2077
3262
  if (bytesRead === 0) return false;
2078
3263
  return isBinaryBuffer(buffer.subarray(0, bytesRead));
2079
3264
  } catch {
@@ -2081,7 +3266,7 @@ function isBinaryFile(absolutePath) {
2081
3266
  } finally {
2082
3267
  if (fd !== void 0) {
2083
3268
  try {
2084
- fs8.closeSync(fd);
3269
+ fs9.closeSync(fd);
2085
3270
  } catch {
2086
3271
  }
2087
3272
  }
@@ -2141,10 +3326,10 @@ var LANGUAGE_BY_BASENAME = {
2141
3326
  };
2142
3327
  function detectLanguage(filePath) {
2143
3328
  const normalized = filePath.replace(/\\/g, "/");
2144
- const base = path9.posix.basename(normalized);
3329
+ const base = path10.posix.basename(normalized);
2145
3330
  if (LANGUAGE_BY_BASENAME[base]) return LANGUAGE_BY_BASENAME[base];
2146
3331
  if (base === ".env" || base.startsWith(".env.")) return "dotenv";
2147
- const ext = path9.posix.extname(base).toLowerCase();
3332
+ const ext = path10.posix.extname(base).toLowerCase();
2148
3333
  return LANGUAGE_BY_EXTENSION[ext] ?? "text";
2149
3334
  }
2150
3335
  function emptyDrift() {
@@ -2163,8 +3348,8 @@ function sha256Hex(buffer) {
2163
3348
  function validateSnapshot(configInput) {
2164
3349
  assertUsableConfigInput(configInput);
2165
3350
  const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
2166
- const manifestPath = path9.join(config.cwd, config.outDir, "manifest.json");
2167
- if (!fs8.existsSync(manifestPath)) {
3351
+ const manifestPath = path10.resolve(config.cwd, config.outDir, "manifest.json");
3352
+ if (!fs9.existsSync(manifestPath)) {
2168
3353
  return {
2169
3354
  valid: false,
2170
3355
  manifestPresent: false,
@@ -2175,7 +3360,7 @@ function validateSnapshot(configInput) {
2175
3360
  }
2176
3361
  let manifest;
2177
3362
  try {
2178
- manifest = JSON.parse(fs8.readFileSync(manifestPath, "utf8"));
3363
+ manifest = JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
2179
3364
  } catch (err) {
2180
3365
  return {
2181
3366
  valid: false,
@@ -2198,7 +3383,7 @@ function validateSnapshot(configInput) {
2198
3383
  }
2199
3384
  const drift = emptyDrift();
2200
3385
  const warnings = [];
2201
- const snapshotDir = path9.join(config.cwd, config.outDir);
3386
+ const snapshotDir = path10.resolve(config.cwd, config.outDir);
2202
3387
  const manifestFileMap = /* @__PURE__ */ new Map();
2203
3388
  for (const file of manifest.files) {
2204
3389
  if (file && typeof file.relativePath === "string") {
@@ -2210,7 +3395,7 @@ function validateSnapshot(configInput) {
2210
3395
  );
2211
3396
  const collection = collectFiles(config.cwd, {
2212
3397
  includePatterns: config.include,
2213
- excludePatterns: config.excludePatterns,
3398
+ excludePatterns: [...config.excludePatterns, ...config.presetExclude],
2214
3399
  includeSensitiveFiles: config.includeSensitiveFiles
2215
3400
  });
2216
3401
  for (const diskFile of collection.files) {
@@ -2222,7 +3407,7 @@ function validateSnapshot(configInput) {
2222
3407
  }
2223
3408
  let diskHash;
2224
3409
  try {
2225
- diskHash = sha256Hex(fs8.readFileSync(path9.join(config.cwd, diskFile)));
3410
+ diskHash = sha256Hex(fs9.readFileSync(path10.join(config.cwd, diskFile)));
2226
3411
  } catch {
2227
3412
  drift.missingOnDisk.push(diskFile);
2228
3413
  continue;
@@ -2232,10 +3417,11 @@ function validateSnapshot(configInput) {
2232
3417
  }
2233
3418
  }
2234
3419
  for (const snapshotFile of manifestFileMap.keys()) {
2235
- if (!fs8.existsSync(path9.join(config.cwd, snapshotFile))) {
3420
+ if (!fs9.existsSync(path10.join(config.cwd, snapshotFile))) {
2236
3421
  drift.missingOnDisk.push(snapshotFile);
2237
3422
  }
2238
3423
  }
3424
+ drift.missingOnDisk = [...new Set(drift.missingOnDisk)];
2239
3425
  const maxEffectiveBudget = Math.max(
2240
3426
  config.maxPartBytes,
2241
3427
  config.budgets.default,
@@ -2246,10 +3432,10 @@ function validateSnapshot(configInput) {
2246
3432
  if (!part || typeof part.filename !== "string") continue;
2247
3433
  partNames.add(part.filename);
2248
3434
  if (!config.artifacts.parts) continue;
2249
- const partPath = path9.join(snapshotDir, "parts", part.filename);
3435
+ const partPath = path10.join(snapshotDir, "parts", part.filename);
2250
3436
  let buffer;
2251
3437
  try {
2252
- buffer = fs8.readFileSync(partPath);
3438
+ buffer = fs9.readFileSync(partPath);
2253
3439
  } catch {
2254
3440
  drift.partIssues.push({ part: part.filename, issue: "missing from snapshot/parts" });
2255
3441
  continue;
@@ -2336,6 +3522,11 @@ function validateSnapshot(configInput) {
2336
3522
  }
2337
3523
  }
2338
3524
  }
3525
+ if (recorded?.preset !== void 0 && recorded.preset !== config.preset) {
3526
+ drift.configMismatches.push(
3527
+ `preset: manifest ${recorded.preset} vs current ${config.preset}`
3528
+ );
3529
+ }
2339
3530
  if (recorded?.layersMode !== void 0 && recorded.layersMode !== config.layersMode) {
2340
3531
  drift.configMismatches.push(
2341
3532
  `layersMode: manifest ${recorded.layersMode} vs current ${config.layersMode}`
@@ -2353,10 +3544,10 @@ function validateSnapshot(configInput) {
2353
3544
  ...Object.keys(recordedByLayer),
2354
3545
  ...Object.keys(config.budgets.byLayer)
2355
3546
  ]);
2356
- for (const layer of Array.from(layerNames).sort()) {
2357
- if (recordedByLayer[layer] !== config.budgets.byLayer[layer]) {
3547
+ for (const layer2 of Array.from(layerNames).sort()) {
3548
+ if (recordedByLayer[layer2] !== config.budgets.byLayer[layer2]) {
2358
3549
  drift.configMismatches.push(
2359
- `budgets.byLayer.${layer}: manifest ${recordedByLayer[layer]} vs current ${config.budgets.byLayer[layer]}`
3550
+ `budgets.byLayer.${layer2}: manifest ${recordedByLayer[layer2]} vs current ${config.budgets.byLayer[layer2]}`
2360
3551
  );
2361
3552
  }
2362
3553
  }
@@ -2439,9 +3630,9 @@ function validateSnapshot(configInput) {
2439
3630
  ["full-codebase.md", manifest.artifacts?.fullMarkdown]
2440
3631
  ]) {
2441
3632
  if (!artifact) continue;
2442
- const artifactPath = path9.join(snapshotDir, key);
3633
+ const artifactPath = path10.join(snapshotDir, key);
2443
3634
  try {
2444
- const buffer = fs8.readFileSync(artifactPath);
3635
+ const buffer = fs9.readFileSync(artifactPath);
2445
3636
  if (sha256Hex(buffer) !== artifact.sha256 || buffer.length !== artifact.byteSize) {
2446
3637
  drift.partIssues.push({ part: key, issue: "artifact does not match its recorded digest" });
2447
3638
  }
@@ -2497,35 +3688,35 @@ function describeMatcher(matcher) {
2497
3688
  return String(matcher);
2498
3689
  }
2499
3690
  function normalizeTarget(cwd, target) {
2500
- const absolute = path9.resolve(cwd, target);
2501
- const relative3 = path9.relative(cwd, absolute);
2502
- if (relative3.startsWith("..") || path9.isAbsolute(relative3)) return target;
2503
- return relative3.split(path9.sep).join("/");
3691
+ const absolute = path10.resolve(cwd, target);
3692
+ const relative3 = path10.relative(cwd, absolute);
3693
+ if (relative3.startsWith("..") || path10.isAbsolute(relative3)) return target;
3694
+ return relative3.split(path10.sep).join("/");
2504
3695
  }
2505
3696
  function findRuleMatch(rules, relativePath) {
2506
3697
  return rules.find(
2507
3698
  (rule) => matchRule(relativePath, rule.match)
2508
3699
  );
2509
3700
  }
2510
- function readManifest(config) {
2511
- const manifestPath = path9.join(config.cwd, config.outDir, "manifest.json");
3701
+ function readManifest2(config) {
3702
+ const manifestPath = path10.resolve(config.cwd, config.outDir, "manifest.json");
2512
3703
  try {
2513
- return JSON.parse(fs8.readFileSync(manifestPath, "utf8"));
3704
+ return JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
2514
3705
  } catch {
2515
3706
  return null;
2516
3707
  }
2517
3708
  }
2518
3709
  function explainPath(config, target) {
2519
3710
  const relative3 = normalizeTarget(config.cwd, target);
2520
- const absolute = path9.join(config.cwd, relative3);
3711
+ const absolute = path10.join(config.cwd, relative3);
2521
3712
  const result = {
2522
3713
  path: relative3,
2523
- exists: fs8.existsSync(absolute),
3714
+ exists: fs9.existsSync(absolute),
2524
3715
  included: false
2525
3716
  };
2526
3717
  const collection = collectFiles(config.cwd, {
2527
3718
  includePatterns: config.include,
2528
- excludePatterns: config.excludePatterns,
3719
+ excludePatterns: [...config.excludePatterns, ...config.presetExclude],
2529
3720
  includeSensitiveFiles: config.includeSensitiveFiles
2530
3721
  });
2531
3722
  if (!collection.files.includes(relative3)) {
@@ -2550,16 +3741,16 @@ function explainPath(config, target) {
2550
3741
  if (layerRule) result.layerMatch = describeMatcher(layerRule.match);
2551
3742
  const kindRule = findRuleMatch(config.kinds, relative3);
2552
3743
  if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
2553
- if (fs8.existsSync(absolute) && fs8.statSync(absolute).isFile()) {
2554
- const buffer = fs8.readFileSync(absolute);
2555
- const text = buffer.toString("utf8");
3744
+ if (fs9.existsSync(absolute) && fs9.statSync(absolute).isFile()) {
3745
+ const buffer = fs9.readFileSync(absolute);
3746
+ const text2 = buffer.toString("utf8");
2556
3747
  result.bytes = buffer.length;
2557
- result.lines = countLines(text);
2558
- const { findings } = scanAndRedactSecrets(text, config.secretRules, relative3);
3748
+ result.lines = countLines(text2);
3749
+ const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative3);
2559
3750
  result.secretCount = findings.length;
2560
3751
  result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
2561
3752
  }
2562
- const manifest = readManifest(config);
3753
+ const manifest = readManifest2(config);
2563
3754
  const entry = manifest?.files.find((file) => file.relativePath === relative3);
2564
3755
  result.part = entry?.part ?? null;
2565
3756
  result.inclusionMode = entry?.inclusionMode;
@@ -2642,10 +3833,16 @@ function compareManifests(before, after) {
2642
3833
  };
2643
3834
  }
2644
3835
  function loadManifest(target) {
2645
- const manifestPath = fs8.statSync(target).isDirectory() ? path9.join(target, "manifest.json") : target;
3836
+ let isDirectory;
3837
+ try {
3838
+ isDirectory = fs9.statSync(target).isDirectory();
3839
+ } catch {
3840
+ throw new Error(`No snapshot found at ${target}: the path does not exist.`);
3841
+ }
3842
+ const manifestPath = isDirectory ? path10.join(target, "manifest.json") : target;
2646
3843
  let raw;
2647
3844
  try {
2648
- raw = fs8.readFileSync(manifestPath, "utf8");
3845
+ raw = fs9.readFileSync(manifestPath, "utf8");
2649
3846
  } catch {
2650
3847
  throw new Error(`Could not read a manifest at ${manifestPath}.`);
2651
3848
  }
@@ -2658,6 +3855,9 @@ function loadManifest(target) {
2658
3855
  if (!parsed || !Array.isArray(parsed.files)) {
2659
3856
  throw new Error(`${manifestPath} is not a snapshot manifest (no "files" array).`);
2660
3857
  }
3858
+ if (!parsed.counts || typeof parsed.counts !== "object") {
3859
+ throw new Error(`${manifestPath} is not a snapshot manifest (no "counts" object).`);
3860
+ }
2661
3861
  return parsed;
2662
3862
  }
2663
3863
  function renderDiffText(diff) {
@@ -2681,14 +3881,659 @@ function renderDiffText(diff) {
2681
3881
  return lines.join("\n");
2682
3882
  }
2683
3883
 
3884
+ // src/wizard/config-template.ts
3885
+ var DEFAULT_ANSWERS = {
3886
+ preset: "generic",
3887
+ maxPartBytes: "300k",
3888
+ largeFileBytes: "200k",
3889
+ include: [],
3890
+ excludePatterns: [],
3891
+ artifacts: {
3892
+ readme: true,
3893
+ manifest: true,
3894
+ parts: true,
3895
+ navigation: true,
3896
+ projectTree: true,
3897
+ audit: true,
3898
+ monolith: false
3899
+ },
3900
+ includeSensitiveFiles: false,
3901
+ outDir: "snapshot"
3902
+ };
3903
+ function list(values) {
3904
+ if (values.length === 0) return "[]";
3905
+ return `[${values.map((value) => JSON.stringify(value)).join(", ")}]`;
3906
+ }
3907
+ function size(value) {
3908
+ return typeof value === "string" ? JSON.stringify(value) : String(value);
3909
+ }
3910
+ var PRESET_NAMES = [
3911
+ "nextjs \xB7 react-vite \xB7 vue-vite \xB7 nuxt \xB7 sveltekit \xB7 angular \xB7 astro",
3912
+ "remix \xB7 solid \xB7 nest \xB7 node-express \xB7 node-lib",
3913
+ "python-django \xB7 python-fastapi \xB7 python-lib \xB7 go \xB7 rust",
3914
+ "java-spring \xB7 dotnet \xB7 php-laravel \xB7 ruby-rails",
3915
+ "monorepo \xB7 docs \xB7 infra \xB7 generic"
3916
+ ];
3917
+ function renderConfig(answers) {
3918
+ const { artifacts } = answers;
3919
+ return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
3920
+
3921
+ export default defineConfig({
3922
+ // \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
3923
+ // Which project shape to use. Detected from the repo when omitted.
3924
+ // ${PRESET_NAMES.join("\n // ")}
3925
+ preset: ${JSON.stringify(answers.preset)},
3926
+
3927
+ // Recorded in the manifest when git cannot report a branch.
3928
+ branch: "main",
3929
+
3930
+ // \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
3931
+ // Where the snapshot is written.
3932
+ outDir: ${JSON.stringify(answers.outDir)},
3933
+ // Scratch directory; the new snapshot is renamed into place atomically.
3934
+ tmpDir: "snapshot.tmp",
3935
+
3936
+ // \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
3937
+ // Hard ceiling per part. "300k", 307200 and "1.5mb" are all accepted.
3938
+ maxPartBytes: ${size(answers.maxPartBytes)},
3939
+ // Files above this size get a head/tail preview instead of the full body.
3940
+ largeFileBytes: ${size(answers.largeFileBytes)},
3941
+ previewHeadLines: 80,
3942
+ previewTailLines: 40,
3943
+ // Different ceilings per layer. Unlisted layers use \`default\`.
3944
+ budgets: {
3945
+ default: ${size(answers.maxPartBytes)},
3946
+ byLayer: {},
3947
+ },
3948
+
3949
+ // \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
3950
+ // Allow-list. Empty means "everything that is not ignored".
3951
+ // Example: ["src/**", "packages/*/src/**"]
3952
+ include: ${list(answers.include)},
3953
+ // Globs or path segments to drop. Example: ["**/*.test.ts", "docs"]
3954
+ excludePatterns: ${list(answers.excludePatterns)},
3955
+
3956
+ // \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
3957
+ // "merge" = the preset's rules plus yours (yours win). "replace" = yours only.
3958
+ layersMode: "merge",
3959
+ // [{ name: "billing", match: "src/features/billing/**" }]
3960
+ layers: [],
3961
+ // [{ kind: "route", match: "src/app/api/**" }]
3962
+ kinds: [],
3963
+
3964
+ // \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
3965
+ // .env, *.pem, .npmrc, .ssh/ and friends are skipped unless this is true.
3966
+ includeSensitiveFiles: ${answers.includeSensitiveFiles},
3967
+ secrets: {
3968
+ // [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
3969
+ customPatterns: [],
3970
+ // Set true to drop the 21 built-in rules.
3971
+ disableDefaultPatterns: false,
3972
+ },
3973
+
3974
+ // \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
3975
+ artifacts: {
3976
+ readme: ${artifacts.readme}, // README.md
3977
+ manifest: ${artifacts.manifest}, // manifest.json \u2014 required by validate and report
3978
+ parts: ${artifacts.parts}, // parts/part-XXX.md
3979
+ navigation: ${artifacts.navigation}, // reports/navigation.md
3980
+ projectTree: ${artifacts.projectTree}, // reports/project-tree.md
3981
+ audit: ${artifacts.audit}, // reports/audit.md
3982
+ monolith: ${artifacts.monolith}, // full-codebase.json + .md (roughly doubles output)
3983
+ },
3984
+ // Skip the monolith above this size. null = no limit.
3985
+ maxMonolithBytes: null,
3986
+
3987
+ // \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
3988
+ // afterGenerate: (manifest) => { /* write your own artifact */ }
3989
+ hooks: {},
3990
+ });
3991
+ `;
3992
+ }
3993
+ function renderMinimalConfig(answers) {
3994
+ const lines = [];
3995
+ if (answers.preset !== DEFAULT_ANSWERS.preset) lines.push(` preset: ${JSON.stringify(answers.preset)},`);
3996
+ if (answers.maxPartBytes !== DEFAULT_ANSWERS.maxPartBytes) {
3997
+ lines.push(` maxPartBytes: ${size(answers.maxPartBytes)},`);
3998
+ }
3999
+ if (answers.largeFileBytes !== DEFAULT_ANSWERS.largeFileBytes) {
4000
+ lines.push(` largeFileBytes: ${size(answers.largeFileBytes)},`);
4001
+ }
4002
+ if (answers.include.length > 0) lines.push(` include: ${list(answers.include)},`);
4003
+ if (answers.excludePatterns.length > 0) {
4004
+ lines.push(` excludePatterns: ${list(answers.excludePatterns)},`);
4005
+ }
4006
+ if (answers.includeSensitiveFiles) lines.push(" includeSensitiveFiles: true,");
4007
+ if (answers.outDir !== DEFAULT_ANSWERS.outDir) lines.push(` outDir: ${JSON.stringify(answers.outDir)},`);
4008
+ const changed = Object.keys(answers.artifacts).filter(
4009
+ (name) => answers.artifacts[name] !== DEFAULT_ANSWERS.artifacts[name]
4010
+ );
4011
+ if (changed.length > 0) {
4012
+ const entries = changed.map((name) => ` ${name}: ${answers.artifacts[name]}`).join(",\n");
4013
+ lines.push(` artifacts: {
4014
+ ${entries},
4015
+ },`);
4016
+ }
4017
+ if (lines.length === 0) {
4018
+ return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
4019
+
4020
+ export default defineConfig({});
4021
+ `;
4022
+ }
4023
+ return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
4024
+
4025
+ export default defineConfig({
4026
+ ${lines.join("\n")}
4027
+ });
4028
+ `;
4029
+ }
4030
+ var PromptCancelled = class extends Error {
4031
+ constructor() {
4032
+ super("Prompt cancelled");
4033
+ this.name = "PromptCancelled";
4034
+ }
4035
+ };
4036
+ function defaultIO() {
4037
+ return { input: process.stdin, output: process.stdout };
4038
+ }
4039
+ function isInteractive(io) {
4040
+ return Boolean(io.input.isTTY && io.output.isTTY);
4041
+ }
4042
+ function moveSelection(current, delta, count) {
4043
+ if (count <= 0) return 0;
4044
+ return ((current + delta) % count + count) % count;
4045
+ }
4046
+ function toggleIndex(selected, index) {
4047
+ const next = new Set(selected);
4048
+ if (next.has(index)) next.delete(index);
4049
+ else next.add(index);
4050
+ return next;
4051
+ }
4052
+ function marker(active) {
4053
+ return active ? "\u276F" : " ";
4054
+ }
4055
+ function choiceLine(choice, active) {
4056
+ const hint = choice.hint ? ` ${choice.hint}` : "";
4057
+ return ` ${marker(active)} ${choice.label}${hint}`;
4058
+ }
4059
+ function renderSelect(message, choices, index) {
4060
+ const lines = [`\u25C6 ${message}`, ""];
4061
+ choices.forEach((choice, position) => lines.push(choiceLine(choice, position === index)));
4062
+ lines.push("", " \u2191\u2193 move \xB7 enter select");
4063
+ return lines.join("\n");
4064
+ }
4065
+ function renderMultiSelect(message, choices, index, selected) {
4066
+ const lines = [`\u25C6 ${message}`, ""];
4067
+ choices.forEach((choice, position) => {
4068
+ const box = selected.has(position) ? "\u25FC" : "\u25FB";
4069
+ const hint = choice.hint ? ` ${choice.hint}` : "";
4070
+ lines.push(` ${marker(position === index)} ${box} ${choice.label}${hint}`);
4071
+ });
4072
+ lines.push("", " \u2191\u2193 move \xB7 space toggle \xB7 enter confirm");
4073
+ return lines.join("\n");
4074
+ }
4075
+ function renderConfirm(message, value) {
4076
+ return [
4077
+ `\u25C6 ${message}`,
4078
+ "",
4079
+ ` ${marker(value)} Yes`,
4080
+ ` ${marker(!value)} No`,
4081
+ "",
4082
+ " \u2190\u2192 or y/n \xB7 enter confirm"
4083
+ ].join("\n");
4084
+ }
4085
+ function renderText(message, value, error) {
4086
+ const lines = [`\u25C6 ${message}`, "", ` ${value || "\u258F"}`];
4087
+ if (error) lines.push("", ` \u2716 ${error}`);
4088
+ return lines.join("\n");
4089
+ }
4090
+ function keyReader(input) {
4091
+ const queue = [];
4092
+ const waiters = [];
4093
+ const wasRaw = input.isRaw;
4094
+ const onKey = (_text, key) => {
4095
+ const waiter = waiters.shift();
4096
+ if (waiter) waiter(key);
4097
+ else queue.push(key);
4098
+ };
4099
+ readline.emitKeypressEvents(input);
4100
+ if (input.isTTY) input.setRawMode(true);
4101
+ input.resume();
4102
+ input.on("keypress", onKey);
4103
+ return {
4104
+ next() {
4105
+ const queued = queue.shift();
4106
+ if (queued) return Promise.resolve(queued);
4107
+ return new Promise((resolve5) => waiters.push(resolve5));
4108
+ },
4109
+ close() {
4110
+ input.off("keypress", onKey);
4111
+ if (input.isTTY) input.setRawMode(wasRaw ?? false);
4112
+ input.pause();
4113
+ }
4114
+ };
4115
+ }
4116
+ var Frame = class {
4117
+ constructor(output) {
4118
+ this.output = output;
4119
+ }
4120
+ output;
4121
+ drawn = 0;
4122
+ draw(text2) {
4123
+ this.erase();
4124
+ this.output.write(`${text2}
4125
+ `);
4126
+ this.drawn = text2.split("\n").length;
4127
+ }
4128
+ erase() {
4129
+ if (this.drawn > 0) this.output.write(`\x1B[${this.drawn}A\x1B[0J`);
4130
+ this.drawn = 0;
4131
+ }
4132
+ };
4133
+ function isCancel(key) {
4134
+ return key.ctrl === true && key.name === "c" || key.ctrl === true && key.name === "d";
4135
+ }
4136
+ async function select(io, message, choices, options = {}) {
4137
+ if (choices.length === 0) throw new Error("select() requires at least one choice.");
4138
+ const fallbackIndex = Math.max(
4139
+ 0,
4140
+ choices.findIndex((choice) => choice.value === options.defaultValue)
4141
+ );
4142
+ if (!isInteractive(io)) {
4143
+ io.output.write(`${renderSelect(message, choices, fallbackIndex)}
4144
+ `);
4145
+ return choices[fallbackIndex].value;
4146
+ }
4147
+ const frame = new Frame(io.output);
4148
+ const reader = keyReader(io.input);
4149
+ let index = fallbackIndex;
4150
+ try {
4151
+ for (; ; ) {
4152
+ frame.draw(renderSelect(message, choices, index));
4153
+ const key = await reader.next();
4154
+ if (isCancel(key)) throw new PromptCancelled();
4155
+ if (key.name === "up") index = moveSelection(index, -1, choices.length);
4156
+ else if (key.name === "down") index = moveSelection(index, 1, choices.length);
4157
+ else if (key.name === "return" || key.name === "enter") {
4158
+ frame.erase();
4159
+ return choices[index].value;
4160
+ } else if (key.name === "j") index = moveSelection(index, 1, choices.length);
4161
+ else if (key.name === "k") index = moveSelection(index, -1, choices.length);
4162
+ }
4163
+ } finally {
4164
+ reader.close();
4165
+ }
4166
+ }
4167
+ async function multiSelect(io, message, choices, options = {}) {
4168
+ const selected = /* @__PURE__ */ new Set();
4169
+ choices.forEach((choice, position) => {
4170
+ if (options.defaults?.includes(choice.value)) selected.add(position);
4171
+ });
4172
+ const result = () => choices.filter((_, position) => selected.has(position)).map((choice) => choice.value);
4173
+ if (!isInteractive(io)) {
4174
+ io.output.write(`${renderMultiSelect(message, choices, 0, selected)}
4175
+ `);
4176
+ return result();
4177
+ }
4178
+ const frame = new Frame(io.output);
4179
+ const reader = keyReader(io.input);
4180
+ let index = 0;
4181
+ try {
4182
+ for (; ; ) {
4183
+ frame.draw(renderMultiSelect(message, choices, index, selected));
4184
+ const key = await reader.next();
4185
+ if (isCancel(key)) throw new PromptCancelled();
4186
+ if (key.name === "up") index = moveSelection(index, -1, choices.length);
4187
+ else if (key.name === "down") index = moveSelection(index, 1, choices.length);
4188
+ else if (key.name === "space") {
4189
+ const next = toggleIndex(selected, index);
4190
+ selected.clear();
4191
+ for (const position of next) selected.add(position);
4192
+ } else if (key.name === "a") {
4193
+ if (selected.size === choices.length) selected.clear();
4194
+ else choices.forEach((_, position) => selected.add(position));
4195
+ } else if (key.name === "return" || key.name === "enter") {
4196
+ frame.erase();
4197
+ return result();
4198
+ }
4199
+ }
4200
+ } finally {
4201
+ reader.close();
4202
+ }
4203
+ }
4204
+ async function confirm(io, message, options = {}) {
4205
+ let value = options.defaultValue ?? true;
4206
+ if (!isInteractive(io)) {
4207
+ io.output.write(`${renderConfirm(message, value)}
4208
+ `);
4209
+ return value;
4210
+ }
4211
+ const frame = new Frame(io.output);
4212
+ const reader = keyReader(io.input);
4213
+ try {
4214
+ for (; ; ) {
4215
+ frame.draw(renderConfirm(message, value));
4216
+ const key = await reader.next();
4217
+ if (isCancel(key)) throw new PromptCancelled();
4218
+ if (key.name === "up" || key.name === "left") value = true;
4219
+ else if (key.name === "down" || key.name === "right") value = false;
4220
+ else if (key.name === "y") value = true;
4221
+ else if (key.name === "n") value = false;
4222
+ else if (key.name === "return" || key.name === "enter") {
4223
+ frame.erase();
4224
+ return value;
4225
+ }
4226
+ }
4227
+ } finally {
4228
+ reader.close();
4229
+ }
4230
+ }
4231
+ async function text(io, message, options = {}) {
4232
+ const fallback = options.defaultValue ?? "";
4233
+ if (!isInteractive(io)) {
4234
+ io.output.write(`${renderText(message, fallback)}
4235
+ `);
4236
+ return fallback;
4237
+ }
4238
+ const frame = new Frame(io.output);
4239
+ const reader = keyReader(io.input);
4240
+ let value = fallback;
4241
+ let error;
4242
+ try {
4243
+ for (; ; ) {
4244
+ frame.draw(renderText(message, value, error));
4245
+ const key = await reader.next();
4246
+ if (isCancel(key)) throw new PromptCancelled();
4247
+ error = void 0;
4248
+ if (key.name === "return" || key.name === "enter") {
4249
+ const problem = options.validate ? options.validate(value) : null;
4250
+ if (problem) {
4251
+ error = problem;
4252
+ continue;
4253
+ }
4254
+ frame.erase();
4255
+ return value;
4256
+ }
4257
+ if (key.name === "backspace") {
4258
+ value = value.slice(0, -1);
4259
+ continue;
4260
+ }
4261
+ if (key.name === "escape") {
4262
+ value = "";
4263
+ continue;
4264
+ }
4265
+ if (_isPrintable(key)) value += _keyText(key);
4266
+ }
4267
+ } finally {
4268
+ reader.close();
4269
+ }
4270
+ }
4271
+ function _isPrintable(key) {
4272
+ return typeof key.sequence === "string" && key.sequence.length === 1 && key.sequence >= " ";
4273
+ }
4274
+ function _keyText(key) {
4275
+ return key.sequence ?? "";
4276
+ }
4277
+
4278
+ // src/wizard/init.ts
4279
+ var CONFIG_CANDIDATES = [
4280
+ "snapshot.config.ts",
4281
+ "snapshot.config.js",
4282
+ "snapshot.config.mjs",
4283
+ "snapshot.config.cjs",
4284
+ "snapshot.config.json",
4285
+ ".snapshotrc.json",
4286
+ ".snapshotrc"
4287
+ ];
4288
+ var SIZE_CHOICES = [
4289
+ { value: "60k", label: "Small", hint: "60 KB \xB7 ~15k tokens \xB7 tight context windows" },
4290
+ { value: "120k", label: "Medium", hint: "120 KB \xB7 ~30k tokens \xB7 recommended" },
4291
+ { value: "300k", label: "Large", hint: "300 KB \xB7 ~75k tokens \xB7 the default" },
4292
+ { value: "custom", label: "Custom\u2026", hint: "type a size like 150k or 1.5mb" }
4293
+ ];
4294
+ function existingConfig(cwd) {
4295
+ for (const name of CONFIG_CANDIDATES) {
4296
+ if (fs9.existsSync(path10.join(cwd, name))) return name;
4297
+ }
4298
+ return null;
4299
+ }
4300
+ function artifactAnswers(options) {
4301
+ const artifacts = { ...DEFAULT_ANSWERS.artifacts };
4302
+ for (const name of options.skipArtifacts ?? []) {
4303
+ if (ARTIFACT_NAMES.includes(name)) {
4304
+ artifacts[name] = false;
4305
+ }
4306
+ }
4307
+ if (options.onlyArtifacts && options.onlyArtifacts.length > 0) {
4308
+ const keep = new Set(options.onlyArtifacts);
4309
+ for (const name of ARTIFACT_NAMES) artifacts[name] = keep.has(name);
4310
+ }
4311
+ return artifacts;
4312
+ }
4313
+ function topLevelDirs(cwd) {
4314
+ try {
4315
+ return fs9.readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort();
4316
+ } catch {
4317
+ return [];
4318
+ }
4319
+ }
4320
+ function addScripts(cwd, notes) {
4321
+ const packagePath = path10.join(cwd, "package.json");
4322
+ if (!fs9.existsSync(packagePath)) {
4323
+ notes.push("No package.json found, so npm scripts were not added.");
4324
+ return [];
4325
+ }
4326
+ let parsed;
4327
+ try {
4328
+ parsed = JSON.parse(fs9.readFileSync(packagePath, "utf8"));
4329
+ } catch {
4330
+ notes.push("Could not parse package.json; npm scripts were not added.");
4331
+ return [];
4332
+ }
4333
+ const scripts = parsed.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {};
4334
+ const added = [];
4335
+ if (!scripts.snapshot) {
4336
+ scripts.snapshot = "codebase-snapshot generate";
4337
+ added.push("snapshot");
4338
+ }
4339
+ if (!scripts["snapshot:validate"]) {
4340
+ scripts["snapshot:validate"] = "codebase-snapshot validate";
4341
+ added.push("snapshot:validate");
4342
+ }
4343
+ if (added.length === 0) return [];
4344
+ parsed.scripts = scripts;
4345
+ fs9.writeFileSync(packagePath, `${JSON.stringify(parsed, null, 2)}
4346
+ `, "utf8");
4347
+ return added;
4348
+ }
4349
+ function addGitignore(cwd) {
4350
+ const ignorePath = path10.join(cwd, ".gitignore");
4351
+ const existing = fs9.existsSync(ignorePath) ? fs9.readFileSync(ignorePath, "utf8") : "";
4352
+ const wanted = ["snapshot/", "snapshot.tmp/"];
4353
+ const added = wanted.filter((entry) => !existing.split(/\r?\n/).includes(entry));
4354
+ if (added.length === 0) return [];
4355
+ const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
4356
+ fs9.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
4357
+ `, "utf8");
4358
+ return added;
4359
+ }
4360
+ function resolvePresetChoice(answers, presetName) {
4361
+ return {
4362
+ value: presetName,
4363
+ label: presetName,
4364
+ hint: presetName === answers.preset ? "detected" : void 0
4365
+ };
4366
+ }
4367
+ async function runInit(options) {
4368
+ const io = options.io ?? defaultIO();
4369
+ const detected = detectPreset(options.cwd, options.preset);
4370
+ const notes = [];
4371
+ const answers = {
4372
+ preset: detected.presetName,
4373
+ maxPartBytes: options.maxPartBytes ?? detected.recommendedBudget ?? DEFAULT_ANSWERS.maxPartBytes,
4374
+ largeFileBytes: DEFAULT_ANSWERS.largeFileBytes,
4375
+ include: options.include ?? [],
4376
+ excludePatterns: options.exclude ?? [],
4377
+ artifacts: artifactAnswers(options),
4378
+ includeSensitiveFiles: options.includeSensitiveFiles ?? DEFAULT_ANSWERS.includeSensitiveFiles,
4379
+ outDir: options.outDir ?? DEFAULT_ANSWERS.outDir
4380
+ };
4381
+ const interactive = isInteractive(io) && options.yes !== true;
4382
+ if (interactive) {
4383
+ io.output.write("\n @ankurhalder/codebase-snapshot\n");
4384
+ io.output.write(" Turn this repository into an LLM-ready, redacted, byte-bounded snapshot.\n\n");
4385
+ io.output.write(` Detected ${detected.presetName}
4386
+ `);
4387
+ io.output.write(` Directory ${options.cwd}
4388
+
4389
+ `);
4390
+ const useDetected = await confirm(io, `Set up for ${detected.presetName}?`, {
4391
+ defaultValue: true
4392
+ });
4393
+ if (!useDetected) {
4394
+ answers.preset = await select(
4395
+ io,
4396
+ "What kind of project is this?",
4397
+ listPresetNames().map((name) => resolvePresetChoice(answers, name))
4398
+ );
4399
+ const chosen = detectPreset(options.cwd, answers.preset);
4400
+ answers.preset = chosen.presetName;
4401
+ }
4402
+ const scope = await select(
4403
+ io,
4404
+ "What should go into the snapshot?",
4405
+ [
4406
+ { value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
4407
+ { value: "folders", label: "Only specific folders\u2026" },
4408
+ { value: "no-tests", label: "Everything, but skip tests" },
4409
+ { value: "custom", label: "Let me write glob patterns" }
4410
+ ],
4411
+ { defaultValue: "all" }
4412
+ );
4413
+ if (scope === "folders") {
4414
+ const dirs = topLevelDirs(options.cwd).slice(0, 40);
4415
+ if (dirs.length === 0) {
4416
+ notes.push("No top-level directories found; the snapshot will include everything.");
4417
+ } else {
4418
+ const chosen = await multiSelect(
4419
+ io,
4420
+ "Select folders",
4421
+ dirs.map((name) => ({ value: name, label: `${name}/` })),
4422
+ { defaults: dirs }
4423
+ );
4424
+ answers.include = chosen.map((name) => `${name}/**`);
4425
+ }
4426
+ } else if (scope === "no-tests") {
4427
+ answers.excludePatterns = [
4428
+ ...answers.excludePatterns,
4429
+ "**/*.test.*",
4430
+ "**/*.spec.*",
4431
+ "tests",
4432
+ "test"
4433
+ ];
4434
+ } else if (scope === "custom") {
4435
+ const raw = await text(io, "Glob patterns to include (comma separated)", {
4436
+ defaultValue: ""
4437
+ });
4438
+ answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
4439
+ }
4440
+ const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
4441
+ defaultValue: "120k"
4442
+ });
4443
+ if (sizeChoice === "custom") {
4444
+ const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
4445
+ defaultValue: String(answers.maxPartBytes),
4446
+ validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null
4447
+ });
4448
+ answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
4449
+ } else {
4450
+ answers.maxPartBytes = sizeChoice;
4451
+ }
4452
+ const artifactChoices = [
4453
+ { value: "readme", label: "README.md", hint: "orientation page" },
4454
+ { value: "manifest", label: "manifest.json", hint: "required by validate and report" },
4455
+ { value: "parts", label: "parts/", hint: "the code payload" },
4456
+ { value: "navigation", label: "reports/navigation.md", hint: "file \u2192 part index" },
4457
+ { value: "projectTree", label: "reports/project-tree.md", hint: "ASCII tree" },
4458
+ { value: "audit", label: "reports/audit.md", hint: "security and health findings" },
4459
+ { value: "monolith", label: "full-codebase.json + .md", hint: "roughly doubles output" }
4460
+ ];
4461
+ const presetEnabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
4462
+ const selectedArtifacts = await multiSelect(
4463
+ io,
4464
+ "Which artifacts should be generated?",
4465
+ artifactChoices,
4466
+ { defaults: presetEnabled }
4467
+ );
4468
+ for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
4469
+ const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
4470
+ answers.includeSensitiveFiles = await confirm(
4471
+ io,
4472
+ "Include credential files (.env, *.pem, .npmrc, .ssh/)?",
4473
+ { defaultValue: false }
4474
+ );
4475
+ io.output.write("\n Review\n\n");
4476
+ io.output.write(` project ${answers.preset}
4477
+ `);
4478
+ io.output.write(` include ${answers.include.length ? answers.include.join(", ") : "everything"}
4479
+ `);
4480
+ io.output.write(` part size ${String(answers.maxPartBytes)}
4481
+ `);
4482
+ io.output.write(` artifacts ${enabledCount} of ${ARTIFACT_NAMES.length}
4483
+ `);
4484
+ io.output.write(` credentials ${answers.includeSensitiveFiles ? "included" : "skipped"}
4485
+
4486
+ `);
4487
+ io.output.write(" Will write snapshot.config with your answers, plus every other setting\n");
4488
+ io.output.write(" at its default \u2014 so nothing is hidden behind the docs.\n\n");
4489
+ const proceed = await confirm(io, "Write this and continue?", { defaultValue: true });
4490
+ if (!proceed) throw new PromptCancelled();
4491
+ }
4492
+ const configText = options.minimal ? renderMinimalConfig(answers) : renderConfig(answers);
4493
+ const configName = supportsTypeScriptImports() ? "snapshot.config.ts" : "snapshot.config.mjs";
4494
+ const configPath = path10.join(options.cwd, configName);
4495
+ if (options.dryRun) {
4496
+ return {
4497
+ configPath,
4498
+ configText,
4499
+ written: false,
4500
+ preset: answers.preset,
4501
+ scripts: [],
4502
+ gitignoreEntries: [],
4503
+ notes
4504
+ };
4505
+ }
4506
+ const present = existingConfig(options.cwd);
4507
+ if (present && present !== configName && !options.force) {
4508
+ throw new Error(
4509
+ `A configuration already exists (${present}). Re-run with --force to replace it.`
4510
+ );
4511
+ }
4512
+ if (fs9.existsSync(configPath) && !options.force) {
4513
+ throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
4514
+ }
4515
+ fs9.writeFileSync(configPath, configText, "utf8");
4516
+ const scripts = addScripts(options.cwd, notes);
4517
+ const gitignoreEntries = addGitignore(options.cwd);
4518
+ return {
4519
+ configPath,
4520
+ configText,
4521
+ written: true,
4522
+ preset: answers.preset,
4523
+ scripts,
4524
+ gitignoreEntries,
4525
+ notes
4526
+ };
4527
+ }
4528
+
2684
4529
  // src/index.ts
2685
4530
  var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
2686
- var GENERATOR_VERSION = "2.0.0";
4531
+ var GENERATOR_VERSION = "2.1.1";
2687
4532
  var SCHEMA_VERSION = "3.0.0";
2688
4533
  function sha256Hex2(input) {
2689
4534
  return crypto.createHash("sha256").update(input).digest("hex");
2690
4535
  }
2691
- var SUMMARY_EXTENSIONS = [
4536
+ var TS_EXTENSIONS = [
2692
4537
  ".ts",
2693
4538
  ".mts",
2694
4539
  ".cts",
@@ -2696,24 +4541,21 @@ var SUMMARY_EXTENSIONS = [
2696
4541
  ".js",
2697
4542
  ".mjs",
2698
4543
  ".cjs",
2699
- ".jsx",
2700
- ".json",
2701
- ".jsonc",
2702
- ".md",
2703
- ".mdx"
4544
+ ".jsx"
2704
4545
  ];
2705
4546
  function summarizeFile(relativePath, redactedContent) {
2706
4547
  const lower = relativePath.toLowerCase();
2707
- if (!SUMMARY_EXTENSIONS.some((extension) => lower.endsWith(extension))) {
2708
- return void 0;
2709
- }
2710
4548
  if (lower.endsWith(".json") || lower.endsWith(".jsonc")) {
2711
- return summarizeJson(redactedContent);
4549
+ return { summary: summarizeJson(redactedContent), quality: "structural" };
2712
4550
  }
2713
4551
  if (lower.endsWith(".md") || lower.endsWith(".mdx")) {
2714
- return summarizeMarkdown(redactedContent);
4552
+ return { summary: summarizeMarkdown(redactedContent), quality: "structural" };
4553
+ }
4554
+ if (TS_EXTENSIONS.some((extension) => lower.endsWith(extension))) {
4555
+ return { summary: summarizeTypeScriptSource(relativePath, redactedContent), quality: "ast" };
2715
4556
  }
2716
- return summarizeTypeScriptSource(relativePath, redactedContent);
4557
+ const profiled = summarizeWithProfile(relativePath, redactedContent);
4558
+ return profiled ? { summary: profiled.summary, quality: profiled.quality } : void 0;
2717
4559
  }
2718
4560
  function isSourceSummary(summary) {
2719
4561
  return Boolean(summary && typeof summary === "object" && "imports" in summary);
@@ -2739,8 +4581,8 @@ var IMPORT_CANDIDATE_SUFFIXES = [
2739
4581
  ];
2740
4582
  function resolveSpecifier(fromFile, specifier, knownFiles) {
2741
4583
  if (!RELATIVE_SPECIFIER.test(specifier)) return null;
2742
- const baseDir = path9.posix.dirname(fromFile);
2743
- const joined = path9.posix.normalize(path9.posix.join(baseDir, specifier));
4584
+ const baseDir = path10.posix.dirname(fromFile);
4585
+ const joined = path10.posix.normalize(path10.posix.join(baseDir, specifier));
2744
4586
  const candidate = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
2745
4587
  for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
2746
4588
  const withSuffix = `${joined}${suffix}`;
@@ -2832,11 +4674,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
2832
4674
  return errors;
2833
4675
  }
2834
4676
  function resolveSafeOutputDirs(config) {
2835
- const cwd = path9.resolve(config.cwd);
2836
- const outDir = path9.resolve(cwd, config.outDir);
2837
- const tmpDir = path9.resolve(cwd, config.tmpDir);
2838
- const root = path9.parse(cwd).root;
2839
- const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path9.sep}`);
4677
+ const cwd = path10.resolve(config.cwd);
4678
+ const outDir = path10.resolve(cwd, config.outDir);
4679
+ const tmpDir = path10.resolve(cwd, config.tmpDir);
4680
+ const root = path10.parse(cwd).root;
4681
+ const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path10.sep}`);
2840
4682
  const reject = (reason) => {
2841
4683
  throw new Error(
2842
4684
  `Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
@@ -2855,26 +4697,66 @@ function resolveSafeOutputDirs(config) {
2855
4697
  if (contains(tmpDir, outDir)) reject("outDir is inside tmpDir");
2856
4698
  return { outDir, tmpDir };
2857
4699
  }
4700
+ var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
4701
+ var OWNERSHIP_MARKER = ".codebase-snapshot";
4702
+ function writeOwnershipMarker(dir) {
4703
+ const marker2 = {
4704
+ generator: GENERATOR_NAME,
4705
+ version: GENERATOR_VERSION,
4706
+ schemaVersion: SCHEMA_VERSION
4707
+ };
4708
+ fs9.writeFileSync(path10.join(dir, OWNERSHIP_MARKER), `${JSON.stringify(marker2, null, 2)}
4709
+ `, "utf8");
4710
+ }
4711
+ function isOwnOutput(dir) {
4712
+ if (fs9.existsSync(path10.join(dir, OWNERSHIP_MARKER))) return true;
4713
+ const manifestPath = path10.join(dir, "manifest.json");
4714
+ if (!fs9.existsSync(manifestPath)) return false;
4715
+ try {
4716
+ const parsed = JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
4717
+ const generator = parsed.generator;
4718
+ return typeof parsed.schemaVersion === "string" && Array.isArray(parsed.files) && typeof generator === "object" && generator !== null && generator.name === GENERATOR_NAME;
4719
+ } catch {
4720
+ return false;
4721
+ }
4722
+ }
4723
+ function assertReplaceable(target, label) {
4724
+ if (!fs9.existsSync(target)) return;
4725
+ if (!fs9.statSync(target).isDirectory()) {
4726
+ throw new Error(
4727
+ `Refusing to write into ${label} ${target}: it exists but is not a directory.`
4728
+ );
4729
+ }
4730
+ const entries = fs9.readdirSync(target);
4731
+ const meaningful = entries.filter((name) => !PLACEHOLDER_ENTRIES.has(name));
4732
+ if (meaningful.length === 0) return;
4733
+ if (isOwnOutput(target)) return;
4734
+ const preview = meaningful.slice(0, 5).join(", ");
4735
+ const more = meaningful.length > 5 ? `, and ${meaningful.length - 5} more` : "";
4736
+ throw new Error(
4737
+ `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.`
4738
+ );
4739
+ }
2858
4740
  function swapIntoPlace(tmpDir, outDir) {
2859
4741
  const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
2860
- const hadPrevious = fs8.existsSync(outDir);
2861
- if (hadPrevious) fs8.renameSync(outDir, backupDir);
4742
+ const hadPrevious = fs9.existsSync(outDir);
4743
+ if (hadPrevious) fs9.renameSync(outDir, backupDir);
2862
4744
  try {
2863
4745
  try {
2864
- fs8.renameSync(tmpDir, outDir);
4746
+ fs9.renameSync(tmpDir, outDir);
2865
4747
  } catch (err) {
2866
4748
  if (err.code === "EXDEV") {
2867
- fs8.cpSync(tmpDir, outDir, { recursive: true });
2868
- fs8.rmSync(tmpDir, { recursive: true, force: true });
4749
+ fs9.cpSync(tmpDir, outDir, { recursive: true });
4750
+ fs9.rmSync(tmpDir, { recursive: true, force: true });
2869
4751
  } else {
2870
4752
  throw err;
2871
4753
  }
2872
4754
  }
2873
- if (hadPrevious) fs8.rmSync(backupDir, { recursive: true, force: true });
4755
+ if (hadPrevious) fs9.rmSync(backupDir, { recursive: true, force: true });
2874
4756
  } catch (err) {
2875
- if (hadPrevious && fs8.existsSync(backupDir)) {
2876
- if (fs8.existsSync(outDir)) fs8.rmSync(outDir, { recursive: true, force: true });
2877
- fs8.renameSync(backupDir, outDir);
4757
+ if (hadPrevious && fs9.existsSync(backupDir)) {
4758
+ if (fs9.existsSync(outDir)) fs9.rmSync(outDir, { recursive: true, force: true });
4759
+ fs9.renameSync(backupDir, outDir);
2878
4760
  }
2879
4761
  throw err;
2880
4762
  }
@@ -2890,7 +4772,9 @@ function artifactWarnings(artifacts) {
2890
4772
  );
2891
4773
  }
2892
4774
  if (ARTIFACT_NAMES.every((name) => artifacts[name] === false)) {
2893
- messages.push("every artifact is disabled: the snapshot directory will be empty.");
4775
+ messages.push(
4776
+ "every artifact is disabled: only the .codebase-snapshot ownership marker will be written."
4777
+ );
2894
4778
  }
2895
4779
  return messages;
2896
4780
  }
@@ -2938,7 +4822,7 @@ function generateSnapshot(configInput, options) {
2938
4822
  const gitInfo = getGitInfo(config.cwd, config.branch);
2939
4823
  const collection = collectFiles(config.cwd, {
2940
4824
  includePatterns: config.include,
2941
- excludePatterns: config.excludePatterns,
4825
+ excludePatterns: [...config.excludePatterns, ...config.presetExclude],
2942
4826
  includeSensitiveFiles: config.includeSensitiveFiles
2943
4827
  });
2944
4828
  const files = [];
@@ -2951,10 +4835,10 @@ function generateSnapshot(configInput, options) {
2951
4835
  let totalSizeBytes = 0;
2952
4836
  let largeFileCount = 0;
2953
4837
  for (const relativePath of collection.files) {
2954
- const absolutePath = path9.join(config.cwd, relativePath);
4838
+ const absolutePath = path10.join(config.cwd, relativePath);
2955
4839
  let stats;
2956
4840
  try {
2957
- stats = fs8.statSync(absolutePath);
4841
+ stats = fs9.statSync(absolutePath);
2958
4842
  if (!stats.isFile()) {
2959
4843
  skipped.push({ path: relativePath, reason: "not-file" });
2960
4844
  continue;
@@ -2969,7 +4853,7 @@ function generateSnapshot(configInput, options) {
2969
4853
  }
2970
4854
  let buffer;
2971
4855
  try {
2972
- buffer = fs8.readFileSync(absolutePath);
4856
+ buffer = fs9.readFileSync(absolutePath);
2973
4857
  } catch {
2974
4858
  skipped.push({ path: relativePath, reason: "read-error" });
2975
4859
  continue;
@@ -2979,10 +4863,10 @@ function generateSnapshot(configInput, options) {
2979
4863
  const lineCount = countLines(rawContent);
2980
4864
  totalLines += lineCount;
2981
4865
  totalSizeBytes += stats.size;
2982
- const layer = classifyLayer(relativePath, config.layers);
4866
+ const layer2 = classifyLayer(relativePath, config.layers);
2983
4867
  const kind = classifyKind(relativePath, config.kinds);
2984
4868
  const language = detectLanguage(relativePath);
2985
- bump(layerCounts, layer);
4869
+ bump(layerCounts, layer2);
2986
4870
  bump(kindCounts, kind);
2987
4871
  const { redacted, findings } = scanAndRedactSecrets(
2988
4872
  rawContent,
@@ -3006,8 +4890,11 @@ function generateSnapshot(configInput, options) {
3006
4890
  includedLines = preview.includedLines;
3007
4891
  }
3008
4892
  let summary;
4893
+ let summaryQuality;
3009
4894
  try {
3010
- summary = summarizeFile(relativePath, redacted);
4895
+ const outcome = summarizeFile(relativePath, redacted);
4896
+ summary = outcome?.summary;
4897
+ summaryQuality = outcome?.quality;
3011
4898
  if (isSourceSummary(summary) && summary.parseErrors) {
3012
4899
  warnings.push(
3013
4900
  `Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
@@ -3024,7 +4911,7 @@ function generateSnapshot(configInput, options) {
3024
4911
  lines: lineCount,
3025
4912
  hashSha256,
3026
4913
  part: "",
3027
- layer,
4914
+ layer: layer2,
3028
4915
  kind,
3029
4916
  language,
3030
4917
  inclusionMode: isPreviewTruncated ? "preview" : "full",
@@ -3034,6 +4921,7 @@ function generateSnapshot(configInput, options) {
3034
4921
  includedLines,
3035
4922
  includedBytes: Buffer.byteLength(finalContent, "utf8"),
3036
4923
  summary,
4924
+ summaryQuality,
3037
4925
  content: finalContent
3038
4926
  });
3039
4927
  }
@@ -3074,10 +4962,11 @@ function generateSnapshot(configInput, options) {
3074
4962
  includeSensitiveFiles: config.includeSensitiveFiles,
3075
4963
  artifacts: { ...config.artifacts },
3076
4964
  include: [...config.include],
4965
+ preset: config.preset,
3077
4966
  layersMode: config.layersMode,
3078
4967
  budgets: { default: config.budgets.default, byLayer: { ...config.budgets.byLayer } },
3079
4968
  excludePatterns: [...config.excludePatterns],
3080
- layers: config.layers.map((layer) => layer.name),
4969
+ layers: config.layers.map((layer2) => layer2.name),
3081
4970
  kinds: config.kinds.map((kind) => kind.kind)
3082
4971
  },
3083
4972
  counts: {
@@ -3164,45 +5053,48 @@ function generateSnapshot(configInput, options) {
3164
5053
  return manifest;
3165
5054
  }
3166
5055
  const { outDir, tmpDir } = resolveSafeOutputDirs(config);
5056
+ assertReplaceable(tmpDir, "tmpDir");
5057
+ assertReplaceable(outDir, "outDir");
3167
5058
  const { artifacts } = config;
3168
- fs8.rmSync(tmpDir, { recursive: true, force: true });
3169
- fs8.mkdirSync(tmpDir, { recursive: true });
3170
- const partsDir = path9.join(tmpDir, "parts");
3171
- const reportsDir = path9.join(tmpDir, "reports");
3172
- if (artifacts.parts) fs8.mkdirSync(partsDir, { recursive: true });
5059
+ fs9.rmSync(tmpDir, { recursive: true, force: true });
5060
+ fs9.mkdirSync(tmpDir, { recursive: true });
5061
+ writeOwnershipMarker(tmpDir);
5062
+ const partsDir = path10.join(tmpDir, "parts");
5063
+ const reportsDir = path10.join(tmpDir, "reports");
5064
+ if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
3173
5065
  if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
3174
- fs8.mkdirSync(reportsDir, { recursive: true });
5066
+ fs9.mkdirSync(reportsDir, { recursive: true });
3175
5067
  }
3176
5068
  if (artifacts.parts) {
3177
5069
  for (const part of parts) {
3178
- fs8.writeFileSync(path9.join(partsDir, part.filename), part.markdown, "utf8");
5070
+ fs9.writeFileSync(path10.join(partsDir, part.filename), part.markdown, "utf8");
3179
5071
  }
3180
5072
  }
3181
5073
  if (artifacts.projectTree) {
3182
5074
  const projectTree = generateProjectTree(files.map((file) => file.relativePath));
3183
- fs8.writeFileSync(path9.join(reportsDir, "project-tree.md"), projectTree, "utf8");
5075
+ fs9.writeFileSync(path10.join(reportsDir, "project-tree.md"), projectTree, "utf8");
3184
5076
  }
3185
5077
  if (artifacts.navigation) {
3186
- fs8.writeFileSync(path9.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
5078
+ fs9.writeFileSync(path10.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
3187
5079
  }
3188
5080
  if (artifacts.audit) {
3189
- fs8.writeFileSync(
3190
- path9.join(reportsDir, "audit.md"),
5081
+ fs9.writeFileSync(
5082
+ path10.join(reportsDir, "audit.md"),
3191
5083
  generateAuditReport(manifest, secretFindings),
3192
5084
  "utf8"
3193
5085
  );
3194
5086
  }
3195
5087
  if (artifacts.readme) {
3196
- fs8.writeFileSync(path9.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
5088
+ fs9.writeFileSync(path10.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
3197
5089
  }
3198
5090
  if (artifacts.manifest) {
3199
- fs8.writeFileSync(path9.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
5091
+ fs9.writeFileSync(path10.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
3200
5092
  }
3201
5093
  if (monolithJson !== null) {
3202
- fs8.writeFileSync(path9.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
5094
+ fs9.writeFileSync(path10.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
3203
5095
  }
3204
5096
  if (monolithMarkdown !== null) {
3205
- fs8.writeFileSync(path9.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
5097
+ fs9.writeFileSync(path10.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
3206
5098
  }
3207
5099
  swapIntoPlace(tmpDir, outDir);
3208
5100
  runAfterGenerate(config, manifest);
@@ -3212,6 +5104,6 @@ async function generateSnapshotAsync(configInput, options) {
3212
5104
  return generateSnapshot(configInput, options);
3213
5105
  }
3214
5106
 
3215
- export { ARTIFACT_NAMES, DEFAULT_SECRET_PATTERNS, GENERATOR_NAME, GENERATOR_VERSION, MIN_MAX_PART_BYTES, SCHEMA_VERSION, anchorForPath, assertUsableConfigInput, collectFiles, compareManifests, countLines, defineConfig, detectLanguage, detectPreset, explainPath, fenceForContent, generateProjectTree, generateSnapshot, generateSnapshotAsync, globToRegExp, isBinaryBuffer, isBinaryExtension, isBinaryFile, isResolvedConfig, isSensitivePath, loadConfigFile, loadConfigFileDetailed, loadManifest, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, renderDiffText, renderExplainText, renumberParts, resolveConfig, resolveSafeOutputDirs, scanAndRedactSecrets, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, supportsTypeScriptImports, truncatePreview, truncateToBytes, validateSnapshot };
3216
- //# sourceMappingURL=chunk-RGDVLQTT.js.map
3217
- //# sourceMappingURL=chunk-RGDVLQTT.js.map
5107
+ export { ARTIFACT_NAMES, DEFAULT_ANSWERS, DEFAULT_SECRET_PATTERNS, GENERATOR_NAME, GENERATOR_VERSION, LANGUAGE_PROFILES, MIN_MAX_PART_BYTES, PRESETS, PromptCancelled, SCHEMA_VERSION, anchorForPath, assertReplaceable, assertUsableConfigInput, collectFiles, compareManifests, countLines, defineConfig, detectLanguage, detectPreset, explainPath, fenceForContent, generateProjectTree, generateSnapshot, generateSnapshotAsync, getPreset, globToRegExp, isBinaryBuffer, isBinaryExtension, isBinaryFile, isInteractive, isResolvedConfig, isSensitivePath, listPresetNames, loadConfigFile, loadConfigFileDetailed, loadManifest, markResolvedConfig, matchRule, matchesExclude, normalizeResolvedConfig, parseByteSize, partitionFiles, profileForPath, renderConfig, renderDiffText, renderExplainText, renderMinimalConfig, renumberParts, resolveConfig, resolveSafeOutputDirs, runInit, scanAndRedactSecrets, stripJsonComments, summarizeJson, summarizeMarkdown, summarizeTypeScriptSource, summarizeWithProfile, supportsTypeScriptImports, truncatePreview, truncateToBytes, unknownPresetMessage, validateSnapshot };
5108
+ //# sourceMappingURL=chunk-SUOJL5EX.js.map
5109
+ //# sourceMappingURL=chunk-SUOJL5EX.js.map