BREAKING CHANGE(daemon): Introduce persistent log storage, numeric ProcessId type, and improved process monitoring / IPC handling
This commit is contained in:
		
							
								
								
									
										13
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								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
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										57
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										57
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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)} │`,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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)} │`,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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}`);
 | 
			
		||||
 
 | 
			
		||||
@@ -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<void> {
 | 
			
		||||
    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<void> {
 | 
			
		||||
  public async unsubscribe(processId: ProcessId | number | string): Promise<void> {
 | 
			
		||||
    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}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										117
									
								
								ts/daemon/logpersistence.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								ts/daemon/logpersistence.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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<void> {
 | 
			
		||||
    await plugins.smartfile.fs.ensureDir(this.logsDir);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Save logs to disk
 | 
			
		||||
   */
 | 
			
		||||
  public async saveLogs(processId: ProcessId, logs: IProcessLog[]): Promise<void> {
 | 
			
		||||
    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<IProcessLog[]> {
 | 
			
		||||
    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<void> {
 | 
			
		||||
    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<void> {
 | 
			
		||||
    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);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<string, ProcessMonitor> = new Map();
 | 
			
		||||
  public processConfigs: Map<string, IProcessConfig> = new Map();
 | 
			
		||||
  public processInfo: Map<string, IProcessInfo> = new Map();
 | 
			
		||||
  public processes: Map<ProcessId, ProcessMonitor> = new Map();
 | 
			
		||||
  public processConfigs: Map<ProcessId, IProcessConfig> = new Map();
 | 
			
		||||
  public processInfo: Map<ProcessId, IProcessInfo> = new Map();
 | 
			
		||||
  private processLogs: Map<ProcessId, IProcessLog[]> = new Map();
 | 
			
		||||
  private config: TspmConfig;
 | 
			
		||||
  private configStorageKey = 'processes';
 | 
			
		||||
  private desiredStateStorageKey = 'desiredStates';
 | 
			
		||||
  private desiredStates: Map<string, IProcessInfo['status']> = new Map();
 | 
			
		||||
  private desiredStates: Map<ProcessId, IProcessInfo['status']> = 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<IProcessConfig, 'id'> & { id?: string }): Promise<string> {
 | 
			
		||||
  public async add(configInput: Omit<IProcessConfig, 'id'> & { id?: ProcessId }): Promise<ProcessId> {
 | 
			
		||||
    // 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<void> {
 | 
			
		||||
  public async stop(id: ProcessId): Promise<void> {
 | 
			
		||||
    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<void> {
 | 
			
		||||
  public async restart(id: ProcessId): Promise<void> {
 | 
			
		||||
    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<void> {
 | 
			
		||||
  public async delete(id: ProcessId): Promise<void> {
 | 
			
		||||
    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<IProcessInfo>): void {
 | 
			
		||||
  private updateProcessInfo(id: ProcessId, update: Partial<IProcessInfo>): 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<IProcessInfo> = {};
 | 
			
		||||
        
 | 
			
		||||
        // 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<string, IProcessInfo['status']> = {};
 | 
			
		||||
      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<string, IProcessInfo['status']>;
 | 
			
		||||
        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<void> {
 | 
			
		||||
    this.desiredStates.set(id, state);
 | 
			
		||||
    await this.saveDesiredStates();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async removeDesiredState(id: string): Promise<void> {
 | 
			
		||||
  public async removeDesiredState(id: ProcessId): Promise<void> {
 | 
			
		||||
    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<any>;
 | 
			
		||||
          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())) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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<void> {
 | 
			
		||||
    // 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<void> => {
 | 
			
		||||
        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<void> => {
 | 
			
		||||
      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<void> {
 | 
			
		||||
    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;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = '';
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -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<void> => {
 | 
			
		||||
  // 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);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										56
									
								
								ts/shared/protocol/id.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								ts/shared/protocol/id.ts
									
									
									
									
									
										Normal file
									
								
							@@ -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>): ProcessId {
 | 
			
		||||
  let maxId = 0;
 | 
			
		||||
  for (const id of existingIds) {
 | 
			
		||||
    maxId = Math.max(maxId, id);
 | 
			
		||||
  }
 | 
			
		||||
  return (maxId + 1) as ProcessId;
 | 
			
		||||
}
 | 
			
		||||
@@ -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<IProcessConfig, 'id'> & { id?: string };
 | 
			
		||||
  config: Omit<IProcessConfig, 'id'> & { 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 {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user