@ai-mapping/mcp-nextjs-dev
Version:
MCP server for managing Next.js development processes with AI tools
240 lines • 8.6 kB
JavaScript
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