Compare commits

...

4 Commits

Author SHA1 Message Date
e507b75c40 4.4.2
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 3m58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 21:22:03 +00:00
97a8377a75 fix(daemon): Fix daemon IPC id handling, reload configs on demand and correct CLI daemon start path 2025-08-29 21:22:03 +00:00
3676bff04c 4.4.1
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 3m58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 21:10:01 +00:00
dfe0677cab fix(cli): Use server-side start-by-id flow for starting processes 2025-08-29 21:10:01 +00:00
9 changed files with 110 additions and 24 deletions

View File

@@ -1,5 +1,21 @@
# Changelog # Changelog
## 2025-08-29 - 4.4.2 - fix(daemon)
Fix daemon IPC id handling, reload configs on demand and correct CLI daemon start path
- Normalize process IDs in daemon IPC handlers (trim strings) to avoid lookup mismatches
- Attempt to reload saved process configurations when a startById request cannot find a config (handles races/stale state)
- Use normalized IDs in responses and messages for stop/restart/delete/remove/describe handlers
- Fix CLI daemon start path to point at dist_ts/daemon/tspm.daemon.js when launching the background daemon
- Ensure the IPC client disconnects after showing CLI version/status to avoid leaked connections
## 2025-08-29 - 4.4.1 - fix(cli)
Use server-side start-by-id flow for starting processes
- CLI: 'tspm start <id>' now calls a new 'startById' IPC method instead of fetching the full config via 'describe' and submitting it back to 'start'.
- Daemon: Added server-side handler for 'startById' which resolves the stored process config and starts the process on the daemon.
- Protocol: Added StartByIdRequest/StartByIdResponse types and registered 'startById' in the IPC method map.
## 2025-08-29 - 4.4.0 - feat(daemon) ## 2025-08-29 - 4.4.0 - feat(daemon)
Persist desired process states and add daemon restart command Persist desired process states and add daemon restart command

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tspm", "name": "@git.zone/tspm",
"version": "4.4.0", "version": "4.4.2",
"private": false, "private": false,
"description": "a no fuzz process manager", "description": "a no fuzz process manager",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",

View File

@@ -3,6 +3,6 @@
*/ */
export const commitinfo = { export const commitinfo = {
name: '@git.zone/tspm', name: '@git.zone/tspm',
version: '4.4.0', version: '4.4.2',
description: 'a no fuzz process manager' description: 'a no fuzz process manager'
} }

View File

@@ -33,7 +33,8 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
const daemonScript = plugins.path.join( const daemonScript = plugins.path.join(
paths.packageDir, paths.packageDir,
'dist_ts', 'dist_ts',
'daemon.js', 'daemon',
'tspm.daemon.js',
); );
// Start daemon as a detached background process // Start daemon as a detached background process

View File

