Compare commits

...

19 Commits

Author SHA1 Message Date
4d1976332b 1.6.0
Some checks failed
Default (tags) / security (push) Successful in 52s
Default (tags) / test (push) Failing after 52s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-25 08:52:57 +00:00
3ad8f29e1c feat(daemon): Add central TSPM daemon and IPC client; refactor CLI to use daemon and improve monitoring/error handling 2025-08-25 08:52:57 +00:00
1c06fb54b9 1.5.1
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 45s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-10 23:38:21 +00:00
779593f73a fix(core): Improve error handling, logging, and test suite; update dependency versions 2025-03-10 23:38:21 +00:00
5c4836fd68 1.5.0
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 44s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-04 11:44:55 +00:00
2dc766fa6e feat(cli): Enhance CLI with new process management commands 2025-03-04 11:44:55 +00:00
0232741b89 1.4.0
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 1h14m52s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-03-03 05:21:52 +00:00
9c1327c9be feat(core): Introduced process management features using ProcessWrapper and enhanced configuration. 2025-03-03 05:21:52 +00:00
74bfcb273a 1.3.1
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Successful in 53s
Default (tags) / release (push) Failing after 47s
Default (tags) / metadata (push) Successful in 56s
2025-03-01 19:47:46 +00:00
cefbce1ba0 fix(test): Update test script to fix type references and remove private method call 2025-03-01 19:47:46 +00:00
51bb3a8967 1.3.0
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Successful in 51s
Default (tags) / release (push) Failing after 47s
Default (tags) / metadata (push) Successful in 1m0s
2025-03-01 19:19:28 +00:00
c4a082031e feat(cli): Add CLI support with command parsing and version display 2025-03-01 19:19:28 +00:00
761f9ca1b6 1.2.0
Some checks failed
Default (tags) / security (push) Successful in 36s
Default (tags) / test (push) Successful in 52s
Default (tags) / release (push) Failing after 46s
Default (tags) / metadata (push) Successful in 59s
2025-03-01 18:02:40 +00:00
ad2c180cfe feat(core): Introduce ProcessMonitor with memory management and spawning features 2025-03-01 18:02:40 +00:00
36eb1a79a7 1.1.1
Some checks failed
Default (tags) / security (push) Successful in 37s
Default (tags) / test (push) Failing after 57s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-01 17:27:24 +00:00
8a0a9dedb1 fix(package): Update dependencies and pnpm configuration 2025-03-01 17:27:24 +00:00
00c4488cc3 1.1.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 45s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-03-01 12:15:22 +00:00
2ffaeff4b5 feat(core): Introduce ProcessMonitor class and integrate native and external plugins 2025-03-01 12:15:22 +00:00
594e006a5a update 2025-02-24 23:08:23 +00:00
25 changed files with 6699 additions and 2236 deletions

4
.gitignore vendored
View File

@@ -16,4 +16,8 @@ node_modules/
dist/
dist_*/
# AI
.claude/
.serena/
#------# custom

View File

@@ -1,8 +1,94 @@
# Changelog
## 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.
- Introduce IPC client and typed IPC contracts (ts/classes.ipcclient.ts, ts/ipc.types.ts) so CLI communicates with the daemon.
- Refactor CLI to use the daemon for commands (ts/cli.ts): start/stop/restart/delete/list/describe/logs/start-all/stop-all/restart-all and new daemon start/stop/status commands.
- Enhance process monitoring and wrapping: ProcessMonitor and ProcessWrapper improvements (ts/classes.processmonitor.ts, ts/classes.processwrapper.ts) with better logging, memory checks, and restart behavior.
- Improve centralized error handling and Logger behavior (ts/utils.errorhandler.ts).
- Persist and load process configurations via TspmConfig and config storage changes (ts/classes.config.ts, ts/classes.tspm.ts).
- Bump dependency and devDependency versions and add packageManager entry in package.json.
- Add ts/daemon entrypoint and export daemon/ipc types from ts/index.ts; add paths for tspm runtime dir (ts/paths.ts).
- Update tests and test tooling imports (test/test.ts) and adjust commitinfo and readme hints.
## 2025-03-10 - 1.5.1 - fix(core)
Improve error handling, logging, and test suite; update dependency versions
- Updated devDependencies versions in package.json (@git.zone/tsbuild, @push.rocks/tapbundle, and @push.rocks/smartdaemon)
- Refactored error handling and enhanced logging in ProcessMonitor and ProcessWrapper modules
- Improved test structure by adding clear module import tests and usage examples in test files
## 2025-03-04 - 1.5.0 - feat(cli)
Enhance CLI with new process management commands
- Added comprehensive CLI commands for process management including start, stop, restart, list, describe and logs.
- Implemented memory string parsing for process memory limits.
- Enhanced CLI output with formatted table listings for active processes.
## 2025-03-03 - 1.4.0 - feat(core)
Introduced process management features using ProcessWrapper and enhanced configuration.
- Added ProcessWrapper for wrapping and managing child processes.
- Refactored process monitoring logic using ProcessWrapper.
- Introduced TspmConfig for configuration handling.
- Enhanced CLI to support new process management commands like 'startAsDaemon'.
## 2025-03-01 - 1.3.1 - fix(test)
Update test script to fix type references and remove private method call
- Corrected type references in test script for IMonitorConfig.
- Fixed test script to use console.log instead of private method monitor.log.
## 2025-03-01 - 1.3.0 - feat(cli)
Add CLI support with command parsing and version display
- Added a basic CLI interface using smartcli.
- Implemented command parsing with a 'restart' command.
- Integrated project version display in the CLI.
## 2025-03-01 - 1.2.0 - feat(core)
Introduce ProcessMonitor with memory management and spawning features
- Added ProcessMonitor class with functionality to manage process execution and memory usage.
- Implemented process spawning with ability to handle command arguments and directories.
- Added periodic memory monitoring and automatic restarts when memory thresholds are exceeded.
- ProcessMonitor now logs its actions with optional configuration name for better identification.
- Updated test file to include example usage of ProcessMonitor.
## 2025-03-01 - 1.1.1 - fix(package)
Update dependencies and pnpm configuration
- Updated @types/node to 22.13.8
- Updated pnpm configuration to include onlyBuiltDependencies with esbuild, mongodb-memory-server, and puppeteer
## 2025-03-01 - 1.1.0 - feat(core)
Introduce ProcessMonitor class and integrate native and external plugins
- Added a new ProcessMonitor class to manage and monitor child processes with memory constraints.
- Integrated native 'path' and external '@push.rocks/smartpath' packages in a unified plugins file.
- Adjusted index and related files for improved modular structure.
## 2025-02-24 - 1.0.3 - fix(core)
Corrected description in package.json and readme.md from 'task manager' to 'process manager'.
- Updated the project description in package.json.
- Aligned the description in readme.md with package.json.
## 2025-02-24 - 1.0.2 - fix(core)
Internal changes with no functional impact.
## 2025-02-24 - 1.0.1 - initial release
Initial release with baseline functionality.

View File

@@ -1,8 +1,8 @@
{
"name": "@git.zone/tspm",
"version": "1.0.2",
"version": "1.6.0",
"private": false,
"description": "a no fuzz task manager",
"description": "a no fuzz process manager",
"main": "dist_ts/index.js",
"typings": "dist_ts/index.d.ts",
"type": "module",
@@ -11,17 +11,30 @@
"scripts": {
"test": "(tstest test/ --web)",
"build": "(tsbuild --web --allowimplicitany)",
"buildDocs": "(tsdoc)"
"buildDocs": "(tsdoc)",
"start": "(tsrun ./cli.ts -v)"
},
"bin": {
"tspm": "./cli.js"
},
"devDependencies": {
"@git.zone/tsbuild": "^2.1.25",
"@git.zone/tsbundle": "^2.0.5",
"@git.zone/tsbuild": "^2.6.7",
"@git.zone/tsbundle": "^2.5.1",
"@git.zone/tsrun": "^1.2.46",
"@git.zone/tstest": "^1.0.44",
"@push.rocks/tapbundle": "^5.0.15",
"@types/node": "^20.8.7"
"@git.zone/tstest": "^2.3.5",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^22.13.10"
},
"dependencies": {
"@push.rocks/npmextra": "^5.3.3",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartcli": "^4.0.11",
"@push.rocks/smartdaemon": "^2.0.8",
"@push.rocks/smartipc": "^2.0.3",
"@push.rocks/smartpath": "^6.0.0",
"pidusage": "^4.0.1",
"ps-tree": "^1.2.0"
},
"dependencies": {},
"repository": {
"type": "git",
"url": "https://code.foss.global/git.zone/tspm.git"
@@ -43,6 +56,12 @@
"readme.md"
],
"pnpm": {
"overrides": {}
}
"overrides": {},
"onlyBuiltDependencies": [
"esbuild",
"mongodb-memory-server",
"puppeteer"
]
},
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
}

5714
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# @git.zone/tspm
a no fuzz task manager
a no fuzz process manager
## How to create the docs

209
readme.plan.md Normal file
View File

