feat(cli/daemon/processmonitor): Add flexible target resolution and search command; improve restart/backoff and error handling

This commit is contained in:
2025-08-30 16:55:10 +00:00
parent 22a43204d4
commit ebc20a9232
17 changed files with 327 additions and 109 deletions

View File

@@ -1,5 +1,17 @@
# Changelog
## 2025-08-30 - 5.3.0 - feat(cli/daemon/processmonitor)
Add flexible target resolution and search command; improve restart/backoff and error handling
- Add new cli command `search` to find processes by id or name fragment.
- Allow flexible process targets in CLI commands (accepts numeric id, id:<n>, or name:<label>) for start/stop/restart/delete/describe/logs/edit commands.
- Introduce a new daemon IPC method `resolveTarget` to normalize user-provided targets to ProcessId (supports id:<n>, name:<label>, or bare numeric id).
- Keep `remove` as a CLI alias but daemon exposes `delete` only; CLI resolves targets and always calls daemon `delete`.
- Implement scheduled restart/backoff in ProcessMonitor with incremental debounce, max retries, and a 1-hour reset window.
- Emit a `failed` event from ProcessMonitor when max restart attempts are exceeded; ProcessManager listens and marks processes as `errored` and clears pid.
- Ensure desired state is set to `stopped` before deleting a process to avoid race conditions.
- Improve cli output messages to include resolved names alongside numeric ids where available.
## 2025-08-30 - 5.2.0 - feat(cli)
Preserve CLI environment when adding processes, simplify edit flow, and refresh README docs

View File

@@ -38,21 +38,23 @@ npm install --save-dev @git.zone/tspm
# Add a process (creates config without starting)
tspm add "node server.js" --name my-server --memory 1GB
# Start the process
tspm start my-server
# Start the process (by name or id)
tspm start name:my-server
# or
tspm start id:1
# Or add and start in one go
tspm add "node app.js" --name my-app
tspm start my-app
tspm start name:my-app
# List all processes
tspm list
# View logs
tspm logs my-app
tspm logs name:my-app
# Stop a process
tspm stop my-app
tspm stop name:my-app
```
## 📋 Commands
@@ -86,38 +88,38 @@ tspm add "tsx watch src/index.ts" --name dev-server --watch --watch-paths "src,c
tspm add "node worker.js" --name one-time-job --autorestart false
```
#### `tspm start <id>`
#### `tspm start <id|id:N|name:LABEL>`
Start a previously added process by its ID or name.
```bash
tspm start my-server
tspm start 1 # Can also use numeric ID
tspm start name:my-server
tspm start id:1 # Or a bare numeric id: tspm start 1
```
#### `tspm stop <id>`
#### `tspm stop <id|id:N|name:LABEL>`
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
```bash
tspm stop my-server
tspm stop name:my-server
```
#### `tspm restart <id>`
#### `tspm restart <id|id:N|name:LABEL>`
Stop and restart a process with the same configuration.
```bash
tspm restart my-server
tspm restart name:my-server
```
#### `tspm delete <id>` / `tspm remove <id>`
#### `tspm delete <id|id:N|name:LABEL>` / `tspm remove <id|id:N|name:LABEL>`
Stop and remove a process from TSPM management. Also deletes persisted logs.
```bash
tspm delete old-server
tspm remove old-server # Alias for delete
tspm delete name:old-server
tspm remove name:old-server # Alias for delete (daemon handles delete)
```
#### `tspm edit <id>`
@@ -148,12 +150,12 @@ tspm list
└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘
```
#### `tspm describe <id>`
#### `tspm describe <id|id:N|name:LABEL>`
Get detailed information about a specific process.
```bash
tspm describe my-server
tspm describe name:my-server
# Output:
Process Details: my-server
@@ -173,7 +175,7 @@ Auto-restart: true
Watch: disabled
```
#### `tspm logs <id> [options]`
#### `tspm logs <id|id:N|name:LABEL> [options]`
View process logs (stdout and stderr combined).
@@ -183,13 +185,13 @@ View process logs (stdout and stderr combined).
```bash
# View last 50 lines
tspm logs my-server
tspm logs name:my-server
# View last 100 lines
tspm logs my-server --lines 100
tspm logs name:my-server --lines 100
# Follow logs in real-time
tspm logs my-server --follow
tspm logs name:my-server --follow
```
### Batch Operations
@@ -496,7 +498,31 @@ Common issues:
- **"Daemon not running"**: Run `tspm daemon start` or `tspm enable`
- **"Permission denied"**: Check socket permissions in `~/.tspm/`
- **"Process won't start"**: Check logs with `tspm logs <id>`
- **"Process won't start"**: Check logs with `tspm logs <id|id:N|name:LABEL>`
## 🎯 Targeting Processes (IDs and Names)
Most process commands accept the following target formats:
- Numeric ID: `tspm start 1`
- Explicit ID: `tspm start id:1`
- Explicit name: `tspm start name:api-server`
Notes:
- Names must be used with the `name:` prefix.
- If multiple processes share the same name, the CLI will report the ambiguous matches. Use `id:N` to disambiguate.
- Use `tspm search <query>` to discover IDs by name or ID fragments.
### `tspm search <query>`
Search processes by name or ID substring and print matching IDs (and names when available):
```bash
tspm search api
# Matches for "api":
# - id:3 name:api-server
```
- **"Memory limit exceeded"**: Increase limit with `tspm edit <id>`
## 🤝 Why Choose TSPM?
@@ -515,6 +541,20 @@ Common issues:
### Perfect For
### Restart Backoff and Failure Handling
TSPM automatically restarts crashed processes with an incremental backoff:
- Debounce delay grows linearly from 1s up to 10s for consecutive retries.
- After the 10th retry, the process is marked as failed (status: "errored") and auto-restarts stop.
- The retry counter resets if no retry happens for 1 hour since the last attempt.
You can manually restart a failed process at any time:
```bash
tspm restart id:1
```
- 🚀 **Production Node.js apps** - Reliable process management
- 🔧 **Microservices** - Manage multiple services easily
- 👨💻 **Development** - File watching and auto-restart
@@ -538,4 +578,4 @@ Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -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'
}

View File

@@ -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');

View File

@@ -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' },

View File

@@ -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'}`);

View File

@@ -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' },
);
}

View File

@@ -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;

View File

@@ -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}`);
},

View 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' },
);
}

View File

@@ -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}`);
},

View File

@@ -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}`);

View File

@@ -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);

View File

@@ -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(

View File

@@ -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.

View File

@@ -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',

View File

@@ -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