vg-coder-cli 2.0.23 → 2.0.25

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.23",
3
+ "version": "2.0.25",
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": {
@@ -274,6 +274,82 @@ class ApiServer {
274
274
  }
275
275
  });
276
276
 
277
+ // --- TERMINAL LOG API ---
278
+
279
+ // Get terminal logs
280
+ this.app.get('/api/terminal/:termId/logs', (req, res) => {
281
+ try {
282
+ const { termId } = req.params;
283
+ const logs = terminalManager.getLogBuffer(termId);
284
+
285
+ res.json({
286
+ termId,
287
+ logs,
288
+ totalLines: logs.length
289
+ });
290
+ } catch (error) {
291
+ console.error(chalk.red('❌ [TERMINAL LOGS] Error:'), error.message);
292
+ res.status(500).json({ error: error.message });
293
+ }
294
+ });
295
+
296
+ // Analyze terminal logs
297
+ this.app.post('/api/terminal/:termId/analyze', (req, res) => {
298
+ try {
299
+ const { termId } = req.params;
300
+ const analysis = terminalManager.analyzeLogBuffer(termId);
301
+
302
+ res.json({
303
+ termId,
304
+ ...analysis
305
+ });
306
+ } catch (error) {
307
+ console.error(chalk.red('❌ [TERMINAL ANALYZE] Error:'), error.message);
308
+ res.status(500).json({ error: error.message });
309
+ }
310
+ });
311
+
312
+ // --- SAVED COMMANDS API ---
313
+
314
+ // Load saved commands
315
+ this.app.get('/api/commands/load', async (req, res) => {
316
+ try {
317
+ const commandsFile = path.join(this.workingDir, '.vg', 'commands.json');
318
+
319
+ if (!await fs.pathExists(commandsFile)) {
320
+ return res.json({ commands: [] });
321
+ }
322
+
323
+ const data = await fs.readJson(commandsFile);
324
+ res.json({ commands: data.commands || [] });
325
+ } catch (error) {
326
+ console.error(chalk.red('❌ [COMMANDS LOAD] Error:'), error.message);
327
+ res.json({ commands: [] });
328
+ }
329
+ });
330
+
331
+ // Save commands
332
+ this.app.post('/api/commands/save', async (req, res) => {
333
+ try {
334
+ const { commands } = req.body;
335
+ if (!Array.isArray(commands)) {
336
+ return res.status(400).json({ error: 'commands must be an array' });
337
+ }
338
+
339
+ const vgDir = path.join(this.workingDir, '.vg');
340
+ await fs.ensureDir(vgDir);
341
+
342
+ const commandsFile = path.join(vgDir, 'commands.json');
343
+ await fs.writeJson(commandsFile, { commands }, { spaces: 2 });
344
+
345
+ console.log(chalk.green(`✓ Saved ${commands.length} commands`));
346
+ res.json({ success: true, count: commands.length });
347
+ } catch (error) {
348
+ console.error(chalk.red('❌ [COMMANDS SAVE] Error:'), error.message);
349
+ res.status(500).json({ error: error.message });
350
+ }
351
+ });
352
+
277
353
  // --- TREE STATE API ---
278
354
 
279
355
  // Save tree state (excluded paths)
@@ -1,11 +1,25 @@
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
8
  // Map: termId -> { process: pty, socketId: string }
7
9
  this.sessions = new Map();
8
- this.shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash';
10
+ // Map: termId -> Array of log lines (circular buffer, max 10000)
11
+ this.logBuffers = new Map();
12
+ this.MAX_LOG_LINES = 10000;
13
+ // Use full path to shell to avoid posix_spawnp errors
14
+ if (os.platform() === 'win32') {
15
+ this.shell = 'powershell.exe';
16
+ } else if (os.platform() === 'darwin') {
17
+ // macOS - use zsh (default shell since Catalina)
18
+ this.shell = process.env.SHELL || '/bin/zsh';
19
+ } else {
20
+ // Linux and others
21
+ this.shell = process.env.SHELL || '/bin/bash';
22
+ }
9
23
  }
10
24
 
11
25
  createTerminal(socket, termId, cols = 80, rows = 24, cwd) {
@@ -23,8 +37,15 @@ class TerminalManager {
23
37
  socketId: socket.id
24
38
  });
25
39
 
40
+ // Initialize log buffer for this terminal
41
+ this.logBuffers.set(termId, []);
42
+
26
43
  // Gửi dữ liệu kèm theo termId để Frontend biết của cửa sổ nào
27
44
  term.onData((data) => {
45
+ // Store in log buffer (strip ANSI for storage)
46
+ this.addToLogBuffer(termId, data);
47
+
48
+ // Send raw data to frontend (keep ANSI for display)
28
49
  socket.emit('terminal:data', { termId, data });
29
50
  });
30
51
 