@@ -0,0 +1,209 @@
# TSPM Refactoring Plan: Central Daemon Architecture
## Problem Analysis
Currently, each `startAsDaemon` creates an isolated tspm instance with no coordination:
- Multiple daemons reading/writing same config file
- No communication between instances
- Inconsistent process management
- `tspm list` shows all processes but each daemon only manages its own
## Proposed Architecture
### 1. Central Daemon Manager (`ts/classes.daemon.ts`)
- Single daemon instance managing ALL processes
- Runs continuously in background
- Uses Unix socket for IPC at `~/.tspm/tspm.sock`
- Maintains single source of truth for process state
### 2. IPC Communication Layer (`ts/classes.ipc.ts`)
- **Framework**: Use `@push.rocks/smartipc` v2.0.1
- **Server**: SmartIpc server in daemon using Unix Domain Socket
- **Client**: SmartIpc client in CLI for all operations
- **Socket Path**: `~/.tspm/tspm.sock` (Unix) or named pipe (Windows)
- **Protocol**: Type-safe request/response with SmartIpc's built-in patterns
- **Features**:
- Automatic reconnection with exponential backoff
- Heartbeat monitoring for daemon health
- Type-safe message contracts
- **Auto-start**: CLI starts daemon if connection fails
### 3. New CLI Commands
- `tspm enable` - Start central daemon using systemd/launchd
- `tspm disable` - Stop and disable central daemon
- `tspm status` - Show daemon status
- Remove `startAsDaemon` (replaced by daemon + `tspm start`)
### 4. Refactored CLI (`ts/cli.ts`)
All commands become daemon clients:
```typescript
// Before: Direct process management
await tspm.start(config);
// After: Send to daemon
await ipcClient.request('start', config);
```
### 5. File Structure Changes
```
ts/
├── classes.daemon.ts # New: Central daemon server
├── classes.ipc.ts # New: IPC client/server
├── classes.tspm.ts # Modified: Used by daemon only
├── cli.ts # Modified: Becomes thin client
└── classes.daemonmanager.ts # New: Systemd/launchd integration
```
## Implementation Steps
### Phase 1: Core Infrastructure
- [ ] Add `@push.rocks/smartipc` dependency (v2.0.1)
- [ ] Create IPC message type definitions for all operations
- [ ] Implement daemon server with SmartIpc server
- [ ] Create IPC client wrapper for CLI
- [ ] Add daemon lifecycle management (enable/disable)
### Phase 2: CLI Refactoring
- [ ] Convert all CLI commands to SmartIpc client requests
- [ ] Add daemon auto-start logic with connection monitoring
- [ ] Leverage SmartIpc's built-in reconnection and error handling
- [ ] Implement type-safe message contracts for all commands
### Phase 3: Migration & Cleanup
- [ ] Migrate existing config to daemon-compatible format
- [ ] Remove `startAsDaemon` command
- [ ] Add migration guide for users
## Technical Details
### IPC Implementation with SmartIpc
```typescript
// Daemon server setup
import { SmartIpc } from '@push.rocks/smartipc';
const ipcServer = SmartIpc.createServer({
id: 'tspm-daemon',
socketPath: '~/.tspm/tspm.sock', // Unix socket
});
// Message handlers with type safety
ipcServer.onMessage<StartRequest, StartResponse>(
'start',
async (data, clientId) => {
const result = await tspmManager.start(data.config);
return { success: true, processId: result.pid };
},
);
// CLI client setup
const ipcClient = SmartIpc.createClient({
id: 'tspm-daemon',
socketPath: '~/.tspm/tspm.sock',
});
// Type-safe requests
const response = await ipcClient.request<StartRequest, StartResponse>('start', {
config: processConfig,
});
```
### Message Types
```typescript
interface StartRequest {
config: ProcessConfig;
}
interface StartResponse {
success: boolean;
processId?: number;
error?: string;
}
```
### Daemon State File
`~/.tspm/daemon.state` - PID, socket path, version
### Process Management
Daemon maintains all ProcessMonitor instances internally, CLI never directly manages processes.
## Key Benefits
### Architecture Benefits
- Single daemon manages all processes
- Consistent state management
- Efficient resource usage
- Better process coordination
- Proper service integration with OS
### SmartIpc Advantages
- **Cross-platform**: Unix sockets on Linux/macOS, named pipes on Windows
- **Type-safe**: Full TypeScript support with generic message types
- **Resilient**: Automatic reconnection with exponential backoff
- **Observable**: Built-in metrics and heartbeat monitoring
- **Performant**: Low-latency messaging with zero external dependencies
- **Secure**: Connection limits and message size restrictions
## Backwards Compatibility
- Keep existing config format
- Auto-migrate on first run
- Provide clear upgrade instructions
## Architecture Diagram
```
┌─────────────┐ IPC ┌──────────────┐
│ CLI │◄────────────►│ Daemon │
│ (thin client)│ Socket │ (server) │
└─────────────┘ └──────────────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ Tspm │
│ │ Manager │
│ └──────────────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ User │ │ProcessMonitor│
│ Commands │ │ Instances │
└─────────────┘ └──────────────┘
```
## Migration Path
1. **Version 2.0.0-alpha**: Implement daemon with backwards compatibility
2. **Version 2.0.0-beta**: Deprecate `startAsDaemon`, encourage daemon mode
3. **Version 2.0.0**: Remove legacy code, daemon-only operation
4. **Documentation**: Update all examples and guides
## Security Considerations
- Unix socket permissions (user-only access)
- Validate all IPC messages
- Rate limiting for IPC requests
- Secure daemon shutdown mechanism
## Testing Requirements
- Unit tests for IPC layer
- Integration tests for daemon lifecycle
- Migration tests from current architecture
- Performance tests for multiple processes
- Stress tests for IPC communication

View File

