vg-coder-cli 1.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/README.md +179 -0
- package/bin/vg-coder.js +11 -0
- package/package.json +64 -0
- package/src/detectors/project-detector.js +333 -0
- package/src/exporter/html-exporter.js +1026 -0
- package/src/ignore/ignore-manager.js +298 -0
- package/src/index.js +282 -0
- package/src/scanner/file-scanner.js +592 -0
- package/src/tokenizer/token-manager.js +389 -0
- package/src/utils/helpers.js +128 -0
- package/test-project/package.json +21 -0
- package/test-project/src/controllers/userController.js +129 -0
- package/test-project/src/index.js +46 -0
- package/test-project/src/middleware/auth.js +142 -0
- package/test-project/styles/main.css +287 -0
- package/vg-coder-cli-1.0.0.tgz +0 -0
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const dirTree = require('directory-tree');
|
|
4
|
+
const IgnoreManager = require('../ignore/ignore-manager');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Scanner để duyệt và nối file mã nguồn
|
|
8
|
+
*/
|
|
9
|
+
class FileScanner {
|
|
10
|
+
constructor(projectPath, options = {}) {
|
|
11
|
+
this.projectPath = projectPath;
|
|
12
|
+
this.options = {
|
|
13
|
+
maxFileSize: options.maxFileSize || 1024 * 1024, // 1MB default
|
|
14
|
+
extensions: options.extensions || this.getDefaultExtensions(),
|
|
15
|
+
includeHidden: options.includeHidden || false,
|
|
16
|
+
maxDepth: options.maxDepth || 10,
|
|
17
|
+
...options
|
|
18
|
+
};
|
|
19
|
+
this.ignoreManager = new IgnoreManager(projectPath);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Lấy danh sách extensions mặc định
|
|
24
|
+
*/
|
|
25
|
+
getDefaultExtensions() {
|
|
26
|
+
return [
|
|
27
|
+
// Web Frontend
|
|
28
|
+
'.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte',
|
|
29
|
+
'.html', '.htm', '.css', '.scss', '.sass', '.less',
|
|
30
|
+
|
|
31
|
+
// Backend
|
|
32
|
+
'.java', '.kt', '.scala', '.groovy',
|
|
33
|
+
'.py', '.rb', '.php', '.go', '.rs',
|
|
34
|
+
'.cs', '.vb', '.fs',
|
|
35
|
+
'.cpp', '.c', '.h', '.hpp',
|
|
36
|
+
|
|
37
|
+
// Config files
|
|
38
|
+
'.json', '.yaml', '.yml', '.toml', '.ini', '.conf',
|
|
39
|
+
'.xml', '.properties',
|
|
40
|
+
|
|
41
|
+
// Build files
|
|
42
|
+
'.gradle', '.maven', '.sbt',
|
|
43
|
+
|
|
44
|
+
// Scripts
|
|
45
|
+
'.sh', '.bat', '.ps1', '.cmd',
|
|
46
|
+
|
|
47
|
+
// Documentation
|
|
48
|
+
'.md', '.txt', '.rst',
|
|
49
|
+
|
|
50
|
+
// SQL
|
|
51
|
+
'.sql', '.ddl', '.dml'
|
|
52
|
+
];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Scan toàn bộ project và trả về cấu trúc file
|
|
57
|
+
*/
|
|
58
|
+
async scanProject() {
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Lấy cấu trúc thư mục
|
|
63
|
+
const tree = await this.buildFileTree();
|
|
64
|
+
|
|
65
|
+
// Lấy danh sách files
|
|
66
|
+
const files = await this.extractFiles(tree);
|
|
67
|
+
|
|
68
|
+
// Lọc files theo ignore rules
|
|
69
|
+
const filteredFiles = await this.filterFiles(files);
|
|
70
|
+
|
|
71
|
+
// Đọc nội dung files
|
|
72
|
+
const filesWithContent = await this.readFilesContent(filteredFiles);
|
|
73
|
+
|
|
74
|
+
const endTime = Date.now();
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
tree,
|
|
78
|
+
files: filesWithContent,
|
|
79
|
+
stats: {
|
|
80
|
+
totalFiles: files.length,
|
|
81
|
+
filteredFiles: filteredFiles.length,
|
|
82
|
+
processedFiles: filesWithContent.length,
|
|
83
|
+
scanTime: endTime - startTime,
|
|
84
|
+
projectPath: this.projectPath
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw new Error(`Failed to scan project: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Xây dựng cây thư mục
|
|
94
|
+
*/
|
|
95
|
+
async buildFileTree() {
|
|
96
|
+
// Use manual scanning instead of directory-tree to ensure all Java files are found
|
|
97
|
+
return await this.scanDirectoryManually(this.projectPath);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Manually scan directory to ensure all files are found
|
|
102
|
+
*/
|
|
103
|
+
async scanDirectoryManually(dirPath, relativePath = '') {
|
|
104
|
+
const stats = await fs.stat(dirPath);
|
|
105
|
+
const name = path.basename(dirPath);
|
|
106
|
+
|
|
107
|
+
const node = {
|
|
108
|
+
path: dirPath,
|
|
109
|
+
name: name,
|
|
110
|
+
size: stats.size,
|
|
111
|
+
type: stats.isDirectory() ? 'directory' : 'file',
|
|
112
|
+
extension: stats.isFile() ? path.extname(name) : undefined
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
if (stats.isDirectory()) {
|
|
116
|
+
try {
|
|
117
|
+
const entries = await fs.readdir(dirPath);
|
|
118
|
+
const children = [];
|
|
119
|
+
|
|
120
|
+
for (const entry of entries) {
|
|
121
|
+
const entryPath = path.join(dirPath, entry);
|
|
122
|
+
const entryRelativePath = path.join(relativePath, entry);
|
|
123
|
+
|
|
124
|
+
// Skip ignored directories
|
|
125
|
+
if (this.shouldIgnoreDirectoryName(entry)) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const childNode = await this.scanDirectoryManually(entryPath, entryRelativePath);
|
|
131
|
+
if (childNode) {
|
|
132
|
+
children.push(childNode);
|
|
133
|
+
}
|
|
134
|
+
} catch (error) {
|
|
135
|
+
// Skip files/directories that can't be accessed
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (children.length > 0) {
|
|
141
|
+
node.children = children;
|
|
142
|
+
}
|
|
143
|
+
} catch (error) {
|
|
144
|
+
// Skip directories that can't be read
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return node;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get regex for build directories and hidden files to ignore
|
|
153
|
+
*/
|
|
154
|
+
getBuildIgnoreRegex() {
|
|
155
|
+
// Create regex pattern for directories to ignore
|
|
156
|
+
const ignorePatterns = [
|
|
157
|
+
// Hidden directories (starting with .)
|
|
158
|
+
'^\\..*',
|
|
159
|
+
|
|
160
|
+
// Build directories
|
|
161
|
+
'^build$',
|
|
162
|
+
'^target$',
|
|
163
|
+
'^dist$',
|
|
164
|
+
'^out$',
|
|
165
|
+
'^bin$',
|
|
166
|
+
|
|
167
|
+
// Dependencies
|
|
168
|
+
'^node_modules$',
|
|
169
|
+
'^vendor$',
|
|
170
|
+
|
|
171
|
+
// Temporary files
|
|
172
|
+
'^tmp$',
|
|
173
|
+
'^temp$',
|
|
174
|
+
|
|
175
|
+
// Logs
|
|
176
|
+
'^logs$',
|
|
177
|
+
'^log$',
|
|
178
|
+
|
|
179
|
+
// Coverage reports
|
|
180
|
+
'^coverage$'
|
|
181
|
+
];
|
|
182
|
+
|
|
183
|
+
// Combine all patterns into one regex
|
|
184
|
+
const combinedPattern = ignorePatterns.join('|');
|
|
185
|
+
return new RegExp(`(${combinedPattern})`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Trích xuất danh sách files từ tree
|
|
194
|
+
*/
|
|
195
|
+
async extractFiles(tree) {
|
|
196
|
+
const files = [];
|
|
197
|
+
|
|
198
|
+
const traverse = (node, currentPath = '') => {
|
|
199
|
+
// Skip ignored directories
|
|
200
|
+
if (this.shouldIgnorePath(node.path || node.name)) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Nếu node có children thì là directory
|
|
205
|
+
if (node.children) {
|
|
206
|
+
node.children.forEach(child => traverse(child, currentPath));
|
|
207
|
+
} else {
|
|
208
|
+
// Nếu không có children thì là file
|
|
209
|
+
const relativePath = path.relative(this.projectPath, node.path);
|
|
210
|
+
|
|
211
|
+
// Skip files in ignored directories
|
|
212
|
+
if (!this.shouldIgnorePath(relativePath)) {
|
|
213
|
+
// Filter by extension
|
|
214
|
+
const extensions = this.options.extensions || this.getDefaultExtensions();
|
|
215
|
+
const hasValidExtension = extensions.some(ext =>
|
|
216
|
+
node.name.toLowerCase().endsWith(ext.toLowerCase())
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (hasValidExtension) {
|
|
220
|
+
files.push({
|
|
221
|
+
path: node.path,
|
|
222
|
+
relativePath: relativePath,
|
|
223
|
+
name: node.name,
|
|
224
|
+
extension: node.extension,
|
|
225
|
+
size: node.size
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
traverse(tree);
|
|
233
|
+
return files;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Check if path should be ignored
|
|
238
|
+
*/
|
|
239
|
+
shouldIgnorePath(filePath) {
|
|
240
|
+
const pathParts = filePath.split(path.sep);
|
|
241
|
+
|
|
242
|
+
// Check each part of the path
|
|
243
|
+
for (const part of pathParts) {
|
|
244
|
+
if (this.shouldIgnoreDirectoryName(part)) {
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Check if directory/file should be ignored
|
|
254
|
+
*/
|
|
255
|
+
shouldIgnoreDirectoryName(name) {
|
|
256
|
+
// List of directories/files to ignore
|
|
257
|
+
const ignoreList = [
|
|
258
|
+
// Hidden directories (starting with .)
|
|
259
|
+
'.git', '.svn', '.hg',
|
|
260
|
+
'.gradle', '.maven', '.cache', '.npm',
|
|
261
|
+
'.idea', '.vscode', '.eclipse',
|
|
262
|
+
'.DS_Store', '.tmp',
|
|
263
|
+
|
|
264
|
+
// Build directories
|
|
265
|
+
'build', 'target', 'dist', 'out', 'bin',
|
|
266
|
+
|
|
267
|
+
// Dependencies
|
|
268
|
+
'node_modules', 'vendor',
|
|
269
|
+
|
|
270
|
+
// Temporary files
|
|
271
|
+
'tmp', 'temp',
|
|
272
|
+
|
|
273
|
+
// Logs
|
|
274
|
+
'logs', 'log',
|
|
275
|
+
|
|
276
|
+
// Coverage reports
|
|
277
|
+
'coverage', '.nyc_output'
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
// Check if name starts with . (hidden files/dirs)
|
|
281
|
+
if (name.startsWith('.')) {
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Check if name is in ignore list
|
|
286
|
+
return ignoreList.includes(name);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Lọc files theo ignore rules
|
|
291
|
+
*/
|
|
292
|
+
async filterFiles(files) {
|
|
293
|
+
const filtered = [];
|
|
294
|
+
|
|
295
|
+
for (const file of files) {
|
|
296
|
+
// Kiểm tra size
|
|
297
|
+
if (file.size > this.options.maxFileSize) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Kiểm tra ignore rules
|
|
302
|
+
const shouldIgnore = await this.ignoreManager.shouldIgnore(file.relativePath);
|
|
303
|
+
if (!shouldIgnore) {
|
|
304
|
+
filtered.push(file);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return filtered;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Đọc nội dung files
|
|
313
|
+
*/
|
|
314
|
+
async readFilesContent(files) {
|
|
315
|
+
const filesWithContent = [];
|
|
316
|
+
|
|
317
|
+
for (const file of files) {
|
|
318
|
+
try {
|
|
319
|
+
const content = await this.readFileContent(file.path);
|
|
320
|
+
if (content !== null) {
|
|
321
|
+
filesWithContent.push({
|
|
322
|
+
...file,
|
|
323
|
+
content,
|
|
324
|
+
lines: content.split('\n').length,
|
|
325
|
+
encoding: 'utf8'
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
} catch (error) {
|
|
329
|
+
// Log error but continue with other files
|
|
330
|
+
console.warn(`Warning: Could not read file ${file.relativePath}: ${error.message}`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return filesWithContent;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Đọc nội dung một file
|
|
339
|
+
*/
|
|
340
|
+
async readFileContent(filePath) {
|
|
341
|
+
try {
|
|
342
|
+
// Kiểm tra xem file có phải binary không
|
|
343
|
+
if (await this.isBinaryFile(filePath)) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
348
|
+
return content;
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (error.code === 'ENOENT') {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
throw error;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Kiểm tra xem file có phải binary không
|
|
359
|
+
*/
|
|
360
|
+
async isBinaryFile(filePath) {
|
|
361
|
+
try {
|
|
362
|
+
const buffer = await fs.readFile(filePath);
|
|
363
|
+
|
|
364
|
+
// Kiểm tra 1024 bytes đầu
|
|
365
|
+
const chunk = buffer.slice(0, 1024);
|
|
366
|
+
|
|
367
|
+
// Nếu có null bytes thì có thể là binary
|
|
368
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
369
|
+
if (chunk[i] === 0) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return false;
|
|
375
|
+
} catch (error) {
|
|
376
|
+
return true; // Assume binary if can't read
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Tạo nội dung kết hợp từ tất cả files
|
|
382
|
+
*/
|
|
383
|
+
async createCombinedContent(files, options = {}) {
|
|
384
|
+
const {
|
|
385
|
+
includeStats = true,
|
|
386
|
+
includeTree = true,
|
|
387
|
+
headerTemplate = this.getDefaultHeaderTemplate(),
|
|
388
|
+
separatorTemplate = this.getDefaultSeparatorTemplate()
|
|
389
|
+
} = options;
|
|
390
|
+
|
|
391
|
+
let content = '';
|
|
392
|
+
|
|
393
|
+
// Header với thông tin project
|
|
394
|
+
if (includeStats) {
|
|
395
|
+
content += this.generateProjectHeader(files);
|
|
396
|
+
content += '\n\n';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Cấu trúc thư mục
|
|
400
|
+
if (includeTree) {
|
|
401
|
+
content += this.generateTreeStructure(files);
|
|
402
|
+
content += '\n\n';
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Nội dung từng file
|
|
406
|
+
for (let i = 0; i < files.length; i++) {
|
|
407
|
+
const file = files[i];
|
|
408
|
+
|
|
409
|
+
// Header cho file
|
|
410
|
+
content += headerTemplate
|
|
411
|
+
.replace('{path}', file.relativePath)
|
|
412
|
+
.replace('{name}', file.name)
|
|
413
|
+
.replace('{extension}', file.extension || '')
|
|
414
|
+
.replace('{size}', file.size)
|
|
415
|
+
.replace('{lines}', file.lines);
|
|
416
|
+
|
|
417
|
+
content += '\n';
|
|
418
|
+
content += file.content;
|
|
419
|
+
content += '\n';
|
|
420
|
+
|
|
421
|
+
// Separator giữa các files
|
|
422
|
+
if (i < files.length - 1) {
|
|
423
|
+
content += separatorTemplate;
|
|
424
|
+
content += '\n';
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return content;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Template header mặc định cho file
|
|
433
|
+
*/
|
|
434
|
+
getDefaultHeaderTemplate() {
|
|
435
|
+
return `
|
|
436
|
+
================================================================================
|
|
437
|
+
File: {path}
|
|
438
|
+
Size: {size} bytes | Lines: {lines}
|
|
439
|
+
================================================================================`;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Template separator mặc định
|
|
444
|
+
*/
|
|
445
|
+
getDefaultSeparatorTemplate() {
|
|
446
|
+
return '\n\n';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Tạo header thông tin project
|
|
451
|
+
*/
|
|
452
|
+
generateProjectHeader(files) {
|
|
453
|
+
const totalSize = files.reduce((sum, file) => sum + file.size, 0);
|
|
454
|
+
const totalLines = files.reduce((sum, file) => sum + file.lines, 0);
|
|
455
|
+
|
|
456
|
+
return `
|
|
457
|
+
# Project Analysis Report
|
|
458
|
+
Generated: ${new Date().toISOString()}
|
|
459
|
+
Project Path: ${this.projectPath}
|
|
460
|
+
|
|
461
|
+
## Statistics
|
|
462
|
+
- Total Files: ${files.length}
|
|
463
|
+
- Total Size: ${this.formatBytes(totalSize)}
|
|
464
|
+
- Total Lines: ${totalLines.toLocaleString()}
|
|
465
|
+
- Extensions: ${[...new Set(files.map(f => f.extension))].filter(Boolean).join(', ')}
|
|
466
|
+
`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Tạo cấu trúc thư mục
|
|
471
|
+
*/
|
|
472
|
+
generateTreeStructure(files) {
|
|
473
|
+
const tree = {};
|
|
474
|
+
|
|
475
|
+
// Build tree structure
|
|
476
|
+
files.forEach(file => {
|
|
477
|
+
const parts = file.relativePath.split(path.sep);
|
|
478
|
+
let current = tree;
|
|
479
|
+
|
|
480
|
+
parts.forEach((part, index) => {
|
|
481
|
+
if (index === parts.length - 1) {
|
|
482
|
+
// File
|
|
483
|
+
current[part] = file;
|
|
484
|
+
} else {
|
|
485
|
+
// Directory
|
|
486
|
+
if (!current[part]) {
|
|
487
|
+
current[part] = {};
|
|
488
|
+
}
|
|
489
|
+
current = current[part];
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
// Generate tree string
|
|
495
|
+
let treeStr = '\n## Project Structure\n```\n';
|
|
496
|
+
treeStr += this.renderTree(tree, '', true);
|
|
497
|
+
treeStr += '```\n';
|
|
498
|
+
|
|
499
|
+
return treeStr;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Render tree structure
|
|
504
|
+
*/
|
|
505
|
+
renderTree(node, prefix = '', isLast = true) {
|
|
506
|
+
let result = '';
|
|
507
|
+
const entries = Object.entries(node);
|
|
508
|
+
|
|
509
|
+
entries.forEach(([name, value], index) => {
|
|
510
|
+
const isLastEntry = index === entries.length - 1;
|
|
511
|
+
const connector = isLastEntry ? '└── ' : '├── ';
|
|
512
|
+
|
|
513
|
+
result += prefix + connector + name + '\n';
|
|
514
|
+
|
|
515
|
+
if (typeof value === 'object' && !value.relativePath) {
|
|
516
|
+
// Directory
|
|
517
|
+
const newPrefix = prefix + (isLastEntry ? ' ' : '│ ');
|
|
518
|
+
result += this.renderTree(value, newPrefix, isLastEntry);
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
return result;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Render project tree structure
|
|
527
|
+
*/
|
|
528
|
+
renderProjectTree(tree, maxDepth = 50) {
|
|
529
|
+
if (!tree) return 'No tree structure available';
|
|
530
|
+
|
|
531
|
+
const renderNode = (node, prefix = '', depth = 0) => {
|
|
532
|
+
if (depth > maxDepth) return '';
|
|
533
|
+
|
|
534
|
+
let result = '';
|
|
535
|
+
|
|
536
|
+
if (node.children && node.children.length > 0) {
|
|
537
|
+
// Filter out ignored directories
|
|
538
|
+
const filteredChildren = node.children.filter(child =>
|
|
539
|
+
!this.shouldIgnoreDirectoryName(child.name)
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
// Sort children: directories first, then files
|
|
543
|
+
filteredChildren.sort((a, b) => {
|
|
544
|
+
const aIsDir = a.children && a.children.length > 0;
|
|
545
|
+
const bIsDir = b.children && b.children.length > 0;
|
|
546
|
+
if (aIsDir && !bIsDir) return -1;
|
|
547
|
+
if (!aIsDir && bIsDir) return 1;
|
|
548
|
+
return a.name.localeCompare(b.name);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
filteredChildren.forEach((child, index) => {
|
|
552
|
+
const isLast = index === filteredChildren.length - 1;
|
|
553
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
554
|
+
const nextPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
555
|
+
const isDirectory = child.children !== undefined;
|
|
556
|
+
|
|
557
|
+
result += `${prefix}${connector}${child.name}`;
|
|
558
|
+
if (!isDirectory) {
|
|
559
|
+
// It's a file, show extension
|
|
560
|
+
result += ` (${child.extension || 'file'})`;
|
|
561
|
+
}
|
|
562
|
+
result += '\n';
|
|
563
|
+
|
|
564
|
+
if (isDirectory) {
|
|
565
|
+
// Always render all children, no depth limit for full structure
|
|
566
|
+
result += renderNode(child, nextPrefix, depth + 1);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return result;
|
|
572
|
+
};
|
|
573
|
+
|
|
574
|
+
let result = `${tree.name}/\n`;
|
|
575
|
+
result += renderNode(tree);
|
|
576
|
+
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Format bytes to human readable
|
|
582
|
+
*/
|
|
583
|
+
formatBytes(bytes) {
|
|
584
|
+
if (bytes === 0) return '0 Bytes';
|
|
585
|
+
const k = 1024;
|
|
586
|
+
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
|
587
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
588
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
module.exports = FileScanner;
|