@@ -32,6 +53,8 @@ class TerminalManager {
32
53
  if (this.sessions.has(termId)) {
33
54
  socket.emit('terminal:exit', { termId });
34
55
  this.sessions.delete(termId);
56
+ // Clean up log buffer
57
+ this.logBuffers.delete(termId);
35
58
  }
36
59
  });
37
60
 
@@ -74,9 +97,94 @@ class TerminalManager {
74
97
  if (session.socketId === socketId) {
75
98
  session.process.kill();
76
99
  this.sessions.delete(termId);
100
+ this.logBuffers.delete(termId);
77
101
  }
78
102
  }
79
103
  }
104
+
105
+ /**
106
+ * Add data to log buffer (circular buffer with max 10000 lines)
107
+ * @param {string} termId - Terminal ID
108
+ * @param {string} data - Raw terminal data (may contain ANSI codes)
109
+ */
110
+ addToLogBuffer(termId, data) {
111
+ if (!this.logBuffers.has(termId)) {
112
+ this.logBuffers.set(termId, []);
113
+ }
114
+
115
+ const buffer = this.logBuffers.get(termId);
116
+
117
+ // Strip ANSI codes before storing
118
+ const cleanData = stripAnsiCodes(data);
119
+
120
+ // Split by newlines and add to buffer
121
+ const lines = cleanData.split(/\r?\n/);
122
+
123
+ lines.forEach(line => {
124
+ // Only add non-empty lines
125
+ if (line.trim().length > 0) {
126
+ buffer.push(line);
127
+
128
+ // Maintain circular buffer - remove oldest if exceeds limit
129
+ if (buffer.length > this.MAX_LOG_LINES) {
130
+ buffer.shift();
131
+ }
132
+ }
133
+ });
134
+ }
135
+
136
+ /**
137
+ * Get log buffer for a terminal
138
+ * @param {string} termId - Terminal ID
139
+ * @returns {string[]} Array of log lines
140
+ */
141
+ getLogBuffer(termId) {
142
+ return this.logBuffers.get(termId) || [];
143
+ }
144
+
145
+ /**
146
+ * Analyze log buffer and return statistics
147
+ * @param {string} termId - Terminal ID
148
+ * @returns {Object} Analysis with error counts, line counts, etc.
149
+ */
150
+ analyzeLogBuffer(termId) {
151
+ const lines = this.getLogBuffer(termId);
152
+
153
+ if (lines.length === 0) {
154
+ return {
155
+ totalLines: 0,
156
+ errorLines: 0,
157
+ warningLines: 0,
158
+ normalLines: 0,
159
+ errors: []
160
+ };
161
+ }
162
+
163
+ let errorCount = 0;
164
+ let warningCount = 0;
165
+ let normalCount = 0;
166
+
167
+ lines.forEach(line => {
168
+ const type = classifyLogLine(line);
169
+ if (type === 'ERROR') errorCount++;
170
+ else if (type === 'WARNING') warningCount++;
171
+ else normalCount++;
172
+ });
173
+
174
+ const errors = extractErrors(lines, 2);
175
+
176
+ return {
177
+ totalLines: lines.length,
178
+ errorLines: errorCount,
179
+ warningLines: warningCount,
180
+ normalLines: normalCount,
181
+ errors: errors.map(e => ({
182
+ line: e.line,
183
+ type: e.type,
184
+ lineIndex: e.lineIndex
185
+ }))
186
+ };
187
+ }
80
188
  }
81
189
 
82
190
  module.exports = new TerminalManager();
@@ -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;
@@ -348,3 +348,270 @@ body.resizing {
348
348
  border-bottom: 1px solid var(--ios-separator);
349
349
  }
350
350
  }
