BREAKING CHANGE(cli): Add persistent process registration (tspm add), alias remove, and change start to use saved process IDs (breaking CLI behavior)
This commit is contained in:
		
							
								
								
									
										10
									
								
								changelog.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								changelog.md
									
									
									
									
									
								
							@@ -1,5 +1,15 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## 2025-08-29 - 4.0.0 - BREAKING CHANGE(cli)
 | 
			
		||||
Add persistent process registration (tspm add), alias remove, and change start to use saved process IDs (breaking CLI behavior)
 | 
			
		||||
 | 
			
		||||
- Add a new CLI command `tspm add` that registers a process configuration without starting it; daemon assigns a sequential numeric ID and returns the stored config.
 | 
			
		||||
- Change `tspm start` to accept a process ID and start the saved configuration instead of accepting ad-hoc commands/files. This is a breaking change to the CLI contract.
 | 
			
		||||
- Add `remove` as an alias for the existing `delete` command; both CLI and daemon now support `remove` which stops and deletes the stored process.
 | 
			
		||||
- Daemon and IPC protocol updated to support `add` and `remove` methods; shared IPC types extended accordingly.
 | 
			
		||||
- ProcessManager: implemented add() and getNextSequentialId() to persist configs and produce numeric IDs.
 | 
			
		||||
- CLI registration updated (registerIpcCommand) to accept multiple command names, enabling aliases for commands.
 | 
			
		||||
 | 
			
		||||
## 2025-08-29 - 3.1.3 - fix(client)
 | 
			
		||||
Improve IPC client robustness and daemon debug logging; update tests and package metadata
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,6 @@
 | 
			
		||||
 */
 | 
			
		||||
