BREAKING CHANGE(daemon): Refactor daemon and service management: remove IPC auto-spawn, add TspmServiceManager, tighten IPC/client/CLI behavior and tests

This commit is contained in:
2025-08-28 15:52:29 +00:00
parent 8e3cfb624b
commit e73f4acd63
38 changed files with 810 additions and 580 deletions

View File

@@ -1,6 +1,19 @@
# Changelog
## 2025-08-28 - 3.0.0 - BREAKING CHANGE(daemon)
Refactor daemon and service management: remove IPC auto-spawn, add TspmServiceManager, tighten IPC/client/CLI behavior and tests
- Remove automatic daemon spawn from the IPC client — clients now error with guidance and require the daemon to be started manually or enabled as a system service
- Add TspmServiceManager to manage the daemon as a systemd service (enable/disable/reload/status)
- Update IPC server/client to use SmartIpc.createServer/createClient with heartbeat defaults and explicit onMessage handlers
- Daemon publishes per-process logs to topics (logs.<processId>) and re-emits ProcessMonitor logs for pub/sub
- CLI updated: add enable/disable service commands, adjust daemon start/stop/status workflows and improve user hints when daemon is not running
- Add/adjust integration and unit tests to cover daemon lifecycle, IPC client behavior, log streaming, heartbeat and resource reporting
- Documentation expanded (README, readme.plan.md, changelog) to reflect the refactor and migration notes
- Various code cleanups, formatting fixes and defensive checks across modules
## 2025-08-28 - 2.0.0 - BREAKING CHANGE(daemon)
Refactor daemon lifecycle and service management: remove IPC auto-spawn, add TspmServiceManager and CLI enable/disable
- Do not auto-spawn the daemon from the IPC client anymore — attempts to connect will now error with instructions to start the daemon manually or enable the system service (breaking change).
@@ -12,6 +25,7 @@ Refactor daemon lifecycle and service management: remove IPC auto-spawn, add Tsp
- Updated development plan/readme (readme.plan.md) to reflect the refactor toward proper SmartDaemon integration and migration notes.
## 2025-08-26 - 1.8.0 - feat(daemon)
Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC client subscribe/unsubscribe, CLI --follow streaming, and sequencing for logs
- Upgrade @push.rocks/smartipc dependency to ^2.1.2
@@ -24,6 +38,7 @@ Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC
- Standardized heartbeat and IPC timing defaults (heartbeatInterval: 5000ms, heartbeatTimeout: 20000ms, heartbeatInitialGracePeriodMs: 10000ms)
## 2025-08-25 - 1.7.0 - feat(readme)
Add comprehensive README with detailed usage, command reference, daemon management, architecture and development instructions
- Expanded README from a short placeholder to a full documentation covering: Quick Start, Installation, Command Reference, Daemon Management, Monitoring & Information, Batch Operations, Architecture, Programmatic Usage, Advanced Features, Development, Debugging, Performance, and Legal information
@@ -32,6 +47,7 @@ Add comprehensive README with detailed usage, command reference, daemon manageme
- Improved onboarding instructions: cloning, installing, testing, building, and running the project
## 2025-08-25 - 1.6.1 - fix(daemon)
Fix smartipc integration and add daemon/ipc integration tests
- Replace direct smartipc server/client construction with SmartIpc.createServer/createClient and set heartbeat: false
@@ -40,6 +56,7 @@ Fix smartipc integration and add daemon/ipc integration tests
- Add comprehensive tests: unit tests for TspmDaemon and TspmIpcClient and full integration tests for daemon lifecycle, process management, error handling, heartbeat and resource reporting
## 2025-08-25 - 1.6.0 - feat(daemon)
Add central TSPM daemon and IPC client; refactor CLI to use daemon and improve monitoring/error handling
- Add central daemon implementation (ts/classes.daemon.ts) to manage all processes via a single background service and Unix socket.

View File

@@ -64,9 +64,11 @@ tspm restart my-server
### Process Management
#### `tspm start <script> [options]`
Start a new process with automatic monitoring and management.
**Options:**
- `--name <name>` - Custom name for the process (default: script name)
- `--memory <size>` - Memory limit (e.g., "512MB", "2GB", default: 512MB)
- `--cwd <path>` - Working directory (default: current directory)
@@ -75,6 +77,7 @@ Start a new process with automatic monitoring and management.
- `--autorestart` - Auto-restart on crash (default: true)
**Examples:**
```bash
# Simple start
tspm start server.js
@@ -90,6 +93,7 @@ tspm start ../other-project/index.js --cwd ../other-project --name other
```
#### `tspm stop <id>`
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
```bash
@@ -97,6 +101,7 @@ tspm stop my-server
```
#### `tspm restart <id>`
Stop and restart a process with the same configuration.
```bash
@@ -104,6 +109,7 @@ tspm restart my-server
```
#### `tspm delete <id>`
Stop and remove a process from TSPM management.
```bash
@@ -113,6 +119,7 @@ tspm delete old-server
### Monitoring & Information
#### `tspm list`
Display all managed processes in a beautiful table.
```bash
@@ -128,6 +135,7 @@ tspm list
```
#### `tspm describe <id>`
Get detailed information about a specific process.
```bash
@@ -153,9 +161,11 @@ Watch Paths: src, config
```
#### `tspm logs <id> [options]`
View process logs (stdout and stderr).
**Options:**
- `--lines <n>` - Number of lines to display (default: 50)
```bash
@@ -165,6 +175,7 @@ tspm logs my-server --lines 100
### Batch Operations
#### `tspm start-all`
Start all saved processes at once.
```bash
@@ -172,6 +183,7 @@ tspm start-all
```
#### `tspm stop-all`
Stop all running processes.
```bash
@@ -179,6 +191,7 @@ tspm stop-all
```
#### `tspm restart-all`
Restart all running processes.
```bash
@@ -188,6 +201,7 @@ tspm restart-all
### Daemon Management
#### `tspm daemon start`
Start the TSPM daemon (happens automatically on first command).
```bash
@@ -195,6 +209,7 @@ tspm daemon start
```
#### `tspm daemon stop`
Stop the TSPM daemon and all managed processes.
```bash
@@ -202,6 +217,7 @@ tspm daemon stop
```
#### `tspm daemon status`
Check daemon health and statistics.
```bash
@@ -245,7 +261,7 @@ const processId = await manager.start({
projectDir: process.cwd(),
memoryLimitBytes: 512 * 1024 * 1024, // 512MB
autorestart: true,
watch: false
watch: false,
});
// Monitor process
@@ -259,18 +275,23 @@ await manager.stop(processId);
## 🔧 Advanced Features
### Memory Limit Enforcement
TSPM tracks memory usage including all child processes spawned by your application. When a process exceeds its memory limit, it's gracefully restarted.
### Process Group Tracking
Using `ps-tree`, TSPM monitors not just your main process but all child processes it spawns, ensuring complete cleanup on stop/restart.
### Intelligent Logging
Logs are buffered and managed efficiently, preventing memory issues from excessive output while ensuring you don't lose important information.
### Graceful Shutdown
Processes receive SIGTERM first, allowing them to clean up. After a timeout, SIGKILL ensures termination.
### Configuration Persistence
Process configurations are saved, allowing you to restart all processes after a system reboot with a single command.
## 🛠️ Development
@@ -304,6 +325,7 @@ tspm list
## 📊 Performance
TSPM is designed to be lightweight and efficient:
- Minimal CPU overhead (typically < 0.5%)
- Small memory footprint (~30-50MB for the daemon)
- Fast process startup and shutdown

