@ankurhalder/codebase-snapshot 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -0
- package/dist/bin.js +2399 -372
- package/dist/{chunk-RGDVLQTT.js → chunk-SUOJL5EX.js} +2247 -355
- package/dist/cli.cjs +2417 -397
- package/dist/cli.d.cts +9 -1
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +160 -31
- package/dist/index.cjs +2260 -354
- 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 +150 -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
|
|
@@ -52,7 +52,13 @@ function compareManifests(before, after) {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
function loadManifest(target) {
|
|
55
|
-
|
|
55
|
+
let isDirectory;
|
|
56
|
+
try {
|
|
57
|
+
isDirectory = fs.statSync(target).isDirectory();
|
|
58
|
+
} catch {
|
|
59
|
+
throw new Error(`No snapshot found at ${target}: the path does not exist.`);
|
|
60
|
+
}
|
|
61
|
+
const manifestPath = isDirectory ? path.join(target, "manifest.json") : target;
|
|
56
62
|
let raw;
|
|
57
63
|
try {
|
|
58
64
|
raw = fs.readFileSync(manifestPath, "utf8");
|
|
@@ -68,6 +74,9 @@ function loadManifest(target) {
|
|
|
68
74
|
if (!parsed || !Array.isArray(parsed.files)) {
|
|
69
75
|
throw new Error(`${manifestPath} is not a snapshot manifest (no "files" array).`);
|
|
70
76
|
}
|
|
77
|
+
if (!parsed.counts || typeof parsed.counts !== "object") {
|
|
78
|
+
throw new Error(`${manifestPath} is not a snapshot manifest (no "counts" object).`);
|
|
79
|
+
}
|
|
71
80
|
return parsed;
|
|
72
81
|
}
|
|
73
82
|
function renderDiffText(diff) {
|
|
@@ -100,9 +109,141 @@ import * as fs4 from "fs";
|
|
|
100
109
|
import * as path4 from "path";
|
|
101
110
|
import { pathToFileURL } from "url";
|
|
102
111
|
|
|
103
|
-
// src/presets/
|
|
112
|
+
// src/presets/context.ts
|
|
104
113
|
import * as fs2 from "fs";
|
|
105
114
|
import * as path2 from "path";
|
|
115
|
+
var MANIFEST_FILES = [
|
|
116
|
+
"package.json",
|
|
117
|
+
"pyproject.toml",
|
|
118
|
+
"requirements.txt",
|
|
119
|
+
"setup.py",
|
|
120
|
+
"go.mod",
|
|
121
|
+
"Cargo.toml",
|
|
122
|
+
"pom.xml",
|
|
123
|
+
"build.gradle",
|
|
124
|
+
"build.gradle.kts",
|
|
125
|
+
"composer.json",
|
|
126
|
+
"Gemfile"
|
|
127
|
+
];
|
|
128
|
+
var MANIFEST_MAX_BYTES = 1e5;
|
|
129
|
+
function readManifest(cwd, name) {
|
|
130
|
+
const full = path2.join(cwd, name);
|
|
131
|
+
try {
|
|
132
|
+
const stats = fs2.statSync(full);
|
|
133
|
+
if (!stats.isFile() || stats.size > MANIFEST_MAX_BYTES) return null;
|
|
134
|
+
return fs2.readFileSync(full, "utf8");
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function collectDependencies(manifests) {
|
|
140
|
+
const names = /* @__PURE__ */ new Set();
|
|
141
|
+
const raw = manifests["package.json"];
|
|
142
|
+
if (!raw) return names;
|
|
143
|
+
try {
|
|
144
|
+
const parsed = JSON.parse(raw);
|
|
145
|
+
for (const section of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
|
|
146
|
+
const block = parsed[section];
|
|
147
|
+
if (block && typeof block === "object") {
|
|
148
|
+
for (const name of Object.keys(block)) names.add(name);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
return names;
|
|
154
|
+
}
|
|
155
|
+
function buildPresetContext(cwd) {
|
|
156
|
+
const files = /* @__PURE__ */ new Set();
|
|
157
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
158
|
+
try {
|
|
159
|
+
for (const entry of fs2.readdirSync(cwd, { withFileTypes: true })) {
|
|
160
|
+
if (entry.isDirectory()) dirs.add(entry.name);
|
|
161
|
+
else if (entry.isFile()) files.add(entry.name);
|
|
162
|
+
}
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
const manifests = {};
|
|
166
|
+
for (const name of MANIFEST_FILES) {
|
|
167
|
+
const content = readManifest(cwd, name);
|
|
168
|
+
if (content !== null) manifests[name] = content;
|
|
169
|
+
}
|
|
170
|
+
return {
|
|
171
|
+
cwd,
|
|
172
|
+
files,
|
|
173
|
+
dirs,
|
|
174
|
+
dependencies: collectDependencies(manifests),
|
|
175
|
+
manifests
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
function hasFile(context, name) {
|
|
179
|
+
return context.files.has(name);
|
|
180
|
+
}
|
|
181
|
+
function hasFileStartingWith(context, prefix) {
|
|
182
|
+
for (const file of context.files) {
|
|
183
|
+
if (file.startsWith(prefix)) return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
function manifestContains(context, manifest, needle) {
|
|
188
|
+
const content = context.manifests[manifest];
|
|
189
|
+
if (!content) return false;
|
|
190
|
+
return content.toLowerCase().includes(needle.toLowerCase());
|
|
191
|
+
}
|
|
192
|
+
function hasDependency(context, ...names) {
|
|
193
|
+
return names.some((name) => context.dependencies.has(name));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// src/presets/helpers.ts
|
|
197
|
+
function layer(name, ...prefixes) {
|
|
198
|
+
return {
|
|
199
|
+
name,
|
|
200
|
+
match: (filePath) => prefixes.some((prefix) => filePath.startsWith(prefix))
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function kindOf(name, ...needles) {
|
|
204
|
+
return {
|
|
205
|
+
kind: name,
|
|
206
|
+
match: (filePath) => needles.some((needle) => filePath.includes(needle))
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function suffixKind(name, ...suffixes) {
|
|
210
|
+
return {
|
|
211
|
+
kind: name,
|
|
212
|
+
match: (filePath) => suffixes.some((suffix) => filePath.endsWith(suffix))
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function commonLayers() {
|
|
216
|
+
return [
|
|
217
|
+
layer("scripts", "scripts/", "tools/"),
|
|
218
|
+
layer("docs", "docs/", "documentation/"),
|
|
219
|
+
{
|
|
220
|
+
name: "config",
|
|
221
|
+
match: (filePath) => filePath.startsWith(".github/") || filePath.startsWith(".husky/") || filePath.startsWith(".vscode/") || filePath.startsWith(".") || filePath.includes("config")
|
|
222
|
+
},
|
|
223
|
+
{ name: "root", match: (filePath) => !filePath.includes("/") }
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
function commonKinds() {
|
|
227
|
+
return [
|
|
228
|
+
{
|
|
229
|
+
kind: "test",
|
|
230
|
+
match: (filePath) => {
|
|
231
|
+
const lower = filePath.toLowerCase();
|
|
232
|
+
return lower.includes(".test.") || lower.includes(".spec.") || lower.startsWith("test/") || lower.startsWith("tests/") || lower.includes("_test.") || lower.endsWith("_test.go") || lower.includes("__tests__");
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
suffixKind("doc", ".md", ".mdx", ".rst", ".txt"),
|
|
236
|
+
suffixKind("data", ".json", ".yaml", ".yml", ".csv", ".toml"),
|
|
237
|
+
{
|
|
238
|
+
kind: "script",
|
|
239
|
+
match: (filePath) => filePath.startsWith("scripts/") || /\.(sh|ps1|bat)$/i.test(filePath)
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
kind: "config",
|
|
243
|
+
match: (filePath) => filePath.startsWith(".") || filePath.includes("config") || filePath === "package.json" || filePath === "tsconfig.json"
|
|
244
|
+
}
|
|
245
|
+
];
|
|
246
|
+
}
|
|
106
247
|
|
|
107
248
|
// src/presets/nextjs.ts
|
|
108
249
|
var NEXTJS_LAYERS = [
|
|
@@ -159,6 +300,47 @@ var NEXTJS_KINDS = [
|
|
|
159
300
|
}
|
|
160
301
|
];
|
|
161
302
|
|
|
303
|
+
// src/presets/node.ts
|
|
304
|
+
var NODE_LAYERS = [
|
|
305
|
+
{ name: "core", match: (p) => p.startsWith("src/core/") || p.startsWith("core/") },
|
|
306
|
+
{ name: "protocols", match: (p) => p.startsWith("src/protocols/") || p.startsWith("protocols/") },
|
|
307
|
+
{ name: "providers", match: (p) => p.startsWith("src/providers/") || p.startsWith("providers/") },
|
|
308
|
+
{ name: "security", match: (p) => p.startsWith("src/security/") || p.startsWith("security/") },
|
|
309
|
+
{ name: "db", match: (p) => p.startsWith("src/db/") || p.startsWith("db/") || p.startsWith("prisma/") },
|
|
310
|
+
{ name: "api", match: (p) => p.startsWith("src/api/") || p.startsWith("api/") || p.startsWith("routes/") },
|
|
311
|
+
{ name: "services", match: (p) => p.startsWith("src/services/") || p.startsWith("services/") },
|
|
312
|
+
{ name: "controllers", match: (p) => p.startsWith("src/controllers/") || p.startsWith("controllers/") },
|
|
313
|
+
{ name: "scripts", match: (p) => p.startsWith("scripts/") },
|
|
314
|
+
{ name: "docs", match: (p) => p.startsWith("docs/") },
|
|
315
|
+
{
|
|
316
|
+
name: "config",
|
|
317
|
+
match: (p) => p.startsWith(".github/") || p.startsWith(".husky/") || p.startsWith(".") || p.includes("config")
|
|
318
|
+
},
|
|
319
|
+
{ name: "root", match: (p) => !p.includes("/") }
|
|
320
|
+
];
|
|
321
|
+
var NODE_KINDS = [
|
|
322
|
+
{ kind: "test", match: (p) => /\.(test|spec)\.[jt]sx?$/.test(p.toLowerCase()) },
|
|
323
|
+
{ kind: "route", match: (p) => p.includes("/routes/") || p.includes("route.") || p.includes("/api/") },
|
|
324
|
+
{ kind: "controller", match: (p) => p.includes("controller") },
|
|
325
|
+
{ kind: "service", match: (p) => p.includes("service") },
|
|
326
|
+
{ kind: "provider", match: (p) => p.includes("provider") },
|
|
327
|
+
{ kind: "model", match: (p) => p.includes("model") || p.includes("entity") },
|
|
328
|
+
{ kind: "schema", match: (p) => p.includes("schema") || p.includes("zod") || p.includes("dto") },
|
|
329
|
+
{ kind: "type", match: (p) => p.endsWith(".d.ts") || p.includes("types.") || p.includes("type.") },
|
|
330
|
+
{ kind: "security", match: (p) => p.includes("auth") || p.includes("security") || p.includes("guard") },
|
|
331
|
+
{ kind: "db", match: (p) => p.includes("db") || p.includes("database") || p.includes("migration") },
|
|
332
|
+
{ kind: "data", match: (p) => p.endsWith(".json") || p.endsWith(".csv") },
|
|
333
|
+
{ kind: "doc", match: (p) => p.endsWith(".md") || p.endsWith(".mdx") },
|
|
334
|
+
{ kind: "script", match: (p) => p.startsWith("scripts/") || p.endsWith(".sh") || p.endsWith(".ps1") },
|
|
335
|
+
{
|
|
336
|
+
kind: "config",
|
|
337
|
+
match: (p) => {
|
|
338
|
+
const base = p.split("/").pop() || "";
|
|
339
|
+
return ["package.json", "tsconfig.json", "Dockerfile"].includes(base) || base.startsWith(".");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
];
|
|
343
|
+
|
|
162
344
|
// src/presets/vite.ts
|
|
163
345
|
var VITE_LAYERS = [
|
|
164
346
|
{ name: "pages", match: (p) => p.startsWith("src/pages/") || p.startsWith("src/views/") },
|
|
@@ -206,44 +388,419 @@ var VITE_KINDS = [
|
|
|
206
388
|
}
|
|
207
389
|
];
|
|
208
390
|
|
|
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/") },
|
|
391
|
+
// src/presets/js-presets.ts
|
|
392
|
+
var NEXT_LIKE_EXCLUDES = [".svelte-kit", ".astro", ".vite", ".output"];
|
|
393
|
+
var JS_PRESETS = [
|
|
221
394
|
{
|
|
222
|
-
name: "
|
|
223
|
-
|
|
395
|
+
name: "nextjs",
|
|
396
|
+
priority: 100,
|
|
397
|
+
detect: (context) => hasFileStartingWith(context, "next.config") || hasDependency(context, "next"),
|
|
398
|
+
layers: NEXTJS_LAYERS,
|
|
399
|
+
kinds: NEXTJS_KINDS,
|
|
400
|
+
exclude: NEXT_LIKE_EXCLUDES,
|
|
401
|
+
recommendedBudget: "300k"
|
|
224
402
|
},
|
|
225
|
-
{
|
|
403
|
+
{
|
|
404
|
+
name: "nuxt",
|
|
405
|
+
priority: 98,
|
|
406
|
+
detect: (context) => hasFileStartingWith(context, "nuxt.config") || hasDependency(context, "nuxt", "nuxt3"),
|
|
407
|
+
layers: [
|
|
408
|
+
layer("pages", "pages/", "src/pages/"),
|
|
409
|
+
layer("components", "components/", "src/components/"),
|
|
410
|
+
layer("composables", "composables/", "src/composables/"),
|
|
411
|
+
layer("server", "server/", "src/server/", "api/"),
|
|
412
|
+
layer("stores", "stores/", "src/stores/"),
|
|
413
|
+
layer("layouts", "layouts/", "src/layouts/"),
|
|
414
|
+
...commonLayers()
|
|
415
|
+
],
|
|
416
|
+
kinds: [
|
|
417
|
+
kindOf("api-route", "server/api/", "server/routes/"),
|
|
418
|
+
suffixKind("component", ".vue"),
|
|
419
|
+
kindOf("composable", "composables/"),
|
|
420
|
+
kindOf("store", "stores/", "store/"),
|
|
421
|
+
kindOf("layout", "layouts/"),
|
|
422
|
+
...commonKinds()
|
|
423
|
+
],
|
|
424
|
+
exclude: [".nuxt", ".output"],
|
|
425
|
+
recommendedBudget: "200k"
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
name: "sveltekit",
|
|
429
|
+
priority: 96,
|
|
430
|
+
detect: (context) => hasFileStartingWith(context, "svelte.config") || hasDependency(context, "@sveltejs/kit", "svelte"),
|
|
431
|
+
layers: [
|
|
432
|
+
layer("routes", "src/routes/", "routes/"),
|
|
433
|
+
layer("lib", "src/lib/", "lib/"),
|
|
434
|
+
layer("components", "src/lib/components/", "src/components/"),
|
|
435
|
+
layer("stores", "src/lib/stores/", "src/stores/"),
|
|
436
|
+
...commonLayers()
|
|
437
|
+
],
|
|
438
|
+
kinds: [
|
|
439
|
+
kindOf("route", "+page.", "+server.", "+layout."),
|
|
440
|
+
suffixKind("component", ".svelte"),
|
|
441
|
+
kindOf("store", "stores/"),
|
|
442
|
+
kindOf("params", "params/"),
|
|
443
|
+
...commonKinds()
|
|
444
|
+
],
|
|
445
|
+
exclude: [".svelte-kit", ".vercel"],
|
|
446
|
+
recommendedBudget: "200k"
|
|
447
|
+
},
|
|
448
|
+
{
|
|
449
|
+
name: "remix",
|
|
450
|
+
priority: 94,
|
|
451
|
+
detect: (context) => hasDependency(context, "@remix-run/react", "@remix-run/node", "@remix-run/dev"),
|
|
452
|
+
layers: [
|
|
453
|
+
layer("routes", "app/routes/", "routes/"),
|
|
454
|
+
layer("components", "app/components/"),
|
|
455
|
+
layer("lib", "app/lib/", "app/utils/"),
|
|
456
|
+
...commonLayers()
|
|
457
|
+
],
|
|
458
|
+
kinds: [kindOf("route", "routes/"), ...commonKinds()],
|
|
459
|
+
exclude: [".cache", "public/build"],
|
|
460
|
+
recommendedBudget: "200k"
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: "astro",
|
|
464
|
+
priority: 92,
|
|
465
|
+
detect: (context) => hasFileStartingWith(context, "astro.config") || hasDependency(context, "astro"),
|
|
466
|
+
layers: [
|
|
467
|
+
layer("pages", "src/pages/"),
|
|
468
|
+
layer("layouts", "src/layouts/"),
|
|
469
|
+
layer("components", "src/components/"),
|
|
470
|
+
layer("content", "src/content/"),
|
|
471
|
+
...commonLayers()
|
|
472
|
+
],
|
|
473
|
+
kinds: [suffixKind("page", ".astro"), kindOf("layout", "layouts/"), ...commonKinds()],
|
|
474
|
+
exclude: [".astro", "dist"],
|
|
475
|
+
recommendedBudget: "150k"
|
|
476
|
+
},
|
|
477
|
+
{
|
|
478
|
+
name: "angular",
|
|
479
|
+
priority: 90,
|
|
480
|
+
detect: (context) => hasFile(context, "angular.json") || hasDependency(context, "@angular/core"),
|
|
481
|
+
layers: [
|
|
482
|
+
layer("app", "src/app/"),
|
|
483
|
+
layer("features", "src/app/features/"),
|
|
484
|
+
layer("shared", "src/app/shared/"),
|
|
485
|
+
layer("core", "src/app/core/"),
|
|
486
|
+
layer("assets", "src/assets/"),
|
|
487
|
+
...commonLayers()
|
|
488
|
+
],
|
|
489
|
+
kinds: [
|
|
490
|
+
suffixKind("component", ".component.ts"),
|
|
491
|
+
suffixKind("service", ".service.ts"),
|
|
492
|
+
suffixKind("module", ".module.ts"),
|
|
493
|
+
suffixKind("guard", ".guard.ts"),
|
|
494
|
+
suffixKind("pipe", ".pipe.ts"),
|
|
495
|
+
suffixKind("directive", ".directive.ts"),
|
|
496
|
+
...commonKinds()
|
|
497
|
+
],
|
|
498
|
+
exclude: [".angular", "dist"],
|
|
499
|
+
recommendedBudget: "200k"
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
name: "solid",
|
|
503
|
+
priority: 88,
|
|
504
|
+
detect: (context) => hasDependency(context, "solid-js", "solid-start"),
|
|
505
|
+
layers: [
|
|
506
|
+
layer("pages", "src/routes/", "src/pages/"),
|
|
507
|
+
layer("components", "src/components/"),
|
|
508
|
+
...commonLayers()
|
|
509
|
+
],
|
|
510
|
+
kinds: [kindOf("route", "routes/"), ...commonKinds()],
|
|
511
|
+
exclude: [".solid", "dist"],
|
|
512
|
+
recommendedBudget: "120k"
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: "vue-vite",
|
|
516
|
+
priority: 86,
|
|
517
|
+
detect: (context) => hasDependency(context, "vue") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
|
|
518
|
+
layers: [
|
|
519
|
+
layer("pages", "src/pages/", "src/views/"),
|
|
520
|
+
layer("components", "src/components/"),
|
|
521
|
+
layer("composables", "src/composables/", "src/hooks/"),
|
|
522
|
+
layer("stores", "src/stores/", "src/store/"),
|
|
523
|
+
layer("services", "src/services/", "src/api/"),
|
|
524
|
+
layer("shared", "src/shared/", "src/common/"),
|
|
525
|
+
layer("styles", "src/styles/", "src/assets/"),
|
|
526
|
+
...commonLayers()
|
|
527
|
+
],
|
|
528
|
+
kinds: [
|
|
529
|
+
suffixKind("component", ".vue"),
|
|
530
|
+
kindOf("composable", "composables/", "use"),
|
|
531
|
+
kindOf("store", "stores/", "store/"),
|
|
532
|
+
kindOf("service", "services/", "api/"),
|
|
533
|
+
suffixKind("style", ".css", ".scss", ".sass", ".less"),
|
|
534
|
+
...commonKinds()
|
|
535
|
+
],
|
|
536
|
+
exclude: [".vite", "dist"],
|
|
537
|
+
recommendedBudget: "120k"
|
|
538
|
+
},
|
|
539
|
+
{
|
|
540
|
+
name: "react-vite",
|
|
541
|
+
priority: 84,
|
|
542
|
+
detect: (context) => hasDependency(context, "react") && (hasDependency(context, "vite") || hasFileStartingWith(context, "vite.config")),
|
|
543
|
+
layers: VITE_LAYERS,
|
|
544
|
+
kinds: VITE_KINDS,
|
|
545
|
+
exclude: [".vite", "dist"],
|
|
546
|
+
recommendedBudget: "120k"
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "nest",
|
|
550
|
+
priority: 80,
|
|
551
|
+
detect: (context) => hasDependency(context, "@nestjs/core"),
|
|
552
|
+
layers: [
|
|
553
|
+
layer("modules", "src/modules/", "modules/"),
|
|
554
|
+
layer("controllers", "src/controllers/", "controllers/"),
|
|
555
|
+
layer("providers", "src/providers/", "providers/"),
|
|
556
|
+
layer("core", "src/core/"),
|
|
557
|
+
layer("security", "src/security/", "src/auth/"),
|
|
558
|
+
layer("db", "src/db/", "src/database/", "prisma/"),
|
|
559
|
+
...commonLayers()
|
|
560
|
+
],
|
|
561
|
+
kinds: [
|
|
562
|
+
suffixKind("controller", ".controller.ts"),
|
|
563
|
+
suffixKind("service", ".service.ts"),
|
|
564
|
+
suffixKind("module", ".module.ts"),
|
|
565
|
+
suffixKind("guard", ".guard.ts"),
|
|
566
|
+
suffixKind("dto", ".dto.ts"),
|
|
567
|
+
suffixKind("entity", ".entity.ts"),
|
|
568
|
+
kindOf("migration", "migrations/"),
|
|
569
|
+
...commonKinds()
|
|
570
|
+
],
|
|
571
|
+
exclude: ["dist", "node_modules"],
|
|
572
|
+
recommendedBudget: "200k"
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: "node-express",
|
|
576
|
+
priority: 78,
|
|
577
|
+
detect: (context) => hasDependency(context, "express", "fastify", "koa", "@hapi/hapi"),
|
|
578
|
+
layers: NODE_LAYERS,
|
|
579
|
+
kinds: NODE_KINDS,
|
|
580
|
+
exclude: ["dist"],
|
|
581
|
+
recommendedBudget: "200k"
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
name: "node-lib",
|
|
585
|
+
// A bare package.json is often just tooling around a non-JS project, so it
|
|
586
|
+
// ranks below every definitive language manifest.
|
|
587
|
+
priority: 45,
|
|
588
|
+
detect: (context) => hasFile(context, "package.json"),
|
|
589
|
+
layers: [
|
|
590
|
+
layer("src", "src/"),
|
|
591
|
+
layer("lib", "lib/"),
|
|
592
|
+
layer("test", "test/", "tests/", "__tests__/"),
|
|
593
|
+
...commonLayers()
|
|
594
|
+
],
|
|
595
|
+
kinds: [suffixKind("type", ".d.ts"), ...commonKinds()],
|
|
596
|
+
exclude: ["dist"],
|
|
597
|
+
recommendedBudget: "120k"
|
|
598
|
+
}
|
|
226
599
|
];
|
|
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") },
|
|
600
|
+
|
|
601
|
+
// src/presets/lang-presets.ts
|
|
602
|
+
var anyFileEndingWith = (context, suffix) => Array.from(context.files).some((file) => file.endsWith(suffix));
|
|
603
|
+
var DIR = (name) => `${name}/`;
|
|
604
|
+
var LANGUAGE_PRESETS = [
|
|
241
605
|
{
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
606
|
+
name: "python-django",
|
|
607
|
+
priority: 56,
|
|
608
|
+
detect: (context) => hasFile(context, "manage.py") || manifestContains(context, "pyproject.toml", "django") || manifestContains(context, "requirements.txt", "django"),
|
|
609
|
+
layers: [
|
|
610
|
+
layer("views", DIR("views"), DIR("app/views"), DIR("apps/views")),
|
|
611
|
+
layer("models", DIR("models"), DIR("app/models"), DIR("apps/models")),
|
|
612
|
+
layer("serializers", DIR("serializers")),
|
|
613
|
+
layer("urls", "urls.py"),
|
|
614
|
+
layer("migrations", DIR("migrations")),
|
|
615
|
+
layer("settings", "settings.py", "settings/"),
|
|
616
|
+
layer("templates", DIR("templates")),
|
|
617
|
+
...commonLayers()
|
|
618
|
+
],
|
|
619
|
+
kinds: [
|
|
620
|
+
suffixKind("model", "models.py"),
|
|
621
|
+
suffixKind("view", "views.py"),
|
|
622
|
+
suffixKind("serializer", "serializers.py"),
|
|
623
|
+
suffixKind("urlconf", "urls.py"),
|
|
624
|
+
kindOf("migration", "migrations/"),
|
|
625
|
+
suffixKind("settings", "settings.py"),
|
|
626
|
+
...commonKinds()
|
|
627
|
+
],
|
|
628
|
+
exclude: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".tox", ".eggs", "staticfiles", "media"],
|
|
629
|
+
recommendedBudget: "150k"
|
|
630
|
+
},
|
|
631
|
+
{
|
|
632
|
+
name: "python-fastapi",
|
|
633
|
+
priority: 54,
|
|
634
|
+
detect: (context) => manifestContains(context, "requirements.txt", "fastapi") || manifestContains(context, "requirements.txt", "flask") || manifestContains(context, "pyproject.toml", "fastapi") || manifestContains(context, "pyproject.toml", "flask"),
|
|
635
|
+
layers: [
|
|
636
|
+
layer("routers", DIR("routers"), DIR("routes"), DIR("api")),
|
|
637
|
+
layer("models", DIR("models"), DIR("db")),
|
|
638
|
+
layer("schemas", DIR("schemas")),
|
|
639
|
+
layer("services", DIR("services")),
|
|
640
|
+
layer("core", DIR("core")),
|
|
641
|
+
layer("tests", DIR("tests")),
|
|
642
|
+
...commonLayers()
|
|
643
|
+
],
|
|
644
|
+
kinds: [
|
|
645
|
+
kindOf("router", "routers/", "routes/", "api/"),
|
|
646
|
+
kindOf("model", "models/"),
|
|
647
|
+
kindOf("schema", "schemas/"),
|
|
648
|
+
kindOf("service", "services/"),
|
|
649
|
+
...commonKinds()
|
|
650
|
+
],
|
|
651
|
+
exclude: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".tox", ".eggs"],
|
|
652
|
+
recommendedBudget: "150k"
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
name: "python-lib",
|
|
656
|
+
// Below node-lib: a Node repo with a stray .py file is still a Node repo.
|
|
657
|
+
priority: 30,
|
|
658
|
+
detect: (context) => hasFile(context, "pyproject.toml") || hasFile(context, "setup.py") || hasFile(context, "requirements.txt") || anyFileEndingWith(context, ".py"),
|
|
659
|
+
layers: [
|
|
660
|
+
layer("package", DIR("src"), DIR("lib")),
|
|
661
|
+
layer("tests", DIR("tests"), DIR("test")),
|
|
662
|
+
...commonLayers()
|
|
663
|
+
],
|
|
664
|
+
kinds: [suffixKind("module", ".py"), ...commonKinds()],
|
|
665
|
+
exclude: ["__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".tox", ".eggs"],
|
|
666
|
+
recommendedBudget: "120k"
|
|
667
|
+
},
|
|
668
|
+
{
|
|
669
|
+
name: "go",
|
|
670
|
+
priority: 50,
|
|
671
|
+
detect: (context) => hasFile(context, "go.mod"),
|
|
672
|
+
layers: [
|
|
673
|
+
layer("cmd", "cmd/"),
|
|
674
|
+
layer("internal", "internal/"),
|
|
675
|
+
layer("pkg", "pkg/"),
|
|
676
|
+
layer("api", "api/", "proto/"),
|
|
677
|
+
layer("migrations", "migrations/"),
|
|
678
|
+
...commonLayers()
|
|
679
|
+
],
|
|
680
|
+
kinds: [
|
|
681
|
+
kindOf("main", "cmd/"),
|
|
682
|
+
kindOf("handler", "handler", "server", "http"),
|
|
683
|
+
kindOf("service", "service"),
|
|
684
|
+
kindOf("repository", "repository", "store", "repo"),
|
|
685
|
+
kindOf("model", "model", "entity"),
|
|
686
|
+
suffixKind("test", "_test.go"),
|
|
687
|
+
...commonKinds()
|
|
688
|
+
],
|
|
689
|
+
exclude: ["vendor", "testdata"],
|
|
690
|
+
recommendedBudget: "200k"
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: "rust",
|
|
694
|
+
priority: 48,
|
|
695
|
+
detect: (context) => hasFile(context, "Cargo.toml"),
|
|
696
|
+
layers: [
|
|
697
|
+
layer("src", "src/"),
|
|
698
|
+
layer("crates", "crates/"),
|
|
699
|
+
layer("bins", "src/bin/"),
|
|
700
|
+
layer("tests", "tests/"),
|
|
701
|
+
layer("benches", "benches/"),
|
|
702
|
+
...commonLayers()
|
|
703
|
+
],
|
|
704
|
+
kinds: [suffixKind("module", ".rs"), suffixKind("test", "_test.rs"), ...commonKinds()],
|
|
705
|
+
exclude: ["target"],
|
|
706
|
+
recommendedBudget: "200k"
|
|
707
|
+
},
|
|
708
|
+
{
|
|
709
|
+
name: "java-spring",
|
|
710
|
+
priority: 46,
|
|
711
|
+
detect: (context) => hasFile(context, "pom.xml") || hasFile(context, "build.gradle") || hasFile(context, "build.gradle.kts"),
|
|
712
|
+
layers: [
|
|
713
|
+
layer("api", "src/main/java/"),
|
|
714
|
+
layer("test", "src/test/java/"),
|
|
715
|
+
layer("resources", "src/main/resources/"),
|
|
716
|
+
layer("migrations", "src/main/resources/db/"),
|
|
717
|
+
...commonLayers()
|
|
718
|
+
],
|
|
719
|
+
kinds: [
|
|
720
|
+
kindOf("controller", "controller"),
|
|
721
|
+
kindOf("service", "service"),
|
|
722
|
+
kindOf("repository", "repository", "dao"),
|
|
723
|
+
kindOf("entity", "entity", "model"),
|
|
724
|
+
kindOf("dto", "dto", "request", "response"),
|
|
725
|
+
kindOf("migration", "migration", "db/migration"),
|
|
726
|
+
...commonKinds()
|
|
727
|
+
],
|
|
728
|
+
exclude: [".gradle", "out", "target"],
|
|
729
|
+
recommendedBudget: "200k"
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
name: "dotnet",
|
|
733
|
+
priority: 44,
|
|
734
|
+
detect: (context) => anyFileEndingWith(context, ".csproj") || anyFileEndingWith(context, ".sln") || hasFile(context, "global.json"),
|
|
735
|
+
layers: [
|
|
736
|
+
layer("web", "src/", "Web/"),
|
|
737
|
+
layer("domain", "Domain/", "Core/"),
|
|
738
|
+
layer("infrastructure", "Infrastructure/", "Data/"),
|
|
739
|
+
layer("tests", "Tests/", "test/"),
|
|
740
|
+
...commonLayers()
|
|
741
|
+
],
|
|
742
|
+
kinds: [
|
|
743
|
+
kindOf("controller", "Controller"),
|
|
744
|
+
kindOf("service", "Service"),
|
|
745
|
+
kindOf("repository", "Repository"),
|
|
746
|
+
kindOf("model", "Model", "Entity"),
|
|
747
|
+
kindOf("dto", "Dto", "Request", "Response"),
|
|
748
|
+
suffixKind("test", "Tests.cs"),
|
|
749
|
+
...commonKinds()
|
|
750
|
+
],
|
|
751
|
+
exclude: ["bin", "obj", ".vs"],
|
|
752
|
+
recommendedBudget: "200k"
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
name: "php-laravel",
|
|
756
|
+
priority: 42,
|
|
757
|
+
detect: (context) => hasFile(context, "artisan") || manifestContains(context, "composer.json", "laravel") || hasFile(context, "composer.json"),
|
|
758
|
+
layers: [
|
|
759
|
+
layer("app", "app/"),
|
|
760
|
+
layer("routes", "routes/"),
|
|
761
|
+
layer("http", "app/Http/"),
|
|
762
|
+
layer("models", "app/Models/"),
|
|
763
|
+
layer("providers", "app/Providers/"),
|
|
764
|
+
layer("database", "database/"),
|
|
765
|
+
layer("tests", "tests/"),
|
|
766
|
+
...commonLayers()
|
|
767
|
+
],
|
|
768
|
+
kinds: [
|
|
769
|
+
kindOf("controller", "Http/Controllers/"),
|
|
770
|
+
kindOf("model", "Models/"),
|
|
771
|
+
kindOf("migration", "migrations/"),
|
|
772
|
+
kindOf("middleware", "Middleware/"),
|
|
773
|
+
kindOf("request", "Requests/"),
|
|
774
|
+
kindOf("provider", "Providers/"),
|
|
775
|
+
...commonKinds()
|
|
776
|
+
],
|
|
777
|
+
exclude: ["vendor", "storage", "bootstrap/cache", "public/build"],
|
|
778
|
+
recommendedBudget: "150k"
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
name: "ruby-rails",
|
|
782
|
+
priority: 40,
|
|
783
|
+
detect: (context) => hasFile(context, "Gemfile") || hasFile(context, "config.ru") || hasFile(context, "Rakefile"),
|
|
784
|
+
layers: [
|
|
785
|
+
layer("app", "app/"),
|
|
786
|
+
layer("controllers", "app/controllers/"),
|
|
787
|
+
layer("models", "app/models/"),
|
|
788
|
+
layer("config", "config/"),
|
|
789
|
+
layer("db", "db/"),
|
|
790
|
+
layer("spec", "spec/", "test/"),
|
|
791
|
+
...commonLayers()
|
|
792
|
+
],
|
|
793
|
+
kinds: [
|
|
794
|
+
kindOf("controller", "controllers/"),
|
|
795
|
+
kindOf("model", "models/"),
|
|
796
|
+
kindOf("migration", "migrate/"),
|
|
797
|
+
kindOf("job", "jobs/"),
|
|
798
|
+
kindOf("mailer", "mailers/"),
|
|
799
|
+
suffixKind("test", "_spec.rb", "_test.rb"),
|
|
800
|
+
...commonKinds()
|
|
801
|
+
],
|
|
802
|
+
exclude: [".bundle", "log", "tmp", "vendor/bundle", "public/assets"],
|
|
803
|
+
recommendedBudget: "150k"
|
|
247
804
|
}
|
|
248
805
|
];
|
|
249
806
|
|
|
@@ -289,50 +846,110 @@ var DEFAULT_KINDS = [
|
|
|
289
846
|
}
|
|
290
847
|
];
|
|
291
848
|
|
|
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
|
-
|
|
849
|
+
// src/presets/shape-presets.ts
|
|
850
|
+
var SHAPE_PRESETS = [
|
|
851
|
+
{
|
|
852
|
+
name: "monorepo",
|
|
853
|
+
priority: 70,
|
|
854
|
+
detect: (context) => hasFile(context, "pnpm-workspace.yaml") || hasFile(context, "nx.json") || hasFile(context, "turbo.json") || hasFile(context, "lerna.json") || manifestContains(context, "package.json", '"workspaces"'),
|
|
855
|
+
layers: [
|
|
856
|
+
layer("apps", "apps/"),
|
|
857
|
+
layer("packages", "packages/"),
|
|
858
|
+
layer("tooling", "tooling/", "tools/", "scripts/"),
|
|
859
|
+
...commonLayers()
|
|
860
|
+
],
|
|
861
|
+
kinds: commonKinds(),
|
|
862
|
+
exclude: [".turbo", ".nx"],
|
|
863
|
+
recommendedBudget: "200k"
|
|
864
|
+
},
|
|
865
|
+
{
|
|
866
|
+
name: "infra",
|
|
867
|
+
priority: 24,
|
|
868
|
+
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"),
|
|
869
|
+
layers: [
|
|
870
|
+
layer("modules", "modules/"),
|
|
871
|
+
layer("environments", "env/", "environments/"),
|
|
872
|
+
layer("manifests", "k8s/", "helm/", "charts/", "deploy/"),
|
|
873
|
+
...commonLayers()
|
|
874
|
+
],
|
|
875
|
+
kinds: [
|
|
876
|
+
{ kind: "terraform", match: (filePath) => filePath.endsWith(".tf") },
|
|
877
|
+
{ kind: "kubernetes", match: (filePath) => /\.(ya?ml)$/i.test(filePath) && /k8s|helm|charts|deploy/.test(filePath) },
|
|
878
|
+
...commonKinds()
|
|
879
|
+
],
|
|
880
|
+
exclude: [".terraform", ".terragrunt-cache"],
|
|
881
|
+
recommendedBudget: "100k"
|
|
882
|
+
},
|
|
883
|
+
{
|
|
884
|
+
name: "docs",
|
|
885
|
+
priority: 22,
|
|
886
|
+
detect: (context) => {
|
|
887
|
+
const markdown = Array.from(context.files).filter((file) => /\.mdx?$/i.test(file)).length;
|
|
888
|
+
return markdown >= 3 && !hasFile(context, "package.json");
|
|
889
|
+
},
|
|
890
|
+
layers: [
|
|
891
|
+
layer("content", "content/", "docs/"),
|
|
892
|
+
layer("assets", "assets/", "static/", "public/"),
|
|
893
|
+
...commonLayers()
|
|
894
|
+
],
|
|
895
|
+
kinds: commonKinds(),
|
|
896
|
+
exclude: [".docusaurus", ".vitepress/dist", "site"],
|
|
897
|
+
recommendedBudget: "100k"
|
|
898
|
+
},
|
|
899
|
+
{
|
|
900
|
+
name: "generic",
|
|
901
|
+
priority: 0,
|
|
902
|
+
detect: () => true,
|
|
903
|
+
layers: DEFAULT_LAYERS,
|
|
904
|
+
kinds: DEFAULT_KINDS,
|
|
905
|
+
exclude: [],
|
|
906
|
+
recommendedBudget: "120k"
|
|
315
907
|
}
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
908
|
+
];
|
|
909
|
+
|
|
910
|
+
// src/presets/registry.ts
|
|
911
|
+
function compare(a, b) {
|
|
912
|
+
if (b.priority !== a.priority) return b.priority - a.priority;
|
|
913
|
+
return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;
|
|
914
|
+
}
|
|
915
|
+
var PRESETS = [
|
|
916
|
+
...JS_PRESETS,
|
|
917
|
+
...LANGUAGE_PRESETS,
|
|
918
|
+
...SHAPE_PRESETS
|
|
919
|
+
].sort(compare);
|
|
920
|
+
var BY_NAME = new Map(PRESETS.map((preset) => [preset.name, preset]));
|
|
921
|
+
function listPresetNames() {
|
|
922
|
+
return PRESETS.map((preset) => preset.name);
|
|
923
|
+
}
|
|
924
|
+
function getPreset(name) {
|
|
925
|
+
return BY_NAME.get(name);
|
|
926
|
+
}
|
|
927
|
+
function unknownPresetMessage(name) {
|
|
928
|
+
return `Unknown preset "${name}". Expected one of: ${listPresetNames().join(", ")}.`;
|
|
929
|
+
}
|
|
930
|
+
function detectPresetFromContext(context, forced) {
|
|
931
|
+
if (forced !== void 0) {
|
|
932
|
+
const preset = BY_NAME.get(forced);
|
|
933
|
+
if (!preset) throw new Error(unknownPresetMessage(forced));
|
|
934
|
+
return toDetected(preset);
|
|
322
935
|
}
|
|
323
|
-
|
|
324
|
-
return
|
|
325
|
-
presetName: "node",
|
|
326
|
-
layers: NODE_LAYERS,
|
|
327
|
-
kinds: NODE_KINDS
|
|
328
|
-
};
|
|
936
|
+
for (const preset of PRESETS) {
|
|
937
|
+
if (preset.detect(context)) return toDetected(preset);
|
|
329
938
|
}
|
|
939
|
+
return toDetected(PRESETS[PRESETS.length - 1]);
|
|
940
|
+
}
|
|
941
|
+
function toDetected(preset) {
|
|
330
942
|
return {
|
|
331
|
-
presetName:
|
|
332
|
-
layers:
|
|
333
|
-
kinds:
|
|
943
|
+
presetName: preset.name,
|
|
944
|
+
layers: preset.layers,
|
|
945
|
+
kinds: preset.kinds,
|
|
946
|
+
exclude: preset.exclude ? [...preset.exclude] : [],
|
|
947
|
+
recommendedBudget: preset.recommendedBudget ?? null
|
|
334
948
|
};
|
|
335
949
|
}
|
|
950
|
+
function detectPreset(cwd, forced) {
|
|
951
|
+
return detectPresetFromContext(buildPresetContext(cwd), forced);
|
|
952
|
+
}
|
|
336
953
|
|
|
337
954
|
// src/scanner/files.ts
|
|
338
955
|
import * as childProcess from "child_process";
|
|
@@ -340,11 +957,12 @@ import * as fs3 from "fs";
|
|
|
340
957
|
import * as path3 from "path";
|
|
341
958
|
function getGitInfo(cwd, fallbackBranch = "main") {
|
|
342
959
|
try {
|
|
343
|
-
const
|
|
960
|
+
const reported = childProcess.execSync("git rev-parse --abbrev-ref HEAD", {
|
|
344
961
|
cwd,
|
|
345
962
|
stdio: ["ignore", "pipe", "ignore"],
|
|
346
963
|
encoding: "utf8"
|
|
347
|
-
}).trim()
|
|
964
|
+
}).trim();
|
|
965
|
+
const branch = !reported || reported === "HEAD" ? fallbackBranch : reported;
|
|
348
966
|
const commit = childProcess.execSync("git rev-parse HEAD", {
|
|
349
967
|
cwd,
|
|
350
968
|
stdio: ["ignore", "pipe", "ignore"],
|
|
@@ -866,6 +1484,8 @@ function normalizeResolvedConfig(config) {
|
|
|
866
1484
|
config.includeMonolith === true
|
|
867
1485
|
),
|
|
868
1486
|
include: resolveInclude(config.include),
|
|
1487
|
+
preset: typeof config.preset === "string" ? config.preset : "generic",
|
|
1488
|
+
presetExclude: Array.isArray(config.presetExclude) ? config.presetExclude : [],
|
|
869
1489
|
layersMode: config.layersMode === "replace" ? "replace" : "merge",
|
|
870
1490
|
budgets: config.budgets && typeof config.budgets === "object" ? config.budgets : { default: config.maxPartBytes, byLayer: {} },
|
|
871
1491
|
hooks: resolveHooks(config.hooks),
|
|
@@ -913,6 +1533,7 @@ function parseJsonish(raw, file) {
|
|
|
913
1533
|
}
|
|
914
1534
|
async function loadConfigFileDetailed(cwd) {
|
|
915
1535
|
const warnings = [];
|
|
1536
|
+
const errors = [];
|
|
916
1537
|
for (const filename of CONFIG_FILENAMES) {
|
|
917
1538
|
const fullPath = path4.join(cwd, filename);
|
|
918
1539
|
if (!fs4.existsSync(fullPath)) continue;
|
|
@@ -921,10 +1542,13 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
921
1542
|
return {
|
|
922
1543
|
config: parseJsonish(fs4.readFileSync(fullPath, "utf8"), fullPath),
|
|
923
1544
|
file: fullPath,
|
|
924
|
-
warnings
|
|
1545
|
+
warnings,
|
|
1546
|
+
errors
|
|
925
1547
|
};
|
|
926
1548
|
} catch (err) {
|
|
927
|
-
|
|
1549
|
+
const message = err.message;
|
|
1550
|
+
warnings.push(message);
|
|
1551
|
+
errors.push(message);
|
|
928
1552
|
continue;
|
|
929
1553
|
}
|
|
930
1554
|
}
|
|
@@ -938,12 +1562,16 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
938
1562
|
const imported = await import(pathToFileURL(fullPath).href);
|
|
939
1563
|
const config = imported.default ?? imported;
|
|
940
1564
|
if (!config || typeof config !== "object") {
|
|
941
|
-
|
|
1565
|
+
const message = `${fullPath} does not export a configuration object.`;
|
|
1566
|
+
warnings.push(message);
|
|
1567
|
+
errors.push(message);
|
|
942
1568
|
continue;
|
|
943
1569
|
}
|
|
944
|
-
return { config, file: fullPath, warnings };
|
|
1570
|
+
return { config, file: fullPath, warnings, errors };
|
|
945
1571
|
} catch (err) {
|
|
946
|
-
|
|
1572
|
+
const message = `Could not load ${fullPath}: ${err.message}`;
|
|
1573
|
+
warnings.push(message);
|
|
1574
|
+
errors.push(message);
|
|
947
1575
|
continue;
|
|
948
1576
|
}
|
|
949
1577
|
}
|
|
@@ -953,13 +1581,18 @@ async function loadConfigFileDetailed(cwd) {
|
|
|
953
1581
|
const parsed = parseJsonish(fs4.readFileSync(pkgJsonPath, "utf8"), pkgJsonPath);
|
|
954
1582
|
const snapshotKey = parsed.snapshot;
|
|
955
1583
|
if (snapshotKey && typeof snapshotKey === "object") {
|
|
956
|
-
return { config: snapshotKey, file: pkgJsonPath, warnings };
|
|
1584
|
+
return { config: snapshotKey, file: pkgJsonPath, warnings, errors };
|
|
1585
|
+
}
|
|
1586
|
+
if (snapshotKey !== void 0) {
|
|
1587
|
+
warnings.push(`${pkgJsonPath}: the "snapshot" key must be an object; it was ignored.`);
|
|
957
1588
|
}
|
|
958
1589
|
} catch (err) {
|
|
959
|
-
|
|
1590
|
+
const message = err.message;
|
|
1591
|
+
warnings.push(message);
|
|
1592
|
+
errors.push(message);
|
|
960
1593
|
}
|
|
961
1594
|
}
|
|
962
|
-
return { config: null, file: null, warnings };
|
|
1595
|
+
return { config: null, file: null, warnings, errors };
|
|
963
1596
|
}
|
|
964
1597
|
var MIN_MAX_PART_BYTES = 4096;
|
|
965
1598
|
var ARTIFACT_NAMES = [
|
|
@@ -1054,7 +1687,9 @@ function boundedBytes(value, fallback, minimum, label) {
|
|
|
1054
1687
|
`${label} must be a byte count such as 120000, "120k" or "1.5mb" (received ${describeBytes(value)}).`
|
|
1055
1688
|
);
|
|
1056
1689
|
}
|
|
1057
|
-
if (parsed <= 0)
|
|
1690
|
+
if (parsed <= 0) {
|
|
1691
|
+
throw new Error(`${label} must be a positive byte count (received ${describeBytes(value)}).`);
|
|
1692
|
+
}
|
|
1058
1693
|
if (parsed < minimum) {
|
|
1059
1694
|
throw new Error(`${label} must be at least ${minimum} bytes (received ${describeBytes(value)}).`);
|
|
1060
1695
|
}
|
|
@@ -1094,12 +1729,12 @@ function resolveBudgets(budgets, fallback) {
|
|
|
1094
1729
|
if (typeof budgets.byLayer !== "object" || budgets.byLayer === null) {
|
|
1095
1730
|
throw new Error("`budgets.byLayer` must be an object of layer name to byte size.");
|
|
1096
1731
|
}
|
|
1097
|
-
for (const [
|
|
1098
|
-
byLayer[
|
|
1732
|
+
for (const [layer2, value] of Object.entries(budgets.byLayer)) {
|
|
1733
|
+
byLayer[layer2] = boundedBytes(
|
|
1099
1734
|
value,
|
|
1100
1735
|
fallback,
|
|
1101
1736
|
MIN_MAX_PART_BYTES,
|
|
1102
|
-
`budgets.byLayer["${
|
|
1737
|
+
`budgets.byLayer["${layer2}"]`
|
|
1103
1738
|
);
|
|
1104
1739
|
}
|
|
1105
1740
|
}
|
|
@@ -1131,7 +1766,11 @@ function resolveCandidateCwd(userConfig, cliOptions, baseDir) {
|
|
|
1131
1766
|
}
|
|
1132
1767
|
function resolveConfig(userConfig, cliOptions, baseDir) {
|
|
1133
1768
|
const cwd = resolveCandidateCwd(userConfig ?? null, cliOptions, baseDir);
|
|
1134
|
-
const
|
|
1769
|
+
const forcedPreset = userConfig?.preset;
|
|
1770
|
+
if (forcedPreset !== void 0 && !getPreset(forcedPreset)) {
|
|
1771
|
+
throw new Error(unknownPresetMessage(forcedPreset));
|
|
1772
|
+
}
|
|
1773
|
+
const preset = detectPreset(cwd, forcedPreset);
|
|
1135
1774
|
const layersMode = resolveLayersMode(userConfig?.layersMode);
|
|
1136
1775
|
const layers = userConfig?.layers && userConfig.layers.length > 0 ? layersMode === "replace" ? userConfig.layers : [...userConfig.layers, ...preset.layers] : preset.layers;
|
|
1137
1776
|
const kinds = userConfig?.kinds && userConfig.kinds.length > 0 ? layersMode === "replace" ? userConfig.kinds : [...userConfig.kinds, ...preset.kinds] : preset.kinds;
|
|
@@ -1167,6 +1806,8 @@ function resolveConfig(userConfig, cliOptions, baseDir) {
|
|
|
1167
1806
|
),
|
|
1168
1807
|
includeSensitiveFiles: cliOptions?.includeSensitiveFiles ?? userConfig?.includeSensitiveFiles ?? false,
|
|
1169
1808
|
include: resolveInclude(userConfig?.include),
|
|
1809
|
+
preset: preset.presetName,
|
|
1810
|
+
presetExclude: [...preset.exclude],
|
|
1170
1811
|
artifacts,
|
|
1171
1812
|
layersMode,
|
|
1172
1813
|
budgets,
|
|
@@ -1201,8 +1842,8 @@ function matchRule(filePath, matcher) {
|
|
|
1201
1842
|
return false;
|
|
1202
1843
|
}
|
|
1203
1844
|
function classifyLayer(filePath, layers) {
|
|
1204
|
-
for (const
|
|
1205
|
-
if (matchRule(filePath,
|
|
1845
|
+
for (const layer2 of layers) {
|
|
1846
|
+
if (matchRule(filePath, layer2.match)) return layer2.name;
|
|
1206
1847
|
}
|
|
1207
1848
|
return "other";
|
|
1208
1849
|
}
|
|
@@ -1337,8 +1978,8 @@ function findRuleMatch(rules, relativePath) {
|
|
|
1337
1978
|
(rule) => matchRule(relativePath, rule.match)
|
|
1338
1979
|
);
|
|
1339
1980
|
}
|
|
1340
|
-
function
|
|
1341
|
-
const manifestPath = path6.
|
|
1981
|
+
function readManifest2(config) {
|
|
1982
|
+
const manifestPath = path6.resolve(config.cwd, config.outDir, "manifest.json");
|
|
1342
1983
|
try {
|
|
1343
1984
|
return JSON.parse(fs5.readFileSync(manifestPath, "utf8"));
|
|
1344
1985
|
} catch {
|
|
@@ -1355,7 +1996,7 @@ function explainPath(config, target) {
|
|
|
1355
1996
|
};
|
|
1356
1997
|
const collection = collectFiles(config.cwd, {
|
|
1357
1998
|
includePatterns: config.include,
|
|
1358
|
-
excludePatterns: config.excludePatterns,
|
|
1999
|
+
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
1359
2000
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
1360
2001
|
});
|
|
1361
2002
|
if (!collection.files.includes(relative4)) {
|
|
@@ -1382,14 +2023,14 @@ function explainPath(config, target) {
|
|
|
1382
2023
|
if (kindRule) result.kindMatch = describeMatcher(kindRule.match);
|
|
1383
2024
|
if (fs5.existsSync(absolute) && fs5.statSync(absolute).isFile()) {
|
|
1384
2025
|
const buffer = fs5.readFileSync(absolute);
|
|
1385
|
-
const
|
|
2026
|
+
const text2 = buffer.toString("utf8");
|
|
1386
2027
|
result.bytes = buffer.length;
|
|
1387
|
-
result.lines = countLines(
|
|
1388
|
-
const { findings } = scanAndRedactSecrets(
|
|
2028
|
+
result.lines = countLines(text2);
|
|
2029
|
+
const { findings } = scanAndRedactSecrets(text2, config.secretRules, relative4);
|
|
1389
2030
|
result.secretCount = findings.length;
|
|
1390
2031
|
result.secrets = findings.slice(0, 20).map((finding) => ({ rule: finding.rule, line: finding.line }));
|
|
1391
2032
|
}
|
|
1392
|
-
const manifest =
|
|
2033
|
+
const manifest = readManifest2(config);
|
|
1393
2034
|
const entry = manifest?.files.find((file) => file.relativePath === relative4);
|
|
1394
2035
|
result.part = entry?.part ?? null;
|
|
1395
2036
|
result.inclusionMode = entry?.inclusionMode;
|
|
@@ -1429,133 +2070,1147 @@ function renderExplainText(result) {
|
|
|
1429
2070
|
return lines.join("\n");
|
|
1430
2071
|
}
|
|
1431
2072
|
|
|
1432
|
-
// src/
|
|
1433
|
-
import * as
|
|
1434
|
-
import * as
|
|
1435
|
-
import * as path9 from "path";
|
|
2073
|
+
// src/wizard/init.ts
|
|
2074
|
+
import * as fs6 from "fs";
|
|
2075
|
+
import * as path7 from "path";
|
|
1436
2076
|
|
|
1437
|
-
// src/
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
2077
|
+
// src/wizard/config-template.ts
|
|
2078
|
+
var DEFAULT_ANSWERS = {
|
|
2079
|
+
preset: "generic",
|
|
2080
|
+
maxPartBytes: "300k",
|
|
2081
|
+
largeFileBytes: "200k",
|
|
2082
|
+
include: [],
|
|
2083
|
+
excludePatterns: [],
|
|
2084
|
+
artifacts: {
|
|
2085
|
+
readme: true,
|
|
2086
|
+
manifest: true,
|
|
2087
|
+
parts: true,
|
|
2088
|
+
navigation: true,
|
|
2089
|
+
projectTree: true,
|
|
2090
|
+
audit: true,
|
|
2091
|
+
monolith: false
|
|
2092
|
+
},
|
|
2093
|
+
includeSensitiveFiles: false,
|
|
2094
|
+
outDir: "snapshot"
|
|
2095
|
+
};
|
|
2096
|
+
function list(values) {
|
|
2097
|
+
if (values.length === 0) return "[]";
|
|
2098
|
+
return `[${values.map((value) => JSON.stringify(value)).join(", ")}]`;
|
|
2099
|
+
}
|
|
2100
|
+
function size(value) {
|
|
2101
|
+
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
2102
|
+
}
|
|
2103
|
+
var PRESET_NAMES = [
|
|
2104
|
+
"nextjs \xB7 react-vite \xB7 vue-vite \xB7 nuxt \xB7 sveltekit \xB7 angular \xB7 astro",
|
|
2105
|
+
"remix \xB7 solid \xB7 nest \xB7 node-express \xB7 node-lib",
|
|
2106
|
+
"python-django \xB7 python-fastapi \xB7 python-lib \xB7 go \xB7 rust",
|
|
2107
|
+
"java-spring \xB7 dotnet \xB7 php-laravel \xB7 ruby-rails",
|
|
2108
|
+
"monorepo \xB7 docs \xB7 infra \xB7 generic"
|
|
2109
|
+
];
|
|
2110
|
+
function renderConfig(answers) {
|
|
2111
|
+
const { artifacts } = answers;
|
|
2112
|
+
return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
|
|
2113
|
+
|
|
2114
|
+
export default defineConfig({
|
|
2115
|
+
// \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
|
|
2116
|
+
// Which project shape to use. Detected from the repo when omitted.
|
|
2117
|
+
// ${PRESET_NAMES.join("\n // ")}
|
|
2118
|
+
preset: ${JSON.stringify(answers.preset)},
|
|
2119
|
+
|
|
2120
|
+
// Recorded in the manifest when git cannot report a branch.
|
|
2121
|
+
branch: "main",
|
|
2122
|
+
|
|
2123
|
+
// \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
|
|
2124
|
+
// Where the snapshot is written.
|
|
2125
|
+
outDir: ${JSON.stringify(answers.outDir)},
|
|
2126
|
+
// Scratch directory; the new snapshot is renamed into place atomically.
|
|
2127
|
+
tmpDir: "snapshot.tmp",
|
|
2128
|
+
|
|
2129
|
+
// \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
|
|
2130
|
+
// Hard ceiling per part. "300k", 307200 and "1.5mb" are all accepted.
|
|
2131
|
+
maxPartBytes: ${size(answers.maxPartBytes)},
|
|
2132
|
+
// Files above this size get a head/tail preview instead of the full body.
|
|
2133
|
+
largeFileBytes: ${size(answers.largeFileBytes)},
|
|
2134
|
+
previewHeadLines: 80,
|
|
2135
|
+
previewTailLines: 40,
|
|
2136
|
+
// Different ceilings per layer. Unlisted layers use \`default\`.
|
|
2137
|
+
budgets: {
|
|
2138
|
+
default: ${size(answers.maxPartBytes)},
|
|
2139
|
+
byLayer: {},
|
|
2140
|
+
},
|
|
2141
|
+
|
|
2142
|
+
// \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
|
|
2143
|
+
// Allow-list. Empty means "everything that is not ignored".
|
|
2144
|
+
// Example: ["src/**", "packages/*/src/**"]
|
|
2145
|
+
include: ${list(answers.include)},
|
|
2146
|
+
// Globs or path segments to drop. Example: ["**/*.test.ts", "docs"]
|
|
2147
|
+
excludePatterns: ${list(answers.excludePatterns)},
|
|
2148
|
+
|
|
2149
|
+
// \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
|
|
2150
|
+
// "merge" = the preset's rules plus yours (yours win). "replace" = yours only.
|
|
2151
|
+
layersMode: "merge",
|
|
2152
|
+
// [{ name: "billing", match: "src/features/billing/**" }]
|
|
2153
|
+
layers: [],
|
|
2154
|
+
// [{ kind: "route", match: "src/app/api/**" }]
|
|
2155
|
+
kinds: [],
|
|
2156
|
+
|
|
2157
|
+
// \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
|
|
2158
|
+
// .env, *.pem, .npmrc, .ssh/ and friends are skipped unless this is true.
|
|
2159
|
+
includeSensitiveFiles: ${answers.includeSensitiveFiles},
|
|
2160
|
+
secrets: {
|
|
2161
|
+
// [{ rule: "internal-token", pattern: /CORP-[A-Z0-9]{32}/ }]
|
|
2162
|
+
customPatterns: [],
|
|
2163
|
+
// Set true to drop the 21 built-in rules.
|
|
2164
|
+
disableDefaultPatterns: false,
|
|
2165
|
+
},
|
|
2166
|
+
|
|
2167
|
+
// \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
|
|
2168
|
+
artifacts: {
|
|
2169
|
+
readme: ${artifacts.readme}, // README.md
|
|
2170
|
+
manifest: ${artifacts.manifest}, // manifest.json \u2014 required by validate and report
|
|
2171
|
+
parts: ${artifacts.parts}, // parts/part-XXX.md
|
|
2172
|
+
navigation: ${artifacts.navigation}, // reports/navigation.md
|
|
2173
|
+
projectTree: ${artifacts.projectTree}, // reports/project-tree.md
|
|
2174
|
+
audit: ${artifacts.audit}, // reports/audit.md
|
|
2175
|
+
monolith: ${artifacts.monolith}, // full-codebase.json + .md (roughly doubles output)
|
|
2176
|
+
},
|
|
2177
|
+
// Skip the monolith above this size. null = no limit.
|
|
2178
|
+
maxMonolithBytes: null,
|
|
2179
|
+
|
|
2180
|
+
// \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
|
|
2181
|
+
// afterGenerate: (manifest) => { /* write your own artifact */ }
|
|
2182
|
+
hooks: {},
|
|
2183
|
+
});
|
|
2184
|
+
`;
|
|
2185
|
+
}
|
|
2186
|
+
function renderMinimalConfig(answers) {
|
|
2187
|
+
const lines = [];
|
|
2188
|
+
if (answers.preset !== DEFAULT_ANSWERS.preset) lines.push(` preset: ${JSON.stringify(answers.preset)},`);
|
|
2189
|
+
if (answers.maxPartBytes !== DEFAULT_ANSWERS.maxPartBytes) {
|
|
2190
|
+
lines.push(` maxPartBytes: ${size(answers.maxPartBytes)},`);
|
|
2191
|
+
}
|
|
2192
|
+
if (answers.largeFileBytes !== DEFAULT_ANSWERS.largeFileBytes) {
|
|
2193
|
+
lines.push(` largeFileBytes: ${size(answers.largeFileBytes)},`);
|
|
2194
|
+
}
|
|
2195
|
+
if (answers.include.length > 0) lines.push(` include: ${list(answers.include)},`);
|
|
2196
|
+
if (answers.excludePatterns.length > 0) {
|
|
2197
|
+
lines.push(` excludePatterns: ${list(answers.excludePatterns)},`);
|
|
2198
|
+
}
|
|
2199
|
+
if (answers.includeSensitiveFiles) lines.push(" includeSensitiveFiles: true,");
|
|
2200
|
+
if (answers.outDir !== DEFAULT_ANSWERS.outDir) lines.push(` outDir: ${JSON.stringify(answers.outDir)},`);
|
|
2201
|
+
const changed = Object.keys(answers.artifacts).filter(
|
|
2202
|
+
(name) => answers.artifacts[name] !== DEFAULT_ANSWERS.artifacts[name]
|
|
2203
|
+
);
|
|
2204
|
+
if (changed.length > 0) {
|
|
2205
|
+
const entries = changed.map((name) => ` ${name}: ${answers.artifacts[name]}`).join(",\n");
|
|
2206
|
+
lines.push(` artifacts: {
|
|
2207
|
+
${entries},
|
|
2208
|
+
},`);
|
|
2209
|
+
}
|
|
2210
|
+
if (lines.length === 0) {
|
|
2211
|
+
return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
|
|
2212
|
+
|
|
2213
|
+
export default defineConfig({});
|
|
2214
|
+
`;
|
|
2215
|
+
}
|
|
2216
|
+
return `import { defineConfig } from "@ankurhalder/codebase-snapshot";
|
|
2217
|
+
|
|
2218
|
+
export default defineConfig({
|
|
2219
|
+
${lines.join("\n")}
|
|
2220
|
+
});
|
|
2221
|
+
`;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
// src/wizard/prompts.ts
|
|
2225
|
+
import * as readline from "readline";
|
|
2226
|
+
var PromptCancelled = class extends Error {
|
|
2227
|
+
constructor() {
|
|
2228
|
+
super("Prompt cancelled");
|
|
2229
|
+
this.name = "PromptCancelled";
|
|
2230
|
+
}
|
|
2231
|
+
};
|
|
2232
|
+
function defaultIO() {
|
|
2233
|
+
return { input: process.stdin, output: process.stdout };
|
|
2234
|
+
}
|
|
2235
|
+
function isInteractive(io) {
|
|
2236
|
+
return Boolean(io.input.isTTY && io.output.isTTY);
|
|
2237
|
+
}
|
|
2238
|
+
function moveSelection(current, delta, count) {
|
|
2239
|
+
if (count <= 0) return 0;
|
|
2240
|
+
return ((current + delta) % count + count) % count;
|
|
2241
|
+
}
|
|
2242
|
+
function toggleIndex(selected, index) {
|
|
2243
|
+
const next = new Set(selected);
|
|
2244
|
+
if (next.has(index)) next.delete(index);
|
|
2245
|
+
else next.add(index);
|
|
2246
|
+
return next;
|
|
2247
|
+
}
|
|
2248
|
+
function marker(active) {
|
|
2249
|
+
return active ? "\u276F" : " ";
|
|
2250
|
+
}
|
|
2251
|
+
function choiceLine(choice, active) {
|
|
2252
|
+
const hint = choice.hint ? ` ${choice.hint}` : "";
|
|
2253
|
+
return ` ${marker(active)} ${choice.label}${hint}`;
|
|
2254
|
+
}
|
|
2255
|
+
function renderSelect(message, choices, index) {
|
|
2256
|
+
const lines = [`\u25C6 ${message}`, ""];
|
|
2257
|
+
choices.forEach((choice, position) => lines.push(choiceLine(choice, position === index)));
|
|
2258
|
+
lines.push("", " \u2191\u2193 move \xB7 enter select");
|
|
2259
|
+
return lines.join("\n");
|
|
2260
|
+
}
|
|
2261
|
+
function renderMultiSelect(message, choices, index, selected) {
|
|
2262
|
+
const lines = [`\u25C6 ${message}`, ""];
|
|
2263
|
+
choices.forEach((choice, position) => {
|
|
2264
|
+
const box = selected.has(position) ? "\u25FC" : "\u25FB";
|
|
2265
|
+
const hint = choice.hint ? ` ${choice.hint}` : "";
|
|
2266
|
+
lines.push(` ${marker(position === index)} ${box} ${choice.label}${hint}`);
|
|
2267
|
+
});
|
|
2268
|
+
lines.push("", " \u2191\u2193 move \xB7 space toggle \xB7 enter confirm");
|
|
2269
|
+
return lines.join("\n");
|
|
2270
|
+
}
|
|
2271
|
+
function renderConfirm(message, value) {
|
|
2272
|
+
return [
|
|
2273
|
+
`\u25C6 ${message}`,
|
|
2274
|
+
"",
|
|
2275
|
+
` ${marker(value)} Yes`,
|
|
2276
|
+
` ${marker(!value)} No`,
|
|
2277
|
+
"",
|
|
2278
|
+
" \u2190\u2192 or y/n \xB7 enter confirm"
|
|
2279
|
+
].join("\n");
|
|
2280
|
+
}
|
|
2281
|
+
function renderText(message, value, error) {
|
|
2282
|
+
const lines = [`\u25C6 ${message}`, "", ` ${value || "\u258F"}`];
|
|
2283
|
+
if (error) lines.push("", ` \u2716 ${error}`);
|
|
2284
|
+
return lines.join("\n");
|
|
2285
|
+
}
|
|
2286
|
+
function keyReader(input) {
|
|
2287
|
+
const queue = [];
|
|
2288
|
+
const waiters = [];
|
|
2289
|
+
const wasRaw = input.isRaw;
|
|
2290
|
+
const onKey = (_text, key) => {
|
|
2291
|
+
const waiter = waiters.shift();
|
|
2292
|
+
if (waiter) waiter(key);
|
|
2293
|
+
else queue.push(key);
|
|
2294
|
+
};
|
|
2295
|
+
readline.emitKeypressEvents(input);
|
|
2296
|
+
if (input.isTTY) input.setRawMode(true);
|
|
2297
|
+
input.resume();
|
|
2298
|
+
input.on("keypress", onKey);
|
|
2299
|
+
return {
|
|
2300
|
+
next() {
|
|
2301
|
+
const queued = queue.shift();
|
|
2302
|
+
if (queued) return Promise.resolve(queued);
|
|
2303
|
+
return new Promise((resolve6) => waiters.push(resolve6));
|
|
2304
|
+
},
|
|
2305
|
+
close() {
|
|
2306
|
+
input.off("keypress", onKey);
|
|
2307
|
+
if (input.isTTY) input.setRawMode(wasRaw ?? false);
|
|
2308
|
+
input.pause();
|
|
1487
2309
|
}
|
|
1488
|
-
|
|
2310
|
+
};
|
|
2311
|
+
}
|
|
2312
|
+
var Frame = class {
|
|
2313
|
+
constructor(output) {
|
|
2314
|
+
this.output = output;
|
|
2315
|
+
}
|
|
2316
|
+
output;
|
|
2317
|
+
drawn = 0;
|
|
2318
|
+
draw(text2) {
|
|
2319
|
+
this.erase();
|
|
2320
|
+
this.output.write(`${text2}
|
|
2321
|
+
`);
|
|
2322
|
+
this.drawn = text2.split("\n").length;
|
|
1489
2323
|
}
|
|
1490
|
-
|
|
2324
|
+
erase() {
|
|
2325
|
+
if (this.drawn > 0) this.output.write(`\x1B[${this.drawn}A\x1B[0J`);
|
|
2326
|
+
this.drawn = 0;
|
|
2327
|
+
}
|
|
2328
|
+
};
|
|
2329
|
+
function isCancel(key) {
|
|
2330
|
+
return key.ctrl === true && key.name === "c" || key.ctrl === true && key.name === "d";
|
|
1491
2331
|
}
|
|
1492
|
-
function
|
|
2332
|
+
async function select(io, message, choices, options = {}) {
|
|
2333
|
+
if (choices.length === 0) throw new Error("select() requires at least one choice.");
|
|
2334
|
+
const fallbackIndex = Math.max(
|
|
2335
|
+
0,
|
|
2336
|
+
choices.findIndex((choice) => choice.value === options.defaultValue)
|
|
2337
|
+
);
|
|
2338
|
+
if (!isInteractive(io)) {
|
|
2339
|
+
io.output.write(`${renderSelect(message, choices, fallbackIndex)}
|
|
2340
|
+
`);
|
|
2341
|
+
return choices[fallbackIndex].value;
|
|
2342
|
+
}
|
|
2343
|
+
const frame = new Frame(io.output);
|
|
2344
|
+
const reader = keyReader(io.input);
|
|
2345
|
+
let index = fallbackIndex;
|
|
1493
2346
|
try {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
const
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
};
|
|
2347
|
+
for (; ; ) {
|
|
2348
|
+
frame.draw(renderSelect(message, choices, index));
|
|
2349
|
+
const key = await reader.next();
|
|
2350
|
+
if (isCancel(key)) throw new PromptCancelled();
|
|
2351
|
+
if (key.name === "up") index = moveSelection(index, -1, choices.length);
|
|
2352
|
+
else if (key.name === "down") index = moveSelection(index, 1, choices.length);
|
|
2353
|
+
else if (key.name === "return" || key.name === "enter") {
|
|
2354
|
+
frame.erase();
|
|
2355
|
+
return choices[index].value;
|
|
2356
|
+
} else if (key.name === "j") index = moveSelection(index, 1, choices.length);
|
|
2357
|
+
else if (key.name === "k") index = moveSelection(index, -1, choices.length);
|
|
1504
2358
|
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
2359
|
+
} finally {
|
|
2360
|
+
reader.close();
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
async function multiSelect(io, message, choices, options = {}) {
|
|
2364
|
+
const selected = /* @__PURE__ */ new Set();
|
|
2365
|
+
choices.forEach((choice, position) => {
|
|
2366
|
+
if (options.defaults?.includes(choice.value)) selected.add(position);
|
|
2367
|
+
});
|
|
2368
|
+
const result = () => choices.filter((_, position) => selected.has(position)).map((choice) => choice.value);
|
|
2369
|
+
if (!isInteractive(io)) {
|
|
2370
|
+
io.output.write(`${renderMultiSelect(message, choices, 0, selected)}
|
|
2371
|
+
`);
|
|
2372
|
+
return result();
|
|
2373
|
+
}
|
|
2374
|
+
const frame = new Frame(io.output);
|
|
2375
|
+
const reader = keyReader(io.input);
|
|
2376
|
+
let index = 0;
|
|
2377
|
+
try {
|
|
2378
|
+
for (; ; ) {
|
|
2379
|
+
frame.draw(renderMultiSelect(message, choices, index, selected));
|
|
2380
|
+
const key = await reader.next();
|
|
2381
|
+
if (isCancel(key)) throw new PromptCancelled();
|
|
2382
|
+
if (key.name === "up") index = moveSelection(index, -1, choices.length);
|
|
2383
|
+
else if (key.name === "down") index = moveSelection(index, 1, choices.length);
|
|
2384
|
+
else if (key.name === "space") {
|
|
2385
|
+
const next = toggleIndex(selected, index);
|
|
2386
|
+
selected.clear();
|
|
2387
|
+
for (const position of next) selected.add(position);
|
|
2388
|
+
} else if (key.name === "a") {
|
|
2389
|
+
if (selected.size === choices.length) selected.clear();
|
|
2390
|
+
else choices.forEach((_, position) => selected.add(position));
|
|
2391
|
+
} else if (key.name === "return" || key.name === "enter") {
|
|
2392
|
+
frame.erase();
|
|
2393
|
+
return result();
|
|
2394
|
+
}
|
|
1513
2395
|
}
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
2396
|
+
} finally {
|
|
2397
|
+
reader.close();
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
async function confirm(io, message, options = {}) {
|
|
2401
|
+
let value = options.defaultValue ?? true;
|
|
2402
|
+
if (!isInteractive(io)) {
|
|
2403
|
+
io.output.write(`${renderConfirm(message, value)}
|
|
2404
|
+
`);
|
|
2405
|
+
return value;
|
|
2406
|
+
}
|
|
2407
|
+
const frame = new Frame(io.output);
|
|
2408
|
+
const reader = keyReader(io.input);
|
|
2409
|
+
try {
|
|
2410
|
+
for (; ; ) {
|
|
2411
|
+
frame.draw(renderConfirm(message, value));
|
|
2412
|
+
const key = await reader.next();
|
|
2413
|
+
if (isCancel(key)) throw new PromptCancelled();
|
|
2414
|
+
if (key.name === "up" || key.name === "left") value = true;
|
|
2415
|
+
else if (key.name === "down" || key.name === "right") value = false;
|
|
2416
|
+
else if (key.name === "y") value = true;
|
|
2417
|
+
else if (key.name === "n") value = false;
|
|
2418
|
+
else if (key.name === "return" || key.name === "enter") {
|
|
2419
|
+
frame.erase();
|
|
2420
|
+
return value;
|
|
2421
|
+
}
|
|
2422
|
+
}
|
|
2423
|
+
} finally {
|
|
2424
|
+
reader.close();
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
async function text(io, message, options = {}) {
|
|
2428
|
+
const fallback = options.defaultValue ?? "";
|
|
2429
|
+
if (!isInteractive(io)) {
|
|
2430
|
+
io.output.write(`${renderText(message, fallback)}
|
|
2431
|
+
`);
|
|
2432
|
+
return fallback;
|
|
2433
|
+
}
|
|
2434
|
+
const frame = new Frame(io.output);
|
|
2435
|
+
const reader = keyReader(io.input);
|
|
2436
|
+
let value = fallback;
|
|
2437
|
+
let error;
|
|
2438
|
+
try {
|
|
2439
|
+
for (; ; ) {
|
|
2440
|
+
frame.draw(renderText(message, value, error));
|
|
2441
|
+
const key = await reader.next();
|
|
2442
|
+
if (isCancel(key)) throw new PromptCancelled();
|
|
2443
|
+
error = void 0;
|
|
2444
|
+
if (key.name === "return" || key.name === "enter") {
|
|
2445
|
+
const problem = options.validate ? options.validate(value) : null;
|
|
2446
|
+
if (problem) {
|
|
2447
|
+
error = problem;
|
|
2448
|
+
continue;
|
|
2449
|
+
}
|
|
2450
|
+
frame.erase();
|
|
2451
|
+
return value;
|
|
2452
|
+
}
|
|
2453
|
+
if (key.name === "backspace") {
|
|
2454
|
+
value = value.slice(0, -1);
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2457
|
+
if (key.name === "escape") {
|
|
2458
|
+
value = "";
|
|
2459
|
+
continue;
|
|
2460
|
+
}
|
|
2461
|
+
if (_isPrintable(key)) value += _keyText(key);
|
|
2462
|
+
}
|
|
2463
|
+
} finally {
|
|
2464
|
+
reader.close();
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
function _isPrintable(key) {
|
|
2468
|
+
return typeof key.sequence === "string" && key.sequence.length === 1 && key.sequence >= " ";
|
|
2469
|
+
}
|
|
2470
|
+
function _keyText(key) {
|
|
2471
|
+
return key.sequence ?? "";
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// src/wizard/init.ts
|
|
2475
|
+
var CONFIG_CANDIDATES = [
|
|
2476
|
+
"snapshot.config.ts",
|
|
2477
|
+
"snapshot.config.js",
|
|
2478
|
+
"snapshot.config.mjs",
|
|
2479
|
+
"snapshot.config.cjs",
|
|
2480
|
+
"snapshot.config.json",
|
|
2481
|
+
".snapshotrc.json",
|
|
2482
|
+
".snapshotrc"
|
|
2483
|
+
];
|
|
2484
|
+
var SIZE_CHOICES = [
|
|
2485
|
+
{ value: "60k", label: "Small", hint: "60 KB \xB7 ~15k tokens \xB7 tight context windows" },
|
|
2486
|
+
{ value: "120k", label: "Medium", hint: "120 KB \xB7 ~30k tokens \xB7 recommended" },
|
|
2487
|
+
{ value: "300k", label: "Large", hint: "300 KB \xB7 ~75k tokens \xB7 the default" },
|
|
2488
|
+
{ value: "custom", label: "Custom\u2026", hint: "type a size like 150k or 1.5mb" }
|
|
2489
|
+
];
|
|
2490
|
+
function existingConfig(cwd) {
|
|
2491
|
+
for (const name of CONFIG_CANDIDATES) {
|
|
2492
|
+
if (fs6.existsSync(path7.join(cwd, name))) return name;
|
|
2493
|
+
}
|
|
2494
|
+
return null;
|
|
2495
|
+
}
|
|
2496
|
+
function artifactAnswers(options) {
|
|
2497
|
+
const artifacts = { ...DEFAULT_ANSWERS.artifacts };
|
|
2498
|
+
for (const name of options.skipArtifacts ?? []) {
|
|
2499
|
+
if (ARTIFACT_NAMES.includes(name)) {
|
|
2500
|
+
artifacts[name] = false;
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
if (options.onlyArtifacts && options.onlyArtifacts.length > 0) {
|
|
2504
|
+
const keep = new Set(options.onlyArtifacts);
|
|
2505
|
+
for (const name of ARTIFACT_NAMES) artifacts[name] = keep.has(name);
|
|
2506
|
+
}
|
|
2507
|
+
return artifacts;
|
|
2508
|
+
}
|
|
2509
|
+
function topLevelDirs(cwd) {
|
|
2510
|
+
try {
|
|
2511
|
+
return fs6.readdirSync(cwd, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !entry.name.startsWith(".") && entry.name !== "node_modules").map((entry) => entry.name).sort();
|
|
2512
|
+
} catch {
|
|
2513
|
+
return [];
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
function addScripts(cwd, notes) {
|
|
2517
|
+
const packagePath = path7.join(cwd, "package.json");
|
|
2518
|
+
if (!fs6.existsSync(packagePath)) {
|
|
2519
|
+
notes.push("No package.json found, so npm scripts were not added.");
|
|
2520
|
+
return [];
|
|
2521
|
+
}
|
|
2522
|
+
let parsed;
|
|
2523
|
+
try {
|
|
2524
|
+
parsed = JSON.parse(fs6.readFileSync(packagePath, "utf8"));
|
|
1520
2525
|
} catch {
|
|
2526
|
+
notes.push("Could not parse package.json; npm scripts were not added.");
|
|
2527
|
+
return [];
|
|
2528
|
+
}
|
|
2529
|
+
const scripts = parsed.scripts && typeof parsed.scripts === "object" ? parsed.scripts : {};
|
|
2530
|
+
const added = [];
|
|
2531
|
+
if (!scripts.snapshot) {
|
|
2532
|
+
scripts.snapshot = "codebase-snapshot generate";
|
|
2533
|
+
added.push("snapshot");
|
|
2534
|
+
}
|
|
2535
|
+
if (!scripts["snapshot:validate"]) {
|
|
2536
|
+
scripts["snapshot:validate"] = "codebase-snapshot validate";
|
|
2537
|
+
added.push("snapshot:validate");
|
|
2538
|
+
}
|
|
2539
|
+
if (added.length === 0) return [];
|
|
2540
|
+
parsed.scripts = scripts;
|
|
2541
|
+
fs6.writeFileSync(packagePath, `${JSON.stringify(parsed, null, 2)}
|
|
2542
|
+
`, "utf8");
|
|
2543
|
+
return added;
|
|
2544
|
+
}
|
|
2545
|
+
function addGitignore(cwd) {
|
|
2546
|
+
const ignorePath = path7.join(cwd, ".gitignore");
|
|
2547
|
+
const existing = fs6.existsSync(ignorePath) ? fs6.readFileSync(ignorePath, "utf8") : "";
|
|
2548
|
+
const wanted = ["snapshot/", "snapshot.tmp/"];
|
|
2549
|
+
const added = wanted.filter((entry) => !existing.split(/\r?\n/).includes(entry));
|
|
2550
|
+
if (added.length === 0) return [];
|
|
2551
|
+
const prefix = existing.length === 0 || existing.endsWith("\n") ? "" : "\n";
|
|
2552
|
+
fs6.writeFileSync(ignorePath, `${existing}${prefix}${added.join("\n")}
|
|
2553
|
+
`, "utf8");
|
|
2554
|
+
return added;
|
|
2555
|
+
}
|
|
2556
|
+
function resolvePresetChoice(answers, presetName) {
|
|
2557
|
+
return {
|
|
2558
|
+
value: presetName,
|
|
2559
|
+
label: presetName,
|
|
2560
|
+
hint: presetName === answers.preset ? "detected" : void 0
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
async function runInit(options) {
|
|
2564
|
+
const io = options.io ?? defaultIO();
|
|
2565
|
+
const detected = detectPreset(options.cwd, options.preset);
|
|
2566
|
+
const notes = [];
|
|
2567
|
+
const answers = {
|
|
2568
|
+
preset: detected.presetName,
|
|
2569
|
+
maxPartBytes: options.maxPartBytes ?? detected.recommendedBudget ?? DEFAULT_ANSWERS.maxPartBytes,
|
|
2570
|
+
largeFileBytes: DEFAULT_ANSWERS.largeFileBytes,
|
|
2571
|
+
include: options.include ?? [],
|
|
2572
|
+
excludePatterns: options.exclude ?? [],
|
|
2573
|
+
artifacts: artifactAnswers(options),
|
|
2574
|
+
includeSensitiveFiles: options.includeSensitiveFiles ?? DEFAULT_ANSWERS.includeSensitiveFiles,
|
|
2575
|
+
outDir: options.outDir ?? DEFAULT_ANSWERS.outDir
|
|
2576
|
+
};
|
|
2577
|
+
const interactive = isInteractive(io) && options.yes !== true;
|
|
2578
|
+
if (interactive) {
|
|
2579
|
+
io.output.write("\n @ankurhalder/codebase-snapshot\n");
|
|
2580
|
+
io.output.write(" Turn this repository into an LLM-ready, redacted, byte-bounded snapshot.\n\n");
|
|
2581
|
+
io.output.write(` Detected ${detected.presetName}
|
|
2582
|
+
`);
|
|
2583
|
+
io.output.write(` Directory ${options.cwd}
|
|
2584
|
+
|
|
2585
|
+
`);
|
|
2586
|
+
const useDetected = await confirm(io, `Set up for ${detected.presetName}?`, {
|
|
2587
|
+
defaultValue: true
|
|
2588
|
+
});
|
|
2589
|
+
if (!useDetected) {
|
|
2590
|
+
answers.preset = await select(
|
|
2591
|
+
io,
|
|
2592
|
+
"What kind of project is this?",
|
|
2593
|
+
listPresetNames().map((name) => resolvePresetChoice(answers, name))
|
|
2594
|
+
);
|
|
2595
|
+
const chosen = detectPreset(options.cwd, answers.preset);
|
|
2596
|
+
answers.preset = chosen.presetName;
|
|
2597
|
+
}
|
|
2598
|
+
const scope = await select(
|
|
2599
|
+
io,
|
|
2600
|
+
"What should go into the snapshot?",
|
|
2601
|
+
[
|
|
2602
|
+
{ value: "all", label: "Everything the scanner finds", hint: "recommended \xB7 respects .gitignore" },
|
|
2603
|
+
{ value: "folders", label: "Only specific folders\u2026" },
|
|
2604
|
+
{ value: "no-tests", label: "Everything, but skip tests" },
|
|
2605
|
+
{ value: "custom", label: "Let me write glob patterns" }
|
|
2606
|
+
],
|
|
2607
|
+
{ defaultValue: "all" }
|
|
2608
|
+
);
|
|
2609
|
+
if (scope === "folders") {
|
|
2610
|
+
const dirs = topLevelDirs(options.cwd).slice(0, 40);
|
|
2611
|
+
if (dirs.length === 0) {
|
|
2612
|
+
notes.push("No top-level directories found; the snapshot will include everything.");
|
|
2613
|
+
} else {
|
|
2614
|
+
const chosen = await multiSelect(
|
|
2615
|
+
io,
|
|
2616
|
+
"Select folders",
|
|
2617
|
+
dirs.map((name) => ({ value: name, label: `${name}/` })),
|
|
2618
|
+
{ defaults: dirs }
|
|
2619
|
+
);
|
|
2620
|
+
answers.include = chosen.map((name) => `${name}/**`);
|
|
2621
|
+
}
|
|
2622
|
+
} else if (scope === "no-tests") {
|
|
2623
|
+
answers.excludePatterns = [
|
|
2624
|
+
...answers.excludePatterns,
|
|
2625
|
+
"**/*.test.*",
|
|
2626
|
+
"**/*.spec.*",
|
|
2627
|
+
"tests",
|
|
2628
|
+
"test"
|
|
2629
|
+
];
|
|
2630
|
+
} else if (scope === "custom") {
|
|
2631
|
+
const raw = await text(io, "Glob patterns to include (comma separated)", {
|
|
2632
|
+
defaultValue: ""
|
|
2633
|
+
});
|
|
2634
|
+
answers.include = raw.split(",").map((value) => value.trim()).filter(Boolean);
|
|
2635
|
+
}
|
|
2636
|
+
const sizeChoice = await select(io, "How large should each part be?", SIZE_CHOICES, {
|
|
2637
|
+
defaultValue: "120k"
|
|
2638
|
+
});
|
|
2639
|
+
if (sizeChoice === "custom") {
|
|
2640
|
+
const raw = await text(io, "Part size (e.g. 150k, 1.5mb, 900000)", {
|
|
2641
|
+
defaultValue: String(answers.maxPartBytes),
|
|
2642
|
+
validate: (value) => parseByteSize(value) === void 0 ? "Not a valid size." : null
|
|
2643
|
+
});
|
|
2644
|
+
answers.maxPartBytes = parseByteSize(raw) ?? answers.maxPartBytes;
|
|
2645
|
+
} else {
|
|
2646
|
+
answers.maxPartBytes = sizeChoice;
|
|
2647
|
+
}
|
|
2648
|
+
const artifactChoices = [
|
|
2649
|
+
{ value: "readme", label: "README.md", hint: "orientation page" },
|
|
2650
|
+
{ value: "manifest", label: "manifest.json", hint: "required by validate and report" },
|
|
2651
|
+
{ value: "parts", label: "parts/", hint: "the code payload" },
|
|
2652
|
+
{ value: "navigation", label: "reports/navigation.md", hint: "file \u2192 part index" },
|
|
2653
|
+
{ value: "projectTree", label: "reports/project-tree.md", hint: "ASCII tree" },
|
|
2654
|
+
{ value: "audit", label: "reports/audit.md", hint: "security and health findings" },
|
|
2655
|
+
{ value: "monolith", label: "full-codebase.json + .md", hint: "roughly doubles output" }
|
|
2656
|
+
];
|
|
2657
|
+
const presetEnabled = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]);
|
|
2658
|
+
const selectedArtifacts = await multiSelect(
|
|
2659
|
+
io,
|
|
2660
|
+
"Which artifacts should be generated?",
|
|
2661
|
+
artifactChoices,
|
|
2662
|
+
{ defaults: presetEnabled }
|
|
2663
|
+
);
|
|
2664
|
+
for (const name of ARTIFACT_NAMES) answers.artifacts[name] = selectedArtifacts.includes(name);
|
|
2665
|
+
const enabledCount = ARTIFACT_NAMES.filter((name) => answers.artifacts[name]).length;
|
|
2666
|
+
answers.includeSensitiveFiles = await confirm(
|
|
2667
|
+
io,
|
|
2668
|
+
"Include credential files (.env, *.pem, .npmrc, .ssh/)?",
|
|
2669
|
+
{ defaultValue: false }
|
|
2670
|
+
);
|
|
2671
|
+
io.output.write("\n Review\n\n");
|
|
2672
|
+
io.output.write(` project ${answers.preset}
|
|
2673
|
+
`);
|
|
2674
|
+
io.output.write(` include ${answers.include.length ? answers.include.join(", ") : "everything"}
|
|
2675
|
+
`);
|
|
2676
|
+
io.output.write(` part size ${String(answers.maxPartBytes)}
|
|
2677
|
+
`);
|
|
2678
|
+
io.output.write(` artifacts ${enabledCount} of ${ARTIFACT_NAMES.length}
|
|
2679
|
+
`);
|
|
2680
|
+
io.output.write(` credentials ${answers.includeSensitiveFiles ? "included" : "skipped"}
|
|
2681
|
+
|
|
2682
|
+
`);
|
|
2683
|
+
io.output.write(" Will write snapshot.config with your answers, plus every other setting\n");
|
|
2684
|
+
io.output.write(" at its default \u2014 so nothing is hidden behind the docs.\n\n");
|
|
2685
|
+
const proceed = await confirm(io, "Write this and continue?", { defaultValue: true });
|
|
2686
|
+
if (!proceed) throw new PromptCancelled();
|
|
2687
|
+
}
|
|
2688
|
+
const configText = options.minimal ? renderMinimalConfig(answers) : renderConfig(answers);
|
|
2689
|
+
const configName = supportsTypeScriptImports() ? "snapshot.config.ts" : "snapshot.config.mjs";
|
|
2690
|
+
const configPath = path7.join(options.cwd, configName);
|
|
2691
|
+
if (options.dryRun) {
|
|
1521
2692
|
return {
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
2693
|
+
configPath,
|
|
2694
|
+
configText,
|
|
2695
|
+
written: false,
|
|
2696
|
+
preset: answers.preset,
|
|
2697
|
+
scripts: [],
|
|
2698
|
+
gitignoreEntries: [],
|
|
2699
|
+
notes
|
|
1526
2700
|
};
|
|
1527
2701
|
}
|
|
2702
|
+
const present = existingConfig(options.cwd);
|
|
2703
|
+
if (present && present !== configName && !options.force) {
|
|
2704
|
+
throw new Error(
|
|
2705
|
+
`A configuration already exists (${present}). Re-run with --force to replace it.`
|
|
2706
|
+
);
|
|
2707
|
+
}
|
|
2708
|
+
if (fs6.existsSync(configPath) && !options.force) {
|
|
2709
|
+
throw new Error(`${configName} already exists. Re-run with --force to replace it.`);
|
|
2710
|
+
}
|
|
2711
|
+
fs6.writeFileSync(configPath, configText, "utf8");
|
|
2712
|
+
const scripts = addScripts(options.cwd, notes);
|
|
2713
|
+
const gitignoreEntries = addGitignore(options.cwd);
|
|
2714
|
+
return {
|
|
2715
|
+
configPath,
|
|
2716
|
+
configText,
|
|
2717
|
+
written: true,
|
|
2718
|
+
preset: answers.preset,
|
|
2719
|
+
scripts,
|
|
2720
|
+
gitignoreEntries,
|
|
2721
|
+
notes
|
|
2722
|
+
};
|
|
1528
2723
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
2724
|
+
|
|
2725
|
+
// src/index.ts
|
|
2726
|
+
import * as crypto3 from "crypto";
|
|
2727
|
+
import * as fs9 from "fs";
|
|
2728
|
+
import * as path10 from "path";
|
|
2729
|
+
|
|
2730
|
+
// src/ast/profiles/shared.ts
|
|
2731
|
+
var MAX_SUMMARY_CHARS = 1e6;
|
|
2732
|
+
function emptySummary() {
|
|
2733
|
+
return {
|
|
2734
|
+
directives: [],
|
|
2735
|
+
imports: [],
|
|
2736
|
+
exports: [],
|
|
2737
|
+
reExports: [],
|
|
2738
|
+
functions: [],
|
|
2739
|
+
classes: [],
|
|
2740
|
+
interfaces: [],
|
|
2741
|
+
types: [],
|
|
2742
|
+
enums: [],
|
|
2743
|
+
variables: [],
|
|
2744
|
+
reactComponents: [],
|
|
2745
|
+
hooks: [],
|
|
2746
|
+
routeHandlers: []
|
|
2747
|
+
};
|
|
2748
|
+
}
|
|
2749
|
+
function dedupeSorted(values) {
|
|
2750
|
+
return Array.from(new Set(values.filter(Boolean))).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
2751
|
+
}
|
|
2752
|
+
function finalizeSummary(summary) {
|
|
2753
|
+
return {
|
|
2754
|
+
directives: dedupeSorted(summary.directives),
|
|
2755
|
+
imports: dedupeSorted(summary.imports),
|
|
2756
|
+
exports: dedupeSorted(summary.exports),
|
|
2757
|
+
reExports: dedupeSorted(summary.reExports),
|
|
2758
|
+
functions: dedupeSorted(summary.functions),
|
|
2759
|
+
classes: dedupeSorted(summary.classes),
|
|
2760
|
+
interfaces: dedupeSorted(summary.interfaces),
|
|
2761
|
+
types: dedupeSorted(summary.types),
|
|
2762
|
+
enums: dedupeSorted(summary.enums),
|
|
2763
|
+
variables: dedupeSorted(summary.variables),
|
|
2764
|
+
reactComponents: dedupeSorted(summary.reactComponents),
|
|
2765
|
+
hooks: dedupeSorted(summary.hooks),
|
|
2766
|
+
routeHandlers: dedupeSorted(summary.routeHandlers)
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
function eachLine(content, visit) {
|
|
2770
|
+
const text2 = content.length > MAX_SUMMARY_CHARS ? content.slice(0, MAX_SUMMARY_CHARS) : content;
|
|
2771
|
+
for (const line of text2.split(/\r?\n/)) visit(line);
|
|
2772
|
+
}
|
|
2773
|
+
function isTopLevel(line) {
|
|
2774
|
+
return !/^[\s]/.test(line);
|
|
2775
|
+
}
|
|
2776
|
+
function isPublicName(name) {
|
|
2777
|
+
return name.length > 0 && !name.startsWith("_") && /^[A-Za-z]/.test(name);
|
|
2778
|
+
}
|
|
2779
|
+
function isCapitalizedName(name) {
|
|
2780
|
+
return /^[A-Z]/.test(name);
|
|
2781
|
+
}
|
|
2782
|
+
|
|
2783
|
+
// src/ast/profiles/cfamily.ts
|
|
2784
|
+
var INCLUDE = /^#\s*include\s*[<"]([^>"]+)[>"]/;
|
|
2785
|
+
var DEFINE = /^#\s*define\s+([A-Za-z_]\w*)/;
|
|
2786
|
+
var USING = /^using\s+(?:namespace\s+)?([\w:]+)\s*;/;
|
|
2787
|
+
var TYPE_DECL = /^(?:typedef\s+)?(?:struct|union|class|enum)\s+([A-Za-z_]\w*)/;
|
|
2788
|
+
var TYPEDEF = /^typedef\b[^;{}]*?\b([A-Za-z_]\w*)\s*;/;
|
|
2789
|
+
var FUNCTION = /^[A-Za-z_][\w \t*&:<>,]*?\b([A-Za-z_]\w*)\s*\([^;]*\)\s*\{?\s*$/;
|
|
2790
|
+
var NOT_A_FUNCTION = /^(?:if|for|while|switch|return|else|do|case|sizeof|catch|throw)\b/;
|
|
2791
|
+
var cfamilyProfile = {
|
|
2792
|
+
id: "cfamily",
|
|
2793
|
+
extensions: [".c", ".h", ".cc", ".cpp", ".cxx", ".hpp", ".hxx", ".m", ".mm"],
|
|
2794
|
+
summarize(_path, content) {
|
|
2795
|
+
const summary = emptySummary();
|
|
2796
|
+
eachLine(content, (line) => {
|
|
2797
|
+
const trimmed = line.trim();
|
|
2798
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
|
|
2799
|
+
return;
|
|
2800
|
+
}
|
|
2801
|
+
const included = INCLUDE.exec(trimmed);
|
|
2802
|
+
if (included) {
|
|
2803
|
+
summary.imports.push(included[1]);
|
|
2804
|
+
return;
|
|
2805
|
+
}
|
|
2806
|
+
const used = USING.exec(trimmed);
|
|
2807
|
+
if (used) {
|
|
2808
|
+
summary.imports.push(used[1]);
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2811
|
+
const macro = DEFINE.exec(trimmed);
|
|
2812
|
+
if (macro) {
|
|
2813
|
+
summary.variables.push(macro[1]);
|
|
2814
|
+
return;
|
|
2815
|
+
}
|
|
2816
|
+
if (!isTopLevel(line)) return;
|
|
2817
|
+
const declared = TYPE_DECL.exec(line);
|
|
2818
|
+
if (declared) {
|
|
2819
|
+
const kind = /^typedef\s+enum/.test(line) || /^enum\b/.test(trimmed) ? "enum" : "type";
|
|
2820
|
+
if (kind === "enum") summary.enums.push(declared[1]);
|
|
2821
|
+
else summary.classes.push(declared[1]);
|
|
2822
|
+
summary.exports.push(declared[1]);
|
|
2823
|
+
return;
|
|
2824
|
+
}
|
|
2825
|
+
const aliased = TYPEDEF.exec(line);
|
|
2826
|
+
if (aliased) {
|
|
2827
|
+
summary.types.push(aliased[1]);
|
|
2828
|
+
summary.exports.push(aliased[1]);
|
|
2829
|
+
return;
|
|
2830
|
+
}
|
|
2831
|
+
if (NOT_A_FUNCTION.test(trimmed)) return;
|
|
2832
|
+
const fn = FUNCTION.exec(line);
|
|
2833
|
+
if (fn) {
|
|
2834
|
+
summary.functions.push(fn[1]);
|
|
2835
|
+
summary.exports.push(fn[1]);
|
|
2836
|
+
}
|
|
2837
|
+
});
|
|
2838
|
+
return { summary: finalizeSummary(summary), quality: "heuristic" };
|
|
2839
|
+
}
|
|
2840
|
+
};
|
|
2841
|
+
|
|
2842
|
+
// src/ast/profiles/dotnet.ts
|
|
2843
|
+
var USING2 = /^using\s+([\w.]+)\s*;/;
|
|
2844
|
+
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*)/;
|
|
2845
|
+
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*[<(]/;
|
|
2846
|
+
var dotnetProfile = {
|
|
2847
|
+
id: "dotnet",
|
|
2848
|
+
extensions: [".cs"],
|
|
2849
|
+
summarize(_path, content) {
|
|
2850
|
+
const summary = emptySummary();
|
|
2851
|
+
eachLine(content, (line) => {
|
|
2852
|
+
const trimmed = line.trim();
|
|
2853
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*")) {
|
|
2854
|
+
return;
|
|
2855
|
+
}
|
|
2856
|
+
const used = USING2.exec(trimmed);
|
|
2857
|
+
if (used) {
|
|
2858
|
+
summary.imports.push(used[1]);
|
|
2859
|
+
return;
|
|
2860
|
+
}
|
|
2861
|
+
const declaration = TYPE_DECL2.exec(line);
|
|
2862
|
+
if (declaration) {
|
|
2863
|
+
const kind = declaration[1];
|
|
2864
|
+
const name = declaration[2];
|
|
2865
|
+
if (kind === "interface") summary.interfaces.push(name);
|
|
2866
|
+
else if (kind === "enum") summary.enums.push(name);
|
|
2867
|
+
else summary.classes.push(name);
|
|
2868
|
+
if (/^\s*public\b/.test(line)) summary.exports.push(name);
|
|
2869
|
+
return;
|
|
2870
|
+
}
|
|
2871
|
+
const method = METHOD.exec(line);
|
|
2872
|
+
if (method) {
|
|
2873
|
+
summary.functions.push(method[1]);
|
|
2874
|
+
if (/^\s*public\b/.test(line)) summary.exports.push(method[1]);
|
|
2875
|
+
}
|
|
2876
|
+
});
|
|
2877
|
+
return { summary: finalizeSummary(summary), quality: "heuristic" };
|
|
2878
|
+
}
|
|
2879
|
+
};
|
|
2880
|
+
|
|
2881
|
+
// src/ast/profiles/go.ts
|
|
2882
|
+
var FUNC = /^func\s+(?:\([^)]*\)\s*)?([A-Za-z_]\w*)/;
|
|
2883
|
+
var TYPE = /^type\s+([A-Za-z_]\w*)\s+(.*)$/;
|
|
2884
|
+
var VAR_CONST = /^(?:var|const)\s+([A-Za-z_]\w*)/;
|
|
2885
|
+
var IMPORT_SINGLE = /^import\s+(?:[\w.]+\s+)?"([^"]+)"/;
|
|
2886
|
+
var IMPORT_OPEN = /^import\s*\(/;
|
|
2887
|
+
var IMPORT_ENTRY = /^\s*(?:[\w.]+\s+)?"([^"]+)"/;
|
|
2888
|
+
var goProfile = {
|
|
2889
|
+
id: "go",
|
|
2890
|
+
extensions: [".go"],
|
|
2891
|
+
summarize(_path, content) {
|
|
2892
|
+
const summary = emptySummary();
|
|
2893
|
+
let inImportBlock = false;
|
|
2894
|
+
eachLine(content, (line) => {
|
|
2895
|
+
const trimmed = line.trim();
|
|
2896
|
+
if (!trimmed || trimmed.startsWith("//")) return;
|
|
2897
|
+
if (inImportBlock) {
|
|
2898
|
+
if (trimmed.startsWith(")")) {
|
|
2899
|
+
inImportBlock = false;
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
const entry = IMPORT_ENTRY.exec(line);
|
|
2903
|
+
if (entry) summary.imports.push(entry[1]);
|
|
2904
|
+
return;
|
|
2905
|
+
}
|
|
2906
|
+
if (IMPORT_OPEN.test(trimmed)) {
|
|
2907
|
+
inImportBlock = true;
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
const single = IMPORT_SINGLE.exec(trimmed);
|
|
2911
|
+
if (single) {
|
|
2912
|
+
summary.imports.push(single[1]);
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
if (!isTopLevel(line)) return;
|
|
2916
|
+
const method = FUNC.exec(line);
|
|
2917
|
+
if (method) {
|
|
2918
|
+
const name = method[1];
|
|
2919
|
+
summary.functions.push(name);
|
|
2920
|
+
if (isCapitalizedName(name)) summary.exports.push(name);
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
const declared = TYPE.exec(line);
|
|
2924
|
+
if (declared) {
|
|
2925
|
+
const name = declared[1];
|
|
2926
|
+
const body = declared[2].trim();
|
|
2927
|
+
if (body.startsWith("struct")) summary.classes.push(name);
|
|
2928
|
+
else if (body.startsWith("interface")) summary.interfaces.push(name);
|
|
2929
|
+
else summary.types.push(name);
|
|
2930
|
+
if (isCapitalizedName(name)) summary.exports.push(name);
|
|
2931
|
+
return;
|
|
2932
|
+
}
|
|
2933
|
+
const value = VAR_CONST.exec(line);
|
|
2934
|
+
if (value) {
|
|
2935
|
+
summary.variables.push(value[1]);
|
|
2936
|
+
if (isCapitalizedName(value[1])) summary.exports.push(value[1]);
|
|
2937
|
+
}
|
|
2938
|
+
});
|
|
2939
|
+
return { summary: finalizeSummary(summary), quality: "heuristic" };
|
|
2940
|
+
}
|
|
2941
|
+
};
|
|
2942
|
+
|
|
2943
|
+
// src/ast/profiles/jvm.ts
|
|
2944
|
+
var IMPORT = /^import\s+(?:static\s+)?([\w.]+?)(?:\.\*)?;?\s*$/;
|
|
2945
|
+
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*)/;
|
|
2946
|
+
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*)/;
|
|
2947
|
+
var SCALA_DEF = /^\s*def\s+([A-Za-z_]\w*)/;
|
|
2948
|
+
var JAVA_METHOD = /^\s+(?:public|private|protected)\s+(?:static\s+)?(?:final\s+)?(?:synchronized\s+)?(?:abstract\s+)?[\w<>[\],.?]+\s+([A-Za-z_]\w*)\s*\(/;
|
|
2949
|
+
var jvmProfile = {
|
|
2950
|
+
id: "jvm",
|
|
2951
|
+
extensions: [".java", ".kt", ".kts", ".scala"],
|
|
2952
|
+
summarize(path12, content) {
|
|
2953
|
+
const summary = emptySummary();
|
|
2954
|
+
const lower = path12.toLowerCase();
|
|
2955
|
+
const isJava = lower.endsWith(".java");
|
|
2956
|
+
const isKotlin = lower.endsWith(".kt") || lower.endsWith(".kts");
|
|
2957
|
+
eachLine(content, (line) => {
|
|
2958
|
+
const trimmed = line.trim();
|
|
2959
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
const imported = IMPORT.exec(trimmed);
|
|
2963
|
+
if (imported) {
|
|
2964
|
+
summary.imports.push(imported[1]);
|
|
2965
|
+
return;
|
|
2966
|
+
}
|
|
2967
|
+
const declaration = TYPE_DECL3.exec(line);
|
|
2968
|
+
if (declaration) {
|
|
2969
|
+
const kind = declaration[1];
|
|
2970
|
+
const name = declaration[2];
|
|
2971
|
+
if (kind === "interface" || kind === "trait") summary.interfaces.push(name);
|
|
2972
|
+
else if (kind === "enum") summary.enums.push(name);
|
|
2973
|
+
else summary.classes.push(name);
|
|
2974
|
+
const hidden = /^\s*(?:private|protected|internal)\b/.test(line);
|
|
2975
|
+
if (isJava ? /^\s*public\b/.test(line) : !hidden) summary.exports.push(name);
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
const kotlinFunction = KOTLIN_FUN.exec(line);
|
|
2979
|
+
if (kotlinFunction && isKotlin) {
|
|
2980
|
+
summary.functions.push(kotlinFunction[1]);
|
|
2981
|
+
const hidden = /^\s*(?:private|protected|internal)\b/.test(line);
|
|
2982
|
+
if (!hidden) summary.exports.push(kotlinFunction[1]);
|
|
2983
|
+
return;
|
|
2984
|
+
}
|
|
2985
|
+
const scalaFunction = SCALA_DEF.exec(line);
|
|
2986
|
+
if (scalaFunction && !isJava && !isKotlin) {
|
|
2987
|
+
summary.functions.push(scalaFunction[1]);
|
|
2988
|
+
const hidden = /^\s*(?:private|protected)\b/.test(line);
|
|
2989
|
+
if (!hidden) summary.exports.push(scalaFunction[1]);
|
|
2990
|
+
return;
|
|
2991
|
+
}
|
|
2992
|
+
if (isJava) {
|
|
2993
|
+
const method = JAVA_METHOD.exec(line);
|
|
2994
|
+
if (method) {
|
|
2995
|
+
summary.functions.push(method[1]);
|
|
2996
|
+
if (/^\s*public\b/.test(line)) summary.exports.push(method[1]);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2999
|
+
});
|
|
3000
|
+
return { summary: finalizeSummary(summary), quality: "heuristic" };
|
|
3001
|
+
}
|
|
3002
|
+
};
|
|
3003
|
+
|
|
3004
|
+
// src/ast/profiles/php.ts
|
|
3005
|
+
var USE = /^use\s+([\w\\]+)\s*(?:as\s+\w+)?;/;
|
|
3006
|
+
var DECLARE = /^(?:abstract\s+|final\s+|readonly\s+)*(class|interface|trait|enum)\s+([A-Za-z_]\w*)/;
|
|
3007
|
+
var FUNCTION2 = /^(?:public\s+|protected\s+|private\s+|static\s+|final\s+|abstract\s+)*function\s+([A-Za-z_]\w*)/;
|
|
3008
|
+
var phpProfile = {
|
|
3009
|
+
id: "php",
|
|
3010
|
+
extensions: [".php"],
|
|
3011
|
+
summarize(_path, content) {
|
|
3012
|
+
const summary = emptySummary();
|
|
3013
|
+
eachLine(content, (line) => {
|
|
3014
|
+
const trimmed = line.trim();
|
|
3015
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("/*")) {
|
|
3016
|
+
return;
|
|
3017
|
+
}
|
|
3018
|
+
const used = USE.exec(trimmed);
|
|
3019
|
+
if (used) {
|
|
3020
|
+
summary.imports.push(used[1].replace(/\\\\/g, "\\"));
|
|
3021
|
+
return;
|
|
3022
|
+
}
|
|
3023
|
+
const declaration = DECLARE.exec(trimmed);
|
|
3024
|
+
if (declaration) {
|
|
3025
|
+
const kind = declaration[1];
|
|
3026
|
+
const name = declaration[2];
|
|
3027
|
+
if (kind === "interface" || kind === "trait") summary.interfaces.push(name);
|
|
3028
|
+
else if (kind === "enum") summary.enums.push(name);
|
|
3029
|
+
else summary.classes.push(name);
|
|
3030
|
+
summary.exports.push(name);
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
3033
|
+
const fn = FUNCTION2.exec(trimmed);
|
|
3034
|
+
if (fn) {
|
|
3035
|
+
summary.functions.push(fn[1]);
|
|
3036
|
+
if (!/^private\b/.test(trimmed)) summary.exports.push(fn[1]);
|
|
3037
|
+
}
|
|
3038
|
+
});
|
|
3039
|
+
return { summary: finalizeSummary(summary), quality: "heuristic" };
|
|
3040
|
+
}
|
|
3041
|
+
};
|
|
3042
|
+
|
|
3043
|
+
// src/ast/profiles/python.ts
|
|
3044
|
+
var IMPORT2 = /^import\s+(.+)$/;
|
|
3045
|
+
var FROM_IMPORT = /^from\s+([\w.]+)\s+import\s+(.+)$/;
|
|
3046
|
+
var CLASS = /^class\s+([A-Za-z_]\w*)/;
|
|
3047
|
+
var DEF = /^(?:async\s+)?def\s+([A-Za-z_]\w*)/;
|
|
3048
|
+
var CONSTANT = /^([A-Z_][A-Z0-9_]*)\s*(?::[^=]+)?=/;
|
|
3049
|
+
var ALL_DUNDER = /^__all__\s*=\s*\[([^\]]*)\]/;
|
|
3050
|
+
var ROUTE_DECORATOR = /^\s*@[\w.]*\b(?:get|post|put|patch|delete|head|options|route)\s*\(/i;
|
|
3051
|
+
var pythonProfile = {
|
|
3052
|
+
id: "python",
|
|
3053
|
+
extensions: [".py", ".pyi"],
|
|
3054
|
+
summarize(_path, content) {
|
|
3055
|
+
const summary = emptySummary();
|
|
3056
|
+
let routePending = false;
|
|
3057
|
+
eachLine(content, (line) => {
|
|
3058
|
+
const trimmed = line.trim();
|
|
3059
|
+
if (!trimmed || trimmed.startsWith("#")) return;
|
|
3060
|
+
if (ROUTE_DECORATOR.test(line)) {
|
|
3061
|
+
routePending = true;
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
const from = FROM_IMPORT.exec(trimmed);
|
|
3065
|
+
if (from) {
|
|
3066
|
+
summary.imports.push(from[1]);
|
|
3067
|
+
routePending = false;
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
const imported = IMPORT2.exec(trimmed);
|
|
3071
|
+
if (imported) {
|
|
3072
|
+
for (const part of imported[1].split(",")) {
|
|
3073
|
+
const name = part.trim().split(/\s+as\s+/)[0].trim();
|
|
3074
|
+
if (name) summary.imports.push(name);
|
|
3075
|
+
}
|
|
3076
|
+
routePending = false;
|
|
3077
|
+
return;
|
|
3078
|
+
}
|
|
3079
|
+
if (isTopLevel(line)) {
|
|
3080
|
+
const klass = CLASS.exec(line);
|
|
3081
|
+
if (klass) {
|
|
3082
|
+
summary.classes.push(klass[1]);
|
|
3083
|
+
if (isPublicName(klass[1])) summary.exports.push(klass[1]);
|
|
3084
|
+
routePending = false;
|
|
3085
|
+
return;
|
|
3086
|
+
}
|
|
3087
|
+
const definition = DEF.exec(line);
|
|
3088
|
+
if (definition) {
|
|
3089
|
+
const name = definition[1];
|
|
3090
|
+
summary.functions.push(name);
|
|
3091
|
+
if (routePending) {
|
|
3092
|
+
summary.routeHandlers.push(name);
|
|
3093
|
+
routePending = false;
|
|
3094
|
+
}
|
|
3095
|
+
if (isPublicName(name)) summary.exports.push(name);
|
|
3096
|
+
return;
|
|
3097
|
+
}
|
|
3098
|
+
const all = ALL_DUNDER.exec(trimmed);
|
|
3099
|
+
if (all) {
|
|
3100
|
+
for (const entry of all[1].split(",")) {
|
|
3101
|
+
const name = entry.trim().replace(/['"]/g, "").trim();
|
|
3102
|
+
if (name) summary.exports.push(name);
|
|
3103
|
+
}
|
|
3104
|
+
return;
|
|
3105
|
+
}
|
|
3106
|
+
const constant = CONSTANT.exec(line);
|
|
3107
|
+
if (constant) summary.variables.push(constant[1]);
|
|
1544
3108
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
continue;
|
|
1548
|
-
}
|
|
1549
|
-
const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line.trim());
|
|
1550
|
-
if (headingMatch) {
|
|
1551
|
-
headings.push(`${headingMatch[1]} ${headingMatch[2].trim()}`);
|
|
1552
|
-
}
|
|
3109
|
+
});
|
|
3110
|
+
return { summary: finalizeSummary(summary), quality: "heuristic" };
|
|
1553
3111
|
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
3112
|
+
};
|
|
3113
|
+
|
|
3114
|
+
// src/ast/profiles/ruby.ts
|
|
3115
|
+
var REQUIRE = /^require(?:_relative)?\s+["']([^"']+)["']/;
|
|
3116
|
+
var CLASS_OR_MODULE = /^(?:class|module)\s+([A-Z][\w:]*)/;
|
|
3117
|
+
var METHOD2 = /^def\s+(?:self\.)?([A-Za-z_]\w*[?!=]?)/;
|
|
3118
|
+
var rubyProfile = {
|
|
3119
|
+
id: "ruby",
|
|
3120
|
+
extensions: [".rb", ".rake", ".gemspec"],
|
|
3121
|
+
summarize(_path, content) {
|
|
3122
|
+
const summary = emptySummary();
|
|
3123
|
+
eachLine(content, (line) => {
|
|
3124
|
+
const trimmed = line.trim();
|
|
3125
|
+
if (!trimmed || trimmed.startsWith("#")) return;
|
|
3126
|
+
const required = REQUIRE.exec(trimmed);
|
|
3127
|
+
if (required) {
|
|
3128
|
+
summary.imports.push(required[1]);
|
|
3129
|
+
return;
|
|
3130
|
+
}
|
|
3131
|
+
if (!isTopLevel(line)) return;
|
|
3132
|
+
const declaration = CLASS_OR_MODULE.exec(line);
|
|
3133
|
+
if (declaration) {
|
|
3134
|
+
summary.classes.push(declaration[1]);
|
|
3135
|
+
summary.exports.push(declaration[1]);
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
const method = METHOD2.exec(line);
|
|
3139
|
+
if (method) {
|
|
3140
|
+
summary.functions.push(method[1]);
|
|
3141
|
+
summary.exports.push(method[1]);
|
|
3142
|
+
}
|
|
3143
|
+
});
|
|
3144
|
+
return { summary: finalizeSummary(summary), quality: "heuristic" };
|
|
3145
|
+
}
|
|
3146
|
+
};
|
|
3147
|
+
|
|
3148
|
+
// src/ast/profiles/rust.ts
|
|
3149
|
+
var USE2 = /^use\s+([^;]+);/;
|
|
3150
|
+
var FN = /^(pub(?:\([^)]*\))?\s+)?(?:const\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+([A-Za-z_]\w*)/;
|
|
3151
|
+
var STRUCT = /^(pub(?:\([^)]*\))?\s+)?struct\s+([A-Za-z_]\w*)/;
|
|
3152
|
+
var TRAIT = /^(pub(?:\([^)]*\))?\s+)?(?:unsafe\s+)?trait\s+([A-Za-z_]\w*)/;
|
|
3153
|
+
var ENUM = /^(pub(?:\([^)]*\))?\s+)?enum\s+([A-Za-z_]\w*)/;
|
|
3154
|
+
var TYPE2 = /^(pub(?:\([^)]*\))?\s+)?type\s+([A-Za-z_]\w*)/;
|
|
3155
|
+
var MOD = /^(pub(?:\([^)]*\))?\s+)?mod\s+([A-Za-z_]\w*)/;
|
|
3156
|
+
var IMPL = /^impl(?:<[^>]*>)?\s+(?:[\w:<>]+\s+for\s+)?([A-Za-z_]\w*)/;
|
|
3157
|
+
var rustProfile = {
|
|
3158
|
+
id: "rust",
|
|
3159
|
+
extensions: [".rs"],
|
|
3160
|
+
summarize(_path, content) {
|
|
3161
|
+
const summary = emptySummary();
|
|
3162
|
+
eachLine(content, (line) => {
|
|
3163
|
+
const trimmed = line.trim();
|
|
3164
|
+
if (!trimmed || trimmed.startsWith("//")) return;
|
|
3165
|
+
const used = USE2.exec(trimmed);
|
|
3166
|
+
if (used) {
|
|
3167
|
+
summary.imports.push(used[1].trim());
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3170
|
+
if (!isTopLevel(line)) return;
|
|
3171
|
+
const isPublic = /^pub\b/.test(trimmed);
|
|
3172
|
+
const fn = FN.exec(line);
|
|
3173
|
+
if (fn) {
|
|
3174
|
+
summary.functions.push(fn[2]);
|
|
3175
|
+
if (isPublic) summary.exports.push(fn[2]);
|
|
3176
|
+
return;
|
|
3177
|
+
}
|
|
3178
|
+
const struct = STRUCT.exec(line);
|
|
3179
|
+
if (struct) {
|
|
3180
|
+
summary.classes.push(struct[2]);
|
|
3181
|
+
if (isPublic) summary.exports.push(struct[2]);
|
|
3182
|
+
return;
|
|
3183
|
+
}
|
|
3184
|
+
const trait = TRAIT.exec(line);
|
|
3185
|
+
if (trait) {
|
|
3186
|
+
summary.interfaces.push(trait[2]);
|
|
3187
|
+
if (isPublic) summary.exports.push(trait[2]);
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
const enumeration = ENUM.exec(line);
|
|
3191
|
+
if (enumeration) {
|
|
3192
|
+
summary.enums.push(enumeration[2]);
|
|
3193
|
+
if (isPublic) summary.exports.push(enumeration[2]);
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
const alias = TYPE2.exec(line);
|
|
3197
|
+
if (alias) {
|
|
3198
|
+
summary.types.push(alias[2]);
|
|
3199
|
+
if (isPublic) summary.exports.push(alias[2]);
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
const module = MOD.exec(line);
|
|
3203
|
+
if (module) {
|
|
3204
|
+
summary.variables.push(module[2]);
|
|
3205
|
+
if (isPublic) summary.exports.push(module[2]);
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
const implementation = IMPL.exec(line);
|
|
3209
|
+
if (implementation) summary.classes.push(implementation[1]);
|
|
3210
|
+
});
|
|
3211
|
+
return { summary: finalizeSummary(summary), quality: "heuristic" };
|
|
3212
|
+
}
|
|
3213
|
+
};
|
|
1559
3214
|
|
|
1560
3215
|
// src/ast/ts-parser.ts
|
|
1561
3216
|
import ts from "typescript";
|
|
@@ -1598,9 +3253,9 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
1598
3253
|
if (parseErrors > 0) summary.parseErrors = parseErrors;
|
|
1599
3254
|
for (const statement of sourceFile.statements) {
|
|
1600
3255
|
if (ts.isExpressionStatement(statement) && ts.isStringLiteral(statement.expression)) {
|
|
1601
|
-
const
|
|
1602
|
-
if (
|
|
1603
|
-
summary.directives.push(
|
|
3256
|
+
const text2 = statement.expression.text;
|
|
3257
|
+
if (text2 === "use client" || text2 === "use server") {
|
|
3258
|
+
summary.directives.push(text2);
|
|
1604
3259
|
}
|
|
1605
3260
|
continue;
|
|
1606
3261
|
}
|
|
@@ -1700,23 +3355,212 @@ function summarizeTypeScriptSource(relativePath, content) {
|
|
|
1700
3355
|
stack.push(children[index]);
|
|
1701
3356
|
}
|
|
1702
3357
|
}
|
|
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 =
|
|
3358
|
+
const dedupeSorted2 = (values) => Array.from(new Set(values)).sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
3359
|
+
summary.directives = dedupeSorted2(summary.directives);
|
|
3360
|
+
summary.imports = dedupeSorted2(summary.imports);
|
|
3361
|
+
summary.exports = dedupeSorted2(summary.exports);
|
|
3362
|
+
summary.reExports = dedupeSorted2(summary.reExports);
|
|
3363
|
+
summary.functions = dedupeSorted2(summary.functions);
|
|
3364
|
+
summary.classes = dedupeSorted2(summary.classes);
|
|
3365
|
+
summary.interfaces = dedupeSorted2(summary.interfaces);
|
|
3366
|
+
summary.types = dedupeSorted2(summary.types);
|
|
3367
|
+
summary.enums = dedupeSorted2(summary.enums);
|
|
3368
|
+
summary.variables = dedupeSorted2(summary.variables);
|
|
3369
|
+
summary.reactComponents = dedupeSorted2(summary.reactComponents);
|
|
3370
|
+
summary.hooks = dedupeSorted2(summary.hooks);
|
|
3371
|
+
summary.routeHandlers = dedupeSorted2(summary.routeHandlers);
|
|
1717
3372
|
return summary;
|
|
1718
3373
|
}
|
|
1719
3374
|
|
|
3375
|
+
// src/ast/profiles/sfc.ts
|
|
3376
|
+
var SCRIPT_BLOCK = /<script[^>]*>([\s\S]*?)<\/script>/gi;
|
|
3377
|
+
var ASTRO_FRONTMATTER = /^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?=\r?\n|$)/;
|
|
3378
|
+
function componentName(path12) {
|
|
3379
|
+
const base = path12.split("/").pop() ?? path12;
|
|
3380
|
+
return base.replace(/\.[^.]+$/, "");
|
|
3381
|
+
}
|
|
3382
|
+
var sfcProfile = {
|
|
3383
|
+
id: "sfc",
|
|
3384
|
+
extensions: [".vue", ".svelte", ".astro"],
|
|
3385
|
+
summarize(path12, content) {
|
|
3386
|
+
let script = "";
|
|
3387
|
+
if (path12.toLowerCase().endsWith(".astro")) {
|
|
3388
|
+
const frontmatter = ASTRO_FRONTMATTER.exec(content);
|
|
3389
|
+
if (frontmatter) script += `${frontmatter[1]}
|
|
3390
|
+
`;
|
|
3391
|
+
}
|
|
3392
|
+
for (const match of content.matchAll(SCRIPT_BLOCK)) {
|
|
3393
|
+
script += `${match[1]}
|
|
3394
|
+
`;
|
|
3395
|
+
}
|
|
3396
|
+
const name = componentName(path12);
|
|
3397
|
+
if (!script.trim()) {
|
|
3398
|
+
const summary2 = emptySummary();
|
|
3399
|
+
if (name) {
|
|
3400
|
+
summary2.exports.push(name);
|
|
3401
|
+
summary2.reactComponents.push(name);
|
|
3402
|
+
}
|
|
3403
|
+
return { summary: finalizeSummary(summary2), quality: "heuristic" };
|
|
3404
|
+
}
|
|
3405
|
+
const summary = summarizeTypeScriptSource(`${name}.ts`, script);
|
|
3406
|
+
summary.reactComponents.push(name);
|
|
3407
|
+
summary.exports.push(name);
|
|
3408
|
+
return { summary, quality: "ast" };
|
|
3409
|
+
}
|
|
3410
|
+
};
|
|
3411
|
+
|
|
3412
|
+
// src/ast/profiles/index.ts
|
|
3413
|
+
var LANGUAGE_PROFILES = [
|
|
3414
|
+
pythonProfile,
|
|
3415
|
+
goProfile,
|
|
3416
|
+
rustProfile,
|
|
3417
|
+
jvmProfile,
|
|
3418
|
+
dotnetProfile,
|
|
3419
|
+
phpProfile,
|
|
3420
|
+
rubyProfile,
|
|
3421
|
+
cfamilyProfile,
|
|
3422
|
+
sfcProfile
|
|
3423
|
+
];
|
|
3424
|
+
var BY_EXTENSION = /* @__PURE__ */ new Map();
|
|
3425
|
+
for (const profile of LANGUAGE_PROFILES) {
|
|
3426
|
+
for (const extension of profile.extensions) {
|
|
3427
|
+
BY_EXTENSION.set(extension.toLowerCase(), profile);
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
function profileForPath(path12) {
|
|
3431
|
+
const lower = path12.toLowerCase();
|
|
3432
|
+
const dot = lower.lastIndexOf(".");
|
|
3433
|
+
if (dot === -1) return null;
|
|
3434
|
+
return BY_EXTENSION.get(lower.slice(dot)) ?? null;
|
|
3435
|
+
}
|
|
3436
|
+
function summarizeWithProfile(path12, content) {
|
|
3437
|
+
const profile = profileForPath(path12);
|
|
3438
|
+
return profile ? profile.summarize(path12, content) : null;
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
// src/ast/structured.ts
|
|
3442
|
+
function stripJsonComments2(content) {
|
|
3443
|
+
const text2 = content.replace(/^\uFEFF/, "");
|
|
3444
|
+
let out = "";
|
|
3445
|
+
let inString = false;
|
|
3446
|
+
let inLineComment = false;
|
|
3447
|
+
let inBlockComment = false;
|
|
3448
|
+
for (let i = 0; i < text2.length; i++) {
|
|
3449
|
+
const char = text2[i];
|
|
3450
|
+
const next = text2[i + 1];
|
|
3451
|
+
if (inLineComment) {
|
|
3452
|
+
if (char === "\n") {
|
|
3453
|
+
inLineComment = false;
|
|
3454
|
+
out += char;
|
|
3455
|
+
}
|
|
3456
|
+
continue;
|
|
3457
|
+
}
|
|
3458
|
+
if (inBlockComment) {
|
|
3459
|
+
if (char === "*" && next === "/") {
|
|
3460
|
+
inBlockComment = false;
|
|
3461
|
+
i += 1;
|
|
3462
|
+
}
|
|
3463
|
+
continue;
|
|
3464
|
+
}
|
|
3465
|
+
if (inString) {
|
|
3466
|
+
out += char;
|
|
3467
|
+
if (char === "\\") {
|
|
3468
|
+
if (next !== void 0) {
|
|
3469
|
+
out += next;
|
|
3470
|
+
i += 1;
|
|
3471
|
+
}
|
|
3472
|
+
} else if (char === '"') {
|
|
3473
|
+
inString = false;
|
|
3474
|
+
}
|
|
3475
|
+
continue;
|
|
3476
|
+
}
|
|
3477
|
+
if (char === '"') {
|
|
3478
|
+
inString = true;
|
|
3479
|
+
out += char;
|
|
3480
|
+
continue;
|
|
3481
|
+
}
|
|
3482
|
+
if (char === "/" && next === "/") {
|
|
3483
|
+
inLineComment = true;
|
|
3484
|
+
i += 1;
|
|
3485
|
+
continue;
|
|
3486
|
+
}
|
|
3487
|
+
if (char === "/" && next === "*") {
|
|
3488
|
+
inBlockComment = true;
|
|
3489
|
+
i += 1;
|
|
3490
|
+
continue;
|
|
3491
|
+
}
|
|
3492
|
+
out += char;
|
|
3493
|
+
}
|
|
3494
|
+
return out;
|
|
3495
|
+
}
|
|
3496
|
+
function summarizeJson(content) {
|
|
3497
|
+
try {
|
|
3498
|
+
const parsed = JSON.parse(stripJsonComments2(content));
|
|
3499
|
+
if (Array.isArray(parsed)) {
|
|
3500
|
+
const sample = parsed[0];
|
|
3501
|
+
const sampleKeys = sample && typeof sample === "object" && !Array.isArray(sample) ? Object.keys(sample).slice(0, 10).sort() : [];
|
|
3502
|
+
return {
|
|
3503
|
+
shape: "array",
|
|
3504
|
+
itemCount: parsed.length,
|
|
3505
|
+
keys: [],
|
|
3506
|
+
sampleKeys
|
|
3507
|
+
};
|
|
3508
|
+
}
|
|
3509
|
+
if (parsed && typeof parsed === "object") {
|
|
3510
|
+
const keys = Object.keys(parsed).sort();
|
|
3511
|
+
return {
|
|
3512
|
+
shape: "object",
|
|
3513
|
+
itemCount: keys.length,
|
|
3514
|
+
keys: keys.slice(0, 30),
|
|
3515
|
+
sampleKeys: []
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
return {
|
|
3519
|
+
shape: typeof parsed,
|
|
3520
|
+
itemCount: null,
|
|
3521
|
+
keys: [],
|
|
3522
|
+
sampleKeys: []
|
|
3523
|
+
};
|
|
3524
|
+
} catch {
|
|
3525
|
+
return {
|
|
3526
|
+
shape: "invalid-json",
|
|
3527
|
+
itemCount: null,
|
|
3528
|
+
keys: [],
|
|
3529
|
+
sampleKeys: []
|
|
3530
|
+
};
|
|
3531
|
+
}
|
|
3532
|
+
}
|
|
3533
|
+
function summarizeMarkdown(content) {
|
|
3534
|
+
const headings = [];
|
|
3535
|
+
const frontmatterKeys = [];
|
|
3536
|
+
const lines = content.replace(/^\uFEFF/, "").split("\n");
|
|
3537
|
+
let inFrontmatter = false;
|
|
3538
|
+
for (let index = 0; index < lines.length; index++) {
|
|
3539
|
+
const line = lines[index];
|
|
3540
|
+
if (index === 0 && line.trim() === "---") {
|
|
3541
|
+
inFrontmatter = true;
|
|
3542
|
+
continue;
|
|
3543
|
+
}
|
|
3544
|
+
if (inFrontmatter) {
|
|
3545
|
+
if (line.trim() === "---") {
|
|
3546
|
+
inFrontmatter = false;
|
|
3547
|
+
continue;
|
|
3548
|
+
}
|
|
3549
|
+
const match = /^([a-zA-Z0-9_-]+):/.exec(line);
|
|
3550
|
+
if (match) frontmatterKeys.push(match[1]);
|
|
3551
|
+
continue;
|
|
3552
|
+
}
|
|
3553
|
+
const headingMatch = /^(#{1,6})\s+(.+)$/.exec(line.trim());
|
|
3554
|
+
if (headingMatch) {
|
|
3555
|
+
headings.push(`${headingMatch[1]} ${headingMatch[2].trim()}`);
|
|
3556
|
+
}
|
|
3557
|
+
}
|
|
3558
|
+
return {
|
|
3559
|
+
headings: headings.slice(0, 40),
|
|
3560
|
+
frontmatterKeys: Array.from(new Set(frontmatterKeys)).sort()
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
|
|
1720
3564
|
// src/generator/audit.ts
|
|
1721
3565
|
function generateAuditReport(manifest, secretFindings) {
|
|
1722
3566
|
const lines = [
|
|
@@ -1782,11 +3626,11 @@ function generateAuditReport(manifest, secretFindings) {
|
|
|
1782
3626
|
"| :--- | :--- | :--- |"
|
|
1783
3627
|
);
|
|
1784
3628
|
const totalFiles = manifest.counts.totalFiles || 1;
|
|
1785
|
-
for (const [
|
|
3629
|
+
for (const [layer2, count] of Object.entries(manifest.layers).sort(
|
|
1786
3630
|
(a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0)
|
|
1787
3631
|
)) {
|
|
1788
3632
|
const pct = (count / totalFiles * 100).toFixed(1);
|
|
1789
|
-
lines.push(`| \`${
|
|
3633
|
+
lines.push(`| \`${layer2}\` | ${count} | ${pct}% |`);
|
|
1790
3634
|
}
|
|
1791
3635
|
lines.push("");
|
|
1792
3636
|
lines.push(
|
|
@@ -1855,21 +3699,21 @@ function partitionFiles(files, maxPartBytes, contentFingerprint) {
|
|
|
1855
3699
|
section.byteSize = Buffer.byteLength(section.text, "utf8");
|
|
1856
3700
|
continue;
|
|
1857
3701
|
}
|
|
1858
|
-
const
|
|
1859
|
-
const target = Math.max(0, budget - Buffer.byteLength(
|
|
3702
|
+
const marker2 = "\n... [part truncated: cannot fit budget] ...\n";
|
|
3703
|
+
const target = Math.max(0, budget - Buffer.byteLength(marker2, "utf8"));
|
|
1860
3704
|
const clipped = truncateToBytes(rendered[oversized].markdown, target);
|
|
1861
|
-
rendered[oversized] = rehashPart(rendered[oversized], clipped.content +
|
|
3705
|
+
rendered[oversized] = rehashPart(rendered[oversized], clipped.content + marker2);
|
|
1862
3706
|
return rendered;
|
|
1863
3707
|
}
|
|
1864
3708
|
return enforceBudget(renderGroups(groups, contentFingerprint), budget);
|
|
1865
3709
|
}
|
|
1866
3710
|
function enforceBudget(parts, budget) {
|
|
1867
|
-
const
|
|
1868
|
-
const target = Math.max(0, budget - Buffer.byteLength(
|
|
3711
|
+
const marker2 = "\n... [part truncated: cannot fit budget] ...\n";
|
|
3712
|
+
const target = Math.max(0, budget - Buffer.byteLength(marker2, "utf8"));
|
|
1869
3713
|
return parts.map((part) => {
|
|
1870
3714
|
if (part.metadata.byteSize <= budget) return part;
|
|
1871
3715
|
const clipped = truncateToBytes(part.markdown, target);
|
|
1872
|
-
return rehashPart(part, clipped.content +
|
|
3716
|
+
return rehashPart(part, clipped.content + marker2);
|
|
1873
3717
|
});
|
|
1874
3718
|
}
|
|
1875
3719
|
function renumberParts(parts, contentFingerprint) {
|
|
@@ -1877,10 +3721,10 @@ function renumberParts(parts, contentFingerprint) {
|
|
|
1877
3721
|
if (total === 0) return [];
|
|
1878
3722
|
const zeroPad = (index) => String(index).padStart(Math.max(3, String(total).length), "0");
|
|
1879
3723
|
const nameFor = (position) => `part-${zeroPad(position)}.md`;
|
|
1880
|
-
const
|
|
3724
|
+
const marker2 = "## Contents\n";
|
|
1881
3725
|
return parts.map((part, index) => {
|
|
1882
|
-
const markerIndex = part.markdown.indexOf(
|
|
1883
|
-
const body = markerIndex === -1 ? "" : part.markdown.slice(markerIndex +
|
|
3726
|
+
const markerIndex = part.markdown.indexOf(marker2);
|
|
3727
|
+
const body = markerIndex === -1 ? "" : part.markdown.slice(markerIndex + marker2.length);
|
|
1884
3728
|
const included = part.metadata.includedFiles;
|
|
1885
3729
|
if (markerIndex === -1) {
|
|
1886
3730
|
throw new Error(`${part.filename} has no "## Contents" section to re-number.`);
|
|
@@ -1938,9 +3782,9 @@ function topEntries(counts, limit) {
|
|
|
1938
3782
|
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
3783
|
}
|
|
1940
3784
|
function prepareSection(file, sectionBudget) {
|
|
1941
|
-
let
|
|
1942
|
-
let byteSize = Buffer.byteLength(
|
|
1943
|
-
if (byteSize <= sectionBudget) return { file, text, byteSize };
|
|
3785
|
+
let text2 = renderFileSection(file);
|
|
3786
|
+
let byteSize = Buffer.byteLength(text2, "utf8");
|
|
3787
|
+
if (byteSize <= sectionBudget) return { file, text: text2, byteSize };
|
|
1944
3788
|
const shrinkBody = () => {
|
|
1945
3789
|
const content = file.content ?? "";
|
|
1946
3790
|
const overhead = byteSize - Buffer.byteLength(content, "utf8");
|
|
@@ -1949,24 +3793,24 @@ function prepareSection(file, sectionBudget) {
|
|
|
1949
3793
|
const truncated = truncateToBytes(content, allowed);
|
|
1950
3794
|
if (!truncated.isTruncated) return;
|
|
1951
3795
|
applyTruncatedContent(file, truncated.content);
|
|
1952
|
-
|
|
1953
|
-
byteSize = Buffer.byteLength(
|
|
3796
|
+
text2 = renderFileSection(file);
|
|
3797
|
+
byteSize = Buffer.byteLength(text2, "utf8");
|
|
1954
3798
|
};
|
|
1955
3799
|
shrinkBody();
|
|
1956
3800
|
if (byteSize > sectionBudget && file.summary) {
|
|
1957
3801
|
file.summary = void 0;
|
|
1958
3802
|
file.summaryOmitted = true;
|
|
1959
|
-
|
|
1960
|
-
byteSize = Buffer.byteLength(
|
|
3803
|
+
text2 = renderFileSection(file);
|
|
3804
|
+
byteSize = Buffer.byteLength(text2, "utf8");
|
|
1961
3805
|
shrinkBody();
|
|
1962
3806
|
}
|
|
1963
3807
|
if (byteSize > sectionBudget) {
|
|
1964
3808
|
const target = sectionBudget - Buffer.byteLength(SECTION_TRUNCATION_NOTICE, "utf8");
|
|
1965
|
-
const truncated = truncateToBytes(
|
|
1966
|
-
|
|
1967
|
-
byteSize = Buffer.byteLength(
|
|
3809
|
+
const truncated = truncateToBytes(text2, Math.max(0, target));
|
|
3810
|
+
text2 = truncated.content + SECTION_TRUNCATION_NOTICE;
|
|
3811
|
+
byteSize = Buffer.byteLength(text2, "utf8");
|
|
1968
3812
|
}
|
|
1969
|
-
return { file, text, byteSize };
|
|
3813
|
+
return { file, text: text2, byteSize };
|
|
1970
3814
|
}
|
|
1971
3815
|
function applyTruncatedContent(file, content) {
|
|
1972
3816
|
file.content = content;
|
|
@@ -2088,6 +3932,7 @@ function renderFileSection(file) {
|
|
|
2088
3932
|
`- Source Lines: ${file.originalLines}`,
|
|
2089
3933
|
`- Included Lines: ${file.includedLines}`,
|
|
2090
3934
|
`- SHA-256: ${file.hashSha256}`,
|
|
3935
|
+
...file.summaryQuality ? [`- Summary Quality: ${file.summaryQuality}`] : [],
|
|
2091
3936
|
...renderSummary(file.summary),
|
|
2092
3937
|
...file.summaryOmitted ? ["- Summary: (omitted: exceeds the part budget)"] : [],
|
|
2093
3938
|
"",
|
|
@@ -2298,8 +4143,8 @@ function generateProjectTree(filePaths) {
|
|
|
2298
4143
|
}
|
|
2299
4144
|
|
|
2300
4145
|
// src/scanner/binary.ts
|
|
2301
|
-
import * as
|
|
2302
|
-
import * as
|
|
4146
|
+
import * as fs7 from "fs";
|
|
4147
|
+
import * as path8 from "path";
|
|
2303
4148
|
var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
2304
4149
|
".png",
|
|
2305
4150
|
".jpg",
|
|
@@ -2339,7 +4184,7 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
2339
4184
|
".so"
|
|
2340
4185
|
]);
|
|
2341
4186
|
function isBinaryExtension(filePath) {
|
|
2342
|
-
const ext =
|
|
4187
|
+
const ext = path8.extname(filePath).toLowerCase();
|
|
2343
4188
|
return BINARY_EXTENSIONS.has(ext);
|
|
2344
4189
|
}
|
|
2345
4190
|
function isBinaryBuffer(buffer) {
|
|
@@ -2353,9 +4198,9 @@ function isBinaryFile(absolutePath) {
|
|
|
2353
4198
|
if (isBinaryExtension(absolutePath)) return true;
|
|
2354
4199
|
let fd;
|
|
2355
4200
|
try {
|
|
2356
|
-
fd =
|
|
4201
|
+
fd = fs7.openSync(absolutePath, "r");
|
|
2357
4202
|
const buffer = Buffer.alloc(8192);
|
|
2358
|
-
const bytesRead =
|
|
4203
|
+
const bytesRead = fs7.readSync(fd, buffer, 0, 8192, 0);
|
|
2359
4204
|
if (bytesRead === 0) return false;
|
|
2360
4205
|
return isBinaryBuffer(buffer.subarray(0, bytesRead));
|
|
2361
4206
|
} catch {
|
|
@@ -2363,7 +4208,7 @@ function isBinaryFile(absolutePath) {
|
|
|
2363
4208
|
} finally {
|
|
2364
4209
|
if (fd !== void 0) {
|
|
2365
4210
|
try {
|
|
2366
|
-
|
|
4211
|
+
fs7.closeSync(fd);
|
|
2367
4212
|
} catch {
|
|
2368
4213
|
}
|
|
2369
4214
|
}
|
|
@@ -2372,8 +4217,8 @@ function isBinaryFile(absolutePath) {
|
|
|
2372
4217
|
|
|
2373
4218
|
// src/validator/validate.ts
|
|
2374
4219
|
import * as crypto2 from "crypto";
|
|
2375
|
-
import * as
|
|
2376
|
-
import * as
|
|
4220
|
+
import * as fs8 from "fs";
|
|
4221
|
+
import * as path9 from "path";
|
|
2377
4222
|
function emptyDrift() {
|
|
2378
4223
|
return {
|
|
2379
4224
|
missingInSnapshot: [],
|
|
@@ -2390,8 +4235,8 @@ function sha256Hex(buffer) {
|
|
|
2390
4235
|
function validateSnapshot(configInput) {
|
|
2391
4236
|
assertUsableConfigInput(configInput);
|
|
2392
4237
|
const config = isResolvedConfig(configInput) ? normalizeResolvedConfig(configInput) : resolveConfig(configInput);
|
|
2393
|
-
const manifestPath =
|
|
2394
|
-
if (!
|
|
4238
|
+
const manifestPath = path9.resolve(config.cwd, config.outDir, "manifest.json");
|
|
4239
|
+
if (!fs8.existsSync(manifestPath)) {
|
|
2395
4240
|
return {
|
|
2396
4241
|
valid: false,
|
|
2397
4242
|
manifestPresent: false,
|
|
@@ -2402,7 +4247,7 @@ function validateSnapshot(configInput) {
|
|
|
2402
4247
|
}
|
|
2403
4248
|
let manifest;
|
|
2404
4249
|
try {
|
|
2405
|
-
manifest = JSON.parse(
|
|
4250
|
+
manifest = JSON.parse(fs8.readFileSync(manifestPath, "utf8"));
|
|
2406
4251
|
} catch (err) {
|
|
2407
4252
|
return {
|
|
2408
4253
|
valid: false,
|
|
@@ -2425,7 +4270,7 @@ function validateSnapshot(configInput) {
|
|
|
2425
4270
|
}
|
|
2426
4271
|
const drift = emptyDrift();
|
|
2427
4272
|
const warnings = [];
|
|
2428
|
-
const snapshotDir =
|
|
4273
|
+
const snapshotDir = path9.resolve(config.cwd, config.outDir);
|
|
2429
4274
|
const manifestFileMap = /* @__PURE__ */ new Map();
|
|
2430
4275
|
for (const file of manifest.files) {
|
|
2431
4276
|
if (file && typeof file.relativePath === "string") {
|
|
@@ -2437,7 +4282,7 @@ function validateSnapshot(configInput) {
|
|
|
2437
4282
|
);
|
|
2438
4283
|
const collection = collectFiles(config.cwd, {
|
|
2439
4284
|
includePatterns: config.include,
|
|
2440
|
-
excludePatterns: config.excludePatterns,
|
|
4285
|
+
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
2441
4286
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
2442
4287
|
});
|
|
2443
4288
|
for (const diskFile of collection.files) {
|
|
@@ -2449,7 +4294,7 @@ function validateSnapshot(configInput) {
|
|
|
2449
4294
|
}
|
|
2450
4295
|
let diskHash;
|
|
2451
4296
|
try {
|
|
2452
|
-
diskHash = sha256Hex(
|
|
4297
|
+
diskHash = sha256Hex(fs8.readFileSync(path9.join(config.cwd, diskFile)));
|
|
2453
4298
|
} catch {
|
|
2454
4299
|
drift.missingOnDisk.push(diskFile);
|
|
2455
4300
|
continue;
|
|
@@ -2459,10 +4304,11 @@ function validateSnapshot(configInput) {
|
|
|
2459
4304
|
}
|
|
2460
4305
|
}
|
|
2461
4306
|
for (const snapshotFile of manifestFileMap.keys()) {
|
|
2462
|
-
if (!
|
|
4307
|
+
if (!fs8.existsSync(path9.join(config.cwd, snapshotFile))) {
|
|
2463
4308
|
drift.missingOnDisk.push(snapshotFile);
|
|
2464
4309
|
}
|
|
2465
4310
|
}
|
|
4311
|
+
drift.missingOnDisk = [...new Set(drift.missingOnDisk)];
|
|
2466
4312
|
const maxEffectiveBudget = Math.max(
|
|
2467
4313
|
config.maxPartBytes,
|
|
2468
4314
|
config.budgets.default,
|
|
@@ -2473,10 +4319,10 @@ function validateSnapshot(configInput) {
|
|
|
2473
4319
|
if (!part || typeof part.filename !== "string") continue;
|
|
2474
4320
|
partNames.add(part.filename);
|
|
2475
4321
|
if (!config.artifacts.parts) continue;
|
|
2476
|
-
const partPath =
|
|
4322
|
+
const partPath = path9.join(snapshotDir, "parts", part.filename);
|
|
2477
4323
|
let buffer;
|
|
2478
4324
|
try {
|
|
2479
|
-
buffer =
|
|
4325
|
+
buffer = fs8.readFileSync(partPath);
|
|
2480
4326
|
} catch {
|
|
2481
4327
|
drift.partIssues.push({ part: part.filename, issue: "missing from snapshot/parts" });
|
|
2482
4328
|
continue;
|
|
@@ -2563,6 +4409,11 @@ function validateSnapshot(configInput) {
|
|
|
2563
4409
|
}
|
|
2564
4410
|
}
|
|
2565
4411
|
}
|
|
4412
|
+
if (recorded?.preset !== void 0 && recorded.preset !== config.preset) {
|
|
4413
|
+
drift.configMismatches.push(
|
|
4414
|
+
`preset: manifest ${recorded.preset} vs current ${config.preset}`
|
|
4415
|
+
);
|
|
4416
|
+
}
|
|
2566
4417
|
if (recorded?.layersMode !== void 0 && recorded.layersMode !== config.layersMode) {
|
|
2567
4418
|
drift.configMismatches.push(
|
|
2568
4419
|
`layersMode: manifest ${recorded.layersMode} vs current ${config.layersMode}`
|
|
@@ -2580,10 +4431,10 @@ function validateSnapshot(configInput) {
|
|
|
2580
4431
|
...Object.keys(recordedByLayer),
|
|
2581
4432
|
...Object.keys(config.budgets.byLayer)
|
|
2582
4433
|
]);
|
|
2583
|
-
for (const
|
|
2584
|
-
if (recordedByLayer[
|
|
4434
|
+
for (const layer2 of Array.from(layerNames).sort()) {
|
|
4435
|
+
if (recordedByLayer[layer2] !== config.budgets.byLayer[layer2]) {
|
|
2585
4436
|
drift.configMismatches.push(
|
|
2586
|
-
`budgets.byLayer.${
|
|
4437
|
+
`budgets.byLayer.${layer2}: manifest ${recordedByLayer[layer2]} vs current ${config.budgets.byLayer[layer2]}`
|
|
2587
4438
|
);
|
|
2588
4439
|
}
|
|
2589
4440
|
}
|
|
@@ -2666,9 +4517,9 @@ function validateSnapshot(configInput) {
|
|
|
2666
4517
|
["full-codebase.md", manifest.artifacts?.fullMarkdown]
|
|
2667
4518
|
]) {
|
|
2668
4519
|
if (!artifact) continue;
|
|
2669
|
-
const artifactPath =
|
|
4520
|
+
const artifactPath = path9.join(snapshotDir, key);
|
|
2670
4521
|
try {
|
|
2671
|
-
const buffer =
|
|
4522
|
+
const buffer = fs8.readFileSync(artifactPath);
|
|
2672
4523
|
if (sha256Hex(buffer) !== artifact.sha256 || buffer.length !== artifact.byteSize) {
|
|
2673
4524
|
drift.partIssues.push({ part: key, issue: "artifact does not match its recorded digest" });
|
|
2674
4525
|
}
|
|
@@ -2717,12 +4568,12 @@ function validateSnapshot(configInput) {
|
|
|
2717
4568
|
|
|
2718
4569
|
// src/index.ts
|
|
2719
4570
|
var GENERATOR_NAME = "@ankurhalder/codebase-snapshot";
|
|
2720
|
-
var GENERATOR_VERSION = "2.
|
|
4571
|
+
var GENERATOR_VERSION = "2.1.1";
|
|
2721
4572
|
var SCHEMA_VERSION = "3.0.0";
|
|
2722
4573
|
function sha256Hex2(input) {
|
|
2723
4574
|
return crypto3.createHash("sha256").update(input).digest("hex");
|
|
2724
4575
|
}
|
|
2725
|
-
var
|
|
4576
|
+
var TS_EXTENSIONS = [
|
|
2726
4577
|
".ts",
|
|
2727
4578
|
".mts",
|
|
2728
4579
|
".cts",
|
|
@@ -2730,24 +4581,21 @@ var SUMMARY_EXTENSIONS = [
|
|
|
2730
4581
|
".js",
|
|
2731
4582
|
".mjs",
|
|
2732
4583
|
".cjs",
|
|
2733
|
-
".jsx"
|
|
2734
|
-
".json",
|
|
2735
|
-
".jsonc",
|
|
2736
|
-
".md",
|
|
2737
|
-
".mdx"
|
|
4584
|
+
".jsx"
|
|
2738
4585
|
];
|
|
2739
4586
|
function summarizeFile(relativePath, redactedContent) {
|
|
2740
4587
|
const lower = relativePath.toLowerCase();
|
|
2741
|
-
if (!SUMMARY_EXTENSIONS.some((extension) => lower.endsWith(extension))) {
|
|
2742
|
-
return void 0;
|
|
2743
|
-
}
|
|
2744
4588
|
if (lower.endsWith(".json") || lower.endsWith(".jsonc")) {
|
|
2745
|
-
return summarizeJson(redactedContent);
|
|
4589
|
+
return { summary: summarizeJson(redactedContent), quality: "structural" };
|
|
2746
4590
|
}
|
|
2747
4591
|
if (lower.endsWith(".md") || lower.endsWith(".mdx")) {
|
|
2748
|
-
return summarizeMarkdown(redactedContent);
|
|
4592
|
+
return { summary: summarizeMarkdown(redactedContent), quality: "structural" };
|
|
2749
4593
|
}
|
|
2750
|
-
|
|
4594
|
+
if (TS_EXTENSIONS.some((extension) => lower.endsWith(extension))) {
|
|
4595
|
+
return { summary: summarizeTypeScriptSource(relativePath, redactedContent), quality: "ast" };
|
|
4596
|
+
}
|
|
4597
|
+
const profiled = summarizeWithProfile(relativePath, redactedContent);
|
|
4598
|
+
return profiled ? { summary: profiled.summary, quality: profiled.quality } : void 0;
|
|
2751
4599
|
}
|
|
2752
4600
|
function isSourceSummary(summary) {
|
|
2753
4601
|
return Boolean(summary && typeof summary === "object" && "imports" in summary);
|
|
@@ -2773,8 +4621,8 @@ var IMPORT_CANDIDATE_SUFFIXES = [
|
|
|
2773
4621
|
];
|
|
2774
4622
|
function resolveSpecifier(fromFile, specifier, knownFiles) {
|
|
2775
4623
|
if (!RELATIVE_SPECIFIER.test(specifier)) return null;
|
|
2776
|
-
const baseDir =
|
|
2777
|
-
const joined =
|
|
4624
|
+
const baseDir = path10.posix.dirname(fromFile);
|
|
4625
|
+
const joined = path10.posix.normalize(path10.posix.join(baseDir, specifier));
|
|
2778
4626
|
const candidate = joined.replace(/\.(?:js|mjs|cjs|jsx)$/, "");
|
|
2779
4627
|
for (const suffix of IMPORT_CANDIDATE_SUFFIXES) {
|
|
2780
4628
|
const withSuffix = `${joined}${suffix}`;
|
|
@@ -2866,11 +4714,11 @@ function verifyInMemory(files, parts, maxPartBytes) {
|
|
|
2866
4714
|
return errors;
|
|
2867
4715
|
}
|
|
2868
4716
|
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}${
|
|
4717
|
+
const cwd = path10.resolve(config.cwd);
|
|
4718
|
+
const outDir = path10.resolve(cwd, config.outDir);
|
|
4719
|
+
const tmpDir = path10.resolve(cwd, config.tmpDir);
|
|
4720
|
+
const root = path10.parse(cwd).root;
|
|
4721
|
+
const contains = (parent, child) => child === parent || child.startsWith(`${parent}${path10.sep}`);
|
|
2874
4722
|
const reject = (reason) => {
|
|
2875
4723
|
throw new Error(
|
|
2876
4724
|
`Refusing to generate: ${reason}. outDir=${outDir} tmpDir=${tmpDir} cwd=${cwd}`
|
|
@@ -2889,26 +4737,66 @@ function resolveSafeOutputDirs(config) {
|
|
|
2889
4737
|
if (contains(tmpDir, outDir)) reject("outDir is inside tmpDir");
|
|
2890
4738
|
return { outDir, tmpDir };
|
|
2891
4739
|
}
|
|
4740
|
+
var PLACEHOLDER_ENTRIES = /* @__PURE__ */ new Set([".gitkeep", ".gitignore", ".DS_Store"]);
|
|
4741
|
+
var OWNERSHIP_MARKER = ".codebase-snapshot";
|
|
4742
|
+
function writeOwnershipMarker(dir) {
|
|
4743
|
+
const marker2 = {
|
|
4744
|
+
generator: GENERATOR_NAME,
|
|
4745
|
+
version: GENERATOR_VERSION,
|
|
4746
|
+
schemaVersion: SCHEMA_VERSION
|
|
4747
|
+
};
|
|
4748
|
+
fs9.writeFileSync(path10.join(dir, OWNERSHIP_MARKER), `${JSON.stringify(marker2, null, 2)}
|
|
4749
|
+
`, "utf8");
|
|
4750
|
+
}
|
|
4751
|
+
function isOwnOutput(dir) {
|
|
4752
|
+
if (fs9.existsSync(path10.join(dir, OWNERSHIP_MARKER))) return true;
|
|
4753
|
+
const manifestPath = path10.join(dir, "manifest.json");
|
|
4754
|
+
if (!fs9.existsSync(manifestPath)) return false;
|
|
4755
|
+
try {
|
|
4756
|
+
const parsed = JSON.parse(fs9.readFileSync(manifestPath, "utf8"));
|
|
4757
|
+
const generator = parsed.generator;
|
|
4758
|
+
return typeof parsed.schemaVersion === "string" && Array.isArray(parsed.files) && typeof generator === "object" && generator !== null && generator.name === GENERATOR_NAME;
|
|
4759
|
+
} catch {
|
|
4760
|
+
return false;
|
|
4761
|
+
}
|
|
4762
|
+
}
|
|
4763
|
+
function assertReplaceable(target, label) {
|
|
4764
|
+
if (!fs9.existsSync(target)) return;
|
|
4765
|
+
if (!fs9.statSync(target).isDirectory()) {
|
|
4766
|
+
throw new Error(
|
|
4767
|
+
`Refusing to write into ${label} ${target}: it exists but is not a directory.`
|
|
4768
|
+
);
|
|
4769
|
+
}
|
|
4770
|
+
const entries = fs9.readdirSync(target);
|
|
4771
|
+
const meaningful = entries.filter((name) => !PLACEHOLDER_ENTRIES.has(name));
|
|
4772
|
+
if (meaningful.length === 0) return;
|
|
4773
|
+
if (isOwnOutput(target)) return;
|
|
4774
|
+
const preview = meaningful.slice(0, 5).join(", ");
|
|
4775
|
+
const more = meaningful.length > 5 ? `, and ${meaningful.length - 5} more` : "";
|
|
4776
|
+
throw new Error(
|
|
4777
|
+
`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.`
|
|
4778
|
+
);
|
|
4779
|
+
}
|
|
2892
4780
|
function swapIntoPlace(tmpDir, outDir) {
|
|
2893
4781
|
const backupDir = `${outDir}.backup-${process.pid}-${Date.now()}`;
|
|
2894
|
-
const hadPrevious =
|
|
2895
|
-
if (hadPrevious)
|
|
4782
|
+
const hadPrevious = fs9.existsSync(outDir);
|
|
4783
|
+
if (hadPrevious) fs9.renameSync(outDir, backupDir);
|
|
2896
4784
|
try {
|
|
2897
4785
|
try {
|
|
2898
|
-
|
|
4786
|
+
fs9.renameSync(tmpDir, outDir);
|
|
2899
4787
|
} catch (err) {
|
|
2900
4788
|
if (err.code === "EXDEV") {
|
|
2901
|
-
|
|
2902
|
-
|
|
4789
|
+
fs9.cpSync(tmpDir, outDir, { recursive: true });
|
|
4790
|
+
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
2903
4791
|
} else {
|
|
2904
4792
|
throw err;
|
|
2905
4793
|
}
|
|
2906
4794
|
}
|
|
2907
|
-
if (hadPrevious)
|
|
4795
|
+
if (hadPrevious) fs9.rmSync(backupDir, { recursive: true, force: true });
|
|
2908
4796
|
} catch (err) {
|
|
2909
|
-
if (hadPrevious &&
|
|
2910
|
-
if (
|
|
2911
|
-
|
|
4797
|
+
if (hadPrevious && fs9.existsSync(backupDir)) {
|
|
4798
|
+
if (fs9.existsSync(outDir)) fs9.rmSync(outDir, { recursive: true, force: true });
|
|
4799
|
+
fs9.renameSync(backupDir, outDir);
|
|
2912
4800
|
}
|
|
2913
4801
|
throw err;
|
|
2914
4802
|
}
|
|
@@ -2924,7 +4812,9 @@ function artifactWarnings(artifacts) {
|
|
|
2924
4812
|
);
|
|
2925
4813
|
}
|
|
2926
4814
|
if (ARTIFACT_NAMES.every((name) => artifacts[name] === false)) {
|
|
2927
|
-
messages.push(
|
|
4815
|
+
messages.push(
|
|
4816
|
+
"every artifact is disabled: only the .codebase-snapshot ownership marker will be written."
|
|
4817
|
+
);
|
|
2928
4818
|
}
|
|
2929
4819
|
return messages;
|
|
2930
4820
|
}
|
|
@@ -2972,7 +4862,7 @@ function generateSnapshot(configInput, options) {
|
|
|
2972
4862
|
const gitInfo = getGitInfo(config.cwd, config.branch);
|
|
2973
4863
|
const collection = collectFiles(config.cwd, {
|
|
2974
4864
|
includePatterns: config.include,
|
|
2975
|
-
excludePatterns: config.excludePatterns,
|
|
4865
|
+
excludePatterns: [...config.excludePatterns, ...config.presetExclude],
|
|
2976
4866
|
includeSensitiveFiles: config.includeSensitiveFiles
|
|
2977
4867
|
});
|
|
2978
4868
|
const files = [];
|
|
@@ -2985,10 +4875,10 @@ function generateSnapshot(configInput, options) {
|
|
|
2985
4875
|
let totalSizeBytes = 0;
|
|
2986
4876
|
let largeFileCount = 0;
|
|
2987
4877
|
for (const relativePath of collection.files) {
|
|
2988
|
-
const absolutePath =
|
|
4878
|
+
const absolutePath = path10.join(config.cwd, relativePath);
|
|
2989
4879
|
let stats;
|
|
2990
4880
|
try {
|
|
2991
|
-
stats =
|
|
4881
|
+
stats = fs9.statSync(absolutePath);
|
|
2992
4882
|
if (!stats.isFile()) {
|
|
2993
4883
|
skipped.push({ path: relativePath, reason: "not-file" });
|
|
2994
4884
|
continue;
|
|
@@ -3003,7 +4893,7 @@ function generateSnapshot(configInput, options) {
|
|
|
3003
4893
|
}
|
|
3004
4894
|
let buffer;
|
|
3005
4895
|
try {
|
|
3006
|
-
buffer =
|
|
4896
|
+
buffer = fs9.readFileSync(absolutePath);
|
|
3007
4897
|
} catch {
|
|
3008
4898
|
skipped.push({ path: relativePath, reason: "read-error" });
|
|
3009
4899
|
continue;
|
|
@@ -3013,10 +4903,10 @@ function generateSnapshot(configInput, options) {
|
|
|
3013
4903
|
const lineCount = countLines(rawContent);
|
|
3014
4904
|
totalLines += lineCount;
|
|
3015
4905
|
totalSizeBytes += stats.size;
|
|
3016
|
-
const
|
|
4906
|
+
const layer2 = classifyLayer(relativePath, config.layers);
|
|
3017
4907
|
const kind = classifyKind(relativePath, config.kinds);
|
|
3018
4908
|
const language = detectLanguage(relativePath);
|
|
3019
|
-
bump(layerCounts,
|
|
4909
|
+
bump(layerCounts, layer2);
|
|
3020
4910
|
bump(kindCounts, kind);
|
|
3021
4911
|
const { redacted, findings } = scanAndRedactSecrets(
|
|
3022
4912
|
rawContent,
|
|
@@ -3040,8 +4930,11 @@ function generateSnapshot(configInput, options) {
|
|
|
3040
4930
|
includedLines = preview.includedLines;
|
|
3041
4931
|
}
|
|
3042
4932
|
let summary;
|
|
4933
|
+
let summaryQuality;
|
|
3043
4934
|
try {
|
|
3044
|
-
|
|
4935
|
+
const outcome = summarizeFile(relativePath, redacted);
|
|
4936
|
+
summary = outcome?.summary;
|
|
4937
|
+
summaryQuality = outcome?.quality;
|
|
3045
4938
|
if (isSourceSummary(summary) && summary.parseErrors) {
|
|
3046
4939
|
warnings.push(
|
|
3047
4940
|
`Parse errors in ${relativePath} (${summary.parseErrors}); its summary is incomplete.`
|
|
@@ -3058,7 +4951,7 @@ function generateSnapshot(configInput, options) {
|
|
|
3058
4951
|
lines: lineCount,
|
|
3059
4952
|
hashSha256,
|
|
3060
4953
|
part: "",
|
|
3061
|
-
layer,
|
|
4954
|
+
layer: layer2,
|
|
3062
4955
|
kind,
|
|
3063
4956
|
language,
|
|
3064
4957
|
inclusionMode: isPreviewTruncated ? "preview" : "full",
|
|
@@ -3068,6 +4961,7 @@ function generateSnapshot(configInput, options) {
|
|
|
3068
4961
|
includedLines,
|
|
3069
4962
|
includedBytes: Buffer.byteLength(finalContent, "utf8"),
|
|
3070
4963
|
summary,
|
|
4964
|
+
summaryQuality,
|
|
3071
4965
|
content: finalContent
|
|
3072
4966
|
});
|
|
3073
4967
|
}
|
|
@@ -3108,10 +5002,11 @@ function generateSnapshot(configInput, options) {
|
|
|
3108
5002
|
includeSensitiveFiles: config.includeSensitiveFiles,
|
|
3109
5003
|
artifacts: { ...config.artifacts },
|
|
3110
5004
|
include: [...config.include],
|
|
5005
|
+
preset: config.preset,
|
|
3111
5006
|
layersMode: config.layersMode,
|
|
3112
5007
|
budgets: { default: config.budgets.default, byLayer: { ...config.budgets.byLayer } },
|
|
3113
5008
|
excludePatterns: [...config.excludePatterns],
|
|
3114
|
-
layers: config.layers.map((
|
|
5009
|
+
layers: config.layers.map((layer2) => layer2.name),
|
|
3115
5010
|
kinds: config.kinds.map((kind) => kind.kind)
|
|
3116
5011
|
},
|
|
3117
5012
|
counts: {
|
|
@@ -3198,45 +5093,48 @@ function generateSnapshot(configInput, options) {
|
|
|
3198
5093
|
return manifest;
|
|
3199
5094
|
}
|
|
3200
5095
|
const { outDir, tmpDir } = resolveSafeOutputDirs(config);
|
|
5096
|
+
assertReplaceable(tmpDir, "tmpDir");
|
|
5097
|
+
assertReplaceable(outDir, "outDir");
|
|
3201
5098
|
const { artifacts } = config;
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
const
|
|
3206
|
-
|
|
5099
|
+
fs9.rmSync(tmpDir, { recursive: true, force: true });
|
|
5100
|
+
fs9.mkdirSync(tmpDir, { recursive: true });
|
|
5101
|
+
writeOwnershipMarker(tmpDir);
|
|
5102
|
+
const partsDir = path10.join(tmpDir, "parts");
|
|
5103
|
+
const reportsDir = path10.join(tmpDir, "reports");
|
|
5104
|
+
if (artifacts.parts) fs9.mkdirSync(partsDir, { recursive: true });
|
|
3207
5105
|
if (artifacts.navigation || artifacts.projectTree || artifacts.audit) {
|
|
3208
|
-
|
|
5106
|
+
fs9.mkdirSync(reportsDir, { recursive: true });
|
|
3209
5107
|
}
|
|
3210
5108
|
if (artifacts.parts) {
|
|
3211
5109
|
for (const part of parts) {
|
|
3212
|
-
|
|
5110
|
+
fs9.writeFileSync(path10.join(partsDir, part.filename), part.markdown, "utf8");
|
|
3213
5111
|
}
|
|
3214
5112
|
}
|
|
3215
5113
|
if (artifacts.projectTree) {
|
|
3216
5114
|
const projectTree = generateProjectTree(files.map((file) => file.relativePath));
|
|
3217
|
-
|
|
5115
|
+
fs9.writeFileSync(path10.join(reportsDir, "project-tree.md"), projectTree, "utf8");
|
|
3218
5116
|
}
|
|
3219
5117
|
if (artifacts.navigation) {
|
|
3220
|
-
|
|
5118
|
+
fs9.writeFileSync(path10.join(reportsDir, "navigation.md"), generateNavigation(manifest), "utf8");
|
|
3221
5119
|
}
|
|
3222
5120
|
if (artifacts.audit) {
|
|
3223
|
-
|
|
3224
|
-
|
|
5121
|
+
fs9.writeFileSync(
|
|
5122
|
+
path10.join(reportsDir, "audit.md"),
|
|
3225
5123
|
generateAuditReport(manifest, secretFindings),
|
|
3226
5124
|
"utf8"
|
|
3227
5125
|
);
|
|
3228
5126
|
}
|
|
3229
5127
|
if (artifacts.readme) {
|
|
3230
|
-
|
|
5128
|
+
fs9.writeFileSync(path10.join(tmpDir, "README.md"), generateReadme(manifest), "utf8");
|
|
3231
5129
|
}
|
|
3232
5130
|
if (artifacts.manifest) {
|
|
3233
|
-
|
|
5131
|
+
fs9.writeFileSync(path10.join(tmpDir, "manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
3234
5132
|
}
|
|
3235
5133
|
if (monolithJson !== null) {
|
|
3236
|
-
|
|
5134
|
+
fs9.writeFileSync(path10.join(tmpDir, "full-codebase.json"), monolithJson, "utf8");
|
|
3237
5135
|
}
|
|
3238
5136
|
if (monolithMarkdown !== null) {
|
|
3239
|
-
|
|
5137
|
+
fs9.writeFileSync(path10.join(tmpDir, "full-codebase.md"), monolithMarkdown, "utf8");
|
|
3240
5138
|
}
|
|
3241
5139
|
swapIntoPlace(tmpDir, outDir);
|
|
3242
5140
|
runAfterGenerate(config, manifest);
|
|
@@ -3276,6 +5174,12 @@ var CLI_OPTIONS = {
|
|
|
3276
5174
|
mode: { type: "string" },
|
|
3277
5175
|
"dry-run": { type: "boolean" },
|
|
3278
5176
|
watch: { type: "boolean" },
|
|
5177
|
+
preset: { type: "string" },
|
|
5178
|
+
include: { type: "string" },
|
|
5179
|
+
exclude: { type: "string" },
|
|
5180
|
+
yes: { type: "boolean" },
|
|
5181
|
+
minimal: { type: "boolean" },
|
|
5182
|
+
force: { type: "boolean" },
|
|
3279
5183
|
help: { type: "boolean", short: "h" },
|
|
3280
5184
|
version: { type: "boolean", short: "v" }
|
|
3281
5185
|
};
|
|
@@ -3313,6 +5217,14 @@ Options:
|
|
|
3313
5217
|
--format <text|json> CLI output format (default: text)
|
|
3314
5218
|
--dry-run Compute the snapshot without writing anything
|
|
3315
5219
|
--watch Re-generate whenever the repository changes
|
|
5220
|
+
|
|
5221
|
+
init only:
|
|
5222
|
+
--preset <name> Force a project preset instead of detecting one
|
|
5223
|
+
--include <globs> Comma-separated allow-list
|
|
5224
|
+
--exclude <globs> Comma-separated exclude patterns
|
|
5225
|
+
--minimal Write only the settings that differ from defaults
|
|
5226
|
+
--yes Accept every default without prompting
|
|
5227
|
+
--force Overwrite an existing config
|
|
3316
5228
|
-h, --help Display this help message
|
|
3317
5229
|
-v, --version Display version
|
|
3318
5230
|
|
|
@@ -3357,9 +5269,32 @@ function parseCliArgs(argv) {
|
|
|
3357
5269
|
strict: true
|
|
3358
5270
|
});
|
|
3359
5271
|
} catch (err) {
|
|
3360
|
-
|
|
5272
|
+
const message = err.message;
|
|
5273
|
+
const ambiguous = /^Option '(--[^']+)' argument is ambiguous/i.exec(message);
|
|
5274
|
+
if (ambiguous) {
|
|
5275
|
+
throw new UsageError(
|
|
5276
|
+
`${ambiguous[1]} received a value that looks like another flag (for example a negative number). Use the "--option=value" form, and note that sizes must be positive.`
|
|
5277
|
+
);
|
|
5278
|
+
}
|
|
5279
|
+
throw new UsageError(message);
|
|
3361
5280
|
}
|
|
3362
5281
|
const values = parsed.values;
|
|
5282
|
+
for (const [name, spec] of Object.entries(CLI_OPTIONS)) {
|
|
5283
|
+
if (spec.type !== "string") continue;
|
|
5284
|
+
const raw = values[name];
|
|
5285
|
+
if (raw !== void 0 && String(raw).trim() === "") {
|
|
5286
|
+
throw new UsageError(`--${name} requires a value (received an empty string).`);
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
if (values.cwd !== void 0) {
|
|
5290
|
+
const target = String(values.cwd);
|
|
5291
|
+
if (!fs10.existsSync(target)) {
|
|
5292
|
+
throw new UsageError(`--cwd directory does not exist: ${target}`);
|
|
5293
|
+
}
|
|
5294
|
+
if (!fs10.statSync(target).isDirectory()) {
|
|
5295
|
+
throw new UsageError(`--cwd is not a directory: ${target}`);
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
3363
5298
|
if (values.help) {
|
|
3364
5299
|
process.stdout.write(`${USAGE}
|
|
3365
5300
|
`);
|
|
@@ -3425,13 +5360,38 @@ function parseCliArgs(argv) {
|
|
|
3425
5360
|
} else if (rawSkip !== void 0) {
|
|
3426
5361
|
skipArtifacts = parseArtifactList(String(rawSkip), "--skip-artifacts");
|
|
3427
5362
|
}
|
|
5363
|
+
if (command !== "init") {
|
|
5364
|
+
const initOnly = [];
|
|
5365
|
+
if (values.preset !== void 0) initOnly.push("--preset");
|
|
5366
|
+
if (values.include !== void 0) initOnly.push("--include");
|
|
5367
|
+
if (values.exclude !== void 0) initOnly.push("--exclude");
|
|
5368
|
+
if (values.yes === true) initOnly.push("--yes");
|
|
5369
|
+
if (values.minimal === true) initOnly.push("--minimal");
|
|
5370
|
+
if (values.force === true) initOnly.push("--force");
|
|
5371
|
+
if (initOnly.length > 0) {
|
|
5372
|
+
throw new UsageError(
|
|
5373
|
+
`${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).`
|
|
5374
|
+
);
|
|
5375
|
+
}
|
|
5376
|
+
}
|
|
5377
|
+
if (command === "init" && values.preset !== void 0) {
|
|
5378
|
+
const requested = String(values.preset);
|
|
5379
|
+
if (!getPreset(requested)) throw new UsageError(unknownPresetMessage(requested));
|
|
5380
|
+
}
|
|
3428
5381
|
return {
|
|
3429
5382
|
command,
|
|
3430
5383
|
args,
|
|
3431
5384
|
format: rawFormat,
|
|
3432
5385
|
dryRun: values["dry-run"] === true,
|
|
3433
5386
|
watch: values.watch === true,
|
|
3434
|
-
|
|
5387
|
+
preset: values.preset === void 0 ? void 0 : String(values.preset),
|
|
5388
|
+
maxPartBytesRaw: values["max-part-bytes"] === void 0 ? void 0 : String(values["max-part-bytes"]),
|
|
5389
|
+
include: values.include === void 0 ? void 0 : String(values.include),
|
|
5390
|
+
exclude: values.exclude === void 0 ? void 0 : String(values.exclude),
|
|
5391
|
+
yes: values.yes === true,
|
|
5392
|
+
minimal: values.minimal === true,
|
|
5393
|
+
force: values.force === true,
|
|
5394
|
+
cwd: values.cwd === void 0 ? process.cwd() : path11.resolve(String(values.cwd)),
|
|
3435
5395
|
outDir: values["out-dir"] === void 0 ? void 0 : String(values["out-dir"]),
|
|
3436
5396
|
includeMonolith: values["include-monolith"] === true ? true : void 0,
|
|
3437
5397
|
includeSensitiveFiles: values["include-sensitive-files"] === true ? true : void 0,
|
|
@@ -3471,32 +5431,65 @@ function emit(format, payload, textRenderer) {
|
|
|
3471
5431
|
}
|
|
3472
5432
|
textRenderer();
|
|
3473
5433
|
}
|
|
3474
|
-
function
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
5434
|
+
function splitList(value) {
|
|
5435
|
+
if (value === void 0) return void 0;
|
|
5436
|
+
return value.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
5437
|
+
}
|
|
5438
|
+
function describeManifestShapeProblem(manifest) {
|
|
5439
|
+
if (!manifest || typeof manifest !== "object" || Array.isArray(manifest)) {
|
|
5440
|
+
return "not a JSON object";
|
|
5441
|
+
}
|
|
5442
|
+
const candidate = manifest;
|
|
5443
|
+
const counts = candidate.counts;
|
|
5444
|
+
if (!counts || typeof counts !== "object") return "missing `counts`";
|
|
5445
|
+
for (const key of ["totalFiles", "totalLines", "totalSizeBytes", "partsCount"]) {
|
|
5446
|
+
if (typeof counts[key] !== "number") return `missing numeric \`counts.${key}\``;
|
|
5447
|
+
}
|
|
5448
|
+
if (!candidate.layers || typeof candidate.layers !== "object") return "missing `layers`";
|
|
5449
|
+
if (!Array.isArray(candidate.files)) return "missing `files` array";
|
|
5450
|
+
return null;
|
|
5451
|
+
}
|
|
5452
|
+
async function runInitCommand(invocation) {
|
|
5453
|
+
try {
|
|
5454
|
+
const result = await runInit({
|
|
5455
|
+
cwd: invocation.cwd,
|
|
5456
|
+
preset: invocation.preset,
|
|
5457
|
+
maxPartBytes: invocation.maxPartBytesRaw ?? invocation.maxPartBytes,
|
|
5458
|
+
include: splitList(invocation.include),
|
|
5459
|
+
exclude: splitList(invocation.exclude),
|
|
5460
|
+
skipArtifacts: invocation.skipArtifacts,
|
|
5461
|
+
includeSensitiveFiles: invocation.includeSensitiveFiles,
|
|
5462
|
+
yes: invocation.yes,
|
|
5463
|
+
minimal: invocation.minimal,
|
|
5464
|
+
force: invocation.force,
|
|
5465
|
+
dryRun: invocation.dryRun
|
|
5466
|
+
});
|
|
5467
|
+
if (invocation.format === "json") {
|
|
5468
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
5469
|
+
`);
|
|
5470
|
+
return 0;
|
|
5471
|
+
}
|
|
5472
|
+
if (!result.written) {
|
|
5473
|
+
info(result.configText);
|
|
5474
|
+
return 0;
|
|
5475
|
+
}
|
|
5476
|
+
info(`\u2705 Created ${path11.basename(result.configPath)} (preset: ${result.preset})`);
|
|
5477
|
+
if (result.scripts.length > 0) info(` Added npm scripts: ${result.scripts.join(", ")}`);
|
|
5478
|
+
if (result.gitignoreEntries.length > 0) {
|
|
5479
|
+
info(` Added to .gitignore: ${result.gitignoreEntries.join(", ")}`);
|
|
5480
|
+
}
|
|
5481
|
+
for (const note of result.notes) info(` \u26A0 ${note}`);
|
|
5482
|
+
info("");
|
|
5483
|
+
info(" Next: npm run snapshot");
|
|
5484
|
+
return 0;
|
|
5485
|
+
} catch (err) {
|
|
5486
|
+
if (err instanceof PromptCancelled) {
|
|
5487
|
+
warn("\u274C Cancelled.");
|
|
5488
|
+
return 1;
|
|
5489
|
+
}
|
|
5490
|
+
warn(`\u274C ${err.message}`);
|
|
3479
5491
|
return 1;
|
|
3480
5492
|
}
|
|
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
5493
|
}
|
|
3501
5494
|
function renderValidateText(result) {
|
|
3502
5495
|
if (result.valid) {
|
|
@@ -3519,6 +5512,9 @@ function renderValidateText(result) {
|
|
|
3519
5512
|
for (const missing of result.drift.missingOnDisk.slice(0, 20)) {
|
|
3520
5513
|
warn(` - deleted: ${missing}`);
|
|
3521
5514
|
}
|
|
5515
|
+
for (const mismatch of result.drift.totalsMismatches.slice(0, 20)) {
|
|
5516
|
+
warn(` - counts: ${mismatch}`);
|
|
5517
|
+
}
|
|
3522
5518
|
}
|
|
3523
5519
|
for (const warning of result.warnings) warn(` \u26A0 ${warning}`);
|
|
3524
5520
|
}
|
|
@@ -3538,11 +5534,15 @@ ${USAGE}
|
|
|
3538
5534
|
throw err;
|
|
3539
5535
|
}
|
|
3540
5536
|
if (invocation.command === "init") {
|
|
3541
|
-
return
|
|
5537
|
+
return await runInitCommand(invocation);
|
|
3542
5538
|
}
|
|
3543
5539
|
const loaded = await loadConfigFileDetailed(invocation.cwd);
|
|
3544
5540
|
for (const warning of loaded.warnings) warn(`\u26A0 Config: ${warning}`);
|
|
3545
|
-
if (loaded.
|
|
5541
|
+
if (loaded.errors.length > 0) {
|
|
5542
|
+
warn("\u274C The config file could not be read. Fix it, or delete it to use the defaults.");
|
|
5543
|
+
return 1;
|
|
5544
|
+
}
|
|
5545
|
+
if (loaded.file) info(`\u2699 Using config ${path11.relative(process.cwd(), loaded.file) || loaded.file}`);
|
|
3546
5546
|
const resolved = resolveConfig(
|
|
3547
5547
|
loaded.config,
|
|
3548
5548
|
{
|
|
@@ -3566,18 +5566,23 @@ ${USAGE}
|
|
|
3566
5566
|
return result.valid ? 0 : 1;
|
|
3567
5567
|
}
|
|
3568
5568
|
if (invocation.command === "report") {
|
|
3569
|
-
const manifestPath =
|
|
3570
|
-
if (!
|
|
5569
|
+
const manifestPath = path11.resolve(resolved.cwd, resolved.outDir, "manifest.json");
|
|
5570
|
+
if (!fs10.existsSync(manifestPath)) {
|
|
3571
5571
|
warn(`\u274C Manifest not found at ${manifestPath}. Run generate first.`);
|
|
3572
5572
|
return 1;
|
|
3573
5573
|
}
|
|
3574
5574
|
let manifest;
|
|
3575
5575
|
try {
|
|
3576
|
-
manifest = JSON.parse(
|
|
5576
|
+
manifest = JSON.parse(fs10.readFileSync(manifestPath, "utf8"));
|
|
3577
5577
|
} catch (err) {
|
|
3578
5578
|
warn(`\u274C Could not read ${manifestPath}: ${err.message}`);
|
|
3579
5579
|
return 1;
|
|
3580
5580
|
}
|
|
5581
|
+
const shapeError = describeManifestShapeProblem(manifest);
|
|
5582
|
+
if (shapeError) {
|
|
5583
|
+
warn(`\u274C ${manifestPath} is not a snapshot manifest: ${shapeError}`);
|
|
5584
|
+
return 1;
|
|
5585
|
+
}
|
|
3581
5586
|
const validation = validateSnapshot(resolved);
|
|
3582
5587
|
emit(invocation.format, { manifest, validation }, () => {
|
|
3583
5588
|
info(`
|
|
@@ -3594,8 +5599,8 @@ ${USAGE}
|
|
|
3594
5599
|
info(` - Integrity: ${validation.valid ? "\u2705 Clean (no drift)" : "\u274C Drift detected"}`);
|
|
3595
5600
|
info(` - Warnings: ${manifest.warnings?.length ?? 0}`);
|
|
3596
5601
|
info("\n Layers:");
|
|
3597
|
-
for (const [
|
|
3598
|
-
info(` - ${
|
|
5602
|
+
for (const [layer2, count] of Object.entries(manifest.layers)) {
|
|
5603
|
+
info(` - ${layer2.padEnd(16)}: ${count} files`);
|
|
3599
5604
|
}
|
|
3600
5605
|
});
|
|
3601
5606
|
return validation.valid ? 0 : 1;
|
|
@@ -3640,9 +5645,23 @@ ${USAGE}
|
|
|
3640
5645
|
info(
|
|
3641
5646
|
` - Total size: ${(manifest.counts.totalSizeBytes / (1024 * 1024)).toFixed(2)} MB`
|
|
3642
5647
|
);
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
5648
|
+
const { artifacts } = resolved;
|
|
5649
|
+
if (artifacts.parts) {
|
|
5650
|
+
info(` - Parts written: ${manifest.counts.partsCount} under ${resolved.outDir}/parts/`);
|
|
5651
|
+
}
|
|
5652
|
+
const reports = [
|
|
5653
|
+
artifacts.projectTree && "project-tree",
|
|
5654
|
+
artifacts.navigation && "navigation",
|
|
5655
|
+
artifacts.audit && "audit"
|
|
5656
|
+
].filter(Boolean);
|
|
5657
|
+
if (reports.length > 0) {
|
|
5658
|
+
info(` - Reports: ${resolved.outDir}/reports/ (${reports.join(", ")})`);
|
|
5659
|
+
}
|
|
5660
|
+
if (artifacts.manifest) info(` - Manifest: ${resolved.outDir}/manifest.json`);
|
|
5661
|
+
if (artifacts.monolith) info(` - Monolith: ${resolved.outDir}/full-codebase.{json,md}`);
|
|
5662
|
+
if (reports.length === 0 && !artifacts.parts && !artifacts.manifest && !artifacts.monolith) {
|
|
5663
|
+
info(` - Artifacts: all disabled (only the ownership marker was written)`);
|
|
5664
|
+
}
|
|
3646
5665
|
info(` - Fingerprint: ${manifest.fingerprint}`);
|
|
3647
5666
|
info(` - Integrity: ${manifest.validation.status}`);
|
|
3648
5667
|
if (manifest.warnings.length > 0) {
|
|
@@ -3659,15 +5678,23 @@ ${USAGE}
|
|
|
3659
5678
|
}
|
|
3660
5679
|
return manifest.validation.status === "passed" ? 0 : 1;
|
|
3661
5680
|
} catch (err) {
|
|
5681
|
+
const code = err.code;
|
|
5682
|
+
if (code === "EPERM" || code === "EACCES") {
|
|
5683
|
+
warn(
|
|
5684
|
+
`\u274C Cannot write the snapshot: permission denied under ${resolved.outDir}. Check the directory's permissions or choose another --out-dir.`
|
|
5685
|
+
);
|
|
5686
|
+
warn(` (${err.message})`);
|
|
5687
|
+
return 1;
|
|
5688
|
+
}
|
|
3662
5689
|
warn(`\u274C Failed to generate snapshot: ${err.message}`);
|
|
3663
5690
|
return 1;
|
|
3664
5691
|
}
|
|
3665
5692
|
}
|
|
3666
5693
|
async function runWatchLoop(invocation, resolved) {
|
|
3667
|
-
const outDir =
|
|
3668
|
-
const tmpDir =
|
|
3669
|
-
const outName =
|
|
3670
|
-
const tmpName =
|
|
5694
|
+
const outDir = path11.resolve(resolved.cwd, resolved.outDir);
|
|
5695
|
+
const tmpDir = path11.resolve(resolved.cwd, resolved.tmpDir);
|
|
5696
|
+
const outName = path11.basename(outDir).toLowerCase();
|
|
5697
|
+
const tmpName = path11.basename(tmpDir).toLowerCase();
|
|
3671
5698
|
const isGenerated = (candidate) => candidate.toLowerCase().split(/[\\/]/).some(
|
|
3672
5699
|
(segment) => segment === outName || segment === tmpName || segment.startsWith(`${outName}.backup-`)
|
|
3673
5700
|
);
|
|
@@ -3691,19 +5718,19 @@ async function runWatchLoop(invocation, resolved) {
|
|
|
3691
5718
|
info("\u{1F440} Watching for changes. Press Ctrl+C to stop.");
|
|
3692
5719
|
regenerate();
|
|
3693
5720
|
let timer = null;
|
|
3694
|
-
const watcher =
|
|
5721
|
+
const watcher = fs10.watch(resolved.cwd, { recursive: true }, (_event, filename) => {
|
|
3695
5722
|
if (filename) {
|
|
3696
|
-
const changed =
|
|
5723
|
+
const changed = path11.resolve(resolved.cwd, filename.toString());
|
|
3697
5724
|
if (isGenerated(changed)) return;
|
|
3698
5725
|
}
|
|
3699
5726
|
if (timer) clearTimeout(timer);
|
|
3700
5727
|
timer = setTimeout(regenerate, 250);
|
|
3701
5728
|
});
|
|
3702
|
-
await new Promise((
|
|
5729
|
+
await new Promise((resolve6) => {
|
|
3703
5730
|
const stop = () => {
|
|
3704
5731
|
if (timer) clearTimeout(timer);
|
|
3705
5732
|
watcher.close();
|
|
3706
|
-
|
|
5733
|
+
resolve6();
|
|
3707
5734
|
};
|
|
3708
5735
|
process.once("SIGINT", stop);
|
|
3709
5736
|
process.once("SIGTERM", stop);
|