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,298 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const ignore = require('ignore');
4
+
5
+ /**
6
+ * Quản lý ignore patterns theo chuẩn Git
7
+ */
8
+ class IgnoreManager {
9
+ constructor(projectPath) {
10
+ this.projectPath = projectPath;
11
+ this.ignoreInstances = new Map(); // Cache ignore instances theo thư mục
12
+ this.defaultIgnores = this.getDefaultIgnores();
13
+ }
14
+
15
+ /**
16
+ * Lấy danh sách ignore patterns mặc định
17
+ */
18
+ getDefaultIgnores() {
19
+ return [
20
+ // Node.js
21
+ 'node_modules/',
22
+ 'npm-debug.log*',
23
+ 'yarn-debug.log*',
24
+ 'yarn-error.log*',
25
+ '.npm',
26
+ '.yarn/',
27
+
28
+ // Build outputs
29
+ 'dist/',
30
+ 'build/',
31
+ 'out/',
32
+ 'target/',
33
+ 'bin/',
34
+ 'obj/',
35
+
36
+ // IDE và Editor
37
+ '.vscode/',
38
+ '.idea/',
39
+ '*.swp',
40
+ '*.swo',
41
+ '*~',
42
+ '.DS_Store',
43
+ 'Thumbs.db',
44
+
45
+ // Logs
46
+ 'logs/',
47
+ '*.log',
48
+
49
+ // Environment files
50
+ '.env',
51
+ '.env.local',
52
+ '.env.development.local',
53
+ '.env.test.local',
54
+ '.env.production.local',
55
+
56
+ // Cache directories
57
+ '.cache/',
58
+ '.parcel-cache/',
59
+ '.next/',
60
+ '.nuxt/',
61
+
62
+ // Coverage reports
63
+ 'coverage/',
64
+ '*.lcov',
65
+
66
+ // Dependency directories
67
+ 'bower_components/',
68
+ 'jspm_packages/',
69
+
70
+ // Java
71
+ '*.class',
72
+ '*.jar',
73
+ '*.war',
74
+ '*.ear',
75
+ '.gradle/',
76
+
77
+ // Python
78
+ '__pycache__/',
79
+ '*.py[cod]',
80
+ '*$py.class',
81
+ '*.so',
82
+ '.Python',
83
+ 'env/',
84
+ 'venv/',
85
+ '.venv/',
86
+ 'pip-log.txt',
87
+ 'pip-delete-this-directory.txt',
88
+
89
+ // .NET
90
+ '[Bb]in/',
91
+ '[Oo]bj/',
92
+ '*.user',
93
+ '*.suo',
94
+ '*.userosscache',
95
+ '*.sln.docstates',
96
+
97
+ // Temporary files
98
+ '*.tmp',
99
+ '*.temp',
100
+ '*.bak',
101
+ '*.backup',
102
+
103
+ // OS generated files
104
+ '.DS_Store?',
105
+ 'ehthumbs.db',
106
+ 'Icon?',
107
+
108
+ // VG Coder output
109
+ 'vg-output/'
110
+ ];
111
+ }
112
+
113
+ /**
114
+ * Lấy ignore instance cho một thư mục cụ thể
115
+ */
116
+ async getIgnoreInstance(dirPath) {
117
+ const relativePath = path.relative(this.projectPath, dirPath);
118
+ const cacheKey = relativePath || '.';
119
+
120
+ if (this.ignoreInstances.has(cacheKey)) {
121
+ return this.ignoreInstances.get(cacheKey);
122
+ }
123
+
124
+ const ig = ignore();
125
+
126
+ // Thêm default ignores
127
+ ig.add(this.defaultIgnores);
128
+
129
+ // Đọc .gitignore từ root đến thư mục hiện tại
130
+ const pathParts = relativePath ? relativePath.split(path.sep) : [];
131
+ let currentPath = this.projectPath;
132
+
133
+ // Đọc .gitignore từ root
134
+ await this.addGitignoreFromPath(ig, currentPath);
135
+
136
+ // Đọc .gitignore từ các thư mục con theo thứ tự
137
+ for (const part of pathParts) {
138
+ currentPath = path.join(currentPath, part);
139
+ await this.addGitignoreFromPath(ig, currentPath);
140
+ }
141
+
142
+ this.ignoreInstances.set(cacheKey, ig);
143
+ return ig;
144
+ }
145
+
146
+ /**
147
+ * Thêm patterns từ .gitignore file
148
+ */
149
+ async addGitignoreFromPath(ig, dirPath) {
150
+ const gitignorePath = path.join(dirPath, '.gitignore');
151
+
152
+ try {
153
+ if (await fs.pathExists(gitignorePath)) {
154
+ const content = await fs.readFile(gitignorePath, 'utf8');
155
+ const patterns = this.parseGitignoreContent(content, dirPath);
156
+ ig.add(patterns);
157
+ }
158
+ } catch (error) {
159
+ // Ignore errors reading .gitignore files
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Parse nội dung .gitignore
165
+ */
166
+ parseGitignoreContent(content, basePath) {
167
+ const lines = content.split('\n');
168
+ const patterns = [];
169
+
170
+ for (let line of lines) {
171
+ line = line.trim();
172
+
173
+ // Skip empty lines và comments
174
+ if (!line || line.startsWith('#')) {
175
+ continue;
176
+ }
177
+
178
+ // Xử lý relative path từ basePath
179
+ const relativePath = path.relative(this.projectPath, basePath);
180
+ if (relativePath && !line.startsWith('/')) {
181
+ // Nếu pattern không bắt đầu bằng /, thêm relative path
182
+ line = path.posix.join(relativePath, line);
183
+ } else if (line.startsWith('/')) {
184
+ // Remove leading slash cho absolute patterns
185
+ line = line.substring(1);
186
+ }
187
+
188
+ patterns.push(line);
189
+ }
190
+
191
+ return patterns;
192
+ }
193
+
194
+ /**
195
+ * Kiểm tra xem một file/thư mục có bị ignore không
196
+ */
197
+ async shouldIgnore(filePath) {
198
+ try {
199
+ const absolutePath = path.resolve(this.projectPath, filePath);
200
+ const relativePath = path.relative(this.projectPath, absolutePath);
201
+
202
+ // Không ignore nếu file nằm ngoài project
203
+ if (relativePath.startsWith('..')) {
204
+ return false;
205
+ }
206
+
207
+ const dirPath = path.dirname(absolutePath);
208
+ const ig = await this.getIgnoreInstance(dirPath);
209
+
210
+ return ig.ignores(relativePath);
211
+ } catch (error) {
212
+ return false;
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Lọc danh sách files/directories
218
+ */
219
+ async filterIgnored(items) {
220
+ const results = [];
221
+
222
+ for (const item of items) {
223
+ const shouldIgnore = await this.shouldIgnore(item);
224
+ if (!shouldIgnore) {
225
+ results.push(item);
226
+ }
227
+ }
228
+
229
+ return results;
230
+ }
231
+
232
+ /**
233
+ * Lấy tất cả patterns đang được áp dụng cho một thư mục
234
+ */
235
+ async getAppliedPatterns(dirPath = this.projectPath) {
236
+ const ig = await this.getIgnoreInstance(dirPath);
237
+ return {
238
+ default: this.defaultIgnores,
239
+ gitignore: ig._rules.filter(rule => !this.defaultIgnores.includes(rule.origin))
240
+ };
241
+ }
242
+
243
+ /**
244
+ * Kiểm tra xem có .gitignore file nào không
245
+ */
246
+ async hasGitignoreFiles() {
247
+ const gitignoreFiles = [];
248
+
249
+ const checkGitignore = async (dirPath) => {
250
+ const gitignorePath = path.join(dirPath, '.gitignore');
251
+ if (await fs.pathExists(gitignorePath)) {
252
+ gitignoreFiles.push(path.relative(this.projectPath, gitignorePath));
253
+ }
254
+ };
255
+
256
+ // Kiểm tra root
257
+ await checkGitignore(this.projectPath);
258
+
259
+ // Kiểm tra subdirectories (chỉ 2 levels để tránh quá chậm)
260
+ try {
261
+ const items = await fs.readdir(this.projectPath);
262
+ for (const item of items) {
263
+ const itemPath = path.join(this.projectPath, item);
264
+ const stat = await fs.stat(itemPath);
265
+ if (stat.isDirectory()) {
266
+ await checkGitignore(itemPath);
267
+
268
+ // Level 2
269
+ try {
270
+ const subItems = await fs.readdir(itemPath);
271
+ for (const subItem of subItems) {
272
+ const subItemPath = path.join(itemPath, subItem);
273
+ const subStat = await fs.stat(subItemPath);
274
+ if (subStat.isDirectory()) {
275
+ await checkGitignore(subItemPath);
276
+ }
277
+ }
278
+ } catch (error) {
279
+ // Ignore errors
280
+ }
281
+ }
282
+ }
283
+ } catch (error) {
284
+ // Ignore errors
285
+ }
286
+
287
+ return gitignoreFiles;
288
+ }
289
+
290
+ /**
291
+ * Clear cache
292
+ */
293
+ clearCache() {
294
+ this.ignoreInstances.clear();
295
+ }
296
+ }
297
+
298
+ module.exports = IgnoreManager;
package/src/index.js ADDED
@@ -0,0 +1,282 @@
1
+ const { Command } = require('commander');
2
+ const path = require('path');
3
+ const fs = require('fs-extra');
4
+ const chalk = require('chalk');
5
+ const ora = require('ora');
6
+
7
+ const ProjectDetector = require('./detectors/project-detector');
8
+ const FileScanner = require('./scanner/file-scanner');
9
+ const TokenManager = require('./tokenizer/token-manager');
10
+ const HtmlExporter = require('./exporter/html-exporter');
11
+
12
+ /**
13
+ * Main CLI Application
14
+ */
15
+ class VGCoderCLI {
16
+ constructor() {
17
+ this.program = new Command();
18
+ this.setupCommands();
19
+ }
20
+
21
+ /**
22
+ * Setup CLI commands
23
+ */
24
+ setupCommands() {
25
+ this.program
26
+ .name('vg-coder')
27
+ .description('CLI tool để phân tích dự án, nối file mã nguồn, đếm token và xuất HTML')
28
+ .version('1.0.0');
29
+
30
+ // Analyze command
31
+ this.program
32
+ .command('analyze [path]')
33
+ .description('Phân tích dự án và tạo output HTML')
34
+ .option('-o, --output <path>', 'Thư mục output', './vg-output')
35
+ .option('-m, --max-tokens <number>', 'Số token tối đa mỗi chunk', '8000')
36
+ .option('-t, --model <model>', 'Model AI để đếm token', 'gpt-4')
37
+ .option('--extensions <extensions>', 'Danh sách extensions (comma-separated)')
38
+ .option('--include-hidden', 'Bao gồm file ẩn')
39
+ .option('--no-structure', 'Không ưu tiên giữ cấu trúc file')
40
+ .option('--theme <theme>', 'Theme cho syntax highlighting', 'github')
41
+ .action(this.handleAnalyze.bind(this));
42
+
43
+ // Info command
44
+ this.program
45
+ .command('info [path]')
46
+ .description('Hiển thị thông tin về dự án')
47
+ .action(this.handleInfo.bind(this));
48
+
49
+ // Clean command
50
+ this.program
51
+ .command('clean')
52
+ .description('Xóa thư mục output')
53
+ .option('-o, --output <path>', 'Thư mục output', './vg-output')
54
+ .action(this.handleClean.bind(this));
55
+ }
56
+
57
+ /**
58
+ * Handle analyze command
59
+ */
60
+ async handleAnalyze(projectPath, options) {
61
+ const spinner = ora('Initializing analysis...').start();
62
+
63
+ try {
64
+ // Resolve project path
65
+ projectPath = path.resolve(projectPath || process.cwd());
66
+ const outputPath = path.resolve(options.output);
67
+
68
+ // Validate project path
69
+ if (!await fs.pathExists(projectPath)) {
70
+ throw new Error(`Project path does not exist: ${projectPath}`);
71
+ }
72
+
73
+ spinner.text = 'Detecting project type...';
74
+
75
+ // Detect project type
76
+ const detector = new ProjectDetector(projectPath);
77
+ const projectInfo = await detector.detectAll();
78
+
79
+ console.log(chalk.blue('\n📁 Project Detection:'));
80
+ console.log(`Primary Type: ${chalk.green(projectInfo.primary)}`);
81
+ if (Object.keys(projectInfo.detected).length > 1) {
82
+ console.log(`Other Types: ${Object.keys(projectInfo.detected).filter(t => t !== projectInfo.primary).join(', ')}`);
83
+ }
84
+
85
+ spinner.text = 'Scanning files...';
86
+
87
+ // Scan files
88
+ const scannerOptions = {
89
+ maxTokens: parseInt(options.maxTokens),
90
+ extensions: options.extensions ? options.extensions.split(',').map(ext => ext.trim()) : undefined,
91
+ includeHidden: options.includeHidden
92
+ };
93
+
94
+ const scanner = new FileScanner(projectPath, scannerOptions);
95
+ const scanResult = await scanner.scanProject();
96
+
97
+ console.log(chalk.blue('\n📊 Scan Results:'));
98
+ console.log(`Files Found: ${scanResult.stats.totalFiles}`);
99
+ console.log(`Files Processed: ${scanResult.stats.processedFiles}`);
100
+ console.log(`Scan Time: ${scanResult.stats.scanTime}ms`);
101
+
102
+ // Hiển thị cấu trúc thư mục được scan
103
+ console.log(chalk.blue('\n📁 Directory Structure:'));
104
+ const treeStructure = scanner.renderProjectTree(scanResult.tree);
105
+ console.log(treeStructure);
106
+
107
+ spinner.text = 'Analyzing tokens...';
108
+
109
+ // Analyze tokens
110
+ const tokenManager = new TokenManager({
111
+ model: options.model,
112
+ maxTokens: parseInt(options.maxTokens),
113
+ preserveStructure: options.structure !== false
114
+ });
115
+
116
+ const tokenAnalysis = tokenManager.analyzeFiles(scanResult.files);
117
+
118
+ console.log(chalk.blue('\n🧮 Token Analysis:'));
119
+ console.log(`Total Tokens: ${chalk.yellow(tokenAnalysis.summary.totalTokens.toLocaleString())}`);
120
+ console.log(`Average Tokens/File: ${tokenAnalysis.summary.averageTokensPerFile.toLocaleString()}`);
121
+ console.log(`Files Exceeding Limit: ${tokenAnalysis.summary.filesExceedingLimit}`);
122
+ console.log(`Estimated Chunks: ${tokenAnalysis.summary.estimatedChunks}`);
123
+
124
+ spinner.text = 'Creating content chunks...';
125
+
126
+ // Create combined content
127
+ const combinedContent = await scanner.createCombinedContent(scanResult.files);
128
+
129
+ // Chunk content
130
+ const chunks = await tokenManager.chunkContent(combinedContent, {
131
+ projectType: projectInfo.primary,
132
+ projectPath: projectPath,
133
+ totalFiles: scanResult.files.length
134
+ });
135
+
136
+ console.log(chalk.blue('\n✂️ Chunking Results:'));
137
+ console.log(`Total Chunks: ${chunks.length}`);
138
+ chunks.forEach((chunk, index) => {
139
+ console.log(` Chunk ${index + 1}: ${chunk.tokens.toLocaleString()} tokens (${chunk.metadata?.type || 'unknown'})`);
140
+ });
141
+
142
+ spinner.text = 'Generating HTML output...';
143
+
144
+ // Export HTML
145
+ const exporter = new HtmlExporter(outputPath, {
146
+ theme: options.theme,
147
+ title: `VG Coder Analysis - ${path.basename(projectPath)}`
148
+ });
149
+
150
+ const exportResult = await exporter.exportChunks(chunks, {
151
+ projectType: projectInfo.primary,
152
+ projectInfo: projectInfo,
153
+ scanStats: scanResult.stats,
154
+ tokenStats: tokenAnalysis.summary,
155
+ directoryStructure: treeStructure
156
+ });
157
+
158
+ // Cleanup
159
+ tokenManager.cleanup();
160
+
161
+ spinner.succeed('Analysis completed successfully!');
162
+
163
+ console.log(chalk.green('\n✅ Output Generated:'));
164
+ console.log(`Index: ${chalk.cyan(exportResult.indexPath)}`);
165
+ console.log(`Combined: ${chalk.cyan(exportResult.combinedPath)}`);
166
+ console.log(`Chunks: ${chalk.cyan(exportResult.chunksPath)}`);
167
+ console.log(`Total Files: ${exportResult.totalFiles}`);
168
+
169
+ console.log(chalk.blue('\n🌐 Open in browser:'));
170
+ console.log(`file://${exportResult.indexPath}`);
171
+
172
+ } catch (error) {
173
+ spinner.fail('Analysis failed');
174
+ console.error(chalk.red('\n❌ Error:'), error.message);
175
+ process.exit(1);
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Handle info command
181
+ */
182
+ async handleInfo(projectPath, options) {
183
+ const spinner = ora('Gathering project information...').start();
184
+
185
+ try {
186
+ // Resolve project path
187
+ projectPath = path.resolve(projectPath || process.cwd());
188
+
189
+ if (!await fs.pathExists(projectPath)) {
190
+ throw new Error(`Project path does not exist: ${projectPath}`);
191
+ }
192
+
193
+ // Detect project
194
+ const detector = new ProjectDetector(projectPath);
195
+ const projectInfo = await detector.detectAll();
196
+
197
+ // Quick scan
198
+ const scanner = new FileScanner(projectPath);
199
+ const scanResult = await scanner.scanProject();
200
+
201
+ // Token analysis
202
+ const tokenManager = new TokenManager();
203
+ const tokenAnalysis = tokenManager.analyzeFiles(scanResult.files);
204
+
205
+ spinner.succeed('Information gathered');
206
+
207
+ console.log(chalk.blue('\n📁 Project Information:'));
208
+ console.log(`Path: ${chalk.cyan(projectPath)}`);
209
+ console.log(`Primary Type: ${chalk.green(projectInfo.primary)}`);
210
+
211
+ if (Object.keys(projectInfo.detected).length > 0) {
212
+ console.log('\nDetected Technologies:');
213
+ Object.entries(projectInfo.detected).forEach(([type, info]) => {
214
+ console.log(` ${chalk.yellow(type)}: ${info.confidence} confidence`);
215
+ if (info.version) {
216
+ console.log(` Version: ${info.version}`);
217
+ }
218
+ });
219
+ }
220
+
221
+ console.log(chalk.blue('\n📊 File Statistics:'));
222
+ console.log(`Total Files: ${scanResult.stats.processedFiles}`);
223
+ console.log(`Total Size: ${scanner.formatBytes(scanResult.files.reduce((sum, f) => sum + f.size, 0))}`);
224
+ console.log(`Total Lines: ${scanResult.files.reduce((sum, f) => sum + f.lines, 0).toLocaleString()}`);
225
+
226
+ const extensions = [...new Set(scanResult.files.map(f => f.extension))].filter(Boolean);
227
+ console.log(`Extensions: ${extensions.join(', ')}`);
228
+
229
+ console.log(chalk.blue('\n🧮 Token Statistics:'));
230
+ console.log(`Total Tokens: ${tokenAnalysis.summary.totalTokens.toLocaleString()}`);
231
+ console.log(`Average Tokens/File: ${tokenAnalysis.summary.averageTokensPerFile.toLocaleString()}`);
232
+ console.log(`Files Exceeding 8K Limit: ${tokenAnalysis.summary.filesExceedingLimit}`);
233
+ console.log(`Estimated Chunks (8K): ${tokenAnalysis.summary.estimatedChunks}`);
234
+
235
+ tokenManager.cleanup();
236
+
237
+ } catch (error) {
238
+ spinner.fail('Failed to gather information');
239
+ console.error(chalk.red('\n❌ Error:'), error.message);
240
+ process.exit(1);
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Handle clean command
246
+ */
247
+ async handleClean(options) {
248
+ const spinner = ora('Cleaning output directory...').start();
249
+
250
+ try {
251
+ const outputPath = path.resolve(options.output);
252
+
253
+ if (await fs.pathExists(outputPath)) {
254
+ await fs.remove(outputPath);
255
+ spinner.succeed(`Cleaned: ${outputPath}`);
256
+ } else {
257
+ spinner.succeed('Output directory does not exist');
258
+ }
259
+
260
+ } catch (error) {
261
+ spinner.fail('Failed to clean');
262
+ console.error(chalk.red('\n❌ Error:'), error.message);
263
+ process.exit(1);
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Run CLI
269
+ */
270
+ run() {
271
+ this.program.parse();
272
+ }
273
+ }
274
+
275
+ // Export for testing
276
+ module.exports = VGCoderCLI;
277
+
278
+ // Run if called directly
279
+ if (require.main === module) {
280
+ const cli = new VGCoderCLI();
281
+ cli.run();
282
+ }