vg-coder-cli 1.0.5 → 1.0.7

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/.vgignore ADDED
@@ -0,0 +1,10 @@
1
+ # VG Coder specific ignores
2
+ *.tgz
3
+ coverage/
4
+ vg-output/
5
+ test-small/
6
+
7
+ # Additional ignores for VG Coder
8
+ *.map
9
+ *.min.js
10
+ *.min.css
package/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  - 🔍 **Phát hiện loại dự án**: Tự động nhận diện Angular, Spring Boot, React, Vue, Node.js, Python, Java, .NET
8
8
  - 📁 **Xử lý .gitignore**: Tuân thủ chuẩn Git với multi-level ignore rules
9
+ - **.vgignore support**: Priority cao hơn .gitignore, với syntax giống hệt
9
10
  - 📄 **Scan và nối file**: Quét toàn bộ dự án và nối file mã nguồn
10
11
  - 🧮 **Đếm token**: Sử dụng tiktoken để đếm token chính xác cho AI models
11
12
  - ✂️ **Chia nhỏ nội dung**: Smart chunking với preserve structure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vg-coder-cli",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "🚀 CLI tool to analyze projects, concatenate source files, count tokens, and export HTML with syntax highlighting and copy functionality",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -39,11 +39,15 @@ class HtmlExporter {
39
39
 
40
40
  // Tạo combined view
41
41
  await this.createCombinedPage(chunks, metadata);
42
-
42
+
43
+ // Tạo combined.txt cho AI tools
44
+ await this.createCombinedTxtFile(chunks, metadata);
45
+
43
46
  return {
44
47
  indexPath: path.join(this.outputPath, 'index.html'),
45
48
  chunksPath: path.join(this.outputPath, 'chunks'),
46
49
  combinedPath: path.join(this.outputPath, 'combined.html'),
50
+ combinedTxtPath: path.join(this.outputPath, 'combined.txt'),
47
51
  totalFiles: chunks.length
48
52
  };
49
53
  }
@@ -250,6 +254,126 @@ Nếu file chưa tồn tại, script sẽ tự tạo file và thư mục cha.</c
250
254
  await fs.writeFile(path.join(this.outputPath, 'combined.html'), html);
251
255
  }
252
256
 
