UNPKG

vibe-annotations-server

Version:

Global MCP server for Vibe Annotations browser extension

227 lines (192 loc) 6.55 kB
#!/usr/bin/env node import { Command } from 'commander'; import { spawn, exec } from 'child_process'; import { promisify } from 'util'; import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, openSync } from 'fs'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import chalk from 'chalk'; import { homedir } from 'os'; import fs from 'fs'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); // Read version from package.json automatically const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8')); const execAsync = promisify(exec); const program = new Command(); // Configuration paths const CONFIG_DIR = join(homedir(), '.vibe-annotations'); const PID_FILE = join(CONFIG_DIR, 'server.pid'); const LOG_FILE = join(CONFIG_DIR, 'server.log'); const PORT = 3846; // Ensure config directory exists if (!existsSync(CONFIG_DIR)) { mkdirSync(CONFIG_DIR, { recursive: true }); } // Helper functions function isServerRunning() { if (!existsSync(PID_FILE)) { return false; } try { const pid = readFileSync(PID_FILE, 'utf8').trim(); process.kill(parseInt(pid), 0); // Check if process exists return true; } catch (e) { // Process doesn't exist if (existsSync(PID_FILE)) { // Clean up stale PID file unlinkSync(PID_FILE); } return false; } } async function checkPort() { try { const response = await fetch(`http://127.0.0.1:${PORT}/health`); return response.ok; } catch (error) { return false; } } // Commands program .name('vibe-annotations-server') .description('Global MCP server for Vibe Annotations browser extension') .version(packageJson.version); program .command('start') .description('Start the Vibe Annotations server') .option('-d, --daemon', 'Run as daemon (background process)') .action(async (options) => { if (isServerRunning()) { console.log(chalk.yellow('✓ Server is already running')); console.log(chalk.gray(` Port: ${PORT}`)); console.log(chalk.gray(` PID: ${readFileSync(PID_FILE, 'utf8').trim()}`)); return; } console.log(chalk.blue('Starting Vibe Annotations server...')); const serverPath = join(dirname(__dirname), 'lib', 'server.js'); if (options.daemon) { // Run as daemon const out = fs.openSync(LOG_FILE, 'a'); const err = fs.openSync(LOG_FILE, 'a'); const child = spawn('node', [serverPath], { detached: true, stdio: ['ignore', out, err], env: { ...process.env, NODE_ENV: 'production' } }); child.unref(); writeFileSync(PID_FILE, child.pid.toString()); // Wait for server to start let attempts = 0; while (attempts < 10) { if (await checkPort()) { console.log(chalk.green('✅ Vibe Annotations server running on http://127.0.0.1:3846/sse')); console.log(chalk.gray(` PID: ${child.pid}`)); console.log(chalk.gray(` Logs: ${LOG_FILE}`)); break; } await new Promise(resolve => setTimeout(resolve, 500)); attempts++; } if (attempts >= 10) { console.log(chalk.red('❌ Failed to start server. Check logs at:')); console.log(chalk.gray(` ${LOG_FILE}`)); } } else { // Run in foreground const child = spawn('node', [serverPath], { stdio: 'inherit', env: { ...process.env, NODE_ENV: 'production' } }); writeFileSync(PID_FILE, child.pid.toString()); child.on('exit', () => { if (existsSync(PID_FILE)) { unlinkSync(PID_FILE); } }); } }); program .command('stop') .description('Stop the Vibe Annotations server') .action(() => { if (!isServerRunning()) { console.log(chalk.yellow('Server is not running')); return; } try { const pid = readFileSync(PID_FILE, 'utf8').trim(); process.kill(parseInt(pid), 'SIGTERM'); if (existsSync(PID_FILE)) { unlinkSync(PID_FILE); } console.log(chalk.green('✅ Server stopped')); } catch (error) { console.log(chalk.red('❌ Failed to stop server:'), error.message); } }); program .command('restart') .description('Restart the Vibe Annotations server') .action(async () => { console.log(chalk.blue('Restarting server...')); // Stop if running if (isServerRunning()) { try { const pid = readFileSync(PID_FILE, 'utf8').trim(); process.kill(parseInt(pid), 'SIGTERM'); if (existsSync(PID_FILE)) { unlinkSync(PID_FILE); } // Wait for process to stop await new Promise(resolve => setTimeout(resolve, 1000)); } catch (error) { console.log(chalk.yellow('Warning:'), error.message); } } // Start with daemon flag program.parse(['node', 'cli.js', 'start', '--daemon'], { from: 'user' }); }); program .command('status') .description('Check server status') .action(async () => { const running = isServerRunning(); const portAvailable = await checkPort(); if (running && portAvailable) { console.log(chalk.green('✅ Server is running')); console.log(chalk.gray(` PID: ${readFileSync(PID_FILE, 'utf8').trim()}`)); console.log(chalk.gray(` Port: ${PORT}`)); console.log(chalk.gray(` URL: http://127.0.0.1:${PORT}/sse`)); } else if (running && !portAvailable) { console.log(chalk.yellow('⚠️ Server process exists but is not responding')); console.log(chalk.gray(' Try running: vibe-annotations-server restart')); } else { console.log(chalk.gray('○ Server is not running')); } }); program .command('logs') .description('View server logs') .option('-f, --follow', 'Follow log output') .option('-n, --lines <number>', 'Number of lines to show', '50') .action((options) => { if (!existsSync(LOG_FILE)) { console.log(chalk.gray('No logs available yet')); return; } if (options.follow) { // Use tail -f const tail = spawn('tail', ['-f', LOG_FILE], { stdio: 'inherit' }); process.on('SIGINT', () => { tail.kill(); process.exit(); }); } else { // Show last N lines const tail = spawn('tail', ['-n', options.lines, LOG_FILE], { stdio: 'inherit' }); } }); program.parse(process.argv);