View File

@@ -1,20 +1,24 @@
# TSPM SmartDaemon Service Management Refactor
## Problem
Currently TSPM auto-spawns the daemon as a detached child process, which is improper daemon management. It should use smartdaemon for all lifecycle management and never spawn processes directly.
## Solution
Refactor to use SmartDaemon for proper systemd service integration.
## Implementation Tasks
### Phase 1: Remove Auto-Spawn Behavior
- [x] Remove spawn import from ts/classes.ipcclient.ts
- [x] Delete startDaemon() method from IpcClient
- [x] Update connect() to throw error when daemon not running
- [x] Remove auto-reconnect logic from request() method
### Phase 2: Create Service Manager
- [x] Create new file ts/classes.servicemanager.ts
- [x] Implement TspmServiceManager class
- [x] Add getOrCreateService() method
@@ -23,6 +27,7 @@ Refactor to use SmartDaemon for proper systemd service integration.
- [x] Add getServiceStatus() method
### Phase 3: Update CLI Commands
- [x] Add 'enable' command to CLI
- [x] Add 'disable' command to CLI
- [x] Update 'daemon start' to work without systemd
@@ -31,11 +36,13 @@ Refactor to use SmartDaemon for proper systemd service integration.
- [x] Add proper error messages with hints
### Phase 4: Update Documentation
- [x] Update help text in CLI
- [ ] Update command descriptions
- [x] Add service management section
### Phase 5: Testing
- [x] Test enable command
- [x] Test disable command
- [x] Test daemon commands
@@ -43,6 +50,7 @@ Refactor to use SmartDaemon for proper systemd service integration.
- [x] Build and verify TypeScript compilation
## Migration Notes
- Users will need to run `tspm enable` once after update
- Existing daemon instances will stop working
- Documentation needs updating to explain new behavior

View File

