vg-coder-cli 2.0.11 → 2.0.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vg-coder-cli",
3
- "version": "2.0.11",
3
+ "version": "2.0.14",
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": {
@@ -21,19 +21,8 @@
21
21
  "code-analysis",
22
22
  "token-counter",
23
23
  "ai-helper",
24
- "gitignore",
25
- "html-export",
26
- "syntax-highlighting",
27
- "project-analyzer",
28
- "source-code",
29
- "tiktoken",
30
- "spring-boot",
31
- "angular",
32
- "react",
33
- "vue",
34
- "java",
35
- "javascript",
36
- "typescript"
24
+ "terminal",
25
+ "web-terminal"
37
26
  ],
38
27
  "author": "VG Coder",
39
28
  "license": "MIT",
@@ -41,10 +30,6 @@
41
30
  "type": "git",
42
31
  "url": "https://github.com/tinhthanh/vg-coder-cli.git"
43
32
  },
44
- "bugs": {
45
- "url": "https://github.com/tinhthanh/vg-coder-cli/issues"
46
- },
47
- "homepage": "https://github.com/tinhthanh/vg-coder-cli#readme",
48
33
  "dependencies": {
49
34
  "commander": "^11.1.0",
50
35
  "directory-tree": "^3.5.1",
@@ -57,7 +42,9 @@
57
42
  "ora": "^5.4.1",
58
43
  "express": "^4.18.2",
59
44
  "cors": "^2.8.5",
60
- "body-parser": "^1.20.2"
45
+ "body-parser": "^1.20.2",
46
+ "node-pty": "^1.0.0",
47
+ "socket.io": "^4.7.2"
61
48
  },