257
+ /**
258
+ * Tạo combined.txt file cho AI tools với formatting tối ưu
259
+ */
260
+ async createCombinedTxtFile(chunks, metadata) {
261
+ // Sử dụng files gốc nếu có, nếu không thì dùng chunks
262
+ const files = metadata.files;
263
+
264
+ if (files && files.length > 0) {
265
+ // Tạo content từ files gốc với formatting AI-friendly
266
+ let content = '';
267
+
268
+ // Minimal header cho AI context
269
+ content += `// VG Coder Analysis - ${metadata.projectInfo?.primary || 'Unknown'} Project\n`;
270
+ content += `// Files: ${files.length} | Generated: ${new Date().toISOString()}\n\n`;
271
+
272
+ // Nội dung từng file với boundaries rõ ràng
273
+ for (let i = 0; i < files.length; i++) {
274
+ const file = files[i];
275
+
276
+ // File boundary marker
277
+ content += `// ===== FILE: ${file.relativePath} =====\n`;
278
+
279
+ // Nội dung file nguyên bản
280
+ content += file.content;
281
+
282
+ // Đảm bảo file kết thúc bằng newline
283
+ if (!file.content.endsWith('\n')) {
284
+ content += '\n';
285
+ }
286
+
287
+ // Separator giữa các files
288
+ if (i < files.length - 1) {
289
+ content += '\n';
290
+ }
291
+ }
292
+
293
+ await fs.writeFile(path.join(this.outputPath, 'combined.txt'), content, 'utf8');
294
+ } else {
295
+ // Fallback: sử dụng chunks (legacy)
296
+ let content = '';
297
+
298
+ // Minimal header cho AI context
299
+ content += `// VG Coder Analysis - ${metadata.projectInfo?.primary || 'Unknown'} Project\n`;
300
+ content += `// Files: ${chunks.length} | Generated: ${new Date().toISOString()}\n\n`;
301
+
302
+ // Parse chunks để extract file content với boundaries rõ ràng
303
+ for (let i = 0; i < chunks.length; i++) {
304
+ const chunk = chunks[i];
305
+
306
+ // Extract file content từ chunk, bỏ qua headers và separators
307
+ const cleanContent = this.extractCleanContent(chunk.content);
308
+
309
+ if (cleanContent.trim()) {
310
+ content += cleanContent;
311
+
312
+ // Đảm bảo kết thúc bằng newline
313
+ if (!cleanContent.endsWith('\n')) {
314
+ content += '\n';
315
+ }
316
+
317
+ // Separator giữa các chunks (minimal)
318
+ if (i < chunks.length - 1) {
319
+ content += '\n';
320
+ }
321
+ }
322
+ }
323
+
324
+ await fs.writeFile(path.join(this.outputPath, 'combined.txt'), content, 'utf8');
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Extract clean content từ chunk, loại bỏ headers và formatting
330
+ */
331
+ extractCleanContent(chunkContent) {
332
+ const lines = chunkContent.split('\n');
333
+ let cleanLines = [];
334
+ let inFileContent = false;
335
+ let currentFilePath = '';
336
+
337
+ for (let i = 0; i < lines.length; i++) {
338
+ const line = lines[i];
339
+
340
+ // Detect file header
341
+ if (line.includes('================================================================================')) {
342
+ if (i + 1 < lines.length && lines[i + 1].startsWith('File: ')) {
343
+ // Start of new file
344
+ currentFilePath = lines[i + 1].replace('File: ', '').trim();
345
+ cleanLines.push(`// ===== FILE: ${currentFilePath} =====`);
346
+ inFileContent = false;
347
+ i += 2; // Skip header lines
348
+ continue;
349
+ }
350
+ }
351
+
352
+ // Skip project headers and structure
353
+ if (line.startsWith('# Project Analysis Report') ||
354
+ line.startsWith('Generated:') ||
355
+ line.startsWith('Project Path:') ||
356
+ line.startsWith('## Statistics') ||
357
+ line.startsWith('## Project Structure') ||
358
+ line.startsWith('- Total') ||
359
+ line.startsWith('- Extensions:') ||
360
+ line.startsWith('```') ||
361
+ line.startsWith('├──') ||
362
+ line.startsWith('└──') ||
363
+ line.trim() === '') {
364
+ continue;
365
+ }
366
+
367
+ // Add actual file content
368
+ if (currentFilePath && !line.includes('================================================================================')) {
369
+ cleanLines.push(line);
370
+ inFileContent = true;
371
+ }
372
+ }
373
+
374
+ return cleanLines.join('\n');
375
+ }
376
+
253
377
  /**
254
378
  * Copy static assets
255
379
  */
@@ -19,6 +19,8 @@ class IgnoreManager {
19
19
  return [
20
20
  // Node.js
21
21
  'node_modules/',
22
+ 'package-lock.json',
23
+ 'yarn.lock',
22
24
  'npm-debug.log*',
23
25
  'yarn-debug.log*',
24
26
  'yarn-error.log*',
@@ -126,17 +128,17 @@ class IgnoreManager {
126
128
  // Thêm default ignores
127
129
  ig.add(this.defaultIgnores);
128
130
 
129
- // Đọc .gitignore từ root đến thư mục hiện tại
131
+ // Đọc .gitignore và .vgignore từ root đến thư mục hiện tại
130
132
  const pathParts = relativePath ? relativePath.split(path.sep) : [];
131
133
  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ự
134
+
135
+ // Đọc .gitignore và .vgignore từ root
136
+ await this.addIgnoreFromPath(ig, currentPath);
137
+
138
+ // Đọc .gitignore và .vgignore từ các thư mục con theo thứ tự
137
139
  for (const part of pathParts) {
138
140
  currentPath = path.join(currentPath, part);
139
- await this.addGitignoreFromPath(ig, currentPath);
141
+ await this.addIgnoreFromPath(ig, currentPath);
140
142
  }
141
143
 
142
144
  this.ignoreInstances.set(cacheKey, ig);
@@ -144,26 +146,38 @@ class IgnoreManager {
144
146
  }
145
147
 
146
148
  /**
147
- * Thêm patterns từ .gitignore file
149
+ * Thêm patterns từ .gitignore và .vgignore files
148
150
  */
149
- async addGitignoreFromPath(ig, dirPath) {
151
+ async addIgnoreFromPath(ig, dirPath) {
152
+ // Đọc .gitignore
150
153
  const gitignorePath = path.join(dirPath, '.gitignore');
151
-
152
154
  try {
153
155
  if (await fs.pathExists(gitignorePath)) {
154
156
  const content = await fs.readFile(gitignorePath, 'utf8');
155
- const patterns = this.parseGitignoreContent(content, dirPath);
157
+ const patterns = this.parseIgnoreContent(content, dirPath);
156
158
  ig.add(patterns);
157
159
  }
158
160
  } catch (error) {
159
161
  // Ignore errors reading .gitignore files
160
162
  }
163
+
164
+ // Đọc .vgignore (có priority cao hơn .gitignore)
165
+ const vgignorePath = path.join(dirPath, '.vgignore');
166
+ try {
167
+ if (await fs.pathExists(vgignorePath)) {
168
+ const content = await fs.readFile(vgignorePath, 'utf8');
169
+ const patterns = this.parseIgnoreContent(content, dirPath);
170
+ ig.add(patterns);
171
+ }
172
+ } catch (error) {
173
+ // Ignore errors reading .vgignore files
174
+ }
161
175
  }
162
176
 
163
177
  /**
164
- * Parse nội dung .gitignore
178
+ * Parse nội dung .gitignore và .vgignore
165
179
  */
166
- parseGitignoreContent(content, basePath) {
180
+ parseIgnoreContent(content, basePath) {
167
181
  const lines = content.split('\n');
168
182
  const patterns = [];
169
183
 
package/src/index.js CHANGED
@@ -9,6 +9,7 @@ const ProjectDetector = require('./detectors/project-detector');
9
9
  const FileScanner = require('./scanner/file-scanner');
10
10
  const TokenManager = require('./tokenizer/token-manager');
11
11
  const HtmlExporter = require('./exporter/html-exporter');
12
+ const ClipboardManager = require('./utils/clipboard');
12
13
 
13
14
  /**
14
15
  * Main CLI Application
@@ -39,6 +40,8 @@ class VGCoderCLI {
39
40
  .option('--include-hidden', 'Bao gồm file ẩn')
40
41
  .option('--no-structure', 'Không ưu tiên giữ cấu trúc file')
41
42
  .option('--theme <theme>', 'Theme cho syntax highlighting', 'github')
43
+ .option('--clipboard-only', 'Copy content to clipboard without creating files')
44
+ .option('--clipboard', 'Alias for --clipboard-only')
42
45
  .action(this.handleAnalyze.bind(this));
43
46
 
44
47
  // Info command
@@ -60,17 +63,25 @@ class VGCoderCLI {
60
63
  */
61
64
  async handleAnalyze(projectPath, options) {
62
65
  const spinner = ora('Initializing analysis...').start();
63
-
66
+
64
67
  try {
65
68
  // Resolve project path
66
69
  projectPath = path.resolve(projectPath || process.cwd());
67
- const outputPath = path.resolve(options.output);
70
+
71
+ // Check if clipboard-only mode
72
+ const clipboardMode = options.clipboardOnly || options.clipboard;
73
+ const outputPath = clipboardMode ? null : path.resolve(options.output || './vg-output');
68
74
 
69
75
  // Validate project path
70
76
  if (!await fs.pathExists(projectPath)) {
71
77
  throw new Error(`Project path does not exist: ${projectPath}`);
72
78
  }
73
79
 
80
+ // Validate output path for non-clipboard mode
81
+ if (!clipboardMode && !outputPath) {
82
+ throw new Error('Output path is required for non-clipboard mode');
83
+ }
84
+
74
85
  spinner.text = 'Detecting project type...';
75
86
 
76
87
  // Detect project type
@@ -123,10 +134,42 @@ class VGCoderCLI {
123
134
  console.log(`Estimated Chunks: ${tokenAnalysis.summary.estimatedChunks}`);
124
135
 
125
136
  spinner.text = 'Creating content chunks...';
126
-
127
- // Create combined content
137
+
138
+ if (clipboardMode) {
139
+ // Clipboard mode: create AI-friendly content and copy to clipboard
140
+ spinner.text = 'Creating AI-friendly content...';
141
+
142
+ const aiContent = await scanner.createCombinedContentForAI(scanResult.files, {
143
+ includeStats: false,
144
+ includeTree: false,
145
+ preserveLineNumbers: true
146
+ });
147
+
148
+ spinner.text = 'Copying to clipboard...';
149
+
150
+ await ClipboardManager.copyToClipboard(aiContent);
151
+ const contentInfo = ClipboardManager.getContentInfo(aiContent);
152
+
153
+ // Cleanup
154
+ tokenManager.cleanup();
155
+
156
+ spinner.succeed('Content copied to clipboard successfully!');
157
+
158
+ console.log(chalk.green('\n📋 Clipboard Content:'));
159
+ console.log(`Files: ${chalk.cyan(scanResult.files.length)}`);
160
+ console.log(`Lines: ${chalk.cyan(contentInfo.lines.toLocaleString())}`);
161
+ console.log(`Characters: ${chalk.cyan(contentInfo.characters.toLocaleString())}`);
162
+ console.log(`Size: ${chalk.cyan(contentInfo.size)}`);
163
+
164
+ console.log(chalk.blue('\n💡 Ready for AI analysis!'));
165
+ console.log('Content is now in your clipboard and ready to paste into AI tools.');
166
+
167
+ return; // Exit early for clipboard mode
168
+ }
169
+
170
+ // Normal mode: create HTML output
128
171
  const combinedContent = await scanner.createCombinedContent(scanResult.files);
129
-
172
+
130
173
  // Chunk content
131
174
  const chunks = await tokenManager.chunkContent(combinedContent, {
132
175
  projectType: projectInfo.primary,
@@ -153,7 +196,8 @@ class VGCoderCLI {
153
196
  projectInfo: projectInfo,
154
197
  scanStats: scanResult.stats,
155
198
  tokenStats: tokenAnalysis.summary,
156
- directoryStructure: treeStructure
199
+ directoryStructure: treeStructure,
200
+ files: scanResult.files // Thêm files gốc để tạo combined.txt
157
201
  });
158
202
 
159
203
  // Cleanup
@@ -164,6 +208,9 @@ class VGCoderCLI {
164
208
  console.log(chalk.green('\n✅ Output Generated:'));
165
209
  console.log(`Index: ${chalk.cyan(exportResult.indexPath)}`);
166
210
  console.log(`Combined: ${chalk.cyan(exportResult.combinedPath)}`);
211
+ if (exportResult.combinedTxtPath) {
212
+ console.log(`Combined.txt: ${chalk.cyan(exportResult.combinedTxtPath)}`);
213
+ }
167
214
  console.log(`Chunks: ${chalk.cyan(exportResult.chunksPath)}`);
168
215
  console.log(`Total Files: ${exportResult.totalFiles}`);
169
216
 
@@ -446,6 +446,54 @@ Size: {size} bytes | Lines: {lines}
446
446
  return '\n\n';
447
447
  }
448
448
 
449
+ /**
450
+ * Tạo nội dung kết hợp cho AI tools với formatting chính xác
451
+ */
452
+ async createCombinedContentForAI(files, options = {}) {
453
+ const {
454
+ includeStats = false,
455
+ includeTree = false,
456
+ preserveLineNumbers = true
457
+ } = options;
458
+
459
+ let content = '';
460
+
461
+ // Header với thông tin project (tùy chọn)
462
+ if (includeStats) {
463
+ content += this.generateProjectHeader(files);
464
+ content += '\n\n';
465
+ }
466
+
467
+ // Cấu trúc thư mục (tùy chọn)
468
+ if (includeTree) {
469
+ content += this.generateTreeStructure(files);
470
+ content += '\n\n';
471
+ }
472
+
473
+ // Nội dung từng file với formatting chính xác
474
+ for (let i = 0; i < files.length; i++) {
475
+ const file = files[i];
476
+
477
+ // File boundary marker - không ảnh hưởng line numbering
478
+ content += `// ===== FILE: ${file.relativePath} =====\n`;
479
+
480
+ // Nội dung file nguyên bản
481
+ content += file.content;
482
+
483
+ // Đảm bảo file kết thúc bằng newline
484
+ if (!file.content.endsWith('\n')) {
485
+ content += '\n';
486
+ }
487
+
488
+ // Separator giữa các files
489
+ if (i < files.length - 1) {
490
+ content += '\n';
491
+ }
492
+ }
493
+
494
+ return content;
495
+ }
496
+
449
497
  /**
450
498
  * Tạo header thông tin project
451
499
  */
@@ -247,17 +247,15 @@ class TokenManager {
247
247
  let currentTokens = 0;
248
248
  let chunkIndex = 0;
249
249
 
250
- // Thêm header cho file
251
- const header = `\n=== Large File: ${filePath} (Part {part}) ===\n`;
252
-
250
+ // Không thêm header "Large File" nữa để UI sạch hơn
251
+
253
252
  for (const line of lines) {
254
253
  const lineTokens = this.countTokens(line + '\n');
255
-
256
- if (currentTokens + lineTokens > this.options.maxTokens - 100 && currentChunk) { // Reserve 100 tokens for header
257
- const partHeader = header.replace('{part}', (chunkIndex + 1).toString());
254
+
255
+ if (currentTokens + lineTokens > this.options.maxTokens && currentChunk) {
258
256
  chunks.push({
259
- content: partHeader + currentChunk.trim(),
260
- tokens: this.countTokens(partHeader + currentChunk.trim()),
257
+ content: currentChunk.trim(),
258
+ tokens: this.countTokens(currentChunk.trim()),
261
259
  chunkIndex: chunkIndex++,
262
260
  totalChunks: 0,
263
261
  metadata: {
@@ -277,10 +275,9 @@ class TokenManager {
277
275
 
278
276
  // Thêm chunk cuối cùng
279
277
  if (currentChunk.trim()) {
280
- const partHeader = header.replace('{part}', (chunkIndex + 1).toString());
281
278
  chunks.push({
282
- content: partHeader + currentChunk.trim(),
283
- tokens: this.countTokens(partHeader + currentChunk.trim()),
279
+ content: currentChunk.trim(),
280
+ tokens: this.countTokens(currentChunk.trim()),
284
281
  chunkIndex: chunkIndex++,
285
282
  totalChunks: 0,
286
283
  metadata: {
@@ -0,0 +1,170 @@
1
+ const { spawn } = require('child_process');
2
+ const os = require('os');
3
+
4
+ /**
5
+ * Cross-platform clipboard utility
6
+ */
7
+ class ClipboardManager {
8
+ /**
9
+ * Copy text to clipboard
10
+ */
11
+ static async copyToClipboard(text) {
12
+ const platform = os.platform();
13
+
14
+ try {
15
+ switch (platform) {
16
+ case 'darwin': // macOS
17
+ return await this.copyMacOS(text);
18
+ case 'win32': // Windows
19
+ return await this.copyWindows(text);
20
+ case 'linux': // Linux
21
+ return await this.copyLinux(text);
22
+ default:
23
+ throw new Error(`Unsupported platform: ${platform}`);
24
+ }
25
+ } catch (error) {
26
+ throw new Error(`Failed to copy to clipboard: ${error.message}`);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Copy to clipboard on macOS
32
+ */
33
+ static async copyMacOS(text) {
34
+ return new Promise((resolve, reject) => {
35
+ const pbcopy = spawn('pbcopy');
36
+
37
+ pbcopy.stdin.write(text);
38
+ pbcopy.stdin.end();
39
+
40
+ pbcopy.on('close', (code) => {
41
+ if (code === 0) {
42
+ resolve();
43
+ } else {
44
+ reject(new Error(`pbcopy exited with code ${code}`));
45
+ }
46
+ });
47
+
48
+ pbcopy.on('error', (error) => {
49
+ reject(error);
50
+ });
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Copy to clipboard on Windows
56
+ */
57
+ static async copyWindows(text) {
58
+ return new Promise((resolve, reject) => {
59
+ const clip = spawn('clip');
60
+
61
+ clip.stdin.write(text);
62
+ clip.stdin.end();
63
+
64
+ clip.on('close', (code) => {
65
+ if (code === 0) {
66
+ resolve();
67
+ } else {
68
+ reject(new Error(`clip exited with code ${code}`));
69
+ }
70
+ });
71
+
72
+ clip.on('error', (error) => {
73
+ reject(error);
74
+ });
75
+ });
76
+ }
77
+
78
+ /**
79
+ * Copy to clipboard on Linux
80
+ */
81
+ static async copyLinux(text) {
82
+ // Try xclip first, then xsel as fallback
83
+ try {
84
+ return await this.copyLinuxXclip(text);
85
+ } catch (error) {
86
+ try {
87
+ return await this.copyLinuxXsel(text);
88
+ } catch (xselError) {
89
+ throw new Error('Neither xclip nor xsel is available. Please install one of them.');
90
+ }
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Copy using xclip
96
+ */
97
+ static async copyLinuxXclip(text) {
98
+ return new Promise((resolve, reject) => {
99
+ const xclip = spawn('xclip', ['-selection', 'clipboard']);
100
+
101
+ xclip.stdin.write(text);
102
+ xclip.stdin.end();
103
+
104
+ xclip.on('close', (code) => {
105
+ if (code === 0) {
106
+ resolve();
107
+ } else {
108
+ reject(new Error(`xclip exited with code ${code}`));
109
+ }
110
+ });
111
+
112
+ xclip.on('error', (error) => {
113
+ reject(error);
114
+ });
115
+ });
116
+ }
117
+
118
+ /**
119
+ * Copy using xsel
120
+ */
121
+ static async copyLinuxXsel(text) {
122
+ return new Promise((resolve, reject) => {
123
+ const xsel = spawn('xsel', ['--clipboard', '--input']);
124
+
125
+ xsel.stdin.write(text);
126
+ xsel.stdin.end();
127
+
128
+ xsel.on('close', (code) => {
129
+ if (code === 0) {
130
+ resolve();
131
+ } else {
132
+ reject(new Error(`xsel exited with code ${code}`));
133
+ }
134
+ });
135
+
136
+ xsel.on('error', (error) => {
137
+ reject(error);
138
+ });
139
+ });
140
+ }
141
+
142
+ /**
143
+ * Get clipboard content size info
144
+ */
145
+ static getContentInfo(text) {
146
+ const lines = text.split('\n').length;
147
+ const chars = text.length;
148
+ const bytes = Buffer.byteLength(text, 'utf8');
149
+
150
+ return {
151
+ lines,
152
+ characters: chars,
153
+ bytes,
154
+ size: this.formatBytes(bytes)
155
+ };
156
+ }
157
+
158
+ /**
159
+ * Format bytes to human readable
160
+ */
161
+ static formatBytes(bytes) {
162
+ if (bytes === 0) return '0 Bytes';
163
+ const k = 1024;
164
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
165
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
166
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
167
+ }
168
+ }
169
+
170
+ module.exports = ClipboardManager;
@@ -0,0 +1 @@
1
+ {"name": "test-large", "version": "1.0.0"}
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file