feat(cli): Add interactive edit flow to CLI and improve UX
This commit is contained in:
@@ -1,5 +1,14 @@
|
|||||||
# Changelog
|
# 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)
|
## 2025-08-31 - 5.8.0 - feat(core)
|
||||||
Add core TypeScript TSPM implementation: CLI, daemon, client, process management and tests
|
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` - Enable file watching for auto-restart
|
||||||
- `--watch-paths <paths>` - Comma-separated paths to watch
|
- `--watch-paths <paths>` - Comma-separated paths to watch
|
||||||
- `--autorestart` - Auto-restart on crash (default: true)
|
- `--autorestart` - Auto-restart on crash (default: true)
|
||||||
|
- `-i, --interactive` - Enter interactive edit mode after adding
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
```bash
|
```bash
|
||||||
@@ -86,6 +87,9 @@ tspm add "tsx watch src/index.ts" --name dev-server --watch --watch-paths "src,c
|
|||||||
|
|
||||||
# Add without auto-restart
|
# Add without auto-restart
|
||||||
tspm add "node worker.js" --name one-time-job --autorestart false
|
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>`
|
#### `tspm start <id|id:N|name:LABEL>`
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tspm',
|
name: '@git.zone/tspm',
|
||||||
version: '5.8.0',
|
version: '5.9.0',
|
||||||
description: 'a no fuzz process manager'
|
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 Watch for file changes');
|
||||||
console.log(' --watch-paths <paths> Comma-separated paths');
|
console.log(' --watch-paths <paths> Comma-separated paths');
|
||||||
console.log(' --autorestart Auto-restart on crash (default true)');
|
console.log(' --autorestart Auto-restart on crash (default true)');
|
||||||
|
console.log(' -i, --interactive Enter interactive edit mode after adding');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +30,9 @@ export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
? parseMemoryString(argvArg.memory)
|
? parseMemoryString(argvArg.memory)
|
||||||
: 512 * 1024 * 1024;
|
: 512 * 1024 * 1024;
|
||||||
|
|
||||||
|
// Check for interactive flag
|
||||||
|
const isInteractive = argvArg.i || argvArg.interactive;
|
||||||
|
|
||||||
// Resolve .ts single-file execution via tsx if needed
|
// Resolve .ts single-file execution via tsx if needed
|
||||||
const parts = script.split(' ');
|
const parts = script.split(' ');
|
||||||
const first = parts[0];
|
const first = parts[0];
|
||||||
@@ -112,6 +116,12 @@ export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
|
|
||||||
console.log('✓ Added');
|
console.log('✓ Added');
|
||||||
console.log(` Assigned ID: ${response.id}`);
|
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' },
|
{ actionLabel: 'add process config' },
|
||||||
);
|
);
|
||||||
|
@@ -16,58 +16,12 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve and load current config
|
// Resolve the target to get the process ID
|
||||||
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
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
|
// Use the shared interactive edit function
|
||||||
const essentialEnvVars: NodeJS.ProcessEnv = {
|
const { interactiveEditProcess } = await import('../../helpers/interactive-edit.js');
|
||||||
PATH: process.env.PATH || '',
|
await interactiveEditProcess(resolved.id);
|
||||||
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');
|
|
||||||
},
|
},
|
||||||
{ actionLabel: 'edit process config' },
|
{ actionLabel: 'edit process config' },
|
||||||
);
|
);
|
||||||
|
@@ -14,7 +14,9 @@ export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
const processes = response.processes;
|
const processes = response.processes;
|
||||||
|
|
||||||
if (processes.length === 0) {
|
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;
|
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;
|
throw configError;
|
||||||
}
|
}
|
||||||
} else {
|
} 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) {
|
} catch (error: Error | unknown) {
|
||||||
// Only throw if it's not the "no configs found" case
|
// 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
|
// If no configs found or error reading, just continue with empty configs
|
||||||
this.logger.info(
|
this.logger.debug('No saved process configurations found or error reading them');
|
||||||
'No saved process configurations found or error reading them',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user