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 +6 -19
- package/src/server/api-server.js +93 -297
- package/src/server/terminal-manager.js +82 -0
- package/src/server/views/css/git-view.css +155 -0
- package/src/server/views/css/terminal.css +98 -0
- package/src/server/views/dashboard.html +39 -3
- package/src/server/views/js/api.js +15 -0
- package/src/server/views/js/features/git-view.js +117 -0
- package/src/server/views/js/features/terminal.js +174 -0
- package/src/server/views/js/main.js +18 -40
- package/vg-coder-cli-2.0.12.tgz +0 -0
- package/vg-coder-cli-2.0.14.tgz +0 -0
- package/vg-coder-cli-2.0.11.tgz +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vg-coder-cli",
|
|
3
|
-
"version": "2.0.
|
|
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
|
-
"
|
|
25
|
-
"
|
|
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",
|
package/src/server/api-server.js
CHANGED
|
@@ -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();
|
|
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' }));
|
|
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
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
this.app.post('/api/analyze', async (req, res) => {
|
|
102
|
+
this.app.get('/api/git/diff', async (req, res) => {
|
|
80
103
|
try {
|
|
81
|
-
const {
|
|
82
|
-
|
|
83
|
-
|
|
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('
|
|
139
|
-
res.
|
|
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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
353
|
-
this.server = this.
|
|
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
|
-
|
|
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();
|