feat(cli/daemon/processmonitor): Add flexible target resolution and search command; improve restart/backoff and error handling
This commit is contained in:
		@@ -3,6 +3,6 @@
 | 
			
		||||
 */
 | 
			
		||||
export const commitinfo = {
 | 
			
		||||
  name: '@git.zone/tspm',
 | 
			
		||||
  version: '5.2.0',
 | 
			
		||||
  version: '5.3.0',
 | 
			
		||||
  description: 'a no fuzz process manager'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,13 +22,14 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
      );
 | 
			
		||||
      console.log('  disable                  Disable TSPM system service');
 | 
			
		||||
      console.log('\nProcess Commands:');
 | 
			
		||||
      console.log('  start <script>           Start a process');
 | 
			
		||||
      console.log('  start <id|id:N|name:LBL> Start a process');
 | 
			
		||||
      console.log('  list                     List all processes');
 | 
			
		||||
      console.log('  stop <id>                Stop a process');
 | 
			
		||||
      console.log('  restart <id>             Restart a process');
 | 
			
		||||
      console.log('  delete <id>              Delete a process');
 | 
			
		||||
      console.log('  describe <id>            Show details for a process');
 | 
			
		||||
      console.log('  logs <id>                Show logs for a process');
 | 
			
		||||
      console.log('  stop <id|id:N|name:LBL>  Stop a process');
 | 
			
		||||
      console.log('  restart <id|id:N|name:LBL> Restart a process');
 | 
			
		||||
      console.log('  delete <id|id:N|name:LBL> Delete a process');
 | 
			
		||||
      console.log('  describe <id|id:N|name:LBL> Show details for a process');
 | 
			
		||||
      console.log('  logs <id|id:N|name:LBL>  Show logs for a process');
 | 
			
		||||
      console.log('  search <query>           Find processes by id/name');
 | 
			
		||||
      console.log('  start-all                Start all saved processes');
 | 
			
		||||
      console.log('  stop-all                 Stop all processes');
 | 
			
		||||
      console.log('  restart-all              Restart all processes');
 | 
			
		||||
 
 | 
			
		||||
