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:
17
changelog.md
17
changelog.md
@@ -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.
|
||||
|
24
readme.md
24
readme.md
@@ -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
|
||||
|
@@ -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
|
@@ -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);
|
||||
|
@@ -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();
|
||||
|
@@ -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');
|
||||
});
|
||||
|
||||
|
@@ -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'
|
||||
}
|
||||
|
@@ -48,7 +48,7 @@ export class TspmDaemon {
|
||||
heartbeat: true,
|
||||
heartbeatInterval: 5000,
|
||||
heartbeatTimeout: 20000,
|
||||
heartbeatInitialGracePeriodMs: 10000 // Grace period for startup
|
||||
heartbeatInitialGracePeriodMs: 10000, // Grace period for startup
|
||||
});
|
||||
|
||||
// Register message handlers
|
||||
@@ -122,7 +122,9 @@ export class TspmDaemon {
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer.onMessage('restart', async (request: RequestForMethod<'restart'>) => {
|
||||
this.ipcServer.onMessage(
|
||||
'restart',
|
||||
async (request: RequestForMethod<'restart'>) => {
|
||||
try {
|
||||
await this.tspmInstance.restart(request.id);
|
||||
const processInfo = this.tspmInstance.processInfo.get(request.id);
|
||||
@@ -134,7 +136,8 @@ export class TspmDaemon {
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to restart process: ${error.message}`);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer.onMessage(
|
||||
'delete',
|
||||
@@ -160,7 +163,9 @@ export class TspmDaemon {
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer.onMessage('describe', async (request: RequestForMethod<'describe'>) => {
|
||||
this.ipcServer.onMessage(
|
||||
'describe',
|
||||
async (request: RequestForMethod<'describe'>) => {
|
||||
const processInfo = await this.tspmInstance.describe(request.id);
|
||||
const config = this.tspmInstance.processConfigs.get(request.id);
|
||||
|
||||
@@ -172,15 +177,21 @@ export class TspmDaemon {
|
||||
processInfo,
|
||||
config,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer.onMessage('getLogs', async (request: RequestForMethod<'getLogs'>) => {
|
||||
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'>) => {
|
||||
this.ipcServer.onMessage(
|
||||
'startAll',
|
||||
async (request: RequestForMethod<'startAll'>) => {
|
||||
const started: string[] = [];
|
||||
const failed: Array<{ id: string; error: string }> = [];
|
||||
|
||||
@@ -196,9 +207,12 @@ export class TspmDaemon {
|
||||
}
|
||||
|
||||
return { started, failed };
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer.onMessage('stopAll', async (request: RequestForMethod<'stopAll'>) => {
|
||||
this.ipcServer.onMessage(
|
||||
'stopAll',
|
||||
async (request: RequestForMethod<'stopAll'>) => {
|
||||
const stopped: string[] = [];
|
||||
const failed: Array<{ id: string; error: string }> = [];
|
||||
|
||||
@@ -214,9 +228,12 @@ export class TspmDaemon {
|
||||
}
|
||||
|
||||
return { stopped, failed };
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer.onMessage('restartAll', async (request: RequestForMethod<'restartAll'>) => {
|
||||
this.ipcServer.onMessage(
|
||||
'restartAll',
|
||||
async (request: RequestForMethod<'restartAll'>) => {
|
||||
const restarted: string[] = [];
|
||||
const failed: Array<{ id: string; error: string }> = [];
|
||||
|
||||
@@ -232,10 +249,13 @@ export class TspmDaemon {
|
||||
}
|
||||
|
||||
return { restarted, failed };
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Daemon management handlers
|
||||
this.ipcServer.onMessage('daemon:status', async (request: RequestForMethod<'daemon:status'>) => {
|
||||
this.ipcServer.onMessage(
|
||||
'daemon:status',
|
||||
async (request: RequestForMethod<'daemon:status'>) => {
|
||||
const memUsage = process.memoryUsage();
|
||||
return {
|
||||
status: 'running',
|
||||
@@ -245,9 +265,12 @@ export class TspmDaemon {
|
||||
memoryUsage: memUsage.heapUsed,
|
||||
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer.onMessage('daemon:shutdown', async (request: RequestForMethod<'daemon:shutdown'>) => {
|
||||
this.ipcServer.onMessage(
|
||||
'daemon:shutdown',
|
||||
async (request: RequestForMethod<'daemon:shutdown'>) => {
|
||||
if (this.isShuttingDown) {
|
||||
return {
|
||||
success: false,
|
||||
@@ -269,15 +292,19 @@ export class TspmDaemon {
|
||||
success: true,
|
||||
message: `Daemon will shutdown ${graceful ? 'gracefully' : 'immediately'} in ${timeout}ms`,
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Heartbeat handler
|
||||
this.ipcServer.onMessage('heartbeat', async (request: RequestForMethod<'heartbeat'>) => {
|
||||
this.ipcServer.onMessage(
|
||||
'heartbeat',
|
||||
async (request: RequestForMethod<'heartbeat'>) => {
|
||||
return {
|
||||
timestamp: Date.now(),
|
||||
status: this.isShuttingDown ? 'degraded' : 'healthy',
|
||||
};
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -38,7 +38,7 @@ export class TspmIpcClient {
|
||||
'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'
|
||||
' 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
|
||||
*/
|
||||
|
@@ -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',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -4,7 +4,10 @@ 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) => {
|
||||
registerIpcCommand(
|
||||
smartcli,
|
||||
'restart-all',
|
||||
async (_argvArg: CliArguments) => {
|
||||
console.log('Restarting all processes...');
|
||||
const response = await tspmIpcClient.request('restartAll', {});
|
||||
|
||||
@@ -22,5 +25,7 @@ export function registerRestartAllCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
}
|
||||
process.exitCode = 1; // Signal partial failure
|
||||
}
|
||||
}, { actionLabel: 'restart all processes' });
|
||||
},
|
||||
{ actionLabel: 'restart all processes' },
|
||||
);
|
||||
}
|
@@ -4,7 +4,10 @@ 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) => {
|
||||
registerIpcCommand(
|
||||
smartcli,
|
||||
'start-all',
|
||||
async (_argvArg: CliArguments) => {
|
||||
console.log('Starting all processes...');
|
||||
const response = await tspmIpcClient.request('startAll', {});
|
||||
|
||||
@@ -22,5 +25,7 @@ export function registerStartAllCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
}
|
||||
process.exitCode = 1; // Signal partial failure
|
||||
}
|
||||
}, { actionLabel: 'start all processes' });
|
||||
},
|
||||
{ actionLabel: 'start all processes' },
|
||||
);
|
||||
}
|
@@ -4,7 +4,10 @@ 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) => {
|
||||
registerIpcCommand(
|
||||
smartcli,
|
||||
'stop-all',
|
||||
async (_argvArg: CliArguments) => {
|
||||
console.log('Stopping all processes...');
|
||||
const response = await tspmIpcClient.request('stopAll', {});
|
||||
|
||||
@@ -22,5 +25,7 @@ export function registerStopAllCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
}
|
||||
process.exitCode = 1; // Signal partial failure
|
||||
}
|
||||
}, { actionLabel: 'stop all processes' });
|
||||
},
|
||||
{ actionLabel: 'stop all processes' },
|
||||
);
|
||||
}
|
@@ -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.');
|
||||
}
|
||||
|
||||
|
@@ -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) => {
|
||||
|
@@ -4,7 +4,10 @@ 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) => {
|
||||
registerIpcCommand(
|
||||
smartcli,
|
||||
'delete',
|
||||
async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
@@ -20,5 +23,7 @@ export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
} else {
|
||||
console.error(`✗ Failed to delete process: ${response.message}`);
|
||||
}
|
||||
}, { actionLabel: 'delete process' });
|
||||
},
|
||||
{ actionLabel: 'delete process' },
|
||||
);
|
||||
}
|
@@ -5,7 +5,10 @@ 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) => {
|
||||
registerIpcCommand(
|
||||
smartcli,
|
||||
'describe',
|
||||
async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
@@ -20,13 +23,19 @@ export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
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(
|
||||
`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(
|
||||
`Memory Limit: ${formatMemory(response.config.memoryLimitBytes)}`,
|
||||
);
|
||||
console.log(`Auto-restart: ${response.config.autorestart}`);
|
||||
if (response.config.watch) {
|
||||
console.log(`Watch: enabled`);
|
||||
@@ -34,5 +43,7 @@ export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
console.log(`Watch Paths: ${response.config.watchPaths.join(', ')}`);
|
||||
}
|
||||
}
|
||||
}, { actionLabel: 'describe process' });
|
||||
},
|
||||
{ actionLabel: 'describe process' },
|
||||
);
|
||||
}
|
@@ -6,7 +6,10 @@ 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) => {
|
||||
registerIpcCommand(
|
||||
smartcli,
|
||||
'list',
|
||||
async (_argvArg: CliArguments) => {
|
||||
const response = await tspmIpcClient.request('list', {});
|
||||
const processes = response.processes;
|
||||
|
||||
@@ -16,15 +19,23 @@ export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
}
|
||||
|
||||
console.log('Process List:');
|
||||
console.log('┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐');
|
||||
console.log('│ ID │ Name │ Status │ PID │ Memory │ Restarts │');
|
||||
console.log('├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤');
|
||||
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';
|
||||
proc.status === 'online'
|
||||
? '\x1b[32m'
|
||||
: proc.status === 'errored'
|
||||
? '\x1b[31m'
|
||||
: '\x1b[33m';
|
||||
const resetColor = '\x1b[0m';
|
||||
|
||||
console.log(
|
||||
@@ -32,6 +43,10 @@ export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
);
|
||||
}
|
||||
|
||||
console.log('└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘');
|
||||
}, { actionLabel: 'list processes' });
|
||||
console.log(
|
||||
'└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘',
|
||||
);
|
||||
},
|
||||
{ actionLabel: 'list processes' },
|
||||
);
|
||||
}
|
@@ -7,7 +7,10 @@ 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) => {
|
||||
registerIpcCommand(
|
||||
smartcli,
|
||||
'logs',
|
||||
async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
@@ -29,7 +32,12 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
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]';
|
||||
const prefix =
|
||||
log.type === 'stdout'
|
||||
? '[OUT]'
|
||||
: log.type === 'stderr'
|
||||
? '[ERR]'
|
||||
: '[SYS]';
|
||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||
}
|
||||
return;
|
||||
@@ -42,7 +50,12 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
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);
|
||||
}
|
||||
@@ -52,22 +65,35 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
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}`);
|
||||
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]';
|
||||
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 {}
|
||||
}
|
||||
try {
|
||||
await tspmIpcClient.unsubscribe(id);
|
||||
} catch {}
|
||||
try {
|
||||
await tspmIpcClient.disconnect();
|
||||
} catch {}
|
||||
},
|
||||
);
|
||||
}, {
|
||||
},
|
||||
{
|
||||
actionLabel: 'get logs',
|
||||
keepAlive: (argv) => getBool(argv, 'follow', 'f')
|
||||
});
|
||||
keepAlive: (argv) => getBool(argv, 'follow', 'f'),
|
||||
},
|
||||
);
|
||||
}
|
@@ -4,7 +4,10 @@ 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) => {
|
||||
registerIpcCommand(
|
||||
smartcli,
|
||||
'restart',
|
||||
async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
@@ -19,5 +22,7 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
console.log(` ID: ${response.processId}`);
|
||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||
console.log(` Status: ${response.status}`);
|
||||
}, { actionLabel: 'restart process' });
|
||||
},
|
||||
{ actionLabel: 'restart process' },
|
||||
);
|
||||
}
|
@@ -6,22 +6,31 @@ 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) => {
|
||||
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(
|
||||
' --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 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 memoryLimit = argvArg.memory
|
||||
? parseMemoryString(argvArg.memory)
|
||||
: 512 * 1024 * 1024;
|
||||
const projectDir = argvArg.cwd || process.cwd();
|
||||
|
||||
// Direct .ts support via tsx (bundled with TSPM)
|
||||
@@ -36,7 +45,9 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
return require.resolve('tsx/dist/cli.mjs');
|
||||
})();
|
||||
|
||||
const scriptPath = plugins.path.isAbsolute(script) ? script : plugins.path.join(projectDir, script);
|
||||
const scriptPath = plugins.path.isAbsolute(script)
|
||||
? script
|
||||
: plugins.path.join(projectDir, script);
|
||||
actualCommand = tsxPath;
|
||||
commandArgs = [scriptPath];
|
||||
} catch {
|
||||
@@ -49,7 +60,9 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
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)
|
||||
? typeof argvArg.watchPaths === 'string'
|
||||
? (argvArg.watchPaths as string).split(',')
|
||||
: argvArg.watchPaths
|
||||
: undefined;
|
||||
|
||||
const processConfig: IProcessConfig = {
|
||||
@@ -65,7 +78,9 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
};
|
||||
|
||||
console.log(`Starting process: ${name}`);
|
||||
console.log(` Command: ${script}${script.endsWith('.ts') ? ' (via tsx)' : ''}`);
|
||||
console.log(
|
||||
` Command: ${script}${script.endsWith('.ts') ? ' (via tsx)' : ''}`,
|
||||
);
|
||||
console.log(` Directory: ${projectDir}`);
|
||||
console.log(` Memory limit: ${formatMemory(memoryLimit)}`);
|
||||
console.log(` Auto-restart: ${autorestart}`);
|
||||
@@ -74,10 +89,14 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
if (watchPaths) console.log(` Watch paths: ${watchPaths.join(', ')}`);
|
||||
}
|
||||
|
||||
const response = await tspmIpcClient.request('start', { config: processConfig });
|
||||
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' });
|
||||
},
|
||||
{ actionLabel: 'start process' },
|
||||
);
|
||||
}
|
@@ -4,7 +4,10 @@ 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) => {
|
||||
registerIpcCommand(
|
||||
smartcli,
|
||||
'stop',
|
||||
async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
@@ -20,5 +23,7 @@ export function registerStopCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
} else {
|
||||
console.error(`✗ Failed to stop process: ${response.message}`);
|
||||
}
|
||||
}, { actionLabel: 'stop process' });
|
||||
},
|
||||
{ actionLabel: 'stop process' },
|
||||
);
|
||||
}
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
};
|
@@ -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') ||
|
||||
if (
|
||||
error.message?.includes('daemon is not running') ||
|
||||
error.message?.includes('Not connected') ||
|
||||
error.message?.includes('ECONNREFUSED')) {
|
||||
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);
|
||||
}
|
||||
|
@@ -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}`;
|
||||
}
|
@@ -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;
|
||||
|
@@ -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: () => {},
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user