export const commitinfo = {
 | 
			
		||||
  name: '@git.zone/tspm',
 | 
			
		||||
  version: '3.1.3',
 | 
			
		||||
  version: '4.0.0',
 | 
			
		||||
  description: 'a no fuzz process manager'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										91
									
								
								ts/cli/commands/process/add.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								ts/cli/commands/process/add.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,91 @@
 | 
			
		||||
import * as plugins from '../../../plugins.js';
 | 
			
		||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
 | 
			
		||||
import type { CliArguments } from '../../types.js';
 | 
			
		||||
import { parseMemoryString, formatMemory } from '../../helpers/memory.js';
 | 
			
		||||
import { registerIpcCommand } from '../../registration/index.js';
 | 
			
		||||
 | 
			
		||||
export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
  registerIpcCommand(
 | 
			
		||||
    smartcli,
 | 
			
		||||
    'add',
 | 
			
		||||
    async (argvArg: CliArguments) => {
 | 
			
		||||
      const args = argvArg._.slice(1);
 | 
			
		||||
      if (args.length === 0) {
 | 
			
		||||
        console.error('Error: Please provide a command or .ts file');
 | 
			
		||||
        console.log('Usage: tspm add <command|file.ts> [options]');
 | 
			
		||||
        console.log('\nOptions:');
 | 
			
		||||
        console.log('  --name <name>         Optional name');
 | 
			
		||||
        console.log('  --memory <size>       Memory limit (e.g., 512MB, 2GB)');
 | 
			
		||||
        console.log('  --cwd <path>          Working directory');
 | 
			
		||||
        console.log('  --watch               Watch for file changes');
 | 
			
		||||
        console.log('  --watch-paths <paths> Comma-separated paths');
 | 
			
		||||
        console.log('  --autorestart         Auto-restart on crash (default true)');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const script = args.join(' ');
 | 
			
		||||
      const projectDir = argvArg.cwd || process.cwd();
 | 
			
		||||
      const memoryLimit = argvArg.memory
 | 
			
		||||
        ? parseMemoryString(argvArg.memory)
 | 
			
		||||
        : 512 * 1024 * 1024;
 | 
			
		||||
 | 
			
		||||
      // Resolve .ts single-file execution via tsx if needed
 | 
			
		||||
      const parts = script.split(' ');
 | 
			
		||||
      const first = parts[0];
 | 
			
		||||
      let command = script;
 | 
			
		||||
      let cmdArgs: string[] | undefined;
 | 
			
		||||
      if (parts.length === 1 && first.endsWith('.ts')) {
 | 
			
		||||
        try {
 | 
			
		||||
          const { createRequire } = await import('module');
 | 
			
		||||
          const require = createRequire(import.meta.url);
 | 
			
		||||
          const tsxPath = require.resolve('tsx/dist/cli.mjs');
 | 
			
		||||
          const filePath = plugins.path.isAbsolute(first)
 | 
			
		||||
            ? first
 | 
			
		||||
            : plugins.path.join(projectDir, first);
 | 
			
		||||
          command = tsxPath;
 | 
			
		||||
          cmdArgs = [filePath];
 | 
			
		||||
        } catch {
 | 
			
		||||
          command = 'tsx';
 | 
			
		||||
          cmdArgs = [first];
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const name = argvArg.name || script;
 | 
			
		||||
      const watch = argvArg.watch || false;
 | 
			
		||||
      const autorestart = argvArg.autorestart !== false;
 | 
			
		||||
      const watchPaths = argvArg.watchPaths
 | 
			
		||||
        ? typeof argvArg.watchPaths === 'string'
 | 
			
		||||
          ? (argvArg.watchPaths as string).split(',')
 | 
			
		||||
          : argvArg.watchPaths
 | 
			
		||||
        : undefined;
 | 
			
		||||
 | 
			
		||||
      console.log('Adding process configuration:');
 | 
			
		||||
      console.log(`  Command: ${script}${parts.length === 1 && first.endsWith('.ts') ? ' (via tsx)' : ''}`);
 | 
			
		||||
      console.log(`  Directory: ${projectDir}`);
 | 
			
		||||
      console.log(`  Memory limit: ${formatMemory(memoryLimit)}`);
 | 
			
		||||
      console.log(`  Auto-restart: ${autorestart}`);
 | 
			
		||||
      if (watch) {
 | 
			
		||||
        console.log(`  Watch: enabled`);
 | 
			
		||||
        if (watchPaths) console.log(`  Watch paths: ${watchPaths.join(',')}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const response = await tspmIpcClient.request('add', {
 | 
			
		||||
        config: {
 | 
			
		||||
          name,
 | 
			
		||||
          command,
 | 
			
		||||
          args: cmdArgs,
 | 
			
		||||
          projectDir,
 | 
			
		||||
          memoryLimitBytes: memoryLimit,
 | 
			
		||||
          autorestart,
 | 
			
		||||
          watch,
 | 
			
		||||
          watchPaths,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      console.log('✓ Added');
 | 
			
		||||
      console.log(`  Assigned ID: ${response.id}`);
 | 
			
		||||
    },
 | 
			
		||||
    { actionLabel: 'add process config' },
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -6,24 +6,27 @@ import { registerIpcCommand } from '../../registration/index.js';
 | 
			
		||||
export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
  registerIpcCommand(
 | 
			
		||||
    smartcli,
 | 
			
		||||
    'delete',
 | 
			
		||||
    ['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>');
 | 
			
		||||
        console.log('Usage: tspm delete <id> | tspm remove <id>');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      console.log(`Deleting process: ${id}`);
 | 
			
		||||
      const response = await tspmIpcClient.request('delete', { id });
 | 
			
		||||
      // 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);
 | 
			
		||||
 | 
			
		||||
      if (response.success) {
 | 
			
		||||
        console.log(`✓ ${response.message}`);
 | 
			
		||||
        console.log(`✓ ${response.message || (useRemove ? 'Removed successfully' : 'Deleted successfully')}`);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.error(`✗ Failed to delete process: ${response.message}`);
 | 
			
		||||
        console.error(`✗ Failed to ${useRemove ? 'remove' : 'delete'} process: ${response.message}`);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    { actionLabel: 'delete process' },
 | 
			
		||||
    { actionLabel: 'delete/remove process' },
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,108 +10,22 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
    smartcli,
 | 
			
		||||
    'start',
 | 
			
		||||
    async (argvArg: CliArguments) => {
 | 
			
		||||
      // Get all arguments after 'start' command
 | 
			
		||||
      const commandArgs = argvArg._.slice(1);
 | 
			
		||||
      if (commandArgs.length === 0) {
 | 
			
		||||
        console.error('Error: Please provide a command to run');
 | 
			
		||||
        console.log('Usage: tspm start <command> [options]');
 | 
			
		||||
        console.log('\nExamples:');
 | 
			
		||||
        console.log('  tspm start "npm run dev"');
 | 
			
		||||
        console.log('  tspm start pnpm start');
 | 
			
		||||
        console.log('  tspm start node server.js');
 | 
			
		||||
        console.log('  tspm start script.ts');
 | 
			
		||||
        console.log('\nOptions:');
 | 
			
		||||
        console.log('  --name <name>         Name for the process');
 | 
			
		||||
        console.log(
 | 
			
		||||
          '  --memory <size>       Memory limit (e.g., "512MB", "2GB")',
 | 
			
		||||
        );
 | 
			
		||||
        console.log('  --cwd <path>          Working directory');
 | 
			
		||||
        console.log(
 | 
			
		||||
          '  --watch               Watch for file changes and restart',
 | 
			
		||||
        );
 | 
			
		||||
        console.log('  --watch-paths <paths> Comma-separated paths to watch');
 | 
			
		||||
        console.log('  --autorestart         Auto-restart on crash');
 | 
			
		||||
      const id = argvArg._[1];
 | 
			
		||||
      if (!id) {
 | 
			
		||||
        console.error('Error: Please provide a process ID to start');
 | 
			
		||||
        console.log('Usage: tspm start <id>');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      // Join all command parts to form the full command
 | 
			
		||||
      const script = commandArgs.join(' ');
 | 
			
		||||
 | 
			
		||||
      const memoryLimit = argvArg.memory
 | 
			
		||||
        ? parseMemoryString(argvArg.memory)
 | 
			
		||||
        : 512 * 1024 * 1024;
 | 
			
		||||
      const projectDir = argvArg.cwd || process.cwd();
 | 
			
		||||
 | 
			
		||||
      // Parse the command to determine if we need to handle .ts files
 | 
			
		||||
      let actualCommand: string;
 | 
			
		||||
      let processArgs: string[] | undefined = undefined;
 | 
			
		||||
      
 | 
			
		||||
      // Split the script to check if it's a single .ts file or a full command
 | 
			
		||||
      const scriptParts = script.split(' ');
 | 
			
		||||
      const firstPart = scriptParts[0];
 | 
			
		||||
      
 | 
			
		||||
      // Check if this is a direct .ts file execution (single argument ending in .ts)
 | 
			
		||||
      if (scriptParts.length === 1 && firstPart.endsWith('.ts')) {
 | 
			
		||||
        try {
 | 
			
		||||
          const tsxPath = await (async () => {
 | 
			
		||||
            const { createRequire } = await import('module');
 | 
			
		||||
            const require = createRequire(import.meta.url);
 | 
			
		||||
            return require.resolve('tsx/dist/cli.mjs');
 | 
			
		||||
          })();
 | 
			
		||||
 | 
			
		||||
          const scriptPath = plugins.path.isAbsolute(firstPart)
 | 
			
		||||
            ? firstPart
 | 
			
		||||
            : plugins.path.join(projectDir, firstPart);
 | 
			
		||||
          actualCommand = tsxPath;
 | 
			
		||||
          processArgs = [scriptPath];
 | 
			
		||||
        } catch {
 | 
			
		||||
          actualCommand = 'tsx';
 | 
			
		||||
          processArgs = [firstPart];
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // For multi-word commands, use the entire script as the command
 | 
			
		||||
        // This handles cases like "pnpm start", "npm run dev", etc.
 | 
			
		||||
        actualCommand = script;
 | 
			
		||||
        processArgs = undefined;
 | 
			
		||||
      const desc = await tspmIpcClient.request('describe', { id }).catch(() => null);
 | 
			
		||||
      if (!desc) {
 | 
			
		||||
        console.error(`Process with id '${id}' not found. Use 'tspm add' first.`);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const name = argvArg.name || script;
 | 
			
		||||
      const watch = argvArg.watch || false;
 | 
			
		||||
      const autorestart = argvArg.autorestart !== false; // default true
 | 
			
		||||
      const watchPaths = argvArg.watchPaths
 | 
			
		||||
        ? typeof argvArg.watchPaths === 'string'
 | 
			
		||||
          ? (argvArg.watchPaths as string).split(',')
 | 
			
		||||
          : argvArg.watchPaths
 | 
			
		||||
        : undefined;
 | 
			
		||||
 | 
			
		||||
      const processConfig: IProcessConfig = {
 | 
			
		||||
        id: name.replace(/[^a-zA-Z0-9-_]/g, '_'),
 | 
			
		||||
        name,
 | 
			
		||||
        command: actualCommand,
 | 
			
		||||
        args: processArgs,
 | 
			
		||||
        projectDir,
 | 
			
		||||
        memoryLimitBytes: memoryLimit,
 | 
			
		||||
        autorestart,
 | 
			
		||||
        watch,
 | 
			
		||||
        watchPaths,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      console.log(`Starting process: ${name}`);
 | 
			
		||||
      console.log(
 | 
			
		||||
        `  Command: ${script}${scriptParts.length === 1 && firstPart.endsWith('.ts') ? ' (via tsx)' : ''}`,
 | 
			
		||||
      );
 | 
			
		||||
      console.log(`  Directory: ${projectDir}`);
 | 
			
		||||
      console.log(`  Memory limit: ${formatMemory(memoryLimit)}`);
 | 
			
		||||
      console.log(`  Auto-restart: ${autorestart}`);
 | 
			
		||||
      if (watch) {
 | 
			
		||||
        console.log(`  Watch mode: enabled`);
 | 
			
		||||
        if (watchPaths) console.log(`  Watch paths: ${watchPaths.join(', ')}`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const response = await tspmIpcClient.request('start', {
 | 
			
		||||
        config: processConfig,
 | 
			
		||||
      });
 | 
			
		||||
      console.log(`✓ Process started successfully`);
 | 
			
		||||
      console.log(`Starting process id ${id} (${desc.config.name || id})...`);
 | 
			
		||||
      const response = await tspmIpcClient.request('start', { config: desc.config });
 | 
			
		||||
      console.log('✓ Process started');
 | 
			
		||||
      console.log(`  ID: ${response.processId}`);
 | 
			
		||||
      console.log(`  PID: ${response.pid || 'N/A'}`);
 | 
			
		||||
      console.log(`  Status: ${response.status}`);
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js';
 | 
			
		||||
// Import command registration functions
 | 
			
		||||
import { registerDefaultCommand } from './commands/default.js';
 | 
			
		||||
import { registerStartCommand } from './commands/process/start.js';
 | 
			
		||||
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';
 | 
			
		||||
@@ -43,6 +44,7 @@ export const run = async (): Promise<void> => {
 | 
			
		||||
  registerDefaultCommand(smartcliInstance);
 | 
			
		||||
 | 
			
		||||
  // Process commands
 | 
			
		||||
  registerAddCommand(smartcliInstance);
 | 
			
		||||
  registerStartCommand(smartcliInstance);
 | 
			
		||||
  registerStopCommand(smartcliInstance);
 | 
			
		||||
  registerRestartCommand(smartcliInstance);
 | 
			
		||||
 
 | 
			
		||||
@@ -17,53 +17,56 @@ import { ensureDaemonOrHint } from './daemon-check.js';
 | 
			
		||||
 */
 | 
			
		||||
export function registerIpcCommand(
 | 
			
		||||
  smartcli: plugins.smartcli.Smartcli,
 | 
			
		||||
  name: string,
 | 
			
		||||
  name: string | string[],
 | 
			
		||||
  action: CommandAction,
 | 
			
		||||
  opts: IpcCommandOptions = {},
 | 
			
		||||
) {
 | 
			
		||||
  const { actionLabel = name, keepAlive = false, requireDaemon = true } = opts;
 | 
			
		||||
  const names = Array.isArray(name) ? name : [name];
 | 
			
		||||
  for (const singleName of names) {
 | 
			
		||||
    const { actionLabel = singleName, keepAlive = false, requireDaemon = true } = opts;
 | 
			
		||||
 | 
			
		||||
  smartcli.addCommand(name).subscribe({
 | 
			
		||||
    next: async (argv: CliArguments) => {
 | 
			
		||||
      // Early preflight for better UX
 | 
			
		||||
      const ok = await ensureDaemonOrHint(requireDaemon, actionLabel);
 | 
			
		||||
      if (!ok) {
 | 
			
		||||
        process.exit(1);
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Evaluate keepAlive - can be boolean or function
 | 
			
		||||
      const shouldKeepAlive =
 | 
			
		||||
        typeof keepAlive === 'function' ? keepAlive(argv) : keepAlive;
 | 
			
		||||
 | 
			
		||||
      if (shouldKeepAlive) {
 | 
			
		||||
        // Let action manage its own connection/cleanup lifecycle
 | 
			
		||||
        try {
 | 
			
		||||
          await action(argv);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          handleDaemonError(error, actionLabel);
 | 
			
		||||
    smartcli.addCommand(singleName).subscribe({
 | 
			
		||||
      next: async (argv: CliArguments) => {
 | 
			
		||||
        // Early preflight for better UX
 | 
			
		||||
        const ok = await ensureDaemonOrHint(requireDaemon, actionLabel);
 | 
			
		||||
        if (!ok) {
 | 
			
		||||
          process.exit(1);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // Auto-disconnect pattern for one-shot IPC commands
 | 
			
		||||
        await runIpcCommand(async () => {
 | 
			
		||||
 | 
			
		||||
        // Evaluate keepAlive - can be boolean or function
 | 
			
		||||
        const shouldKeepAlive =
 | 
			
		||||
          typeof keepAlive === 'function' ? keepAlive(argv) : keepAlive;
 | 
			
		||||
 | 
			
		||||
        if (shouldKeepAlive) {
 | 
			
		||||
          // Let action manage its own connection/cleanup lifecycle
 | 
			
		||||
          try {
 | 
			
		||||
            await action(argv);
 | 
			
		||||
          } catch (error) {
 | 
			
		||||
            handleDaemonError(error, actionLabel);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    error: (err) => {
 | 
			
		||||
      // Fallback error path (should be rare with try/catch in next)
 | 
			
		||||
      console.error(
 | 
			
		||||
        `Unexpected error in command "${name}":`,
 | 
			
		||||
        unknownError(err),
 | 
			
		||||
      );
 | 
			
		||||
      process.exit(1);
 | 
			
		||||
    },
 | 
			
		||||
    complete: () => {},
 | 
			
		||||
  });
 | 
			
		||||
        } else {
 | 
			
		||||
          // Auto-disconnect pattern for one-shot IPC commands
 | 
			
		||||
          await runIpcCommand(async () => {
 | 
			
		||||
            try {
 | 
			
		||||
              await action(argv);
 | 
			
		||||
            } catch (error) {
 | 
			
		||||
              handleDaemonError(error, actionLabel);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      error: (err) => {
 | 
			
		||||
        // Fallback error path (should be rare with try/catch in next)
 | 
			
		||||
        console.error(
 | 
			
		||||
          `Unexpected error in command "${singleName}":`,
 | 
			
		||||
          unknownError(err),
 | 
			
		||||
        );
 | 
			
		||||
        process.exit(1);
 | 
			
		||||
      },
 | 
			
		||||
      complete: () => {},
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -34,6 +34,42 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
    this.loadProcessConfigs();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Add a process configuration without starting it.
 | 
			
		||||
   * Returns the assigned numeric sequential id as string.
 | 
			
		||||
   */
 | 
			
		||||
  public async add(configInput: Omit<IProcessConfig, 'id'> & { id?: string }): Promise<string> {
 | 
			
		||||
    // Determine next numeric id
 | 
			
		||||
    const nextId = this.getNextSequentialId();
 | 
			
		||||
 | 
			
		||||
    const config: IProcessConfig = {
 | 
			
		||||
      id: String(nextId),
 | 
			
		||||
      name: configInput.name || `process-${nextId}`,
 | 
			
		||||
      command: configInput.command,
 | 
			
		||||
      args: configInput.args,
 | 
			
		||||
      projectDir: configInput.projectDir,
 | 
			
		||||
      memoryLimitBytes: configInput.memoryLimitBytes || 512 * 1024 * 1024,
 | 
			
		||||
      monitorIntervalMs: configInput.monitorIntervalMs,
 | 
			
		||||
      env: configInput.env,
 | 
			
		||||
      logBufferSize: configInput.logBufferSize,
 | 
			
		||||
      autorestart: configInput.autorestart ?? true,
 | 
			
		||||
      watch: configInput.watch,
 | 
			
		||||
      watchPaths: configInput.watchPaths,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Store config and initial info
 | 
			
		||||
    this.processConfigs.set(config.id, config);
 | 
			
		||||
    this.processInfo.set(config.id, {
 | 
			
		||||
      id: config.id,
 | 
			
		||||
      status: 'stopped',
 | 
			
		||||
      memory: 0,
 | 
			
		||||
      restarts: 0,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await this.saveProcessConfigs();
 | 
			
		||||
    return config.id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Start a new process with the given configuration
 | 
			
		||||
   */
 | 
			
		||||
@@ -342,6 +378,20 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Compute next sequential numeric id based on existing configs
 | 
			
		||||
   */
 | 
			
		||||
  private getNextSequentialId(): number {
 | 
			
		||||
    let maxId = 0;
 | 
			
		||||
    for (const id of this.processConfigs.keys()) {
 | 
			
		||||
      const n = parseInt(id, 10);
 | 
			
		||||
      if (!isNaN(n)) {
 | 
			
		||||
        maxId = Math.max(maxId, n);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return maxId + 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Save all process configurations to config storage
 | 
			
		||||
   */
 | 
			
		||||
 
 | 
			
		||||
@@ -171,6 +171,31 @@ export class TspmDaemon {
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Query handlers
 | 
			
		||||
    this.ipcServer.onMessage(
 | 
			
		||||
      'add',
 | 
			
		||||
      async (request: RequestForMethod<'add'>) => {
 | 
			
		||||
        try {
 | 
			
		||||
          const id = await this.tspmInstance.add(request.config as any);
 | 
			
		||||
          const config = this.tspmInstance.processConfigs.get(id)!;
 | 
			
		||||
          return { id, config };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          throw new Error(`Failed to add process: ${error.message}`);
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.ipcServer.onMessage(
 | 
			
		||||
      'remove',
 | 
			
		||||
      async (request: RequestForMethod<'remove'>) => {
 | 
			
		||||
        try {
 | 
			
		||||
          await this.tspmInstance.delete(request.id);
 | 
			
		||||
          return { success: true, message: `Process ${request.id} deleted successfully` };
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          throw new Error(`Failed to remove process: ${error.message}`);
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.ipcServer.onMessage(
 | 
			
		||||
      'list',
 | 
			
		||||
      async (request: RequestForMethod<'list'>) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -200,12 +200,35 @@ export interface HeartbeatResponse {
 | 
			
		||||
  status: 'healthy' | 'degraded';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Add (register config without starting)
 | 
			
		||||
export interface AddRequest {
 | 
			
		||||
  // Optional id is ignored server-side if present; server assigns sequential id
 | 
			
		||||
  config: Omit<IProcessConfig, 'id'> & { id?: string };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AddResponse {
 | 
			
		||||
  id: string;
 | 
			
		||||
  config: IProcessConfig;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Remove (delete config and stop if running)
 | 
			
		||||
export interface RemoveRequest {
 | 
			
		||||
  id: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RemoveResponse {
 | 
			
		||||
  success: boolean;
 | 
			
		||||
  message?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Type mappings for methods
 | 
			
		||||
export type IpcMethodMap = {
 | 
			
		||||
  start: { request: StartRequest; response: StartResponse };
 | 
			
		||||
  stop: { request: StopRequest; response: StopResponse };
 | 
			
		||||
  restart: { request: RestartRequest; response: RestartResponse };
 | 
			
		||||
  delete: { request: DeleteRequest; response: DeleteResponse };
 | 
			
		||||
  add: { request: AddRequest; response: AddResponse };
 | 
			
		||||
  remove: { request: RemoveRequest; response: RemoveResponse };
 | 
			
		||||
  list: { request: ListRequest; response: ListResponse };
 | 
			
		||||
  describe: { request: DescribeRequest; response: DescribeResponse };
 | 
			
		||||
  getLogs: { request: GetLogsRequest; response: GetLogsResponse };
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user