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