62
49
  "devDependencies": {
63
50
  "jest": "^29.7.0",
@@ -4,379 +4,175 @@ const bodyParser = require('body-parser');
4
4
  const path = require('path');
5
5
  const fs = require('fs-extra');
6
6
  const chalk = require('chalk');
7
+ const http = require('http');
8
+ const { Server } = require('socket.io');
9
+ const { exec } = require('child_process');
10
+ const util = require('util');
11
+ const execAsync = util.promisify(exec);
7
12
  const packageJson = require('../../package.json');
8
13
 
9
14
  const ProjectDetector = require('../detectors/project-detector');
10
15
  const FileScanner = require('../scanner/file-scanner');
11
16
  const TokenManager = require('../tokenizer/token-manager');
12
17
  const BashExecutor = require('../utils/bash-executor');
18
+ const terminalManager = require('./terminal-manager');
13
19
 
14
- /**
15
- * API Server for VG Coder CLI
16
- */
17
20
  class ApiServer {
18
21
  constructor(port = 6868) {
19
22
  this.port = port;
20
23
  this.app = express();
24
+
25
+ // Create HTTP server for Socket.IO
26
+ this.httpServer = http.createServer(this.app);
27
+ this.io = new Server(this.httpServer, {
28
+ cors: { origin: "*" }
29
+ });
30
+
21
31
  this.server = null;
22
- this.workingDir = process.cwd(); // Track working directory
32
+ this.workingDir = process.cwd();
23
33
  this.setupMiddleware();
24
34
  this.setupRoutes();
35
+ this.setupSocketIO();
25
36
  }
26
37
 
27
- /**
28
- * Setup Express middleware
29
- */
30
38
  setupMiddleware() {
31
39
  this.app.use(cors());
32
- this.app.use(bodyParser.json({ limit: '50mb' })); // Increase limit for large file lists
40
+ this.app.use(bodyParser.json({ limit: '50mb' }));
33
41
  this.app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
34
-
35
- // Serve static files from views directory (CSS, JS)
36
42
  this.app.use(express.static(path.join(__dirname, 'views')));
37
43
 
38
- // Request logging
39
44
  this.app.use((req, res, next) => {
40
- console.log(chalk.blue(`[${new Date().toISOString()}] ${req.method} ${req.path}`));
45
+ if (!req.path.includes('.')) {
46
+ console.log(chalk.blue(`[REQ] ${req.method} ${req.path}`));
47
+ }
41
48
  next();
42
49
  });
43
50
  }
44
51
 
45
- /**
46
- * Setup API routes
47
- */
48
- setupRoutes() {
49
- // Dashboard - serve HTML interface
50
- this.app.get('/', (req, res) => {
51
- res.sendFile(path.join(__dirname, 'views', 'dashboard.html'));
52
- });
52
+ setupSocketIO() {
53
+ this.io.on('connection', (socket) => {
54
+ // Nhận event init với termId
55
+ // FIX: Thêm default value = {} để tránh crash khi data undefined
56
+ socket.on('terminal:init', (data) => {
57
+ if (!data || !data.termId) {
58
+ // console.warn('[Socket] Ignored invalid terminal:init', data);
59
+ return;
60
+ }
61
+ const { termId, cols, rows } = data;
62
+ terminalManager.createTerminal(socket, termId, cols, rows, this.workingDir);
63
+ });
53
64
 
54
- // Health check
55
- this.app.get('/health', (req, res) => {
56
- res.json({
57
- status: 'ok',
58
- version: packageJson.version,
59
- timestamp: new Date().toISOString()
60
- });
65
+ socket.on('terminal:input', (data) => {
66
+ if (data && data.termId) {
67
+ terminalManager.write(data.termId, data.data);
68
+ }
69
+ });
70
+
71
+ socket.on('terminal:resize', (data) => {
72
+ if (data && data.termId) {
73
+ terminalManager.resize(data.termId, data.cols, data.rows);
74
+ }
75
+ });
76
+
77
+ socket.on('terminal:kill', (data) => {
78
+ if (data && data.termId) {
79
+ terminalManager.kill(data.termId);
80
+ }
81
+ });
82
+
83
+ socket.on('disconnect', () => {
84
+ terminalManager.cleanupSocket(socket.id);
85
+ });
61
86
  });
87
+ }
88
+
89
+ setupRoutes() {
90
+ this.app.get('/', (req, res) => res.sendFile(path.join(__dirname, 'views', 'dashboard.html')));
91
+ this.app.get('/health', (req, res) => res.json({ status: 'ok', version: packageJson.version }));
62
92
 
63
- // NEW: Get Extension Path
64
93
  this.app.get('/api/extension-path', (req, res) => {
65
94
  try {
66
95
  const extensionPath = path.join(__dirname, 'views', 'vg-coder');
67
- const exists = fs.existsSync(extensionPath);
68
-
69
- res.json({
70
- path: extensionPath,
71
- exists: exists
72
- });
96
+ res.json({ path: extensionPath, exists: fs.existsSync(extensionPath) });
73
97
  } catch (error) {
74
98
  res.status(500).json({ error: error.message });
75
99
  }
76
100
  });
77
101
 
78
- // Analyze endpoint - returns project.txt file
79
- this.app.post('/api/analyze', async (req, res) => {
102
+ this.app.get('/api/git/diff', async (req, res) => {
80
103
  try {
81
- const { path: projectPath, options = {}, specificFiles } = req.body;
82
-
83
- if (!projectPath) {
84
- return res.status(400).json({
85
- error: 'Missing required field: path'
86
- });
87
- }
88
-
89
- const resolvedPath = path.resolve(projectPath);
90
-
91
- // Validate project path
92
- if (!await fs.pathExists(resolvedPath)) {
93
- return res.status(404).json({
94
- error: `Project path does not exist: ${projectPath}`
95
- });
96
- }
97
-
98
- console.log(chalk.yellow(`Analyzing project: ${resolvedPath}`));
99
- if (specificFiles) {
100
- console.log(chalk.yellow(`Filtering for ${specificFiles.length} specific files`));
101
- }
102
-
103
- // Detect project type
104
- const detector = new ProjectDetector(resolvedPath);
105
- const projectInfo = await detector.detectAll();
106
-
107
- // Scan files (no token limit, get all files)
108
- const scannerOptions = {
109
- extensions: options.extensions ? options.extensions.split(',').map(ext => ext.trim()) : undefined,
110
- includeHidden: options.includeHidden || false
111
- };
112
-
113
- const scanner = new FileScanner(resolvedPath, scannerOptions);
114
- const scanResult = await scanner.scanProject();
115
-
116
- let filesToProcess = scanResult.files;
117
-
118
- // Filter specific files if requested
119
- if (specificFiles && Array.isArray(specificFiles) && specificFiles.length > 0) {
120
- filesToProcess = filesToProcess.filter(file => specificFiles.includes(file.relativePath));
121
- }
122
-
123
- // Create AI-friendly content
124
- const aiContent = await scanner.createCombinedContentForAI(filesToProcess, {
125
- includeStats: true,
126
- includeTree: true,
127
- preserveLineNumbers: true
104
+ const { stdout, stderr } = await execAsync('git diff HEAD', {
105
+ cwd: this.workingDir,
106
+ maxBuffer: 20 * 1024 * 1024
128
107
  });
129
-
130
- // Set response headers for file download
131
- res.setHeader('Content-Type', 'text/plain; charset=utf-8');
132
- res.setHeader('Content-Disposition', 'attachment; filename="project.txt"');
133
- res.send(aiContent);
134
-
135
- console.log(chalk.green(`✓ Analysis completed: ${filesToProcess.length} files`));
136
-
108
+ res.json({ diff: stdout });
137
109
  } catch (error) {
138
- console.error(chalk.red('Error during analysis:'), error);
139
- res.status(500).json({
140
- error: 'Analysis failed',
141
- message: error.message
142
- });
110
+ console.error(chalk.red(' [GIT] Error:'), error.message);
111
+ res.json({ diff: '', error: error.message });
143
112
  }
144
113
  });
145
114
 
146
- // Info endpoint
115
+ this.app.post('/api/analyze', async (req, res) => {
116
+ const { path: projectPath, options = {}, specificFiles } = req.body;
117
+ if (!projectPath) return res.status(400).json({ error: 'Missing path' });
118
+ const resolvedPath = path.resolve(projectPath);
119
+ if (!await fs.pathExists(resolvedPath)) return res.status(404).json({ error: 'Path not found' });
120
+ const scanner = new FileScanner(resolvedPath, {
121
+ extensions: options.extensions ? options.extensions.split(',') : undefined,
122
+ includeHidden: options.includeHidden
123
+ });
124
+ let scanResult = await scanner.scanProject();
125
+ let filesToProcess = scanResult.files;
126
+ if (specificFiles?.length) filesToProcess = filesToProcess.filter(f => specificFiles.includes(f.relativePath));
127
+ const content = await scanner.createCombinedContentForAI(filesToProcess, { includeStats: true, preserveLineNumbers: true });
128
+ res.send(content);
129
+ });
130
+
147
131
  this.app.get('/api/info', async (req, res) => {
148
- try {
149
132
  const projectPath = req.query.path;
150
-
151
- if (!projectPath) {
152
- return res.status(400).json({
153
- error: 'Missing required query parameter: path'
154
- });
155
- }
156
-
133
+ if (!projectPath) return res.status(400).json({ error: 'Missing path' });
157
134
  const resolvedPath = path.resolve(projectPath);
158
-
159
- if (!await fs.pathExists(resolvedPath)) {
160
- return res.status(404).json({
161
- error: `Project path does not exist: ${projectPath}`
162
- });
163
- }
164
-
165
- // Detect project
166
135
  const detector = new ProjectDetector(resolvedPath);
167
136
  const projectInfo = await detector.detectAll();
168
-
169
- // Quick scan
170
137
  const scanner = new FileScanner(resolvedPath);
171
138
  const scanResult = await scanner.scanProject();
172
-
173
- // Token analysis
174
- const tokenManager = new TokenManager();
175
- const tokenAnalysis = tokenManager.analyzeFiles(scanResult.files);
176
- tokenManager.cleanup();
177
-
178
- const extensions = [...new Set(scanResult.files.map(f => f.extension))].filter(Boolean);
179
-
180
- res.json({
181
- path: resolvedPath,
182
- primaryType: projectInfo.primary,
183
- detectedTechnologies: projectInfo.detected,
184
- stats: {
185
- totalFiles: scanResult.stats.processedFiles,
186
- totalSize: scanResult.files.reduce((sum, f) => sum + f.size, 0),
187
- totalLines: scanResult.files.reduce((sum, f) => sum + f.lines, 0),
188
- extensions: extensions
189
- },
190
- tokens: {
191
- total: tokenAnalysis.summary.totalTokens,
192
- averagePerFile: tokenAnalysis.summary.averageTokensPerFile,
193
- filesExceedingLimit: tokenAnalysis.summary.filesExceedingLimit,
194
- estimatedChunks: tokenAnalysis.summary.estimatedChunks
195
- }
196
- });
197
-
198
- console.log(chalk.green(`✓ Info retrieved for: ${resolvedPath}`));
199
-
200
- } catch (error) {
201
- console.error(chalk.red('Error getting info:'), error);
202
- res.status(500).json({
203
- error: 'Failed to get project info',
204
- message: error.message
205
- });
206
- }
139
+ res.json({ path: resolvedPath, primaryType: projectInfo.primary, stats: { totalFiles: scanResult.files.length } });
207
140
  });
208
141
 
209
- // Structure endpoint
210
142
  this.app.get('/api/structure', async (req, res) => {
211
- try {
212
143
  const projectPath = req.query.path || '.';
213
144
  const resolvedPath = path.resolve(projectPath);
214
-
215
- // Validate path
216
- if (!await fs.pathExists(resolvedPath)) {
217
- return res.status(404).json({
218
- error: `Project path does not exist: ${projectPath}`
219
- });
220
- }
221
-
222
- console.log(chalk.yellow(`Analyzing structure for: ${resolvedPath}`));
223
-
224
- // 1. Scan files
225
145
  const scanner = new FileScanner(resolvedPath);
226
146
  const scanResult = await scanner.scanProject();
227
-
228
- // 2. Tokenize tree
229
147
  const tokenManager = new TokenManager();
230
148
  const enrichedTree = tokenManager.analyzeTree(scanResult.tree, scanResult.files);
231
-
232
- tokenManager.cleanup();
233
-
234
- // 3. Return result
235
- res.json({
236
- path: resolvedPath,
237
- totalFiles: scanResult.files.length,
238
- rootTokens: enrichedTree.tokens,
239
- structure: enrichedTree
240
- });
241
-
242
- console.log(chalk.green(`✓ Structure analysis completed: ${scanResult.files.length} files`));
243
-
244
- } catch (error) {
245
- console.error(chalk.red('Error getting structure:'), error);
246
- res.status(500).json({
247
- error: 'Failed to get structure',
248
- message: error.message
249
- });
250
- }
149
+ res.json({ path: resolvedPath, structure: enrichedTree });
251
150
  });
252
151
 
253
- // Clean endpoint
254
- this.app.delete('/api/clean', async (req, res) => {
255
- try {
256
- const { output } = req.body;
257
-
258
- if (!output) {
259
- return res.status(400).json({
260
- error: 'Missing required field: output'
261
- });
262
- }
263
-
264
- const outputPath = path.resolve(output);
265
-
266
- if (await fs.pathExists(outputPath)) {
267
- await fs.remove(outputPath);
268
- res.json({
269
- success: true,
270
- message: `Cleaned: ${outputPath}`
271
- });
272
- console.log(chalk.green(`✓ Cleaned: ${outputPath}`));
273
- } else {
274
- res.json({
275
- success: true,
276
- message: 'Output directory does not exist'
277
- });
278
- }
279
-
280
- } catch (error) {
281
- console.error(chalk.red('Error cleaning:'), error);
282
- res.status(500).json({
283
- error: 'Failed to clean',
284
- message: error.message
285
- });
286
- }
287
- });
288
-
289
- // Execute bash script endpoint
290
152
  this.app.post('/api/execute', async (req, res) => {
291
- try {
292
153
  const { bash } = req.body;
293
-
294
- if (!bash) {
295
- return res.status(400).json({
296
- error: 'Missing required field: bash'
297
- });
298
- }
299
-
300
- console.log(chalk.yellow(`Executing bash script (${bash.length} chars)...`));
301
-
302
- // Create executor with working directory
303
154
  const executor = new BashExecutor(this.workingDir);
304
-
305
- // Execute script (validates syntax first, then executes)
306
155
  const result = await executor.execute(bash);
307
-
308
- if (result.success) {
309
- console.log(chalk.green(`✓ Bash execution completed in ${result.executionTime}ms`));
310
- res.json(result);
311
- } else {
312
- // Check if it's a syntax error
313
- const isSyntaxError = result.error === 'Syntax validation failed';
314
- console.log(chalk.red(`✗ Bash execution failed: ${result.error || 'Exit code ' + result.exitCode}`));
315
- res.status(400).json({
316
- ...result,
317
- syntaxError: isSyntaxError
318
- });
319
- }
320
-
321
- } catch (error) {
322
- console.error(chalk.red('Error executing bash:'), error);
323
- res.status(500).json({
324
- error: 'Execution failed',
325
- message: error.message
326
- });
327
- }
328
- });
329
-
330
- // 404 handler
331
- this.app.use((req, res) => {
332
- res.status(404).json({
333
- error: 'Not found',
334
- message: `Route ${req.method} ${req.path} not found`
335
- });
156
+ res.status(result.success ? 200 : 400).json(result);
336
157
  });
337
-
338
- // Error handler
339
- this.app.use((err, req, res, next) => {
340
- console.error(chalk.red('Server error:'), err);
341
- res.status(500).json({
342
- error: 'Internal server error',
343
- message: err.message
344
- });
158
+
159
+ this.app.delete('/api/clean', async (req, res) => {
160
+ await fs.remove(path.resolve(req.body.output));
161
+ res.json({ success: true });
345
162
  });
346
163
  }
347
164
 
348
- /**
349
- * Start the server
350
- */
351
165
  async start() {
352
- return new Promise((resolve, reject) => {
353
- this.server = this.app.listen(this.port, () => {
354
- console.log(chalk.green(`\n🚀 VG Coder API Server started!`));
355
- console.log(chalk.blue(`📡 Listening on: http://localhost:${this.port}`));
356
- console.log(chalk.cyan(`\n🎨 Dashboard: http://localhost:${this.port}`));
166
+ return new Promise((resolve) => {
167
+ this.server = this.httpServer.listen(this.port, () => {
168
+ console.log(chalk.green(`\n🚀 VG Coder API Server & Socket.IO started on port ${this.port}`));
357
169
  resolve();
358
170
  });
359
-
360
- this.server.on('error', (err) => {
361
- reject(err);
362
- });
363
171
  });
364
172
  }
365
173
 
366
- /**
367
- * Stop the server
368
- */
369
174
  async stop() {
370
- return new Promise((resolve) => {
371
- if (this.server) {
372
- this.server.close(() => {
373
- console.log(chalk.yellow('\n👋 Server stopped\n'));
374
- resolve();
375
- });
376
- } else {
377
- resolve();
378
- }
379
- });
175
+ if (this.server) this.server.close();
380
176
  }
381
177
  }
382
178
 
@@ -0,0 +1,82 @@
1
+ const os = require('os');
2
+ const pty = require('node-pty');
3
+
4
+ class TerminalManager {
5
+ constructor() {
6
+ // Map: termId -> { process: pty, socketId: string }
7
+ this.sessions = new Map();
8
+ this.shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
9
+ }
10
+
11
+ createTerminal(socket, termId, cols = 80, rows = 24, cwd) {
12
+ try {
13
+ const term = pty.spawn(this.shell, [], {
14
+ name: 'xterm-256color',
15
+ cols: cols,
16
+ rows: rows,
17
+ cwd: cwd || process.cwd(),
18
+ env: process.env
19
+ });
20
+
21
+ this.sessions.set(termId, {
22
+ process: term,
23
+ socketId: socket.id
24
+ });
25
+
26
+ // Gửi dữ liệu kèm theo termId để Frontend biết của cửa sổ nào
27
+ term.onData((data) => {
28
+ socket.emit('terminal:data', { termId, data });
29
+ });
30
+
31
+ term.onExit(() => {
32
+ if (this.sessions.has(termId)) {
33
+ socket.emit('terminal:exit', { termId });
34
+ this.sessions.delete(termId);
35
+ }
36
+ });
37
+
38
+ return term;
39
+ } catch (error) {
40
+ console.error('Failed to create terminal:', error);
41
+ socket.emit('terminal:error', { termId, error: 'Failed to create process' });
42
+ }
43
+ }
44
+
45
+ write(termId, data) {
46
+ const session = this.sessions.get(termId);
47
+ if (session) {
48
+ session.process.write(data);
49
+ }
50
+ }
51
+
52
+ resize(termId, cols, rows) {
53
+ const session = this.sessions.get(termId);
54
+ if (session) {
55
+ try {
56
+ session.process.resize(cols, rows);
57
+ } catch (err) {
58
+ // Ignore resize errors
59
+ }
60
+ }
61
+ }
62
+
63
+ kill(termId) {
64
+ const session = this.sessions.get(termId);
65
+ if (session) {
66
+ session.process.kill();
67
+ this.sessions.delete(termId);
68
+ }
69
+ }
70
+
71
+ // Dọn dẹp tất cả terminal của một socket khi client disconnect
72
+ cleanupSocket(socketId) {
73
+ for (const [termId, session] of this.sessions.entries()) {
74
+ if (session.socketId === socketId) {
75
+ session.process.kill();
76
+ this.sessions.delete(termId);
77
+ }
78
+ }
79
+ }
80
+ }
81
+
82
+ module.exports = new TerminalManager();