feat(cli): Add interactive edit flow to CLI and improve UX
This commit is contained in:
		@@ -1,5 +1,14 @@
 | 
			
		||||
# Changelog
 | 
			
		||||
 | 
			
		||||
## 2025-08-31 - 5.9.0 - feat(cli)
 | 
			
		||||
Add interactive edit flow to CLI and improve UX
 | 
			
		||||
 | 
			
		||||
- Add -i / --interactive flag to tspm add to open an interactive editor immediately after adding a process
 | 
			
		||||
- Implement interactiveEditProcess helper (smartinteract-based) to provide interactive editing for process configs
 | 
			
		||||
- Enable tspm edit to launch the interactive editor (replaces prior placeholder flow)
 | 
			
		||||
- Improve user-facing message when no processes are configured in tspm list
 | 
			
		||||
- Lower verbosity for missing saved configs on daemon startup (changed logger.info → logger.debug)
 | 
			
		||||
 | 
			
		||||
## 2025-08-31 - 5.8.0 - feat(core)
 | 
			
		||||
Add core TypeScript TSPM implementation: CLI, daemon, client, process management and tests
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -72,6 +72,7 @@ Add a new process configuration without starting it. This is the recommended way
 | 
			
		||||
- `--watch` - Enable file watching for auto-restart
 | 
			
		||||
- `--watch-paths <paths>` - Comma-separated paths to watch
 | 
			
		||||
- `--autorestart` - Auto-restart on crash (default: true)
 | 
			
		||||
- `-i, --interactive` - Enter interactive edit mode after adding
 | 
			
		||||
 | 
			
		||||
**Examples:**
 | 
			
		||||