351
+
352
+ /* --- SAVED COMMANDS STYLES --- */
353
+ .saved-commands-panel {
354
+ margin-bottom: 12px;
355
+ }
356
+
357
+ .saved-commands-header {
358
+ display: flex;
359
+ justify-content: space-between;
360
+ align-items: center;
361
+ margin-bottom: 10px;
362
+ }
363
+
364
+ .saved-commands-title {
365
+ display: flex;
366
+ align-items: center;
367
+ gap: 8px;
368
+ font-weight: 600;
369
+ font-size: 13px;
370
+ color: var(--text-primary);
371
+ }
372
+
373
+ .command-icon-header {
374
+ font-size: 16px;
375
+ }
376
+
377
+ .saved-commands-actions {
378
+ display: flex;
379
+ gap: 6px;
380
+ }
381
+
382
+ .btn-new-terminal {
383
+ width: 28px;
384
+ height: 28px;
385
+ border-radius: 6px;
386
+ background: var(--ios-gray-light);
387
+ border: 1px solid var(--ios-separator);
388
+ color: var(--text-primary);
389
+ cursor: pointer;
390
+ display: flex;
391
+ align-items: center;
392
+ justify-content: center;
393
+ font-size: 14px;
394
+ transition: all 0.2s;
395
+ }
396
+
397
+ .btn-new-terminal:hover {
398
+ background: var(--ios-separator);
399
+ transform: scale(1.05);
400
+ }
401
+
402
+ .btn-add-command {
403
+ width: 28px;
404
+ height: 28px;
405
+ border-radius: 6px;
406
+ background: var(--ios-blue);
407
+ border: none;
408
+ color: white;
409
+ cursor: pointer;
410
+ display: flex;
411
+ align-items: center;
412
+ justify-content: center;
413
+ font-size: 14px;
414
+ transition: all 0.2s;
415
+ }
416
+
417
+ .btn-add-command:hover {
418
+ opacity: 0.8;
419
+ transform: scale(1.05);
420
+ }
421
+
422
+ .saved-commands-content {
423
+ margin-top: 0;
424
+ }
425
+
426
+ .commands-list {
427
+ display: flex;
428
+ flex-direction: column;
429
+ gap: 8px;
430
+ }
431
+
432
+ .command-card {
433
+ display: flex;
434
+ justify-content: space-between;
435
+ align-items: center;
436
+ padding: 8px 10px;
437
+ background: var(--ios-input-bg);
438
+ border-radius: 6px;
439
+ cursor: pointer;
440
+ transition: all 0.2s;
441
+ border: 1px solid transparent;
442
+ }
443
+
444
+ .command-card:hover {
445
+ background: var(--ios-gray-light);
446
+ border-color: var(--ios-blue);
447
+ transform: translateX(2px);
448
+ }
449
+
450
+ .command-card-main {
451
+ display: flex;
452
+ align-items: center;
453
+ gap: 10px;
454
+ flex: 1;
455
+ min-width: 0;
456
+ }
457
+
458
+ .command-icon {
459
+ font-size: 20px;
460
+ flex-shrink: 0;
461
+ }
462
+
463
+ .command-info {
464
+ display: flex;
465
+ flex-direction: column;
466
+ gap: 2px;
467
+ min-width: 0;
468
+ flex: 1;
469
+ }
470
+
471
+ .command-name {
472
+ font-weight: 600;
473
+ font-size: 13px;
474
+ color: var(--text-primary);
475
+ }
476
+
477
+ .command-text {
478
+ font-family: monospace;
479
+ font-size: 11px;
480
+ color: var(--text-secondary);
481
+ white-space: nowrap;
482
+ overflow: hidden;
483
+ text-overflow: ellipsis;
484
+ }
485
+
486
+ .command-card-actions {
487
+ display: flex;
488
+ gap: 4px;
489
+ opacity: 0;
490
+ transition: opacity 0.2s;
491
+ }
492
+
493
+ .command-card:hover .command-card-actions {
494
+ opacity: 1;
495
+ }
496
+
497
+ .command-action-btn {
498
+ width: 24px;
499
+ height: 24px;
500
+ border: none;
501
+ background: rgba(0, 0, 0, 0.05);
502
+ border-radius: 4px;
503
+ cursor: pointer;
504
+ display: flex;
505
+ align-items: center;
506
+ justify-content: center;
507
+ font-size: 12px;
508
+ transition: all 0.2s;
509
+ }
510
+
511
+ .command-action-btn:hover {
512
+ background: rgba(0, 0, 0, 0.1);
513
+ transform: scale(1.1);
514
+ }
515
+
516
+ /* Modal Styles */
517
+ .modal {
518
+ display: none;
519
+ position: fixed;
520
+ top: 0;
521
+ left: 0;
522
+ width: 100%;
523
+ height: 100%;
524
+ background: rgba(0, 0, 0, 0.5);
525
+ z-index: 10000;
526
+ align-items: center;
527
+ justify-content: center;
528
+ }
529
+
530
+ .modal-content {
531
+ background: var(--ios-card);
532
+ border-radius: 12px;
533
+ padding: 20px;
534
+ width: 90%;
535
+ max-width: 400px;
536
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
537
+ }
538
+
539
+ .modal-content h3 {
540
+ margin: 0 0 15px 0;
541
+ font-size: 16px;
542
+ color: var(--text-primary);
543
+ }
544
+
545
+ .modal-content .form-group {
546
+ margin-bottom: 15px;
547
+ }
548
+
549
+ .modal-content .form-group label {
550
+ display: block;
551
+ margin-bottom: 5px;
552
+ font-size: 12px;
553
+ font-weight: 600;
554
+ color: var(--text-secondary);
555
+ }
556
+
557
+ .modal-content .form-group input {
558
+ width: 100%;
559
+ padding: 8px 12px;
560
+ background: var(--ios-input-bg);
561
+ border: 1px solid var(--ios-separator);
562
+ border-radius: 8px;
563
+ font-size: 13px;
564
+ color: var(--text-primary);
565
+ }
566
+
567
+ .modal-content .form-group input:focus {
568
+ outline: none;
569
+ border-color: var(--ios-blue);
570
+ }
571
+
572
+ .modal-actions {
573
+ display: flex;
574
+ gap: 10px;
575
+ margin-top: 20px;
576
+ }
577
+
578
+ .modal-actions .btn {
579
+ flex: 1;
580
+ padding: 10px;
581
+ border: none;
582
+ border-radius: 8px;
583
+ font-size: 13px;
584
+ font-weight: 600;
585
+ cursor: pointer;
586
+ transition: all 0.2s;
587
+ }
588
+
589
+ .modal-actions .btn:first-child {
590
+ background: var(--ios-gray-light);
591
+ color: var(--text-primary);
592
+ }
593
+
594
+ .modal-actions .btn:first-child:hover {
595
+ background: var(--ios-separator);
596
+ }
597
+
598
+ .modal-actions .btn-primary {
599
+ background: var(--ios-blue);
600
+ color: white;
601
+ }
602
+
603
+ .modal-actions .btn-primary:hover {
604
+ opacity: 0.9;
605
+ }
606
+
607
+ .endpoint-title-group {
608
+ display: flex;
609
+ align-items: center;
610
+ gap: 8px;
611
+ }
612
+
613
+ .toggle-icon {
614
+ font-size: 12px;
615
+ color: var(--text-secondary);
616
+ transition: transform 0.2s;
617
+ }
@@ -76,14 +76,32 @@
76
76
  </button>
