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.
@@ -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;