UNPKG

mcp-extended-tools

Version:
240 lines (204 loc) 7.15 kB
import { spawn } from 'child_process'; import { EventEmitter } from 'events'; import path from 'path'; import fs from 'fs/promises'; import winston from 'winston'; // Simple debug logging function function debug(message) { console.log(`[DEBUG] ${message}`); } class CommandExecutor extends EventEmitter { constructor(options = {}) { super(); this.activeProcesses = new Map(); this.watchers = new Map(); // Configure logging this.logger = winston.createLogger({ level: options.logLevel || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.json() ), defaultMeta: { service: 'command-executor' }, transports: [ new winston.transports.File({ filename: path.join(options.logDir || 'logs', 'error.log'), level: 'error' }), new winston.transports.File({ filename: path.join(options.logDir || 'logs', 'combined.log') }) ] }); // Allowed commands whitelist this.allowedCommands = [ 'npm', 'yarn', 'pnpm', 'git', 'node', 'jest', 'vitest', 'vite', 'webpack', 'python', 'pip' ]; // Initialize log directory this.initializeLogDirectory(options.logDir || 'logs'); } async initializeLogDirectory(logDir) { try { await fs.mkdir(logDir, { recursive: true }); debug(`Log directory initialized: ${logDir}`); } catch (error) { debug(`Error creating log directory: ${error.message}`); throw new Error(`Cannot create log directory: ${error.message}`); } } isCommandAllowed(command) { const firstArg = command.split(' ')[0]; return this.allowedCommands.includes(firstArg); } async execute(command, args = [], options = {}) { if (!this.isCommandAllowed(command)) { const error = new Error('Command not allowed'); this.logger.error('Command execution blocked', { command, error: error.message }); throw error; } return new Promise((resolve, reject) => { const processId = Date.now().toString(); const startTime = new Date(); debug(`Executing command: ${command} ${args.join(' ')}`); const process = spawn(command, args, { shell: true, ...options }); this.activeProcesses.set(processId, { process, command, args, startTime, options }); 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 endTime = new Date(); const runtime = endTime - startTime; this.logger.info('Command execution completed', { processId, command, code, runtime }); this.activeProcesses.delete(processId); if (code === 0) { resolve({ processId, stdout, stderr, code, runtime }); } else { reject({ processId, stdout, stderr, code, runtime }); } }); process.on('error', (error) => { this.logger.error('Command execution failed', { processId, command, error: error.message }); this.activeProcesses.delete(processId); reject({ processId, error: error.message, code: 1 }); }); }); } async stop(processId, options = { force: false }) { const processInfo = this.activeProcesses.get(processId); if (!processInfo) { debug(`Process not found: ${processId}`); return false; } const { process } = processInfo; debug(`Stopping process: ${processId}, force: ${options.force}`); if (options.force) { process.kill('SIGKILL'); } else { process.kill('SIGTERM'); await new Promise(resolve => { const timeout = setTimeout(() => { process.kill('SIGKILL'); resolve(); }, 5000); process.on('close', () => { clearTimeout(timeout); resolve(); }); }); } this.activeProcesses.delete(processId); this.logger.info('Process stopped', { processId, force: options.force }); return true; } listActive() { return Array.from(this.activeProcesses.entries()).map(([id, info]) => ({ processId: id, command: info.command, args: info.args, startTime: info.startTime, runtime: new Date() - info.startTime, pid: info.process.pid })); } async cleanup() { debug('Cleaning up all processes and watchers'); // 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 })) ); this.logger.info('Cleanup completed', { processesTerminated: processes.length, watchersClosed: this.watchers.size }); } } // Create singleton instance const executor = new CommandExecutor(); // Cleanup on exit process.on('SIGINT', () => executor.cleanup()); process.on('SIGTERM', () => executor.cleanup()); export { CommandExecutor, executor, executor as default, executeCommand }; function executeCommand(command, args, options) { return executor.execute(command, args, options); }