77
77
  </div>
78
78
 
79
- <!-- Quick Actions -->
80
- <div class="endpoint-card"
81
- style="display: flex; gap: 10px; align-items: center; justify-content: space-between;">
82
- <span style="font-weight: 600;">Tools:</span>
83
- <button class="btn" onclick="createNewTerminal()"
84
- style="background: #252526; border: 1px solid #444;">
85
- <span>🖥️</span> New Terminal
86
- </button>
79
+ <!-- Saved Commands Panel -->
80
+ <div class="endpoint-card saved-commands-panel">
81
+ <div class="saved-commands-header">
82
+ <div class="saved-commands-title">
83
+ <span class="command-icon-header">💾</span>
84
+ <span>Saved Commands</span>
85
+ </div>
86
+ <div class="saved-commands-actions">
87
+ <button class="btn-new-terminal" onclick="createNewTerminal()" title="New Terminal">
88
+ 🖥️
89
+ </button>
90
+ <button class="btn-add-command" onclick="openAddCommandModal()" title="Add Command">
91
+
92
+ </button>
93
+ </div>
94
+ </div>
95
+ <div class="saved-commands-content" id="saved-commands-content">
96
+ <div id="commands-list" class="commands-list">
97
+ <!-- Commands will be rendered here -->
98
+ </div>
99
+ <div class="empty-state" id="commands-empty-state" style="display: none;">
100
+ <p style="color: #888; text-align: center; padding: 15px 10px; font-size: 12px; margin: 0;">
101
+ Click ➕ to add a command
102
+ </p>
103
+ </div>
104
+ </div>
87
105
  </div>
88
106
 
89
107
  <!-- System Prompt Section -->
@@ -280,6 +298,36 @@
280
298
 
281
299
  <div id="floating-terminals-layer"></div>
282
300
  <div class="toast" id="toast"></div>
301
+
302
+ <!-- Add/Edit Command Modal -->
303
+ <div id="command-modal" class="modal" style="display: none;">
304
+ <div class="modal-content">
305
+ <h3 id="modal-title">Add Command</h3>
306
+ <form id="command-form">
307
+ <div class="form-group">
308
+ <label>Icon (emoji)</label>
309
+ <input type="text" id="command-icon" placeholder="🚀" maxlength="2" required>
310
+ </div>
311
+ <div class="form-group">
312
+ <label>Name</label>
313
+ <input type="text" id="command-name" placeholder="Run Dev Server" required>
314
+ </div>
315
+ <div class="form-group">
316
+ <label>Command</label>
317
+ <input type="text" id="command-text" placeholder="npm run dev" required>
318
+ </div>
319
+ <div class="modal-actions">
320
+ <button type="button" class="btn" onclick="closeCommandModal()">Cancel</button>
321
+ <button type="submit" class="btn btn-primary">Save</button>
322
+ </div>
323
+ </form>
324
+ </div>
325
+ </div>
326
+
327
+ <!-- Smart Log Copy Utilities -->
328
+ <script src="/js/utils/log-utils.js"></script>
329
+ <script src="/js/utils/smart-copy-engine.js"></script>
330
+
283
331
  <script type="module" src="/js/main.js"></script>
284
332
  </body>
285
333