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 +10 -0
- package/README.md +1 -0
- package/package.json +1 -1
- package/src/exporter/html-exporter.js +125 -1
- package/src/ignore/ignore-manager.js +27 -13
- package/src/index.js +53 -6
- package/src/scanner/file-scanner.js +48 -0
- package/src/tokenizer/token-manager.js +8 -11
- package/src/utils/clipboard.js +170 -0
- package/test-small/package-lock.json +1 -0
- package/vg-coder-cli-1.0.6.tgz +0 -0
- package/vg-coder-cli-1.0.7.tgz +0 -0
- package/vg-coder-cli-1.0.2.tgz +0 -0
- package/vg-coder-cli-1.0.3.tgz +0 -0
- package/vg-coder-cli-1.0.4.tgz +0 -0
- package/vg-coder-cli-1.0.5.tgz +0 -0
package/.vgignore
ADDED
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
|
@@ -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.
|
|
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.
|
|
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
|
|
149
|
+
* Thêm patterns từ .gitignore và .vgignore files
|
|
148
150
|
*/
|
|
149
|
-
async
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
251
|
-
|
|
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
|
|
257
|
-
const partHeader = header.replace('{part}', (chunkIndex + 1).toString());
|
|
254
|
+
|
|
255
|
+
if (currentTokens + lineTokens > this.options.maxTokens && currentChunk) {
|
|
258
256
|
chunks.push({
|
|
259
|
-
content:
|
|
260
|
-
tokens: this.countTokens(
|
|
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:
|
|
283
|
-
tokens: this.countTokens(
|
|
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
|
package/vg-coder-cli-1.0.2.tgz
DELETED
|
Binary file
|
package/vg-coder-cli-1.0.3.tgz
DELETED
|
Binary file
|
package/vg-coder-cli-1.0.4.tgz
DELETED
|
Binary file
|
package/vg-coder-cli-1.0.5.tgz
DELETED
|
Binary file
|