@@ -97,7 +97,7 @@ tap.test('Daemon uptime calculation', async () => {
const startTime = Date.now();
// Wait a bit
await new Promise(resolve => setTimeout(resolve, 100));
await new Promise((resolve) => setTimeout(resolve, 100));
const uptime = Date.now() - startTime;
expect(uptime).toBeGreaterThanOrEqual(100);

View File

@@ -10,7 +10,7 @@ import { tspmIpcClient } from '../ts/classes.ipcclient.js';
async function ensureDaemonStopped() {
try {
await tspmIpcClient.stopDaemon(false);
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) {
// Ignore errors if daemon is not running
}
@@ -43,7 +43,7 @@ tap.test('Full daemon lifecycle test', async (tools) => {
await tspmIpcClient.connect();
// Give daemon time to fully initialize
await new Promise(resolve => setTimeout(resolve, 2000));
await new Promise((resolve) => setTimeout(resolve, 2000));
// Test 3: Check daemon is running
status = await tspmIpcClient.getDaemonStatus();
@@ -57,7 +57,7 @@ tap.test('Full daemon lifecycle test', async (tools) => {
await tspmIpcClient.stopDaemon(true);
// Give daemon time to shutdown
await new Promise(resolve => setTimeout(resolve, 2000));
await new Promise((resolve) => setTimeout(resolve, 2000));
// Test 5: Check daemon is stopped
status = await tspmIpcClient.getDaemonStatus();
@@ -71,7 +71,7 @@ tap.test('Process management through daemon', async (tools) => {
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
// Test 1: List processes (should be empty initially)
let listResponse = await tspmIpcClient.request('list', {});
@@ -88,7 +88,9 @@ tap.test('Process management through daemon', async (tools) => {
autorestart: false,
};
const startResponse = await tspmIpcClient.request('start', { config: testConfig });
const startResponse = await tspmIpcClient.request('start', {
config: testConfig,
});
expect(startResponse.processId).toEqual('test-echo');
expect(startResponse.status).toBeDefined();
@@ -96,12 +98,14 @@ tap.test('Process management through daemon', async (tools) => {
listResponse = await tspmIpcClient.request('list', {});
expect(listResponse.processes.length).toBeGreaterThanOrEqual(1);
const process = listResponse.processes.find(p => p.id === 'test-echo');
const process = listResponse.processes.find((p) => p.id === 'test-echo');
expect(process).toBeDefined();
expect(process?.id).toEqual('test-echo');
// Test 4: Describe the process
const describeResponse = await tspmIpcClient.request('describe', { id: 'test-echo' });
const describeResponse = await tspmIpcClient.request('describe', {
id: 'test-echo',
});
expect(describeResponse.processInfo).toBeDefined();
expect(describeResponse.config).toBeDefined();
expect(describeResponse.config.id).toEqual('test-echo');
@@ -112,12 +116,16 @@ tap.test('Process management through daemon', async (tools) => {
expect(stopResponse.message).toInclude('stopped successfully');
// Test 6: Delete the process
const deleteResponse = await tspmIpcClient.request('delete', { id: 'test-echo' });
const deleteResponse = await tspmIpcClient.request('delete', {
id: 'test-echo',
});
expect(deleteResponse.success).toEqual(true);
// Test 7: Verify process is gone
listResponse = await tspmIpcClient.request('list', {});
const deletedProcess = listResponse.processes.find(p => p.id === 'test-echo');
const deletedProcess = listResponse.processes.find(
(p) => p.id === 'test-echo',
);
expect(deletedProcess).toBeUndefined();
// Cleanup: stop daemon
@@ -131,7 +139,7 @@ tap.test('Batch operations through daemon', async (tools) => {
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
// Add multiple test processes
const testConfigs: tspm.IProcessConfig[] = [
@@ -187,7 +195,7 @@ tap.test('Daemon error handling', async (tools) => {
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
// Test 1: Try to stop non-existent process
try {
@@ -224,7 +232,7 @@ tap.test('Daemon heartbeat functionality', async (tools) => {
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
// Test heartbeat
const heartbeatResponse = await tspmIpcClient.request('heartbeat', {});
@@ -242,7 +250,7 @@ tap.test('Daemon memory and CPU reporting', async (tools) => {
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
// Get daemon status
const status = await tspmIpcClient.getDaemonStatus();

View File

@@ -61,7 +61,10 @@ tap.test('IPC client daemon running check - stale PID', async () => {
expect(isRunning).toEqual(false);
// Clean up - the stale PID should be removed
const fileExists = await fs.access(pidFile).then(() => true).catch(() => false);
const fileExists = await fs
.access(pidFile)
.then(() => true)
.catch(() => false);
expect(fileExists).toEqual(false);
});
@@ -95,7 +98,9 @@ tap.test('IPC client singleton instance', async () => {
expect(tspmIpcClient).toBeInstanceOf(TspmIpcClient);
// Test that it's the same instance
const { tspmIpcClient: secondImport } = await import('../ts/classes.ipcclient.js');
const { tspmIpcClient: secondImport } = await import(
'../ts/classes.ipcclient.js'
);
expect(tspmIpcClient).toBe(secondImport);
});
@@ -111,7 +116,8 @@ tap.test('IPC client request method type safety', async () => {
});
tap.test('IPC client error message formatting', async () => {
const errorMessage = 'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.';
const errorMessage =
'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.';
expect(errorMessage).toInclude('tspm daemon start');
});

View File

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

View File

@@ -43,12 +43,12 @@ export class TspmDaemon {
this.ipcServer = plugins.smartipc.SmartIpc.createServer({
id: 'tspm-daemon',
socketPath: this.socketPath,
autoCleanupSocketFile: true, // Clean up stale sockets
socketMode: 0o600, // Set proper permissions
autoCleanupSocketFile: true, // Clean up stale sockets
socketMode: 0o600, // Set proper permissions
heartbeat: true,
heartbeatInterval: 5000,
heartbeatTimeout: 20000,
heartbeatInitialGracePeriodMs: 10000 // Grace period for startup
heartbeatInitialGracePeriodMs: 10000, // Grace period for startup
});
// Register message handlers
@@ -122,19 +122,22 @@ export class TspmDaemon {
},
);
this.ipcServer.onMessage('restart', async (request: RequestForMethod<'restart'>) => {
try {
await this.tspmInstance.restart(request.id);
const processInfo = this.tspmInstance.processInfo.get(request.id);
return {
processId: request.id,
pid: processInfo?.pid,
status: processInfo?.status || 'stopped',
};
} catch (error) {
throw new Error(`Failed to restart process: ${error.message}`);
}
});
this.ipcServer.onMessage(
'restart',
async (request: RequestForMethod<'restart'>) => {
try {
await this.tspmInstance.restart(request.id);
const processInfo = this.tspmInstance.processInfo.get(request.id);
return {
processId: request.id,
pid: processInfo?.pid,
status: processInfo?.status || 'stopped',
};
} catch (error) {
throw new Error(`Failed to restart process: ${error.message}`);
}
},
);
this.ipcServer.onMessage(
'delete',
@@ -160,124 +163,148 @@ export class TspmDaemon {
},
);
this.ipcServer.onMessage('describe', async (request: RequestForMethod<'describe'>) => {
const processInfo = await this.tspmInstance.describe(request.id);
const config = this.tspmInstance.processConfigs.get(request.id);
this.ipcServer.onMessage(
'describe',
async (request: RequestForMethod<'describe'>) => {
const processInfo = await this.tspmInstance.describe(request.id);
const config = this.tspmInstance.processConfigs.get(request.id);
if (!processInfo || !config) {
throw new Error(`Process ${request.id} not found`);
}
if (!processInfo || !config) {
throw new Error(`Process ${request.id} not found`);
}
return {
processInfo,
config,
};
});
return {
processInfo,
config,
};
},
);
this.ipcServer.onMessage('getLogs', async (request: RequestForMethod<'getLogs'>) => {
const logs = await this.tspmInstance.getLogs(request.id);
return { logs };
});
this.ipcServer.onMessage(
'getLogs',
async (request: RequestForMethod<'getLogs'>) => {
const logs = await this.tspmInstance.getLogs(request.id);
return { logs };
},
);
// Batch operations handlers
this.ipcServer.onMessage('startAll', async (request: RequestForMethod<'startAll'>) => {
const started: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
this.ipcServer.onMessage(
'startAll',
async (request: RequestForMethod<'startAll'>) => {
const started: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
await this.tspmInstance.startAll();
await this.tspmInstance.startAll();
// Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'online') {
started.push(id);
} else {
failed.push({ id, error: 'Failed to start' });
// Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'online') {
started.push(id);
} else {
failed.push({ id, error: 'Failed to start' });
}
}
}
return { started, failed };
});
return { started, failed };
},
);
this.ipcServer.onMessage('stopAll', async (request: RequestForMethod<'stopAll'>) => {
const stopped: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
this.ipcServer.onMessage(
'stopAll',
async (request: RequestForMethod<'stopAll'>) => {
const stopped: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
await this.tspmInstance.stopAll();
await this.tspmInstance.stopAll();
// Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'stopped') {
stopped.push(id);
} else {
failed.push({ id, error: 'Failed to stop' });
// Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'stopped') {
stopped.push(id);
} else {
failed.push({ id, error: 'Failed to stop' });
}
}
}
return { stopped, failed };
});
return { stopped, failed };
},
);
this.ipcServer.onMessage('restartAll', async (request: RequestForMethod<'restartAll'>) => {
const restarted: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
this.ipcServer.onMessage(
'restartAll',
async (request: RequestForMethod<'restartAll'>) => {
const restarted: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
await this.tspmInstance.restartAll();
await this.tspmInstance.restartAll();
// Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'online') {
restarted.push(id);
} else {
failed.push({ id, error: 'Failed to restart' });
// Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'online') {
restarted.push(id);
} else {
failed.push({ id, error: 'Failed to restart' });
}
}
}
return { restarted, failed };
});
return { restarted, failed };
},
);
// Daemon management handlers
this.ipcServer.onMessage('daemon:status', async (request: RequestForMethod<'daemon:status'>) => {
const memUsage = process.memoryUsage();
return {
status: 'running',
pid: process.pid,
uptime: Date.now() - this.startTime,
processCount: this.tspmInstance.processes.size,
memoryUsage: memUsage.heapUsed,
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
};
});
this.ipcServer.onMessage('daemon:shutdown', async (request: RequestForMethod<'daemon:shutdown'>) => {
if (this.isShuttingDown) {
this.ipcServer.onMessage(
'daemon:status',
async (request: RequestForMethod<'daemon:status'>) => {
const memUsage = process.memoryUsage();
return {
success: false,
message: 'Daemon is already shutting down',
status: 'running',
pid: process.pid,
uptime: Date.now() - this.startTime,
processCount: this.tspmInstance.processes.size,
memoryUsage: memUsage.heapUsed,
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
};
}
},
);
// Schedule shutdown
const graceful = request.graceful !== false;
const timeout = request.timeout || 10000;
this.ipcServer.onMessage(
'daemon:shutdown',
async (request: RequestForMethod<'daemon:shutdown'>) => {
if (this.isShuttingDown) {
return {
success: false,
message: 'Daemon is already shutting down',
};
}
if (graceful) {
setTimeout(() => this.shutdown(true), 100);
} else {
setTimeout(() => this.shutdown(false), 100);
}
// Schedule shutdown
const graceful = request.graceful !== false;
const timeout = request.timeout || 10000;
return {
success: true,
message: `Daemon will shutdown ${graceful ? 'gracefully' : 'immediately'} in ${timeout}ms`,
};
});
if (graceful) {
setTimeout(() => this.shutdown(true), 100);
} else {
setTimeout(() => this.shutdown(false), 100);
}
return {
success: true,
message: `Daemon will shutdown ${graceful ? 'gracefully' : 'immediately'} in ${timeout}ms`,
};
},
);
// Heartbeat handler
this.ipcServer.onMessage('heartbeat', async (request: RequestForMethod<'heartbeat'>) => {
return {
timestamp: Date.now(),
status: this.isShuttingDown ? 'degraded' : 'healthy',
};
});
this.ipcServer.onMessage(
'heartbeat',
async (request: RequestForMethod<'heartbeat'>) => {
return {
timestamp: Date.now(),
status: this.isShuttingDown ? 'degraded' : 'healthy',
};
},
);
}
/**

View File

@@ -36,9 +36,9 @@ export class TspmIpcClient {
if (!daemonRunning) {
throw new Error(
'TSPM daemon is not running.\n\n' +
'To start the daemon, run one of:\n' +
' tspm daemon start - Start daemon for this session\n' +
' tspm enable - Enable daemon as system service (recommended)\n'
'To start the daemon, run one of:\n' +
' tspm daemon start - Start daemon for this session\n' +
' tspm enable - Enable daemon as system service (recommended)\n',
);
}
@@ -59,7 +59,7 @@ export class TspmIpcClient {
heartbeatInterval: 5000,
heartbeatTimeout: 20000,
heartbeatInitialGracePeriodMs: 10000,
heartbeatThrowOnTimeout: false // Don't throw, emit events instead
heartbeatThrowOnTimeout: false, // Don't throw, emit events instead
});
// Connect to the daemon
@@ -121,7 +121,10 @@ export class TspmIpcClient {
/**
* Subscribe to log updates for a specific process
*/
public async subscribe(processId: string, handler: (log: any) => void): Promise<void> {
public async subscribe(
processId: string,
handler: (log: any) => void,
): Promise<void> {
if (!this.ipcClient || !this.isConnected) {
throw new Error('Not connected to daemon');
}
@@ -184,8 +187,6 @@ export class TspmIpcClient {
}
}
/**
* Stop the daemon
*/

View File

@@ -25,7 +25,7 @@ export class TspmServiceManager {
description: 'TSPM Process Manager Daemon',
command: `${process.execPath} ${cliPath} daemon start-service`,
workingDir: process.env.HOME || process.cwd(),
version: '1.0.0'
version: '1.0.0',
});
}
return this.service;
@@ -82,13 +82,13 @@ export class TspmServiceManager {
return {
enabled: true, // Would need to check systemctl is-enabled
running: true, // Would need to check systemctl is-active
status: 'active'
status: 'active',
};
} catch (error) {
return {
enabled: false,
running: false,
status: 'inactive'
status: 'inactive',
};
}
}

View File

@@ -32,8 +32,6 @@ export interface IProcessInfo {
restarts: number;
}
export class Tspm extends EventEmitter {
public processes: Map<string, ProcessMonitor> = new Map();
public processConfigs: Map<string, IProcessConfig> = new Map();

View File

@@ -4,23 +4,28 @@ import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerRestartAllCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(smartcli, 'restart-all', async (_argvArg: CliArguments) => {
console.log('Restarting all processes...');
const response = await tspmIpcClient.request('restartAll', {});
registerIpcCommand(
smartcli,
'restart-all',
async (_argvArg: CliArguments) => {
console.log('Restarting all processes...');
const response = await tspmIpcClient.request('restartAll', {});
if (response.restarted.length > 0) {
console.log(`✓ Restarted ${response.restarted.length} processes:`);
for (const id of response.restarted) {
console.log(` - ${id}`);
if (response.restarted.length > 0) {
console.log(`✓ Restarted ${response.restarted.length} processes:`);
for (const id of response.restarted) {
console.log(` - ${id}`);
}
}
}
if (response.failed.length > 0) {
console.log(`✗ Failed to restart ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
if (response.failed.length > 0) {
console.log(`✗ Failed to restart ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
process.exitCode = 1; // Signal partial failure
}
process.exitCode = 1; // Signal partial failure
}
}, { actionLabel: 'restart all processes' });
},
{ actionLabel: 'restart all processes' },
);
}

View File

@@ -4,23 +4,28 @@ import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStartAllCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(smartcli, 'start-all', async (_argvArg: CliArguments) => {
console.log('Starting all processes...');
const response = await tspmIpcClient.request('startAll', {});
registerIpcCommand(
smartcli,
'start-all',
async (_argvArg: CliArguments) => {
console.log('Starting all processes...');
const response = await tspmIpcClient.request('startAll', {});
if (response.started.length > 0) {
console.log(`✓ Started ${response.started.length} processes:`);
for (const id of response.started) {
console.log(` - ${id}`);
if (response.started.length > 0) {
console.log(`✓ Started ${response.started.length} processes:`);
for (const id of response.started) {
console.log(` - ${id}`);
}
}
}
if (response.failed.length > 0) {
console.log(`✗ Failed to start ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
if (response.failed.length > 0) {
console.log(`✗ Failed to start ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
process.exitCode = 1; // Signal partial failure
}
process.exitCode = 1; // Signal partial failure
}
}, { actionLabel: 'start all processes' });
},
{ actionLabel: 'start all processes' },
);
}

View File

@@ -4,23 +4,28 @@ import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStopAllCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(smartcli, 'stop-all', async (_argvArg: CliArguments) => {
console.log('Stopping all processes...');
const response = await tspmIpcClient.request('stopAll', {});
registerIpcCommand(
smartcli,
'stop-all',
async (_argvArg: CliArguments) => {
console.log('Stopping all processes...');
const response = await tspmIpcClient.request('stopAll', {});
if (response.stopped.length > 0) {
console.log(`✓ Stopped ${response.stopped.length} processes:`);
for (const id of response.stopped) {
console.log(` - ${id}`);
if (response.stopped.length > 0) {
console.log(`✓ Stopped ${response.stopped.length} processes:`);
for (const id of response.stopped) {
console.log(` - ${id}`);
}
}
}
if (response.failed.length > 0) {
console.log(`✗ Failed to stop ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
if (response.failed.length > 0) {
console.log(`✗ Failed to stop ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
process.exitCode = 1; // Signal partial failure
}
process.exitCode = 1; // Signal partial failure
}
}, { actionLabel: 'stop all processes' });
},
{ actionLabel: 'stop all processes' },
);
}

View File

@@ -52,13 +52,15 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
console.log(`Started daemon with PID: ${daemonProcess.pid}`);
// Wait for daemon to be ready
await new Promise(resolve => setTimeout(resolve, 2000));
await new Promise((resolve) => setTimeout(resolve, 2000));
const newStatus = await tspmIpcClient.getDaemonStatus();
if (newStatus) {
console.log('✓ TSPM daemon started successfully');
console.log(` PID: ${newStatus.pid}`);
console.log('\nNote: This daemon will run until you stop it or logout.');
console.log(
'\nNote: This daemon will run until you stop it or logout.',
);
console.log('For automatic startup, use "tspm enable" instead.');
}

View File

@@ -17,7 +17,9 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
);
console.log('Usage: tspm [command] [options]');
console.log('\nService Management:');
console.log(' enable Enable TSPM as system service (systemd)');
console.log(
' enable Enable TSPM as system service (systemd)',
);
console.log(' disable Disable TSPM system service');
console.log('\nProcess Commands:');
console.log(' start <script> Start a process');
@@ -31,7 +33,9 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
console.log(' stop-all Stop all processes');
console.log(' restart-all Restart all processes');
console.log('\nDaemon Commands:');
console.log(' daemon start Start daemon manually (current session)');
console.log(
' daemon start Start daemon manually (current session)',
);
console.log(' daemon stop Stop the daemon');
console.log(' daemon status Show daemon status');
console.log(
@@ -85,7 +89,9 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
console.error('Error: TSPM daemon is not running.');
console.log('\nTo start the daemon, run one of:');
console.log(' tspm daemon start - Start for this session only');
console.log(' tspm enable - Enable as system service (recommended)');
console.log(
' tspm enable - Enable as system service (recommended)',
);
}
},
error: (err) => {

View File

@@ -4,21 +4,26 @@ import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(smartcli, 'delete', async (argvArg: CliArguments) => {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm delete <id>');
return;
}
registerIpcCommand(
smartcli,
'delete',
async (argvArg: CliArguments) => {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm delete <id>');
return;
}
console.log(`Deleting process: ${id}`);
const response = await tspmIpcClient.request('delete', { id });
console.log(`Deleting process: ${id}`);
const response = await tspmIpcClient.request('delete', { id });
if (response.success) {
console.log(`${response.message}`);
} else {
console.error(`✗ Failed to delete process: ${response.message}`);
}
}, { actionLabel: 'delete process' });
if (response.success) {
console.log(`${response.message}`);
} else {
console.error(`✗ Failed to delete process: ${response.message}`);
}
},
{ actionLabel: 'delete process' },
);
}

View File

@@ -5,34 +5,45 @@ import { registerIpcCommand } from '../../registration/index.js';
import { formatMemory } from '../../helpers/memory.js';
export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(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>');
return;
}
const response = await tspmIpcClient.request('describe', { id });
console.log(`Process Details: ${id}`);
console.log('─'.repeat(40));
console.log(`Status: ${response.processInfo.status}`);
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
console.log(`Memory: ${formatMemory(response.processInfo.memory)}`);
console.log(`CPU: ${response.processInfo.cpu ? response.processInfo.cpu.toFixed(1) + '%' : 'N/A'}`);
console.log(`Uptime: ${response.processInfo.uptime ? Math.floor(response.processInfo.uptime / 1000) + 's' : 'N/A'}`);
console.log(`Restarts: ${response.processInfo.restarts}`);
console.log('\nConfiguration:');
console.log(`Command: ${response.config.command}`);
console.log(`Directory: ${response.config.projectDir}`);
console.log(`Memory Limit: ${formatMemory(response.config.memoryLimitBytes)}`);
console.log(`Auto-restart: ${response.config.autorestart}`);
if (response.config.watch) {
console.log(`Watch: enabled`);
if (response.config.watchPaths) {
console.log(`Watch Paths: ${response.config.watchPaths.join(', ')}`);
registerIpcCommand(
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>');
return;
}
}
}, { actionLabel: 'describe process' });
const response = await tspmIpcClient.request('describe', { id });
console.log(`Process Details: ${id}`);
console.log('─'.repeat(40));
console.log(`Status: ${response.processInfo.status}`);
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
console.log(`Memory: ${formatMemory(response.processInfo.memory)}`);
console.log(
`CPU: ${response.processInfo.cpu ? response.processInfo.cpu.toFixed(1) + '%' : 'N/A'}`,
);
console.log(
`Uptime: ${response.processInfo.uptime ? Math.floor(response.processInfo.uptime / 1000) + 's' : 'N/A'}`,
);
console.log(`Restarts: ${response.processInfo.restarts}`);
console.log('\nConfiguration:');
console.log(`Command: ${response.config.command}`);
console.log(`Directory: ${response.config.projectDir}`);
console.log(
`Memory Limit: ${formatMemory(response.config.memoryLimitBytes)}`,
);
console.log(`Auto-restart: ${response.config.autorestart}`);
if (response.config.watch) {
console.log(`Watch: enabled`);
if (response.config.watchPaths) {
console.log(`Watch Paths: ${response.config.watchPaths.join(', ')}`);
}
}
},
{ actionLabel: 'describe process' },
);
}

View File

@@ -6,32 +6,47 @@ import { pad } from '../../helpers/formatting.js';
import { formatMemory } from '../../helpers/memory.js';
export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(smartcli, 'list', async (_argvArg: CliArguments) => {
const response = await tspmIpcClient.request('list', {});
const processes = response.processes;
registerIpcCommand(
smartcli,
'list',
async (_argvArg: CliArguments) => {
const response = await tspmIpcClient.request('list', {});
const processes = response.processes;
if (processes.length === 0) {
console.log('No processes running.');
return;
}
if (processes.length === 0) {
console.log('No processes running.');
return;
}
console.log('Process List:');
console.log('┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐');
console.log('│ ID │ Name │ Status │ PID │ Memory │ Restarts │');
console.log('├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤');
console.log('Process List:');
console.log(
'┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐',
);
console.log(
'│ ID │ Name │ Status │ PID │ Memory │ Restarts │',
);
console.log(
'├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤',
);
for (const proc of processes) {
const statusColor =
proc.status === 'online' ? '\x1b[32m' :
proc.status === 'errored' ? '\x1b[31m' :
'\x1b[33m';
const resetColor = '\x1b[0m';
for (const proc of processes) {
const statusColor =
proc.status === 'online'
? '\x1b[32m'
: proc.status === 'errored'
? '\x1b[31m'
: '\x1b[33m';
const resetColor = '\x1b[0m';
console.log(
`${pad(proc.id, 7)}${pad(proc.id, 11)}${statusColor}${pad(proc.status, 9)}${resetColor}${pad((proc.pid || '-').toString(), 9)}${pad(formatMemory(proc.memory), 8)}${pad(proc.restarts.toString(), 8)}`,
);
}
console.log(
`${pad(proc.id, 7)}${pad(proc.id, 11)}${statusColor}${pad(proc.status, 9)}${resetColor}${pad((proc.pid || '-').toString(), 9)}${pad(formatMemory(proc.memory), 8)}${pad(proc.restarts.toString(), 8)}`,
'└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘',
);
}
console.log('└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘');
}, { actionLabel: 'list processes' });
},
{ actionLabel: 'list processes' },
);
}

View File

@@ -7,67 +7,93 @@ import { formatLog } from '../../helpers/formatting.js';
import { withStreamingLifecycle } from '../../helpers/lifecycle.js';
export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(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]');
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)');
return;
}
registerIpcCommand(
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]');
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)');
return;
}
const lines = getNumber(argvArg, 'lines', 50);
const follow = getBool(argvArg, 'follow', 'f');
const lines = getNumber(argvArg, 'lines', 50);
const follow = getBool(argvArg, 'follow', 'f');
const response = await tspmIpcClient.request('getLogs', { id, lines });
const response = await tspmIpcClient.request('getLogs', { id, lines });
if (!follow) {
// One-shot mode - auto-disconnect handled by registerIpcCommand
console.log(`Logs for process: ${id} (last ${lines} lines)`);
if (!follow) {
// One-shot mode - auto-disconnect handled by registerIpcCommand
console.log(`Logs for process: ${id} (last ${lines} lines)`);
console.log('─'.repeat(60));
for (const log of response.logs) {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix =
log.type === 'stdout'
? '[OUT]'
: log.type === 'stderr'
? '[ERR]'
: '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
}
return;
}
// Streaming mode
console.log(`Logs for process: ${id} (streaming...)`);
console.log('─'.repeat(60));
let lastSeq = 0;
for (const log of response.logs) {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
const prefix =
log.type === 'stdout'
? '[OUT]'
: log.type === 'stderr'
? '[ERR]'
: '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
if (log.seq !== undefined) lastSeq = Math.max(lastSeq, log.seq);
}
return;
}
// Streaming mode
console.log(`Logs for process: ${id} (streaming...)`);
console.log('─'.repeat(60));
let lastSeq = 0;
for (const log of response.logs) {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
if (log.seq !== undefined) lastSeq = Math.max(lastSeq, log.seq);
}
await withStreamingLifecycle(
async () => {
await tspmIpcClient.subscribe(id, (log: any) => {
if (log.seq !== undefined && log.seq <= lastSeq) return;
if (log.seq !== undefined && log.seq > lastSeq + 1) {
console.log(`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`);
}
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
if (log.seq !== undefined) lastSeq = log.seq;
});
},
async () => {
console.log('\n\nStopping log stream...');
try { await tspmIpcClient.unsubscribe(id); } catch {}
try { await tspmIpcClient.disconnect(); } catch {}
}
);
}, {
actionLabel: 'get logs',
keepAlive: (argv) => getBool(argv, 'follow', 'f')
});
await withStreamingLifecycle(
async () => {
await tspmIpcClient.subscribe(id, (log: any) => {
if (log.seq !== undefined && log.seq <= lastSeq) return;
if (log.seq !== undefined && log.seq > lastSeq + 1) {
console.log(
`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`,
);
}
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix =
log.type === 'stdout'
? '[OUT]'
: log.type === 'stderr'
? '[ERR]'
: '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
if (log.seq !== undefined) lastSeq = log.seq;
});
},
async () => {
console.log('\n\nStopping log stream...');
try {
await tspmIpcClient.unsubscribe(id);
} catch {}
try {
await tspmIpcClient.disconnect();
} catch {}
},
);
},
{
actionLabel: 'get logs',
keepAlive: (argv) => getBool(argv, 'follow', 'f'),
},
);
}

