UNPKG

@ai-mapping/mcp-nextjs-dev

Version:

MCP server for managing Next.js development processes with AI tools

240 lines 8.6 kB
import { spawn } from 'child_process'; import { StateManager } from './state-manager.js'; export class ProcessManager { static instance; currentProcess = null; stateManager; constructor() { this.stateManager = StateManager.getInstance(); } static getInstance() { if (ProcessManager.instance === undefined) { ProcessManager.instance = new ProcessManager(); } return ProcessManager.instance; } async startNextJsProcess(options) { if (this.currentProcess && !this.currentProcess.killed) { throw new Error('Next.js development server is already running'); } try { const fs = await import('fs/promises'); await fs.access(options.projectPath); } catch (error) { throw new Error(`Project path does not exist: ${options.projectPath}`); } const script = options.script || 'pnpm run dev'; const [command, ...scriptArgs] = script.split(' '); const env = { ...process.env, PORT: options.port.toString(), FORCE_COLOR: '1', NODE_ENV: 'development', }; if (options.turbo && !options.script) { env['TURBO'] = '1'; } this.currentProcess = spawn(command, scriptArgs, { cwd: options.projectPath, stdio: ['ignore', 'pipe', 'pipe'], detached: false, env, shell: true, }); const pid = this.currentProcess?.pid; if (pid === undefined || !this.currentProcess) { throw new Error('Failed to get process ID from spawned Next.js process'); } const serverUrl = `http://localhost:${options.port}`; this.setupProcessHandlers(this.currentProcess); this.stateManager.setRunning(options.port, serverUrl); return { processId: pid, serverUrl, port: options.port, }; } stopNextJsProcess(force = false, timeout = 5) { if (!this.currentProcess || this.currentProcess.killed) { return Promise.resolve({ wasRunning: false, message: 'No Next.js server was running', }); } const wasRunning = true; if (force) { this.currentProcess.kill('SIGKILL'); this.stateManager.addLog({ timestamp: new Date().toISOString(), level: 'warn', message: 'Force killed Next.js development server', source: 'system', }); return Promise.resolve({ wasRunning, message: 'Next.js server force stopped', }); } return new Promise((resolve) => { if (!this.currentProcess) { resolve({ wasRunning: false, message: 'No process to stop', }); return; } const process = this.currentProcess; let resolved = false; const timeoutHandle = setTimeout(() => { if (!resolved && !process.killed) { process.kill('SIGKILL'); this.stateManager.addLog({ timestamp: new Date().toISOString(), level: 'warn', message: `Graceful shutdown timed out after ${timeout}s, force killed`, source: 'system', }); resolved = true; resolve({ wasRunning, message: `Next.js server stopped (force killed after ${timeout}s timeout)`, }); } }, timeout * 1000); process.once('exit', () => { if (!resolved) { clearTimeout(timeoutHandle); resolved = true; resolve({ wasRunning, message: 'Next.js server stopped gracefully', }); } }); process.kill('SIGTERM'); this.stateManager.addLog({ timestamp: new Date().toISOString(), level: 'info', message: 'Stopping Next.js development server gracefully', source: 'system', }); }); } getProcessInfo() { if (!this.currentProcess) { return { isRunning: false }; } const result = { isRunning: !this.currentProcess.killed, }; if (this.currentProcess.pid !== undefined) { result.pid = this.currentProcess.pid; } if (this.currentProcess.killed !== undefined) { result.killed = this.currentProcess.killed; } return result; } setupProcessHandlers(process) { process.on('exit', (code, signal) => { this.stateManager.setStopped(); if (signal) { this.stateManager.addLog({ timestamp: new Date().toISOString(), level: 'info', message: `Next.js process exited with signal ${signal}`, source: 'system', }); } else { this.stateManager.addLog({ timestamp: new Date().toISOString(), level: 'info', message: `Next.js process exited with code ${code}`, source: 'system', }); } this.currentProcess = null; }); process.on('error', (error) => { this.stateManager.addLog({ timestamp: new Date().toISOString(), level: 'error', message: `Process error: ${error.message}`, source: 'system', }); this.stateManager.setStopped(); this.currentProcess = null; }); process.stdout?.on('data', (data) => { const message = data.toString().trim(); if (message.length > 0) { const lines = message.split('\n'); lines.forEach((line) => { if (line.trim().length > 0) { this.stateManager.addLog({ timestamp: new Date().toISOString(), level: 'info', message: line.trim(), source: 'stdout', }); } }); } }); process.stderr?.on('data', (data) => { const message = data.toString().trim(); if (message.length > 0) { const lines = message.split('\n'); lines.forEach((line) => { if (line.trim().length > 0) { const level = this.detectLogLevel(line); this.stateManager.addLog({ timestamp: new Date().toISOString(), level, message: line.trim(), source: 'stderr', }); } }); } }); process.on('spawn', () => { this.stateManager.addLog({ timestamp: new Date().toISOString(), level: 'info', message: `Next.js process spawned with PID ${process.pid}`, source: 'system', }); }); } detectLogLevel(message) { const lowerMessage = message.toLowerCase(); if (lowerMessage.includes('error') || lowerMessage.includes('failed') || lowerMessage.includes('fatal')) { return 'error'; } if (lowerMessage.includes('warn') || lowerMessage.includes('warning') || lowerMessage.includes('deprecated')) { return 'warn'; } return 'info'; } cleanup() { if (this.currentProcess && !this.currentProcess.killed) { this.currentProcess.kill('SIGTERM'); } } } process.on('SIGINT', () => { ProcessManager.getInstance().cleanup(); process.exit(0); }); process.on('SIGTERM', () => { ProcessManager.getInstance().cleanup(); process.exit(0); }); //# sourceMappingURL=process-manager.js.map