@ams-dev/process-manager
Version:
MCP server for managing and monitoring development processes
183 lines • 6.43 kB
JavaScript
import { execa } from 'execa';
import EventEmitter from 'eventemitter3';
export class ProcessManager extends EventEmitter {
processes = new Map();
nextId = 1;
defaultLogBuffer = 1000;
constructor() {
super();
}
async startProcess(config) {
const id = `proc-${this.nextId++}`;
const [cmd, ...args] = config.command.split(' ');
const info = {
id,
config,
status: 'running',
startTime: new Date(),
};
const childProcess = execa(cmd, args, {
cwd: config.cwd,
env: { ...globalThis.process.env, ...config.env },
reject: false,
all: true,
buffer: false,
});
info.pid = childProcess.pid;
const managedProcess = {
info,
process: childProcess,
logs: [],
logBuffer: this.defaultLogBuffer,
};
this.processes.set(id, managedProcess);
childProcess.stdout?.on('data', (data) => {
this.addLog(id, 'stdout', data.toString());
});
childProcess.stderr?.on('data', (data) => {
this.addLog(id, 'stderr', data.toString());
});
childProcess.on('exit', (code) => {
const proc = this.processes.get(id);
if (proc) {
proc.info.status = code === 0 ? 'stopped' : 'failed';
proc.info.stopTime = new Date();
proc.info.exitCode = code ?? undefined;
this.emit('process:exit', { id, code });
if (config.autoRestart && code !== 0) {
setTimeout(() => {
this.restartProcess(id).catch(err => {
console.error(`Failed to restart process ${id}:`, err);
});
}, 1000);
}
}
});
childProcess.on('error', (error) => {
const proc = this.processes.get(id);
if (proc) {
proc.info.status = 'failed';
proc.info.error = error.message;
this.emit('process:error', { id, error });
}
});
this.emit('process:start', info);
return info;
}
async stopProcess(processId, timeout = 5000) {
const managed = this.processes.get(processId);
if (!managed) {
throw new Error(`Process ${processId} not found`);
}
if (!managed.process || managed.info.status !== 'running') {
throw new Error(`Process ${processId} is not running`);
}
managed.process.kill('SIGTERM');
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
if (managed.process && managed.info.status === 'running') {
managed.process.kill('SIGKILL');
}
resolve();
}, timeout);
managed.process?.on('exit', () => {
clearTimeout(timer);
resolve();
});
managed.process?.on('error', (error) => {
clearTimeout(timer);
reject(error);
});
});
}
async restartProcess(processId) {
const managed = this.processes.get(processId);
if (!managed) {
throw new Error(`Process ${processId} not found`);
}
if (managed.info.status === 'running') {
await this.stopProcess(processId);
}
const newInfo = await this.startProcess(managed.info.config);
this.processes.delete(processId);
return newInfo;
}
getProcessLogs(processId, filter, since, lines) {
const managed = this.processes.get(processId);
if (!managed) {
throw new Error(`Process ${processId} not found`);
}
let logs = managed.logs;
if (since !== undefined) {
const sinceDate = new Date(since);
logs = logs.filter(log => log.timestamp >= sinceDate);
}
if (filter) {
const regex = new RegExp(filter);
logs = logs.filter(log => regex.test(log.data));
}
if (lines !== undefined) {
logs = logs.slice(-lines);
}
return logs;
}
listProcesses(status) {
const processes = Array.from(this.processes.values());
if (!status || status === 'all') {
return processes.map(p => p.info);
}
return processes
.filter(p => p.info.status === status)
.map(p => p.info);
}
sendInput(processId, input) {
const managed = this.processes.get(processId);
if (!managed) {
throw new Error(`Process ${processId} not found`);
}
if (!managed.process || managed.info.status !== 'running') {
throw new Error(`Process ${processId} is not running`);
}
if (!managed.process.stdin) {
throw new Error(`Process ${processId} does not support stdin`);
}
managed.process.stdin.write(input + '\n');
}
addLog(processId, stream, data) {
const managed = this.processes.get(processId);
if (!managed)
return;
const log = {
timestamp: new Date(),
stream,
data: data.trim(),
};
managed.logs.push(log);
if (managed.logs.length > managed.logBuffer) {
managed.logs = managed.logs.slice(-managed.logBuffer);
this.processes.get(processId).logs = managed.logs;
}
this.emit('process:log', { processId, log });
if (managed.info.config.watchPatterns) {
for (const pattern of managed.info.config.watchPatterns) {
const regex = new RegExp(pattern);
if (regex.test(data)) {
this.emit('process:pattern-match', {
processId,
pattern,
log,
});
}
}
}
}
cleanup() {
for (const managed of this.processes.values()) {
if (managed.info.status === 'running' && managed.process) {
managed.process.kill('SIGTERM');
}
}
this.processes.clear();
}
}
//# sourceMappingURL=process-manager.js.map