View File

@@ -4,20 +4,25 @@ import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(smartcli, 'restart', async (argvArg: CliArguments) => {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm restart <id>');
return;
}
registerIpcCommand(
smartcli,
'restart',
async (argvArg: CliArguments) => {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm restart <id>');
return;
}
console.log(`Restarting process: ${id}`);
const response = await tspmIpcClient.request('restart', { id });
console.log(`Restarting process: ${id}`);
const response = await tspmIpcClient.request('restart', { id });
console.log(`✓ Process restarted successfully`);
console.log(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`);
}, { actionLabel: 'restart process' });
console.log(`✓ Process restarted successfully`);
console.log(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`);
},
{ actionLabel: 'restart process' },
);
}

View File

@@ -6,78 +6,97 @@ import { parseMemoryString, formatMemory } from '../../helpers/memory.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(smartcli, 'start', async (argvArg: CliArguments) => {
const script = argvArg._[1];
if (!script) {
console.error('Error: Please provide a script to run');
console.log('Usage: tspm start <script> [options]');
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');
return;
}
const memoryLimit = argvArg.memory ? parseMemoryString(argvArg.memory) : 512 * 1024 * 1024;
const projectDir = argvArg.cwd || process.cwd();
// Direct .ts support via tsx (bundled with TSPM)
let actualCommand = script;
let commandArgs: string[] | undefined = undefined;
if (script.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(script) ? script : plugins.path.join(projectDir, script);
actualCommand = tsxPath;
commandArgs = [scriptPath];
} catch {
actualCommand = 'tsx';
commandArgs = [script];
registerIpcCommand(
smartcli,
'start',
async (argvArg: CliArguments) => {
const script = argvArg._[1];
if (!script) {
console.error('Error: Please provide a script to run');
console.log('Usage: tspm start <script> [options]');
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');
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 memoryLimit = argvArg.memory
? parseMemoryString(argvArg.memory)
: 512 * 1024 * 1024;
const projectDir = argvArg.cwd || process.cwd();
const processConfig: IProcessConfig = {
id: name.replace(/[^a-zA-Z0-9-_]/g, '_'),
name,
command: actualCommand,
args: commandArgs,
projectDir,
memoryLimitBytes: memoryLimit,
autorestart,
watch,
watchPaths,
};
// Direct .ts support via tsx (bundled with TSPM)
let actualCommand = script;
let commandArgs: string[] | undefined = undefined;
console.log(`Starting process: ${name}`);
console.log(` Command: ${script}${script.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(', ')}`);
}
if (script.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 response = await tspmIpcClient.request('start', { config: processConfig });
console.log(`✓ Process started successfully`);
console.log(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`);
}, { actionLabel: 'start process' });
const scriptPath = plugins.path.isAbsolute(script)
? script
: plugins.path.join(projectDir, script);
actualCommand = tsxPath;
commandArgs = [scriptPath];
} catch {
actualCommand = 'tsx';
commandArgs = [script];
}
}
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: commandArgs,
projectDir,
memoryLimitBytes: memoryLimit,
autorestart,
watch,
watchPaths,
};
console.log(`Starting process: ${name}`);
console.log(
` Command: ${script}${script.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(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`);
},
{ actionLabel: 'start process' },
);
}

View File

@@ -4,21 +4,26 @@ import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStopCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(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>');
return;
}
registerIpcCommand(
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>');
return;
}
console.log(`Stopping process: ${id}`);
const response = await tspmIpcClient.request('stop', { id });
console.log(`Stopping process: ${id}`);
const response = await tspmIpcClient.request('stop', { id });
if (response.success) {
console.log(`${response.message}`);
} else {
console.error(`✗ Failed to stop process: ${response.message}`);
}
}, { actionLabel: 'stop process' });
if (response.success) {
console.log(`${response.message}`);
} else {
console.error(`✗ Failed to stop process: ${response.message}`);
}
},
{ actionLabel: 'stop process' },
);
}

View File

@@ -19,7 +19,10 @@ export function registerDisableCommand(smartcli: plugins.smartcli.Smartcli) {
console.log(' Use "tspm enable" to re-enable the service');
} catch (error) {
console.error('Error disabling service:', error.message);
if (error.message.includes('permission') || error.message.includes('denied')) {
if (
error.message.includes('permission') ||
error.message.includes('denied')
) {
console.log('\nNote: You may need to run this command with sudo');
}
process.exit(1);

View File

@@ -19,7 +19,10 @@ export function registerEnableCommand(smartcli: plugins.smartcli.Smartcli) {
console.log(' Use "tspm disable" to remove the service');
} catch (error) {
console.error('Error enabling service:', error.message);
if (error.message.includes('permission') || error.message.includes('denied')) {
if (
error.message.includes('permission') ||
error.message.includes('denied')
) {
console.log('\nNote: You may need to run this command with sudo');
}
process.exit(1);

View File

@@ -2,15 +2,23 @@ import type { CliArguments } from '../types.js';
// Argument parsing helpers
export const getBool = (argv: CliArguments, ...keys: string[]) =>
keys.some(k => Boolean((argv as any)[k]));
keys.some((k) => Boolean((argv as any)[k]));
export const getNumber = (argv: CliArguments, key: string, fallback: number) => {
export const getNumber = (
argv: CliArguments,
key: string,
fallback: number,
) => {
const v = (argv as any)[key];
const n = typeof v === 'string' ? Number(v) : v;
return Number.isFinite(n) ? n : fallback;
};
export const getString = (argv: CliArguments, key: string, fallback?: string) => {
export const getString = (
argv: CliArguments,
key: string,
fallback?: string,
) => {
const v = (argv as any)[key];
return typeof v === 'string' ? v : fallback;
};

View File

@@ -1,12 +1,16 @@
// Helper function to handle daemon connection errors
export function handleDaemonError(error: any, action: string): void {
if (error.message?.includes('daemon is not running') ||
error.message?.includes('Not connected') ||
error.message?.includes('ECONNREFUSED')) {
if (
error.message?.includes('daemon is not running') ||
error.message?.includes('Not connected') ||
error.message?.includes('ECONNREFUSED')
) {
console.error(`Error: Cannot ${action} - TSPM daemon is not running.`);
console.log('\nTo start the daemon, run one of:');
console.log(' tspm daemon start - Start for this session only');
console.log(' tspm enable - Enable as system service (recommended)');
console.log(
' tspm enable - Enable as system service (recommended)',
);
} else {
console.error(`Error ${action}:`, error.message);
}

View File

@@ -7,11 +7,12 @@ export function pad(str: string, length: number): string {
// Helper for unknown errors
export const unknownError = (err: any) =>
(err?.message && typeof err.message === 'string') ? err.message : String(err);
err?.message && typeof err.message === 'string' ? err.message : String(err);
// Helper function to format log entries
export function formatLog(log: any): string {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
const prefix =
log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
return `${timestamp} ${prefix} ${log.message}`;
}

View File

@@ -4,15 +4,22 @@ import { tspmIpcClient } from '../../classes.ipcclient.js';
* Preflight the daemon if required. Uses getDaemonStatus() which is safe and cheap:
* it only connects if the PID file is valid.
*/
export async function ensureDaemonOrHint(requireDaemon: boolean | undefined, actionLabel?: string): Promise<boolean> {
export async function ensureDaemonOrHint(
requireDaemon: boolean | undefined,
actionLabel?: string,
): Promise<boolean> {
if (requireDaemon === false) return true; // command does not require daemon
const status = await tspmIpcClient.getDaemonStatus();
if (!status) {
// Same hint as handleDaemonError, but early and consistent
console.error(`Error: Cannot ${actionLabel || 'perform action'} - TSPM daemon is not running.`);
console.error(
`Error: Cannot ${actionLabel || 'perform action'} - TSPM daemon is not running.`,
);
console.log('\nTo start the daemon, run one of:');
console.log(' tspm daemon start - Start for this session only');
console.log(' tspm enable - Enable as system service (recommended)');
console.log(
' tspm enable - Enable as system service (recommended)',
);
return false;
}
return true;

View File

@@ -1,5 +1,9 @@
import * as plugins from '../../plugins.js';
import type { CliArguments, CommandAction, IpcCommandOptions } from '../types.js';
import type {
CliArguments,
CommandAction,
IpcCommandOptions,
} from '../types.js';
import { handleDaemonError } from '../helpers/errors.js';
import { unknownError } from '../helpers/formatting.js';
import { runIpcCommand } from '../utils/ipc.js';
@@ -15,7 +19,7 @@ export function registerIpcCommand(
smartcli: plugins.smartcli.Smartcli,
name: string,
action: CommandAction,
opts: IpcCommandOptions = {}
opts: IpcCommandOptions = {},
) {
const { actionLabel = name, keepAlive = false, requireDaemon = true } = opts;
@@ -29,7 +33,8 @@ export function registerIpcCommand(
}
// Evaluate keepAlive - can be boolean or function
const shouldKeepAlive = typeof keepAlive === 'function' ? keepAlive(argv) : keepAlive;
const shouldKeepAlive =
typeof keepAlive === 'function' ? keepAlive(argv) : keepAlive;
if (shouldKeepAlive) {
// Let action manage its own connection/cleanup lifecycle
@@ -51,7 +56,10 @@ export function registerIpcCommand(
},
error: (err) => {
// Fallback error path (should be rare with try/catch in next)
console.error(`Unexpected error in command "${name}":`, unknownError(err));
console.error(
`Unexpected error in command "${name}":`,
unknownError(err),
);
process.exit(1);
},
complete: () => {},
@@ -66,7 +74,7 @@ export function registerLocalCommand(
smartcli: plugins.smartcli.Smartcli,
name: string,
action: (argv: CliArguments) => Promise<void>,
opts: { actionLabel?: string } = {}
opts: { actionLabel?: string } = {},
) {
const { actionLabel = name } = opts;
smartcli.addCommand(name).subscribe({
@@ -79,7 +87,10 @@ export function registerLocalCommand(
}
},
error: (err) => {
console.error(`Unexpected error in command "${name}":`, unknownError(err));
console.error(
`Unexpected error in command "${name}":`,
unknownError(err),
);
process.exit(1);
},
complete: () => {},

View File

@@ -14,7 +14,7 @@ export interface CliArguments {
export type CommandAction = (argv: CliArguments) => Promise<void>;
export interface IpcCommandOptions {
actionLabel?: string; // used in error message, e.g. "start process"
keepAlive?: boolean | ((argv: CliArguments) => boolean); // true for streaming commands (don't auto-disconnect), or function to determine at runtime
requireDaemon?: boolean; // default true for IPC-bound commands
actionLabel?: string; // used in error message, e.g. "start process"
keepAlive?: boolean | ((argv: CliArguments) => boolean); // true for streaming commands (don't auto-disconnect), or function to determine at runtime
requireDaemon?: boolean; // default true for IPC-bound commands
}

View File

@@ -1,7 +1,4 @@
import type {
IProcessConfig,
IProcessInfo,
} from './classes.tspm.js';
import type { IProcessConfig, IProcessInfo } from './classes.tspm.js';
import type { IProcessLog } from './classes.processwrapper.js';
// Base message types