```bash
 | 
			
		||||
@@ -86,6 +87,9 @@ tspm add "tsx watch src/index.ts" --name dev-server --watch --watch-paths "src,c
 | 
			
		||||
 | 
			
		||||
# Add without auto-restart
 | 
			
		||||
tspm add "node worker.js" --name one-time-job --autorestart false
 | 
			
		||||
 | 
			
		||||
# Add and immediately edit interactively
 | 
			
		||||
tspm add "node server.js" --name api -i
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
#### `tspm start <id|id:N|name:LABEL>`
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,6 @@
 | 
			
		||||
 */
 | 
			
		||||
export const commitinfo = {
 | 
			
		||||
  name: '@git.zone/tspm',
 | 
			
		||||
  version: '5.8.0',
 | 
			
		||||
  version: '5.9.0',
 | 
			
		||||
  description: 'a no fuzz process manager'
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
        console.log('  --watch               Watch for file changes');
 | 
			
		||||
        console.log('  --watch-paths <paths> Comma-separated paths');
 | 
			
		||||
        console.log('  --autorestart         Auto-restart on crash (default true)');
 | 
			
		||||
        console.log('  -i, --interactive     Enter interactive edit mode after adding');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@@ -29,6 +30,9 @@ export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
        ? parseMemoryString(argvArg.memory)
 | 
			
		||||
        : 512 * 1024 * 1024;
 | 
			
		||||
 | 
			
		||||
      // Check for interactive flag
 | 
			
		||||
      const isInteractive = argvArg.i || argvArg.interactive;
 | 
			
		||||
 | 
			
		||||
      // Resolve .ts single-file execution via tsx if needed
 | 
			
		||||
      const parts = script.split(' ');
 | 
			
		||||
      const first = parts[0];
 | 
			
		||||
@@ -112,6 +116,12 @@ export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
 | 
			
		||||
      console.log('✓ Added');
 | 
			
		||||
      console.log(`  Assigned ID: ${response.id}`);
 | 
			
		||||
 | 
			
		||||
      // If interactive flag is set, enter edit mode
 | 
			
		||||
      if (isInteractive) {
 | 
			
		||||
        const { interactiveEditProcess } = await import('../../helpers/interactive-edit.js');
 | 
			
		||||
        await interactiveEditProcess(response.id);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    { actionLabel: 'add process config' },
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -16,58 +16,12 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Resolve and load current config
 | 
			
		||||
      // Resolve the target to get the process ID
 | 
			
		||||
      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.');
 | 
			
		||||
      console.log('Current configuration:');
 | 
			
		||||
      console.log(`  Name: ${config.name}`);
 | 
			
		||||
      console.log(`  Command: ${config.command}`);
 | 
			
		||||
      console.log(`  Directory: ${config.projectDir}`);
 | 
			
		||||
      console.log(`  Memory: ${formatMemory(config.memoryLimitBytes)}`);
 | 
			
		||||
      console.log(`  Auto-restart: ${config.autorestart}`);
 | 
			
		||||
      console.log(`  Watch: ${config.watch ? 'enabled' : 'disabled'}`);
 | 
			
		||||
      
 | 
			
		||||
      // For now, just update environment variables to current
 | 
			
		||||
      const essentialEnvVars: NodeJS.ProcessEnv = {
 | 
			
		||||
        PATH: process.env.PATH || '',
 | 
			
		||||
        HOME: process.env.HOME,
 | 
			
		||||
        USER: process.env.USER,
 | 
			
		||||
        SHELL: process.env.SHELL,
 | 
			
		||||
        LANG: process.env.LANG,
 | 
			
		||||
        LC_ALL: process.env.LC_ALL,
 | 
			
		||||
        // Node.js specific
 | 
			
		||||
        NODE_ENV: process.env.NODE_ENV,
 | 
			
		||||
        NODE_PATH: process.env.NODE_PATH,
 | 
			
		||||
        // npm/pnpm/yarn paths
 | 
			
		||||
        npm_config_prefix: process.env.npm_config_prefix,
 | 
			
		||||
        // Include any TSPM_ prefixed vars
 | 
			
		||||
        ...Object.fromEntries(
 | 
			
		||||
          Object.entries(process.env).filter(([key]) => key.startsWith('TSPM_'))
 | 
			
		||||
        ),
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      // Remove undefined values
 | 
			
		||||
      Object.keys(essentialEnvVars).forEach(key => {
 | 
			
		||||
        if (essentialEnvVars[key] === undefined) {
 | 
			
		||||
          delete essentialEnvVars[key];
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      // Update environment variables
 | 
			
		||||
      const updates = {
 | 
			
		||||
        env: { ...(config.env || {}), ...essentialEnvVars }
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const updateResponse = await tspmIpcClient.request('update', {
 | 
			
		||||
        id: resolved.id,
 | 
			
		||||
        updates,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      console.log('✓ Environment variables updated');
 | 
			
		||||
      console.log('  Process configuration updated successfully');
 | 
			
		||||
      // Use the shared interactive edit function
 | 
			
		||||
      const { interactiveEditProcess } = await import('../../helpers/interactive-edit.js');
 | 
			
		||||
      await interactiveEditProcess(resolved.id);
 | 
			
		||||
    },
 | 
			
		||||
    { actionLabel: 'edit process config' },
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,9 @@ export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
 | 
			
		||||
      const processes = response.processes;
 | 
			
		||||
 | 
			
		||||
      if (processes.length === 0) {
 | 
			
		||||
        console.log('No processes running.');
 | 
			
		||||
        console.log('No processes configured.');
 | 
			
		||||
        console.log('Use "tspm add <command>" to add one, e.g.:');
 | 
			
		||||
        console.log('  tspm add "pnpm start"');
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										164
									
								
								ts/cli/helpers/interactive-edit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								ts/cli/helpers/interactive-edit.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,164 @@
 | 
			
		||||
import * as plugins from '../plugins.js';
 | 
			
		||||
import { tspmIpcClient } from '../../client/tspm.ipcclient.js';
 | 
			
		||||
import { formatMemory, parseMemoryString } from './memory.js';
 | 
			
		||||
 | 
			
		||||
export async function interactiveEditProcess(processId: number): Promise<void> {
 | 
			
		||||
  // Load current config
 | 
			
		||||
  const { config } = await tspmIpcClient.request('describe', { id: processId as any });
 | 
			
		||||
 | 
			
		||||
  // Create interactive prompts for editing
 | 
			
		||||
  const smartInteract = new plugins.smartinteract.SmartInteract([
 | 
			
		||||
    {
 | 
			
		||||
      name: 'name',
 | 
			
		||||
      type: 'input',
 | 
			
		||||
      message: 'Process name:',
 | 
			
		||||
      default: config.name,
 | 
			
		||||
      validate: (input: string) => {
 | 
			
		||||
        return input && input.trim() !== '';
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'command',
 | 
			
		||||
      type: 'input',
 | 
			
		||||
      message: 'Command to execute:',
 | 
			
		||||
      default: config.command,
 | 
			
		||||
      validate: (input: string) => {
 | 
			
		||||
        return input && input.trim() !== '';
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'projectDir',
 | 
			
		||||
      type: 'input',
 | 
			
		||||
      message: 'Working directory:',
 | 
			
		||||
      default: config.projectDir,
 | 
			
		||||
      validate: (input: string) => {
 | 
			
		||||
        return input && input.trim() !== '';
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'memoryLimit',
 | 
			
		||||
      type: 'input',
 | 
			
		||||
      message: 'Memory limit (e.g., 512M, 1G):',
 | 
			
		||||
      default: formatMemory(config.memoryLimitBytes),
 | 
			
		||||
      validate: (input: string) => {
 | 
			
		||||
        const parsed = parseMemoryString(input);
 | 
			
		||||
        return parsed !== null;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'autorestart',
 | 
			
		||||
      type: 'confirm',
 | 
			
		||||
      message: 'Enable auto-restart on failure?',
 | 
			
		||||
      default: config.autorestart
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'watch',
 | 
			
		||||
      type: 'confirm',
 | 
			
		||||
      message: 'Enable file watching for auto-restart?',
 | 
			
		||||
      default: config.watch || false
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      name: 'updateEnv',
 | 
			
		||||
      type: 'confirm',
 | 
			
		||||
      message: 'Update environment variables to current environment?',
 | 
			
		||||
      default: true
 | 
			
		||||
    }
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  console.log('\n📝 Edit Process Configuration');
 | 
			
		||||
  console.log(`   Process ID: ${processId}`);
 | 
			
		||||
  console.log('   (Press Enter to keep current values)\n');
 | 
			
		||||
 | 
			
		||||
  // Run the interactive prompts
 | 
			
		||||
  const answerBucket = await smartInteract.runQueue();
 | 
			
		||||
 | 
			
		||||
  // Get answers from the bucket
 | 
			
		||||
  const name = answerBucket.getAnswerFor('name');
 | 
			
		||||
  const command = answerBucket.getAnswerFor('command');
 | 
			
		||||
  const projectDir = answerBucket.getAnswerFor('projectDir');
 | 
			
		||||
  const memoryLimit = answerBucket.getAnswerFor('memoryLimit');
 | 
			
		||||
  const autorestart = answerBucket.getAnswerFor('autorestart');
 | 
			
		||||
  const watch = answerBucket.getAnswerFor('watch');
 | 
			
		||||
  const updateEnv = answerBucket.getAnswerFor('updateEnv');
 | 
			
		||||
 | 
			
		||||
  // Prepare updates object
 | 
			
		||||
  const updates: any = {};
 | 
			
		||||
  
 | 
			
		||||
  // Check what has changed
 | 
			
		||||
  if (name !== config.name) {
 | 
			
		||||
    updates.name = name;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (command !== config.command) {
 | 
			
		||||
    updates.command = command;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (projectDir !== config.projectDir) {
 | 
			
		||||
    updates.projectDir = projectDir;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  const newMemoryBytes = parseMemoryString(memoryLimit);
 | 
			
		||||
  if (newMemoryBytes !== config.memoryLimitBytes) {
 | 
			
		||||
    updates.memoryLimitBytes = newMemoryBytes;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (autorestart !== config.autorestart) {
 | 
			
		||||
    updates.autorestart = autorestart;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  if (watch !== config.watch) {
 | 
			
		||||
    updates.watch = watch;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle environment variables update if requested
 | 
			
		||||
  if (updateEnv) {
 | 
			
		||||
    const essentialEnvVars: NodeJS.ProcessEnv = {
 | 
			
		||||
      PATH: process.env.PATH || '',
 | 
			
		||||
      HOME: process.env.HOME,
 | 
			
		||||
      USER: process.env.USER,
 | 
			
		||||
      SHELL: process.env.SHELL,
 | 
			
		||||
      LANG: process.env.LANG,
 | 
			
		||||
      LC_ALL: process.env.LC_ALL,
 | 
			
		||||
      // Node.js specific
 | 
			
		||||
      NODE_ENV: process.env.NODE_ENV,
 | 
			
		||||
      NODE_PATH: process.env.NODE_PATH,
 | 
			
		||||
      // npm/pnpm/yarn paths
 | 
			
		||||
      npm_config_prefix: process.env.npm_config_prefix,
 | 
			
		||||
      // Include any TSPM_ prefixed vars
 | 
			
		||||
      ...Object.fromEntries(
 | 
			
		||||
        Object.entries(process.env).filter(([key]) => key.startsWith('TSPM_'))
 | 
			
		||||
      ),
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Remove undefined values
 | 
			
		||||
    Object.keys(essentialEnvVars).forEach(key => {
 | 
			
		||||
      if (essentialEnvVars[key] === undefined) {
 | 
			
		||||
        delete essentialEnvVars[key];
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    updates.env = { ...(config.env || {}), ...essentialEnvVars };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Only update if there are changes
 | 
			
		||||
  if (Object.keys(updates).length === 0) {
 | 
			
		||||
    console.log('\n✓ No changes made');
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Send updates to daemon
 | 
			
		||||
  await tspmIpcClient.request('update', {
 | 
			
		||||
    id: processId as any,
 | 
			
		||||
    updates,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  // Display what was updated
 | 
			
		||||
  console.log('\n✓ Process configuration updated successfully');
 | 
			
		||||
  if (updates.name) console.log(`  Name: ${updates.name}`);
 | 
			
		||||
  if (updates.command) console.log(`  Command: ${updates.command}`);
 | 
			
		||||
  if (updates.projectDir) console.log(`  Directory: ${updates.projectDir}`);
 | 
			
		||||
  if (updates.memoryLimitBytes) console.log(`  Memory limit: ${formatMemory(updates.memoryLimitBytes)}`);
 | 
			
		||||
  if (updates.autorestart !== undefined) console.log(`  Auto-restart: ${updates.autorestart}`);
 | 
			
		||||
  if (updates.watch !== undefined) console.log(`  Watch: ${updates.watch ? 'enabled' : 'disabled'}`);
 | 
			
		||||
  if (updateEnv) console.log('  Environment variables: updated');
 | 
			
		||||
}
 | 
			
		||||
@@ -739,7 +739,8 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
          throw configError;
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        this.logger.info('No saved process configurations found');
 | 
			
		||||
        // First run / no configs yet — keep this quiet unless debugging
 | 
			
		||||
        this.logger.debug('No saved process configurations found');
 | 
			
		||||
      }
 | 
			
		||||
    } catch (error: Error | unknown) {
 | 
			
		||||
      // Only throw if it's not the "no configs found" case
 | 
			
		||||
@@ -748,9 +749,7 @@ export class ProcessManager extends EventEmitter {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // If no configs found or error reading, just continue with empty configs
 | 
			
		||||
      this.logger.info(
 | 
			
		||||
        'No saved process configurations found or error reading them',
 | 
			
		||||
      );
 | 
			
		||||
      this.logger.debug('No saved process configurations found or error reading them');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user