@@ -17,14 +17,8 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
return; return;
} }
const desc = await tspmIpcClient.request('describe', { id }).catch(() => null); console.log(`Starting process id ${id}...`);
if (!desc) { const response = await tspmIpcClient.request('startById', { id });
console.error(`Process with id '${id}' not found. Use 'tspm add' first.`);
return;
}
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('✓ Process started');
console.log(` ID: ${response.processId}`); console.log(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`); console.log(` PID: ${response.pid || 'N/A'}`);

View File

@@ -1,4 +1,5 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import { tspmIpcClient } from '../client/tspm.ipcclient.js';
import * as paths from '../paths.js'; import * as paths from '../paths.js';
import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js'; import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js';
@@ -38,6 +39,24 @@ export const run = async (): Promise<void> => {
} }
const smartcliInstance = new plugins.smartcli.Smartcli(); const smartcliInstance = new plugins.smartcli.Smartcli();
// Intercept -v/--version to show CLI and daemon versions
const args = process.argv.slice(2);
if (args.includes('-v') || args.includes('--version')) {
const cliVersion = tspmProjectinfo.npm.version;
console.log(`tspm CLI: ${cliVersion}`);
const status = await tspmIpcClient.getDaemonStatus();
if (status) {
console.log(
`Daemon: running v${status.version || 'unknown'} (pid ${status.pid})`,
);
} else {
console.log('Daemon: not running');
}
// Ensure we disconnect any IPC client connection used for status
try { await tspmIpcClient.disconnect(); } catch {}
return; // do not start parser
}
// Keep Smartcli version info for help output but not used for -v now
smartcliInstance.addVersion(tspmProjectinfo.npm.version); smartcliInstance.addVersion(tspmProjectinfo.npm.version);
// Register all commands // Register all commands

View File

@@ -20,12 +20,20 @@ export class TspmDaemon {
private socketPath: string; private socketPath: string;
private heartbeatInterval: NodeJS.Timeout | null = null; private heartbeatInterval: NodeJS.Timeout | null = null;
private daemonPidFile: string; private daemonPidFile: string;
private version: string;
constructor() { constructor() {
this.tspmInstance = new ProcessManager(); this.tspmInstance = new ProcessManager();
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock'); this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid'); this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
this.startTime = Date.now(); this.startTime = Date.now();
// Determine daemon version from package metadata
try {
const proj = new plugins.projectinfo.ProjectInfo(paths.packageDir);
this.version = proj.npm.version || 'unknown';
} catch {
this.version = 'unknown';
}
} }
/** /**
@@ -128,15 +136,45 @@ export class TspmDaemon {
}, },
); );
// Start by id (resolve config on server)
this.ipcServer.onMessage(
'startById',
async (request: RequestForMethod<'startById'>) => {
try {
const id = String(request.id).trim();
let config = this.tspmInstance.processConfigs.get(id);
if (!config) {
// Try to reload configs if not found (handles races or stale state)
await this.tspmInstance.loadProcessConfigs();
config = this.tspmInstance.processConfigs.get(id) || null as any;
}
if (!config) {
throw new Error(`Process ${id} not found`);
}
await this.tspmInstance.setDesiredState(id, 'online');
await this.tspmInstance.start(config);
const processInfo = this.tspmInstance.processInfo.get(id);
return {
processId: id,
pid: processInfo?.pid,
status: processInfo?.status || 'stopped',
};
} catch (error) {
throw new Error(`Failed to start process: ${error.message}`);
}
},
);
this.ipcServer.onMessage( this.ipcServer.onMessage(
'stop', 'stop',
async (request: RequestForMethod<'stop'>) => { async (request: RequestForMethod<'stop'>) => {
try { try {
await this.tspmInstance.setDesiredState(request.id, 'stopped'); const id = String(request.id).trim();
await this.tspmInstance.stop(request.id); await this.tspmInstance.setDesiredState(id, 'stopped');
await this.tspmInstance.stop(id);
return { return {
success: true, success: true,
message: `Process ${request.id} stopped successfully`, message: `Process ${id} stopped successfully`,
}; };
} catch (error) { } catch (error) {
throw new Error(`Failed to stop process: ${error.message}`); throw new Error(`Failed to stop process: ${error.message}`);
@@ -148,11 +186,12 @@ export class TspmDaemon {
'restart', 'restart',
async (request: RequestForMethod<'restart'>) => { async (request: RequestForMethod<'restart'>) => {
try { try {
await this.tspmInstance.setDesiredState(request.id, 'online'); const id = String(request.id).trim();
await this.tspmInstance.restart(request.id); await this.tspmInstance.setDesiredState(id, 'online');
const processInfo = this.tspmInstance.processInfo.get(request.id); await this.tspmInstance.restart(id);
const processInfo = this.tspmInstance.processInfo.get(id);
return { return {
processId: request.id, processId: id,
pid: processInfo?.pid, pid: processInfo?.pid,
status: processInfo?.status || 'stopped', status: processInfo?.status || 'stopped',
}; };
@@ -166,10 +205,11 @@ export class TspmDaemon {
'delete', 'delete',
async (request: RequestForMethod<'delete'>) => { async (request: RequestForMethod<'delete'>) => {
try { try {
await this.tspmInstance.delete(request.id); const id = String(request.id).trim();
await this.tspmInstance.delete(id);
return { return {
success: true, success: true,
message: `Process ${request.id} deleted successfully`, message: `Process ${id} deleted successfully`,
}; };
} catch (error) { } catch (error) {
throw new Error(`Failed to delete process: ${error.message}`); throw new Error(`Failed to delete process: ${error.message}`);
@@ -195,8 +235,9 @@ export class TspmDaemon {
'remove', 'remove',
async (request: RequestForMethod<'remove'>) => { async (request: RequestForMethod<'remove'>) => {
try { try {
await this.tspmInstance.delete(request.id); const id = String(request.id).trim();
return { success: true, message: `Process ${request.id} deleted successfully` }; await this.tspmInstance.delete(id);
return { success: true, message: `Process ${id} deleted successfully` };
} catch (error) { } catch (error) {
throw new Error(`Failed to remove process: ${error.message}`); throw new Error(`Failed to remove process: ${error.message}`);
} }
@@ -214,9 +255,10 @@ export class TspmDaemon {
this.ipcServer.onMessage( this.ipcServer.onMessage(
'describe', 'describe',
async (request: RequestForMethod<'describe'>) => { async (request: RequestForMethod<'describe'>) => {
const result = await this.tspmInstance.describe(request.id); const id = String(request.id).trim();
const result = await this.tspmInstance.describe(id);
if (!result) { if (!result) {
throw new Error(`Process ${request.id} not found`); throw new Error(`Process ${id} not found`);
} }
// Return correctly shaped response // Return correctly shaped response
return { return {
@@ -321,6 +363,7 @@ export class TspmDaemon {
processCount: this.tspmInstance.processes.size, processCount: this.tspmInstance.processes.size,
memoryUsage: memUsage.heapUsed, memoryUsage: memUsage.heapUsed,
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
version: this.version,
}; };
}, },
); );

View File

@@ -66,6 +66,17 @@ export interface StartResponse {
status: 'online' | 'stopped' | 'errored'; status: 'online' | 'stopped' | 'errored';
} }
// Start by id (server resolves config)
export interface StartByIdRequest {
id: string;
}
export interface StartByIdResponse {
processId: string;
pid?: number;
status: 'online' | 'stopped' | 'errored';
}
// Stop command // Stop command
export interface StopRequest { export interface StopRequest {
id: string; id: string;
@@ -191,6 +202,7 @@ export interface DaemonStatusResponse {
processCount: number; processCount: number;
memoryUsage?: number; memoryUsage?: number;
cpuUsage?: number; cpuUsage?: number;
version?: string;
} }
// Daemon shutdown command // Daemon shutdown command
@@ -238,6 +250,7 @@ export interface RemoveResponse {
// Type mappings for methods // Type mappings for methods
export type IpcMethodMap = { export type IpcMethodMap = {
start: { request: StartRequest; response: StartResponse }; start: { request: StartRequest; response: StartResponse };
startById: { request: StartByIdRequest; response: StartByIdResponse };
stop: { request: StopRequest; response: StopResponse }; stop: { request: StopRequest; response: StopResponse };
restart: { request: RestartRequest; response: RestartResponse }; restart: { request: RestartRequest; response: RestartResponse };
delete: { request: DeleteRequest; response: DeleteResponse }; delete: { request: DeleteRequest; response: DeleteResponse };