vg-coder-cli 2.0.25 → 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.25",
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);
@@ -314,7 +423,7 @@ class ApiServer {
314
423
  // Load saved commands
315
424
  this.app.get('/api/commands/load', async (req, res) => {
316
425
  try {
317
- const commandsFile = path.join(this.workingDir, '.vg', 'commands.json');
426
+ const commandsFile = path.join(req.workingDir, '.vg', 'commands.json');
318
427
 
319
428
  if (!await fs.pathExists(commandsFile)) {
320
429
  return res.json({ commands: [] });
@@ -336,7 +445,7 @@ class ApiServer {
336
445
  return res.status(400).json({ error: 'commands must be an array' });
337
446
  }
338
447
 
339
- const vgDir = path.join(this.workingDir, '.vg');
448
+ const vgDir = path.join(req.workingDir, '.vg');
340
449
  await fs.ensureDir(vgDir);
341
450
 
342
451
  const commandsFile = path.join(vgDir, 'commands.json');
@@ -361,7 +470,7 @@ class ApiServer {
361
470
  }
362
471
 
363
472
  // Create .vg directory if it doesn't exist
364
- const vgDir = path.join(this.workingDir, '.vg');
473
+ const vgDir = path.join(req.workingDir, '.vg');
365
474
  await fs.ensureDir(vgDir);
366
475
 
367
476
  // Save state to .vg/tree-state.json
@@ -379,7 +488,7 @@ class ApiServer {
379
488
  // Load tree state
380
489
  this.app.get('/api/tree-state/load', async (req, res) => {
381
490
  try {
382
- const stateFile = path.join(this.workingDir, '.vg', 'tree-state.json');
491
+ const stateFile = path.join(req.workingDir, '.vg', 'tree-state.json');
383
492
 
384
493
  // Check if state file exists
385
494
  if (!await fs.pathExists(stateFile)) {
@@ -401,7 +510,7 @@ class ApiServer {
401
510
  this.app.post('/api/analyze', async (req, res) => {
402
511
  const { path: projectPath, options = {}, specificFiles } = req.body;
403
512
  if (!projectPath) return res.status(400).json({ error: 'Missing path' });
404
- const resolvedPath = path.resolve(projectPath);
513
+ const resolvedPath = path.resolve(req.workingDir, projectPath);
405
514
  if (!await fs.pathExists(resolvedPath)) return res.status(404).json({ error: 'Path not found' });
406
515
  const scanner = new FileScanner(resolvedPath, {
407
516
  extensions: options.extensions ? options.extensions.split(',') : undefined,
@@ -415,9 +524,8 @@ class ApiServer {
415
524
  });
416
525
 
417
526
  this.app.get('/api/info', async (req, res) => {
418
- const projectPath = req.query.path;
419
- if (!projectPath) return res.status(400).json({ error: 'Missing path' });
420
- const resolvedPath = path.resolve(projectPath);
527
+ const projectPath = req.query.path || '.';
528
+ const resolvedPath = path.resolve(req.workingDir, projectPath);
421
529
  const detector = new ProjectDetector(resolvedPath);
422
530
  const projectInfo = await detector.detectAll();
423
531
  const scanner = new FileScanner(resolvedPath);
@@ -427,7 +535,7 @@ class ApiServer {
427
535
 
428
536
  this.app.get('/api/structure', async (req, res) => {
429
537
  const projectPath = req.query.path || '.';
430
- const resolvedPath = path.resolve(projectPath);
538
+ const resolvedPath = path.resolve(req.workingDir, projectPath);
431
539
  const scanner = new FileScanner(resolvedPath);
432
540
  const scanResult = await scanner.scanProject();
433
541
  const tokenManager = new TokenManager();
@@ -437,7 +545,7 @@ class ApiServer {
437
545
 
438
546
  this.app.post('/api/execute', async (req, res) => {
439
547
  const { bash } = req.body;
440
- const executor = new BashExecutor(this.workingDir);
548
+ const executor = new BashExecutor(req.workingDir);
441
549
  const result = await executor.execute(bash);
442
550
  res.status(result.success ? 200 : 400).json(result);
443
551
  });
@@ -446,6 +554,24 @@ class ApiServer {
446
554
  await fs.remove(path.resolve(req.body.output));
447
555
  res.json({ success: true });
448
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
+ });
449
575
  }
450
576
 
451
577
  async start() {
@@ -490,7 +616,17 @@ class ApiServer {
490
616
  }
491
617
 
492
618
  async stop() {
493
- 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'));
494
630
  }
495
631
  }
496
632