vg-coder-cli 2.0.24 → 2.0.26
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/.vg/tree-state.json +1 -7
- package/package.json +1 -1
- package/src/index.js +36 -0
- package/src/server/api-server.js +241 -29
- package/src/server/project-manager.js +353 -0
- package/src/server/terminal-manager.js +130 -3
- package/src/server/views/css/iframe.css +1 -0
- package/src/server/views/css/terminal.css +88 -0
- package/src/server/views/dashboard.css +391 -16
- package/src/server/views/dashboard.html +150 -19
- package/src/server/views/js/features/commands.js +230 -0
- package/src/server/views/js/features/iframe-manager.js +6 -2
- package/src/server/views/js/features/project-switcher.js +153 -0
- package/src/server/views/js/features/terminal.js +280 -8
- package/src/server/views/js/main.js +65 -0
- package/src/server/views/js/utils/log-utils.js +164 -0
- package/src/server/views/js/utils/smart-copy-engine.js +283 -0
- package/src/server/views/vg-coder/controller.js +376 -2
- package/vetgo-auto/chrome/src/controller.ts +75 -1
- package/vetgo-auto/chrome/src/utils/ai-domains.ts +33 -0
- package/vetgo-auto/chrome/src/utils/injector-script.ts +251 -0
- package/vetgo-auto/vg-coder.zip +0 -0
- package/vg-coder-cli-2.0.26.tgz +0 -0
- package/vg-coder-cli-2.0.23.tgz +0 -0
- package/vg-coder-cli-2.0.24.tgz +0 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const http = require('http');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* ProjectManager - Singleton for managing multiple projects in one server
|
|
10
|
+
*/
|
|
11
|
+
class ProjectManager {
|
|
12
|
+
constructor() {
|
|
13
|
+
if (ProjectManager.instance) {
|
|
14
|
+
return ProjectManager.instance;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
this.projects = new Map(); // projectId -> ProjectContext
|
|
18
|
+
this.currentProjectId = null;
|
|
19
|
+
this.lockFilePath = path.join(os.homedir(), '.vg', 'vg-leader.lock');
|
|
20
|
+
|
|
21
|
+
ProjectManager.instance = this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Register a new project
|
|
26
|
+
* @param {string} workingDir - Absolute path to project directory
|
|
27
|
+
* @returns {string} projectId
|
|
28
|
+
*/
|
|
29
|
+
registerProject(workingDir) {
|
|
30
|
+
const resolvedPath = path.resolve(workingDir);
|
|
31
|
+
const projectId = this.generateProjectId(resolvedPath);
|
|
32
|
+
|
|
33
|
+
// Check if already registered
|
|
34
|
+
if (this.projects.has(projectId)) {
|
|
35
|
+
console.log(chalk.yellow(`⚠️ Project already registered: ${path.basename(resolvedPath)}`));
|
|
36
|
+
return projectId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const projectContext = {
|
|
40
|
+
id: projectId,
|
|
41
|
+
name: path.basename(resolvedPath),
|
|
42
|
+
workingDir: resolvedPath,
|
|
43
|
+
active: this.projects.size === 0, // First project is active by default
|
|
44
|
+
createdAt: Date.now(),
|
|
45
|
+
lastActive: Date.now()
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.projects.set(projectId, projectContext);
|
|
49
|
+
|
|
50
|
+
// Set as current if first project
|
|
51
|
+
if (this.projects.size === 1) {
|
|
52
|
+
this.currentProjectId = projectId;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log(chalk.green(`✓ Registered project: ${chalk.cyan(projectContext.name)} (${this.projects.size} total)`));
|
|
56
|
+
|
|
57
|
+
return projectId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Switch to a different project
|
|
62
|
+
* @param {string} projectId
|
|
63
|
+
* @returns {boolean} success
|
|
64
|
+
*/
|
|
65
|
+
switchProject(projectId) {
|
|
66
|
+
if (!this.projects.has(projectId)) {
|
|
67
|
+
console.error(chalk.red(`❌ Project not found: ${projectId}`));
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Deactivate current project
|
|
72
|
+
if (this.currentProjectId) {
|
|
73
|
+
const current = this.projects.get(this.currentProjectId);
|
|
74
|
+
if (current) {
|
|
75
|
+
current.active = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Activate new project
|
|
80
|
+
const newProject = this.projects.get(projectId);
|
|
81
|
+
newProject.active = true;
|
|
82
|
+
newProject.lastActive = Date.now();
|
|
83
|
+
this.currentProjectId = projectId;
|
|
84
|
+
|
|
85
|
+
console.log(chalk.blue(`Switched to project: ${chalk.cyan(newProject.name)}`));
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get currently active project
|
|
92
|
+
* @returns {ProjectContext|null}
|
|
93
|
+
*/
|
|
94
|
+
getActiveProject() {
|
|
95
|
+
if (!this.currentProjectId) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return this.projects.get(this.currentProjectId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get all projects
|
|
103
|
+
* @returns {Array<ProjectContext>}
|
|
104
|
+
*/
|
|
105
|
+
getAllProjects() {
|
|
106
|
+
return Array.from(this.projects.values())
|
|
107
|
+
.sort((a, b) => b.lastActive - a.lastActive); // Sort by last active
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Remove a project
|
|
112
|
+
* @param {string} projectId
|
|
113
|
+
*/
|
|
114
|
+
removeProject(projectId) {
|
|
115
|
+
const project = this.projects.get(projectId);
|
|
116
|
+
if (!project) return;
|
|
117
|
+
|
|
118
|
+
this.projects.delete(projectId);
|
|
119
|
+
console.log(chalk.yellow(`Removed project: ${project.name}`));
|
|
120
|
+
|
|
121
|
+
// If removed project was active, switch to another
|
|
122
|
+
if (projectId === this.currentProjectId) {
|
|
123
|
+
const remaining = Array.from(this.projects.keys());
|
|
124
|
+
if (remaining.length > 0) {
|
|
125
|
+
this.switchProject(remaining[0]);
|
|
126
|
+
} else {
|
|
127
|
+
this.currentProjectId = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Generate unique project ID from path
|
|
134
|
+
* @param {string} projectPath
|
|
135
|
+
* @returns {string}
|
|
136
|
+
*/
|
|
137
|
+
generateProjectId(projectPath) {
|
|
138
|
+
return crypto
|
|
139
|
+
.createHash('md5')
|
|
140
|
+
.update(projectPath)
|
|
141
|
+
.digest('hex')
|
|
142
|
+
.substring(0, 8);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Acquire leader lock
|
|
147
|
+
* @param {number} port
|
|
148
|
+
* @param {number} pid
|
|
149
|
+
* @returns {Promise<boolean>}
|
|
150
|
+
*/
|
|
151
|
+
async acquireLock(port, pid = process.pid) {
|
|
152
|
+
try {
|
|
153
|
+
// Ensure .vg directory exists
|
|
154
|
+
await fs.ensureDir(path.dirname(this.lockFilePath));
|
|
155
|
+
|
|
156
|
+
// Check if lock already exists
|
|
157
|
+
if (await fs.pathExists(this.lockFilePath)) {
|
|
158
|
+
const existingLock = await fs.readJson(this.lockFilePath);
|
|
159
|
+
|
|
160
|
+
// Check if existing leader is still alive
|
|
161
|
+
const isAlive = await this.checkLeaderHealth(existingLock.port);
|
|
162
|
+
if (isAlive) {
|
|
163
|
+
console.log(chalk.yellow('⚠️ Leader already exists'));
|
|
164
|
+
return false;
|
|
165
|
+
} else {
|
|
166
|
+
console.log(chalk.yellow('⚠️ Stale lock file detected, cleaning up...'));
|
|
167
|
+
await fs.remove(this.lockFilePath);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Write lock file
|
|
172
|
+
const lockData = {
|
|
173
|
+
port,
|
|
174
|
+
pid,
|
|
175
|
+
startTime: Date.now(),
|
|
176
|
+
hostname: os.hostname()
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
await fs.writeJson(this.lockFilePath, lockData, { spaces: 2 });
|
|
180
|
+
console.log(chalk.green(`✓ Acquired leader lock (PID: ${pid}, Port: ${port})`));
|
|
181
|
+
|
|
182
|
+
return true;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error(chalk.red('❌ Failed to acquire lock:'), error.message);
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Release leader lock
|
|
191
|
+
* @returns {Promise<void>}
|
|
192
|
+
*/
|
|
193
|
+
async releaseLock() {
|
|
194
|
+
try {
|
|
195
|
+
if (await fs.pathExists(this.lockFilePath)) {
|
|
196
|
+
await fs.remove(this.lockFilePath);
|
|
197
|
+
console.log(chalk.green('✓ Released leader lock'));
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(chalk.red('❌ Failed to release lock:'), error.message);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Check if leader exists and is healthy
|
|
206
|
+
* @returns {Promise<Object|null>} Leader info or null
|
|
207
|
+
*/
|
|
208
|
+
async checkLeader() {
|
|
209
|
+
try {
|
|
210
|
+
if (!await fs.pathExists(this.lockFilePath)) {
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const lockData = await fs.readJson(this.lockFilePath);
|
|
215
|
+
|
|
216
|
+
// Check if leader is healthy
|
|
217
|
+
const isHealthy = await this.checkLeaderHealth(lockData.port);
|
|
218
|
+
|
|
219
|
+
if (isHealthy) {
|
|
220
|
+
return lockData;
|
|
221
|
+
} else {
|
|
222
|
+
// Stale lock file
|
|
223
|
+
console.log(chalk.yellow('⚠️ Leader not responding, cleaning up stale lock...'));
|
|
224
|
+
await fs.remove(this.lockFilePath);
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error(chalk.red('❌ Error checking leader:'), error.message);
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Check if leader server is healthy
|
|
235
|
+
* @param {number} port
|
|
236
|
+
* @returns {Promise<boolean>}
|
|
237
|
+
*/
|
|
238
|
+
async checkLeaderHealth(port) {
|
|
239
|
+
return new Promise((resolve) => {
|
|
240
|
+
const req = http.get(`http://localhost:${port}/health`, (res) => {
|
|
241
|
+
resolve(res.statusCode === 200);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
req.on('error', () => {
|
|
245
|
+
resolve(false);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
req.setTimeout(2000, () => {
|
|
249
|
+
req.destroy();
|
|
250
|
+
resolve(false);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Join existing leader as follower
|
|
257
|
+
* @param {Object} leaderInfo
|
|
258
|
+
* @param {string} projectPath
|
|
259
|
+
* @returns {Promise<boolean>}
|
|
260
|
+
*/
|
|
261
|
+
async joinLeader(leaderInfo, projectPath) {
|
|
262
|
+
try {
|
|
263
|
+
const { port } = leaderInfo;
|
|
264
|
+
const resolvedPath = path.resolve(projectPath);
|
|
265
|
+
const projectName = path.basename(resolvedPath);
|
|
266
|
+
|
|
267
|
+
console.log(chalk.blue(`\n📡 Joining leader at port ${port}...`));
|
|
268
|
+
|
|
269
|
+
// Send registration request to leader
|
|
270
|
+
const response = await this.sendHttpRequest(port, '/api/projects/register', {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
body: JSON.stringify({
|
|
273
|
+
workingDir: resolvedPath,
|
|
274
|
+
name: projectName
|
|
275
|
+
})
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (response.success) {
|
|
279
|
+
console.log(chalk.green('\n──────────────────────────────────────────────────'));
|
|
280
|
+
console.log(`🔗 ${chalk.bold('Project Registered')} ${chalk.green('● Joined')}`);
|
|
281
|
+
console.log(chalk.gray('──────────────────────────────────────────────────'));
|
|
282
|
+
console.log(`📁 Project: ${chalk.cyan(projectName)}`);
|
|
283
|
+
console.log(`🌐 Dashboard: ${chalk.blue(`http://localhost:${port}`)}`);
|
|
284
|
+
console.log(`📊 Total: ${chalk.yellow(response.totalProjects)} project(s)`);
|
|
285
|
+
console.log(chalk.green('──────────────────────────────────────────────────\n'));
|
|
286
|
+
console.log(chalk.blue('💡 Open the dashboard to switch between projects.'));
|
|
287
|
+
return true;
|
|
288
|
+
} else {
|
|
289
|
+
throw new Error(response.error || 'Failed to register project');
|
|
290
|
+
}
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error(chalk.red('❌ Failed to join leader:'), error.message);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Send HTTP request to server
|
|
299
|
+
* @param {number} port
|
|
300
|
+
* @param {string} path
|
|
301
|
+
* @param {Object} options
|
|
302
|
+
* @returns {Promise<Object>}
|
|
303
|
+
*/
|
|
304
|
+
sendHttpRequest(port, path, options = {}) {
|
|
305
|
+
return new Promise((resolve, reject) => {
|
|
306
|
+
const postData = options.body || '';
|
|
307
|
+
|
|
308
|
+
const req = http.request({
|
|
309
|
+
hostname: 'localhost',
|
|
310
|
+
port: port,
|
|
311
|
+
path: path,
|
|
312
|
+
method: options.method || 'GET',
|
|
313
|
+
headers: {
|
|
314
|
+
'Content-Type': 'application/json',
|
|
315
|
+
'Content-Length': Buffer.byteLength(postData)
|
|
316
|
+
}
|
|
317
|
+
}, (res) => {
|
|
318
|
+
let data = '';
|
|
319
|
+
|
|
320
|
+
res.on('data', (chunk) => {
|
|
321
|
+
data += chunk;
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
res.on('end', () => {
|
|
325
|
+
try {
|
|
326
|
+
const parsed = JSON.parse(data);
|
|
327
|
+
resolve(parsed);
|
|
328
|
+
} catch (error) {
|
|
329
|
+
reject(new Error('Invalid JSON response'));
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
req.on('error', (error) => {
|
|
335
|
+
reject(error);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
req.setTimeout(5000, () => {
|
|
339
|
+
req.destroy();
|
|
340
|
+
reject(new Error('Request timeout'));
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
if (postData) {
|
|
344
|
+
req.write(postData);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
req.end();
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Export singleton instance
|
|
353
|
+
module.exports = new ProjectManager();
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
const os = require('os');
|
|
2
2
|
const pty = require('node-pty');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { stripAnsiCodes, classifyLogLine, extractErrors } = require(path.join(__dirname, 'views/js/utils/log-utils'));
|
|
3
5
|
|
|
4
6
|
class TerminalManager {
|
|
5
7
|
constructor() {
|
|
6
|
-
// Map: termId -> { process: pty, socketId: string }
|
|
8
|
+
// Map: termId -> { process: pty, socketId: string, projectId: string }
|
|
7
9
|
this.sessions = new Map();
|
|
10
|
+
// Map: termId -> Array of log lines (circular buffer, max 10000)
|
|
11
|
+
this.logBuffers = new Map();
|
|
12
|
+
this.MAX_LOG_LINES = 10000;
|
|
8
13
|
// Use full path to shell to avoid posix_spawnp errors
|
|
9
14
|
if (os.platform() === 'win32') {
|
|
10
15
|
this.shell = 'powershell.exe';
|
|
@@ -17,7 +22,7 @@ class TerminalManager {
|
|
|
17
22
|
}
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
createTerminal(socket, termId, cols = 80, rows = 24, cwd) {
|
|
25
|
+
createTerminal(socket, termId, cols = 80, rows = 24, cwd, projectId = null) {
|
|
21
26
|
try {
|
|
22
27
|
const term = pty.spawn(this.shell, [], {
|
|
23
28
|
name: 'xterm-256color',
|
|
@@ -29,11 +34,19 @@ class TerminalManager {
|
|
|
29
34
|
|
|
30
35
|
this.sessions.set(termId, {
|
|
31
36
|
process: term,
|
|
32
|
-
socketId: socket.id
|
|
37
|
+
socketId: socket.id,
|
|
38
|
+
projectId: projectId // Store project association
|
|
33
39
|
});
|
|
34
40
|
|
|
41
|
+
// Initialize log buffer for this terminal
|
|
42
|
+
this.logBuffers.set(termId, []);
|
|
43
|
+
|
|
35
44
|
// Gửi dữ liệu kèm theo termId để Frontend biết của cửa sổ nào
|
|
36
45
|
term.onData((data) => {
|
|
46
|
+
// Store in log buffer (strip ANSI for storage)
|
|
47
|
+
this.addToLogBuffer(termId, data);
|
|
48
|
+
|
|
49
|
+
// Send raw data to frontend (keep ANSI for display)
|
|
37
50
|
socket.emit('terminal:data', { termId, data });
|
|
38
51
|
});
|
|
39
52
|
|
|
@@ -41,6 +54,8 @@ class TerminalManager {
|
|
|
41
54
|
if (this.sessions.has(termId)) {
|
|
42
55
|
socket.emit('terminal:exit', { termId });
|
|
43
56
|
this.sessions.delete(termId);
|
|
57
|
+
// Clean up log buffer
|
|
58
|
+
this.logBuffers.delete(termId);
|
|
44
59
|
}
|
|
45
60
|
});
|
|
46
61
|
|
|
@@ -83,8 +98,120 @@ class TerminalManager {
|
|
|
83
98
|
if (session.socketId === socketId) {
|
|
84
99
|
session.process.kill();
|
|
85
100
|
this.sessions.delete(termId);
|
|
101
|
+
this.logBuffers.delete(termId);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Add data to log buffer (circular buffer with max 10000 lines)
|
|
108
|
+
* @param {string} termId - Terminal ID
|
|
109
|
+
* @param {string} data - Raw terminal data (may contain ANSI codes)
|
|
110
|
+
*/
|
|
111
|
+
addToLogBuffer(termId, data) {
|
|
112
|
+
if (!this.logBuffers.has(termId)) {
|
|
113
|
+
this.logBuffers.set(termId, []);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const buffer = this.logBuffers.get(termId);
|
|
117
|
+
|
|
118
|
+
// Strip ANSI codes before storing
|
|
119
|
+
const cleanData = stripAnsiCodes(data);
|
|
120
|
+
|
|
121
|
+
// Split by newlines and add to buffer
|
|
122
|
+
const lines = cleanData.split(/\r?\n/);
|
|
123
|
+
|
|
124
|
+
lines.forEach(line => {
|
|
125
|
+
// Only add non-empty lines
|
|
126
|
+
if (line.trim().length > 0) {
|
|
127
|
+
buffer.push(line);
|
|
128
|
+
|
|
129
|
+
// Maintain circular buffer - remove oldest if exceeds limit
|
|
130
|
+
if (buffer.length > this.MAX_LOG_LINES) {
|
|
131
|
+
buffer.shift();
|
|
132
|
+
}
|
|
86
133
|
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Get log buffer for a terminal
|
|
139
|
+
* @param {string} termId - Terminal ID
|
|
140
|
+
* @returns {string[]} Array of log lines
|
|
141
|
+
*/
|
|
142
|
+
getLogBuffer(termId) {
|
|
143
|
+
return this.logBuffers.get(termId) || [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Analyze log buffer and return statistics
|
|
148
|
+
* @param {string} termId - Terminal ID
|
|
149
|
+
* @returns {Object} Analysis with error counts, line counts, etc.
|
|
150
|
+
*/
|
|
151
|
+
analyzeLogBuffer(termId) {
|
|
152
|
+
const lines = this.getLogBuffer(termId);
|
|
153
|
+
|
|
154
|
+
if (lines.length === 0) {
|
|
155
|
+
return {
|
|
156
|
+
totalLines: 0,
|
|
157
|
+
errorLines: 0,
|
|
158
|
+
warningLines: 0,
|
|
159
|
+
normalLines: 0,
|
|
160
|
+
errors: []
|
|
161
|
+
};
|
|
87
162
|
}
|
|
163
|
+
|
|
164
|
+
let errorCount = 0;
|
|
165
|
+
let warningCount = 0;
|
|
166
|
+
let normalCount = 0;
|
|
167
|
+
|
|
168
|
+
lines.forEach(line => {
|
|
169
|
+
const type = classifyLogLine(line);
|
|
170
|
+
if (type === 'ERROR') errorCount++;
|
|
171
|
+
else if (type === 'WARNING') warningCount++;
|
|
172
|
+
else normalCount++;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const errors = extractErrors(lines, 2);
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
totalLines: lines.length,
|
|
179
|
+
errorLines: errorCount,
|
|
180
|
+
warningLines: warningCount,
|
|
181
|
+
normalLines: normalCount,
|
|
182
|
+
errors: errors.map(e => ({
|
|
183
|
+
line: e.line,
|
|
184
|
+
type: e.type,
|
|
185
|
+
lineIndex: e.lineIndex
|
|
186
|
+
}))
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get all terminal IDs for a specific project
|
|
192
|
+
* @param {string} projectId - Project ID
|
|
193
|
+
* @returns {string[]} Array of terminal IDs
|
|
194
|
+
*/
|
|
195
|
+
getProjectTerminals(projectId) {
|
|
196
|
+
const terminals = [];
|
|
197
|
+
for (const [termId, session] of this.sessions.entries()) {
|
|
198
|
+
if (session.projectId === projectId) {
|
|
199
|
+
terminals.push(termId);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return terminals;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Clean up all terminals for a project
|
|
207
|
+
* @param {string} projectId - Project ID
|
|
208
|
+
*/
|
|
209
|
+
cleanupProject(projectId) {
|
|
210
|
+
const terminalsToRemove = this.getProjectTerminals(projectId);
|
|
211
|
+
terminalsToRemove.forEach(termId => {
|
|
212
|
+
this.kill(termId);
|
|
213
|
+
});
|
|
214
|
+
console.log(`Cleaned up ${terminalsToRemove.length} terminal(s) for project ${projectId}`);
|
|
88
215
|
}
|
|
89
216
|
}
|
|
90
217
|
|
|
@@ -69,6 +69,94 @@
|
|
|
69
69
|
text-overflow: ellipsis;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/* Copy Button Group */
|
|
73
|
+
.terminal-copy-group {
|
|
74
|
+
display: flex;
|
|
75
|
+
gap: 4px;
|
|
76
|
+
margin-left: auto;
|
|
77
|
+
margin-right: 8px;
|
|
78
|
+
align-items: center;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.copy-btn {
|
|
82
|
+
padding: 3px 6px;
|
|
83
|
+
font-size: 10px;
|
|
84
|
+
border-radius: 3px;
|
|
85
|
+
background: #2d2d30;
|
|
86
|
+
border: 1px solid #3e3e42;
|
|
87
|
+
color: #ccc;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
transition: all 0.2s;
|
|
90
|
+
display: flex;
|
|
91
|
+
align-items: center;
|
|
92
|
+
gap: 3px;
|
|
93
|
+
white-space: nowrap;
|
|
94
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.copy-btn:hover {
|
|
98
|
+
background: #3e3e42;
|
|
99
|
+
border-color: #555;
|
|
100
|
+
transform: translateY(-1px);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.copy-btn:active {
|
|
104
|
+
transform: translateY(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.token-badge {
|
|
108
|
+
font-size: 9px;
|
|
109
|
+
color: #888;
|
|
110
|
+
font-weight: 600;
|
|
111
|
+
min-width: 20px;
|
|
112
|
+
text-align: right;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/* Specific button colors */
|
|
116
|
+
.copy-smart:hover {
|
|
117
|
+
border-color: #4a9eff;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.copy-errors:hover {
|
|
121
|
+
border-color: #f48771;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.copy-recent:hover {
|
|
125
|
+
border-color: #89d185;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.copy-all:hover {
|
|
129
|
+
border-color: #dcdcaa;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/* Clear Button */
|
|
133
|
+
.term-btn-clear {
|
|
134
|
+
width: 24px;
|
|
135
|
+
height: 24px;
|
|
136
|
+
border-radius: 4px;
|
|
137
|
+
background: #2d2d30;
|
|
138
|
+
border: 1px solid #3e3e42;
|
|
139
|
+
color: #ccc;
|
|
140
|
+
cursor: pointer;
|
|
141
|
+
display: flex;
|
|
142
|
+
align-items: center;
|
|
143
|
+
justify-content: center;
|
|
144
|
+
font-size: 14px;
|
|
145
|
+
margin-left: 4px;
|
|
146
|
+
margin-right: 8px;
|
|
147
|
+
transition: all 0.2s;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.term-btn-clear:hover {
|
|
151
|
+
background: #3e3e42;
|
|
152
|
+
border-color: #f48771;
|
|
153
|
+
transform: translateY(-1px);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.term-btn-clear:active {
|
|
157
|
+
transform: translateY(0);
|
|
158
|
+
}
|
|
159
|
+
|
|
72
160
|
.terminal-controls {
|
|
73
161
|
display: flex;
|
|
74
162
|
gap: 6px;
|