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