@@ -8,23 +8,25 @@ export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
    smartcli,
 | 
			
		||||
    ['delete', 'remove'],
 | 
			
		||||
    async (argvArg: CliArguments) => {
 | 
			
		||||
      const id = argvArg._[1];
 | 
			
		||||
      if (!id) {
 | 
			
		||||
        console.error('Error: Please provide a process ID');
 | 
			
		||||
        console.log('Usage: tspm delete <id> | tspm remove <id>');
 | 
			
		||||
      const target = argvArg._[1];
 | 
			
		||||
      if (!target) {
 | 
			
		||||
        console.error('Error: Please provide a process target');
 | 
			
		||||
        console.log('Usage: tspm delete <id|id:N|name:LABEL> | tspm remove <id|id:N|name:LABEL>');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Determine if command was 'remove' to use the new IPC route, otherwise 'delete'
 | 
			
		||||
      const cmd = String(argvArg._[0]);
 | 
			
		||||
      const useRemove = cmd === 'remove';
 | 
			
		||||
      console.log(`${useRemove ? 'Removing' : 'Deleting'} process: ${id}`);
 | 
			
		||||
      const response = await tspmIpcClient.request(useRemove ? 'remove' : 'delete', { id } as any);
 | 
			
		||||
      const isRemoveAlias = cmd === 'remove';
 | 
			
		||||
      console.log(`${isRemoveAlias ? 'Removing' : 'Deleting'} process: ${target}`);
 | 
			
		||||
      const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
 | 
			
		||||
      // Always call daemon 'delete'; 'remove' is CLI alias only
 | 
			
		||||
      const response = await tspmIpcClient.request('delete', { id: resolved.id } as any);
 | 
			
		||||
 | 
			
		||||
      if (response.success) {
 | 
			
		||||
        console.log(`✓ ${response.message || (useRemove ? 'Removed successfully' : 'Deleted successfully')}`);
 | 
			
		||||
        console.log(`✓ ${response.message || (isRemoveAlias ? 'Removed successfully' : 'Deleted successfully')}`);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.error(`✗ Failed to ${useRemove ? 'remove' : 'delete'} process: ${response.message}`);
 | 
			
		||||
        console.error(`✗ Failed to ${isRemoveAlias ? 'remove' : 'delete'} process: ${response.message}`);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    { actionLabel: 'delete/remove process' },
 | 
			
		||||
 
 | 
			
		||||
@@ -9,16 +9,17 @@ export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
    smartcli,
 | 
			
		||||
    'describe',
 | 
			
		||||
    async (argvArg: CliArguments) => {
 | 
			
		||||
      const id = argvArg._[1];
 | 
			
		||||
      if (!id) {
 | 
			
		||||
        console.error('Error: Please provide a process ID');
 | 
			
		||||
        console.log('Usage: tspm describe <id>');
 | 
			
		||||
      const target = argvArg._[1];
 | 
			
		||||
      if (!target) {
 | 
			
		||||
        console.error('Error: Please provide a process target');
 | 
			
		||||
        console.log('Usage: tspm describe <id | id:N | name:LABEL>');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const response = await tspmIpcClient.request('describe', { id });
 | 
			
		||||
      const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
 | 
			
		||||
      const response = await tspmIpcClient.request('describe', { id: resolved.id });
 | 
			
		||||
 | 
			
		||||
      console.log(`Process Details: ${id}`);
 | 
			
		||||
      console.log(`Process Details: ${response.config.name || resolved.id}`);
 | 
			
		||||
      console.log('─'.repeat(40));
 | 
			
		||||
      console.log(`Status:      ${response.processInfo.status}`);
 | 
			
		||||
      console.log(`PID:         ${response.processInfo.pid || 'N/A'}`);
 | 
			
		||||
 
 | 
			
		||||
@@ -9,17 +9,16 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
    smartcli,
 | 
			
		||||
    'edit',
 | 
			
		||||
    async (argvArg: CliArguments) => {
 | 
			
		||||
      const idRaw = argvArg._[1];
 | 
			
		||||
      if (!idRaw) {
 | 
			
		||||
        console.error('Error: Please provide a process ID to edit');
 | 
			
		||||
        console.log('Usage: tspm edit <id>');
 | 
			
		||||
      const target = argvArg._[1];
 | 
			
		||||
      if (!target) {
 | 
			
		||||
        console.error('Error: Please provide a process target to edit');
 | 
			
		||||
        console.log('Usage: tspm edit <id | id:N | name:LABEL>');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const id = idRaw;
 | 
			
		||||
 | 
			
		||||
      // Load current config
 | 
			
		||||
      const { config } = await tspmIpcClient.request('describe', { id });
 | 
			
		||||
      // Resolve and load current config
 | 
			
		||||
      const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
 | 
			
		||||
      const { config } = await tspmIpcClient.request('describe', { id: resolved.id });
 | 
			
		||||
 | 
			
		||||
      // Interactive editing is temporarily disabled - needs smartinteract API update
 | 
			
		||||
      console.log('Interactive editing is temporarily disabled.');
 | 
			
		||||
@@ -63,7 +62,7 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const updateResponse = await tspmIpcClient.request('update', {
 | 
			
		||||
        id,
 | 
			
		||||
        id: resolved.id,
 | 
			
		||||
        updates,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
@@ -73,4 +72,3 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
    { actionLabel: 'edit process config' },
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,10 +11,10 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
    smartcli,
 | 
			
		||||
    'logs',
 | 
			
		||||
    async (argvArg: CliArguments) => {
 | 
			
		||||
      const id = argvArg._[1];
 | 
			
		||||
      if (!id) {
 | 
			
		||||
        console.error('Error: Please provide a process ID');
 | 
			
		||||
        console.log('Usage: tspm logs <id> [options]');
 | 
			
		||||
      const target = argvArg._[1];
 | 
			
		||||
      if (!target) {
 | 
			
		||||
        console.error('Error: Please provide a process target');
 | 
			
		||||
        console.log('Usage: tspm logs <id | id:N | name:LABEL> [options]');
 | 
			
		||||
        console.log('\nOptions:');
 | 
			
		||||
        console.log('  --lines <n>   Number of lines to show (default: 50)');
 | 
			
		||||
        console.log('  --follow      Stream logs in real-time (like tail -f)');
 | 
			
		||||
@@ -24,6 +24,8 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
      const lines = getNumber(argvArg, 'lines', 50);
 | 
			
		||||
      const follow = getBool(argvArg, 'follow', 'f');
 | 
			
		||||
 | 
			
		||||
      const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
 | 
			
		||||
      const id = resolved.id;
 | 
			
		||||
      const response = await tspmIpcClient.request('getLogs', { id, lines });
 | 
			
		||||
 | 
			
		||||
      if (!follow) {
 | 
			
		||||
@@ -44,7 +46,7 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Streaming mode
 | 
			
		||||
      console.log(`Logs for process: ${id} (streaming...)`);
 | 
			
		||||
      console.log(`Logs for process: ${resolved.name || id} (streaming...)`);
 | 
			
		||||
      console.log('─'.repeat(60));
 | 
			
		||||
 | 
			
		||||
      let lastSeq = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
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';
 | 
			
		||||
 | 
			
		||||
@@ -11,9 +10,9 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
    async (argvArg: CliArguments) => {
 | 
			
		||||
      const arg = argvArg._[1];
 | 
			
		||||
      if (!arg) {
 | 
			
		||||
        console.error('Error: Please provide a process ID or "all"');
 | 
			
		||||
        console.error('Error: Please provide a process target or "all"');
 | 
			
		||||
        console.log('Usage:');
 | 
			
		||||
        console.log('  tspm restart <id>');
 | 
			
		||||
        console.log('  tspm restart <id | id:N | name:LABEL>');
 | 
			
		||||
        console.log('  tspm restart all');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
@@ -33,12 +32,13 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const id = String(arg);
 | 
			
		||||
      console.log(`Restarting process: ${id}`);
 | 
			
		||||
      const response = await tspmIpcClient.request('restart', { id: toProcessId(id) });
 | 
			
		||||
      const target = String(arg);
 | 
			
		||||
      console.log(`Restarting process: ${target}`);
 | 
			
		||||
      const resolved = await tspmIpcClient.request('resolveTarget', { target });
 | 
			
		||||
      const response = await tspmIpcClient.request('restart', { id: resolved.id });
 | 
			
		||||
 | 
			
		||||
      console.log(`✓ Process restarted successfully`);
 | 
			
		||||
      console.log(`  ID: ${response.processId}`);
 | 
			
		||||
      console.log(`  ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
 | 
			
		||||
      console.log(`  PID: ${response.pid || 'N/A'}`);
 | 
			
		||||
      console.log(`  Status: ${response.status}`);
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										62
									
								
								ts/cli/commands/process/search.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								ts/cli/commands/process/search.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,62 @@
 | 
			
		||||
import * as plugins from '../../plugins.js';
 | 
			
		||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
 | 
			
		||||
import type { CliArguments } from '../../types.js';
 | 
			
		||||
import { registerIpcCommand } from '../../registration/index.js';
 | 
			
		||||
 | 
			
		||||
export function registerSearchCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
  registerIpcCommand(
 | 
			
		||||
    smartcli,
 | 
			
		||||
    'search',
 | 
			
		||||
    async (argvArg: CliArguments) => {
 | 
			
		||||
      const query = String(argvArg._[1] || '').trim();
 | 
			
		||||
      if (!query) {
 | 
			
		||||
        console.error('Error: Please provide a search query');
 | 
			
		||||
        console.log('Usage: tspm search <name-fragment | id-fragment>');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Fetch list of processes, then enrich with names via describe
 | 
			
		||||
      const listRes = await tspmIpcClient.request('list', {});
 | 
			
		||||
      const processes = listRes.processes;
 | 
			
		||||
 | 
			
		||||
      // If there are no processes, short-circuit
 | 
			
		||||
      if (processes.length === 0) {
 | 
			
		||||
        console.log('No processes found.');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const lowerQ = query.toLowerCase();
 | 
			
		||||
      const matches: Array<{ id: number; name?: string }> = [];
 | 
			
		||||
 | 
			
		||||
      // Collect describe calls to obtain names
 | 
			
		||||
      for (const proc of processes) {
 | 
			
		||||
        try {
 | 
			
		||||
          const desc = await tspmIpcClient.request('describe', { id: proc.id });
 | 
			
		||||
          const name = desc.config.name || '';
 | 
			
		||||
          const idStr = String(proc.id);
 | 
			
		||||
          if (name.toLowerCase().includes(lowerQ) || idStr.includes(query)) {
 | 
			
		||||
            matches.push({ id: proc.id, name });
 | 
			
		||||
          }
 | 
			
		||||
        } catch {
 | 
			
		||||
          // Ignore describe errors for individual processes
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (matches.length === 0) {
 | 
			
		||||
        console.log(`No matches for "${query}"`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log(`Matches for "${query}":`);
 | 
			
		||||
      for (const m of matches) {
 | 
			
		||||
        if (m.name) {
 | 
			
		||||
          console.log(`- id:${m.id}\tname:${m.name}`);
 | 
			
		||||
        } else {
 | 
			
		||||
          console.log(`- id:${m.id}`);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    { actionLabel: 'search processes' },
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -10,17 +10,18 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
    smartcli,
 | 
			
		||||
    'start',
 | 
			
		||||
    async (argvArg: CliArguments) => {
 | 
			
		||||
      const id = argvArg._[1];
 | 
			
		||||
      if (!id) {
 | 
			
		||||
        console.error('Error: Please provide a process ID to start');
 | 
			
		||||
        console.log('Usage: tspm start <id>');
 | 
			
		||||
      const target = argvArg._[1];
 | 
			
		||||
      if (!target) {
 | 
			
		||||
        console.error('Error: Please provide a process target to start');
 | 
			
		||||
        console.log('Usage: tspm start <id | id:N | name:LABEL>');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log(`Starting process id ${id}...`);
 | 
			
		||||
      const response = await tspmIpcClient.request('startById', { id });
 | 
			
		||||
      console.log(`Starting process: ${target}...`);
 | 
			
		||||
      const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
 | 
			
		||||
      const response = await tspmIpcClient.request('startById', { id: resolved.id });
 | 
			
		||||
      console.log('✓ Process started');
 | 
			
		||||
      console.log(`  ID: ${response.processId}`);
 | 
			
		||||
      console.log(`  ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
 | 
			
		||||
      console.log(`  PID: ${response.pid || 'N/A'}`);
 | 
			
		||||
      console.log(`  Status: ${response.status}`);
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
@@ -8,15 +8,16 @@ export function registerStopCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
    smartcli,
 | 
			
		||||
    'stop',
 | 
			
		||||
    async (argvArg: CliArguments) => {
 | 
			
		||||
      const id = argvArg._[1];
 | 
			
		||||
      if (!id) {
 | 
			
		||||
        console.error('Error: Please provide a process ID');
 | 
			
		||||
        console.log('Usage: tspm stop <id>');
 | 
			
		||||
      const target = argvArg._[1];
 | 
			
		||||
      if (!target) {
 | 
			
		||||
        console.error('Error: Please provide a process target');
 | 
			
		||||
        console.log('Usage: tspm stop <id | id:N | name:LABEL>');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log(`Stopping process: ${id}`);
 | 
			
		||||
      const response = await tspmIpcClient.request('stop', { id });
 | 
			
		||||
      console.log(`Stopping process: ${target}`);
 | 
			
		||||
      const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
 | 
			
		||||
      const response = await tspmIpcClient.request('stop', { id: resolved.id });
 | 
			
		||||
 | 
			
		||||
      if (response.success) {
 | 
			
		||||
        console.log(`✓ ${response.message}`);
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import { registerAddCommand } from './commands/process/add.js';
 | 
			
		||||
import { registerStopCommand } from './commands/process/stop.js';
 | 
			
		||||
import { registerRestartCommand } from './commands/process/restart.js';
 | 
			
		||||
import { registerDeleteCommand } from './commands/process/delete.js';
 | 
			
		||||
import { registerSearchCommand } from './commands/process/search.js';
 | 
			
		||||
import { registerListCommand } from './commands/process/list.js';
 | 
			
		||||
import { registerDescribeCommand } from './commands/process/describe.js';
 | 
			
		||||
import { registerLogsCommand } from './commands/process/logs.js';
 | 
			
		||||
@@ -74,6 +75,7 @@ export const run = async (): Promise<void> => {
 | 
			
		||||
  registerDescribeCommand(smartcliInstance);
 | 
			
		||||
  registerLogsCommand(smartcliInstance);
 | 
			
		||||
  registerEditCommand(smartcliInstance);
 | 
			
		||||
  registerSearchCommand(smartcliInstance);
 | 
			
		||||
 | 
			
		||||
  // Batch commands
 | 
			
		||||
  registerStartAllCommand(smartcliInstance);
 | 
			
		||||
 
 | 
			
		||||
@@ -156,6 +156,11 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
        this.updateProcessInfo(config.id, { pid: undefined });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Set up failure handler to mark process as errored
 | 
			
		||||
      monitor.on('failed', () => {
 | 
			
		||||
        this.updateProcessInfo(config.id, { status: 'errored', pid: undefined });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      await monitor.start();
 | 
			
		||||
 | 
			
		||||
      // Wait a moment for the process to spawn and get its PID
 | 
			
		||||
@@ -327,6 +332,11 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Mark errored on failure events
 | 
			
		||||
      newMonitor.on('failed', () => {
 | 
			
		||||
        this.updateProcessInfo(id, { status: 'errored', pid: undefined });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      this.logger.info(`Successfully restarted process with id '${id}'`);
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
      const processError = new ProcessError(
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,10 @@ export class ProcessMonitor extends EventEmitter {
 | 
			
		||||
  private processId?: ProcessId;
 | 
			
		||||
  private currentLogMemorySize: number = 0;
 | 
			
		||||
  private readonly MAX_LOG_MEMORY_SIZE = 10 * 1024 * 1024; // 10MB
 | 
			
		||||
  private restartTimer: NodeJS.Timeout | null = null;
 | 
			
		||||
  private lastRetryAt: number | null = null;
 | 
			
		||||
  private readonly MAX_RETRIES = 10;
 | 
			
		||||
  private readonly RESET_WINDOW_MS = 60 * 60 * 1000; // 1 hour
 | 
			
		||||
 | 
			
		||||
  constructor(config: IMonitorConfig & { id?: ProcessId }) {
 | 
			
		||||
    super();
 | 
			
		||||
@@ -132,10 +136,7 @@ export class ProcessMonitor extends EventEmitter {
 | 
			
		||||
        this.emit('exit', code, signal);
 | 
			
		||||
 | 
			
		||||
        if (!this.stopped) {
 | 
			
		||||
          this.logger.info('Restarting process...');
 | 
			
		||||
          this.log('Restarting process...');
 | 
			
		||||
          this.restartCount++;
 | 
			
		||||
          this.spawnProcess();
 | 
			
		||||
          this.scheduleRestart('exit');
 | 
			
		||||
        } else {
 | 
			
		||||
          this.logger.debug(
 | 
			
		||||
            'Not restarting process because monitor is stopped',
 | 
			
		||||
@@ -164,10 +165,7 @@ export class ProcessMonitor extends EventEmitter {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!this.stopped) {
 | 
			
		||||
        this.logger.info('Restarting process due to error...');
 | 
			
		||||
        this.log('Restarting process due to error...');
 | 
			
		||||
        this.restartCount++;
 | 
			
		||||
        this.spawnProcess();
 | 
			
		||||
        this.scheduleRestart('error');
 | 
			
		||||
      } else {
 | 
			
		||||
        this.logger.debug('Not restarting process because monitor is stopped');
 | 
			
		||||
      }
 | 
			
		||||
@@ -185,6 +183,49 @@ export class ProcessMonitor extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Schedule a restart with incremental debounce and failure cutoff.
 | 
			
		||||
   */
 | 
			
		||||
  private scheduleRestart(reason: 'exit' | 'error'): void {
 | 
			
		||||
    const now = Date.now();
 | 
			
		||||
    // Reset window: if last retry was more than 1 hour ago, reset counter
 | 
			
		||||
    if (this.lastRetryAt && now - this.lastRetryAt >= this.RESET_WINDOW_MS) {
 | 
			
		||||
      this.logger.info('Resetting retry counter after 1 hour window');
 | 
			
		||||
      this.restartCount = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Already at or above max retries?
 | 
			
		||||
    if (this.restartCount >= this.MAX_RETRIES) {
 | 
			
		||||
      const msg = 'Maximum restart attempts reached. Marking process as failed.';
 | 
			
		||||
      this.logger.warn(msg);
 | 
			
		||||
      this.log(msg);
 | 
			
		||||
      this.stopped = true;
 | 
			
		||||
      // Emit a specific event so manager can set status to errored
 | 
			
		||||
      this.emit('failed');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Increment and compute delay (1..10 seconds)
 | 
			
		||||
    this.restartCount++;
 | 
			
		||||
    const delaySec = Math.min(this.restartCount, 10);
 | 
			
		||||
    const msg = `Restarting process in ${delaySec}s (attempt ${this.restartCount}/${this.MAX_RETRIES}) due to ${reason}...`;
 | 
			
		||||
    this.logger.info(msg);
 | 
			
		||||
    this.log(msg);
 | 
			
		||||
 | 
			
		||||
    // Clear existing timer if any, then schedule
 | 
			
		||||
    if (this.restartTimer) {
 | 
			
		||||
      clearTimeout(this.restartTimer);
 | 
			
		||||
    }
 | 
			
		||||
    this.lastRetryAt = now;
 | 
			
		||||
    this.restartTimer = setTimeout(() => {
 | 
			
		||||
      // If stopped in the meantime, do not spawn
 | 
			
		||||
      if (this.stopped) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      this.spawnProcess();
 | 
			
		||||
    }, delaySec * 1000);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Monitor the process group's memory usage. If the total memory exceeds the limit,
 | 
			
		||||
   * kill the process group so that the 'exit' handler can restart it.
 | 
			
		||||
 
 | 
			
		||||
@@ -208,6 +208,8 @@ export class TspmDaemon {
 | 
			
		||||
      async (request: RequestForMethod<'delete'>) => {
 | 
			
		||||
        try {
 | 
			
		||||
          const id = toProcessId(request.id);
 | 
			
		||||
          // Ensure desired state reflects stopped before deletion
 | 
			
		||||
          await this.tspmInstance.setDesiredState(id, 'stopped');
 | 
			
		||||
          await this.tspmInstance.delete(id);
 | 
			
		||||
          return {
 | 
			
		||||
            success: true,
 | 
			
		||||
@@ -246,18 +248,7 @@ export class TspmDaemon {
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.ipcServer.onMessage(
 | 
			
		||||
      'remove',
 | 
			
		||||
      async (request: RequestForMethod<'remove'>) => {
 | 
			
		||||
        try {
 | 
			
		||||
          const id = toProcessId(request.id);
 | 
			
		||||
          await this.tspmInstance.delete(id);
 | 
			
		||||
          return { success: true, message: `Process ${id} deleted successfully` };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          throw new Error(`Failed to remove process: ${error.message}`);
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
    // Note: 'remove' is only a CLI alias. Daemon exposes 'delete' only.
 | 
			
		||||
 | 
			
		||||
    this.ipcServer.onMessage(
 | 
			
		||||
      'list',
 | 
			
		||||
@@ -291,6 +282,58 @@ export class TspmDaemon {
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Resolve target (id:n | name:foo | numeric string) to ProcessId
 | 
			
		||||
    this.ipcServer.onMessage(
 | 
			
		||||
      'resolveTarget',
 | 
			
		||||
      async (request: RequestForMethod<'resolveTarget'>) => {
 | 
			
		||||
        const raw = String(request.target || '').trim();
 | 
			
		||||
        if (!raw) {
 | 
			
		||||
          throw new Error('Empty target');
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // id:<n>
 | 
			
		||||
        if (/^id:\s*\d+$/i.test(raw)) {
 | 
			
		||||
          const idNum = raw.split(':')[1].trim();
 | 
			
		||||
          const id = toProcessId(idNum);
 | 
			
		||||
          const config = this.tspmInstance.processConfigs.get(id);
 | 
			
		||||
          if (!config) throw new Error(`Process ${id} not found`);
 | 
			
		||||
          return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // name:<label>
 | 
			
		||||
        if (/^name:/i.test(raw)) {
 | 
			
		||||
          const name = raw.slice(raw.indexOf(':') + 1).trim();
 | 
			
		||||
          if (!name) throw new Error('Missing name after name:');
 | 
			
		||||
          const matches = Array.from(this.tspmInstance.processConfigs.values()).filter(
 | 
			
		||||
            (c) => (c.name || '').trim() === name,
 | 
			
		||||
          );
 | 
			
		||||
          if (matches.length === 0) {
 | 
			
		||||
            throw new Error(`No process found with name "${name}"`);
 | 
			
		||||
          }
 | 
			
		||||
          if (matches.length > 1) {
 | 
			
		||||
            const ids = matches.map((c) => String(c.id)).join(', ');
 | 
			
		||||
            throw new Error(
 | 
			
		||||
              `Multiple processes found with name "${name}": ids [${ids}]. Please use id:<n>.`,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
          return { id: matches[0].id, name } as ResponseForMethod<'resolveTarget'>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // bare numeric id
 | 
			
		||||
        if (/^\d+$/.test(raw)) {
 | 
			
		||||
          const id = toProcessId(raw);
 | 
			
		||||
          const config = this.tspmInstance.processConfigs.get(id);
 | 
			
		||||
          if (!config) throw new Error(`Process ${id} not found`);
 | 
			
		||||
          return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Unknown format
 | 
			
		||||
        throw new Error(
 | 
			
		||||
          'Unsupported target format. Use numeric id (e.g. 1), id:<n> (e.g. id:1), or name:<label> (e.g. name:api).',
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Batch operations handlers
 | 
			
		||||
    this.ipcServer.onMessage(
 | 
			
		||||
      'startAll',
 | 
			
		||||
 
 | 
			
		||||
@@ -240,14 +240,6 @@ export interface AddResponse {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Remove (delete config and stop if running)
 | 
			
		||||
export interface RemoveRequest {
 | 
			
		||||
  id: ProcessId;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RemoveResponse {
 | 
			
		||||
  success: boolean;
 | 
			
		||||
  message?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Update (modify existing config)
 | 
			
		||||
export interface UpdateRequest {
 | 
			
		||||
@@ -260,6 +252,16 @@ export interface UpdateResponse {
 | 
			
		||||
  config: IProcessConfig;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Resolve a user-provided target (id:n or name:foo or numeric string) to a ProcessId
 | 
			
		||||
export interface ResolveTargetRequest {
 | 
			
		||||
  target: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ResolveTargetResponse {
 | 
			
		||||
  id: ProcessId;
 | 
			
		||||
  name?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Type mappings for methods
 | 
			
		||||
export type IpcMethodMap = {
 | 
			
		||||
  start: { request: StartRequest; response: StartResponse };
 | 
			
		||||
@@ -269,7 +271,6 @@ export type IpcMethodMap = {
 | 
			
		||||
  delete: { request: DeleteRequest; response: DeleteResponse };
 | 
			
		||||
  add: { request: AddRequest; response: AddResponse };
 | 
			
		||||
  update: { request: UpdateRequest; response: UpdateResponse };
 | 
			
		||||
  remove: { request: RemoveRequest; response: RemoveResponse };
 | 
			
		||||
  list: { request: ListRequest; response: ListResponse };
 | 
			
		||||
  describe: { request: DescribeRequest; response: DescribeResponse };
 | 
			
		||||
  getLogs: { request: GetLogsRequest; response: GetLogsResponse };
 | 
			
		||||
@@ -286,6 +287,7 @@ export type IpcMethodMap = {
 | 
			
		||||
    response: DaemonShutdownResponse;
 | 
			
		||||
  };
 | 
			
		||||
  heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse };
 | 
			
		||||
  resolveTarget: { request: ResolveTargetRequest; response: ResolveTargetResponse };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Helper type to extract request type for a method
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user