From 538f282b622042304b3ebd3e0258c9f2e85a48f6 Mon Sep 17 00:00:00 2001 From: Juergen Kunz Date: Sat, 30 Aug 2025 13:47:14 +0000 Subject: [PATCH] BREAKING CHANGE(daemon): Introduce persistent log storage, numeric ProcessId type, and improved process monitoring / IPC handling --- changelog.md | 13 ++ package.json | 1 + pnpm-lock.yaml | 57 +------ ts/00_commitinfo_data.ts | 2 +- ts/cli/commands/default.ts | 2 +- ts/cli/commands/process/list.ts | 2 +- ts/cli/commands/process/restart.ts | 3 +- ts/client/tspm.ipcclient.ts | 12 +- ts/daemon/logpersistence.ts | 117 ++++++++++++++ ts/daemon/processmanager.ts | 251 +++++++++++++++++++++++------ ts/daemon/processmonitor.ts | 97 ++++++++++- ts/daemon/processwrapper.ts | 56 ++++++- ts/daemon/tspm.daemon.ts | 36 +++-- ts/plugins.ts | 3 +- ts/shared/protocol/id.ts | 56 +++++++ ts/shared/protocol/ipc.types.ts | 48 +++--- 16 files changed, 589 insertions(+), 167 deletions(-) create mode 100644 ts/daemon/logpersistence.ts create mode 100644 ts/shared/protocol/id.ts diff --git a/changelog.md b/changelog.md index 010b42c..05b2a60 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,18 @@ # Changelog +## 2025-08-30 - 5.0.0 - BREAKING CHANGE(daemon) +Introduce persistent log storage, numeric ProcessId type, and improved process monitoring / IPC handling + +- Add LogPersistence: persistent on-disk storage for process logs (save/load/delete/cleanup). +- Persist logs on process exit/error/stop and trim in-memory buffers to avoid excessive memory usage. +- Introduce a branded numeric ProcessId type and toProcessId helpers; migrate IPC types and internal maps from string ids to ProcessId. +- ProcessManager refactor: typed maps for processes/configs/info/logs, async start/stop/restart flows, improved PID/uptime/restart tracking, and desired state persistence handling. +- ProcessMonitor refactor: async lifecycle (start/stop), load persisted logs on startup, flush logs to disk on exit/error/stop, log memory capping, and improved event emissions. +- ProcessWrapper improvements: buffer stdout/stderr remainders, flush partial lines on stream end, clearer debug logging. +- IPC client/server changes: handlers now normalize ids with toProcessId, subscribe/unsubscribe accept numeric/string ids, getLogs/start/stop/restart/delete use typed ids. +- CLI tweaks: format process id output safely with String() to avoid formatting issues. +- Add dependency and plugin export for @push.rocks/smartfile and update package.json accordingly. + ## 2025-08-29 - 4.4.2 - fix(daemon) Fix daemon IPC id handling, reload configs on demand and correct CLI daemon start path diff --git a/package.json b/package.json index a4e5bc7..e7371e8 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "@push.rocks/projectinfo": "^5.0.2", "@push.rocks/smartcli": "^4.0.11", "@push.rocks/smartdaemon": "^2.0.9", + "@push.rocks/smartfile": "^11.2.7", "@push.rocks/smartinteract": "^2.0.16", "@push.rocks/smartipc": "^2.2.2", "@push.rocks/smartpath": "^6.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c7c305c..28e4940 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@push.rocks/smartdaemon': specifier: ^2.0.9 version: 2.0.9 + '@push.rocks/smartfile': + specifier: ^11.2.7 + version: 11.2.7 '@push.rocks/smartinteract': specifier: ^2.0.16 version: 2.0.16 @@ -844,9 +847,6 @@ packages: '@push.rocks/smartfile@10.0.41': resolution: {integrity: sha512-xOOy0duI34M2qrJZggpk51EHGXmg9+mBL1Q55tNiQKXzfx89P3coY1EAZG8tvmep3qB712QEKe7T+u04t42Kjg==} - '@push.rocks/smartfile@11.2.0': - resolution: {integrity: sha512-0Gw6DvCQ2D/BXNN6airSC7hoSBut0p/uNWf2+rqO+D6VLhIJ/QUBvF6xm/LnpPI/zcF8YlDn/GEriInB5DUtEw==} - '@push.rocks/smartfile@11.2.7': resolution: {integrity: sha512-8Yp7/sAgPpWJBHohV92ogHWKzRomI5MEbSG6b5W2n18tqwfAmjMed0rQvsvGrSBlnEWCKgoOrYIIZbLO61+J0Q==} @@ -2669,11 +2669,6 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true - glob@11.0.1: - resolution: {integrity: sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==} - engines: {node: 20 || >=22} - hasBin: true - glob@11.0.3: resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==} engines: {node: 20 || >=22} @@ -2989,10 +2984,6 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jackspeak@4.1.0: - resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==} - engines: {node: 20 || >=22} - jackspeak@4.1.1: resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==} engines: {node: 20 || >=22} @@ -3412,10 +3403,6 @@ packages: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} - minimatch@10.0.1: - resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==} - engines: {node: 20 || >=22} - minimatch@10.0.3: resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==} engines: {node: 20 || >=22} @@ -5665,7 +5652,7 @@ snapshots: '@git.zone/tsrun@1.3.3': dependencies: - '@push.rocks/smartfile': 11.2.0 + '@push.rocks/smartfile': 11.2.7 '@push.rocks/smartshell': 3.2.3 tsx: 4.20.5 @@ -6290,25 +6277,6 @@ snapshots: glob: 10.4.5 js-yaml: 4.1.0 - '@push.rocks/smartfile@11.2.0': - dependencies: - '@push.rocks/lik': 6.1.0 - '@push.rocks/smartdelay': 3.0.5 - '@push.rocks/smartfile-interfaces': 1.0.7 - '@push.rocks/smarthash': 3.0.4 - '@push.rocks/smartjson': 5.0.20 - '@push.rocks/smartmime': 2.0.4 - '@push.rocks/smartpath': 5.1.0 - '@push.rocks/smartpromise': 4.2.3 - '@push.rocks/smartrequest': 2.0.23 - '@push.rocks/smartstream': 3.2.5 - '@types/fs-extra': 11.0.4 - '@types/glob': 8.1.0 - '@types/js-yaml': 4.0.9 - fs-extra: 11.3.0 - glob: 11.0.1 - js-yaml: 4.1.0 - '@push.rocks/smartfile@11.2.7': dependencies: '@push.rocks/lik': 6.2.2 @@ -8736,15 +8704,6 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 - glob@11.0.1: - dependencies: - foreground-child: 3.3.1 - jackspeak: 4.1.0 - minimatch: 10.0.1 - minipass: 7.1.2 - package-json-from-dist: 1.0.1 - path-scurry: 2.0.0 - glob@11.0.3: dependencies: foreground-child: 3.3.1 @@ -9093,10 +9052,6 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jackspeak@4.1.0: - dependencies: - '@isaacs/cliui': 8.0.2 - jackspeak@4.1.1: dependencies: '@isaacs/cliui': 8.0.2 @@ -9729,10 +9684,6 @@ snapshots: min-indent@1.0.1: {} - minimatch@10.0.1: - dependencies: - brace-expansion: 2.0.1 - minimatch@10.0.3: dependencies: '@isaacs/brace-expansion': 5.0.0 diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index afc6976..f6e1f8a 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@git.zone/tspm', - version: '4.4.2', + version: '5.0.0', description: 'a no fuzz process manager' } diff --git a/ts/cli/commands/default.ts b/ts/cli/commands/default.ts index 26f7ec6..290ad19 100644 --- a/ts/cli/commands/default.ts +++ b/ts/cli/commands/default.ts @@ -74,7 +74,7 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) { const resetColor = '\x1b[0m'; console.log( - `│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad(formatMemory(proc.memory), 9)} │ ${pad(proc.restarts.toString(), 8)} │`, + `│ ${pad(String(proc.id), 7)} │ ${pad(String(proc.id), 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad(formatMemory(proc.memory), 9)} │ ${pad(proc.restarts.toString(), 8)} │`, ); } diff --git a/ts/cli/commands/process/list.ts b/ts/cli/commands/process/list.ts index eab488d..4011059 100644 --- a/ts/cli/commands/process/list.ts +++ b/ts/cli/commands/process/list.ts @@ -39,7 +39,7 @@ export function registerListCommand(smartcli: plugins.smartcli.Smartcli) { const resetColor = '\x1b[0m'; console.log( - `│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad((proc.pid || '-').toString(), 9)} │ ${pad(formatMemory(proc.memory), 8)} │ ${pad(proc.restarts.toString(), 8)} │`, + `│ ${pad(String(proc.id), 7)} │ ${pad(String(proc.id), 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad((proc.pid || '-').toString(), 9)} │ ${pad(formatMemory(proc.memory), 8)} │ ${pad(proc.restarts.toString(), 8)} │`, ); } diff --git a/ts/cli/commands/process/restart.ts b/ts/cli/commands/process/restart.ts index 097f093..c277836 100644 --- a/ts/cli/commands/process/restart.ts +++ b/ts/cli/commands/process/restart.ts @@ -1,5 +1,6 @@ import * as plugins from '../../plugins.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js'; +import { toProcessId } from '../../../shared/protocol/id.js'; import type { CliArguments } from '../../types.js'; import { registerIpcCommand } from '../../registration/index.js'; @@ -34,7 +35,7 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) { const id = String(arg); console.log(`Restarting process: ${id}`); - const response = await tspmIpcClient.request('restart', { id }); + const response = await tspmIpcClient.request('restart', { id: toProcessId(id) }); console.log(`✓ Process restarted successfully`); console.log(` ID: ${response.processId}`); diff --git a/ts/client/tspm.ipcclient.ts b/ts/client/tspm.ipcclient.ts index 86be83f..2d85823 100644 --- a/ts/client/tspm.ipcclient.ts +++ b/ts/client/tspm.ipcclient.ts @@ -1,5 +1,7 @@ import * as plugins from './plugins.js'; import * as paths from '../paths.js'; +import { toProcessId } from '../shared/protocol/id.js'; +import type { ProcessId } from '../shared/protocol/id.js'; import type { IpcMethodMap, @@ -144,26 +146,28 @@ export class TspmIpcClient { * Subscribe to log updates for a specific process */ public async subscribe( - processId: string, + processId: ProcessId | number | string, handler: (log: any) => void, ): Promise { if (!this.ipcClient || !this.isConnected) { throw new Error('Not connected to daemon'); } - const topic = `logs.${processId}`; + const id = toProcessId(processId); + const topic = `logs.${id}`; await this.ipcClient.subscribe(`topic:${topic}`, handler); } /** * Unsubscribe from log updates for a specific process */ - public async unsubscribe(processId: string): Promise { + public async unsubscribe(processId: ProcessId | number | string): Promise { if (!this.ipcClient || !this.isConnected) { throw new Error('Not connected to daemon'); } - const topic = `logs.${processId}`; + const id = toProcessId(processId); + const topic = `logs.${id}`; await this.ipcClient.unsubscribe(`topic:${topic}`); } diff --git a/ts/daemon/logpersistence.ts b/ts/daemon/logpersistence.ts new file mode 100644 index 0000000..f91b292 --- /dev/null +++ b/ts/daemon/logpersistence.ts @@ -0,0 +1,117 @@ +import * as plugins from '../plugins.js'; +import * as paths from '../paths.js'; +import type { IProcessLog } from '../shared/protocol/ipc.types.js'; +import type { ProcessId } from '../shared/protocol/id.js'; + +/** + * Manages persistent log storage for processes + */ +export class LogPersistence { + private logsDir: string; + + constructor() { + this.logsDir = plugins.path.join(paths.tspmDir, 'logs'); + } + + /** + * Get the log file path for a process + */ + private getLogFilePath(processId: ProcessId): string { + return plugins.path.join(this.logsDir, `process-${processId}.json`); + } + + /** + * Ensure the logs directory exists + */ + private async ensureLogsDir(): Promise { + await plugins.smartfile.fs.ensureDir(this.logsDir); + } + + /** + * Save logs to disk + */ + public async saveLogs(processId: ProcessId, logs: IProcessLog[]): Promise { + await this.ensureLogsDir(); + const filePath = this.getLogFilePath(processId); + + // Write logs as JSON + await plugins.smartfile.memory.toFs( + JSON.stringify(logs, null, 2), + filePath + ); + } + + /** + * Load logs from disk + */ + public async loadLogs(processId: ProcessId): Promise { + const filePath = this.getLogFilePath(processId); + + try { + const exists = await plugins.smartfile.fs.fileExists(filePath); + if (!exists) { + return []; + } + + const content = await plugins.smartfile.fs.toStringSync(filePath); + const logs = JSON.parse(content) as IProcessLog[]; + + // Convert date strings back to Date objects + return logs.map(log => ({ + ...log, + timestamp: new Date(log.timestamp) + })); + } catch (error) { + console.error(`Failed to load logs for process ${processId}:`, error); + return []; + } + } + + /** + * Delete logs from disk after loading + */ + public async deleteLogs(processId: ProcessId): Promise { + const filePath = this.getLogFilePath(processId); + + try { + const exists = await plugins.smartfile.fs.fileExists(filePath); + if (exists) { + await plugins.smartfile.fs.remove(filePath); + } + } catch (error) { + console.error(`Failed to delete logs for process ${processId}:`, error); + } + } + + /** + * Calculate approximate memory size of logs in bytes + */ + public static calculateLogMemorySize(logs: IProcessLog[]): number { + // Estimate based on JSON string size + // This is an approximation but good enough for our purposes + return JSON.stringify(logs).length; + } + + /** + * Clean up old log files (for maintenance) + */ + public async cleanupOldLogs(): Promise { + try { + await this.ensureLogsDir(); + const files = await plugins.smartfile.fs.listFileTree(this.logsDir, '*.json'); + + for (const file of files) { + const filePath = plugins.path.join(this.logsDir, file); + const stats = await plugins.smartfile.fs.stat(filePath); + + // Delete files older than 7 days + const ageInDays = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24); + if (ageInDays > 7) { + await plugins.smartfile.fs.remove(filePath); + } + } + } catch (error) { + console.error('Failed to cleanup old logs:', error); + } + } +} \ No newline at end of file diff --git a/ts/daemon/processmanager.ts b/ts/daemon/processmanager.ts index 969988b..ba81c81 100644 --- a/ts/daemon/processmanager.ts +++ b/ts/daemon/processmanager.ts @@ -2,6 +2,7 @@ import * as plugins from '../plugins.js'; import { EventEmitter } from 'events'; import * as paths from '../paths.js'; import { ProcessMonitor } from './processmonitor.js'; +import { LogPersistence } from './logpersistence.js'; import { TspmConfig } from './tspm.config.js'; import { Logger, @@ -16,17 +17,20 @@ import type { IProcessLog, IMonitorConfig } from '../shared/protocol/ipc.types.js'; +import { toProcessId, getNextProcessId } from '../shared/protocol/id.js'; +import type { ProcessId } from '../shared/protocol/id.js'; export class ProcessManager extends EventEmitter { - public processes: Map = new Map(); - public processConfigs: Map = new Map(); - public processInfo: Map = new Map(); + public processes: Map = new Map(); + public processConfigs: Map = new Map(); + public processInfo: Map = new Map(); + private processLogs: Map = new Map(); private config: TspmConfig; private configStorageKey = 'processes'; private desiredStateStorageKey = 'desiredStates'; - private desiredStates: Map = new Map(); + private desiredStates: Map = new Map(); private logger: Logger; constructor() { @@ -39,14 +43,14 @@ export class ProcessManager extends EventEmitter { /** * Add a process configuration without starting it. - * Returns the assigned numeric sequential id as string. + * Returns the assigned numeric sequential id. */ - public async add(configInput: Omit & { id?: string }): Promise { + public async add(configInput: Omit & { id?: ProcessId }): Promise { // Determine next numeric id const nextId = this.getNextSequentialId(); const config: IProcessConfig = { - id: String(nextId), + id: nextId, name: configInput.name || `process-${nextId}`, command: configInput.command, args: configInput.args, @@ -111,7 +115,8 @@ export class ProcessManager extends EventEmitter { // Create and start process monitor const monitor = new ProcessMonitor({ - name: config.name || config.id, + id: config.id, // Pass the ProcessId for log persistence + name: config.name || String(config.id), projectDir: config.projectDir, command: config.command, args: config.args, @@ -125,13 +130,43 @@ export class ProcessManager extends EventEmitter { // Set up log event handler to re-emit for pub/sub monitor.on('log', (log: IProcessLog) => { + // Store log in our persistent storage + if (!this.processLogs.has(config.id)) { + this.processLogs.set(config.id, []); + } + const logs = this.processLogs.get(config.id)!; + logs.push(log); + + // Trim logs if they exceed buffer size (default 1000) + const bufferSize = config.logBufferSize || 1000; + if (logs.length > bufferSize) { + this.processLogs.set(config.id, logs.slice(-bufferSize)); + } + this.emit('process:log', { processId: config.id, log }); }); + + // Set up event handler to track PID when process starts + monitor.on('start', (pid: number) => { + this.updateProcessInfo(config.id, { pid }); + }); + + // Set up event handler to clear PID when process exits + monitor.on('exit', () => { + this.updateProcessInfo(config.id, { pid: undefined }); + }); - monitor.start(); + await monitor.start(); - // Update process info - this.updateProcessInfo(config.id, { status: 'online' }); + // Wait a moment for the process to spawn and get its PID + await new Promise(resolve => setTimeout(resolve, 100)); + + // Update process info with PID + const pid = monitor.getPid(); + this.updateProcessInfo(config.id, { + status: 'online', + pid: pid || undefined + }); // Save updated configs await this.saveProcessConfigs(); @@ -165,7 +200,7 @@ export class ProcessManager extends EventEmitter { /** * Stop a process by id */ - public async stop(id: string): Promise { + public async stop(id: ProcessId): Promise { this.logger.info(`Stopping process with id '${id}'`); const monitor = this.processes.get(id); @@ -179,7 +214,7 @@ export class ProcessManager extends EventEmitter { } try { - monitor.stop(); + await monitor.stop(); this.updateProcessInfo(id, { status: 'stopped' }); this.logger.info(`Successfully stopped process with id '${id}'`); } catch (error: Error | unknown) { @@ -199,7 +234,7 @@ export class ProcessManager extends EventEmitter { /** * Restart a process by id */ - public async restart(id: string): Promise { + public async restart(id: ProcessId): Promise { this.logger.info(`Restarting process with id '${id}'`); const monitor = this.processes.get(id); @@ -216,11 +251,12 @@ export class ProcessManager extends EventEmitter { try { // Stop and then start the process - monitor.stop(); + await monitor.stop(); // Create a new monitor instance const newMonitor = new ProcessMonitor({ - name: config.name || config.id, + id: config.id, // Pass the ProcessId for log persistence + name: config.name || String(config.id), projectDir: config.projectDir, command: config.command, args: config.args, @@ -230,14 +266,37 @@ export class ProcessManager extends EventEmitter { logBufferSize: config.logBufferSize, }); + // Set up log event handler for the new monitor + newMonitor.on('log', (log: IProcessLog) => { + // Store log in our persistent storage + if (!this.processLogs.has(id)) { + this.processLogs.set(id, []); + } + const logs = this.processLogs.get(id)!; + logs.push(log); + + // Trim logs if they exceed buffer size (default 1000) + const bufferSize = config.logBufferSize || 1000; + if (logs.length > bufferSize) { + this.processLogs.set(id, logs.slice(-bufferSize)); + } + + this.emit('process:log', { processId: id, log }); + }); + this.processes.set(id, newMonitor); - newMonitor.start(); + await newMonitor.start(); - // Update restart count + // Wait a moment for the process to spawn and get its PID + await new Promise(resolve => setTimeout(resolve, 100)); + + // Update restart count and PID const info = this.processInfo.get(id); if (info) { + const pid = newMonitor.getPid(); this.updateProcessInfo(id, { status: 'online', + pid: pid || undefined, restarts: info.restarts + 1, }); } @@ -257,7 +316,7 @@ export class ProcessManager extends EventEmitter { /** * Delete a process by id */ - public async delete(id: string): Promise { + public async delete(id: ProcessId): Promise { this.logger.info(`Deleting process with id '${id}'`); // Check if process exists @@ -280,6 +339,11 @@ export class ProcessManager extends EventEmitter { this.processes.delete(id); this.processConfigs.delete(id); this.processInfo.delete(id); + this.processLogs.delete(id); + + // Delete persisted logs from disk + const logPersistence = new LogPersistence(); + await logPersistence.deleteLogs(id); // Save updated configs await this.saveProcessConfigs(); @@ -292,6 +356,12 @@ export class ProcessManager extends EventEmitter { this.processes.delete(id); this.processConfigs.delete(id); this.processInfo.delete(id); + this.processLogs.delete(id); + + // Delete persisted logs from disk even if stop failed + const logPersistence = new LogPersistence(); + await logPersistence.deleteLogs(id); + await this.saveProcessConfigs(); await this.removeDesiredState(id); @@ -314,14 +384,42 @@ export class ProcessManager extends EventEmitter { * Get a list of all process infos */ public list(): IProcessInfo[] { - return Array.from(this.processInfo.values()); + const infos = Array.from(this.processInfo.values()); + + // Enrich with live data from monitors + for (const info of infos) { + const monitor = this.processes.get(info.id); + if (monitor) { + // Update with current PID if the monitor is running + const pid = monitor.getPid(); + if (pid) { + info.pid = pid; + } + + // Update uptime if available + const uptime = monitor.getUptime(); + if (uptime !== null) { + info.uptime = uptime; + } + + // Update restart count + info.restarts = monitor.getRestartCount(); + + // Update status based on actual running state + if (monitor.isRunning()) { + info.status = 'online'; + } + } + } + + return infos; } /** * Get detailed info for a specific process */ public describe( - id: string, + id: ProcessId, ): { config: IProcessConfig; info: IProcessInfo } | null { const config = this.processConfigs.get(id); const info = this.processInfo.get(id); @@ -336,13 +434,21 @@ export class ProcessManager extends EventEmitter { /** * Get process logs */ - public getLogs(id: string, limit?: number): IProcessLog[] { + public getLogs(id: ProcessId, limit?: number): IProcessLog[] { + // Get logs from the ProcessMonitor instance const monitor = this.processes.get(id); - if (!monitor) { - return []; + + if (monitor) { + const logs = monitor.getLogs(limit); + return logs; } - - return monitor.getLogs(limit); + + // Fallback to stored logs if monitor doesn't exist + const logs = this.processLogs.get(id) || []; + if (limit && limit > 0) { + return logs.slice(-limit); + } + return logs; } /** @@ -377,7 +483,7 @@ export class ProcessManager extends EventEmitter { /** * Update the info for a process */ - private updateProcessInfo(id: string, update: Partial): void { + private updateProcessInfo(id: ProcessId, update: Partial): void { const info = this.processInfo.get(id); if (info) { this.processInfo.set(id, { ...info, ...update }); @@ -387,15 +493,40 @@ export class ProcessManager extends EventEmitter { /** * Compute next sequential numeric id based on existing configs */ - private getNextSequentialId(): number { - let maxId = 0; - for (const id of this.processConfigs.keys()) { - const n = parseInt(id, 10); - if (!isNaN(n)) { - maxId = Math.max(maxId, n); + /** + * Sync process stats from monitors to processInfo + */ + public syncProcessStats(): void { + for (const [id, monitor] of this.processes.entries()) { + const info = this.processInfo.get(id); + if (info) { + const pid = monitor.getPid(); + const updates: Partial = {}; + + // Update PID if available + if (pid) { + updates.pid = pid; + } + + // Update uptime if available + const uptime = monitor.getUptime(); + if (uptime !== null) { + updates.uptime = uptime; + } + + // Update restart count + updates.restarts = monitor.getRestartCount(); + + // Update status based on actual running state + updates.status = monitor.isRunning() ? 'online' : 'stopped'; + + this.updateProcessInfo(id, updates); } } - return maxId + 1; + } + + private getNextSequentialId(): ProcessId { + return getNextProcessId(this.processConfigs.keys()); } /** @@ -426,7 +557,7 @@ export class ProcessManager extends EventEmitter { try { const obj: Record = {}; for (const [id, state] of this.desiredStates.entries()) { - obj[id] = state; + obj[String(id)] = state; } await this.config.writeKey( this.desiredStateStorageKey, @@ -444,7 +575,9 @@ export class ProcessManager extends EventEmitter { const raw = await this.config.readKey(this.desiredStateStorageKey); if (raw) { const obj = JSON.parse(raw) as Record; - this.desiredStates = new Map(Object.entries(obj)); + this.desiredStates = new Map( + Object.entries(obj).map(([k, v]) => [toProcessId(k), v] as const) + ); this.logger.debug( `Loaded desired states for ${this.desiredStates.size} processes`, ); @@ -457,14 +590,14 @@ export class ProcessManager extends EventEmitter { } public async setDesiredState( - id: string, + id: ProcessId, state: IProcessInfo['status'], ): Promise { this.desiredStates.set(id, state); await this.saveDesiredStates(); } - public async removeDesiredState(id: string): Promise { + public async removeDesiredState(id: ProcessId): Promise { this.desiredStates.delete(id); await this.saveDesiredStates(); } @@ -505,23 +638,35 @@ export class ProcessManager extends EventEmitter { 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`); + const parsed = JSON.parse(configsJson) as Array; + this.logger.debug(`Loaded ${parsed.length} process configurations`); - for (const config of configs) { - // Validate config - if (!config.id || !config.command || !config.projectDir) { + for (const raw of parsed) { + // Convert legacy string IDs to ProcessId + let id: ProcessId; + try { + id = toProcessId(raw.id); + } catch { this.logger.warn( - `Skipping invalid process config for id '${config.id || 'unknown'}'`, + `Skipping invalid process config with non-numeric id '${raw.id || 'unknown'}'`, ); continue; } - - this.processConfigs.set(config.id, config); + + // Validate config + if (!id || !raw.command || !raw.projectDir) { + this.logger.warn( + `Skipping invalid process config for id '${id || 'unknown'}'`, + ); + continue; + } + + const config: IProcessConfig = { ...raw, id }; + this.processConfigs.set(id, config); // Initialize process info - this.processInfo.set(config.id, { - id: config.id, + this.processInfo.set(id, { + id: id, status: 'stopped', memory: 0, restarts: 0, @@ -555,15 +700,15 @@ export class ProcessManager extends EventEmitter { * Reset: stop all running processes and clear all saved configurations */ public async reset(): Promise<{ - stopped: string[]; - removed: string[]; - failed: Array<{ id: string; error: string }>; + stopped: ProcessId[]; + removed: ProcessId[]; + failed: Array<{ id: ProcessId; error: string }>; }> { this.logger.info('Resetting TSPM: stopping all processes and clearing configs'); const removed = Array.from(this.processConfigs.keys()); - const stopped: string[] = []; - const failed: Array<{ id: string; error: string }> = []; + const stopped: ProcessId[] = []; + const failed: Array<{ id: ProcessId; error: string }> = []; // Attempt to stop all currently running processes with per-id error collection for (const id of Array.from(this.processes.keys())) { diff --git a/ts/daemon/processmonitor.ts b/ts/daemon/processmonitor.ts index 31a494d..a0b432e 100644 --- a/ts/daemon/processmonitor.ts +++ b/ts/daemon/processmonitor.ts @@ -1,8 +1,10 @@ import * as plugins from '../plugins.js'; import { EventEmitter } from 'events'; import { ProcessWrapper } from './processwrapper.js'; +import { LogPersistence } from './logpersistence.js'; import { Logger, ProcessError, handleError } from '../shared/common/utils.errorhandler.js'; import type { IMonitorConfig, IProcessLog } from '../shared/protocol/ipc.types.js'; +import type { ProcessId } from '../shared/protocol/id.js'; export class ProcessMonitor extends EventEmitter { private processWrapper: ProcessWrapper | null = null; @@ -11,14 +13,36 @@ export class ProcessMonitor extends EventEmitter { private stopped: boolean = true; // Initially stopped until start() is called private restartCount: number = 0; private logger: Logger; + private logs: IProcessLog[] = []; + private logPersistence: LogPersistence; + private processId?: ProcessId; + private currentLogMemorySize: number = 0; + private readonly MAX_LOG_MEMORY_SIZE = 10 * 1024 * 1024; // 10MB - constructor(config: IMonitorConfig) { + constructor(config: IMonitorConfig & { id?: ProcessId }) { super(); this.config = config; this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`); + this.logs = []; + this.logPersistence = new LogPersistence(); + this.processId = config.id; + this.currentLogMemorySize = 0; } - public start(): void { + public async start(): Promise { + // Load previously persisted logs if available + if (this.processId) { + const persistedLogs = await this.logPersistence.loadLogs(this.processId); + if (persistedLogs.length > 0) { + this.logs = persistedLogs; + this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs); + this.logger.info(`Loaded ${persistedLogs.length} persisted logs from disk`); + + // Delete the persisted file after loading + await this.logPersistence.deleteLogs(this.processId); + } + } + // Reset the stopped flag so that new processes can spawn. this.stopped = false; this.log(`Starting process monitor.`); @@ -57,6 +81,22 @@ export class ProcessMonitor extends EventEmitter { // Set up event handlers this.processWrapper.on('log', (log: IProcessLog): void => { + // Store the log in our buffer + this.logs.push(log); + console.error(`[ProcessMonitor:${this.config.name}] Received log (type=${log.type}): ${log.message}`); + console.error(`[ProcessMonitor:${this.config.name}] Logs array now has ${this.logs.length} items`); + this.logger.debug(`ProcessMonitor received log: ${log.message}`); + + // Update memory size tracking + this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs); + + // Trim logs if they exceed memory limit (10MB) + while (this.currentLogMemorySize > this.MAX_LOG_MEMORY_SIZE && this.logs.length > 1) { + // Remove oldest logs until we're under the memory limit + this.logs.shift(); + this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs); + } + // Re-emit the log event for upstream handlers this.emit('log', log); @@ -65,13 +105,31 @@ export class ProcessMonitor extends EventEmitter { this.log(log.message); } }); + + // Re-emit start event with PID for upstream handlers + this.processWrapper.on('start', (pid: number): void => { + this.emit('start', pid); + }); this.processWrapper.on( 'exit', - (code: number | null, signal: string | null): void => { + async (code: number | null, signal: string | null): Promise => { const exitMsg = `Process exited with code ${code}, signal ${signal}.`; this.logger.info(exitMsg); this.log(exitMsg); + + // Flush logs to disk on exit + if (this.processId && this.logs.length > 0) { + try { + await this.logPersistence.saveLogs(this.processId, this.logs); + this.logger.debug(`Flushed ${this.logs.length} logs to disk on exit`); + } catch (error) { + this.logger.error(`Failed to flush logs to disk on exit: ${error}`); + } + } + + // Re-emit exit event for upstream handlers + this.emit('exit', code, signal); if (!this.stopped) { this.logger.info('Restarting process...'); @@ -86,7 +144,7 @@ export class ProcessMonitor extends EventEmitter { }, ); - this.processWrapper.on('error', (error: Error | ProcessError): void => { + this.processWrapper.on('error', async (error: Error | ProcessError): Promise => { const errorMsg = error instanceof ProcessError ? `Process error: ${error.toString()}` @@ -95,6 +153,16 @@ export class ProcessMonitor extends EventEmitter { this.logger.error(error); this.log(errorMsg); + // Flush logs to disk on error + if (this.processId && this.logs.length > 0) { + try { + await this.logPersistence.saveLogs(this.processId, this.logs); + this.logger.debug(`Flushed ${this.logs.length} logs to disk on error`); + } catch (flushError) { + this.logger.error(`Failed to flush logs to disk on error: ${flushError}`); + } + } + if (!this.stopped) { this.logger.info('Restarting process due to error...'); this.log('Restarting process due to error...'); @@ -239,9 +307,20 @@ export class ProcessMonitor extends EventEmitter { /** * Stop the monitor and prevent any further respawns. */ - public stop(): void { + public async stop(): Promise { this.log('Stopping process monitor.'); this.stopped = true; + + // Flush logs to disk before stopping + if (this.processId && this.logs.length > 0) { + try { + await this.logPersistence.saveLogs(this.processId, this.logs); + this.logger.info(`Flushed ${this.logs.length} logs to disk on stop`); + } catch (error) { + this.logger.error(`Failed to flush logs to disk on stop: ${error}`); + } + } + if (this.intervalId) { clearInterval(this.intervalId); } @@ -254,10 +333,12 @@ export class ProcessMonitor extends EventEmitter { * Get the current logs from the process */ public getLogs(limit?: number): IProcessLog[] { - if (!this.processWrapper) { - return []; + console.error(`[ProcessMonitor:${this.config.name}] getLogs called, logs.length=${this.logs.length}, limit=${limit}`); + this.logger.debug(`Getting logs, total stored: ${this.logs.length}`); + if (limit && limit > 0) { + return this.logs.slice(-limit); } - return this.processWrapper.getLogs(limit); + return this.logs; } /** diff --git a/ts/daemon/processwrapper.ts b/ts/daemon/processwrapper.ts index 208e573..8a9c843 100644 --- a/ts/daemon/processwrapper.ts +++ b/ts/daemon/processwrapper.ts @@ -21,6 +21,8 @@ export class ProcessWrapper extends EventEmitter { private logger: Logger; private nextSeq: number = 0; private runId: string = ''; + private stdoutRemainder: string = ''; + private stderrRemainder: string = ''; constructor(options: IProcessWrapperOptions) { super(); @@ -66,6 +68,11 @@ export class ProcessWrapper extends EventEmitter { const exitMessage = `Process exited with code ${code}, signal ${signal}`; this.logger.info(exitMessage); this.addSystemLog(exitMessage); + + // Clear remainder buffers on exit + this.stdoutRemainder = ''; + this.stderrRemainder = ''; + this.emit('exit', code, signal); }); @@ -83,24 +90,57 @@ export class ProcessWrapper extends EventEmitter { // Capture stdout if (this.process.stdout) { + console.error(`[ProcessWrapper] Setting up stdout listener for process ${this.process.pid}`); this.process.stdout.on('data', (data) => { - const lines = data.toString().split('\n'); + console.error(`[ProcessWrapper] Received stdout data from PID ${this.process?.pid}: ${data.toString().substring(0, 100)}`); + // Add data to remainder buffer and split by newlines + const text = this.stdoutRemainder + data.toString(); + const lines = text.split('\n'); + + // The last element might be a partial line + this.stdoutRemainder = lines.pop() || ''; + + // Process complete lines for (const line of lines) { - if (line.trim()) { - this.addLog('stdout', line); - } + console.error(`[ProcessWrapper] Processing stdout line: ${line}`); + this.logger.debug(`Captured stdout: ${line}`); + this.addLog('stdout', line); } }); + + // Flush remainder on stream end + this.process.stdout.on('end', () => { + if (this.stdoutRemainder) { + this.logger.debug(`Flushing stdout remainder: ${this.stdoutRemainder}`); + this.addLog('stdout', this.stdoutRemainder); + this.stdoutRemainder = ''; + } + }); + } else { + this.logger.warn('Process stdout is null'); } // Capture stderr if (this.process.stderr) { this.process.stderr.on('data', (data) => { - const lines = data.toString().split('\n'); + // Add data to remainder buffer and split by newlines + const text = this.stderrRemainder + data.toString(); + const lines = text.split('\n'); + + // The last element might be a partial line + this.stderrRemainder = lines.pop() || ''; + + // Process complete lines for (const line of lines) { - if (line.trim()) { - this.addLog('stderr', line); - } + this.addLog('stderr', line); + } + }); + + // Flush remainder on stream end + this.process.stderr.on('end', () => { + if (this.stderrRemainder) { + this.addLog('stderr', this.stderrRemainder); + this.stderrRemainder = ''; } }); } diff --git a/ts/daemon/tspm.daemon.ts b/ts/daemon/tspm.daemon.ts index dfa48ed..306fd81 100644 --- a/ts/daemon/tspm.daemon.ts +++ b/ts/daemon/tspm.daemon.ts @@ -1,5 +1,7 @@ import * as plugins from '../plugins.js'; import * as paths from '../paths.js'; +import { toProcessId } from '../shared/protocol/id.js'; +import type { ProcessId } from '../shared/protocol/id.js'; import { ProcessManager } from './processmanager.js'; import type { IpcMethodMap, @@ -141,7 +143,7 @@ export class TspmDaemon { 'startById', async (request: RequestForMethod<'startById'>) => { try { - const id = String(request.id).trim(); + const id = toProcessId(request.id); let config = this.tspmInstance.processConfigs.get(id); if (!config) { // Try to reload configs if not found (handles races or stale state) @@ -169,7 +171,7 @@ export class TspmDaemon { 'stop', async (request: RequestForMethod<'stop'>) => { try { - const id = String(request.id).trim(); + const id = toProcessId(request.id); await this.tspmInstance.setDesiredState(id, 'stopped'); await this.tspmInstance.stop(id); return { @@ -186,7 +188,7 @@ export class TspmDaemon { 'restart', async (request: RequestForMethod<'restart'>) => { try { - const id = String(request.id).trim(); + const id = toProcessId(request.id); await this.tspmInstance.setDesiredState(id, 'online'); await this.tspmInstance.restart(id); const processInfo = this.tspmInstance.processInfo.get(id); @@ -205,7 +207,7 @@ export class TspmDaemon { 'delete', async (request: RequestForMethod<'delete'>) => { try { - const id = String(request.id).trim(); + const id = toProcessId(request.id); await this.tspmInstance.delete(id); return { success: true, @@ -235,7 +237,7 @@ export class TspmDaemon { 'remove', async (request: RequestForMethod<'remove'>) => { try { - const id = String(request.id).trim(); + const id = toProcessId(request.id); await this.tspmInstance.delete(id); return { success: true, message: `Process ${id} deleted successfully` }; } catch (error) { @@ -255,7 +257,7 @@ export class TspmDaemon { this.ipcServer.onMessage( 'describe', async (request: RequestForMethod<'describe'>) => { - const id = String(request.id).trim(); + const id = toProcessId(request.id); const result = await this.tspmInstance.describe(id); if (!result) { throw new Error(`Process ${id} not found`); @@ -271,7 +273,7 @@ export class TspmDaemon { this.ipcServer.onMessage( 'getLogs', async (request: RequestForMethod<'getLogs'>) => { - const logs = await this.tspmInstance.getLogs(request.id); + const logs = await this.tspmInstance.getLogs(toProcessId(request.id)); return { logs }; }, ); @@ -280,8 +282,8 @@ export class TspmDaemon { this.ipcServer.onMessage( 'startAll', async (request: RequestForMethod<'startAll'>) => { - const started: string[] = []; - const failed: Array<{ id: string; error: string }> = []; + const started: ProcessId[] = []; + const failed: Array<{ id: ProcessId; error: string }> = []; await this.tspmInstance.setDesiredStateForAll('online'); await this.tspmInstance.startAll(); @@ -302,8 +304,8 @@ export class TspmDaemon { this.ipcServer.onMessage( 'stopAll', async (request: RequestForMethod<'stopAll'>) => { - const stopped: string[] = []; - const failed: Array<{ id: string; error: string }> = []; + const stopped: ProcessId[] = []; + const failed: Array<{ id: ProcessId; error: string }> = []; await this.tspmInstance.setDesiredStateForAll('stopped'); await this.tspmInstance.stopAll(); @@ -324,8 +326,8 @@ export class TspmDaemon { this.ipcServer.onMessage( 'restartAll', async (request: RequestForMethod<'restartAll'>) => { - const restarted: string[] = []; - const failed: Array<{ id: string; error: string }> = []; + const restarted: ProcessId[] = []; + const failed: Array<{ id: ProcessId; error: string }> = []; await this.tspmInstance.restartAll(); @@ -556,3 +558,11 @@ export const startDaemon = async (): Promise => { // Keep the process alive await new Promise(() => {}); }; + +// If this file is run directly (not imported), start the daemon +if (process.env.TSPM_DAEMON_MODE === 'true') { + startDaemon().catch((error) => { + console.error('Failed to start TSPM daemon:', error); + process.exit(1); + }); +} diff --git a/ts/plugins.ts b/ts/plugins.ts index 6db9e38..bbcdefc 100644 --- a/ts/plugins.ts +++ b/ts/plugins.ts @@ -10,12 +10,13 @@ import * as npmextra from '@push.rocks/npmextra'; import * as projectinfo from '@push.rocks/projectinfo'; import * as smartcli from '@push.rocks/smartcli'; import * as smartdaemon from '@push.rocks/smartdaemon'; +import * as smartfile from '@push.rocks/smartfile'; import * as smartipc from '@push.rocks/smartipc'; import * as smartpath from '@push.rocks/smartpath'; import * as smartinteract from '@push.rocks/smartinteract'; // Export with explicit module types -export { npmextra, projectinfo, smartcli, smartdaemon, smartipc, smartpath, smartinteract }; +export { npmextra, projectinfo, smartcli, smartdaemon, smartfile, smartipc, smartpath, smartinteract }; // third-party scope import psTree from 'ps-tree'; diff --git a/ts/shared/protocol/id.ts b/ts/shared/protocol/id.ts new file mode 100644 index 0000000..a1c2924 --- /dev/null +++ b/ts/shared/protocol/id.ts @@ -0,0 +1,56 @@ +/** + * Branded type for process IDs to ensure type safety + */ +export type ProcessId = number & { readonly __brand: 'ProcessId' }; + +/** + * Input type that accepts various ID formats for backward compatibility + */ +export type ProcessIdInput = ProcessId | number | string; + +/** + * Normalizes various ID input formats to a ProcessId + * @param input - The ID in various formats (string, number, or ProcessId) + * @returns A normalized ProcessId + * @throws Error if the input is not a valid process ID + */ +export function toProcessId(input: ProcessIdInput): ProcessId { + let num: number; + + if (typeof input === 'string') { + const trimmed = input.trim(); + if (!/^\d+$/.test(trimmed)) { + throw new Error(`Invalid process ID: "${input}" is not a numeric string`); + } + num = parseInt(trimmed, 10); + } else if (typeof input === 'number') { + num = input; + } else { + // Already a ProcessId + return input; + } + + if (!Number.isInteger(num) || num < 1) { + throw new Error(`Invalid process ID: ${input} must be a positive integer`); + } + + return num as ProcessId; +} + +/** + * Type guard to check if a value is a ProcessId + */ +export function isProcessId(value: unknown): value is ProcessId { + return typeof value === 'number' && Number.isInteger(value) && value >= 1; +} + +/** + * Gets the next sequential ID given existing IDs + */ +export function getNextProcessId(existingIds: Iterable): ProcessId { + let maxId = 0; + for (const id of existingIds) { + maxId = Math.max(maxId, id); + } + return (maxId + 1) as ProcessId; +} \ No newline at end of file diff --git a/ts/shared/protocol/ipc.types.ts b/ts/shared/protocol/ipc.types.ts index 82af1ff..ae42158 100644 --- a/ts/shared/protocol/ipc.types.ts +++ b/ts/shared/protocol/ipc.types.ts @@ -1,3 +1,5 @@ +import type { ProcessId } from './id.js'; + // Process-related interfaces (used in IPC communication) export interface IMonitorConfig { name?: string; // Optional name to identify the instance @@ -11,14 +13,14 @@ export interface IMonitorConfig { } export interface IProcessConfig extends IMonitorConfig { - id: string; // Unique identifier for the process + id: ProcessId; // 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; + id: ProcessId; pid?: number; status: 'online' | 'stopped' | 'errored'; memory: number; @@ -61,25 +63,25 @@ export interface StartRequest { } export interface StartResponse { - processId: string; + processId: ProcessId; pid?: number; status: 'online' | 'stopped' | 'errored'; } // Start by id (server resolves config) export interface StartByIdRequest { - id: string; + id: ProcessId; } export interface StartByIdResponse { - processId: string; + processId: ProcessId; pid?: number; status: 'online' | 'stopped' | 'errored'; } // Stop command export interface StopRequest { - id: string; + id: ProcessId; } export interface StopResponse { @@ -89,18 +91,18 @@ export interface StopResponse { // Restart command export interface RestartRequest { - id: string; + id: ProcessId; } export interface RestartResponse { - processId: string; + processId: ProcessId; pid?: number; status: 'online' | 'stopped' | 'errored'; } // Delete command export interface DeleteRequest { - id: string; + id: ProcessId; } export interface DeleteResponse { @@ -119,7 +121,7 @@ export interface ListResponse { // Describe command export interface DescribeRequest { - id: string; + id: ProcessId; } export interface DescribeResponse { @@ -129,7 +131,7 @@ export interface DescribeResponse { // Get logs command export interface GetLogsRequest { - id: string; + id: ProcessId; lines?: number; } @@ -143,9 +145,9 @@ export interface StartAllRequest { } export interface StartAllResponse { - started: string[]; + started: ProcessId[]; failed: Array<{ - id: string; + id: ProcessId; error: string; }>; } @@ -156,9 +158,9 @@ export interface StopAllRequest { } export interface StopAllResponse { - stopped: string[]; + stopped: ProcessId[]; failed: Array<{ - id: string; + id: ProcessId; error: string; }>; } @@ -169,9 +171,9 @@ export interface RestartAllRequest { } export interface RestartAllResponse { - restarted: string[]; + restarted: ProcessId[]; failed: Array<{ - id: string; + id: ProcessId; error: string; }>; } @@ -182,10 +184,10 @@ export interface ResetRequest { } export interface ResetResponse { - stopped: string[]; - removed: string[]; + stopped: ProcessId[]; + removed: ProcessId[]; failed: Array<{ - id: string; + id: ProcessId; error: string; }>; } @@ -229,17 +231,17 @@ export interface HeartbeatResponse { // Add (register config without starting) export interface AddRequest { // Optional id is ignored server-side if present; server assigns sequential id - config: Omit & { id?: string }; + config: Omit & { id?: ProcessId }; } export interface AddResponse { - id: string; + id: ProcessId; config: IProcessConfig; } // Remove (delete config and stop if running) export interface RemoveRequest { - id: string; + id: ProcessId; } export interface RemoveResponse {