UNPKG

mcp-extended-tools

Version:
265 lines (232 loc) 8.11 kB
const { exec, spawn } = require('child_process'); const { EventEmitter } = require('events'); const path = require('path'); class CommandExecutor extends EventEmitter { constructor() { super(); this.activeProcesses = new Map(); this.watchers = new Map(); } // Original execute command - now more configurable execute(command, args = [], options = {}) { return new Promise((resolve, reject) => { const process = spawn(command, args, { shell: true, ...options, env: { ...process.env, ...options.env } }); const processId = Date.now().toString(); this.activeProcesses.set(processId, { process, command, args, options, startTime: new Date() }); let stdout = ''; let stderr = ''; process.stdout.on('data', (data) => { stdout += data; this.emit('output', { processId, type: 'stdout', data: data.toString() }); }); process.stderr.on('data', (data) => { stderr += data; this.emit('output', { processId, type: 'stderr', data: data.toString() }); }); process.on('close', (code) => { const processInfo = this.activeProcesses.get(processId); this.activeProcesses.delete(processId); if (code === 0) { resolve({ processId, stdout, stderr, code, runtime: new Date() - processInfo.startTime }); } else { reject({ processId, stdout, stderr, code, runtime: new Date() - processInfo.startTime }); } }); process.on('error', (error) => { const processInfo = this.activeProcesses.get(processId); this.activeProcesses.delete(processId); reject({ processId, error: error.message, stderr, code: 1, runtime: new Date() - processInfo.startTime }); }); }); } // Enhanced process management async stop(processId, options = { force: false }) { const processInfo = this.activeProcesses.get(processId); if (processInfo) { const { process } = processInfo; if (options.force) { process.kill('SIGKILL'); } else { process.kill('SIGTERM'); // Wait for graceful shutdown await new Promise(resolve => { const timeout = setTimeout(() => { process.kill('SIGKILL'); resolve(); }, 5000); process.on('close', () => { clearTimeout(timeout); resolve(); }); }); } this.activeProcesses.delete(processId); return true; } return false; } // Enhanced process listing with more details listActive() { const processes = []; for (const [id, info] of this.activeProcesses) { processes.push({ processId: id, command: info.command, args: info.args, startTime: info.startTime, runtime: new Date() - info.startTime, pid: info.process.pid }); } return processes; } // New method - Watch and auto-restart async watchAndExecute(command, args = [], options = {}) { const { watch = ['.'], ignore = ['node_modules/**', '**/*.pyc', '__pycache__/**'], restartDelay = 1000, ...spawnOptions } = options; // Initial start let currentProcess = await this.execute(command, args, spawnOptions); // Setup watcher const chokidar = require('chokidar'); const watcher = chokidar.watch(watch, { ignored: ignore, persistent: true }); watcher.on('change', async (path) => { this.emit('file-change', { path }); // Debounce restart if (this.restartTimeout) { clearTimeout(this.restartTimeout); } this.restartTimeout = setTimeout(async () => { // Stop current process await this.stop(currentProcess.processId); // Start new process try { currentProcess = await this.execute(command, args, spawnOptions); this.emit('restart', { processId: currentProcess.processId, trigger: path }); } catch (error) { this.emit('restart-error', error); } }, restartDelay); }); // Store watcher reference this.watchers.set(currentProcess.processId, watcher); return { processId: currentProcess.processId, stop: async () => { watcher.close(); this.watchers.delete(currentProcess.processId); await this.stop(currentProcess.processId); } }; } // New method - Server process management async manageServer(config) { const { command, args = [], port = 'auto', autoRestart = true, ...options } = config; // Auto port assignment if needed if (port === 'auto') { const portfinder = require('portfinder'); options.env = { ...options.env, PORT: await portfinder.getPortPromise() }; } else { options.env = { ...options.env, PORT: port }; } // Start server with auto-restart if (autoRestart) { return this.watchAndExecute(command, args, { ...options, watch: [options.watch || '.'], ignore: [ ...(options.ignore || []), 'node_modules/**', '**/*.pyc', '__pycache__/**' ] }); } // Start server without auto-restart return this.execute(command, args, options); } // Cleanup method async cleanup() { // Stop all watchers for (const [processId, watcher] of this.watchers) { watcher.close(); this.watchers.delete(processId); } // Stop all processes const processes = this.listActive(); await Promise.all( processes.map(proc => this.stop(proc.processId, { force: true })) ); } } // Create singleton instance const executor = new CommandExecutor(); // Cleanup on exit process.on('SIGINT', () => executor.cleanup()); process.on('SIGTERM', () => executor.cleanup()); module.exports = { executor, executeCommand: (command, args, options) => executor.execute(command, args, options), watchAndExecute: (command, args, options) => executor.watchAndExecute(command, args, options), manageServer: (config) => executor.manageServer(config) };