fix(core): Improve error handling, logging, and test suite; update dependency versions
This commit is contained in:
		@@ -1,5 +1,12 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## 2025-03-10 - 1.5.1 - fix(core)
 | 
			
		||||
Improve error handling, logging, and test suite; update dependency versions
 | 
			
		||||
 | 
			
		||||
- Updated devDependencies versions in package.json (@git.zone/tsbuild, @push.rocks/tapbundle, and @push.rocks/smartdaemon)
 | 
			
		||||
- Refactored error handling and enhanced logging in ProcessMonitor and ProcessWrapper modules
 | 
			
		||||
- Improved test structure by adding clear module import tests and usage examples in test files
 | 
			
		||||
 | 
			
		||||
## 2025-03-04 - 1.5.0 - feat(cli)
 | 
			
		||||
Enhance CLI with new process management commands
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,18 +18,18 @@
 | 
			
		||||
    "tspm": "./cli.js"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@git.zone/tsbuild": "^2.1.25",
 | 
			
		||||
    "@git.zone/tsbuild": "^2.2.6",
 | 
			
		||||
    "@git.zone/tsbundle": "^2.0.5",
 | 
			
		||||
    "@git.zone/tsrun": "^1.2.46",
 | 
			
		||||
    "@git.zone/tstest": "^1.0.44",
 | 
			
		||||
    "@push.rocks/tapbundle": "^5.0.15",
 | 
			
		||||
    "@types/node": "^22.13.8"
 | 
			
		||||
    "@push.rocks/tapbundle": "^5.5.9",
 | 
			
		||||
    "@types/node": "^22.13.10"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@push.rocks/npmextra": "^5.1.2",
 | 
			
		||||
    "@push.rocks/projectinfo": "^5.0.2",
 | 
			
		||||
    "@push.rocks/smartcli": "^4.0.11",
 | 
			
		||||
    "@push.rocks/smartdaemon": "^2.0.6",
 | 
			
		||||
    "@push.rocks/smartdaemon": "^2.0.8",
 | 
			
		||||
    "@push.rocks/smartpath": "^5.0.18",
 | 
			
		||||
    "pidusage": "^4.0.0",
 | 
			
		||||
    "ps-tree": "^1.2.0"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										989
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										989
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										133
									
								
								test/test.ts
									
									
									
									
									
								
							
							
						
						
									
										133
									
								
								test/test.ts
									
									
									
									
									
								
							@@ -1,27 +1,124 @@
 | 
			
		||||
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
 | 
			
		||||
import * as tspm from '../ts/index.js';
 | 
			
		||||
import { join } from 'path';
 | 
			
		||||
 | 
			
		||||