@@ -1,8 +1,124 @@
import { expect, expectAsync, tap } from '@push.rocks/tapbundle';
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as tspm from '../ts/index.js';
import { join } from 'path';
tap.test('first test', async () => {
console.log(tspm);
// Basic module import test
tap.test('module import test', async () => {
console.log('Imported modules:', Object.keys(tspm));
expect(tspm.ProcessMonitor).toBeTypeOf('function');
expect(tspm.Tspm).toBeTypeOf('function');
});
// ProcessMonitor test
tap.test('ProcessMonitor test', async () => {
const config: tspm.IMonitorConfig = {
name: 'Test Monitor',
projectDir: process.cwd(),
command: 'echo "Test process running"',
memoryLimitBytes: 50 * 1024 * 1024, // 50MB
monitorIntervalMs: 1000,
};
const monitor = new tspm.ProcessMonitor(config);
// Test monitor creation
expect(monitor).toBeInstanceOf(tspm.ProcessMonitor);
// We won't actually start it in tests to avoid side effects
// but we can test the API
expect(monitor.start).toBeInstanceOf('function');
expect(monitor.stop).toBeInstanceOf('function');
expect(monitor.getLogs).toBeInstanceOf('function');
});
// Tspm class test
tap.test('Tspm class test', async () => {
const tspmInstance = new tspm.Tspm();
expect(tspmInstance).toBeInstanceOf(tspm.Tspm);
expect(tspmInstance.start).toBeInstanceOf('function');
expect(tspmInstance.stop).toBeInstanceOf('function');
expect(tspmInstance.restart).toBeInstanceOf('function');
expect(tspmInstance.list).toBeInstanceOf('function');
expect(tspmInstance.describe).toBeInstanceOf('function');
expect(tspmInstance.getLogs).toBeInstanceOf('function');
});
tap.start();
// ====================================================
// Example usage (this part is not executed in tests)
// ====================================================
// Example 1: Using ProcessMonitor directly
function exampleUsingProcessMonitor() {
const config: tspm.IMonitorConfig = {
name: 'Project XYZ Monitor',
projectDir: '/path/to/your/project',
command: 'npm run xyz',
memoryLimitBytes: 500 * 1024 * 1024, // 500 MB memory limit
monitorIntervalMs: 5000, // Check memory usage every 5 seconds
logBufferSize: 200, // Keep last 200 log lines
};
const monitor = new tspm.ProcessMonitor(config);
monitor.start();
// Ensure that on process exit (e.g. Ctrl+C) we clean up the child process and prevent respawns.
process.on('SIGINT', () => {
console.log('Received SIGINT, stopping monitor...');
monitor.stop();
process.exit();
});
// Get logs example
setTimeout(() => {
const logs = monitor.getLogs(10); // Get last 10 log lines
console.log('Latest logs:', logs);
}, 10000);
}
// Example 2: Using Tspm (higher-level process manager)
async function exampleUsingTspm() {
const tspmInstance = new tspm.Tspm();
// Start a process
await tspmInstance.start({
id: 'web-server',
name: 'Web Server',
projectDir: '/path/to/web/project',
command: 'npm run serve',
memoryLimitBytes: 300 * 1024 * 1024, // 300 MB
autorestart: true,
watch: true,
monitorIntervalMs: 10000,
});
// Start another process
await tspmInstance.start({
id: 'api-server',
name: 'API Server',
projectDir: '/path/to/api/project',
command: 'npm run api',
memoryLimitBytes: 400 * 1024 * 1024, // 400 MB
autorestart: true,
});
// List all processes
const processes = tspmInstance.list();
console.log('Running processes:', processes);
// Get logs from a process
const logs = tspmInstance.getLogs('web-server', 20);
console.log('Web server logs:', logs);
// Stop a process
await tspmInstance.stop('api-server');
// Handle graceful shutdown
process.on('SIGINT', async () => {
console.log('Shutting down all processes...');
await tspmInstance.stopAll();
process.exit();
});
}

View File

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

20
ts/classes.config.ts Normal file
View File

@@ -0,0 +1,20 @@
import * as plugins from './plugins.js';
export class TspmConfig {
public npmextraInstance = new plugins.npmextra.KeyValueStore({
identityArg: '@git.zone__tspm',
typeArg: 'userHomeDir',
});
public async readKey(keyArg: string): Promise<string> {
return await this.npmextraInstance.readKey(keyArg);
}
public async writeKey(keyArg: string, value: string): Promise<void> {
return await this.npmextraInstance.writeKey(keyArg, value);
}
public async deleteKey(keyArg: string): Promise<void> {
return await this.npmextraInstance.deleteKey(keyArg);
}
}

415
ts/classes.daemon.ts Normal file
View File

@@ -0,0 +1,415 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { Tspm } from './classes.tspm.js';
import type {
IpcMethodMap,
RequestForMethod,
ResponseForMethod,
DaemonStatusResponse,
HeartbeatResponse,
} from './ipc.types.js';
/**
* Central daemon server that manages all TSPM processes
*/
export class TspmDaemon {
private tspmInstance: Tspm;
private ipcServer: plugins.smartipc.IpcServer;
private startTime: number;
private isShuttingDown: boolean = false;
private socketPath: string;
private heartbeatInterval: NodeJS.Timeout | null = null;
private daemonPidFile: string;
constructor() {
this.tspmInstance = new Tspm();
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
this.startTime = Date.now();
}
/**
* Start the daemon server
*/
public async start(): Promise<void> {
console.log('Starting TSPM daemon...');
// Check if another daemon is already running
if (await this.isDaemonRunning()) {
throw new Error('Another TSPM daemon instance is already running');
}
// Initialize IPC server
this.ipcServer = new plugins.smartipc.IpcServer({
id: 'tspm-daemon',
socketPath: this.socketPath,
});
// Register message handlers
this.registerHandlers();
// Start the IPC server
await this.ipcServer.start();
// Write PID file
await this.writePidFile();
// Start heartbeat monitoring
this.startHeartbeatMonitoring();
// Load existing process configurations
await this.tspmInstance.loadProcessConfigs();
// Set up graceful shutdown handlers
this.setupShutdownHandlers();
console.log(`TSPM daemon started successfully on ${this.socketPath}`);
console.log(`PID: ${process.pid}`);
}
/**
* Register all IPC message handlers
*/
private registerHandlers(): void {
// Process management handlers
this.ipcServer.on<RequestForMethod<'start'>>(
'start',
async (request) => {
try {
await this.tspmInstance.start(request.config);
const processInfo = this.tspmInstance.processInfo.get(
request.config.id,
);
return {
processId: request.config.id,
pid: processInfo?.pid,
status: processInfo?.status || 'stopped',
};
} catch (error) {
throw new Error(`Failed to start process: ${error.message}`);
}
},
);
this.ipcServer.on<RequestForMethod<'stop'>>(
'stop',
async (request) => {
try {
await this.tspmInstance.stop(request.id);
return {
success: true,
message: `Process ${request.id} stopped successfully`,
};
} catch (error) {
throw new Error(`Failed to stop process: ${error.message}`);
}
},
);
this.ipcServer.on<RequestForMethod<'restart'>>('restart', async (request) => {
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.on<RequestForMethod<'delete'>>(
'delete',
async (request) => {
try {
await this.tspmInstance.delete(request.id);
return {
success: true,
message: `Process ${request.id} deleted successfully`,
};
} catch (error) {
throw new Error(`Failed to delete process: ${error.message}`);
}
},
);
// Query handlers
this.ipcServer.on<RequestForMethod<'list'>>(
'list',
async () => {
const processes = await this.tspmInstance.list();
return { processes };
},
);
this.ipcServer.on<RequestForMethod<'describe'>>('describe', async (request) => {
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`);
}
return {
processInfo,
config,
};
});
this.ipcServer.on<RequestForMethod<'getLogs'>>('getLogs', async (request) => {
const logs = await this.tspmInstance.getLogs(request.id);
return { logs };
});
// Batch operations handlers
this.ipcServer.on<RequestForMethod<'startAll'>>('startAll', async () => {
const started: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
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' });
}
}
return { started, failed };
});
this.ipcServer.on<RequestForMethod<'stopAll'>>('stopAll', async () => {
const stopped: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
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' });
}
}
return { stopped, failed };
});
this.ipcServer.on<RequestForMethod<'restartAll'>>('restartAll', async () => {
const restarted: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
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' });
}
}
return { restarted, failed };
});
// Daemon management handlers
this.ipcServer.on<RequestForMethod<'daemon:status'>>('daemon:status', async () => {
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.on<RequestForMethod<'daemon:shutdown'>>('daemon:shutdown', async (request) => {
if (this.isShuttingDown) {
return {
success: false,
message: 'Daemon is already shutting down',
};
}
// Schedule shutdown
const graceful = request.graceful !== false;
const timeout = request.timeout || 10000;
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.on<RequestForMethod<'heartbeat'>>('heartbeat', async () => {
return {
timestamp: Date.now(),
status: this.isShuttingDown ? 'degraded' : 'healthy',
};
});
}
/**
* Start heartbeat monitoring
*/
private startHeartbeatMonitoring(): void {
// Send heartbeat every 30 seconds
this.heartbeatInterval = setInterval(() => {
// This is where we could implement health checks
// For now, just log that the daemon is alive
const uptime = Math.floor((Date.now() - this.startTime) / 1000);
console.log(
`[Heartbeat] Daemon alive - Uptime: ${uptime}s, Processes: ${this.tspmInstance.processes.size}`,
);
}, 30000);
}
/**
* Set up graceful shutdown handlers
*/
private setupShutdownHandlers(): void {
const shutdownHandler = async (signal: string) => {
console.log(`\nReceived ${signal}, initiating graceful shutdown...`);
await this.shutdown(true);
};
process.on('SIGTERM', () => shutdownHandler('SIGTERM'));
process.on('SIGINT', () => shutdownHandler('SIGINT'));
process.on('SIGHUP', () => shutdownHandler('SIGHUP'));
// Handle uncaught errors
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
this.shutdown(false);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
// Don't exit on unhandled rejection, just log it
});
}
/**
* Shutdown the daemon
*/
public async shutdown(graceful: boolean = true): Promise<void> {
if (this.isShuttingDown) {
return;
}
this.isShuttingDown = true;
console.log('Shutting down TSPM daemon...');
// Clear heartbeat interval
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
if (graceful) {
// Stop all processes gracefully
try {
console.log('Stopping all managed processes...');
await this.tspmInstance.stopAll();
} catch (error) {
console.error('Error stopping processes:', error);
}
}
// Stop IPC server
if (this.ipcServer) {
try {
await this.ipcServer.stop();
} catch (error) {
console.error('Error stopping IPC server:', error);
}
}
// Remove PID file
await this.removePidFile();
// Remove socket file if it exists
try {
const fs = await import('fs');
await fs.promises.unlink(this.socketPath).catch(() => {});
} catch (error) {
// Ignore errors
}
console.log('TSPM daemon shutdown complete');
process.exit(0);
}
/**
* Check if another daemon instance is running
*/
private async isDaemonRunning(): Promise<boolean> {
try {
const fs = await import('fs');
const pidContent = await fs.promises.readFile(
this.daemonPidFile,
'utf-8',
);
const pid = parseInt(pidContent.trim(), 10);
// Check if process is running
try {
process.kill(pid, 0);
return true; // Process exists
} catch {
// Process doesn't exist, clean up stale PID file
await this.removePidFile();
return false;
}
} catch {
// PID file doesn't exist
return false;
}
}
/**
* Write the daemon PID to a file
*/
private async writePidFile(): Promise<void> {
const fs = await import('fs');
await fs.promises.writeFile(this.daemonPidFile, process.pid.toString());
}
/**
* Remove the daemon PID file
*/
private async removePidFile(): Promise<void> {
try {
const fs = await import('fs');
await fs.promises.unlink(this.daemonPidFile);
} catch {
// Ignore if file doesn't exist
}
}
}
/**
* Main entry point for the daemon
*/
export const startDaemon = async (): Promise<void> => {
const daemon = new TspmDaemon();
await daemon.start();
// Keep the process alive
await new Promise(() => {});
};

263
ts/classes.ipcclient.ts Normal file
View File

@@ -0,0 +1,263 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { spawn } from 'child_process';
import type {
IpcMethodMap,
RequestForMethod,
ResponseForMethod,
} from './ipc.types.js';
/**
* IPC client for communicating with the TSPM daemon
*/
export class TspmIpcClient {
private ipcClient: plugins.smartipc.IpcClient | null = null;
private socketPath: string;
private daemonPidFile: string;
private isConnected: boolean = false;
constructor() {
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
}
/**
* Connect to the daemon, starting it if necessary
*/
public async connect(): Promise<void> {
// Check if already connected
if (this.isConnected && this.ipcClient) {
return;
}
// Check if daemon is running
const daemonRunning = await this.isDaemonRunning();
if (!daemonRunning) {
console.log('Daemon not running, starting it...');
await this.startDaemon();
// Wait a bit for daemon to initialize
await new Promise((resolve) => setTimeout(resolve, 1000));
}
// Create IPC client
this.ipcClient = new plugins.smartipc.IpcClient({
id: 'tspm-cli',
socketPath: this.socketPath,
});
// Connect to the daemon
try {
await this.ipcClient.connect();
this.isConnected = true;
console.log('Connected to TSPM daemon');
} catch (error) {
console.error('Failed to connect to daemon:', error);
throw new Error(
'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.',
);
}
}
/**
* Disconnect from the daemon
*/
public async disconnect(): Promise<void> {
if (this.ipcClient) {
await this.ipcClient.disconnect();
this.ipcClient = null;
this.isConnected = false;
}
}
/**
* Send a request to the daemon
*/
public async request<M extends keyof IpcMethodMap>(
method: M,
params: RequestForMethod<M>,
): Promise<ResponseForMethod<M>> {
if (!this.isConnected || !this.ipcClient) {
await this.connect();
}
try {
const response = await this.ipcClient!.request<
RequestForMethod<M>,
ResponseForMethod<M>
>(method, params);
return response;
} catch (error) {
// Handle connection errors by trying to reconnect once
if (
error.message?.includes('ECONNREFUSED') ||
error.message?.includes('ENOENT')
) {
console.log('Connection lost, attempting to reconnect...');
this.isConnected = false;
await this.connect();
// Retry the request
return await this.ipcClient!.request<
RequestForMethod<M>,
ResponseForMethod<M>
>(method, params);
}
throw error;
}
}
/**
* Check if the daemon is running
*/
private async isDaemonRunning(): Promise<boolean> {
try {
const fs = await import('fs');
// Check if PID file exists
try {
const pidContent = await fs.promises.readFile(
this.daemonPidFile,
'utf-8',
);
const pid = parseInt(pidContent.trim(), 10);
// Check if process is running
try {
process.kill(pid, 0);
// Also check if socket exists and is accessible
try {
await fs.promises.access(this.socketPath);
return true;
} catch {
// Socket doesn't exist, daemon might be starting
return false;
}
} catch {
// Process doesn't exist, clean up stale PID file
await fs.promises.unlink(this.daemonPidFile).catch(() => {});
return false;
}
} catch {
// PID file doesn't exist
return false;
}
} catch {
return false;
}
}
/**
* Start the daemon process
*/
private async startDaemon(): Promise<void> {
const daemonScript = plugins.path.join(
paths.packageDir,
'dist_ts',
'daemon.js',
);
// Spawn the daemon as a detached process
const daemonProcess = spawn(process.execPath, [daemonScript], {
detached: true,
stdio: ['ignore', 'ignore', 'ignore'],
env: {
...process.env,
TSPM_DAEMON_MODE: 'true',
},
});
// Unref the process so the parent can exit
daemonProcess.unref();
console.log(`Started daemon process with PID: ${daemonProcess.pid}`);
// Wait for daemon to be ready (check for socket file)
const maxWaitTime = 10000; // 10 seconds
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
if (await this.isDaemonRunning()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error('Daemon failed to start within timeout period');
}
/**
* Stop the daemon
*/
public async stopDaemon(graceful: boolean = true): Promise<void> {
if (!(await this.isDaemonRunning())) {
console.log('Daemon is not running');
return;
}
try {
await this.connect();
await this.request('daemon:shutdown', {
graceful,
timeout: 10000,
});
console.log('Daemon shutdown initiated');
// Wait for daemon to actually stop
const maxWaitTime = 15000; // 15 seconds
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
if (!(await this.isDaemonRunning())) {
console.log('Daemon stopped successfully');
return;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
console.warn(
'Daemon did not stop within timeout, it may still be running',
);
} catch (error) {
console.error('Error stopping daemon:', error);
// Try to kill the process directly if graceful shutdown failed
try {
const fs = await import('fs');
const pidContent = await fs.promises.readFile(
this.daemonPidFile,
'utf-8',
);
const pid = parseInt(pidContent.trim(), 10);
process.kill(pid, 'SIGKILL');
console.log('Force killed daemon process');
} catch {
console.error('Could not force kill daemon');
}
}
}
/**
* Get daemon status
*/
public async getDaemonStatus(): Promise<ResponseForMethod<'daemon:status'> | null> {
try {
if (!(await this.isDaemonRunning())) {
return null;
}
await this.connect();
return await this.request('daemon:status', {});
} catch (error) {
console.error('Error getting daemon status:', error);
return null;
}
}
}
// Singleton instance
export const tspmIpcClient = new TspmIpcClient();

View File

@@ -0,0 +1,304 @@
import * as plugins from './plugins.js';
import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js';
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
export interface IMonitorConfig {
name?: string; // Optional name to identify the instance
projectDir: string; // Directory where the command will run
command: string; // Full command to run (e.g., "npm run xyz")
args?: string[]; // Optional: arguments for the command
memoryLimitBytes: number; // Maximum allowed memory (in bytes) for the process group
monitorIntervalMs?: number; // Interval (in ms) at which memory is checked (default: 5000)
env?: NodeJS.ProcessEnv; // Optional: custom environment variables
logBufferSize?: number; // Optional: number of log lines to keep (default: 100)
}
export class ProcessMonitor {
private processWrapper: ProcessWrapper | null = null;
private config: IMonitorConfig;
private intervalId: NodeJS.Timeout | null = null;
private stopped: boolean = true; // Initially stopped until start() is called
private restartCount: number = 0;
private logger: Logger;
constructor(config: IMonitorConfig) {
this.config = config;
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
}
public start(): void {
// Reset the stopped flag so that new processes can spawn.
this.stopped = false;
this.log(`Starting process monitor.`);
this.spawnProcess();
// Set the monitoring interval.
const interval = this.config.monitorIntervalMs || 5000;
this.intervalId = setInterval((): void => {
if (this.processWrapper && this.processWrapper.getPid()) {
this.monitorProcessGroup(
this.processWrapper.getPid()!,
this.config.memoryLimitBytes,
);
}
}, interval);
}
private spawnProcess(): void {
// Don't spawn if the monitor has been stopped.
if (this.stopped) {
this.logger.debug('Not spawning process because monitor is stopped');
return;
}
this.logger.info(`Spawning process: ${this.config.command}`);
// Create a new process wrapper
this.processWrapper = new ProcessWrapper({
name: this.config.name || 'unnamed-process',
command: this.config.command,
args: this.config.args,
cwd: this.config.projectDir,
env: this.config.env,
logBuffer: this.config.logBufferSize,
});
// Set up event handlers
this.processWrapper.on('log', (log: IProcessLog): void => {
// Here we could add handlers to send logs somewhere
// For now, we just log system messages to the console
if (log.type === 'system') {
this.log(log.message);
}
});
this.processWrapper.on(
'exit',
(code: number | null, signal: string | null): void => {
const exitMsg = `Process exited with code ${code}, signal ${signal}.`;
this.logger.info(exitMsg);
this.log(exitMsg);
if (!this.stopped) {
this.logger.info('Restarting process...');
this.log('Restarting process...');
this.restartCount++;
this.spawnProcess();
} else {
this.logger.debug(
'Not restarting process because monitor is stopped',
);
}
},
);
this.processWrapper.on('error', (error: Error | ProcessError): void => {
const errorMsg =
error instanceof ProcessError
? `Process error: ${error.toString()}`
: `Process error: ${error.message}`;
this.logger.error(error);
this.log(errorMsg);
if (!this.stopped) {
this.logger.info('Restarting process due to error...');
this.log('Restarting process due to error...');
this.restartCount++;
this.spawnProcess();
} else {
this.logger.debug('Not restarting process because monitor is stopped');
}
});
// Start the process
try {
this.processWrapper.start();
} catch (error: Error | unknown) {
// The process wrapper will handle logging the error
// Just prevent it from bubbling up further
this.logger.error(
`Failed to start process: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
/**
* Monitor the process group's memory usage. If the total memory exceeds the limit,
* kill the process group so that the 'exit' handler can restart it.
*/
private async monitorProcessGroup(
pid: number,
memoryLimit: number,
): Promise<void> {
try {
const memoryUsage = await this.getProcessGroupMemory(pid);
this.logger.debug(
`Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`,
);
// Only log to the process log at longer intervals to avoid spamming
this.log(
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
memoryUsage,
)} (${memoryUsage} bytes)`,
);
if (memoryUsage > memoryLimit) {
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
memoryUsage,
)} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.`;
this.logger.warn(memoryLimitMsg);
this.log(memoryLimitMsg);
// Stop the process wrapper, which will trigger the exit handler and restart
if (this.processWrapper) {
this.processWrapper.stop();
}
}
} catch (error: Error | unknown) {
const processError = new ProcessError(
error instanceof Error ? error.message : String(error),
'ERR_MEMORY_MONITORING_FAILED',
{ pid },
);
this.logger.error(processError);
this.log(`Error monitoring process group: ${processError.toString()}`);
}
}
/**
* Get the total memory usage (in bytes) for the process group (the main process and its children).
*/
private getProcessGroupMemory(pid: number): Promise<number> {
return new Promise((resolve, reject) => {
this.logger.debug(
`Getting memory usage for process group with PID ${pid}`,
);
plugins.psTree(
pid,
(err: Error | null, children: Array<{ PID: string }>) => {
if (err) {
const processError = new ProcessError(
`Failed to get process tree: ${err.message}`,
'ERR_PSTREE_FAILED',
{ pid },
);
this.logger.debug(`psTree error: ${err.message}`);
return reject(processError);
}
// Include the main process and its children.
const pids: number[] = [
pid,
...children.map((child) => Number(child.PID)),
];
this.logger.debug(
`Found ${pids.length} processes in group with parent PID ${pid}`,
);
plugins.pidusage(
pids,
(err: Error | null, stats: Record<string, { memory: number }>) => {
if (err) {
const processError = new ProcessError(
`Failed to get process usage stats: ${err.message}`,
'ERR_PIDUSAGE_FAILED',
{ pids },
);
this.logger.debug(`pidusage error: ${err.message}`);
return reject(processError);
}
let totalMemory = 0;
for (const key in stats) {
totalMemory += stats[key].memory;
}
this.logger.debug(
`Total memory for process group: ${this.humanReadableBytes(totalMemory)}`,
);
resolve(totalMemory);
},
);
},
);
});
}
/**
* Convert a number of bytes into a human-readable string (e.g. "1.23 MB").
*/
private humanReadableBytes(bytes: number, decimals: number = 2): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];
}
/**
* Stop the monitor and prevent any further respawns.
*/
public stop(): void {
this.log('Stopping process monitor.');
this.stopped = true;
if (this.intervalId) {
clearInterval(this.intervalId);
}
if (this.processWrapper) {
this.processWrapper.stop();
}
}
/**
* Get the current logs from the process
*/
public getLogs(limit?: number): IProcessLog[] {
if (!this.processWrapper) {
return [];
}
return this.processWrapper.getLogs(limit);
}
/**
* Get the number of times the process has been restarted
*/
public getRestartCount(): number {
return this.restartCount;
}
/**
* Get the process ID if running
*/
public getPid(): number | null {
return this.processWrapper?.getPid() || null;
}
/**
* Get process uptime in milliseconds
*/
public getUptime(): number {
return this.processWrapper?.getUptime() || 0;
}
/**
* Check if the process is currently running
*/
public isRunning(): boolean {
return this.processWrapper?.isRunning() || false;
}
/**
* Helper method for logging messages with the instance name.
*/
private log(message: string): void {
const prefix = this.config.name ? `[${this.config.name}] ` : '';
console.log(prefix + message);
}
}

View File

@@ -0,0 +1,253 @@
import * as plugins from './plugins.js';
import { EventEmitter } from 'events';
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
export interface IProcessWrapperOptions {
command: string;
args?: string[];
cwd: string;
env?: NodeJS.ProcessEnv;
name: string;
logBuffer?: number; // Number of log lines to keep in memory (default: 100)
}
export interface IProcessLog {
timestamp: Date;
type: 'stdout' | 'stderr' | 'system';
message: string;
}
export class ProcessWrapper extends EventEmitter {
private process: plugins.childProcess.ChildProcess | null = null;
private options: IProcessWrapperOptions;
private logs: IProcessLog[] = [];
private logBufferSize: number;
private startTime: Date | null = null;
private logger: Logger;
constructor(options: IProcessWrapperOptions) {
super();
this.options = options;
this.logBufferSize = options.logBuffer || 100;
this.logger = new Logger(`ProcessWrapper:${options.name}`);
}
/**
* Start the wrapped process
*/
public start(): void {
this.addSystemLog('Starting process...');
try {
this.logger.debug(`Starting process: ${this.options.command}`);
if (this.options.args && this.options.args.length > 0) {
this.process = plugins.childProcess.spawn(
this.options.command,
this.options.args,
{
cwd: this.options.cwd,
env: this.options.env || process.env,
stdio: ['ignore', 'pipe', 'pipe'], // We need to pipe stdout and stderr
},
);
} else {
// Use shell mode to allow a full command string
this.process = plugins.childProcess.spawn(this.options.command, {
cwd: this.options.cwd,
env: this.options.env || process.env,
stdio: ['ignore', 'pipe', 'pipe'], // We need to pipe stdout and stderr
shell: true,
});
}
this.startTime = new Date();
// Handle process exit
this.process.on('exit', (code, signal) => {
const exitMessage = `Process exited with code ${code}, signal ${signal}`;
this.logger.info(exitMessage);
this.addSystemLog(exitMessage);
this.emit('exit', code, signal);
});
// Handle errors
this.process.on('error', (error) => {
const processError = new ProcessError(
error.message,
'ERR_PROCESS_EXECUTION',
{ command: this.options.command, pid: this.process?.pid },
);
this.logger.error(processError);
this.addSystemLog(`Process error: ${processError.toString()}`);
this.emit('error', processError);
});
// Capture stdout
if (this.process.stdout) {
this.process.stdout.on('data', (data) => {
const lines = data.toString().split('\n');
for (const line of lines) {
if (line.trim()) {
this.addLog('stdout', line);
}
}
});
}
// Capture stderr
if (this.process.stderr) {
this.process.stderr.on('data', (data) => {
const lines = data.toString().split('\n');
for (const line of lines) {
if (line.trim()) {
this.addLog('stderr', line);
}
}
});
}
this.addSystemLog(`Process started with PID ${this.process.pid}`);
this.logger.info(`Process started with PID ${this.process.pid}`);
this.emit('start', this.process.pid);
} catch (error: Error | unknown) {
const processError =
error instanceof ProcessError
? error
: new ProcessError(
error instanceof Error ? error.message : String(error),
'ERR_PROCESS_START_FAILED',
{ command: this.options.command },
);
this.logger.error(processError);
this.addSystemLog(`Failed to start process: ${processError.toString()}`);
this.emit('error', processError);
throw processError;
}
}
/**
* Stop the wrapped process
*/
public stop(): void {
if (!this.process) {
this.logger.debug('Stop called but no process is running');
this.addSystemLog('No process running');
return;
}
this.logger.info('Stopping process...');
this.addSystemLog('Stopping process...');
// First try SIGTERM for graceful shutdown
if (this.process.pid) {
try {
this.logger.debug(`Sending SIGTERM to process ${this.process.pid}`);
process.kill(this.process.pid, 'SIGTERM');
// Give it 5 seconds to shut down gracefully
setTimeout((): void => {
if (this.process && this.process.pid) {
this.logger.warn(
`Process ${this.process.pid} did not exit gracefully, force killing...`,
);
this.addSystemLog(
'Process did not exit gracefully, force killing...',
);
try {
process.kill(this.process.pid, 'SIGKILL');
} catch (error: Error | unknown) {
// Process might have exited between checks
this.logger.debug(
`Failed to send SIGKILL, process probably already exited: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
}
}, 5000);
} catch (error: Error | unknown) {
const processError = new ProcessError(
error instanceof Error ? error.message : String(error),
'ERR_PROCESS_STOP_FAILED',
{ pid: this.process.pid },
);
this.logger.error(processError);
this.addSystemLog(`Error stopping process: ${processError.toString()}`);
}
}
}
/**
* Get the process ID if running
*/
public getPid(): number | null {
return this.process?.pid || null;
}
/**
* Get the current logs
*/
public getLogs(limit: number = this.logBufferSize): IProcessLog[] {
// Return the most recent logs up to the limit
return this.logs.slice(-limit);
}
/**
* Get uptime in milliseconds
*/
public getUptime(): number {
if (!this.startTime) return 0;
return Date.now() - this.startTime.getTime();
}
/**
* Check if the process is currently running
*/
public isRunning(): boolean {
return this.process !== null && typeof this.process.exitCode !== 'number';
}
/**
* Add a log entry from stdout or stderr
*/
private addLog(type: 'stdout' | 'stderr', message: string): void {
const log: IProcessLog = {
timestamp: new Date(),
type,
message,
};
this.logs.push(log);
// Trim logs if they exceed buffer size
if (this.logs.length > this.logBufferSize) {
this.logs = this.logs.slice(-this.logBufferSize);
}
// Emit log event for potential handlers
this.emit('log', log);
}
/**
* Add a system log entry (not from the process itself)
*/
private addSystemLog(message: string): void {
const log: IProcessLog = {
timestamp: new Date(),
type: 'system',
message,
};
this.logs.push(log);
// Trim logs if they exceed buffer size
if (this.logs.length > this.logBufferSize) {
this.logs = this.logs.slice(-this.logBufferSize);
}
// Emit log event for potential handlers
this.emit('log', log);
}
}

433
ts/classes.tspm.ts Normal file
View File

@@ -0,0 +1,433 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import {
ProcessMonitor,
type IMonitorConfig,
} from './classes.processmonitor.js';
import { TspmConfig } from './classes.config.js';
import {
Logger,
ProcessError,
ConfigError,
ValidationError,
handleError,
} from './utils.errorhandler.js';
export interface IProcessConfig extends IMonitorConfig {
id: string; // Unique identifier for the process
autorestart: boolean; // Whether to restart the process automatically on crash
watch?: boolean; // Whether to watch for file changes and restart
watchPaths?: string[]; // Paths to watch for changes
}
export interface IProcessInfo {
id: string;
pid?: number;
status: 'online' | 'stopped' | 'errored';
memory: number;
cpu?: number;
uptime?: number;
restarts: number;
}
export interface IProcessLog {
timestamp: Date;
type: 'stdout' | 'stderr' | 'system';
message: string;
}
export class Tspm {
public processes: Map<string, ProcessMonitor> = new Map();
public processConfigs: Map<string, IProcessConfig> = new Map();
public processInfo: Map<string, IProcessInfo> = new Map();
private config: TspmConfig;
private configStorageKey = 'processes';
private logger: Logger;
constructor() {
this.logger = new Logger('Tspm');
this.config = new TspmConfig();
this.loadProcessConfigs();
}
/**
* Start a new process with the given configuration
*/
public async start(config: IProcessConfig): Promise<void> {
this.logger.info(`Starting process with id '${config.id}'`);
// Validate config
if (!config.id || !config.command || !config.projectDir) {
throw new ValidationError(
'Invalid process configuration: missing required fields',
'ERR_INVALID_CONFIG',
{ config },
);
}
// Check if process with this id already exists
if (this.processes.has(config.id)) {
throw new ValidationError(
`Process with id '${config.id}' already exists`,
'ERR_DUPLICATE_PROCESS',
);
}
try {
// Create and store process config
this.processConfigs.set(config.id, config);
// Initialize process info
this.processInfo.set(config.id, {
id: config.id,
status: 'stopped',
memory: 0,
restarts: 0,
});
// Create and start process monitor
const monitor = new ProcessMonitor({
name: config.name || config.id,
projectDir: config.projectDir,
command: config.command,
args: config.args,
memoryLimitBytes: config.memoryLimitBytes,
monitorIntervalMs: config.monitorIntervalMs,
env: config.env,
logBufferSize: config.logBufferSize,
});
this.processes.set(config.id, monitor);
monitor.start();
// Update process info
this.updateProcessInfo(config.id, { status: 'online' });
// Save updated configs
await this.saveProcessConfigs();
this.logger.info(`Successfully started process with id '${config.id}'`);
} catch (error: Error | unknown) {
// Clean up in case of error
this.processConfigs.delete(config.id);
this.processInfo.delete(config.id);
this.processes.delete(config.id);
if (error instanceof Error) {
this.logger.error(error);
throw new ProcessError(
`Failed to start process: ${error.message}`,
'ERR_PROCESS_START_FAILED',
{ id: config.id, command: config.command },
);
} else {
const genericError = new ProcessError(
`Failed to start process: ${String(error)}`,
'ERR_PROCESS_START_FAILED',
{ id: config.id },
);
this.logger.error(genericError);
throw genericError;
}
}
}
/**
* Stop a process by id
*/
public async stop(id: string): Promise<void> {
this.logger.info(`Stopping process with id '${id}'`);
const monitor = this.processes.get(id);
if (!monitor) {
const error = new ValidationError(
`Process with id '${id}' not found`,
'ERR_PROCESS_NOT_FOUND',
);
this.logger.error(error);
throw error;
}
try {
monitor.stop();
this.updateProcessInfo(id, { status: 'stopped' });
this.logger.info(`Successfully stopped process with id '${id}'`);
} catch (error: Error | unknown) {
const processError = new ProcessError(
`Failed to stop process: ${error instanceof Error ? error.message : String(error)}`,
'ERR_PROCESS_STOP_FAILED',
{ id },
);
this.logger.error(processError);
throw processError;
}
// Don't remove from the maps, just mark as stopped
// This allows it to be restarted later
}
/**
* Restart a process by id
*/
public async restart(id: string): Promise<void> {
this.logger.info(`Restarting process with id '${id}'`);
const monitor = this.processes.get(id);
const config = this.processConfigs.get(id);
if (!monitor || !config) {
const error = new ValidationError(
`Process with id '${id}' not found`,
'ERR_PROCESS_NOT_FOUND',
);
this.logger.error(error);
throw error;
}
try {
// Stop and then start the process
monitor.stop();
// Create a new monitor instance
const newMonitor = new ProcessMonitor({
name: config.name || config.id,
projectDir: config.projectDir,
command: config.command,
args: config.args,
memoryLimitBytes: config.memoryLimitBytes,
monitorIntervalMs: config.monitorIntervalMs,
env: config.env,
logBufferSize: config.logBufferSize,
});
this.processes.set(id, newMonitor);
newMonitor.start();
// Update restart count
const info = this.processInfo.get(id);
if (info) {
this.updateProcessInfo(id, {
status: 'online',
restarts: info.restarts + 1,
});
}
this.logger.info(`Successfully restarted process with id '${id}'`);
} catch (error: Error | unknown) {
const processError = new ProcessError(
`Failed to restart process: ${error instanceof Error ? error.message : String(error)}`,
'ERR_PROCESS_RESTART_FAILED',
{ id },
);
this.logger.error(processError);
throw processError;
}
}
/**
* Delete a process by id
*/
public async delete(id: string): Promise<void> {
this.logger.info(`Deleting process with id '${id}'`);
// Check if process exists
if (!this.processConfigs.has(id)) {
const error = new ValidationError(
`Process with id '${id}' not found`,
'ERR_PROCESS_NOT_FOUND',
);
this.logger.error(error);
throw error;
}
// Stop the process if it's running
try {
if (this.processes.has(id)) {
await this.stop(id);
}
// Remove from all maps
this.processes.delete(id);
this.processConfigs.delete(id);
this.processInfo.delete(id);
// Save updated configs
await this.saveProcessConfigs();
this.logger.info(`Successfully deleted process with id '${id}'`);
} catch (error: Error | unknown) {
// Even if stop fails, we should still try to delete the configuration
try {
this.processes.delete(id);
this.processConfigs.delete(id);
this.processInfo.delete(id);
await this.saveProcessConfigs();
this.logger.info(
`Successfully deleted process with id '${id}' after stopping failure`,
);
} catch (deleteError: Error | unknown) {
const configError = new ConfigError(
`Failed to delete process configuration: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`,
'ERR_CONFIG_DELETE_FAILED',
{ id },
);
this.logger.error(configError);
throw configError;
}
}
}
/**
* Get a list of all process infos
*/
public list(): IProcessInfo[] {
return Array.from(this.processInfo.values());
}
/**
* Get detailed info for a specific process
*/
public describe(
id: string,
): { config: IProcessConfig; info: IProcessInfo } | null {
const config = this.processConfigs.get(id);
const info = this.processInfo.get(id);
if (!config || !info) {
return null;
}
return { config, info };
}
/**
* Get process logs
*/
public getLogs(id: string, limit?: number): IProcessLog[] {
const monitor = this.processes.get(id);
if (!monitor) {
return [];
}
return monitor.getLogs(limit);
}
/**
* Start all saved processes
*/
public async startAll(): Promise<void> {
for (const [id, config] of this.processConfigs.entries()) {
if (!this.processes.has(id)) {
await this.start(config);
}
}
}
/**
* Stop all running processes
*/
public async stopAll(): Promise<void> {
for (const id of this.processes.keys()) {
await this.stop(id);
}
}
/**
* Restart all processes
*/
public async restartAll(): Promise<void> {
for (const id of this.processes.keys()) {
await this.restart(id);
}
}
/**
* Update the info for a process
*/
private updateProcessInfo(id: string, update: Partial<IProcessInfo>): void {
const info = this.processInfo.get(id);
if (info) {
this.processInfo.set(id, { ...info, ...update });
}
}
/**
* Save all process configurations to config storage
*/
private async saveProcessConfigs(): Promise<void> {
this.logger.debug('Saving process configurations to storage');
try {
const configs = Array.from(this.processConfigs.values());
await this.config.writeKey(
this.configStorageKey,
JSON.stringify(configs),
);
this.logger.debug(`Saved ${configs.length} process configurations`);
} catch (error: Error | unknown) {
const configError = new ConfigError(
`Failed to save process configurations: ${error instanceof Error ? error.message : String(error)}`,
'ERR_CONFIG_SAVE_FAILED',
);
this.logger.error(configError);
throw configError;
}
}
/**
* Load process configurations from config storage
*/
public async loadProcessConfigs(): Promise<void> {
this.logger.debug('Loading process configurations from storage');
try {
const configsJson = await this.config.readKey(this.configStorageKey);
if (configsJson) {
try {
const configs = JSON.parse(configsJson) as IProcessConfig[];
this.logger.debug(`Loaded ${configs.length} process configurations`);
for (const config of configs) {
// Validate config
if (!config.id || !config.command || !config.projectDir) {
this.logger.warn(
`Skipping invalid process config for id '${config.id || 'unknown'}'`,
);
continue;
}
this.processConfigs.set(config.id, config);
// Initialize process info
this.processInfo.set(config.id, {
id: config.id,
status: 'stopped',
memory: 0,
restarts: 0,
});
}
} catch (parseError: Error | unknown) {
const configError = new ConfigError(
`Failed to parse process configurations: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
'ERR_CONFIG_PARSE_FAILED',
);
this.logger.error(configError);
throw configError;
}
} else {
this.logger.info('No saved process configurations found');
}
} catch (error: Error | unknown) {
// Only throw if it's not the "no configs found" case
if (error instanceof ConfigError) {
throw error;
}
// If no configs found or error reading, just continue with empty configs
this.logger.info(
'No saved process configurations found or error reading them',
);
}
}
}

638
ts/cli.ts Normal file
View File

@@ -0,0 +1,638 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { tspmIpcClient } from './classes.ipcclient.js';
import { Logger, LogLevel } from './utils.errorhandler.js';
import type { IProcessConfig } from './classes.tspm.js';
export interface CliArguments {
verbose?: boolean;
watch?: boolean;
memory?: string;
cwd?: string;
daemon?: boolean;
test?: boolean;
name?: string;
autorestart?: boolean;
watchPaths?: string[];
[key: string]: any;
}
// Helper function to parse memory strings (e.g., "512MB", "2GB")
function parseMemoryString(memStr: string): number {
const units = {
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
};
const match = memStr.toUpperCase().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/);
if (!match) {
throw new Error(
`Invalid memory format: ${memStr}. Use format like "512MB" or "2GB"`,
);
}
const value = parseFloat(match[1]);
const unit = (match[2] || 'MB') as keyof typeof units;
return Math.floor(value * units[unit]);
}
// Helper function to format memory for display
function formatMemory(bytes: number): string {
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
} else if (bytes >= 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
} else if (bytes >= 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
} else {
return `${bytes} B`;
}
}
// Helper function for padding strings
function pad(str: string, length: number): string {
return str.length > length
? str.substring(0, length - 3) + '...'
: str.padEnd(length);
}
export const run = async (): Promise<void> => {
const cliLogger = new Logger('CLI');
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
// Check if debug mode is enabled
const debugMode = process.env.TSPM_DEBUG === 'true';
if (debugMode) {
cliLogger.setLevel(LogLevel.DEBUG);
cliLogger.debug('Debug mode enabled');
}
const smartcliInstance = new plugins.smartcli.Smartcli();
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
// Default command - show help and list processes
smartcliInstance.standardCommand().subscribe({
next: async (argvArg: CliArguments) => {
console.log(
`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`,
);
console.log('Usage: tspm [command] [options]');
console.log('\nCommands:');
console.log(' start <script> Start a process');
console.log(' list List all processes');
console.log(' stop <id> Stop a process');
console.log(' restart <id> Restart a process');
console.log(' delete <id> Delete a process');
console.log(' describe <id> Show details for a process');
console.log(' logs <id> Show logs for a process');
console.log(' start-all Start all saved processes');
console.log(' stop-all Stop all processes');
console.log(' restart-all Restart all processes');
console.log('\nDaemon Commands:');
console.log(' daemon start Start the TSPM daemon');
console.log(' daemon stop Stop the TSPM daemon');
console.log(' daemon status Show daemon status');
console.log(
'\nUse tspm [command] --help for more information about a command.',
);
// Show current process list
console.log('\nProcess List:');
try {
const response = await tspmIpcClient.request('list', {});
const processes = response.processes;
if (processes.length === 0) {
console.log(
' No processes running. Use "tspm start" to start a process.',
);
} else {
console.log(
'┌─────────┬─────────────┬───────────┬───────────┬──────────┐',
);
console.log(
'│ ID │ Name │ Status │ 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';
console.log(
`${pad(proc.id, 7)}${pad(proc.id, 11)}${statusColor}${pad(proc.status, 9)}${resetColor}${pad(formatMemory(proc.memory), 9)}${pad(proc.restarts.toString(), 8)}`,
);
}
console.log(
'└─────────┴─────────────┴───────────┴───────────┴──────────┘',
);
}
} catch (error) {
console.error(
'Error: Could not connect to TSPM daemon. Use "tspm daemon start" to start it.',
);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Start command
smartcliInstance.addCommand('start').subscribe({
next: async (argvArg: CliArguments) => {
try {
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; // Default 512MB
const projectDir = argvArg.cwd || process.cwd();
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: script,
projectDir,
memoryLimitBytes: memoryLimit,
autorestart,
watch,
watchPaths,
};
console.log(`Starting process: ${name}`);
console.log(` Command: ${script}`);
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}`);
} catch (error) {
console.error('Error starting process:', error.message);
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Stop command
smartcliInstance.addCommand('stop').subscribe({
next: async (argvArg: CliArguments) => {
try {
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 });
if (response.success) {
console.log(`${response.message}`);
} else {
console.error(`✗ Failed to stop process: ${response.message}`);
}
} catch (error) {
console.error('Error stopping process:', error.message);
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Restart command
smartcliInstance.addCommand('restart').subscribe({
next: async (argvArg: CliArguments) => {
try {
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(`✓ Process restarted successfully`);
console.log(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`);
} catch (error) {
console.error('Error restarting process:', error.message);
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Delete command
smartcliInstance.addCommand('delete').subscribe({
next: async (argvArg: CliArguments) => {
try {
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 });
if (response.success) {
console.log(`${response.message}`);
} else {
console.error(`✗ Failed to delete process: ${response.message}`);
}
} catch (error) {
console.error('Error deleting process:', error.message);
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// List command
smartcliInstance.addCommand('list').subscribe({
next: async (argvArg: CliArguments) => {
try {
const response = await tspmIpcClient.request('list', {});
const processes = response.processes;
if (processes.length === 0) {
console.log('No processes running.');
} else {
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';
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(
'└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘',
);
}
} catch (error) {
console.error('Error listing processes:', error.message);
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Describe command
smartcliInstance.addCommand('describe').subscribe({
next: async (argvArg: CliArguments) => {
try {
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(', ')}`,
);
}
}
} catch (error) {
console.error('Error describing process:', error.message);
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Logs command
smartcliInstance.addCommand('logs').subscribe({
next: async (argvArg: CliArguments) => {
try {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm logs <id>');
return;
}
const lines = argvArg.lines || 50;
const response = await tspmIpcClient.request('getLogs', { id, lines });
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]' : '[ERR]';
console.log(`${timestamp} ${prefix} ${log.message}`);
}
} catch (error) {
console.error('Error getting logs:', error.message);
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Start-all command
smartcliInstance.addCommand('start-all').subscribe({
next: async (argvArg: CliArguments) => {
try {
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.failed.length > 0) {
console.log(`✗ Failed to start ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
}
} catch (error) {
console.error('Error starting all processes:', error.message);
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Stop-all command
smartcliInstance.addCommand('stop-all').subscribe({
next: async (argvArg: CliArguments) => {
try {
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.failed.length > 0) {
console.log(`✗ Failed to stop ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
}
} catch (error) {
console.error('Error stopping all processes:', error.message);
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Restart-all command
smartcliInstance.addCommand('restart-all').subscribe({
next: async (argvArg: CliArguments) => {
try {
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.failed.length > 0) {
console.log(
`✗ Failed to restart ${response.failed.length} processes:`,
);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
}
} catch (error) {
console.error('Error restarting all processes:', error.message);
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Daemon commands
smartcliInstance.addCommand('daemon').subscribe({
next: async (argvArg: CliArguments) => {
const subCommand = argvArg._[1];
switch (subCommand) {
case 'start':
try {
const status = await tspmIpcClient.getDaemonStatus();
if (status) {
console.log('TSPM daemon is already running');
console.log(` PID: ${status.pid}`);
console.log(
` Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
);
console.log(` Processes: ${status.processCount}`);
return;
}
console.log('Starting TSPM daemon...');
await tspmIpcClient.connect();
console.log('✓ TSPM daemon started successfully');
const newStatus = await tspmIpcClient.getDaemonStatus();
if (newStatus) {
console.log(` PID: ${newStatus.pid}`);
}
} catch (error) {
console.error('Error starting daemon:', error.message);
process.exit(1);
}
break;
case 'stop':
try {
console.log('Stopping TSPM daemon...');
await tspmIpcClient.stopDaemon(true);
console.log('✓ TSPM daemon stopped successfully');
} catch (error) {
console.error('Error stopping daemon:', error.message);
process.exit(1);
}
break;
case 'status':
try {
const status = await tspmIpcClient.getDaemonStatus();
if (!status) {
console.log('TSPM daemon is not running');
console.log('Use "tspm daemon start" to start it');
return;
}
console.log('TSPM Daemon Status:');
console.log('─'.repeat(40));
console.log(`Status: ${status.status}`);
console.log(`PID: ${status.pid}`);
console.log(
`Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
);
console.log(`Processes: ${status.processCount}`);
console.log(
`Memory: ${formatMemory(status.memoryUsage || 0)}`,
);
console.log(`CPU: ${status.cpuUsage?.toFixed(1) || 0}s`);
} catch (error) {
console.error('Error getting daemon status:', error.message);
process.exit(1);
}
break;
default:
console.log('Usage: tspm daemon <command>');
console.log('\nCommands:');
console.log(' start Start the TSPM daemon');
console.log(' stop Stop the TSPM daemon');
console.log(' status Show daemon status');
break;
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Start parsing commands
smartcliInstance.startParse();
};

9
ts/daemon.ts Normal file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env node
import { startDaemon } from './classes.daemon.js';
// Start the daemon
startDaemon().catch((error) => {
console.error('Failed to start daemon:', error);
process.exit(1);
});

View File

@@ -1,3 +1,14 @@
import * as plugins from './tspm.plugins.js';
export * from './classes.tspm.js';
export * from './classes.processmonitor.js';
export * from './classes.daemon.js';
export * from './classes.ipcclient.js';
export * from './ipc.types.js';
export let demoExport = 'Hi there! :) This is an exported string';
import * as cli from './cli.js';
/**
* called to run as cli
*/
export const runCli = async () => {
await cli.run();
};

201
ts/ipc.types.ts Normal file
View File

@@ -0,0 +1,201 @@
import type {
IProcessConfig,
IProcessInfo,
IProcessLog,
} from './classes.tspm.js';
// Base message types
export interface IpcRequest<T = any> {
id: string;
method: string;
params: T;
}
export interface IpcResponse<T = any> {
id: string;
success: boolean;
result?: T;
error?: {
code: number;
message: string;
data?: any;
};
}
// Request/Response pairs for each operation
// Start command
export interface StartRequest {
config: IProcessConfig;
}
export interface StartResponse {
processId: string;
pid?: number;
status: 'online' | 'stopped' | 'errored';
}
// Stop command
export interface StopRequest {
id: string;
}
export interface StopResponse {
success: boolean;
message?: string;
}
// Restart command
export interface RestartRequest {
id: string;
}
export interface RestartResponse {
processId: string;
pid?: number;
status: 'online' | 'stopped' | 'errored';
}
// Delete command
export interface DeleteRequest {
id: string;
}
export interface DeleteResponse {
success: boolean;
message?: string;
}
// List command
export interface ListRequest {
// No parameters needed
}
export interface ListResponse {
processes: IProcessInfo[];
}
// Describe command
export interface DescribeRequest {
id: string;
}
export interface DescribeResponse {
processInfo: IProcessInfo;
config: IProcessConfig;
}
// Get logs command
export interface GetLogsRequest {
id: string;
lines?: number;
}
export interface GetLogsResponse {
logs: IProcessLog[];
}
// Start all command
export interface StartAllRequest {
// No parameters needed
}
export interface StartAllResponse {
started: string[];
failed: Array<{
id: string;
error: string;
}>;
}
// Stop all command
export interface StopAllRequest {
// No parameters needed
}
export interface StopAllResponse {
stopped: string[];
failed: Array<{
id: string;
error: string;
}>;
}
// Restart all command
export interface RestartAllRequest {
// No parameters needed
}
export interface RestartAllResponse {
restarted: string[];
failed: Array<{
id: string;
error: string;
}>;
}
// Daemon status command
export interface DaemonStatusRequest {
// No parameters needed
}
export interface DaemonStatusResponse {
status: 'running' | 'stopped';
pid?: number;
uptime?: number;
processCount: number;
memoryUsage?: number;
cpuUsage?: number;
}
// Daemon shutdown command
export interface DaemonShutdownRequest {
graceful?: boolean;
timeout?: number; // milliseconds
}
export interface DaemonShutdownResponse {
success: boolean;
message?: string;
}
// Heartbeat command
export interface HeartbeatRequest {
// No parameters needed
}
export interface HeartbeatResponse {
timestamp: number;
status: 'healthy' | 'degraded';
}
// Type mappings for methods
export type IpcMethodMap = {
start: { request: StartRequest; response: StartResponse };
stop: { request: StopRequest; response: StopResponse };
restart: { request: RestartRequest; response: RestartResponse };
delete: { request: DeleteRequest; response: DeleteResponse };
list: { request: ListRequest; response: ListResponse };
describe: { request: DescribeRequest; response: DescribeResponse };
getLogs: { request: GetLogsRequest; response: GetLogsResponse };
startAll: { request: StartAllRequest; response: StartAllResponse };
stopAll: { request: StopAllRequest; response: StopAllResponse };
restartAll: { request: RestartAllRequest; response: RestartAllResponse };
'daemon:status': {
request: DaemonStatusRequest;
response: DaemonStatusResponse;
};
'daemon:shutdown': {
request: DaemonShutdownRequest;
response: DaemonShutdownResponse;
};
heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse };
};
// Helper type to extract request type for a method
export type RequestForMethod<M extends keyof IpcMethodMap> =
IpcMethodMap[M]['request'];
// Helper type to extract response type for a method
export type ResponseForMethod<M extends keyof IpcMethodMap> =
IpcMethodMap[M]['response'];

10
ts/paths.ts Normal file
View File

@@ -0,0 +1,10 @@
import * as plugins from './plugins.js';
export const packageDir: string = plugins.path.join(
plugins.smartpath.get.dirnameFromImportMetaUrl(import.meta.url),
'..',
);
export const cwd: string = process.cwd();
import * as os from 'os';
export const tspmDir: string = plugins.path.join(os.homedir(), '.tspm');

24
ts/plugins.ts Normal file
View File

@@ -0,0 +1,24 @@
// native scope
import * as childProcess from 'child_process';
import * as path from 'node:path';
// Export with explicit module types
export { childProcess, path };
// @push.rocks scope
import * as npmextra from '@push.rocks/npmextra';
import * as projectinfo from '@push.rocks/projectinfo';
import * as smartcli from '@push.rocks/smartcli';
import * as smartdaemon from '@push.rocks/smartdaemon';
import * as smartipc from '@push.rocks/smartipc';
import * as smartpath from '@push.rocks/smartpath';
// Export with explicit module types
export { npmextra, projectinfo, smartcli, smartdaemon, smartipc, smartpath };
// third-party scope
import psTree from 'ps-tree';
import pidusage from 'pidusage';
// Add explicit types for third-party exports
export { psTree, pidusage };

View File

@@ -1,2 +0,0 @@
const removeme = {};
export { removeme };

152
ts/utils.errorhandler.ts Normal file
View File

@@ -0,0 +1,152 @@
/**
* Centralized error handling utility for TSPM
*/
// Define error types
export enum ErrorType {
CONFIG = 'ConfigError',
PROCESS = 'ProcessError',
RUNTIME = 'RuntimeError',
VALIDATION = 'ValidationError',
UNKNOWN = 'UnknownError',
}
// Base error class with type and code support
export class TspmError extends Error {
type: ErrorType;
code: string;
details?: Record<string, any>;
constructor(
message: string,
type: ErrorType = ErrorType.UNKNOWN,
code: string = 'ERR_UNKNOWN',
details?: Record<string, any>,
) {
super(message);
this.name = type;
this.type = type;
this.code = code;
this.details = details;
// Preserve proper stack trace
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
toString(): string {
return `[${this.type}:${this.code}] ${this.message}`;
}
}
// Specific error classes
export class ConfigError extends TspmError {
constructor(
message: string,
code: string = 'ERR_CONFIG',
details?: Record<string, any>,
) {
super(message, ErrorType.CONFIG, code, details);
}
}
export class ProcessError extends TspmError {
constructor(
message: string,
code: string = 'ERR_PROCESS',
details?: Record<string, any>,
) {
super(message, ErrorType.PROCESS, code, details);
}
}
export class ValidationError extends TspmError {
constructor(
message: string,
code: string = 'ERR_VALIDATION',
details?: Record<string, any>,
) {
super(message, ErrorType.VALIDATION, code, details);
}
}
// Utility for handling any error type
export const handleError = (error: Error | unknown): TspmError => {
if (error instanceof TspmError) {
return error;
}
if (error instanceof Error) {
return new TspmError(error.message, ErrorType.UNKNOWN, 'ERR_UNKNOWN', {
originalError: error,
});
}
return new TspmError(String(error), ErrorType.UNKNOWN, 'ERR_UNKNOWN');
};
// Logger with different log levels
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
NONE = 4,
}
export class Logger {
private static instance: Logger;
private level: LogLevel = LogLevel.INFO;
private componentName: string;
constructor(componentName: string) {
this.componentName = componentName;
}
static getInstance(componentName: string): Logger {
if (!Logger.instance) {
Logger.instance = new Logger(componentName);
}
return Logger.instance;
}
setLevel(level: LogLevel): void {
this.level = level;
}
private formatMessage(message: string): string {
const timestamp = new Date().toISOString();
return `[${timestamp}] [${this.componentName}] ${message}`;
}
debug(message: string): void {
if (this.level <= LogLevel.DEBUG) {
console.log(this.formatMessage(`DEBUG: ${message}`));
}
}
info(message: string): void {
if (this.level <= LogLevel.INFO) {
console.log(this.formatMessage(message));
}
}
warn(message: string): void {
if (this.level <= LogLevel.WARN) {
console.warn(this.formatMessage(`WARNING: ${message}`));
}
}
error(error: Error | unknown): void {
if (this.level <= LogLevel.ERROR) {
const tspmError = handleError(error);
console.error(this.formatMessage(`ERROR: ${tspmError.toString()}`));
// In debug mode, also log stack trace
if (this.level === LogLevel.DEBUG && tspmError.stack) {
console.error(tspmError.stack);
}
}
}
}

View File

@@ -11,7 +11,5 @@
"baseUrl": ".",
"paths": {}
},
"exclude": [
"dist_*/**/*.d.ts"
]
"exclude": ["dist_*/**/*.d.ts"]
}