combicode 1.7.3 ā 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/CHANGELOG.md +22 -0
- package/README.md +340 -19
- package/index.js +1482 -142
- package/package.json +22 -1
- package/test/test.js +473 -119
package/index.js
CHANGED
|
@@ -8,25 +8,34 @@ const ignore = require("ignore");
|
|
|
8
8
|
|
|
9
9
|
const { version } = require("./package.json");
|
|
10
10
|
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// System Prompts (v2.0.0)
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
11
15
|
const DEFAULT_SYSTEM_PROMPT = `You are an expert software architect. The user is providing you with the complete source code for a project, contained in a single file. Your task is to meticulously analyze the provided codebase to gain a comprehensive understanding of its structure, functionality, dependencies, and overall architecture.
|
|
12
16
|
|
|
13
|
-
A
|
|
17
|
+
A code map with expanded tree structure \`<code_index>\` is provided below to give you a high-level overview. The subsequent section \`<merged_code>\` contain the full content of each file (read using the command \`sed -n '<ML_START>,<ML_END>p' combicode.txt\`), clearly marked with a file header.
|
|
14
18
|
|
|
15
19
|
Your instructions are:
|
|
16
|
-
1.
|
|
17
|
-
2.
|
|
20
|
+
1. Analyze Thoroughly: Read through every file to understand its purpose and how it interacts with other files.
|
|
21
|
+
2. Identify Key Components: Pay close attention to configuration files (like package.json, pyproject.toml), entry points (like index.js, main.py), and core logic.
|
|
22
|
+
3. Use the Code Map: The code map shows classes, functions, loops with their line numbers (OL = Original Line, ML = Merged Line) and sizes for precise navigation.
|
|
18
23
|
`;
|
|
19
24
|
|
|
20
|
-
const LLMS_TXT_SYSTEM_PROMPT = `You are an expert software architect. The user is providing you with the full documentation for a project
|
|
25
|
+
const LLMS_TXT_SYSTEM_PROMPT = `You are an expert software architect. The user is providing you with the full documentation for a project. This file contains the complete context needed to understand the project's features, APIs, and usage for a specific version. Your task is to act as a definitive source of truth based *only* on this provided documentation.
|
|
21
26
|
|
|
22
27
|
When answering questions or writing code, adhere strictly to the functions, variables, and methods described in this context. Do not use or suggest any deprecated or older functionalities that are not present here.
|
|
23
28
|
|
|
24
|
-
A
|
|
29
|
+
A code map with expanded tree structure is provided below for a high-level overview.
|
|
25
30
|
`;
|
|
26
31
|
|
|
27
|
-
// Minimal safety ignores
|
|
32
|
+
// Minimal safety ignores
|
|
28
33
|
const SAFETY_IGNORES = [".git", ".DS_Store"];
|
|
29
34
|
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Utility helpers
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
30
39
|
function isLikelyBinary(filePath) {
|
|
31
40
|
const buffer = Buffer.alloc(512);
|
|
32
41
|
let fd;
|
|
@@ -42,17 +51,1093 @@ function isLikelyBinary(filePath) {
|
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
function formatBytes(bytes, decimals = 1) {
|
|
45
|
-
if (bytes === 0) return "
|
|
54
|
+
if (bytes === 0) return "0B";
|
|
46
55
|
const k = 1024;
|
|
47
56
|
const dm = decimals < 0 ? 0 : decimals;
|
|
48
57
|
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
|
49
58
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
50
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) +
|
|
59
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + sizes[i];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Code Parsers (regex-based for all languages)
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parse code structure from file content. Returns array of elements:
|
|
68
|
+
* { type, label, startLine, endLine, startByte, endByte }
|
|
69
|
+
*
|
|
70
|
+
* type: "class" | "fn" | "async" | "ctor" | "loop" | "impl" | "test" | "describe"
|
|
71
|
+
*/
|
|
72
|
+
function parseCodeStructure(filePath, content) {
|
|
73
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
74
|
+
const lines = content.split("\n");
|
|
75
|
+
|
|
76
|
+
switch (ext) {
|
|
77
|
+
case ".py":
|
|
78
|
+
return parsePython(lines);
|
|
79
|
+
case ".js":
|
|
80
|
+
case ".jsx":
|
|
81
|
+
case ".mjs":
|
|
82
|
+
case ".cjs":
|
|
83
|
+
return parseJavaScript(lines);
|
|
84
|
+
case ".ts":
|
|
85
|
+
case ".tsx":
|
|
86
|
+
case ".mts":
|
|
87
|
+
case ".cts":
|
|
88
|
+
return parseTypeScript(lines);
|
|
89
|
+
case ".go":
|
|
90
|
+
return parseGo(lines);
|
|
91
|
+
case ".rs":
|
|
92
|
+
return parseRust(lines);
|
|
93
|
+
case ".java":
|
|
94
|
+
return parseJava(lines);
|
|
95
|
+
case ".c":
|
|
96
|
+
case ".h":
|
|
97
|
+
case ".cpp":
|
|
98
|
+
case ".hpp":
|
|
99
|
+
case ".cc":
|
|
100
|
+
case ".cxx":
|
|
101
|
+
return parseCCpp(lines);
|
|
102
|
+
case ".cs":
|
|
103
|
+
return parseCSharp(lines);
|
|
104
|
+
case ".php":
|
|
105
|
+
return parsePHP(lines);
|
|
106
|
+
case ".rb":
|
|
107
|
+
return parseRuby(lines);
|
|
108
|
+
case ".swift":
|
|
109
|
+
return parseSwift(lines);
|
|
110
|
+
case ".kt":
|
|
111
|
+
case ".kts":
|
|
112
|
+
return parseKotlin(lines);
|
|
113
|
+
case ".scala":
|
|
114
|
+
case ".sc":
|
|
115
|
+
return parseScala(lines);
|
|
116
|
+
case ".lua":
|
|
117
|
+
return parseLua(lines);
|
|
118
|
+
case ".pl":
|
|
119
|
+
case ".pm":
|
|
120
|
+
return parsePerl(lines);
|
|
121
|
+
case ".sh":
|
|
122
|
+
case ".bash":
|
|
123
|
+
case ".zsh":
|
|
124
|
+
return parseBash(lines);
|
|
125
|
+
default:
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Find the end of a block that starts at `startLine` using brace/indent counting.
|
|
132
|
+
* For brace-based languages.
|
|
133
|
+
*/
|
|
134
|
+
function findBraceBlockEnd(lines, startLine) {
|
|
135
|
+
let depth = 0;
|
|
136
|
+
let foundOpen = false;
|
|
137
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
138
|
+
const line = lines[i];
|
|
139
|
+
for (const ch of line) {
|
|
140
|
+
if (ch === "{") {
|
|
141
|
+
depth++;
|
|
142
|
+
foundOpen = true;
|
|
143
|
+
} else if (ch === "}") {
|
|
144
|
+
depth--;
|
|
145
|
+
if (foundOpen && depth === 0) {
|
|
146
|
+
return i;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return lines.length - 1;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Find block end for Python (indent-based).
|
|
156
|
+
*/
|
|
157
|
+
function findIndentBlockEnd(lines, startLine) {
|
|
158
|
+
if (startLine >= lines.length) return startLine;
|
|
159
|
+
const defLine = lines[startLine];
|
|
160
|
+
const baseIndent = defLine.match(/^(\s*)/)[1].length;
|
|
161
|
+
|
|
162
|
+
for (let i = startLine + 1; i < lines.length; i++) {
|
|
163
|
+
const line = lines[i];
|
|
164
|
+
if (line.trim() === "") continue; // skip blank lines
|
|
165
|
+
const indent = line.match(/^(\s*)/)[1].length;
|
|
166
|
+
if (indent <= baseIndent) {
|
|
167
|
+
return i - 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return lines.length - 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Find block end for Ruby-like (def/end, class/end, module/end).
|
|
175
|
+
*/
|
|
176
|
+
function findRubyBlockEnd(lines, startLine) {
|
|
177
|
+
let depth = 0;
|
|
178
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
179
|
+
const trimmed = lines[i].trim();
|
|
180
|
+
// Keywords that open blocks
|
|
181
|
+
if (
|
|
182
|
+
/^(class|module|def|do|if|unless|case|while|until|for|begin)\b/.test(
|
|
183
|
+
trimmed,
|
|
184
|
+
) ||
|
|
185
|
+
/\bdo\s*(\|[^|]*\|)?\s*$/.test(trimmed)
|
|
186
|
+
) {
|
|
187
|
+
depth++;
|
|
188
|
+
}
|
|
189
|
+
if (/^end\b/.test(trimmed)) {
|
|
190
|
+
depth--;
|
|
191
|
+
if (depth === 0) return i;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return lines.length - 1;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Find block end for Lua (function/end).
|
|
199
|
+
*/
|
|
200
|
+
function findLuaBlockEnd(lines, startLine) {
|
|
201
|
+
let depth = 0;
|
|
202
|
+
for (let i = startLine; i < lines.length; i++) {
|
|
203
|
+
const trimmed = lines[i].trim();
|
|
204
|
+
if (/\b(function|if|for|while|repeat)\b/.test(trimmed)) depth++;
|
|
205
|
+
if (
|
|
206
|
+
/^end\b/.test(trimmed) ||
|
|
207
|
+
/\bend\s*[,)\]]/.test(trimmed) ||
|
|
208
|
+
trimmed === "end"
|
|
209
|
+
) {
|
|
210
|
+
depth--;
|
|
211
|
+
if (depth === 0) return i;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return lines.length - 1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function computeByteSize(lines, startLine, endLine) {
|
|
218
|
+
let size = 0;
|
|
219
|
+
for (let i = startLine; i <= endLine && i < lines.length; i++) {
|
|
220
|
+
size += Buffer.byteLength(lines[i], "utf8") + 1; // +1 for newline
|
|
221
|
+
}
|
|
222
|
+
return size;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function buildElement(type, label, startLine, endLine, lines) {
|
|
226
|
+
return {
|
|
227
|
+
type,
|
|
228
|
+
label,
|
|
229
|
+
startLine: startLine + 1, // 1-indexed
|
|
230
|
+
endLine: endLine + 1,
|
|
231
|
+
size: computeByteSize(lines, startLine, endLine),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// --- Language Parsers ---
|
|
236
|
+
|
|
237
|
+
function parsePython(lines) {
|
|
238
|
+
const elements = [];
|
|
239
|
+
for (let i = 0; i < lines.length; i++) {
|
|
240
|
+
const line = lines[i];
|
|
241
|
+
const trimmed = line.trim();
|
|
242
|
+
|
|
243
|
+
// class
|
|
244
|
+
let m = trimmed.match(/^class\s+(\w+)(\(.*?\))?\s*:/);
|
|
245
|
+
if (m) {
|
|
246
|
+
const end = findIndentBlockEnd(lines, i);
|
|
247
|
+
elements.push(buildElement("class", `class ${m[1]}`, i, end, lines));
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// async def
|
|
252
|
+
m = trimmed.match(/^async\s+def\s+(\w+)\s*\((.*?)\)(\s*->.*?)?\s*:/);
|
|
253
|
+
if (m) {
|
|
254
|
+
const end = findIndentBlockEnd(lines, i);
|
|
255
|
+
const sig = `${m[1]}(${m[2]})${m[3] || ""}`;
|
|
256
|
+
const type = m[1] === "__init__" ? "ctor" : "async";
|
|
257
|
+
elements.push(
|
|
258
|
+
buildElement(
|
|
259
|
+
type,
|
|
260
|
+
`${type === "ctor" ? "ctor" : "async"} ${sig}`,
|
|
261
|
+
i,
|
|
262
|
+
end,
|
|
263
|
+
lines,
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// def
|
|
270
|
+
m = trimmed.match(/^def\s+(\w+)\s*\((.*?)\)(\s*->.*?)?\s*:/);
|
|
271
|
+
if (m) {
|
|
272
|
+
const end = findIndentBlockEnd(lines, i);
|
|
273
|
+
const sig = `${m[1]}(${m[2]})${m[3] || ""}`;
|
|
274
|
+
let type = "fn";
|
|
275
|
+
if (m[1] === "__init__") type = "ctor";
|
|
276
|
+
else if (m[1].startsWith("test_")) type = "test";
|
|
277
|
+
const label =
|
|
278
|
+
type === "ctor"
|
|
279
|
+
? `ctor ${sig}`
|
|
280
|
+
: type === "test"
|
|
281
|
+
? `test ${sig}`
|
|
282
|
+
: `fn ${sig}`;
|
|
283
|
+
elements.push(buildElement(type, label, i, end, lines));
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// for/while loops (> 5 lines)
|
|
288
|
+
m = trimmed.match(/^(for|while)\s+(.+):\s*$/);
|
|
289
|
+
if (m) {
|
|
290
|
+
const end = findIndentBlockEnd(lines, i);
|
|
291
|
+
if (end - i + 1 > 5) {
|
|
292
|
+
elements.push(
|
|
293
|
+
buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return elements;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function parseJavaScript(lines) {
|
|
303
|
+
const elements = [];
|
|
304
|
+
for (let i = 0; i < lines.length; i++) {
|
|
305
|
+
const line = lines[i];
|
|
306
|
+
const trimmed = line.trim();
|
|
307
|
+
|
|
308
|
+
// class
|
|
309
|
+
let m = trimmed.match(/^(export\s+)?(default\s+)?class\s+(\w+)/);
|
|
310
|
+
if (m) {
|
|
311
|
+
const end = findBraceBlockEnd(lines, i);
|
|
312
|
+
elements.push(buildElement("class", `class ${m[3]}`, i, end, lines));
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// describe (test suite)
|
|
317
|
+
m = trimmed.match(/^describe\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
318
|
+
if (m) {
|
|
319
|
+
const end = findBraceBlockEnd(lines, i);
|
|
320
|
+
elements.push(
|
|
321
|
+
buildElement("describe", `describe ${m[1]}`, i, end, lines),
|
|
322
|
+
);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// test/it blocks
|
|
327
|
+
m = trimmed.match(/^(it|test)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
328
|
+
if (m) {
|
|
329
|
+
const end = findBraceBlockEnd(lines, i);
|
|
330
|
+
elements.push(buildElement("test", `test ${m[2]}`, i, end, lines));
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// async function
|
|
335
|
+
m = trimmed.match(
|
|
336
|
+
/^(export\s+)?(default\s+)?async\s+function\s+(\w+)\s*\((.*?)\)/,
|
|
337
|
+
);
|
|
338
|
+
if (m) {
|
|
339
|
+
const end = findBraceBlockEnd(lines, i);
|
|
340
|
+
elements.push(
|
|
341
|
+
buildElement("async", `async ${m[3]}(${m[4]})`, i, end, lines),
|
|
342
|
+
);
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// function
|
|
347
|
+
m = trimmed.match(/^(export\s+)?(default\s+)?function\s+(\w+)\s*\((.*?)\)/);
|
|
348
|
+
if (m) {
|
|
349
|
+
const end = findBraceBlockEnd(lines, i);
|
|
350
|
+
const type = m[3] === "constructor" ? "ctor" : "fn";
|
|
351
|
+
elements.push(
|
|
352
|
+
buildElement(type, `${type} ${m[3]}(${m[4]})`, i, end, lines),
|
|
353
|
+
);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// arrow functions assigned to const/let/var (with explicit function body)
|
|
358
|
+
m = trimmed.match(
|
|
359
|
+
/^(export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(?(.*?)\)?\s*=>/,
|
|
360
|
+
);
|
|
361
|
+
if (m && (trimmed.includes("{") || i + 1 < lines.length)) {
|
|
362
|
+
// Only include if it has a block body
|
|
363
|
+
if (
|
|
364
|
+
trimmed.includes("{") ||
|
|
365
|
+
(i + 1 < lines.length && lines[i + 1].trim().startsWith("{"))
|
|
366
|
+
) {
|
|
367
|
+
const end = findBraceBlockEnd(lines, i);
|
|
368
|
+
if (end > i) {
|
|
369
|
+
const isAsync = !!m[4];
|
|
370
|
+
const type = isAsync ? "async" : "fn";
|
|
371
|
+
elements.push(
|
|
372
|
+
buildElement(type, `${type} ${m[3]}(${m[5] || ""})`, i, end, lines),
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// for/while loops (> 5 lines)
|
|
380
|
+
m = trimmed.match(/^(for|while)\s*\((.+)\)\s*\{?/);
|
|
381
|
+
if (m) {
|
|
382
|
+
const end = findBraceBlockEnd(lines, i);
|
|
383
|
+
if (end - i + 1 > 5) {
|
|
384
|
+
elements.push(
|
|
385
|
+
buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return elements;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function parseTypeScript(lines) {
|
|
395
|
+
const elements = [];
|
|
396
|
+
for (let i = 0; i < lines.length; i++) {
|
|
397
|
+
const line = lines[i];
|
|
398
|
+
const trimmed = line.trim();
|
|
399
|
+
|
|
400
|
+
// interface
|
|
401
|
+
let m = trimmed.match(/^(export\s+)?(default\s+)?interface\s+(\w+)/);
|
|
402
|
+
if (m) {
|
|
403
|
+
const end = findBraceBlockEnd(lines, i);
|
|
404
|
+
elements.push(buildElement("class", `interface ${m[3]}`, i, end, lines));
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// class
|
|
409
|
+
m = trimmed.match(/^(export\s+)?(default\s+)?(abstract\s+)?class\s+(\w+)/);
|
|
410
|
+
if (m) {
|
|
411
|
+
const end = findBraceBlockEnd(lines, i);
|
|
412
|
+
elements.push(buildElement("class", `class ${m[4]}`, i, end, lines));
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// describe
|
|
417
|
+
m = trimmed.match(/^describe\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
418
|
+
if (m) {
|
|
419
|
+
const end = findBraceBlockEnd(lines, i);
|
|
420
|
+
elements.push(
|
|
421
|
+
buildElement("describe", `describe ${m[1]}`, i, end, lines),
|
|
422
|
+
);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// test/it
|
|
427
|
+
m = trimmed.match(/^(it|test)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
428
|
+
if (m) {
|
|
429
|
+
const end = findBraceBlockEnd(lines, i);
|
|
430
|
+
elements.push(buildElement("test", `test ${m[2]}`, i, end, lines));
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// async function
|
|
435
|
+
m = trimmed.match(
|
|
436
|
+
/^(export\s+)?(default\s+)?async\s+function\s+(\w+)\s*\((.*?)\)/,
|
|
437
|
+
);
|
|
438
|
+
if (m) {
|
|
439
|
+
const end = findBraceBlockEnd(lines, i);
|
|
440
|
+
elements.push(
|
|
441
|
+
buildElement("async", `async ${m[3]}(${m[4]})`, i, end, lines),
|
|
442
|
+
);
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// function
|
|
447
|
+
m = trimmed.match(/^(export\s+)?(default\s+)?function\s+(\w+)\s*\((.*?)\)/);
|
|
448
|
+
if (m) {
|
|
449
|
+
const end = findBraceBlockEnd(lines, i);
|
|
450
|
+
elements.push(buildElement("fn", `fn ${m[3]}(${m[4]})`, i, end, lines));
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// arrow functions
|
|
455
|
+
m = trimmed.match(
|
|
456
|
+
/^(export\s+)?(const|let|var)\s+(\w+)\s*=\s*(async\s+)?\(?(.*?)\)?\s*=>/,
|
|
457
|
+
);
|
|
458
|
+
if (m && trimmed.includes("{")) {
|
|
459
|
+
const end = findBraceBlockEnd(lines, i);
|
|
460
|
+
if (end > i) {
|
|
461
|
+
const isAsync = !!m[4];
|
|
462
|
+
const type = isAsync ? "async" : "fn";
|
|
463
|
+
elements.push(
|
|
464
|
+
buildElement(type, `${type} ${m[3]}(${m[5] || ""})`, i, end, lines),
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// for/while loops (> 5 lines)
|
|
471
|
+
m = trimmed.match(/^(for|while)\s*\((.+)\)\s*\{?/);
|
|
472
|
+
if (m) {
|
|
473
|
+
const end = findBraceBlockEnd(lines, i);
|
|
474
|
+
if (end - i + 1 > 5) {
|
|
475
|
+
elements.push(
|
|
476
|
+
buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return elements;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function parseGo(lines) {
|
|
485
|
+
const elements = [];
|
|
486
|
+
for (let i = 0; i < lines.length; i++) {
|
|
487
|
+
const trimmed = lines[i].trim();
|
|
488
|
+
|
|
489
|
+
// struct
|
|
490
|
+
let m = trimmed.match(/^type\s+(\w+)\s+struct\b/);
|
|
491
|
+
if (m) {
|
|
492
|
+
const end = findBraceBlockEnd(lines, i);
|
|
493
|
+
elements.push(buildElement("class", `struct ${m[1]}`, i, end, lines));
|
|
494
|
+
continue;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// interface
|
|
498
|
+
m = trimmed.match(/^type\s+(\w+)\s+interface\b/);
|
|
499
|
+
if (m) {
|
|
500
|
+
const end = findBraceBlockEnd(lines, i);
|
|
501
|
+
elements.push(buildElement("class", `interface ${m[1]}`, i, end, lines));
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// func
|
|
506
|
+
m = trimmed.match(/^func\s+(\(.*?\)\s*)?(\w+)\s*\((.*?)\)/);
|
|
507
|
+
if (m) {
|
|
508
|
+
const end = findBraceBlockEnd(lines, i);
|
|
509
|
+
const receiver = m[1] ? m[1].trim() + " " : "";
|
|
510
|
+
const name = m[2];
|
|
511
|
+
const type = name.startsWith("Test") ? "test" : "fn";
|
|
512
|
+
elements.push(
|
|
513
|
+
buildElement(
|
|
514
|
+
type,
|
|
515
|
+
`${type === "test" ? "test" : "fn"} ${receiver}${name}(${m[3]})`,
|
|
516
|
+
i,
|
|
517
|
+
end,
|
|
518
|
+
lines,
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
continue;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// for loops (> 5 lines) - Go only has for
|
|
525
|
+
m = trimmed.match(/^for\s+(.+)\s*\{/);
|
|
526
|
+
if (m) {
|
|
527
|
+
const end = findBraceBlockEnd(lines, i);
|
|
528
|
+
if (end - i + 1 > 5) {
|
|
529
|
+
elements.push(buildElement("loop", `loop for ${m[1]}`, i, end, lines));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return elements;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function parseRust(lines) {
|
|
537
|
+
const elements = [];
|
|
538
|
+
for (let i = 0; i < lines.length; i++) {
|
|
539
|
+
const trimmed = lines[i].trim();
|
|
540
|
+
|
|
541
|
+
// struct
|
|
542
|
+
let m = trimmed.match(/^(pub\s+)?struct\s+(\w+)/);
|
|
543
|
+
if (m) {
|
|
544
|
+
const end = findBraceBlockEnd(lines, i);
|
|
545
|
+
elements.push(buildElement("class", `struct ${m[2]}`, i, end, lines));
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// enum
|
|
550
|
+
m = trimmed.match(/^(pub\s+)?enum\s+(\w+)/);
|
|
551
|
+
if (m) {
|
|
552
|
+
const end = findBraceBlockEnd(lines, i);
|
|
553
|
+
elements.push(buildElement("class", `enum ${m[2]}`, i, end, lines));
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// trait
|
|
558
|
+
m = trimmed.match(/^(pub\s+)?trait\s+(\w+)/);
|
|
559
|
+
if (m) {
|
|
560
|
+
const end = findBraceBlockEnd(lines, i);
|
|
561
|
+
elements.push(buildElement("class", `trait ${m[2]}`, i, end, lines));
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// impl
|
|
566
|
+
m = trimmed.match(/^impl\s+(.+?)\s*\{/);
|
|
567
|
+
if (m) {
|
|
568
|
+
const end = findBraceBlockEnd(lines, i);
|
|
569
|
+
elements.push(buildElement("impl", `impl ${m[1]}`, i, end, lines));
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// fn
|
|
574
|
+
m = trimmed.match(/^(pub\s+)?(async\s+)?fn\s+(\w+)\s*\((.*?)\)/);
|
|
575
|
+
if (m) {
|
|
576
|
+
const end = findBraceBlockEnd(lines, i);
|
|
577
|
+
const isAsync = !!m[2];
|
|
578
|
+
const isTest = m[3].startsWith("test_");
|
|
579
|
+
const type = isTest ? "test" : isAsync ? "async" : "fn";
|
|
580
|
+
elements.push(
|
|
581
|
+
buildElement(type, `${type} ${m[3]}(${m[4]})`, i, end, lines),
|
|
582
|
+
);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// loop/for/while (> 5 lines)
|
|
587
|
+
m = trimmed.match(/^(for|while|loop)\b(.*)?\{/);
|
|
588
|
+
if (m) {
|
|
589
|
+
const end = findBraceBlockEnd(lines, i);
|
|
590
|
+
if (end - i + 1 > 5) {
|
|
591
|
+
elements.push(
|
|
592
|
+
buildElement(
|
|
593
|
+
"loop",
|
|
594
|
+
`loop ${m[1]}${m[2] ? " " + m[2].trim() : ""}`,
|
|
595
|
+
i,
|
|
596
|
+
end,
|
|
597
|
+
lines,
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
return elements;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function parseJava(lines) {
|
|
607
|
+
const elements = [];
|
|
608
|
+
for (let i = 0; i < lines.length; i++) {
|
|
609
|
+
const trimmed = lines[i].trim();
|
|
610
|
+
|
|
611
|
+
// class
|
|
612
|
+
let m = trimmed.match(
|
|
613
|
+
/^(public\s+|private\s+|protected\s+)?(static\s+)?(abstract\s+)?(final\s+)?class\s+(\w+)/,
|
|
614
|
+
);
|
|
615
|
+
if (m) {
|
|
616
|
+
const end = findBraceBlockEnd(lines, i);
|
|
617
|
+
elements.push(buildElement("class", `class ${m[5]}`, i, end, lines));
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// interface
|
|
622
|
+
m = trimmed.match(/^(public\s+|private\s+|protected\s+)?interface\s+(\w+)/);
|
|
623
|
+
if (m) {
|
|
624
|
+
const end = findBraceBlockEnd(lines, i);
|
|
625
|
+
elements.push(buildElement("class", `interface ${m[2]}`, i, end, lines));
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// enum
|
|
630
|
+
m = trimmed.match(/^(public\s+|private\s+|protected\s+)?enum\s+(\w+)/);
|
|
631
|
+
if (m) {
|
|
632
|
+
const end = findBraceBlockEnd(lines, i);
|
|
633
|
+
elements.push(buildElement("class", `enum ${m[2]}`, i, end, lines));
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// method (including constructors)
|
|
638
|
+
m = trimmed.match(
|
|
639
|
+
/^(public\s+|private\s+|protected\s+)?(static\s+)?(abstract\s+)?(final\s+)?(synchronized\s+)?(\w+\s+)?(\w+)\s*\((.*?)\)\s*(\{|throws)/,
|
|
640
|
+
);
|
|
641
|
+
if (
|
|
642
|
+
m &&
|
|
643
|
+
!["if", "for", "while", "switch", "catch", "return"].includes(m[7])
|
|
644
|
+
) {
|
|
645
|
+
const end = findBraceBlockEnd(lines, i);
|
|
646
|
+
const name = m[7];
|
|
647
|
+
// Constructor: return type is absent and name matches class-like pattern
|
|
648
|
+
const hasReturnType = m[6] && m[6].trim();
|
|
649
|
+
const type = !hasReturnType
|
|
650
|
+
? "ctor"
|
|
651
|
+
: name.startsWith("test")
|
|
652
|
+
? "test"
|
|
653
|
+
: "fn";
|
|
654
|
+
elements.push(
|
|
655
|
+
buildElement(type, `${type} ${name}(${m[8]})`, i, end, lines),
|
|
656
|
+
);
|
|
657
|
+
continue;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// for/while loops (> 5 lines)
|
|
661
|
+
m = trimmed.match(/^(for|while)\s*\((.+)\)\s*\{?/);
|
|
662
|
+
if (m) {
|
|
663
|
+
const end = findBraceBlockEnd(lines, i);
|
|
664
|
+
if (end - i + 1 > 5) {
|
|
665
|
+
elements.push(
|
|
666
|
+
buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return elements;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function parseCCpp(lines) {
|
|
675
|
+
const elements = [];
|
|
676
|
+
for (let i = 0; i < lines.length; i++) {
|
|
677
|
+
const trimmed = lines[i].trim();
|
|
678
|
+
|
|
679
|
+
// class
|
|
680
|
+
let m = trimmed.match(/^class\s+(\w+)/);
|
|
681
|
+
if (m) {
|
|
682
|
+
const end = findBraceBlockEnd(lines, i);
|
|
683
|
+
elements.push(buildElement("class", `class ${m[1]}`, i, end, lines));
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// struct
|
|
688
|
+
m = trimmed.match(/^(typedef\s+)?struct\s+(\w+)/);
|
|
689
|
+
if (m) {
|
|
690
|
+
const end = findBraceBlockEnd(lines, i);
|
|
691
|
+
elements.push(buildElement("class", `struct ${m[2]}`, i, end, lines));
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// function (C-style: return_type name(...))
|
|
696
|
+
m = trimmed.match(/^(\w[\w\s*&]+?)\s+(\w+)\s*\(([^)]*)\)\s*(\{|$)/);
|
|
697
|
+
if (
|
|
698
|
+
m &&
|
|
699
|
+
![
|
|
700
|
+
"if",
|
|
701
|
+
"for",
|
|
702
|
+
"while",
|
|
703
|
+
"switch",
|
|
704
|
+
"return",
|
|
705
|
+
"typedef",
|
|
706
|
+
"struct",
|
|
707
|
+
"class",
|
|
708
|
+
"enum",
|
|
709
|
+
].includes(m[2])
|
|
710
|
+
) {
|
|
711
|
+
const end = findBraceBlockEnd(lines, i);
|
|
712
|
+
elements.push(buildElement("fn", `fn ${m[2]}(${m[3]})`, i, end, lines));
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// for/while loops (> 5 lines)
|
|
717
|
+
m = trimmed.match(/^(for|while)\s*\((.+)\)\s*\{?/);
|
|
718
|
+
if (m) {
|
|
719
|
+
const end = findBraceBlockEnd(lines, i);
|
|
720
|
+
if (end - i + 1 > 5) {
|
|
721
|
+
elements.push(
|
|
722
|
+
buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return elements;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function parseCSharp(lines) {
|
|
731
|
+
const elements = [];
|
|
732
|
+
for (let i = 0; i < lines.length; i++) {
|
|
733
|
+
const trimmed = lines[i].trim();
|
|
734
|
+
|
|
735
|
+
// class / struct / interface / enum / record
|
|
736
|
+
let m = trimmed.match(
|
|
737
|
+
/^(public\s+|private\s+|protected\s+|internal\s+)?(static\s+)?(abstract\s+|sealed\s+)?(partial\s+)?(class|struct|interface|enum|record)\s+(\w+)/,
|
|
738
|
+
);
|
|
739
|
+
if (m) {
|
|
740
|
+
const end = findBraceBlockEnd(lines, i);
|
|
741
|
+
elements.push(buildElement("class", `${m[5]} ${m[6]}`, i, end, lines));
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// method
|
|
746
|
+
m = trimmed.match(
|
|
747
|
+
/^(public\s+|private\s+|protected\s+|internal\s+)?(static\s+)?(async\s+)?(virtual\s+|override\s+|abstract\s+)?(\w[\w<>\[\],\s]*?)\s+(\w+)\s*\((.*?)\)\s*\{?/,
|
|
748
|
+
);
|
|
749
|
+
if (
|
|
750
|
+
m &&
|
|
751
|
+
![
|
|
752
|
+
"if",
|
|
753
|
+
"for",
|
|
754
|
+
"while",
|
|
755
|
+
"switch",
|
|
756
|
+
"catch",
|
|
757
|
+
"return",
|
|
758
|
+
"class",
|
|
759
|
+
"struct",
|
|
760
|
+
"interface",
|
|
761
|
+
"enum",
|
|
762
|
+
].includes(m[6])
|
|
763
|
+
) {
|
|
764
|
+
const end = findBraceBlockEnd(lines, i);
|
|
765
|
+
const isAsync = !!m[3];
|
|
766
|
+
const type = isAsync ? "async" : "fn";
|
|
767
|
+
elements.push(
|
|
768
|
+
buildElement(type, `${type} ${m[6]}(${m[7]})`, i, end, lines),
|
|
769
|
+
);
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// for/while loops (> 5 lines)
|
|
774
|
+
m = trimmed.match(/^(for|foreach|while)\s*\((.+)\)\s*\{?/);
|
|
775
|
+
if (m) {
|
|
776
|
+
const end = findBraceBlockEnd(lines, i);
|
|
777
|
+
if (end - i + 1 > 5) {
|
|
778
|
+
elements.push(
|
|
779
|
+
buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
return elements;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
function parsePHP(lines) {
|
|
788
|
+
const elements = [];
|
|
789
|
+
for (let i = 0; i < lines.length; i++) {
|
|
790
|
+
const trimmed = lines[i].trim();
|
|
791
|
+
|
|
792
|
+
// class / interface / trait
|
|
793
|
+
let m = trimmed.match(
|
|
794
|
+
/^(abstract\s+)?(final\s+)?(class|interface|trait)\s+(\w+)/,
|
|
795
|
+
);
|
|
796
|
+
if (m) {
|
|
797
|
+
const end = findBraceBlockEnd(lines, i);
|
|
798
|
+
elements.push(buildElement("class", `${m[3]} ${m[4]}`, i, end, lines));
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// function
|
|
803
|
+
m = trimmed.match(
|
|
804
|
+
/^(public\s+|private\s+|protected\s+)?(static\s+)?function\s+(\w+)\s*\((.*?)\)/,
|
|
805
|
+
);
|
|
806
|
+
if (m) {
|
|
807
|
+
const end = findBraceBlockEnd(lines, i);
|
|
808
|
+
const type =
|
|
809
|
+
m[3] === "__construct"
|
|
810
|
+
? "ctor"
|
|
811
|
+
: m[3].startsWith("test")
|
|
812
|
+
? "test"
|
|
813
|
+
: "fn";
|
|
814
|
+
elements.push(
|
|
815
|
+
buildElement(type, `${type} ${m[3]}(${m[4]})`, i, end, lines),
|
|
816
|
+
);
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// for/while loops (> 5 lines)
|
|
821
|
+
m = trimmed.match(/^(for|foreach|while)\s*\((.+)\)\s*\{?/);
|
|
822
|
+
if (m) {
|
|
823
|
+
const end = findBraceBlockEnd(lines, i);
|
|
824
|
+
if (end - i + 1 > 5) {
|
|
825
|
+
elements.push(
|
|
826
|
+
buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return elements;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function parseRuby(lines) {
|
|
835
|
+
const elements = [];
|
|
836
|
+
for (let i = 0; i < lines.length; i++) {
|
|
837
|
+
const trimmed = lines[i].trim();
|
|
838
|
+
|
|
839
|
+
// class
|
|
840
|
+
let m = trimmed.match(/^class\s+(\w+)/);
|
|
841
|
+
if (m) {
|
|
842
|
+
const end = findRubyBlockEnd(lines, i);
|
|
843
|
+
elements.push(buildElement("class", `class ${m[1]}`, i, end, lines));
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// module
|
|
848
|
+
m = trimmed.match(/^module\s+(\w+)/);
|
|
849
|
+
if (m) {
|
|
850
|
+
const end = findRubyBlockEnd(lines, i);
|
|
851
|
+
elements.push(buildElement("class", `module ${m[1]}`, i, end, lines));
|
|
852
|
+
continue;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// def
|
|
856
|
+
m = trimmed.match(/^def\s+(self\.)?(\w+[?!=]?)\s*(\(.*?\))?/);
|
|
857
|
+
if (m) {
|
|
858
|
+
const end = findRubyBlockEnd(lines, i);
|
|
859
|
+
const prefix = m[1] || "";
|
|
860
|
+
const type =
|
|
861
|
+
m[2] === "initialize"
|
|
862
|
+
? "ctor"
|
|
863
|
+
: m[2].startsWith("test_")
|
|
864
|
+
? "test"
|
|
865
|
+
: "fn";
|
|
866
|
+
elements.push(
|
|
867
|
+
buildElement(
|
|
868
|
+
type,
|
|
869
|
+
`${type} ${prefix}${m[2]}${m[3] || ""}`,
|
|
870
|
+
i,
|
|
871
|
+
end,
|
|
872
|
+
lines,
|
|
873
|
+
),
|
|
874
|
+
);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
return elements;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function parseSwift(lines) {
|
|
882
|
+
const elements = [];
|
|
883
|
+
for (let i = 0; i < lines.length; i++) {
|
|
884
|
+
const trimmed = lines[i].trim();
|
|
885
|
+
|
|
886
|
+
// class / struct / enum / protocol
|
|
887
|
+
let m = trimmed.match(
|
|
888
|
+
/^(public\s+|private\s+|internal\s+|open\s+|fileprivate\s+)?(final\s+)?(class|struct|enum|protocol)\s+(\w+)/,
|
|
889
|
+
);
|
|
890
|
+
if (m) {
|
|
891
|
+
const end = findBraceBlockEnd(lines, i);
|
|
892
|
+
elements.push(buildElement("class", `${m[3]} ${m[4]}`, i, end, lines));
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// func
|
|
897
|
+
m = trimmed.match(
|
|
898
|
+
/^(public\s+|private\s+|internal\s+|open\s+)?(static\s+|class\s+)?(override\s+)?func\s+(\w+)\s*\((.*?)\)/,
|
|
899
|
+
);
|
|
900
|
+
if (m) {
|
|
901
|
+
const end = findBraceBlockEnd(lines, i);
|
|
902
|
+
const name = m[4];
|
|
903
|
+
const type =
|
|
904
|
+
name === "init" ? "ctor" : name.startsWith("test") ? "test" : "fn";
|
|
905
|
+
elements.push(
|
|
906
|
+
buildElement(type, `${type} ${name}(${m[5]})`, i, end, lines),
|
|
907
|
+
);
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// for/while loops (> 5 lines)
|
|
912
|
+
m = trimmed.match(/^(for|while)\s+(.+)\s*\{/);
|
|
913
|
+
if (m) {
|
|
914
|
+
const end = findBraceBlockEnd(lines, i);
|
|
915
|
+
if (end - i + 1 > 5) {
|
|
916
|
+
elements.push(
|
|
917
|
+
buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
return elements;
|
|
51
923
|
}
|
|
52
924
|
|
|
925
|
+
function parseKotlin(lines) {
|
|
926
|
+
const elements = [];
|
|
927
|
+
for (let i = 0; i < lines.length; i++) {
|
|
928
|
+
const trimmed = lines[i].trim();
|
|
929
|
+
|
|
930
|
+
// class / interface / object
|
|
931
|
+
let m = trimmed.match(
|
|
932
|
+
/^(open\s+|abstract\s+|data\s+|sealed\s+)?(class|interface|object)\s+(\w+)/,
|
|
933
|
+
);
|
|
934
|
+
if (m) {
|
|
935
|
+
const end = findBraceBlockEnd(lines, i);
|
|
936
|
+
elements.push(buildElement("class", `${m[2]} ${m[3]}`, i, end, lines));
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// fun
|
|
941
|
+
m = trimmed.match(
|
|
942
|
+
/^(public\s+|private\s+|protected\s+|internal\s+)?(override\s+)?(suspend\s+)?fun\s+(\w+)\s*\((.*?)\)/,
|
|
943
|
+
);
|
|
944
|
+
if (m) {
|
|
945
|
+
const end = findBraceBlockEnd(lines, i);
|
|
946
|
+
const isSuspend = !!m[3];
|
|
947
|
+
const type = m[4].startsWith("test")
|
|
948
|
+
? "test"
|
|
949
|
+
: isSuspend
|
|
950
|
+
? "async"
|
|
951
|
+
: "fn";
|
|
952
|
+
elements.push(
|
|
953
|
+
buildElement(type, `${type} ${m[4]}(${m[5]})`, i, end, lines),
|
|
954
|
+
);
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
// for/while loops (> 5 lines)
|
|
959
|
+
m = trimmed.match(/^(for|while)\s*\((.+)\)\s*\{?/);
|
|
960
|
+
if (m) {
|
|
961
|
+
const end = findBraceBlockEnd(lines, i);
|
|
962
|
+
if (end - i + 1 > 5) {
|
|
963
|
+
elements.push(
|
|
964
|
+
buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
return elements;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function parseScala(lines) {
|
|
973
|
+
const elements = [];
|
|
974
|
+
for (let i = 0; i < lines.length; i++) {
|
|
975
|
+
const trimmed = lines[i].trim();
|
|
976
|
+
|
|
977
|
+
// class / object / trait
|
|
978
|
+
let m = trimmed.match(/^(case\s+)?(class|object|trait)\s+(\w+)/);
|
|
979
|
+
if (m) {
|
|
980
|
+
const end = findBraceBlockEnd(lines, i);
|
|
981
|
+
elements.push(
|
|
982
|
+
buildElement("class", `${m[1] || ""}${m[2]} ${m[3]}`, i, end, lines),
|
|
983
|
+
);
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// def
|
|
988
|
+
m = trimmed.match(/^(override\s+)?def\s+(\w+)\s*(\(.*?\))?/);
|
|
989
|
+
if (m) {
|
|
990
|
+
const end = findBraceBlockEnd(lines, i);
|
|
991
|
+
const type = m[2].startsWith("test") ? "test" : "fn";
|
|
992
|
+
elements.push(
|
|
993
|
+
buildElement(type, `${type} ${m[2]}${m[3] || ""}`, i, end, lines),
|
|
994
|
+
);
|
|
995
|
+
continue;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return elements;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
function parseLua(lines) {
|
|
1002
|
+
const elements = [];
|
|
1003
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1004
|
+
const trimmed = lines[i].trim();
|
|
1005
|
+
|
|
1006
|
+
// function / local function
|
|
1007
|
+
let m = trimmed.match(/^(local\s+)?function\s+([\w.:]+)\s*\((.*?)\)/);
|
|
1008
|
+
if (m) {
|
|
1009
|
+
const end = findLuaBlockEnd(lines, i);
|
|
1010
|
+
elements.push(buildElement("fn", `fn ${m[2]}(${m[3]})`, i, end, lines));
|
|
1011
|
+
continue;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// for/while loops (> 5 lines)
|
|
1015
|
+
m = trimmed.match(/^(for|while)\s+(.+)\s+do/);
|
|
1016
|
+
if (m) {
|
|
1017
|
+
const end = findLuaBlockEnd(lines, i);
|
|
1018
|
+
if (end - i + 1 > 5) {
|
|
1019
|
+
elements.push(
|
|
1020
|
+
buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
|
|
1021
|
+
);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return elements;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
function parsePerl(lines) {
|
|
1029
|
+
const elements = [];
|
|
1030
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1031
|
+
const trimmed = lines[i].trim();
|
|
1032
|
+
|
|
1033
|
+
// package
|
|
1034
|
+
let m = trimmed.match(/^package\s+([\w:]+)/);
|
|
1035
|
+
if (m) {
|
|
1036
|
+
elements.push(buildElement("class", `package ${m[1]}`, i, i, lines));
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// sub
|
|
1041
|
+
m = trimmed.match(/^sub\s+(\w+)/);
|
|
1042
|
+
if (m) {
|
|
1043
|
+
const end = findBraceBlockEnd(lines, i);
|
|
1044
|
+
elements.push(buildElement("fn", `fn ${m[1]}`, i, end, lines));
|
|
1045
|
+
continue;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return elements;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function parseBash(lines) {
|
|
1052
|
+
const elements = [];
|
|
1053
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1054
|
+
const trimmed = lines[i].trim();
|
|
1055
|
+
|
|
1056
|
+
// function keyword or name()
|
|
1057
|
+
let m = trimmed.match(/^(function\s+)?(\w+)\s*\(\s*\)\s*\{?/);
|
|
1058
|
+
if (m && m[1]) {
|
|
1059
|
+
// function keyword form
|
|
1060
|
+
const end = findBraceBlockEnd(lines, i);
|
|
1061
|
+
elements.push(buildElement("fn", `fn ${m[2]}`, i, end, lines));
|
|
1062
|
+
continue;
|
|
1063
|
+
}
|
|
1064
|
+
if (m && !m[1] && trimmed.includes("()")) {
|
|
1065
|
+
// name() form
|
|
1066
|
+
const end = findBraceBlockEnd(lines, i);
|
|
1067
|
+
elements.push(buildElement("fn", `fn ${m[2]}`, i, end, lines));
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
// for/while loops (> 5 lines)
|
|
1072
|
+
m = trimmed.match(/^(for|while)\s+(.+?);\s*do/);
|
|
1073
|
+
if (!m) m = trimmed.match(/^(for|while)\s+(.+)/);
|
|
1074
|
+
if (m) {
|
|
1075
|
+
// Look for done
|
|
1076
|
+
let end = i;
|
|
1077
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
1078
|
+
if (lines[j].trim() === "done") {
|
|
1079
|
+
end = j;
|
|
1080
|
+
break;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (end - i + 1 > 5) {
|
|
1084
|
+
elements.push(
|
|
1085
|
+
buildElement("loop", `loop ${m[1]} ${m[2]}`, i, end, lines),
|
|
1086
|
+
);
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return elements;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// ---------------------------------------------------------------------------
|
|
1094
|
+
// Nesting + Tree Building
|
|
1095
|
+
// ---------------------------------------------------------------------------
|
|
1096
|
+
|
|
53
1097
|
/**
|
|
54
|
-
*
|
|
1098
|
+
* Nest flat elements into a tree based on line ranges.
|
|
1099
|
+
* Elements that fall within the range of a parent become children.
|
|
55
1100
|
*/
|
|
1101
|
+
function nestElements(elements) {
|
|
1102
|
+
if (!elements.length) return [];
|
|
1103
|
+
|
|
1104
|
+
// Sort by start line, then by larger range first (parents before children)
|
|
1105
|
+
const sorted = [...elements].sort((a, b) => {
|
|
1106
|
+
if (a.startLine !== b.startLine) return a.startLine - b.startLine;
|
|
1107
|
+
return b.endLine - b.startLine - (a.endLine - a.startLine);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
const root = [];
|
|
1111
|
+
const stack = []; // stack of { element, children }
|
|
1112
|
+
|
|
1113
|
+
for (const el of sorted) {
|
|
1114
|
+
const node = { ...el, children: [] };
|
|
1115
|
+
|
|
1116
|
+
// Pop from stack if current element is outside of parent's range
|
|
1117
|
+
while (stack.length > 0) {
|
|
1118
|
+
const parent = stack[stack.length - 1];
|
|
1119
|
+
if (el.startLine >= parent.startLine && el.endLine <= parent.endLine) {
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
stack.pop();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
if (stack.length > 0) {
|
|
1126
|
+
stack[stack.length - 1].children.push(node);
|
|
1127
|
+
} else {
|
|
1128
|
+
root.push(node);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
stack.push(node);
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
return root;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// ---------------------------------------------------------------------------
|
|
1138
|
+
// Directory Walker (unchanged from v1)
|
|
1139
|
+
// ---------------------------------------------------------------------------
|
|
1140
|
+
|
|
56
1141
|
function walkDirectory(
|
|
57
1142
|
currentDir,
|
|
58
1143
|
rootDir,
|
|
@@ -60,12 +1145,11 @@ function walkDirectory(
|
|
|
60
1145
|
allowedExts,
|
|
61
1146
|
absoluteOutputPath,
|
|
62
1147
|
useGitIgnore,
|
|
63
|
-
stats
|
|
1148
|
+
stats,
|
|
64
1149
|
) {
|
|
65
1150
|
let results = [];
|
|
66
1151
|
let currentIgnoreManager = null;
|
|
67
1152
|
|
|
68
|
-
// 1. Check for local .gitignore and add to chain for this scope
|
|
69
1153
|
if (useGitIgnore) {
|
|
70
1154
|
const gitignorePath = path.join(currentDir, ".gitignore");
|
|
71
1155
|
if (fs.existsSync(gitignorePath)) {
|
|
@@ -73,13 +1157,10 @@ function walkDirectory(
|
|
|
73
1157
|
const content = fs.readFileSync(gitignorePath, "utf8");
|
|
74
1158
|
const ig = ignore().add(content);
|
|
75
1159
|
currentIgnoreManager = { manager: ig, root: currentDir };
|
|
76
|
-
} catch (e) {
|
|
77
|
-
// Warning could go here
|
|
78
|
-
}
|
|
1160
|
+
} catch (e) {}
|
|
79
1161
|
}
|
|
80
1162
|
}
|
|
81
1163
|
|
|
82
|
-
// Create a new chain for this directory and its children
|
|
83
1164
|
const nextIgnoreChain = currentIgnoreManager
|
|
84
1165
|
? [...ignoreChain, currentIgnoreManager]
|
|
85
1166
|
: ignoreChain;
|
|
@@ -94,25 +1175,17 @@ function walkDirectory(
|
|
|
94
1175
|
for (const entry of entries) {
|
|
95
1176
|
const fullPath = path.join(currentDir, entry.name);
|
|
96
1177
|
|
|
97
|
-
// SKIP CHECK: Output file
|
|
98
1178
|
if (path.resolve(fullPath) === absoluteOutputPath) continue;
|
|
99
1179
|
|
|
100
|
-
// SKIP CHECK: Ignore Chain
|
|
101
1180
|
let shouldIgnore = false;
|
|
102
1181
|
for (const item of nextIgnoreChain) {
|
|
103
|
-
// Calculate path relative to the specific ignore manager's root
|
|
104
|
-
// IMPORTANT: Normalize to POSIX slashes for 'ignore' package compatibility
|
|
105
1182
|
let relToIgnoreRoot = path.relative(item.root, fullPath);
|
|
106
|
-
|
|
107
1183
|
if (path.sep === "\\") {
|
|
108
1184
|
relToIgnoreRoot = relToIgnoreRoot.replace(/\\/g, "/");
|
|
109
1185
|
}
|
|
110
|
-
|
|
111
|
-
// If checking a directory, ensure trailing slash for proper 'ignore' directory matching
|
|
112
1186
|
if (entry.isDirectory() && !relToIgnoreRoot.endsWith("/")) {
|
|
113
1187
|
relToIgnoreRoot += "/";
|
|
114
1188
|
}
|
|
115
|
-
|
|
116
1189
|
if (item.manager.ignores(relToIgnoreRoot)) {
|
|
117
1190
|
shouldIgnore = true;
|
|
118
1191
|
break;
|
|
@@ -125,7 +1198,6 @@ function walkDirectory(
|
|
|
125
1198
|
}
|
|
126
1199
|
|
|
127
1200
|
if (entry.isDirectory()) {
|
|
128
|
-
// Recurse
|
|
129
1201
|
results = results.concat(
|
|
130
1202
|
walkDirectory(
|
|
131
1203
|
fullPath,
|
|
@@ -134,22 +1206,18 @@ function walkDirectory(
|
|
|
134
1206
|
allowedExts,
|
|
135
1207
|
absoluteOutputPath,
|
|
136
1208
|
useGitIgnore,
|
|
137
|
-
stats
|
|
138
|
-
)
|
|
1209
|
+
stats,
|
|
1210
|
+
),
|
|
139
1211
|
);
|
|
140
1212
|
} else if (entry.isFile()) {
|
|
141
|
-
// SKIP CHECK: Binary
|
|
142
1213
|
if (isLikelyBinary(fullPath)) {
|
|
143
1214
|
stats.ignored++;
|
|
144
1215
|
continue;
|
|
145
1216
|
}
|
|
146
|
-
|
|
147
|
-
// SKIP CHECK: Extensions
|
|
148
1217
|
if (allowedExts && !allowedExts.has(path.extname(entry.name))) {
|
|
149
1218
|
stats.ignored++;
|
|
150
1219
|
continue;
|
|
151
1220
|
}
|
|
152
|
-
|
|
153
1221
|
try {
|
|
154
1222
|
const fileStats = fs.statSync(fullPath);
|
|
155
1223
|
const relativeToRoot = path.relative(rootDir, fullPath);
|
|
@@ -160,62 +1228,175 @@ function walkDirectory(
|
|
|
160
1228
|
size: fileStats.size,
|
|
161
1229
|
formattedSize: formatBytes(fileStats.size),
|
|
162
1230
|
});
|
|
163
|
-
} catch (e) {
|
|
164
|
-
// Skip inaccessible files
|
|
165
|
-
}
|
|
1231
|
+
} catch (e) {}
|
|
166
1232
|
}
|
|
167
1233
|
}
|
|
168
1234
|
|
|
169
1235
|
return results;
|
|
170
1236
|
}
|
|
171
1237
|
|
|
172
|
-
|
|
1238
|
+
// ---------------------------------------------------------------------------
|
|
1239
|
+
// Code Index Tree Generator (v2.0.0)
|
|
1240
|
+
// ---------------------------------------------------------------------------
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Build the <code_index> tree with expanded code elements.
|
|
1244
|
+
*/
|
|
1245
|
+
function generateCodeIndex(filesWithMeta, root, skipContentSet, noParse) {
|
|
173
1246
|
let tree = `${path.basename(root)}/\n`;
|
|
174
|
-
const structure = {};
|
|
175
1247
|
|
|
176
|
-
|
|
177
|
-
|
|
1248
|
+
// Build directory structure
|
|
1249
|
+
const structure = {};
|
|
1250
|
+
for (const file of filesWithMeta) {
|
|
1251
|
+
const parts = file.relativePath.split(path.sep);
|
|
178
1252
|
let currentLevel = structure;
|
|
179
|
-
parts.
|
|
180
|
-
const
|
|
1253
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1254
|
+
const part = parts[i];
|
|
1255
|
+
const isFile = i === parts.length - 1;
|
|
181
1256
|
if (isFile) {
|
|
182
|
-
|
|
183
|
-
skipContentSet && skipContentSet.has(relativePath);
|
|
184
|
-
currentLevel[part] = {
|
|
185
|
-
size: formattedSize,
|
|
186
|
-
skipContent: shouldSkipContent,
|
|
187
|
-
};
|
|
1257
|
+
currentLevel[part] = { __file: file };
|
|
188
1258
|
} else {
|
|
189
|
-
if (!currentLevel[part]) {
|
|
190
|
-
currentLevel[part] = {};
|
|
191
|
-
}
|
|
1259
|
+
if (!currentLevel[part]) currentLevel[part] = {};
|
|
192
1260
|
currentLevel = currentLevel[part];
|
|
193
1261
|
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
196
1264
|
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
const
|
|
1265
|
+
function renderTree(level, prefix) {
|
|
1266
|
+
const keys = Object.keys(level);
|
|
1267
|
+
for (let i = 0; i < keys.length; i++) {
|
|
1268
|
+
const key = keys[i];
|
|
1269
|
+
const isLast = i === keys.length - 1;
|
|
1270
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251c\u2500\u2500 ";
|
|
1271
|
+
const childPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
1272
|
+
const value = level[key];
|
|
204
1273
|
|
|
205
|
-
if (
|
|
206
|
-
const
|
|
207
|
-
|
|
1274
|
+
if (value.__file) {
|
|
1275
|
+
const f = value.__file;
|
|
1276
|
+
const olRange = `OL: 1-${f.lineCount}`;
|
|
1277
|
+
const mlRange = `ML: ${f.mlStart}-${f.mlEnd}`;
|
|
1278
|
+
const sizeStr = f.formattedSize;
|
|
1279
|
+
const isSkipped = skipContentSet && skipContentSet.has(f.relativePath);
|
|
1280
|
+
|
|
1281
|
+
tree += `${prefix}${connector}${key} [${olRange} | ${mlRange} | ${sizeStr}]\n`;
|
|
1282
|
+
|
|
1283
|
+
if (isSkipped) {
|
|
1284
|
+
tree += `${childPrefix}(Content omitted - file size: ${sizeStr})\n`;
|
|
1285
|
+
} else if (!noParse && f.codeElements && f.codeElements.length > 0) {
|
|
1286
|
+
renderCodeElements(f.codeElements, childPrefix, f.mlStart);
|
|
1287
|
+
}
|
|
208
1288
|
} else {
|
|
209
|
-
|
|
210
|
-
|
|
1289
|
+
// Directory
|
|
1290
|
+
tree += `${prefix}${connector}${key}/\n`;
|
|
1291
|
+
renderTree(value, childPrefix);
|
|
211
1292
|
}
|
|
212
|
-
}
|
|
213
|
-
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function renderCodeElements(elements, prefix, mlOffset) {
|
|
1297
|
+
for (let i = 0; i < elements.length; i++) {
|
|
1298
|
+
const el = elements[i];
|
|
1299
|
+
const isLast = i === elements.length - 1;
|
|
1300
|
+
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251c\u2500\u2500 ";
|
|
1301
|
+
const childPrefix = prefix + (isLast ? " " : "\u2502 ");
|
|
1302
|
+
|
|
1303
|
+
const olRange = `OL: ${el.startLine}-${el.endLine}`;
|
|
1304
|
+
const mlStart = mlOffset + el.startLine - 1;
|
|
1305
|
+
const mlEnd = mlOffset + el.endLine - 1;
|
|
1306
|
+
const mlRange = `ML: ${mlStart}-${mlEnd}`;
|
|
1307
|
+
const sizeStr = formatBytes(el.size);
|
|
1308
|
+
|
|
1309
|
+
tree += `${prefix}${connector}${el.label} [${olRange} | ${mlRange} | ${sizeStr}]\n`;
|
|
1310
|
+
|
|
1311
|
+
if (el.children && el.children.length > 0) {
|
|
1312
|
+
renderCodeElements(el.children, childPrefix, mlOffset);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
214
1316
|
|
|
215
|
-
|
|
1317
|
+
renderTree(structure, "");
|
|
216
1318
|
return tree;
|
|
217
1319
|
}
|
|
218
1320
|
|
|
1321
|
+
// ---------------------------------------------------------------------------
|
|
1322
|
+
// Recreate functionality (v2.0.0)
|
|
1323
|
+
// ---------------------------------------------------------------------------
|
|
1324
|
+
|
|
1325
|
+
function recreateFromFile(inputFile, outputDir, dryRun, overwrite) {
|
|
1326
|
+
if (!fs.existsSync(inputFile)) {
|
|
1327
|
+
console.error(`\n\u274c Input file not found: ${inputFile}`);
|
|
1328
|
+
process.exit(1);
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
const content = fs.readFileSync(inputFile, "utf8");
|
|
1332
|
+
|
|
1333
|
+
// Parse files from the merged_code section (or legacy format)
|
|
1334
|
+
const files = [];
|
|
1335
|
+
// Match: # FILE: path [...] followed by ```` block
|
|
1336
|
+
const fileRegex = /# FILE:\s*(.+?)\s*\[.*?\]\n````\n([\s\S]*?)\n````/g;
|
|
1337
|
+
let match;
|
|
1338
|
+
|
|
1339
|
+
while ((match = fileRegex.exec(content)) !== null) {
|
|
1340
|
+
const filePath = match[1].trim();
|
|
1341
|
+
const fileContent = match[2];
|
|
1342
|
+
|
|
1343
|
+
// Skip content-omitted files
|
|
1344
|
+
if (fileContent.trim().startsWith("(Content omitted")) continue;
|
|
1345
|
+
|
|
1346
|
+
files.push({ path: filePath, content: fileContent });
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (files.length === 0) {
|
|
1350
|
+
// Try legacy format: ### **FILE:** `path`
|
|
1351
|
+
const legacyRegex = /### \*\*FILE:\*\*\s*`(.+?)`\n````\n([\s\S]*?)\n````/g;
|
|
1352
|
+
while ((match = legacyRegex.exec(content)) !== null) {
|
|
1353
|
+
const filePath = match[1].trim();
|
|
1354
|
+
const fileContent = match[2];
|
|
1355
|
+
if (fileContent.trim().startsWith("(Content omitted")) continue;
|
|
1356
|
+
files.push({ path: filePath, content: fileContent });
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
if (files.length === 0) {
|
|
1361
|
+
console.error("\n\u274c No files found in the input file.");
|
|
1362
|
+
process.exit(1);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
const resolvedOutputDir = path.resolve(outputDir);
|
|
1366
|
+
console.log(`\n\ud83d\udcc2 Output directory: ${resolvedOutputDir}\n`);
|
|
1367
|
+
|
|
1368
|
+
let totalSize = 0;
|
|
1369
|
+
|
|
1370
|
+
for (const file of files) {
|
|
1371
|
+
const fullPath = path.join(resolvedOutputDir, file.path);
|
|
1372
|
+
const size = Buffer.byteLength(file.content, "utf8");
|
|
1373
|
+
totalSize += size;
|
|
1374
|
+
|
|
1375
|
+
console.log(` ${file.path} (${formatBytes(size)})`);
|
|
1376
|
+
|
|
1377
|
+
if (!dryRun) {
|
|
1378
|
+
if (fs.existsSync(fullPath) && !overwrite) {
|
|
1379
|
+
console.error(` \u26a0\ufe0f Skipped (exists): ${file.path}`);
|
|
1380
|
+
continue;
|
|
1381
|
+
}
|
|
1382
|
+
const dir = path.dirname(fullPath);
|
|
1383
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
1384
|
+
fs.writeFileSync(fullPath, file.content, "utf8");
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
console.log(`\n\ud83d\udcca Summary:`);
|
|
1389
|
+
console.log(
|
|
1390
|
+
` \u2022 ${dryRun ? "Files to recreate" : "Files recreated"}: ${files.length}`,
|
|
1391
|
+
);
|
|
1392
|
+
console.log(` \u2022 Total size: ${formatBytes(totalSize)}`);
|
|
1393
|
+
console.log(`\n\u2705 Done!`);
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// ---------------------------------------------------------------------------
|
|
1397
|
+
// Main
|
|
1398
|
+
// ---------------------------------------------------------------------------
|
|
1399
|
+
|
|
219
1400
|
async function main() {
|
|
220
1401
|
const rawArgv = hideBin(process.argv);
|
|
221
1402
|
if (rawArgv.includes("--version") || rawArgv.includes("-v")) {
|
|
@@ -228,13 +1409,13 @@ async function main() {
|
|
|
228
1409
|
.usage("$0 [options]")
|
|
229
1410
|
.option("o", {
|
|
230
1411
|
alias: "output",
|
|
231
|
-
describe: "Output file
|
|
1412
|
+
describe: "Output file (combine) or directory (recreate)",
|
|
232
1413
|
type: "string",
|
|
233
1414
|
default: "combicode.txt",
|
|
234
1415
|
})
|
|
235
1416
|
.option("d", {
|
|
236
1417
|
alias: "dry-run",
|
|
237
|
-
describe: "Preview
|
|
1418
|
+
describe: "Preview without making changes",
|
|
238
1419
|
type: "boolean",
|
|
239
1420
|
default: false,
|
|
240
1421
|
})
|
|
@@ -254,41 +1435,81 @@ async function main() {
|
|
|
254
1435
|
type: "boolean",
|
|
255
1436
|
default: false,
|
|
256
1437
|
})
|
|
257
|
-
.option("
|
|
258
|
-
describe: "
|
|
1438
|
+
.option("gitignore", {
|
|
1439
|
+
describe: "Use patterns from the project's .gitignore file",
|
|
259
1440
|
type: "boolean",
|
|
260
|
-
default:
|
|
1441
|
+
default: true,
|
|
261
1442
|
})
|
|
262
|
-
.option("
|
|
263
|
-
describe: "
|
|
1443
|
+
.option("header", {
|
|
1444
|
+
describe: "Include the introductory prompt and code index in the output",
|
|
264
1445
|
type: "boolean",
|
|
265
|
-
default:
|
|
1446
|
+
default: true,
|
|
266
1447
|
})
|
|
267
1448
|
.option("skip-content", {
|
|
268
1449
|
describe:
|
|
269
1450
|
"Comma-separated glob patterns for files to include in tree but omit content",
|
|
270
1451
|
type: "string",
|
|
271
1452
|
})
|
|
1453
|
+
.option("parse", {
|
|
1454
|
+
describe: "Enable code structure parsing",
|
|
1455
|
+
type: "boolean",
|
|
1456
|
+
default: true,
|
|
1457
|
+
})
|
|
1458
|
+
.option("r", {
|
|
1459
|
+
alias: "recreate",
|
|
1460
|
+
describe: "Recreate project from a combicode.txt file",
|
|
1461
|
+
type: "boolean",
|
|
1462
|
+
default: false,
|
|
1463
|
+
})
|
|
1464
|
+
.option("input", {
|
|
1465
|
+
describe: "Input combicode.txt file for recreate",
|
|
1466
|
+
type: "string",
|
|
1467
|
+
default: "combicode.txt",
|
|
1468
|
+
})
|
|
1469
|
+
.option("overwrite", {
|
|
1470
|
+
describe: "Overwrite existing files when recreating",
|
|
1471
|
+
type: "boolean",
|
|
1472
|
+
default: false,
|
|
1473
|
+
})
|
|
272
1474
|
.version(version)
|
|
273
1475
|
.alias("v", "version")
|
|
274
1476
|
.help()
|
|
275
1477
|
.alias("h", "help").argv;
|
|
276
1478
|
|
|
277
1479
|
const projectRoot = process.cwd();
|
|
278
|
-
console.log(`\
|
|
279
|
-
console.log(
|
|
1480
|
+
console.log(`\u2728 Combicode v${version}`);
|
|
1481
|
+
console.log(`\ud83d\udcc2 Root: ${projectRoot}`);
|
|
280
1482
|
|
|
281
|
-
|
|
1483
|
+
// --- Recreate mode ---
|
|
1484
|
+
if (argv.recreate) {
|
|
1485
|
+
const inputFile = path.resolve(projectRoot, argv.input);
|
|
1486
|
+
const outputDir =
|
|
1487
|
+
argv.output !== "combicode.txt" ? argv.output : projectRoot;
|
|
1488
|
+
recreateFromFile(inputFile, outputDir, argv.dryRun, argv.overwrite);
|
|
1489
|
+
return;
|
|
1490
|
+
}
|
|
282
1491
|
|
|
283
|
-
//
|
|
284
|
-
|
|
1492
|
+
// --- Combine mode ---
|
|
1493
|
+
const rootIgnoreManager = ignore();
|
|
285
1494
|
rootIgnoreManager.add(SAFETY_IGNORES);
|
|
286
1495
|
|
|
287
1496
|
if (argv.exclude) {
|
|
288
1497
|
rootIgnoreManager.add(argv.exclude.split(","));
|
|
289
1498
|
}
|
|
290
1499
|
|
|
291
|
-
//
|
|
1500
|
+
// Parse .gitmodules for submodule paths
|
|
1501
|
+
const gitModulesPath = path.join(projectRoot, ".gitmodules");
|
|
1502
|
+
if (fs.existsSync(gitModulesPath)) {
|
|
1503
|
+
try {
|
|
1504
|
+
const content = fs.readFileSync(gitModulesPath, "utf8");
|
|
1505
|
+
const lines = content.split(/\r?\n/);
|
|
1506
|
+
for (const line of lines) {
|
|
1507
|
+
const m = line.match(/^\s*path\s*=\s*(.+?)\s*$/);
|
|
1508
|
+
if (m) rootIgnoreManager.add([m[1]]);
|
|
1509
|
+
}
|
|
1510
|
+
} catch (e) {}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
292
1513
|
const skipContentManager = ignore();
|
|
293
1514
|
if (argv.skipContent) {
|
|
294
1515
|
skipContentManager.add(argv.skipContent.split(","));
|
|
@@ -300,30 +1521,26 @@ async function main() {
|
|
|
300
1521
|
? new Set(
|
|
301
1522
|
argv.includeExt
|
|
302
1523
|
.split(",")
|
|
303
|
-
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`))
|
|
1524
|
+
.map((ext) => (ext.startsWith(".") ? ext : `.${ext}`)),
|
|
304
1525
|
)
|
|
305
1526
|
: null;
|
|
306
1527
|
|
|
307
|
-
// Initialize the ignore chain with the root manager
|
|
308
1528
|
const ignoreChain = [{ manager: rootIgnoreManager, root: projectRoot }];
|
|
309
|
-
|
|
310
|
-
// Statistics container
|
|
311
1529
|
const stats = { scanned: 0, ignored: 0 };
|
|
312
1530
|
|
|
313
|
-
// Perform Recursive Walk
|
|
314
1531
|
const includedFiles = walkDirectory(
|
|
315
1532
|
projectRoot,
|
|
316
1533
|
projectRoot,
|
|
317
1534
|
ignoreChain,
|
|
318
1535
|
allowedExtensions,
|
|
319
1536
|
absoluteOutputPath,
|
|
320
|
-
|
|
321
|
-
stats
|
|
1537
|
+
argv.gitignore,
|
|
1538
|
+
stats,
|
|
322
1539
|
);
|
|
323
1540
|
|
|
324
1541
|
includedFiles.sort((a, b) => a.path.localeCompare(b.path));
|
|
325
1542
|
|
|
326
|
-
// Determine
|
|
1543
|
+
// Determine skip-content set
|
|
327
1544
|
const skipContentSet = new Set();
|
|
328
1545
|
if (argv.skipContent) {
|
|
329
1546
|
includedFiles.forEach((file) => {
|
|
@@ -334,96 +1551,219 @@ async function main() {
|
|
|
334
1551
|
});
|
|
335
1552
|
}
|
|
336
1553
|
|
|
337
|
-
// Calculate total size of included files (excluding files with skipped content)
|
|
338
|
-
const totalSizeBytes = includedFiles.reduce((acc, file) => {
|
|
339
|
-
// Don't count files with skipped content in the total size
|
|
340
|
-
if (skipContentSet.has(file.relativePath)) {
|
|
341
|
-
return acc;
|
|
342
|
-
}
|
|
343
|
-
return acc + file.size;
|
|
344
|
-
}, 0);
|
|
345
|
-
|
|
346
1554
|
if (includedFiles.length === 0) {
|
|
347
1555
|
console.error(
|
|
348
|
-
"\n
|
|
1556
|
+
"\n\u274c No files to include. Check your path, .gitignore, or filters.",
|
|
349
1557
|
);
|
|
350
1558
|
process.exit(1);
|
|
351
1559
|
}
|
|
352
1560
|
|
|
1561
|
+
// --- Read file contents & parse code structure ---
|
|
1562
|
+
for (const fileObj of includedFiles) {
|
|
1563
|
+
const isSkipped = skipContentSet.has(fileObj.relativePath);
|
|
1564
|
+
if (isSkipped) {
|
|
1565
|
+
fileObj.content = null;
|
|
1566
|
+
fileObj.lineCount = 0;
|
|
1567
|
+
fileObj.codeElements = [];
|
|
1568
|
+
} else {
|
|
1569
|
+
try {
|
|
1570
|
+
fileObj.content = fs.readFileSync(fileObj.path, "utf8");
|
|
1571
|
+
// Count actual lines: strings ending with \n get an extra empty element from split
|
|
1572
|
+
const parts = fileObj.content.split("\n");
|
|
1573
|
+
fileObj.lineCount = fileObj.content.endsWith("\n")
|
|
1574
|
+
? parts.length - 1
|
|
1575
|
+
: parts.length;
|
|
1576
|
+
|
|
1577
|
+
if (argv.parse) {
|
|
1578
|
+
const flat = parseCodeStructure(
|
|
1579
|
+
fileObj.relativePath,
|
|
1580
|
+
fileObj.content,
|
|
1581
|
+
);
|
|
1582
|
+
fileObj.codeElements = nestElements(flat);
|
|
1583
|
+
} else {
|
|
1584
|
+
fileObj.codeElements = [];
|
|
1585
|
+
}
|
|
1586
|
+
} catch (e) {
|
|
1587
|
+
fileObj.content = `... (error reading file: ${e.message}) ...`;
|
|
1588
|
+
fileObj.lineCount = 1;
|
|
1589
|
+
fileObj.codeElements = [];
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// --- Calculate ML (Merged Line) offsets ---
|
|
1595
|
+
// First pass: figure out how many header lines come before merged_code
|
|
1596
|
+
const systemPrompt = argv.llmsTxt
|
|
1597
|
+
? LLMS_TXT_SYSTEM_PROMPT
|
|
1598
|
+
: DEFAULT_SYSTEM_PROMPT;
|
|
1599
|
+
let headerLines = 0;
|
|
1600
|
+
if (argv.header) {
|
|
1601
|
+
headerLines += systemPrompt.split("\n").length; // system prompt
|
|
1602
|
+
headerLines += 1; // blank line after prompt
|
|
1603
|
+
// <code_index> section will be added but we need to calculate it after ML offsets are known
|
|
1604
|
+
// We'll do a two-pass approach
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Two-pass: first compute code_index size, then compute ML offsets
|
|
1608
|
+
// Pass 1: Assign provisional ML offsets (we'll need the code_index line count first)
|
|
1609
|
+
// The structure is: header + code_index_block + merged_code_block
|
|
1610
|
+
|
|
1611
|
+
// Compute provisional line count for each file in merged_code
|
|
1612
|
+
// Each file: "# FILE: path [...]\n````\n" + content + "\n````\n" = 4 lines + content lines
|
|
1613
|
+
let mergedCodeHeaderLine = 1; // Will be set properly
|
|
1614
|
+
let currentMl = 1;
|
|
1615
|
+
|
|
1616
|
+
// We need to do this iteratively:
|
|
1617
|
+
// 1. Calculate code_index with placeholder MLs
|
|
1618
|
+
// 2. Count code_index lines
|
|
1619
|
+
// 3. Calculate real MLs
|
|
1620
|
+
// 4. Regenerate code_index with real MLs
|
|
1621
|
+
|
|
1622
|
+
// Step 1: Assign temporary ML values
|
|
1623
|
+
let tempMl = 1; // placeholder
|
|
1624
|
+
for (const fileObj of includedFiles) {
|
|
1625
|
+
fileObj.mlStart = tempMl;
|
|
1626
|
+
const isSkipped = skipContentSet.has(fileObj.relativePath);
|
|
1627
|
+
if (isSkipped) {
|
|
1628
|
+
fileObj.mlEnd = tempMl + 1; // placeholder line
|
|
1629
|
+
tempMl += 4 + 1; // header(2) + placeholder(1) + footer(1) = 4
|
|
1630
|
+
} else {
|
|
1631
|
+
fileObj.mlEnd = tempMl + fileObj.lineCount - 1;
|
|
1632
|
+
tempMl += 4 + fileObj.lineCount; // header(2) + content + footer(2)
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
// Step 2: Generate provisional code_index to count lines
|
|
1637
|
+
let codeIndex = generateCodeIndex(
|
|
1638
|
+
includedFiles,
|
|
1639
|
+
projectRoot,
|
|
1640
|
+
skipContentSet,
|
|
1641
|
+
!argv.parse,
|
|
1642
|
+
);
|
|
1643
|
+
// codeIndex ends with \n, so split produces an extra empty element
|
|
1644
|
+
const codeIndexLineCount = codeIndex.endsWith("\n")
|
|
1645
|
+
? codeIndex.split("\n").length - 1
|
|
1646
|
+
: codeIndex.split("\n").length;
|
|
1647
|
+
|
|
1648
|
+
// Step 3: Calculate real header line count
|
|
1649
|
+
let totalHeaderLines = 0;
|
|
1650
|
+
if (argv.header) {
|
|
1651
|
+
totalHeaderLines += systemPrompt.split("\n").length; // prompt lines + blank line from extra \n
|
|
1652
|
+
totalHeaderLines += 1; // "<code_index>"
|
|
1653
|
+
totalHeaderLines += codeIndexLineCount; // code index content
|
|
1654
|
+
totalHeaderLines += 1; // "</code_index>"
|
|
1655
|
+
totalHeaderLines += 1; // blank line
|
|
1656
|
+
totalHeaderLines += 1; // "<merged_code>"
|
|
1657
|
+
} else {
|
|
1658
|
+
totalHeaderLines += 1; // "<merged_code>"
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
// Step 4: Recalculate ML offsets with real header
|
|
1662
|
+
currentMl = totalHeaderLines + 1;
|
|
1663
|
+
for (const fileObj of includedFiles) {
|
|
1664
|
+
const isSkipped = skipContentSet.has(fileObj.relativePath);
|
|
1665
|
+
// File header: "# FILE: path [...]\n````\n" = 2 lines
|
|
1666
|
+
fileObj.mlStart = currentMl + 2; // Content starts after header
|
|
1667
|
+
if (isSkipped) {
|
|
1668
|
+
fileObj.mlEnd = fileObj.mlStart; // 1 line placeholder
|
|
1669
|
+
currentMl += 2 + 1 + 2; // header(2) + placeholder(1) + footer(2: "\n````\n")
|
|
1670
|
+
} else {
|
|
1671
|
+
fileObj.mlEnd = fileObj.mlStart + fileObj.lineCount - 1;
|
|
1672
|
+
currentMl += 2 + fileObj.lineCount + 2; // header(2) + content + footer(2)
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
// Step 5: Regenerate code_index with correct MLs
|
|
1677
|
+
codeIndex = generateCodeIndex(
|
|
1678
|
+
includedFiles,
|
|
1679
|
+
projectRoot,
|
|
1680
|
+
skipContentSet,
|
|
1681
|
+
!argv.parse,
|
|
1682
|
+
);
|
|
1683
|
+
|
|
1684
|
+
// Calculate total content size
|
|
1685
|
+
const totalSizeBytes = includedFiles.reduce((acc, file) => {
|
|
1686
|
+
if (skipContentSet.has(file.relativePath)) return acc;
|
|
1687
|
+
return acc + file.size;
|
|
1688
|
+
}, 0);
|
|
1689
|
+
|
|
1690
|
+
// --- Dry run ---
|
|
353
1691
|
if (argv.dryRun) {
|
|
354
|
-
console.log("\n
|
|
355
|
-
|
|
356
|
-
console.log(
|
|
357
|
-
console.log(
|
|
358
|
-
console.log(
|
|
359
|
-
` ⢠Included: ${includedFiles.length} files (${formatBytes(
|
|
360
|
-
totalSizeBytes
|
|
361
|
-
)})`
|
|
362
|
-
);
|
|
1692
|
+
console.log("\n\ud83d\udccb Files to include (dry run):\n");
|
|
1693
|
+
console.log(codeIndex);
|
|
1694
|
+
console.log(`\n\ud83d\udcca Summary:`);
|
|
1695
|
+
console.log(` \u2022 Total files: ${includedFiles.length}`);
|
|
1696
|
+
console.log(` \u2022 Total size: ${formatBytes(totalSizeBytes)}`);
|
|
363
1697
|
if (skipContentSet.size > 0) {
|
|
364
|
-
console.log(`
|
|
1698
|
+
console.log(` \u2022 Content omitted: ${skipContentSet.size} files`);
|
|
365
1699
|
}
|
|
366
|
-
console.log(
|
|
1700
|
+
console.log(`\n\u2705 Done!`);
|
|
367
1701
|
return;
|
|
368
1702
|
}
|
|
369
1703
|
|
|
1704
|
+
// --- Write output ---
|
|
370
1705
|
const outputStream = fs.createWriteStream(argv.output);
|
|
371
1706
|
let totalLines = 0;
|
|
372
1707
|
|
|
373
|
-
if (
|
|
374
|
-
const systemPrompt = argv.llmsTxt
|
|
375
|
-
? LLMS_TXT_SYSTEM_PROMPT
|
|
376
|
-
: DEFAULT_SYSTEM_PROMPT;
|
|
1708
|
+
if (argv.header) {
|
|
377
1709
|
outputStream.write(systemPrompt + "\n");
|
|
378
|
-
totalLines += systemPrompt.split("\n").length;
|
|
1710
|
+
totalLines += systemPrompt.split("\n").length + 1;
|
|
379
1711
|
|
|
380
|
-
outputStream.write("
|
|
381
|
-
outputStream.write(
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
outputStream.write("```\n\n");
|
|
385
|
-
outputStream.write("---\n\n");
|
|
1712
|
+
outputStream.write("<code_index>\n");
|
|
1713
|
+
outputStream.write(codeIndex);
|
|
1714
|
+
outputStream.write("</code_index>\n\n");
|
|
1715
|
+
totalLines += codeIndexLineCount + 3;
|
|
386
1716
|
|
|
387
|
-
|
|
1717
|
+
outputStream.write("<merged_code>\n");
|
|
1718
|
+
totalLines += 1;
|
|
1719
|
+
} else {
|
|
1720
|
+
outputStream.write("<merged_code>\n");
|
|
1721
|
+
totalLines += 1;
|
|
388
1722
|
}
|
|
389
1723
|
|
|
390
1724
|
for (const fileObj of includedFiles) {
|
|
391
1725
|
const relativePath = fileObj.relativePath.replace(/\\/g, "/");
|
|
392
|
-
const
|
|
1726
|
+
const isSkipped = skipContentSet.has(fileObj.relativePath);
|
|
1727
|
+
|
|
1728
|
+
const olRange = `OL: 1-${fileObj.lineCount}`;
|
|
1729
|
+
const mlRange = `ML: ${fileObj.mlStart}-${fileObj.mlEnd}`;
|
|
1730
|
+
const sizeStr = fileObj.formattedSize;
|
|
393
1731
|
|
|
394
|
-
outputStream.write(
|
|
1732
|
+
outputStream.write(
|
|
1733
|
+
`# FILE: ${relativePath} [${olRange} | ${mlRange} | ${sizeStr}]\n`,
|
|
1734
|
+
);
|
|
395
1735
|
outputStream.write("````\n");
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
);
|
|
1736
|
+
totalLines += 2;
|
|
1737
|
+
|
|
1738
|
+
if (isSkipped) {
|
|
1739
|
+
outputStream.write(`(Content omitted - file size: ${sizeStr})\n`);
|
|
400
1740
|
totalLines += 1;
|
|
401
1741
|
} else {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
outputStream.write(
|
|
405
|
-
totalLines += content.split("\n").length;
|
|
406
|
-
} catch (e) {
|
|
407
|
-
outputStream.write(`... (error reading file: ${e.message}) ...`);
|
|
1742
|
+
outputStream.write(fileObj.content);
|
|
1743
|
+
if (!fileObj.content.endsWith("\n")) {
|
|
1744
|
+
outputStream.write("\n");
|
|
408
1745
|
}
|
|
1746
|
+
totalLines += fileObj.lineCount;
|
|
409
1747
|
}
|
|
410
|
-
|
|
411
|
-
|
|
1748
|
+
|
|
1749
|
+
outputStream.write("````\n\n");
|
|
1750
|
+
totalLines += 2;
|
|
412
1751
|
}
|
|
1752
|
+
|
|
1753
|
+
outputStream.write("</merged_code>\n");
|
|
1754
|
+
totalLines += 1;
|
|
413
1755
|
outputStream.end();
|
|
414
1756
|
|
|
415
|
-
console.log(`\n
|
|
1757
|
+
console.log(`\n\ud83d\udcca Summary:`);
|
|
416
1758
|
console.log(
|
|
417
|
-
`
|
|
418
|
-
totalSizeBytes
|
|
419
|
-
)})`
|
|
1759
|
+
` \u2022 Included: ${includedFiles.length} files (${formatBytes(totalSizeBytes)})`,
|
|
420
1760
|
);
|
|
421
1761
|
if (skipContentSet.size > 0) {
|
|
422
|
-
console.log(`
|
|
1762
|
+
console.log(` \u2022 Content omitted: ${skipContentSet.size} files`);
|
|
423
1763
|
}
|
|
424
|
-
console.log(`
|
|
425
|
-
console.log(`
|
|
426
|
-
console.log(`\n
|
|
1764
|
+
console.log(` \u2022 Ignored: ${stats.ignored} files/dirs`);
|
|
1765
|
+
console.log(` \u2022 Output: ${argv.output} (~${totalLines} lines)`);
|
|
1766
|
+
console.log(`\n\u2705 Done!`);
|
|
427
1767
|
}
|
|
428
1768
|
|
|
429
1769
|
main().catch((err) => {
|