import * as plugins from './plugins.js'; import * as paths from './paths.js'; import { ProcessMonitor, type IMonitorConfig } from './classes.processmonitor.js'; import { TspmConfig } from './classes.config.js'; import { Logger, ProcessError, ConfigError, ValidationError, handleError } from './utils.errorhandler.js'; export interface IProcessConfig extends IMonitorConfig { id: string; // Unique identifier for the process autorestart: boolean; // Whether to restart the process automatically on crash watch?: boolean; // Whether to watch for file changes and restart watchPaths?: string[]; // Paths to watch for changes } export interface IProcessInfo { id: string; pid?: number; status: 'online' | 'stopped' | 'errored'; memory: number; cpu?: number; uptime?: number; restarts: number; } export interface IProcessLog { timestamp: Date; type: 'stdout' | 'stderr' | 'system'; message: string; } export class Tspm { private processes: Map = new Map(); private processConfigs: Map = new Map(); private processInfo: Map = new Map(); private config: TspmConfig; private configStorageKey = 'processes'; private logger: Logger; constructor() { this.logger = new Logger('Tspm'); this.config = new TspmConfig(); this.loadProcessConfigs(); } /** * Start a new process with the given configuration */ public async start(config: IProcessConfig): Promise { this.logger.info(`Starting process with id '${config.id}'`); // Validate config if (!config.id || !config.command || !config.projectDir) { throw new ValidationError( 'Invalid process configuration: missing required fields', 'ERR_INVALID_CONFIG', { config } ); } // Check if process with this id already exists if (this.processes.has(config.id)) { throw new ValidationError( `Process with id '${config.id}' already exists`, 'ERR_DUPLICATE_PROCESS' ); } try { // Create and store process config this.processConfigs.set(config.id, config); // Initialize process info this.processInfo.set(config.id, { id: config.id, status: 'stopped', memory: 0, restarts: 0 }); // Create and start process monitor const monitor = new ProcessMonitor({ name: config.name || config.id, projectDir: config.projectDir, command: config.command, args: config.args, memoryLimitBytes: config.memoryLimitBytes, monitorIntervalMs: config.monitorIntervalMs, env: config.env, logBufferSize: config.logBufferSize }); this.processes.set(config.id, monitor); monitor.start(); // Update process info this.updateProcessInfo(config.id, { status: 'online' }); // Save updated configs await this.saveProcessConfigs(); this.logger.info(`Successfully started process with id '${config.id}'`); } catch (error: Error | unknown) { // Clean up in case of error this.processConfigs.delete(config.id); this.processInfo.delete(config.id); this.processes.delete(config.id); if (error instanceof Error) { this.logger.error(error); throw new ProcessError( `Failed to start process: ${error.message}`, 'ERR_PROCESS_START_FAILED', { id: config.id, command: config.command } ); } else { const genericError = new ProcessError( `Failed to start process: ${String(error)}`, 'ERR_PROCESS_START_FAILED', { id: config.id } ); this.logger.error(genericError); throw genericError; } } } /** * Stop a process by id */ public async stop(id: string): Promise { this.logger.info(`Stopping process with id '${id}'`); const monitor = this.processes.get(id); if (!monitor) { const error = new ValidationError( `Process with id '${id}' not found`, 'ERR_PROCESS_NOT_FOUND' ); this.logger.error(error); throw error; } try { monitor.stop(); this.updateProcessInfo(id, { status: 'stopped' }); this.logger.info(`Successfully stopped process with id '${id}'`); } catch (error: Error | unknown) { const processError = new ProcessError( `Failed to stop process: ${error instanceof Error ? error.message : String(error)}`, 'ERR_PROCESS_STOP_FAILED', { id } ); this.logger.error(processError); throw processError; } // Don't remove from the maps, just mark as stopped // This allows it to be restarted later } /** * Restart a process by id */ public async restart(id: string): Promise { this.logger.info(`Restarting process with id '${id}'`); const monitor = this.processes.get(id); const config = this.processConfigs.get(id); if (!monitor || !config) { const error = new ValidationError( `Process with id '${id}' not found`, 'ERR_PROCESS_NOT_FOUND' ); this.logger.error(error); throw error; } try { // Stop and then start the process monitor.stop(); // Create a new monitor instance const newMonitor = new ProcessMonitor({ name: config.name || config.id, projectDir: config.projectDir, command: config.command, args: config.args, memoryLimitBytes: config.memoryLimitBytes, monitorIntervalMs: config.monitorIntervalMs, env: config.env, logBufferSize: config.logBufferSize }); this.processes.set(id, newMonitor); newMonitor.start(); // Update restart count const info = this.processInfo.get(id); if (info) { this.updateProcessInfo(id, { status: 'online', restarts: info.restarts + 1 }); } this.logger.info(`Successfully restarted process with id '${id}'`); } catch (error: Error | unknown) { const processError = new ProcessError( `Failed to restart process: ${error instanceof Error ? error.message : String(error)}`, 'ERR_PROCESS_RESTART_FAILED', { id } ); this.logger.error(processError); throw processError; } } /** * Delete a process by id */ public async delete(id: string): Promise { this.logger.info(`Deleting process with id '${id}'`); // Check if process exists if (!this.processConfigs.has(id)) { const error = new ValidationError( `Process with id '${id}' not found`, 'ERR_PROCESS_NOT_FOUND' ); this.logger.error(error); throw error; } // Stop the process if it's running try { if (this.processes.has(id)) { await this.stop(id); } // Remove from all maps this.processes.delete(id); this.processConfigs.delete(id); this.processInfo.delete(id); // Save updated configs await this.saveProcessConfigs(); this.logger.info(`Successfully deleted process with id '${id}'`); } catch (error: Error | unknown) { // Even if stop fails, we should still try to delete the configuration try { this.processes.delete(id); this.processConfigs.delete(id); this.processInfo.delete(id); await this.saveProcessConfigs(); this.logger.info(`Successfully deleted process with id '${id}' after stopping failure`); } catch (deleteError: Error | unknown) { const configError = new ConfigError( `Failed to delete process configuration: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`, 'ERR_CONFIG_DELETE_FAILED', { id } ); this.logger.error(configError); throw configError; } } } /** * Get a list of all process infos */ public list(): IProcessInfo[] { return Array.from(this.processInfo.values()); } /** * Get detailed info for a specific process */ public describe(id: string): { config: IProcessConfig; info: IProcessInfo } | null { const config = this.processConfigs.get(id); const info = this.processInfo.get(id); if (!config || !info) { return null; } return { config, info }; } /** * Get process logs */ public getLogs(id: string, limit?: number): IProcessLog[] { const monitor = this.processes.get(id); if (!monitor) { return []; } return monitor.getLogs(limit); } /** * Start all saved processes */ public async startAll(): Promise { for (const [id, config] of this.processConfigs.entries()) { if (!this.processes.has(id)) { await this.start(config); } } } /** * Stop all running processes */ public async stopAll(): Promise { for (const id of this.processes.keys()) { await this.stop(id); } } /** * Restart all processes */ public async restartAll(): Promise { for (const id of this.processes.keys()) { await this.restart(id); } } /** * Update the info for a process */ private updateProcessInfo(id: string, update: Partial): void { const info = this.processInfo.get(id); if (info) { this.processInfo.set(id, { ...info, ...update }); } } /** * Save all process configurations to config storage */ private async saveProcessConfigs(): Promise { this.logger.debug('Saving process configurations to storage'); try { const configs = Array.from(this.processConfigs.values()); await this.config.writeKey(this.configStorageKey, JSON.stringify(configs)); this.logger.debug(`Saved ${configs.length} process configurations`); } catch (error: Error | unknown) { const configError = new ConfigError( `Failed to save process configurations: ${error instanceof Error ? error.message : String(error)}`, 'ERR_CONFIG_SAVE_FAILED' ); this.logger.error(configError); throw configError; } } /** * Load process configurations from config storage */ private async loadProcessConfigs(): Promise { this.logger.debug('Loading process configurations from storage'); try { const configsJson = await this.config.readKey(this.configStorageKey); if (configsJson) { try { const configs = JSON.parse(configsJson) as IProcessConfig[]; this.logger.debug(`Loaded ${configs.length} process configurations`); for (const config of configs) { // Validate config if (!config.id || !config.command || !config.projectDir) { this.logger.warn(`Skipping invalid process config for id '${config.id || 'unknown'}'`); continue; } this.processConfigs.set(config.id, config); // Initialize process info this.processInfo.set(config.id, { id: config.id, status: 'stopped', memory: 0, restarts: 0 }); } } catch (parseError: Error | unknown) { const configError = new ConfigError( `Failed to parse process configurations: ${parseError instanceof Error ? parseError.message : String(parseError)}`, 'ERR_CONFIG_PARSE_FAILED' ); this.logger.error(configError); throw configError; } } else { this.logger.info('No saved process configurations found'); } } catch (error: Error | unknown) { // Only throw if it's not the "no configs found" case if (error instanceof ConfigError) { throw error; } // If no configs found or error reading, just continue with empty configs this.logger.info('No saved process configurations found or error reading them'); } } }