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.
@@ -1,9 +1,3 @@
1
1
  {
2
- "excludedPaths": [
3
- "scripts/build.js",
4
- "src/detectors/project-detector.js",
5
- "src/exporter/html-exporter.js",
6
- "src/ignore",
7
- "src/ignore/ignore-manager.js"
8
- ]
2
+ "excludedPaths": []
9
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vg-coder-cli",
3
- "version": "2.0.24",
3
+ "version": "2.0.26",
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": {
package/src/index.js CHANGED
@@ -372,11 +372,43 @@ class VGCoderCLI {
372
372
  */
373
373
  async handleStart(options) {
374
374
  try {
375
+ const projectPath = process.cwd();
375
376
  const initialPort = parseInt(options.port);
377
+ const projectManager = require('./server/project-manager');
378
+
379
+ // Check if leader exists
380
+ const leaderInfo = await projectManager.checkLeader();
381
+
382
+ if (leaderInfo) {
383
+ // Leader exists - join as follower
384
+ console.log(chalk.blue(`\nšŸ” Found existing server at port ${leaderInfo.port}`));
385
+ const joined = await projectManager.joinLeader(leaderInfo, projectPath);
386
+
387
+ if (joined) {
388
+ // Successfully joined, no need to start new server
389
+ return;
390
+ } else {
391
+ // Failed to join, fallback to starting new server
392
+ console.log(chalk.yellow('āš ļø Failed to join leader, starting new server...'));
393
+ }
394
+ }
395
+
396
+ // No leader or failed to join - become leader
397
+ console.log(chalk.blue('\nšŸš€ No existing server found, becoming leader...'));
398
+
376
399
  const server = new ApiServer(initialPort);
377
400
 
401
+ // Try to acquire lock before starting
402
+ const lockAcquired = await projectManager.acquireLock(initialPort);
403
+ if (!lockAcquired) {
404
+ throw new Error('Failed to acquire leader lock');
405
+ }
406
+
378
407
  await server.start();
379
408
 
409
+ // Register current project
410
+ server.projectManager.registerProject(projectPath);
411
+
380
412
  // Auto-open browser to dashboard using actual port from server
381
413
  const dashboardUrl = `http://localhost:${server.port}`;
382
414
  const { exec } = require('child_process');
@@ -402,6 +434,10 @@ class VGCoderCLI {
402
434
  // Handle graceful shutdown
403
435
  const shutdown = async () => {
404
436
  console.log(chalk.yellow('\n\nShutting down server...'));
437
+
438
+ // Release lock
439
+ await projectManager.releaseLock();
440
+
405
441
  await server.stop();
406
442
  process.exit(0);
407
443
  };
@@ -16,6 +16,7 @@ const FileScanner = require('../scanner/file-scanner');
16
16
  const TokenManager = require('../tokenizer/token-manager');
17
17
  const BashExecutor = require('../utils/bash-executor');
18
18
  const terminalManager = require('./terminal-manager');
19
+ const projectManager = require('./project-manager');
19
20
 
20
21
  class ApiServer {
21
22
  constructor(port = 6868) {
@@ -29,7 +30,8 @@ class ApiServer {
29
30
  });
30
31
 
31
32
  this.server = null;
32
- this.workingDir = process.cwd();
33
+ this.workingDir = process.cwd(); // Fallback for backward compatibility
34
+ this.projectManager = projectManager; // Reference to singleton
33
35
  this.setupMiddleware();
34
36
  this.setupRoutes();
35
37
  this.setupSocketIO();
@@ -41,6 +43,14 @@ class ApiServer {
41
43
  this.app.use(bodyParser.urlencoded({ extended: true, limit: '50mb' }));
42
44
  this.app.use(express.static(path.join(__dirname, 'views')));
43
45
 
46
+ // Project context middleware
47
+ this.app.use((req, res, next) => {
48
+ const activeProject = this.projectManager.getActiveProject();
49
+ req.projectContext = activeProject;
50
+ req.workingDir = activeProject ? activeProject.workingDir : this.workingDir;
51
+ next();
52
+ });
53
+
44
54
  this.app.use((req, res, next) => {
45
55
  if (!req.path.includes('.')) {
46
56
  console.log(chalk.blue(`[REQ] ${req.method} ${req.path}`));
@@ -53,8 +63,22 @@ class ApiServer {
53
63
  this.io.on('connection', (socket) => {
54
64
  socket.on('terminal:init', (data) => {
55
65
  if (!data || !data.termId) return;
56
- const { termId, cols, rows } = data;
57
- terminalManager.createTerminal(socket, termId, cols, rows, this.workingDir);
66
+ const { termId, cols, rows, projectId } = data;
67
+
68
+ // Get working directory from project context
69
+ let cwd;
70
+ if (projectId) {
71
+ // Use specific project
72
+ const projects = this.projectManager.getAllProjects();
73
+ const project = projects.find(p => p.id === projectId);
74
+ cwd = project ? project.workingDir : this.workingDir;
75
+ } else {
76
+ // Use active project
77
+ const activeProject = this.projectManager.getActiveProject();
78
+ cwd = activeProject ? activeProject.workingDir : this.workingDir;
79
+ }
80
+
81
+ terminalManager.createTerminal(socket, termId, cols, rows, cwd, projectId);
58
82
  });
59
83
 
60
84
  socket.on('terminal:input', (data) => {
@@ -94,6 +118,91 @@ class ApiServer {
94
118
  }
95
119
  });
96
120
 
121
+ // --- MULTI-PROJECT MANAGEMENT API ---
122
+
123
+ // List all projects
124
+ this.app.get('/api/projects', (req, res) => {
125
+ try {
126
+ const projects = this.projectManager.getAllProjects();
127
+ const activeProject = this.projectManager.getActiveProject();
128
+ res.json({
129
+ projects,
130
+ activeProjectId: activeProject ? activeProject.id : null,
131
+ totalProjects: projects.length
132
+ });
133
+ } catch (error) {
134
+ res.status(500).json({ error: error.message });
135
+ }
136
+ });
137
+
138
+ // Register new project (for follower join)
139
+ this.app.post('/api/projects/register', (req, res) => {
140
+ try {
141
+ const { workingDir, name } = req.body;
142
+ if (!workingDir) {
143
+ return res.status(400).json({ error: 'Missing workingDir' });
144
+ }
145
+
146
+ const projectId = this.projectManager.registerProject(workingDir);
147
+ const projects = this.projectManager.getAllProjects();
148
+
149
+ // Emit socket event to notify all clients about new project
150
+ this.io.emit('project:registered', { projectId, name });
151
+
152
+ res.json({
153
+ success: true,
154
+ projectId,
155
+ totalProjects: projects.length
156
+ });
157
+ } catch (error) {
158
+ res.status(500).json({ error: error.message });
159
+ }
160
+ });
161
+
162
+ // Switch active project
163
+ this.app.post('/api/projects/switch', (req, res) => {
164
+ try {
165
+ const { projectId } = req.body;
166
+ if (!projectId) {
167
+ return res.status(400).json({ error: 'Missing projectId' });
168
+ }
169
+
170
+ const success = this.projectManager.switchProject(projectId);
171
+
172
+ if (success) {
173
+ const activeProject = this.projectManager.getActiveProject();
174
+
175
+ // Emit socket event to notify all clients
176
+ this.io.emit('project:switched', {
177
+ projectId,
178
+ projectName: activeProject.name
179
+ });
180
+
181
+ res.json({ success: true, project: activeProject });
182
+ } else {
183
+ res.status(404).json({ error: 'Project not found' });
184
+ }
185
+ } catch (error) {
186
+ res.status(500).json({ error: error.message });
187
+ }
188
+ });
189
+
190
+ // Remove project
191
+ this.app.delete('/api/projects/:id', (req, res) => {
192
+ try {
193
+ const projectId = req.params.id;
194
+ this.projectManager.removeProject(projectId);
195
+
196
+ // Emit socket event
197
+ this.io.emit('project:removed', { projectId });
198
+
199
+ res.json({ success: true });
200
+ } catch (error) {
201
+ res.status(500).json({ error: error.message });
202
+ }
203
+ });
204
+
205
+
97
206
  // --- FILE OPERATIONS (NEW) ---
98
207
 
99
208
  // Read raw file content
@@ -103,8 +212,8 @@ class ApiServer {
103
212
  if (!filePath) return res.status(400).json({ error: 'Missing path' });
104
213
 
105
214
  // Prevent directory traversal (basic check)
106
- const resolvedPath = path.resolve(this.workingDir, filePath);
107
- if (!resolvedPath.startsWith(this.workingDir)) {
215
+ const resolvedPath = path.resolve(req.workingDir, filePath);
216
+ if (!resolvedPath.startsWith(req.workingDir)) {
108
217
  // Allow reading but log warning - in dev tool we might want flexibility
109
218
  // For strict mode: return res.status(403).json({ error: 'Access denied' });
110
219
  }
@@ -126,10 +235,10 @@ class ApiServer {
126
235
  const { path: filePath, content } = req.body;
127
236
  if (!filePath || content === undefined) return res.status(400).json({ error: 'Missing data' });
128
237
 
129
- const resolvedPath = path.resolve(this.workingDir, filePath);
238
+ const resolvedPath = path.resolve(req.workingDir, filePath);
130
239
 
131
240
  // Security check
132
- if (!resolvedPath.startsWith(this.workingDir)) {
241
+ if (!resolvedPath.startsWith(req.workingDir)) {
133
242
  // For strict mode: return res.status(403).json({ error: 'Access denied' });
134
243
  }
135
244
 
@@ -145,7 +254,7 @@ class ApiServer {
145
254
  // Get Git Status
146
255
  this.app.get('/api/git/status', async (req, res) => {
147
256
  try {
148
- const { stdout } = await execAsync('git status --porcelain -u', { cwd: this.workingDir });
257
+ const { stdout } = await execAsync('git status --porcelain -u', { cwd: req.workingDir });
149
258
 
150
259
  const staged = [];
151
260
  const unstaged = [];
@@ -184,7 +293,7 @@ class ApiServer {
184
293
  try {
185
294
  const { files } = req.body;
186
295
  const target = files.includes('*') ? '.' : files.map(f => `"${f}"`).join(' ');
187
- await execAsync(`git add ${target}`, { cwd: this.workingDir });
296
+ await execAsync(`git add ${target}`, { cwd: req.workingDir });
188
297
  res.json({ success: true });
189
298
  } catch (error) {
190
299
  res.status(500).json({ error: error.message });
@@ -196,7 +305,7 @@ class ApiServer {
196
305
  try {
197
306
  const { files } = req.body;
198
307
  const target = files.includes('*') ? '' : files.map(f => `"${f}"`).join(' ');
199
- await execAsync(`git reset HEAD ${target}`, { cwd: this.workingDir });
308
+ await execAsync(`git reset HEAD ${target}`, { cwd: req.workingDir });
200
309
  res.json({ success: true });
201
310
  } catch (error) {
202
311
  res.status(500).json({ error: error.message });
@@ -208,12 +317,12 @@ class ApiServer {
208
317
  try {
209
318
  const { files } = req.body;
210
319
  if (files.includes('*')) {
211
- try { await execAsync('git restore .', { cwd: this.workingDir }); } catch (e) {}
212
- try { await execAsync('git clean -fd', { cwd: this.workingDir }); } catch (e) {}
320
+ try { await execAsync('git restore .', { cwd: req.workingDir }); } catch (e) {}
321
+ try { await execAsync('git clean -fd', { cwd: req.workingDir }); } catch (e) {}
213
322
  } else {
214
323
  for (const file of files) {
215
- try { await execAsync(`git restore "${file}"`, { cwd: this.workingDir }); } catch (e) {}
216
- try { await execAsync(`git clean -f "${file}"`, { cwd: this.workingDir }); } catch (e) {}
324
+ try { await execAsync(`git restore "${file}"`, { cwd: req.workingDir }); } catch (e) {}
325
+ try { await execAsync(`git clean -f "${file}"`, { cwd: req.workingDir }); } catch (e) {}
217
326
  }
218
327
  }
219
328
  res.json({ success: true });
@@ -229,7 +338,7 @@ class ApiServer {
229
338
  const { message } = req.body;
230
339
  if (!message) throw new Error('Commit message is required');
231
340
  const safeMessage = message.replace(/"/g, '\\"');
232
- await execAsync(`git commit -m "${safeMessage}"`, { cwd: this.workingDir });
341
+ await execAsync(`git commit -m "${safeMessage}"`, { cwd: req.workingDir });
233
342
  res.json({ success: true });
234
343
  } catch (error) {
235
344
  res.status(500).json({ error: error.message });
@@ -246,17 +355,17 @@ class ApiServer {
246
355
  if (type === 'staged') {
247
356
  cmd = file ? `git diff --cached -- "${file}"` : `git diff --cached`;
248
357
  } else {
249
- if (file) {
358
+ if (file) {
250
359
  try {
251
- const filePath = path.join(this.workingDir, file);
360
+ const filePath = path.join(req.workingDir, file);
252
361
  if (await fs.pathExists(filePath)) {
253
362
  const stat = await fs.stat(filePath);
254
363
  if (stat.isDirectory()) return res.json({ diff: '' });
255
364
  }
256
365
  } catch (e) {}
257
- const { stdout: isUntracked } = await execAsync(`git ls-files --others --exclude-standard "${file}"`, { cwd: this.workingDir });
366
+ const { stdout: isUntracked } = await execAsync(`git ls-files --others --exclude-standard "${file}"`, { cwd: req.workingDir });
258
367
  if (isUntracked.trim()) {
259
- const content = await fs.readFile(path.join(this.workingDir, file), 'utf8');
368
+ const content = await fs.readFile(path.join(req.workingDir, file), 'utf8');
260
369
  let fakeDiff = `diff --git a/${file} b/${file}\nnew file mode 100644\n--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${content.split('\n').length} @@\n`;
261
370
  content.split('\n').forEach(l => fakeDiff += `+${l}\n`);
262
371
  return res.json({ diff: fakeDiff });
@@ -266,7 +375,7 @@ class ApiServer {
266
375
  cmd = `git diff`;
267
376
  }
268
377
  }
269
- const { stdout } = await execAsync(cmd, { cwd: this.workingDir, maxBuffer: 20 * 1024 * 1024 });
378
+ const { stdout } = await execAsync(cmd, { cwd: req.workingDir, maxBuffer: 20 * 1024 * 1024 });
270
379
  res.json({ diff: stdout });
271
380
  } catch (error) {
272
381
  console.error(chalk.red('āŒ [GIT DIFF] Error:'), error.message);
@@ -274,6 +383,82 @@ class ApiServer {
274
383
  }
275
384
  });
276
385
 
386
+ // --- TERMINAL LOG API ---
387
+
388
+ // Get terminal logs
389
+ this.app.get('/api/terminal/:termId/logs', (req, res) => {
390
+ try {
391
+ const { termId } = req.params;
392
+ const logs = terminalManager.getLogBuffer(termId);
393
+
394
+ res.json({
395
+ termId,
396
+ logs,
397
+ totalLines: logs.length
398
+ });
399
+ } catch (error) {
400
+ console.error(chalk.red('āŒ [TERMINAL LOGS] Error:'), error.message);
401
+ res.status(500).json({ error: error.message });
402
+ }
403
+ });
404
+
405
+ // Analyze terminal logs
406
+ this.app.post('/api/terminal/:termId/analyze', (req, res) => {
407
+ try {
408
+ const { termId } = req.params;
409
+ const analysis = terminalManager.analyzeLogBuffer(termId);
410
+
411
+ res.json({
412
+ termId,
413
+ ...analysis
414
+ });
415
+ } catch (error) {
416
+ console.error(chalk.red('āŒ [TERMINAL ANALYZE] Error:'), error.message);
417
+ res.status(500).json({ error: error.message });
418
+ }
419
+ });
420
+
421
+ // --- SAVED COMMANDS API ---
422
+
423
+ // Load saved commands
424
+ this.app.get('/api/commands/load', async (req, res) => {
425
+ try {
426
+ const commandsFile = path.join(req.workingDir, '.vg', 'commands.json');
427
+
428
+ if (!await fs.pathExists(commandsFile)) {
429
+ return res.json({ commands: [] });
430
+ }
431
+
432
+ const data = await fs.readJson(commandsFile);
433
+ res.json({ commands: data.commands || [] });
434
+ } catch (error) {
435
+ console.error(chalk.red('āŒ [COMMANDS LOAD] Error:'), error.message);
436
+ res.json({ commands: [] });
437
+ }
438
+ });
439
+
440
+ // Save commands
441
+ this.app.post('/api/commands/save', async (req, res) => {
442
+ try {
443
+ const { commands } = req.body;
444
+ if (!Array.isArray(commands)) {
445
+ return res.status(400).json({ error: 'commands must be an array' });
446
+ }
447
+
448
+ const vgDir = path.join(req.workingDir, '.vg');
449
+ await fs.ensureDir(vgDir);
450
+
451
+ const commandsFile = path.join(vgDir, 'commands.json');
452
+ await fs.writeJson(commandsFile, { commands }, { spaces: 2 });
453
+
454
+ console.log(chalk.green(`āœ“ Saved ${commands.length} commands`));
455
+ res.json({ success: true, count: commands.length });
456
+ } catch (error) {
457
+ console.error(chalk.red('āŒ [COMMANDS SAVE] Error:'), error.message);
458
+ res.status(500).json({ error: error.message });
459
+ }
460
+ });
461
+
277
462
  // --- TREE STATE API ---
278
463
 
279
464
  // Save tree state (excluded paths)
@@ -285,7 +470,7 @@ class ApiServer {
285
470
  }
286
471
 
287
472
  // Create .vg directory if it doesn't exist
288
- const vgDir = path.join(this.workingDir, '.vg');
473
+ const vgDir = path.join(req.workingDir, '.vg');
289
474
  await fs.ensureDir(vgDir);
290
475
 
291
476
  // Save state to .vg/tree-state.json
@@ -303,7 +488,7 @@ class ApiServer {
303
488
  // Load tree state
304
489
  this.app.get('/api/tree-state/load', async (req, res) => {
305
490
  try {
306
- const stateFile = path.join(this.workingDir, '.vg', 'tree-state.json');
491
+ const stateFile = path.join(req.workingDir, '.vg', 'tree-state.json');
307
492
 
308
493
  // Check if state file exists
309
494
  if (!await fs.pathExists(stateFile)) {
@@ -325,7 +510,7 @@ class ApiServer {
325
510
  this.app.post('/api/analyze', async (req, res) => {
326
511
  const { path: projectPath, options = {}, specificFiles } = req.body;
327
512
  if (!projectPath) return res.status(400).json({ error: 'Missing path' });
328
- const resolvedPath = path.resolve(projectPath);
513
+ const resolvedPath = path.resolve(req.workingDir, projectPath);
329
514
  if (!await fs.pathExists(resolvedPath)) return res.status(404).json({ error: 'Path not found' });
330
515
  const scanner = new FileScanner(resolvedPath, {
331
516
  extensions: options.extensions ? options.extensions.split(',') : undefined,
@@ -339,9 +524,8 @@ class ApiServer {
339
524
  });
340
525
 
341
526
  this.app.get('/api/info', async (req, res) => {
342
- const projectPath = req.query.path;
343
- if (!projectPath) return res.status(400).json({ error: 'Missing path' });
344
- const resolvedPath = path.resolve(projectPath);
527
+ const projectPath = req.query.path || '.';
528
+ const resolvedPath = path.resolve(req.workingDir, projectPath);
345
529
  const detector = new ProjectDetector(resolvedPath);
346
530
  const projectInfo = await detector.detectAll();
347
531
  const scanner = new FileScanner(resolvedPath);
@@ -351,7 +535,7 @@ class ApiServer {
351
535
 
352
536
  this.app.get('/api/structure', async (req, res) => {
353
537
  const projectPath = req.query.path || '.';
354
- const resolvedPath = path.resolve(projectPath);
538
+ const resolvedPath = path.resolve(req.workingDir, projectPath);
355
539
  const scanner = new FileScanner(resolvedPath);
356
540
  const scanResult = await scanner.scanProject();
357
541
  const tokenManager = new TokenManager();
@@ -361,7 +545,7 @@ class ApiServer {
361
545
 
362
546
  this.app.post('/api/execute', async (req, res) => {
363
547
  const { bash } = req.body;
364
- const executor = new BashExecutor(this.workingDir);
548
+ const executor = new BashExecutor(req.workingDir);
365
549
  const result = await executor.execute(bash);
366
550
  res.status(result.success ? 200 : 400).json(result);
367
551
  });
@@ -370,6 +554,24 @@ class ApiServer {
370
554
  await fs.remove(path.resolve(req.body.output));
371
555
  res.json({ success: true });
372
556
  });
557
+
558
+ // Shutdown server endpoint
559
+ this.app.post('/api/shutdown', async (req, res) => {
560
+ try {
561
+ console.log(chalk.yellow('\nšŸ›‘ Shutdown requested via API...'));
562
+
563
+ // Send response first
564
+ res.json({ success: true, message: 'Server shutting down...' });
565
+
566
+ // Give time for response to be sent
567
+ setTimeout(async () => {
568
+ await this.stop();
569
+ process.exit(0);
570
+ }, 500);
571
+ } catch (error) {
572
+ res.status(500).json({ error: error.message });
573
+ }
574
+ });
373
575
  }
374
576
 
375
577
  async start() {
@@ -414,7 +616,17 @@ class ApiServer {
414
616
  }
415
617
 
416
618
  async stop() {
417
- if (this.server) this.server.close();
619
+ console.log(chalk.yellow('Stopping server...'));
620
+
621
+ // Release leader lock
622
+ await this.projectManager.releaseLock();
623
+
624
+ // Close server
625
+ if (this.server) {
626
+ this.server.close();
627
+ }
628
+
629
+ console.log(chalk.green('āœ“ Server stopped'));
418
630
  }
419
631
  }
420
632