tap.test('first test', async () => {
 | 
			
		||||
  console.log(tspm);
 | 
			
		||||
// Basic module import test
 | 
			
		||||
tap.test('module import test', async () => {
 | 
			
		||||
  console.log('Imported modules:', Object.keys(tspm));
 | 
			
		||||
  expect(tspm.ProcessMonitor).toBeTypeOf('function');
 | 
			
		||||
  expect(tspm.Tspm).toBeTypeOf('function');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// ProcessMonitor test
 | 
			
		||||
tap.test('ProcessMonitor test', async () => {
 | 
			
		||||
  const config: tspm.IMonitorConfig = {
 | 
			
		||||
    name: 'Test Monitor',
 | 
			
		||||
    projectDir: process.cwd(),
 | 
			
		||||
    command: 'echo "Test process running"',
 | 
			
		||||
    memoryLimitBytes: 50 * 1024 * 1024, // 50MB
 | 
			
		||||
    monitorIntervalMs: 1000,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const monitor = new tspm.ProcessMonitor(config);
 | 
			
		||||
  
 | 
			
		||||
  // Test monitor creation
 | 
			
		||||
  expect(monitor).toBeInstanceOf(tspm.ProcessMonitor);
 | 
			
		||||
  
 | 
			
		||||
  // We won't actually start it in tests to avoid side effects
 | 
			
		||||
  // but we can test the API
 | 
			
		||||
  expect(monitor.start).toBeInstanceOf('function');
 | 
			
		||||
  expect(monitor.stop).toBeInstanceOf('function');
 | 
			
		||||
  expect(monitor.getLogs).toBeInstanceOf('function');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// Tspm class test
 | 
			
		||||
tap.test('Tspm class test', async () => {
 | 
			
		||||
  const tspmInstance = new tspm.Tspm();
 | 
			
		||||
  
 | 
			
		||||
  expect(tspmInstance).toBeInstanceOf(tspm.Tspm);
 | 
			
		||||
  expect(tspmInstance.start).toBeInstanceOf('function');
 | 
			
		||||
  expect(tspmInstance.stop).toBeInstanceOf('function');
 | 
			
		||||
  expect(tspmInstance.restart).toBeInstanceOf('function');
 | 
			
		||||
  expect(tspmInstance.list).toBeInstanceOf('function');
 | 
			
		||||
  expect(tspmInstance.describe).toBeInstanceOf('function');
 | 
			
		||||
  expect(tspmInstance.getLogs).toBeInstanceOf('function');
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
tap.start();
 | 
			
		||||
 | 
			
		||||
// Example usage:
 | 
			
		||||
const config: tspm.IMonitorConfig = {
 | 
			
		||||
  name: 'Project XYZ Monitor',        // Identifier for the instance
 | 
			
		||||
  projectDir: '/path/to/your/project',  // Set the project directory here
 | 
			
		||||
  command: 'npm run xyz',               // Full command string (no need for args)
 | 
			
		||||
  memoryLimitBytes: 500 * 1024 * 1024,    // 500 MB memory limit
 | 
			
		||||
  monitorIntervalMs: 5000,              // Check memory usage every 5 seconds
 | 
			
		||||
};
 | 
			
		||||
// ====================================================
 | 
			
		||||
// Example usage (this part is not executed in tests)
 | 
			
		||||
// ====================================================
 | 
			
		||||
 | 
			
		||||
const monitor = new tspm.ProcessMonitor(config);
 | 
			
		||||
monitor.start();
 | 
			
		||||
// Example 1: Using ProcessMonitor directly
 | 
			
		||||
function exampleUsingProcessMonitor() {
 | 
			
		||||
  const config: tspm.IMonitorConfig = {
 | 
			
		||||
    name: 'Project XYZ Monitor',
 | 
			
		||||
    projectDir: '/path/to/your/project',
 | 
			
		||||
    command: 'npm run xyz',
 | 
			
		||||
    memoryLimitBytes: 500 * 1024 * 1024, // 500 MB memory limit
 | 
			
		||||
    monitorIntervalMs: 5000, // Check memory usage every 5 seconds
 | 
			
		||||
    logBufferSize: 200, // Keep last 200 log lines
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
// Ensure that on process exit (e.g. Ctrl+C) we clean up the child process and prevent respawns.
 | 
			
		||||
process.on('SIGINT', () => {
 | 
			
		||||
  console.log('Received SIGINT, stopping monitor...');
 | 
			
		||||
  monitor.stop();
 | 
			
		||||
  process.exit();
 | 
			
		||||
});
 | 
			
		||||
  const monitor = new tspm.ProcessMonitor(config);
 | 
			
		||||
  monitor.start();
 | 
			
		||||
 | 
			
		||||
  // Ensure that on process exit (e.g. Ctrl+C) we clean up the child process and prevent respawns.
 | 
			
		||||
  process.on('SIGINT', () => {
 | 
			
		||||
    console.log('Received SIGINT, stopping monitor...');
 | 
			
		||||
    monitor.stop();
 | 
			
		||||
    process.exit();
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  // Get logs example
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    const logs = monitor.getLogs(10); // Get last 10 log lines
 | 
			
		||||
    console.log('Latest logs:', logs);
 | 
			
		||||
  }, 10000);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Example 2: Using Tspm (higher-level process manager)
 | 
			
		||||
async function exampleUsingTspm() {
 | 
			
		||||
  const tspmInstance = new tspm.Tspm();
 | 
			
		||||
  
 | 
			
		||||
  // Start a process
 | 
			
		||||
  await tspmInstance.start({
 | 
			
		||||
    id: 'web-server',
 | 
			
		||||
    name: 'Web Server',
 | 
			
		||||
    projectDir: '/path/to/web/project',
 | 
			
		||||
    command: 'npm run serve',
 | 
			
		||||
    memoryLimitBytes: 300 * 1024 * 1024, // 300 MB
 | 
			
		||||
    autorestart: true,
 | 
			
		||||
    watch: true,
 | 
			
		||||
    monitorIntervalMs: 10000,
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  // Start another process
 | 
			
		||||
  await tspmInstance.start({
 | 
			
		||||
    id: 'api-server',
 | 
			
		||||
    name: 'API Server',
 | 
			
		||||
    projectDir: '/path/to/api/project',
 | 
			
		||||
    command: 'npm run api',
 | 
			
		||||
    memoryLimitBytes: 400 * 1024 * 1024, // 400 MB
 | 
			
		||||
    autorestart: true,
 | 
			
		||||
  });
 | 
			
		||||
  
 | 
			
		||||
  // List all processes
 | 
			
		||||
  const processes = tspmInstance.list();
 | 
			
		||||
  console.log('Running processes:', processes);
 | 
			
		||||
  
 | 
			
		||||
  // Get logs from a process
 | 
			
		||||
  const logs = tspmInstance.getLogs('web-server', 20);
 | 
			
		||||
  console.log('Web server logs:', logs);
 | 
			
		||||
  
 | 
			
		||||
  // Stop a process
 | 
			
		||||
  await tspmInstance.stop('api-server');
 | 
			
		||||
  
 | 
			
		||||
  // Handle graceful shutdown
 | 
			
		||||
  process.on('SIGINT', async () => {
 | 
			
		||||
    console.log('Shutting down all processes...');
 | 
			
		||||
    await tspmInstance.stopAll();
 | 
			
		||||
    process.exit();
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
@@ -3,6 +3,6 @@
 | 
			
		||||
 */
 | 
			
		||||
export const commitinfo = {
 | 
			
		||||
  name: '@git.zone/tspm',
 | 
			
		||||
  version: '1.5.0',
 | 
			
		||||
  version: '1.5.1',
 | 
			
		||||
  description: 'a no fuzz process manager'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import * as plugins from './plugins.js';
 | 
			
		||||
import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js';
 | 
			
		||||
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
 | 
			
		||||
 | 
			
		||||
export interface IMonitorConfig {
 | 
			
		||||
  name?: string;                 // Optional name to identify the instance
 | 
			
		||||
@@ -18,9 +19,11 @@ export class ProcessMonitor {
 | 
			
		||||
  private intervalId: NodeJS.Timeout | null = null;
 | 
			
		||||
  private stopped: boolean = true; // Initially stopped until start() is called
 | 
			
		||||
  private restartCount: number = 0;
 | 
			
		||||
  private logger: Logger;
 | 
			
		||||
 | 
			
		||||
  constructor(config: IMonitorConfig) {
 | 
			
		||||
    this.config = config;
 | 
			
		||||
    this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public start(): void {
 | 
			
		||||
@@ -31,7 +34,7 @@ export class ProcessMonitor {
 | 
			
		||||
 | 
			
		||||
    // Set the monitoring interval.
 | 
			
		||||
    const interval = this.config.monitorIntervalMs || 5000;
 | 
			
		||||
    this.intervalId = setInterval(() => {
 | 
			
		||||
    this.intervalId = setInterval((): void => {
 | 
			
		||||
      if (this.processWrapper && this.processWrapper.getPid()) {
 | 
			
		||||
        this.monitorProcessGroup(this.processWrapper.getPid()!, this.config.memoryLimitBytes);
 | 
			
		||||
      }
 | 
			
		||||
@@ -40,7 +43,12 @@ export class ProcessMonitor {
 | 
			
		||||
 | 
			
		||||
  private spawnProcess(): void {
 | 
			
		||||
    // Don't spawn if the monitor has been stopped.
 | 
			
		||||
    if (this.stopped) return;
 | 
			
		||||
    if (this.stopped) {
 | 
			
		||||
      this.logger.debug('Not spawning process because monitor is stopped');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.logger.info(`Spawning process: ${this.config.command}`);
 | 
			
		||||
 | 
			
		||||
    // Create a new process wrapper
 | 
			
		||||
    this.processWrapper = new ProcessWrapper({
 | 
			
		||||
@@ -53,7 +61,7 @@ export class ProcessMonitor {
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Set up event handlers
 | 
			
		||||
    this.processWrapper.on('log', (log) => {
 | 
			
		||||
    this.processWrapper.on('log', (log: IProcessLog): void => {
 | 
			
		||||
      // Here we could add handlers to send logs somewhere
 | 
			
		||||
      // For now, we just log system messages to the console
 | 
			
		||||
      if (log.type === 'system') {
 | 
			
		||||
@@ -61,26 +69,47 @@ export class ProcessMonitor {
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.processWrapper.on('exit', (code, signal) => {
 | 
			
		||||
      this.log(`Process exited with code ${code}, signal ${signal}.`);
 | 
			
		||||
    this.processWrapper.on('exit', (code: number | null, signal: string | null): void => {
 | 
			
		||||
      const exitMsg = `Process exited with code ${code}, signal ${signal}.`;
 | 
			
		||||
      this.logger.info(exitMsg);
 | 
			
		||||
      this.log(exitMsg);
 | 
			
		||||
      
 | 
			
		||||
      if (!this.stopped) {
 | 
			
		||||
        this.logger.info('Restarting process...');
 | 
			
		||||
        this.log('Restarting process...');
 | 
			
		||||
        this.restartCount++;
 | 
			
		||||
        this.spawnProcess();
 | 
			
		||||
      } else {
 | 
			
		||||
        this.logger.debug('Not restarting process because monitor is stopped');
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.processWrapper.on('error', (error) => {
 | 
			
		||||
      this.log(`Process error: ${error.message}`);
 | 
			
		||||
    this.processWrapper.on('error', (error: Error | ProcessError): void => {
 | 
			
		||||
      const errorMsg = error instanceof ProcessError 
 | 
			
		||||
        ? `Process error: ${error.toString()}`
 | 
			
		||||
        : `Process error: ${error.message}`;
 | 
			
		||||
      
 | 
			
		||||
      this.logger.error(error);
 | 
			
		||||
      this.log(errorMsg);
 | 
			
		||||
      
 | 
			
		||||
      if (!this.stopped) {
 | 
			
		||||
        this.logger.info('Restarting process due to error...');
 | 
			
		||||
        this.log('Restarting process due to error...');
 | 
			
		||||
        this.restartCount++;
 | 
			
		||||
        this.spawnProcess();
 | 
			
		||||
      } else {
 | 
			
		||||
        this.logger.debug('Not restarting process because monitor is stopped');
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // Start the process
 | 
			
		||||
    this.processWrapper.start();
 | 
			
		||||
    try {
 | 
			
		||||
      this.processWrapper.start();
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
      // The process wrapper will handle logging the error
 | 
			
		||||
      // Just prevent it from bubbling up further
 | 
			
		||||
      this.logger.error(`Failed to start process: ${error instanceof Error ? error.message : String(error)}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
@@ -90,24 +119,40 @@ export class ProcessMonitor {
 | 
			
		||||
  private async monitorProcessGroup(pid: number, memoryLimit: number): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const memoryUsage = await this.getProcessGroupMemory(pid);
 | 
			
		||||
      
 | 
			
		||||
      this.logger.debug(
 | 
			
		||||
        `Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`
 | 
			
		||||
      );
 | 
			
		||||
      
 | 
			
		||||
      // Only log to the process log at longer intervals to avoid spamming
 | 
			
		||||
      this.log(
 | 
			
		||||
        `Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
 | 
			
		||||
          memoryUsage
 | 
			
		||||
        )} (${memoryUsage} bytes)`
 | 
			
		||||
      );
 | 
			
		||||
      
 | 
			
		||||
      if (memoryUsage > memoryLimit) {
 | 
			
		||||
        this.log(
 | 
			
		||||
          `Memory usage ${this.humanReadableBytes(
 | 
			
		||||
            memoryUsage
 | 
			
		||||
          )} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.`
 | 
			
		||||
        );
 | 
			
		||||
        const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
 | 
			
		||||
          memoryUsage
 | 
			
		||||
        )} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.`;
 | 
			
		||||
        
 | 
			
		||||
        this.logger.warn(memoryLimitMsg);
 | 
			
		||||
        this.log(memoryLimitMsg);
 | 
			
		||||
        
 | 
			
		||||
        // Stop the process wrapper, which will trigger the exit handler and restart
 | 
			
		||||
        if (this.processWrapper) {
 | 
			
		||||
          this.processWrapper.stop();
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.log('Error monitoring process group: ' + error);
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
      const processError = new ProcessError(
 | 
			
		||||
        error instanceof Error ? error.message : String(error),
 | 
			
		||||
        'ERR_MEMORY_MONITORING_FAILED',
 | 
			
		||||
        { pid }
 | 
			
		||||
      );
 | 
			
		||||
      
 | 
			
		||||
      this.logger.error(processError);
 | 
			
		||||
      this.log(`Error monitoring process group: ${processError.toString()}`);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -116,16 +161,40 @@ export class ProcessMonitor {
 | 
			
		||||
   */
 | 
			
		||||
  private getProcessGroupMemory(pid: number): Promise<number> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      plugins.psTree(pid, (err, children) => {
 | 
			
		||||
        if (err) return reject(err);
 | 
			
		||||
      this.logger.debug(`Getting memory usage for process group with PID ${pid}`);
 | 
			
		||||
      
 | 
			
		||||
      plugins.psTree(pid, (err: Error | null, children: Array<{ PID: string }>) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          const processError = new ProcessError(
 | 
			
		||||
            `Failed to get process tree: ${err.message}`, 
 | 
			
		||||
            'ERR_PSTREE_FAILED',
 | 
			
		||||
            { pid }
 | 
			
		||||
          );
 | 
			
		||||
          this.logger.debug(`psTree error: ${err.message}`);
 | 
			
		||||
          return reject(processError);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // Include the main process and its children.
 | 
			
		||||
        const pids: number[] = [pid, ...children.map(child => Number(child.PID))];
 | 
			
		||||
        plugins.pidusage(pids, (err, stats) => {
 | 
			
		||||
          if (err) return reject(err);
 | 
			
		||||
        this.logger.debug(`Found ${pids.length} processes in group with parent PID ${pid}`);
 | 
			
		||||
        
 | 
			
		||||
        plugins.pidusage(pids, (err: Error | null, stats: Record<string, { memory: number }>) => {
 | 
			
		||||
          if (err) {
 | 
			
		||||
            const processError = new ProcessError(
 | 
			
		||||
              `Failed to get process usage stats: ${err.message}`, 
 | 
			
		||||
              'ERR_PIDUSAGE_FAILED',
 | 
			
		||||
              { pids }
 | 
			
		||||
            );
 | 
			
		||||
            this.logger.debug(`pidusage error: ${err.message}`);
 | 
			
		||||
            return reject(processError);
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          let totalMemory = 0;
 | 
			
		||||
          for (const key in stats) {
 | 
			
		||||
            totalMemory += stats[key].memory;
 | 
			
		||||
          }
 | 
			
		||||
          
 | 
			
		||||
          this.logger.debug(`Total memory for process group: ${this.humanReadableBytes(totalMemory)}`);
 | 
			
		||||
          resolve(totalMemory);
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import * as plugins from './plugins.js';
 | 
			
		||||
import { EventEmitter } from 'events';
 | 
			
		||||
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
 | 
			
		||||
 | 
			
		||||
export interface IProcessWrapperOptions {
 | 
			
		||||
  command: string;
 | 
			
		||||
@@ -22,11 +23,13 @@ export class ProcessWrapper extends EventEmitter {
 | 
			
		||||
  private logs: IProcessLog[] = [];
 | 
			
		||||
  private logBufferSize: number;
 | 
			
		||||
  private startTime: Date | null = null;
 | 
			
		||||
  private logger: Logger;
 | 
			
		||||
  
 | 
			
		||||
  constructor(options: IProcessWrapperOptions) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.options = options;
 | 
			
		||||
    this.logBufferSize = options.logBuffer || 100;
 | 
			
		||||
    this.logger = new Logger(`ProcessWrapper:${options.name}`);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
@@ -36,6 +39,8 @@ export class ProcessWrapper extends EventEmitter {
 | 
			
		||||
    this.addSystemLog('Starting process...');
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      this.logger.debug(`Starting process: ${this.options.command}`);
 | 
			
		||||
      
 | 
			
		||||
      if (this.options.args && this.options.args.length > 0) {
 | 
			
		||||
        this.process = plugins.childProcess.spawn(this.options.command, this.options.args, {
 | 
			
		||||
          cwd: this.options.cwd,
 | 
			
		||||
@@ -56,14 +61,22 @@ export class ProcessWrapper extends EventEmitter {
 | 
			
		||||
      
 | 
			
		||||
      // Handle process exit
 | 
			
		||||
      this.process.on('exit', (code, signal) => {
 | 
			
		||||
        this.addSystemLog(`Process exited with code ${code}, signal ${signal}`);
 | 
			
		||||
        const exitMessage = `Process exited with code ${code}, signal ${signal}`;
 | 
			
		||||
        this.logger.info(exitMessage);
 | 
			
		||||
        this.addSystemLog(exitMessage);
 | 
			
		||||
        this.emit('exit', code, signal);
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      // Handle errors
 | 
			
		||||
      this.process.on('error', (error) => {
 | 
			
		||||
        this.addSystemLog(`Process error: ${error.message}`);
 | 
			
		||||
        this.emit('error', error);
 | 
			
		||||
        const processError = new ProcessError(
 | 
			
		||||
          error.message,
 | 
			
		||||
          'ERR_PROCESS_EXECUTION',
 | 
			
		||||
          { command: this.options.command, pid: this.process?.pid }
 | 
			
		||||
        );
 | 
			
		||||
        this.logger.error(processError);
 | 
			
		||||
        this.addSystemLog(`Process error: ${processError.toString()}`);
 | 
			
		||||
        this.emit('error', processError);
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      // Capture stdout
 | 
			
		||||
@@ -91,12 +104,22 @@ export class ProcessWrapper extends EventEmitter {
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      this.addSystemLog(`Process started with PID ${this.process.pid}`);
 | 
			
		||||
      this.logger.info(`Process started with PID ${this.process.pid}`);
 | 
			
		||||
      this.emit('start', this.process.pid);
 | 
			
		||||
      
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      this.addSystemLog(`Failed to start process: ${error.message}`);
 | 
			
		||||
      this.emit('error', error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
      const processError = error instanceof ProcessError 
 | 
			
		||||
        ? error 
 | 
			
		||||
        : new ProcessError(
 | 
			
		||||
            error instanceof Error ? error.message : String(error),
 | 
			
		||||
            'ERR_PROCESS_START_FAILED',
 | 
			
		||||
            { command: this.options.command }
 | 
			
		||||
          );
 | 
			
		||||
      
 | 
			
		||||
      this.logger.error(processError);
 | 
			
		||||
      this.addSystemLog(`Failed to start process: ${processError.toString()}`);
 | 
			
		||||
      this.emit('error', processError);
 | 
			
		||||
      throw processError;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
@@ -105,30 +128,43 @@ export class ProcessWrapper extends EventEmitter {
 | 
			
		||||
   */
 | 
			
		||||
  public stop(): void {
 | 
			
		||||
    if (!this.process) {
 | 
			
		||||
      this.logger.debug('Stop called but no process is running');
 | 
			
		||||
      this.addSystemLog('No process running');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    this.logger.info('Stopping process...');
 | 
			
		||||
    this.addSystemLog('Stopping process...');
 | 
			
		||||
    
 | 
			
		||||
    // First try SIGTERM for graceful shutdown
 | 
			
		||||
    if (this.process.pid) {
 | 
			
		||||
      try {
 | 
			
		||||
        this.logger.debug(`Sending SIGTERM to process ${this.process.pid}`);
 | 
			
		||||
        process.kill(this.process.pid, 'SIGTERM');
 | 
			
		||||
        
 | 
			
		||||
        // Give it 5 seconds to shut down gracefully
 | 
			
		||||
        setTimeout(() => {
 | 
			
		||||
        setTimeout((): void => {
 | 
			
		||||
          if (this.process && this.process.pid) {
 | 
			
		||||
            this.logger.warn(`Process ${this.process.pid} did not exit gracefully, force killing...`);
 | 
			
		||||
            this.addSystemLog('Process did not exit gracefully, force killing...');
 | 
			
		||||
            try {
 | 
			
		||||
              process.kill(this.process.pid, 'SIGKILL');
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
            } catch (error: Error | unknown) {
 | 
			
		||||
              // Process might have exited between checks
 | 
			
		||||
              this.logger.debug(`Failed to send SIGKILL, process probably already exited: ${
 | 
			
		||||
                error instanceof Error ? error.message : String(error)
 | 
			
		||||
              }`);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }, 5000);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        this.addSystemLog(`Error stopping process: ${error.message}`);
 | 
			
		||||
      } catch (error: Error | unknown) {
 | 
			
		||||
        const processError = new ProcessError(
 | 
			
		||||
          error instanceof Error ? error.message : String(error),
 | 
			
		||||
          'ERR_PROCESS_STOP_FAILED',
 | 
			
		||||
          { pid: this.process.pid }
 | 
			
		||||
        );
 | 
			
		||||
        this.logger.error(processError);
 | 
			
		||||
        this.addSystemLog(`Error stopping process: ${processError.toString()}`);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,13 @@ import * as plugins from './plugins.js';
 | 
			
		||||
import * as paths from './paths.js';
 | 
			
		||||
import { ProcessMonitor, type IMonitorConfig } from './classes.processmonitor.js';
 | 
			
		||||
import { TspmConfig } from './classes.config.js';
 | 
			
		||||
import { 
 | 
			
		||||
  Logger, 
 | 
			
		||||
  ProcessError, 
 | 
			
		||||
  ConfigError, 
 | 
			
		||||
  ValidationError, 
 | 
			
		||||
  handleError 
 | 
			
		||||
} from './utils.errorhandler.js';
 | 
			
		||||
 | 
			
		||||
export interface IProcessConfig extends IMonitorConfig {
 | 
			
		||||
  id: string;         // Unique identifier for the process
 | 
			
		||||
@@ -32,8 +39,10 @@ export class Tspm {
 | 
			
		||||
  private processInfo: Map<string, IProcessInfo> = new Map();
 | 
			
		||||
  private config: TspmConfig;
 | 
			
		||||
  private configStorageKey = 'processes';
 | 
			
		||||
  private logger: Logger;
 | 
			
		||||
  
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.logger = new Logger('Tspm');
 | 
			
		||||
    this.config = new TspmConfig();
 | 
			
		||||
    this.loadProcessConfigs();
 | 
			
		||||
  }
 | 
			
		||||
@@ -42,53 +51,113 @@ export class Tspm {
 | 
			
		||||
   * Start a new process with the given configuration
 | 
			
		||||
   */
 | 
			
		||||
  public async start(config: IProcessConfig): Promise<void> {
 | 
			
		||||
    // Check if process with this id already exists
 | 
			
		||||
    if (this.processes.has(config.id)) {
 | 
			
		||||
      throw new Error(`Process with id '${config.id}' already exists`);
 | 
			
		||||
    this.logger.info(`Starting process with id '${config.id}'`);
 | 
			
		||||
    
 | 
			
		||||
    // Validate config
 | 
			
		||||
    if (!config.id || !config.command || !config.projectDir) {
 | 
			
		||||
      throw new ValidationError(
 | 
			
		||||
        'Invalid process configuration: missing required fields',
 | 
			
		||||
        'ERR_INVALID_CONFIG',
 | 
			
		||||
        { config }
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Create and store process config
 | 
			
		||||
    this.processConfigs.set(config.id, config);
 | 
			
		||||
    // Check if process with this id already exists
 | 
			
		||||
    if (this.processes.has(config.id)) {
 | 
			
		||||
      throw new ValidationError(
 | 
			
		||||
        `Process with id '${config.id}' already exists`,
 | 
			
		||||
        'ERR_DUPLICATE_PROCESS'
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Initialize process info
 | 
			
		||||
    this.processInfo.set(config.id, {
 | 
			
		||||
      id: config.id,
 | 
			
		||||
      status: 'stopped',
 | 
			
		||||
      memory: 0,
 | 
			
		||||
      restarts: 0
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    // Create and start process monitor
 | 
			
		||||
    const monitor = new ProcessMonitor({
 | 
			
		||||
      name: config.name || config.id,
 | 
			
		||||
      projectDir: config.projectDir,
 | 
			
		||||
      command: config.command,
 | 
			
		||||
      args: config.args,
 | 
			
		||||
      memoryLimitBytes: config.memoryLimitBytes,
 | 
			
		||||
      monitorIntervalMs: config.monitorIntervalMs
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    this.processes.set(config.id, monitor);
 | 
			
		||||
    monitor.start();
 | 
			
		||||
    
 | 
			
		||||
    // Update process info
 | 
			
		||||
    this.updateProcessInfo(config.id, { status: 'online' });
 | 
			
		||||
    
 | 
			
		||||
    // Save updated configs
 | 
			
		||||
    await this.saveProcessConfigs();
 | 
			
		||||
    try {
 | 
			
		||||
      // Create and store process config
 | 
			
		||||
      this.processConfigs.set(config.id, config);
 | 
			
		||||
      
 | 
			
		||||
      // Initialize process info
 | 
			
		||||
      this.processInfo.set(config.id, {
 | 
			
		||||
        id: config.id,
 | 
			
		||||
        status: 'stopped',
 | 
			
		||||
        memory: 0,
 | 
			
		||||
        restarts: 0
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      // Create and start process monitor
 | 
			
		||||
      const monitor = new ProcessMonitor({
 | 
			
		||||
        name: config.name || config.id,
 | 
			
		||||
        projectDir: config.projectDir,
 | 
			
		||||
        command: config.command,
 | 
			
		||||
        args: config.args,
 | 
			
		||||
        memoryLimitBytes: config.memoryLimitBytes,
 | 
			
		||||
        monitorIntervalMs: config.monitorIntervalMs,
 | 
			
		||||
        env: config.env,
 | 
			
		||||
        logBufferSize: config.logBufferSize
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      this.processes.set(config.id, monitor);
 | 
			
		||||
      monitor.start();
 | 
			
		||||
      
 | 
			
		||||
      // Update process info
 | 
			
		||||
      this.updateProcessInfo(config.id, { status: 'online' });
 | 
			
		||||
      
 | 
			
		||||
      // Save updated configs
 | 
			
		||||
      await this.saveProcessConfigs();
 | 
			
		||||
      
 | 
			
		||||
      this.logger.info(`Successfully started process with id '${config.id}'`);
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
      // Clean up in case of error
 | 
			
		||||
      this.processConfigs.delete(config.id);
 | 
			
		||||
      this.processInfo.delete(config.id);
 | 
			
		||||
      this.processes.delete(config.id);
 | 
			
		||||
      
 | 
			
		||||
      if (error instanceof Error) {
 | 
			
		||||
        this.logger.error(error);
 | 
			
		||||
        throw new ProcessError(
 | 
			
		||||
          `Failed to start process: ${error.message}`,
 | 
			
		||||
          'ERR_PROCESS_START_FAILED',
 | 
			
		||||
          { id: config.id, command: config.command }
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        const genericError = new ProcessError(
 | 
			
		||||
          `Failed to start process: ${String(error)}`,
 | 
			
		||||
          'ERR_PROCESS_START_FAILED',
 | 
			
		||||
          { id: config.id }
 | 
			
		||||
        );
 | 
			
		||||
        this.logger.error(genericError);
 | 
			
		||||
        throw genericError;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
   * Stop a process by id
 | 
			
		||||
   */
 | 
			
		||||
  public async stop(id: string): Promise<void> {
 | 
			
		||||
    this.logger.info(`Stopping process with id '${id}'`);
 | 
			
		||||
    
 | 
			
		||||
    const monitor = this.processes.get(id);
 | 
			
		||||
    if (!monitor) {
 | 
			
		||||
      throw new Error(`Process with id '${id}' not found`);
 | 
			
		||||
      const error = new ValidationError(
 | 
			
		||||
        `Process with id '${id}' not found`,
 | 
			
		||||
        'ERR_PROCESS_NOT_FOUND'
 | 
			
		||||
      );
 | 
			
		||||
      this.logger.error(error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    monitor.stop();
 | 
			
		||||
    this.updateProcessInfo(id, { status: 'stopped' });
 | 
			
		||||
    try {
 | 
			
		||||
      monitor.stop();
 | 
			
		||||
      this.updateProcessInfo(id, { status: 'stopped' });
 | 
			
		||||
      this.logger.info(`Successfully stopped process with id '${id}'`);
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
      const processError = new ProcessError(
 | 
			
		||||
        `Failed to stop process: ${error instanceof Error ? error.message : String(error)}`,
 | 
			
		||||
        'ERR_PROCESS_STOP_FAILED',
 | 
			
		||||
        { id }
 | 
			
		||||
      );
 | 
			
		||||
      this.logger.error(processError);
 | 
			
		||||
      throw processError;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Don't remove from the maps, just mark as stopped
 | 
			
		||||
    // This allows it to be restarted later
 | 
			
		||||
@@ -98,36 +167,57 @@ export class Tspm {
 | 
			
		||||
   * Restart a process by id
 | 
			
		||||
   */
 | 
			
		||||
  public async restart(id: string): Promise<void> {
 | 
			
		||||
    this.logger.info(`Restarting process with id '${id}'`);
 | 
			
		||||
    
 | 
			
		||||
    const monitor = this.processes.get(id);
 | 
			
		||||
    const config = this.processConfigs.get(id);
 | 
			
		||||
    
 | 
			
		||||
    if (!monitor || !config) {
 | 
			
		||||
      throw new Error(`Process with id '${id}' not found`);
 | 
			
		||||
      const error = new ValidationError(
 | 
			
		||||
        `Process with id '${id}' not found`,
 | 
			
		||||
        'ERR_PROCESS_NOT_FOUND'
 | 
			
		||||
      );
 | 
			
		||||
      this.logger.error(error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Stop and then start the process
 | 
			
		||||
    monitor.stop();
 | 
			
		||||
    
 | 
			
		||||
    // Create a new monitor instance
 | 
			
		||||
    const newMonitor = new ProcessMonitor({
 | 
			
		||||
      name: config.name || config.id,
 | 
			
		||||
      projectDir: config.projectDir,
 | 
			
		||||
      command: config.command,
 | 
			
		||||
      args: config.args,
 | 
			
		||||
      memoryLimitBytes: config.memoryLimitBytes,
 | 
			
		||||
      monitorIntervalMs: config.monitorIntervalMs
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    this.processes.set(id, newMonitor);
 | 
			
		||||
    newMonitor.start();
 | 
			
		||||
    
 | 
			
		||||
    // Update restart count
 | 
			
		||||
    const info = this.processInfo.get(id);
 | 
			
		||||
    if (info) {
 | 
			
		||||
      this.updateProcessInfo(id, { 
 | 
			
		||||
        status: 'online',
 | 
			
		||||
        restarts: info.restarts + 1
 | 
			
		||||
    try {
 | 
			
		||||
      // Stop and then start the process
 | 
			
		||||
      monitor.stop();
 | 
			
		||||
      
 | 
			
		||||
      // Create a new monitor instance
 | 
			
		||||
      const newMonitor = new ProcessMonitor({
 | 
			
		||||
        name: config.name || config.id,
 | 
			
		||||
        projectDir: config.projectDir,
 | 
			
		||||
        command: config.command,
 | 
			
		||||
        args: config.args,
 | 
			
		||||
        memoryLimitBytes: config.memoryLimitBytes,
 | 
			
		||||
        monitorIntervalMs: config.monitorIntervalMs,
 | 
			
		||||
        env: config.env,
 | 
			
		||||
        logBufferSize: config.logBufferSize
 | 
			
		||||
      });
 | 
			
		||||
      
 | 
			
		||||
      this.processes.set(id, newMonitor);
 | 
			
		||||
      newMonitor.start();
 | 
			
		||||
      
 | 
			
		||||
      // Update restart count
 | 
			
		||||
      const info = this.processInfo.get(id);
 | 
			
		||||
      if (info) {
 | 
			
		||||
        this.updateProcessInfo(id, { 
 | 
			
		||||
          status: 'online',
 | 
			
		||||
          restarts: info.restarts + 1
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      this.logger.info(`Successfully restarted process with id '${id}'`);
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
      const processError = new ProcessError(
 | 
			
		||||
        `Failed to restart process: ${error instanceof Error ? error.message : String(error)}`,
 | 
			
		||||
        'ERR_PROCESS_RESTART_FAILED',
 | 
			
		||||
        { id }
 | 
			
		||||
      );
 | 
			
		||||
      this.logger.error(processError);
 | 
			
		||||
      throw processError;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
@@ -135,20 +225,52 @@ export class Tspm {
 | 
			
		||||
   * Delete a process by id
 | 
			
		||||
   */
 | 
			
		||||
  public async delete(id: string): Promise<void> {
 | 
			
		||||
    // Stop the process if it's running
 | 
			
		||||
    try {
 | 
			
		||||
      await this.stop(id);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // Ignore errors if the process is not running
 | 
			
		||||
    this.logger.info(`Deleting process with id '${id}'`);
 | 
			
		||||
    
 | 
			
		||||
    // Check if process exists
 | 
			
		||||
    if (!this.processConfigs.has(id)) {
 | 
			
		||||
      const error = new ValidationError(
 | 
			
		||||
        `Process with id '${id}' not found`,
 | 
			
		||||
        'ERR_PROCESS_NOT_FOUND'
 | 
			
		||||
      );
 | 
			
		||||
      this.logger.error(error);
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    // Remove from all maps
 | 
			
		||||
    this.processes.delete(id);
 | 
			
		||||
    this.processConfigs.delete(id);
 | 
			
		||||
    this.processInfo.delete(id);
 | 
			
		||||
    
 | 
			
		||||
    // Save updated configs
 | 
			
		||||
    await this.saveProcessConfigs();
 | 
			
		||||
    // Stop the process if it's running
 | 
			
		||||
    try {
 | 
			
		||||
      if (this.processes.has(id)) {
 | 
			
		||||
        await this.stop(id);
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Remove from all maps
 | 
			
		||||
      this.processes.delete(id);
 | 
			
		||||
      this.processConfigs.delete(id);
 | 
			
		||||
      this.processInfo.delete(id);
 | 
			
		||||
      
 | 
			
		||||
      // Save updated configs
 | 
			
		||||
      await this.saveProcessConfigs();
 | 
			
		||||
      
 | 
			
		||||
      this.logger.info(`Successfully deleted process with id '${id}'`);
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
      // Even if stop fails, we should still try to delete the configuration
 | 
			
		||||
      try {
 | 
			
		||||
        this.processes.delete(id);
 | 
			
		||||
        this.processConfigs.delete(id);
 | 
			
		||||
        this.processInfo.delete(id);
 | 
			
		||||
        await this.saveProcessConfigs();
 | 
			
		||||
        
 | 
			
		||||
        this.logger.info(`Successfully deleted process with id '${id}' after stopping failure`);
 | 
			
		||||
      } catch (deleteError: Error | unknown) {
 | 
			
		||||
        const configError = new ConfigError(
 | 
			
		||||
          `Failed to delete process configuration: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`,
 | 
			
		||||
          'ERR_CONFIG_DELETE_FAILED',
 | 
			
		||||
          { id }
 | 
			
		||||
        );
 | 
			
		||||
        this.logger.error(configError);
 | 
			
		||||
        throw configError;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
@@ -227,33 +349,71 @@ export class Tspm {
 | 
			
		||||
   * Save all process configurations to config storage
 | 
			
		||||
   */
 | 
			
		||||
  private async saveProcessConfigs(): Promise<void> {
 | 
			
		||||
    const configs = Array.from(this.processConfigs.values());
 | 
			
		||||
    await this.config.writeKey(this.configStorageKey, JSON.stringify(configs));
 | 
			
		||||
    this.logger.debug('Saving process configurations to storage');
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      const configs = Array.from(this.processConfigs.values());
 | 
			
		||||
      await this.config.writeKey(this.configStorageKey, JSON.stringify(configs));
 | 
			
		||||
      this.logger.debug(`Saved ${configs.length} process configurations`);
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
      const configError = new ConfigError(
 | 
			
		||||
        `Failed to save process configurations: ${error instanceof Error ? error.message : String(error)}`,
 | 
			
		||||
        'ERR_CONFIG_SAVE_FAILED'
 | 
			
		||||
      );
 | 
			
		||||
      this.logger.error(configError);
 | 
			
		||||
      throw configError;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  /**
 | 
			
		||||
   * Load process configurations from config storage
 | 
			
		||||
   */
 | 
			
		||||
  private async loadProcessConfigs(): Promise<void> {
 | 
			
		||||
    this.logger.debug('Loading process configurations from storage');
 | 
			
		||||
    
 | 
			
		||||
    try {
 | 
			
		||||
      const configsJson = await this.config.readKey(this.configStorageKey);
 | 
			
		||||
      if (configsJson) {
 | 
			
		||||
        const configs = JSON.parse(configsJson) as IProcessConfig[];
 | 
			
		||||
        for (const config of configs) {
 | 
			
		||||
          this.processConfigs.set(config.id, config);
 | 
			
		||||
        try {
 | 
			
		||||
          const configs = JSON.parse(configsJson) as IProcessConfig[];
 | 
			
		||||
          this.logger.debug(`Loaded ${configs.length} process configurations`);
 | 
			
		||||
          
 | 
			
		||||
          // Initialize process info
 | 
			
		||||
          this.processInfo.set(config.id, {
 | 
			
		||||
            id: config.id,
 | 
			
		||||
            status: 'stopped',
 | 
			
		||||
            memory: 0,
 | 
			
		||||
            restarts: 0
 | 
			
		||||
          });
 | 
			
		||||
          for (const config of configs) {
 | 
			
		||||
            // Validate config
 | 
			
		||||
            if (!config.id || !config.command || !config.projectDir) {
 | 
			
		||||
              this.logger.warn(`Skipping invalid process config for id '${config.id || 'unknown'}'`);
 | 
			
		||||
              continue;
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            this.processConfigs.set(config.id, config);
 | 
			
		||||
            
 | 
			
		||||
            // Initialize process info
 | 
			
		||||
            this.processInfo.set(config.id, {
 | 
			
		||||
              id: config.id,
 | 
			
		||||
              status: 'stopped',
 | 
			
		||||
              memory: 0,
 | 
			
		||||
              restarts: 0
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        } catch (parseError: Error | unknown) {
 | 
			
		||||
          const configError = new ConfigError(
 | 
			
		||||
            `Failed to parse process configurations: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
 | 
			
		||||
            'ERR_CONFIG_PARSE_FAILED'
 | 
			
		||||
          );
 | 
			
		||||
          this.logger.error(configError);
 | 
			
		||||
          throw configError;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        this.logger.info('No saved process configurations found');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
      // Only throw if it's not the "no configs found" case
 | 
			
		||||
      if (error instanceof ConfigError) {
 | 
			
		||||
        throw error;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // If no configs found or error reading, just continue with empty configs
 | 
			
		||||
      console.log('No saved process configurations found');
 | 
			
		||||
      this.logger.info('No saved process configurations found or error reading them');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										109
									
								
								ts/cli.ts
									
									
									
									
									
								
							
							
						
						
									
										109
									
								
								ts/cli.ts
									
									
									
									
									
								
							@@ -1,17 +1,40 @@
 | 
			
		||||
import * as plugins from './plugins.js';
 | 
			
		||||
import * as paths from './paths.js';
 | 
			
		||||
import { Tspm, IProcessConfig } from './classes.tspm.js';
 | 
			
		||||
import { Tspm, type IProcessConfig } from './classes.tspm.js';
 | 
			
		||||
import { 
 | 
			
		||||
  Logger, 
 | 
			
		||||
  LogLevel, 
 | 
			
		||||
  handleError, 
 | 
			
		||||
  TspmError,
 | 
			
		||||
  ProcessError, 
 | 
			
		||||
  ConfigError, 
 | 
			
		||||
  ValidationError 
 | 
			
		||||
} from './utils.errorhandler.js';
 | 
			
		||||
 | 
			
		||||
export const run = async () => {
 | 
			
		||||
// Define interface for CLI arguments
 | 
			
		||||
interface CliArguments {
 | 
			
		||||
  _: (string | number)[];
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const run = async (): Promise<void> => {
 | 
			
		||||
  const cliLogger = new Logger('CLI');
 | 
			
		||||
  const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
 | 
			
		||||
  const tspm = new Tspm();
 | 
			
		||||
 | 
			
		||||
  // Check if debug mode is enabled
 | 
			
		||||
  const debugMode = process.env.TSPM_DEBUG === 'true';
 | 
			
		||||
  if (debugMode) {
 | 
			
		||||
    cliLogger.setLevel(LogLevel.DEBUG);
 | 
			
		||||
    cliLogger.debug('Debug mode enabled');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const smartcliInstance = new plugins.smartcli.Smartcli();
 | 
			
		||||
  smartcliInstance.addVersion(tspmProjectinfo.npm.version);
 | 
			
		||||
  
 | 
			
		||||
  // Default command - show help and list processes
 | 
			
		||||
  smartcliInstance.standardCommand().subscribe({
 | 
			
		||||
    next: async (argvArg) => {
 | 
			
		||||
    next: async (argvArg: CliArguments) => {
 | 
			
		||||
      console.log(`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`);
 | 
			
		||||
      console.log('Usage: tspm [command] [options]');
 | 
			
		||||
      console.log('\nCommands:');
 | 
			
		||||
@@ -46,7 +69,7 @@ export const run = async () => {
 | 
			
		||||
 | 
			
		||||
  // Start command - start a new process
 | 
			
		||||
  smartcliInstance.addCommand('start').subscribe({
 | 
			
		||||
    next: async (argvArg) => {
 | 
			
		||||
    next: async (argvArg: CliArguments) => {
 | 
			
		||||
      const script = argvArg._.length > 1 ? String(argvArg._[1]) : '';
 | 
			
		||||
      if (!script) {
 | 
			
		||||
        console.error('Error: Missing script argument. Usage: tspm start <script>');
 | 
			
		||||
@@ -59,6 +82,8 @@ export const run = async () => {
 | 
			
		||||
      const memLimit = parseMemoryString(argvArg.memory || '500MB');
 | 
			
		||||
      
 | 
			
		||||
      try {
 | 
			
		||||
        cliLogger.debug(`Starting process with script: ${script}`);
 | 
			
		||||
        
 | 
			
		||||
        const processConfig: IProcessConfig = {
 | 
			
		||||
          id: argvArg.id || name.replace(/[^a-zA-Z0-9_-]/g, '_').toLowerCase(),
 | 
			
		||||
          name: name,
 | 
			
		||||
@@ -71,17 +96,32 @@ export const run = async () => {
 | 
			
		||||
          watch: Boolean(argvArg.watch)
 | 
			
		||||
        };
 | 
			
		||||
        
 | 
			
		||||
        cliLogger.debug(`Created process config: ${JSON.stringify(processConfig)}`);
 | 
			
		||||
        
 | 
			
		||||
        await tspm.start(processConfig);
 | 
			
		||||
        console.log(`Process ${processConfig.id} started successfully.`);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error(`Error starting process: ${error.message}`);
 | 
			
		||||
      } catch (error: Error | unknown) {
 | 
			
		||||
        const tspmError = handleError(error);
 | 
			
		||||
        
 | 
			
		||||
        if (tspmError instanceof ValidationError) {
 | 
			
		||||
          console.error(`Validation error: ${tspmError.message}`);
 | 
			
		||||
        } else if (tspmError instanceof ProcessError) {
 | 
			
		||||
          console.error(`Process error: ${tspmError.message}`);
 | 
			
		||||
          if (debugMode) {
 | 
			
		||||
            console.error(`Error details: ${JSON.stringify(tspmError.details)}`);
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          console.error(`Error starting process: ${tspmError.message}`);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        cliLogger.error(tspmError);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Start as daemon command
 | 
			
		||||
  smartcliInstance.addCommand('startAsDaemon').subscribe({
 | 
			
		||||
    next: async (argvArg) => {
 | 
			
		||||
    next: async (argvArg: CliArguments) => {
 | 
			
		||||
      const script = argvArg._.length > 1 ? String(argvArg._[1]) : '';
 | 
			
		||||
      if (!script) {
 | 
			
		||||
        console.error('Error: Missing script argument. Usage: tspm startAsDaemon <script>');
 | 
			
		||||
@@ -114,7 +154,7 @@ export const run = async () => {
 | 
			
		||||
 | 
			
		||||
  // Stop command
 | 
			
		||||
  smartcliInstance.addCommand('stop').subscribe({
 | 
			
		||||
    next: async (argvArg) => {
 | 
			
		||||
    next: async (argvArg: CliArguments) => {
 | 
			
		||||
      const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
 | 
			
		||||
      
 | 
			
		||||
      if (!id) {
 | 
			
		||||
@@ -123,17 +163,26 @@ export const run = async () => {
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      try {
 | 
			
		||||
        cliLogger.debug(`Stopping process: ${id}`);
 | 
			
		||||
        await tspm.stop(id);
 | 
			
		||||
        console.log(`Process ${id} stopped.`);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error(`Error stopping process: ${error.message}`);
 | 
			
		||||
      } catch (error: Error | unknown) {
 | 
			
		||||
        const tspmError = handleError(error);
 | 
			
		||||
        
 | 
			
		||||
        if (tspmError instanceof ValidationError) {
 | 
			
		||||
          console.error(`Validation error: ${tspmError.message}`);
 | 
			
		||||
        } else {
 | 
			
		||||
          console.error(`Error stopping process: ${tspmError.message}`);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        cliLogger.error(tspmError);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Restart command
 | 
			
		||||
  smartcliInstance.addCommand('restart').subscribe({
 | 
			
		||||
    next: async (argvArg) => {
 | 
			
		||||
    next: async (argvArg: CliArguments) => {
 | 
			
		||||
      const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
 | 
			
		||||
      
 | 
			
		||||
      if (!id) {
 | 
			
		||||
@@ -142,17 +191,28 @@ export const run = async () => {
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      try {
 | 
			
		||||
        cliLogger.debug(`Restarting process: ${id}`);
 | 
			
		||||
        await tspm.restart(id);
 | 
			
		||||
        console.log(`Process ${id} restarted.`);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error(`Error restarting process: ${error.message}`);
 | 
			
		||||
      } catch (error: Error | unknown) {
 | 
			
		||||
        const tspmError = handleError(error);
 | 
			
		||||
        
 | 
			
		||||
        if (tspmError instanceof ValidationError) {
 | 
			
		||||
          console.error(`Validation error: ${tspmError.message}`);
 | 
			
		||||
        } else if (tspmError instanceof ProcessError) {
 | 
			
		||||
          console.error(`Process error: ${tspmError.message}`);
 | 
			
		||||
        } else {
 | 
			
		||||
          console.error(`Error restarting process: ${tspmError.message}`);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        cliLogger.error(tspmError);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Delete command
 | 
			
		||||
  smartcliInstance.addCommand('delete').subscribe({
 | 
			
		||||
    next: async (argvArg) => {
 | 
			
		||||
    next: async (argvArg: CliArguments) => {
 | 
			
		||||
      const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
 | 
			
		||||
      
 | 
			
		||||
      if (!id) {
 | 
			
		||||
@@ -161,17 +221,28 @@ export const run = async () => {
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      try {
 | 
			
		||||
        cliLogger.debug(`Deleting process: ${id}`);
 | 
			
		||||
        await tspm.delete(id);
 | 
			
		||||
        console.log(`Process ${id} deleted.`);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        console.error(`Error deleting process: ${error.message}`);
 | 
			
		||||
      } catch (error: Error | unknown) {
 | 
			
		||||
        const tspmError = handleError(error);
 | 
			
		||||
        
 | 
			
		||||
        if (tspmError instanceof ValidationError) {
 | 
			
		||||
          console.error(`Validation error: ${tspmError.message}`);
 | 
			
		||||
        } else if (tspmError instanceof ConfigError) {
 | 
			
		||||
          console.error(`Configuration error: ${tspmError.message}`);
 | 
			
		||||
        } else {
 | 
			
		||||
          console.error(`Error deleting process: ${tspmError.message}`);
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        cliLogger.error(tspmError);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // List command
 | 
			
		||||
  smartcliInstance.addCommand('list').subscribe({
 | 
			
		||||
    next: async (argvArg) => {
 | 
			
		||||
    next: async (argvArg: CliArguments) => {
 | 
			
		||||
      const processes = tspm.list();
 | 
			
		||||
      
 | 
			
		||||
      if (processes.length === 0) {
 | 
			
		||||
@@ -193,7 +264,7 @@ export const run = async () => {
 | 
			
		||||
 | 
			
		||||
  // Describe command
 | 
			
		||||
  smartcliInstance.addCommand('describe').subscribe({
 | 
			
		||||
    next: async (argvArg) => {
 | 
			
		||||
    next: async (argvArg: CliArguments) => {
 | 
			
		||||
      const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
 | 
			
		||||
      
 | 
			
		||||
      if (!id) {
 | 
			
		||||
@@ -224,7 +295,7 @@ export const run = async () => {
 | 
			
		||||
  
 | 
			
		||||
  // Logs command
 | 
			
		||||
  smartcliInstance.addCommand('logs').subscribe({
 | 
			
		||||
    next: async (argvArg) => {
 | 
			
		||||
    next: async (argvArg: CliArguments) => {
 | 
			
		||||
      const id = argvArg._.length > 1 ? String(argvArg._[1]) : '';
 | 
			
		||||
      
 | 
			
		||||
      if (!id) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import * as plugins from './plugins.js';
 | 
			
		||||
 | 
			
		||||
export const packageDir = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '..');
 | 
			
		||||
export const cwd = process.cwd();
 | 
			
		||||
export const packageDir: string = plugins.path.join(plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url), '..');
 | 
			
		||||
export const cwd: string = process.cwd();
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
import * as childProcess from 'child_process';
 | 
			
		||||
import * as path from 'node:path';
 | 
			
		||||
 | 
			
		||||
// Export with explicit module types
 | 
			
		||||
export {
 | 
			
		||||
  childProcess,
 | 
			
		||||
  path,
 | 
			
		||||
@@ -14,6 +15,7 @@ import * as smartcli from '@push.rocks/smartcli';
 | 
			
		||||
import * as smartdaemon from '@push.rocks/smartdaemon';
 | 
			
		||||
import * as smartpath from '@push.rocks/smartpath';
 | 
			
		||||
 | 
			
		||||
// Export with explicit module types
 | 
			
		||||
export {
 | 
			
		||||
  npmextra,
 | 
			
		||||
  projectinfo,
 | 
			
		||||
@@ -26,6 +28,7 @@ export {
 | 
			
		||||
import psTree from 'ps-tree';
 | 
			
		||||
import pidusage from 'pidusage';
 | 
			
		||||
 | 
			
		||||
// Add explicit types for third-party exports
 | 
			
		||||
export {
 | 
			
		||||
  psTree,
 | 
			
		||||
  pidusage,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										138
									
								
								ts/utils.errorhandler.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								ts/utils.errorhandler.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
			
		||||
/**
 | 
			
		||||
 * Centralized error handling utility for TSPM
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// Define error types
 | 
			
		||||
export enum ErrorType {
 | 
			
		||||
  CONFIG = 'ConfigError',
 | 
			
		||||
  PROCESS = 'ProcessError',
 | 
			
		||||
  RUNTIME = 'RuntimeError',
 | 
			
		||||
  VALIDATION = 'ValidationError',
 | 
			
		||||
  UNKNOWN = 'UnknownError'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Base error class with type and code support
 | 
			
		||||
export class TspmError extends Error {
 | 
			
		||||
  type: ErrorType;
 | 
			
		||||
  code: string;
 | 
			
		||||
  details?: Record<string, any>;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    message: string, 
 | 
			
		||||
    type: ErrorType = ErrorType.UNKNOWN, 
 | 
			
		||||
    code: string = 'ERR_UNKNOWN',
 | 
			
		||||
    details?: Record<string, any>
 | 
			
		||||
  ) {
 | 
			
		||||
    super(message);
 | 
			
		||||
    this.name = type;
 | 
			
		||||
    this.type = type;
 | 
			
		||||
    this.code = code;
 | 
			
		||||
    this.details = details;
 | 
			
		||||
    
 | 
			
		||||
    // Preserve proper stack trace
 | 
			
		||||
    if (Error.captureStackTrace) {
 | 
			
		||||
      Error.captureStackTrace(this, this.constructor);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  toString(): string {
 | 
			
		||||
    return `[${this.type}:${this.code}] ${this.message}`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Specific error classes
 | 
			
		||||
export class ConfigError extends TspmError {
 | 
			
		||||
  constructor(message: string, code: string = 'ERR_CONFIG', details?: Record<string, any>) {
 | 
			
		||||
    super(message, ErrorType.CONFIG, code, details);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ProcessError extends TspmError {
 | 
			
		||||
  constructor(message: string, code: string = 'ERR_PROCESS', details?: Record<string, any>) {
 | 
			
		||||
    super(message, ErrorType.PROCESS, code, details);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ValidationError extends TspmError {
 | 
			
		||||
  constructor(message: string, code: string = 'ERR_VALIDATION', details?: Record<string, any>) {
 | 
			
		||||
    super(message, ErrorType.VALIDATION, code, details);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Utility for handling any error type
 | 
			
		||||
export const handleError = (error: Error | unknown): TspmError => {
 | 
			
		||||
  if (error instanceof TspmError) {
 | 
			
		||||
    return error;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (error instanceof Error) {
 | 
			
		||||
    return new TspmError(error.message, ErrorType.UNKNOWN, 'ERR_UNKNOWN', { originalError: error });
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  return new TspmError(String(error), ErrorType.UNKNOWN, 'ERR_UNKNOWN');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Logger with different log levels
 | 
			
		||||
export enum LogLevel {
 | 
			
		||||
  DEBUG = 0,
 | 
			
		||||
  INFO = 1,
 | 
			
		||||
  WARN = 2,
 | 
			
		||||
  ERROR = 3,
 | 
			
		||||
  NONE = 4
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Logger {
 | 
			
		||||
  private static instance: Logger;
 | 
			
		||||
  private level: LogLevel = LogLevel.INFO;
 | 
			
		||||
  private componentName: string;
 | 
			
		||||
 | 
			
		||||
  constructor(componentName: string) {
 | 
			
		||||
    this.componentName = componentName;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static getInstance(componentName: string): Logger {
 | 
			
		||||
    if (!Logger.instance) {
 | 
			
		||||
      Logger.instance = new Logger(componentName);
 | 
			
		||||
    }
 | 
			
		||||
    return Logger.instance;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setLevel(level: LogLevel): void {
 | 
			
		||||
    this.level = level;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private formatMessage(message: string): string {
 | 
			
		||||
    const timestamp = new Date().toISOString();
 | 
			
		||||
    return `[${timestamp}] [${this.componentName}] ${message}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  debug(message: string): void {
 | 
			
		||||
    if (this.level <= LogLevel.DEBUG) {
 | 
			
		||||
      console.log(this.formatMessage(`DEBUG: ${message}`));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  info(message: string): void {
 | 
			
		||||
    if (this.level <= LogLevel.INFO) {
 | 
			
		||||
      console.log(this.formatMessage(message));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  warn(message: string): void {
 | 
			
		||||
    if (this.level <= LogLevel.WARN) {
 | 
			
		||||
      console.warn(this.formatMessage(`WARNING: ${message}`));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  error(error: Error | unknown): void {
 | 
			
		||||
    if (this.level <= LogLevel.ERROR) {
 | 
			
		||||
      const tspmError = handleError(error);
 | 
			
		||||
      console.error(this.formatMessage(`ERROR: ${tspmError.toString()}`));
 | 
			
		||||
      
 | 
			
		||||
      // In debug mode, also log stack trace
 | 
			
		||||
      if (this.level === LogLevel.DEBUG && tspmError.stack) {
 | 
			
		||||
        console.error(tspmError.stack);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user