Compare commits

..

24 Commits

Author SHA1 Message Date
1c2310c185 2.0.0
Some checks failed
Default (tags) / security (push) Successful in 1m2s
Default (tags) / test (push) Failing after 1m31s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-28 10:39:35 +00:00
d33a001edc BREAKING CHANGE(daemon): Refactor daemon lifecycle and service management: remove IPC auto-spawn, add TspmServiceManager and CLI enable/disable 2025-08-28 10:39:35 +00:00
35b6a6a8d0 1.8.0
Some checks failed
Default (tags) / security (push) Failing after 10m29s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-08-26 15:00:15 +00:00
50c5fdb0ea 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 2025-08-26 15:00:15 +00:00
4e0944034b 1.7.0
Some checks failed
Default (tags) / security (push) Successful in 49s
Default (tags) / test (push) Failing after 4m6s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-25 12:33:57 +00:00
ca0dfa6432 feat(readme): Add comprehensive README with detailed usage, command reference, daemon management, architecture and development instructions 2025-08-25 12:33:57 +00:00
b020cdcbf4 1.6.1
Some checks failed
Default (tags) / security (push) Successful in 51s
Default (tags) / test (push) Failing after 4m6s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-25 12:10:56 +00:00
80fae0589f fix(daemon): Fix smartipc integration and add daemon/ipc integration tests 2025-08-25 12:10:56 +00:00
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
29 changed files with 7613 additions and 2340 deletions

4
.gitignore vendored
View File

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

View File

@@ -1,6 +1,116 @@
# Changelog # Changelog
## 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).
- Add TspmServiceManager to manage the daemon as a systemd service via smartdaemon (enable/disable/reload/status helpers).
- CLI: add 'enable' and 'disable' commands to install/uninstall the daemon as a system service and add 'daemon start-service' entrypoint used by systemd.
- CLI: improve error handling and user hints when the daemon is not running (suggests `tspm daemon start` or `tspm enable`).
- IPC client: removed startDaemon() and related auto-reconnect/start logic; request() no longer auto-reconnects or implicitly start the daemon.
- Export TspmServiceManager from the package index so service management is part of the public API.
- 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
- Daemon: initialize SmartIpc server with heartbeat and publish process logs to topic `logs.<processId>`; write PID file and start heartbeat monitoring
- Tspm: re-emit monitor log events as 'process:log' so daemon can broadcast logs
- ProcessWrapper: include seq and runId on IProcessLog entries and maintain nextSeq/runId (adds sequencing to logs); default log buffer size applied
- TspmIpcClient: improved connect options (retries, timeouts, heartbeat handling), add subscribe/unsubscribe for real-time logs, and use SmartIpc.waitForServer when starting daemon
- CLI: add --follow flag to `logs` command to stream live logs, detect sequence gaps/duplicates, and handle graceful cleanup on Ctrl+C
- ProcessMonitor: now extends EventEmitter and re-emits process logs for upstream consumption
- 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
- Included usage examples and CLI command reference for start/stop/restart/delete/list/describe/logs and batch/daemon commands
- Added human-friendly memory formatting and examples, process and daemon status outputs, and programmatic TypeScript usage snippet
- 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
- Switch IPC handler registration to use onMessage and add explicit Request/Response typing for handlers
- Update IPC client to use SmartIpc.createClient and improve daemon start/connect logic
- 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.
- 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) ## 2025-03-01 - 1.1.0 - feat(core)
Introduce ProcessMonitor class and integrate native and external plugins Introduce ProcessMonitor class and integrate native and external plugins
- Added a new ProcessMonitor class to manage and monitor child processes with memory constraints. - Added a new ProcessMonitor class to manage and monitor child processes with memory constraints.
@@ -8,14 +118,16 @@ Introduce ProcessMonitor class and integrate native and external plugins
- Adjusted index and related files for improved modular structure. - Adjusted index and related files for improved modular structure.
## 2025-02-24 - 1.0.3 - fix(core) ## 2025-02-24 - 1.0.3 - fix(core)
Corrected description in package.json and readme.md from 'task manager' to 'process manager'. Corrected description in package.json and readme.md from 'task manager' to 'process manager'.
- Updated the project description in package.json. - Updated the project description in package.json.
- Aligned the description in readme.md with package.json. - Aligned the description in readme.md with package.json.
## 2025-02-24 - 1.0.2 - fix(core) ## 2025-02-24 - 1.0.2 - fix(core)
Internal changes with no functional impact. Internal changes with no functional impact.
## 2025-02-24 - 1.0.1 - initial release ## 2025-02-24 - 1.0.1 - initial release
Initial release with baseline functionality. Initial release with baseline functionality.

0
cli.js Normal file → Executable file
View File

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tspm", "name": "@git.zone/tspm",
"version": "1.1.0", "version": "2.0.0",
"private": false, "private": false,
"description": "a no fuzz process manager", "description": "a no fuzz process manager",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@@ -11,18 +11,29 @@
"scripts": { "scripts": {
"test": "(tstest test/ --web)", "test": "(tstest test/ --web)",
"build": "(tsbuild --web --allowimplicitany)", "build": "(tsbuild --web --allowimplicitany)",
"buildDocs": "(tsdoc)" "buildDocs": "(tsdoc)",
"start": "(tsrun ./cli.ts -v)"
},
"bin": {
"tspm": "./cli.js"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.1.25", "@git.zone/tsbuild": "^2.6.7",
"@git.zone/tsbundle": "^2.0.5", "@git.zone/tsbundle": "^2.5.1",
"@git.zone/tsrun": "^1.2.46", "@git.zone/tsrun": "^1.2.46",
"@git.zone/tstest": "^1.0.44", "@git.zone/tstest": "^2.3.5",
"@push.rocks/tapbundle": "^5.0.15", "@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^20.8.7" "@types/node": "^22.13.10"
}, },
"dependencies": { "dependencies": {
"@push.rocks/smartpath": "^5.0.18" "@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.1.2",
"@push.rocks/smartpath": "^6.0.0",
"pidusage": "^4.0.1",
"ps-tree": "^1.2.0"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -45,6 +56,12 @@
"readme.md" "readme.md"
], ],
"pnpm": { "pnpm": {
"overrides": {} "overrides": {},
} "onlyBuiltDependencies": [
"esbuild",
"mongodb-memory-server",
"puppeteer"
]
},
"packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748"
} }

5716
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

341
readme.md
View File

@@ -1,7 +1,340 @@
# @git.zone/tspm # @git.zone/tspm 🚀
a no fuzz process manager **TypeScript Process Manager** - A robust, no-fuss process manager designed specifically for TypeScript and Node.js applications. Built for developers who need reliable process management without the complexity.
## How to create the docs ## 🎯 What TSPM Does
To create docs run gitzone aidoc. TSPM is your production-ready process manager that handles the hard parts of running Node.js applications:
- **Automatic Memory Management** - Set memory limits and let TSPM handle the rest
- **Smart Auto-Restart** - Crashed processes come back automatically (when you want them to)
- **File Watching** - Auto-restart on file changes during development
- **Process Groups** - Track parent and child processes together
- **Daemon Architecture** - Survives terminal sessions with a persistent background daemon
- **Beautiful CLI** - Clean, informative terminal output with real-time status
- **Structured Logging** - Capture and manage stdout/stderr with intelligent buffering
- **Zero Config** - Works out of the box, customize when you need to
## 📦 Installation
```bash
# Install globally
npm install -g @git.zone/tspm
# Or with pnpm (recommended)
pnpm add -g @git.zone/tspm
# Or use in your project
npm install --save-dev @git.zone/tspm
```
## 🚀 Quick Start
```bash
# Start the daemon (happens automatically on first use)
tspm daemon start
# Start a process
tspm start server.js --name my-server
# Start with memory limit
tspm start app.js --memory 512MB --name my-app
# Start with file watching (great for development)
tspm start dev.js --watch --name dev-server
# List all processes
tspm list
# Check process details
tspm describe my-server
# View logs
tspm logs my-server --lines 100
# Stop a process
tspm stop my-server
# Restart a process
tspm restart my-server
```
## 📋 Command Reference
### 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)
- `--watch` - Enable file watching for auto-restart
- `--watch-paths <paths>` - Comma-separated paths to watch (with --watch)
- `--autorestart` - Auto-restart on crash (default: true)
**Examples:**
```bash
# Simple start
tspm start server.js
# Production setup with 2GB memory
tspm start app.js --name production-api --memory 2GB
# Development with watching
tspm start dev-server.js --watch --watch-paths "src,config" --name dev
# Custom working directory
tspm start ../other-project/index.js --cwd ../other-project --name other
```
#### `tspm stop <id>`
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
```bash
tspm stop my-server
```
#### `tspm restart <id>`
Stop and restart a process with the same configuration.
```bash
tspm restart my-server
```
#### `tspm delete <id>`
Stop and remove a process from TSPM management.
```bash
tspm delete old-server
```
### Monitoring & Information
#### `tspm list`
Display all managed processes in a beautiful table.
```bash
tspm list
# Output:
┌─────────┬─────────────┬───────────┬───────────┬──────────┐
│ ID │ Name │ Status │ Memory │ Restarts │
├─────────┼─────────────┼───────────┼───────────┼──────────┤
│ my-app │ my-app │ online │ 245.3 MB │ 0
│ worker │ worker │ online │ 128.7 MB │ 2
└─────────┴─────────────┴───────────┴───────────┴──────────┘
```
#### `tspm describe <id>`
Get detailed information about a specific process.
```bash
tspm describe my-server
# Output:
Process Details: my-server
────────────────────────────────────────
Status: online
PID: 45123
Memory: 245.3 MB
CPU: 2.3%
Uptime: 3600s
Restarts: 0
Configuration:
Command: server.js
Directory: /home/user/project
Memory Limit: 2 GB
Auto-restart: true
Watch: enabled
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
tspm logs my-server --lines 100
```
### Batch Operations
#### `tspm start-all`
Start all saved processes at once.
```bash
tspm start-all
```
#### `tspm stop-all`
Stop all running processes.
```bash
tspm stop-all
```
#### `tspm restart-all`
Restart all running processes.
```bash
tspm restart-all
```
### Daemon Management
#### `tspm daemon start`
Start the TSPM daemon (happens automatically on first command).
```bash
tspm daemon start
```
#### `tspm daemon stop`
Stop the TSPM daemon and all managed processes.
```bash
tspm daemon stop
```
#### `tspm daemon status`
Check daemon health and statistics.
```bash
tspm daemon status
# Output:
TSPM Daemon Status:
────────────────────────────────────────
Status: running
PID: 12345
Uptime: 86400s
Processes: 5
Memory: 45.2 MB
CPU: 0.1%
```
## 🏗️ Architecture
TSPM uses a three-tier architecture for maximum reliability:
1. **ProcessWrapper** - Low-level process management with stream handling
2. **ProcessMonitor** - Adds monitoring, memory limits, and auto-restart logic
3. **Tspm Core** - High-level orchestration with configuration persistence
The daemon architecture ensures your processes keep running even after you close your terminal. All process communication happens through a robust IPC (Inter-Process Communication) system.
## 🎮 Programmatic Usage
TSPM can also be used as a library in your Node.js applications:
```typescript
import { Tspm } from '@git.zone/tspm';
const manager = new Tspm();
// Start a process
const processId = await manager.start({
id: 'worker',
name: 'Background Worker',
command: 'node worker.js',
projectDir: process.cwd(),
memoryLimitBytes: 512 * 1024 * 1024, // 512MB
autorestart: true,
watch: false
});
// Monitor process
const info = await manager.getProcessInfo(processId);
console.log(`Process ${info.id} is ${info.status}`);
// Stop process
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
```bash
# Clone the repository
git clone https://code.foss.global/git.zone/tspm.git
# Install dependencies
pnpm install
# Run tests
pnpm test
# Build the project
pnpm build
# Start development
pnpm start
```
## 🐛 Debugging
Enable debug mode for verbose logging:
```bash
export TSPM_DEBUG=true
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
- Efficient log buffering and rotation
## 🤝 Why TSPM?
Unlike general-purpose process managers, TSPM is built specifically for the TypeScript/Node.js ecosystem:
- **TypeScript First** - Written in TypeScript, for TypeScript projects
- **ESM Native** - Full support for ES modules
- **Developer Friendly** - Beautiful CLI output and helpful error messages
- **Production Ready** - Battle-tested memory management and error handling
- **No Configuration Required** - Sensible defaults that just work
- **Modern Architecture** - Async/await throughout, no callback hell
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
### Trademarks
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
### Company Information
Task Venture Capital GmbH
Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

48
readme.plan.md Normal file
View File

@@ -0,0 +1,48 @@
# 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
- [x] Add enableService() method
- [x] Add disableService() method
- [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
- [x] Add 'daemon start-service' internal command for systemd
- [x] Update all commands to handle missing daemon gracefully
- [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
- [x] Test error handling when daemon not running
- [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

107
test/test.daemon.ts Normal file
View File

@@ -0,0 +1,107 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as tspm from '../ts/index.js';
import * as path from 'path';
import * as fs from 'fs/promises';
import { TspmDaemon } from '../ts/classes.daemon.js';
// Test daemon server functionality
tap.test('TspmDaemon creation', async () => {
const daemon = new TspmDaemon();
expect(daemon).toBeInstanceOf(TspmDaemon);
});
tap.test('Daemon PID file management', async (tools) => {
const testDir = path.join(process.cwd(), '.nogit');
const testPidFile = path.join(testDir, 'test-daemon.pid');
// Create directory if it doesn't exist
await fs.mkdir(testDir, { recursive: true });
// Clean up any existing test file
await fs.unlink(testPidFile).catch(() => {});
// Test writing PID file
await fs.writeFile(testPidFile, process.pid.toString());
const pidContent = await fs.readFile(testPidFile, 'utf-8');
expect(parseInt(pidContent)).toEqual(process.pid);
// Clean up
await fs.unlink(testPidFile);
});
tap.test('Daemon socket path generation', async () => {
const daemon = new TspmDaemon();
// Access private property for testing (normally wouldn't do this)
const socketPath = (daemon as any).socketPath;
expect(socketPath).toInclude('tspm.sock');
});
tap.test('Daemon shutdown handlers', async (tools) => {
const daemon = new TspmDaemon();
// Test that shutdown handlers are registered
const sigintListeners = process.listeners('SIGINT');
const sigtermListeners = process.listeners('SIGTERM');
// We expect at least one listener for each signal
// (Note: in actual test we won't start the daemon to avoid side effects)
expect(sigintListeners.length).toBeGreaterThanOrEqual(0);
expect(sigtermListeners.length).toBeGreaterThanOrEqual(0);
});
tap.test('Daemon process info tracking', async () => {
const daemon = new TspmDaemon();
const tspmInstance = (daemon as any).tspmInstance;
expect(tspmInstance).toBeDefined();
expect(tspmInstance.processes).toBeInstanceOf(Map);
expect(tspmInstance.processConfigs).toBeInstanceOf(Map);
expect(tspmInstance.processInfo).toBeInstanceOf(Map);
});
tap.test('Daemon heartbeat monitoring setup', async (tools) => {
const daemon = new TspmDaemon();
// Test heartbeat interval property exists
const heartbeatInterval = (daemon as any).heartbeatInterval;
expect(heartbeatInterval).toEqual(null); // Should be null before start
});
tap.test('Daemon shutdown state management', async () => {
const daemon = new TspmDaemon();
const isShuttingDown = (daemon as any).isShuttingDown;
expect(isShuttingDown).toEqual(false);
});
tap.test('Daemon memory usage reporting', async () => {
const memUsage = process.memoryUsage();
expect(memUsage.heapUsed).toBeGreaterThan(0);
expect(memUsage.heapTotal).toBeGreaterThan(0);
expect(memUsage.rss).toBeGreaterThan(0);
});
tap.test('Daemon CPU usage calculation', async () => {
const cpuUsage = process.cpuUsage();
expect(cpuUsage.user).toBeGreaterThanOrEqual(0);
expect(cpuUsage.system).toBeGreaterThanOrEqual(0);
// Test conversion to seconds
const cpuSeconds = cpuUsage.user / 1000000;
expect(cpuSeconds).toBeGreaterThanOrEqual(0);
});
tap.test('Daemon uptime calculation', async () => {
const startTime = Date.now();
// Wait a bit
await new Promise(resolve => setTimeout(resolve, 100));
const uptime = Date.now() - startTime;
expect(uptime).toBeGreaterThanOrEqual(100);
expect(uptime).toBeLessThan(200);
});
export default tap.start();

266
test/test.integration.ts Normal file
View File

@@ -0,0 +1,266 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as tspm from '../ts/index.js';
import * as path from 'path';
import * as fs from 'fs/promises';
import * as os from 'os';
import { spawn } from 'child_process';
import { tspmIpcClient } from '../ts/classes.ipcclient.js';
// Helper to ensure daemon is stopped before tests
async function ensureDaemonStopped() {
try {
await tspmIpcClient.stopDaemon(false);
await new Promise(resolve => setTimeout(resolve, 1000));
} catch (error) {
// Ignore errors if daemon is not running
}
}
// Helper to clean up test files
async function cleanupTestFiles() {
const tspmDir = path.join(os.homedir(), '.tspm');
const pidFile = path.join(tspmDir, 'daemon.pid');
const socketFile = path.join(tspmDir, 'tspm.sock');
await fs.unlink(pidFile).catch(() => {});
await fs.unlink(socketFile).catch(() => {});
}
// Integration tests for daemon-client communication
tap.test('Full daemon lifecycle test', async (tools) => {
const done = tools.defer();
// Ensure clean state
await ensureDaemonStopped();
await cleanupTestFiles();
// Test 1: Check daemon is not running
let status = await tspmIpcClient.getDaemonStatus();
expect(status).toEqual(null);
// Test 2: Start daemon
console.log('Starting daemon...');
await tspmIpcClient.connect();
// Give daemon time to fully initialize
await new Promise(resolve => setTimeout(resolve, 2000));
// Test 3: Check daemon is running
status = await tspmIpcClient.getDaemonStatus();
expect(status).toBeDefined();
expect(status?.status).toEqual('running');
expect(status?.pid).toBeGreaterThan(0);
expect(status?.processCount).toBeGreaterThanOrEqual(0);
// Test 4: Stop daemon
console.log('Stopping daemon...');
await tspmIpcClient.stopDaemon(true);
// Give daemon time to shutdown
await new Promise(resolve => setTimeout(resolve, 2000));
// Test 5: Check daemon is stopped
status = await tspmIpcClient.getDaemonStatus();
expect(status).toEqual(null);
done.resolve();
});
tap.test('Process management through daemon', async (tools) => {
const done = tools.defer();
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
// Test 1: List processes (should be empty initially)
let listResponse = await tspmIpcClient.request('list', {});
expect(listResponse.processes).toBeArray();
expect(listResponse.processes.length).toEqual(0);
// Test 2: Start a test process
const testConfig: tspm.IProcessConfig = {
id: 'test-echo',
name: 'Test Echo Process',
command: 'echo "Test process"',
projectDir: process.cwd(),
memoryLimitBytes: 50 * 1024 * 1024,
autorestart: false,
};
const startResponse = await tspmIpcClient.request('start', { config: testConfig });
expect(startResponse.processId).toEqual('test-echo');
expect(startResponse.status).toBeDefined();
// Test 3: List processes (should have one process)
listResponse = await tspmIpcClient.request('list', {});
expect(listResponse.processes.length).toBeGreaterThanOrEqual(1);
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' });
expect(describeResponse.processInfo).toBeDefined();
expect(describeResponse.config).toBeDefined();
expect(describeResponse.config.id).toEqual('test-echo');
// Test 5: Stop the process
const stopResponse = await tspmIpcClient.request('stop', { id: 'test-echo' });
expect(stopResponse.success).toEqual(true);
expect(stopResponse.message).toInclude('stopped successfully');
// Test 6: Delete the process
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');
expect(deletedProcess).toBeUndefined();
// Cleanup: stop daemon
await tspmIpcClient.stopDaemon(true);
done.resolve();
});
tap.test('Batch operations through daemon', async (tools) => {
const done = tools.defer();
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
// Add multiple test processes
const testConfigs: tspm.IProcessConfig[] = [
{
id: 'batch-test-1',
name: 'Batch Test 1',
command: 'echo "Process 1"',
projectDir: process.cwd(),
memoryLimitBytes: 50 * 1024 * 1024,
autorestart: false,
},
{
id: 'batch-test-2',
name: 'Batch Test 2',
command: 'echo "Process 2"',
projectDir: process.cwd(),
memoryLimitBytes: 50 * 1024 * 1024,
autorestart: false,
},
];
// Start processes
for (const config of testConfigs) {
await tspmIpcClient.request('start', { config });
}
// Test 1: Stop all processes
const stopAllResponse = await tspmIpcClient.request('stopAll', {});
expect(stopAllResponse.stopped).toBeArray();
expect(stopAllResponse.stopped.length).toBeGreaterThanOrEqual(2);
// Test 2: Start all processes
const startAllResponse = await tspmIpcClient.request('startAll', {});
expect(startAllResponse.started).toBeArray();
// Test 3: Restart all processes
const restartAllResponse = await tspmIpcClient.request('restartAll', {});
expect(restartAllResponse.restarted).toBeArray();
// Cleanup: delete all test processes
for (const config of testConfigs) {
await tspmIpcClient.request('delete', { id: config.id }).catch(() => {});
}
// Stop daemon
await tspmIpcClient.stopDaemon(true);
done.resolve();
});
tap.test('Daemon error handling', async (tools) => {
const done = tools.defer();
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
// Test 1: Try to stop non-existent process
try {
await tspmIpcClient.request('stop', { id: 'non-existent-process' });
expect(false).toEqual(true); // Should not reach here
} catch (error) {
expect(error.message).toInclude('Failed to stop process');
}
// Test 2: Try to describe non-existent process
try {
await tspmIpcClient.request('describe', { id: 'non-existent-process' });
expect(false).toEqual(true); // Should not reach here
} catch (error) {
expect(error.message).toInclude('not found');
}
// Test 3: Try to restart non-existent process
try {
await tspmIpcClient.request('restart', { id: 'non-existent-process' });
expect(false).toEqual(true); // Should not reach here
} catch (error) {
expect(error.message).toInclude('Failed to restart process');
}
// Stop daemon
await tspmIpcClient.stopDaemon(true);
done.resolve();
});
tap.test('Daemon heartbeat functionality', async (tools) => {
const done = tools.defer();
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
// Test heartbeat
const heartbeatResponse = await tspmIpcClient.request('heartbeat', {});
expect(heartbeatResponse.timestamp).toBeGreaterThan(0);
expect(heartbeatResponse.status).toEqual('healthy');
// Stop daemon
await tspmIpcClient.stopDaemon(true);
done.resolve();
});
tap.test('Daemon memory and CPU reporting', async (tools) => {
const done = tools.defer();
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
// Get daemon status
const status = await tspmIpcClient.getDaemonStatus();
expect(status).toBeDefined();
expect(status?.memoryUsage).toBeGreaterThan(0);
expect(status?.cpuUsage).toBeGreaterThanOrEqual(0);
expect(status?.uptime).toBeGreaterThan(0);
// Stop daemon
await tspmIpcClient.stopDaemon(true);
done.resolve();
});
// Cleanup after all tests
tap.test('Final cleanup', async () => {
await ensureDaemonStopped();
await cleanupTestFiles();
});
export default tap.start();

145
test/test.ipcclient.ts Normal file
View File

@@ -0,0 +1,145 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as tspm from '../ts/index.js';
import * as path from 'path';
import * as fs from 'fs/promises';
import { TspmIpcClient } from '../ts/classes.ipcclient.js';
import * as os from 'os';
// Test IPC client functionality
tap.test('TspmIpcClient creation', async () => {
const client = new TspmIpcClient();
expect(client).toBeInstanceOf(TspmIpcClient);
});
tap.test('IPC client socket path', async () => {
const client = new TspmIpcClient();
const socketPath = (client as any).socketPath;
expect(socketPath).toInclude('.tspm');
expect(socketPath).toInclude('tspm.sock');
});
tap.test('IPC client daemon PID file path', async () => {
const client = new TspmIpcClient();
const daemonPidFile = (client as any).daemonPidFile;
expect(daemonPidFile).toInclude('.tspm');
expect(daemonPidFile).toInclude('daemon.pid');
});
tap.test('IPC client connection state', async () => {
const client = new TspmIpcClient();
const isConnected = (client as any).isConnected;
expect(isConnected).toEqual(false); // Should be false initially
});
tap.test('IPC client daemon running check - no daemon', async () => {
const client = new TspmIpcClient();
const tspmDir = path.join(os.homedir(), '.tspm');
const pidFile = path.join(tspmDir, 'daemon.pid');
// Ensure no PID file exists for this test
await fs.unlink(pidFile).catch(() => {});
const isRunning = await (client as any).isDaemonRunning();
expect(isRunning).toEqual(false);
});
tap.test('IPC client daemon running check - stale PID', async () => {
const client = new TspmIpcClient();
const tspmDir = path.join(os.homedir(), '.tspm');
const pidFile = path.join(tspmDir, 'daemon.pid');
// Create directory if it doesn't exist
await fs.mkdir(tspmDir, { recursive: true });
// Write a fake PID that doesn't exist
await fs.writeFile(pidFile, '99999999');
const isRunning = await (client as any).isDaemonRunning();
expect(isRunning).toEqual(false);
// Clean up - the stale PID should be removed
const fileExists = await fs.access(pidFile).then(() => true).catch(() => false);
expect(fileExists).toEqual(false);
});
tap.test('IPC client daemon running check - current process', async () => {
const client = new TspmIpcClient();
const tspmDir = path.join(os.homedir(), '.tspm');
const pidFile = path.join(tspmDir, 'daemon.pid');
const socketFile = path.join(tspmDir, 'tspm.sock');
// Create directory if it doesn't exist
await fs.mkdir(tspmDir, { recursive: true });
// Write current process PID (simulating daemon is this process)
await fs.writeFile(pidFile, process.pid.toString());
// Create a fake socket file
await fs.writeFile(socketFile, '');
const isRunning = await (client as any).isDaemonRunning();
expect(isRunning).toEqual(true);
// Clean up
await fs.unlink(pidFile).catch(() => {});
await fs.unlink(socketFile).catch(() => {});
});
tap.test('IPC client singleton instance', async () => {
// Import the singleton
const { tspmIpcClient } = await import('../ts/classes.ipcclient.js');
expect(tspmIpcClient).toBeInstanceOf(TspmIpcClient);
// Test that it's the same instance
const { tspmIpcClient: secondImport } = await import('../ts/classes.ipcclient.js');
expect(tspmIpcClient).toBe(secondImport);
});
tap.test('IPC client request method type safety', async () => {
const client = new TspmIpcClient();
// Test that request method exists
expect(client.request).toBeInstanceOf(Function);
expect(client.connect).toBeInstanceOf(Function);
expect(client.disconnect).toBeInstanceOf(Function);
expect(client.stopDaemon).toBeInstanceOf(Function);
expect(client.getDaemonStatus).toBeInstanceOf(Function);
});
tap.test('IPC client error message formatting', async () => {
const errorMessage = 'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.';
expect(errorMessage).toInclude('tspm daemon start');
});
tap.test('IPC client reconnection logic', async () => {
const client = new TspmIpcClient();
// Test reconnection error conditions
const econnrefusedError = new Error('ECONNREFUSED');
expect(econnrefusedError.message).toInclude('ECONNREFUSED');
const enoentError = new Error('ENOENT');
expect(enoentError.message).toInclude('ENOENT');
});
tap.test('IPC client daemon start timeout', async () => {
const maxWaitTime = 10000; // 10 seconds
const checkInterval = 500; // 500ms
const maxChecks = maxWaitTime / checkInterval;
expect(maxChecks).toEqual(20);
});
tap.test('IPC client daemon stop timeout', async () => {
const maxWaitTime = 15000; // 15 seconds
const checkInterval = 500; // 500ms
const maxChecks = maxWaitTime / checkInterval;
expect(maxChecks).toEqual(30);
});
export default tap.start();

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 * as tspm from '../ts/index.js';
import { join } from 'path';
tap.test('first test', async () => { // Basic module import test
console.log(tspm); 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(); 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 = { export const commitinfo = {
name: '@git.zone/tspm', name: '@git.zone/tspm',
version: '1.1.0', version: '2.0.0',
description: 'a no fuzz process manager' 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);
}
}

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

@@ -0,0 +1,431 @@
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 = plugins.smartipc.SmartIpc.createServer({
id: 'tspm-daemon',
socketPath: this.socketPath,
autoCleanupSocketFile: true, // Clean up stale sockets
socketMode: 0o600, // Set proper permissions
heartbeat: true,
heartbeatInterval: 5000,
heartbeatTimeout: 20000,
heartbeatInitialGracePeriodMs: 10000 // Grace period for startup
});
// Register message handlers
this.registerHandlers();
// Start the IPC server and wait until ready to accept connections
await this.ipcServer.start({ readyWhen: 'accepting' });
// Write PID file
await this.writePidFile();
// Start heartbeat monitoring
this.startHeartbeatMonitoring();
// Load existing process configurations
await this.tspmInstance.loadProcessConfigs();
// Set up log publishing
this.tspmInstance.on('process:log', ({ processId, log }) => {
// Publish to topic for this process
const topic = `logs.${processId}`;
// Broadcast to all connected clients subscribed to this topic
if (this.ipcServer) {
this.ipcServer.broadcast(`topic:${topic}`, log);
}
});
// 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.onMessage(
'start',
async (request: RequestForMethod<'start'>) => {
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.onMessage(
'stop',
async (request: RequestForMethod<'stop'>) => {
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.onMessage('restart', async (request: RequestForMethod<'restart'>) => {
try {
await this.tspmInstance.restart(request.id);
const processInfo = this.tspmInstance.processInfo.get(request.id);
return {
processId: request.id,
pid: processInfo?.pid,
status: processInfo?.status || 'stopped',
};
} catch (error) {
throw new Error(`Failed to restart process: ${error.message}`);
}
});
this.ipcServer.onMessage(
'delete',
async (request: RequestForMethod<'delete'>) => {
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.onMessage(
'list',
async (request: RequestForMethod<'list'>) => {
const processes = await this.tspmInstance.list();
return { processes };
},
);
this.ipcServer.onMessage('describe', async (request: RequestForMethod<'describe'>) => {
const processInfo = await this.tspmInstance.describe(request.id);
const config = this.tspmInstance.processConfigs.get(request.id);
if (!processInfo || !config) {
throw new Error(`Process ${request.id} not found`);
}
return {
processInfo,
config,
};
});
this.ipcServer.onMessage('getLogs', async (request: RequestForMethod<'getLogs'>) => {
const logs = await this.tspmInstance.getLogs(request.id);
return { logs };
});
// Batch operations handlers
this.ipcServer.onMessage('startAll', async (request: RequestForMethod<'startAll'>) => {
const started: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
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.onMessage('stopAll', async (request: RequestForMethod<'stopAll'>) => {
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.onMessage('restartAll', async (request: RequestForMethod<'restartAll'>) => {
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.onMessage('daemon:status', async (request: RequestForMethod<'daemon:status'>) => {
const memUsage = process.memoryUsage();
return {
status: 'running',
pid: process.pid,
uptime: Date.now() - this.startTime,
processCount: this.tspmInstance.processes.size,
memoryUsage: memUsage.heapUsed,
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
};
});
this.ipcServer.onMessage('daemon:shutdown', async (request: RequestForMethod<'daemon:shutdown'>) => {
if (this.isShuttingDown) {
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.onMessage('heartbeat', async (request: RequestForMethod<'heartbeat'>) => {
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(() => {});
};

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

@@ -0,0 +1,261 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
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) {
throw new Error(
'TSPM daemon is not running.\n\n' +
'To start the daemon, run one of:\n' +
' tspm daemon start - Start daemon for this session\n' +
' tspm enable - Enable daemon as system service (recommended)\n'
);
}
// Create IPC client
this.ipcClient = plugins.smartipc.SmartIpc.createClient({
id: 'tspm-cli',
socketPath: this.socketPath,
clientId: `cli-${process.pid}`,
connectRetry: {
enabled: true,
initialDelay: 100,
maxDelay: 2000,
maxAttempts: 30,
totalTimeout: 15000,
},
registerTimeoutMs: 8000,
heartbeat: true,
heartbeatInterval: 5000,
heartbeatTimeout: 20000,
heartbeatInitialGracePeriodMs: 10000,
heartbeatThrowOnTimeout: false // Don't throw, emit events instead
});
// Connect to the daemon
try {
await this.ipcClient.connect({ waitForReady: true });
this.isConnected = true;
// Handle heartbeat timeouts gracefully
this.ipcClient.on('heartbeatTimeout', () => {
console.warn('Heartbeat timeout detected, connection may be degraded');
this.isConnected = false;
});
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" or "tspm enable".',
);
}
}
/**
* 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) {
throw new Error(
'Not connected to TSPM daemon.\n' +
'Run "tspm daemon start" or "tspm enable" first.'
);
}
try {
const response = await this.ipcClient!.request<
RequestForMethod<M>,
ResponseForMethod<M>
>(method, params);
return response;
} catch (error) {
// Don't try to auto-reconnect, just throw the error
throw error;
}
}
/**
* Subscribe to log updates for a specific process
*/
public async subscribe(processId: string, handler: (log: any) => void): Promise<void> {
if (!this.ipcClient || !this.isConnected) {
throw new Error('Not connected to daemon');
}
const topic = `logs.${processId}`;
await this.ipcClient.subscribe(`topic:${topic}`, handler);
}
/**
* Unsubscribe from log updates for a specific process
*/
public async unsubscribe(processId: string): Promise<void> {
if (!this.ipcClient || !this.isConnected) {
throw new Error('Not connected to daemon');
}
const topic = `logs.${processId}`;
await this.ipcClient.unsubscribe(`topic:${topic}`);
}
/**
* 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;
}
}
/**
* 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

@@ -1,104 +1,176 @@
import { spawn, ChildProcess } from 'child_process'; import * as plugins from './plugins.js';
import psTree from 'ps-tree'; import { EventEmitter } from 'events';
import pidusage from 'pidusage'; import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js';
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
interface IMonitorConfig { export interface IMonitorConfig {
name?: string; // Optional name to identify the instance name?: string; // Optional name to identify the instance
projectDir: string; // Directory where the command will run projectDir: string; // Directory where the command will run
command: string; // Full command to run (e.g., "npm run xyz") command: string; // Full command to run (e.g., "npm run xyz")
args?: string[]; // Optional: arguments for the command args?: string[]; // Optional: arguments for the command
memoryLimitBytes: number; // Maximum allowed memory (in bytes) for the process group memoryLimitBytes: number; // Maximum allowed memory (in bytes) for the process group
monitorIntervalMs?: number; // Interval (in ms) at which memory is checked (default: 5000) 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)
} }
class ProcessMonitor { export class ProcessMonitor extends EventEmitter {
private child: ChildProcess | null = null; private processWrapper: ProcessWrapper | null = null;
private config: IMonitorConfig; private config: IMonitorConfig;
private intervalId: NodeJS.Timeout | null = null; private intervalId: NodeJS.Timeout | null = null;
private stopped: boolean = true; // Initially stopped until start() is called private stopped: boolean = true; // Initially stopped until start() is called
private restartCount: number = 0;
private logger: Logger;
constructor(config: IMonitorConfig) { constructor(config: IMonitorConfig) {
super();
this.config = config; this.config = config;
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
} }
public start(): void { public start(): void {
// Reset the stopped flag so that new processes can spawn. // Reset the stopped flag so that new processes can spawn.
this.stopped = false; this.stopped = false;
this.log(`Starting process monitor.`); this.log(`Starting process monitor.`);
this.spawnChild(); this.spawnProcess();
// Set the monitoring interval. // Set the monitoring interval.
const interval = this.config.monitorIntervalMs || 5000; const interval = this.config.monitorIntervalMs || 5000;
this.intervalId = setInterval(() => { this.intervalId = setInterval((): void => {
if (this.child && this.child.pid) { if (this.processWrapper && this.processWrapper.getPid()) {
this.monitorProcessGroup(this.child.pid, this.config.memoryLimitBytes); this.monitorProcessGroup(
this.processWrapper.getPid()!,
this.config.memoryLimitBytes,
);
} }
}, interval); }, interval);
} }
private spawnChild(): void { private spawnProcess(): void {
// Don't spawn if the monitor has been stopped. // Don't spawn if the monitor has been stopped.
if (this.stopped) return; if (this.stopped) {
this.logger.debug('Not spawning process because monitor is stopped');
if (this.config.args && this.config.args.length > 0) { return;
this.log(
`Spawning command "${this.config.command}" with args [${this.config.args.join(
', '
)}] in directory: ${this.config.projectDir}`
);
this.child = spawn(this.config.command, this.config.args, {
cwd: this.config.projectDir,
detached: true,
stdio: 'inherit',
});
} else {
this.log(
`Spawning command "${this.config.command}" in directory: ${this.config.projectDir}`
);
// Use shell mode to allow a full command string.
this.child = spawn(this.config.command, {
cwd: this.config.projectDir,
detached: true,
stdio: 'inherit',
shell: true,
});
} }
this.log(`Spawned process with PID ${this.child.pid}`); this.logger.info(`Spawning process: ${this.config.command}`);
// When the child process exits, restart it if the monitor isn't stopped. // Create a new process wrapper
this.child.on('exit', (code, signal) => { this.processWrapper = new ProcessWrapper({
this.log(`Child process exited with code ${code}, signal ${signal}.`); name: this.config.name || 'unnamed-process',
if (!this.stopped) { command: this.config.command,
this.log('Restarting process...'); args: this.config.args,
this.spawnChild(); cwd: this.config.projectDir,
env: this.config.env,
logBuffer: this.config.logBufferSize,
});
// Set up event handlers
this.processWrapper.on('log', (log: IProcessLog): void => {
// Re-emit the log event for upstream handlers
this.emit('log', log);
// 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 groups memory usage. If the total memory exceeds the limit, * 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. * kill the process group so that the 'exit' handler can restart it.
*/ */
private async monitorProcessGroup(pid: number, memoryLimit: number): Promise<void> { private async monitorProcessGroup(
pid: number,
memoryLimit: number,
): Promise<void> {
try { try {
const memoryUsage = await this.getProcessGroupMemory(pid); 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( this.log(
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes( `Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
memoryUsage memoryUsage,
)} (${memoryUsage} bytes)` )} (${memoryUsage} bytes)`,
); );
if (memoryUsage > memoryLimit) { if (memoryUsage > memoryLimit) {
this.log( const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
`Memory usage ${this.humanReadableBytes( memoryUsage,
memoryUsage )} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.`;
)} exceeds limit of ${this.humanReadableBytes(memoryLimit)}. Restarting process.`
); this.logger.warn(memoryLimitMsg);
// Kill the entire process group by sending a signal to -PID. this.log(memoryLimitMsg);
process.kill(-pid, 'SIGKILL');
// Stop the process wrapper, which will trigger the exit handler and restart
if (this.processWrapper) {
this.processWrapper.stop();
}
} }
} catch (error) { } catch (error: Error | unknown) {
this.log('Error monitoring process group: ' + error); 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()}`);
} }
} }
@@ -107,19 +179,58 @@ class ProcessMonitor {
*/ */
private getProcessGroupMemory(pid: number): Promise<number> { private getProcessGroupMemory(pid: number): Promise<number> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
psTree(pid, (err, children) => { this.logger.debug(
if (err) return reject(err); `Getting memory usage for process group with PID ${pid}`,
// Include the main process and its children. );
const pids: number[] = [pid, ...children.map(child => Number(child.PID))];
pidusage(pids, (err, stats) => { plugins.psTree(
if (err) return reject(err); pid,
let totalMemory = 0; (err: Error | null, children: Array<{ PID: string }>) => {
for (const key in stats) { if (err) {
totalMemory += stats[key].memory; 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);
} }
resolve(totalMemory);
}); // 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);
},
);
},
);
}); });
} }
@@ -144,11 +255,49 @@ class ProcessMonitor {
if (this.intervalId) { if (this.intervalId) {
clearInterval(this.intervalId); clearInterval(this.intervalId);
} }
if (this.child && this.child.pid) { if (this.processWrapper) {
process.kill(-this.child.pid, 'SIGKILL'); 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. * Helper method for logging messages with the instance name.
*/ */
@@ -157,22 +306,3 @@ class ProcessMonitor {
console.log(prefix + message); console.log(prefix + message);
} }
} }
// Example usage:
const config: IMonitorConfig = {
name: 'Project XYZ Monitor', // Identifier for the instance
projectDir: '/path/to/your/project', // Set the project directory here
command: 'npm run xyz', // Full command string (no need for args)
memoryLimitBytes: 500 * 1024 * 1024, // 500 MB memory limit
monitorIntervalMs: 5000, // Check memory usage every 5 seconds
};
const monitor = new 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', () => {
monitor.log('Received SIGINT, stopping monitor...');
monitor.stop();
process.exit();
});

View File

@@ -0,0 +1,262 @@
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;
seq: number;
runId: 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;
private nextSeq: number = 0;
private runId: string = '';
constructor(options: IProcessWrapperOptions) {
super();
this.options = options;
this.logBufferSize = options.logBuffer || 100;
this.logger = new Logger(`ProcessWrapper:${options.name}`);
this.runId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* 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,
seq: this.nextSeq++,
runId: this.runId,
};
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,
seq: this.nextSeq++,
runId: this.runId,
};
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);
}
}

View File

@@ -0,0 +1,103 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
/**
* Manages TSPM daemon as a systemd service via smartdaemon
*/
export class TspmServiceManager {
private smartDaemon: plugins.smartdaemon.SmartDaemon;
private service: any = null; // SmartDaemonService type is not exported
constructor() {
this.smartDaemon = new plugins.smartdaemon.SmartDaemon();
}
/**
* Get or create the TSPM daemon service configuration
*/
private async getOrCreateService(): Promise<any> {
if (!this.service) {
const cliPath = plugins.path.join(paths.packageDir, 'cli.js');
// Create service configuration
this.service = await this.smartDaemon.addService({
name: 'tspm-daemon',
description: 'TSPM Process Manager Daemon',
command: `${process.execPath} ${cliPath} daemon start-service`,
workingDir: process.env.HOME || process.cwd(),
version: '1.0.0'
});
}
return this.service;
}
/**
* Enable the TSPM daemon as a system service
*/
public async enableService(): Promise<void> {
const service = await this.getOrCreateService();
// Save service configuration
await service.save();
// Enable service to start on boot
await service.enable();
// Start the service immediately
await service.start();
}
/**
* Disable the TSPM daemon service
*/
public async disableService(): Promise<void> {
const service = await this.getOrCreateService();
// Stop the service if running
try {
await service.stop();
} catch (error) {
// Service might not be running
console.log('Service was not running');
}
// Disable service from starting on boot
await service.disable();
}
/**
* Get the current status of the systemd service
*/
public async getServiceStatus(): Promise<{
enabled: boolean;
running: boolean;
status: string;
}> {
try {
await this.getOrCreateService();
// Note: SmartDaemon doesn't provide direct status methods,
// so we'll need to check via systemctl commands
// This is a simplified implementation
return {
enabled: true, // Would need to check systemctl is-enabled
running: true, // Would need to check systemctl is-active
status: 'active'
};
} catch (error) {
return {
enabled: false,
running: false,
status: 'inactive'
};
}
}
/**
* Reload the systemd service configuration
*/
public async reloadService(): Promise<void> {
const service = await this.getOrCreateService();
await service.reload();
}
}

View File

@@ -1,6 +1,438 @@
import * as plugins from './plugins.js'; import * as plugins from './plugins.js';
import { EventEmitter } from 'events';
import * as paths from './paths.js'; import * as paths from './paths.js';
import {
ProcessMonitor,
type IMonitorConfig,
} from './classes.processmonitor.js';
import { type IProcessLog } from './classes.processwrapper.js';
import { TspmConfig } from './classes.config.js';
import {
Logger,
ProcessError,
ConfigError,
ValidationError,
handleError,
} from './utils.errorhandler.js';
export class Tspm { 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 class Tspm extends EventEmitter {
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() {
super();
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);
// Set up log event handler to re-emit for pub/sub
monitor.on('log', (log: IProcessLog) => {
this.emit('process:log', { processId: config.id, log });
});
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',
);
}
}
} }

797
ts/cli.ts Normal file
View File

@@ -0,0 +1,797 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { tspmIpcClient } from './classes.ipcclient.js';
import { TspmServiceManager } from './classes.servicemanager.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 to handle daemon connection errors
function handleDaemonError(error: any, action: string): void {
if (error.message?.includes('daemon is not running') ||
error.message?.includes('Not connected') ||
error.message?.includes('ECONNREFUSED')) {
console.error(`Error: Cannot ${action} - TSPM daemon is not running.`);
console.log('\nTo start the daemon, run one of:');
console.log(' tspm daemon start - Start for this session only');
console.log(' tspm enable - Enable as system service (recommended)');
} else {
console.error(`Error ${action}:`, error.message);
}
process.exit(1);
}
// 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('\nService Management:');
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');
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 daemon manually (current session)');
console.log(' daemon stop Stop the 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: 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)');
}
},
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) {
handleDaemonError(error, 'start process');
}
},
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) {
handleDaemonError(error, 'stop process');
}
},
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) {
handleDaemonError(error, 'restart process');
}
},
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) {
handleDaemonError(error, 'delete process');
}
},
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) {
handleDaemonError(error, 'list processes');
}
},
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) {
handleDaemonError(error, 'describe process');
}
},
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> [options]');
console.log('\nOptions:');
console.log(' --lines <n> Number of lines to show (default: 50)');
console.log(' --follow Stream logs in real-time (like tail -f)');
return;
}
const lines = argvArg.lines || 50;
const follow = argvArg.follow || argvArg.f || false;
// Get initial logs
const response = await tspmIpcClient.request('getLogs', { id, lines });
if (!follow) {
// Static log output
console.log(`Logs for process: ${id} (last ${lines} lines)`);
console.log('─'.repeat(60));
for (const log of response.logs) {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
}
} else {
// Streaming log output
console.log(`Logs for process: ${id} (streaming...)`);
console.log('─'.repeat(60));
// Display initial logs
let lastSeq = 0;
for (const log of response.logs) {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
if (log.seq !== undefined) {
lastSeq = Math.max(lastSeq, log.seq);
}
}
// Subscribe to real-time updates
await tspmIpcClient.subscribe(id, (log: any) => {
// Check for sequence gap or duplicate
if (log.seq !== undefined && log.seq <= lastSeq) {
return; // Skip duplicate
}
if (log.seq !== undefined && log.seq > lastSeq + 1) {
console.log(`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`);
}
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
if (log.seq !== undefined) {
lastSeq = log.seq;
}
});
// Handle Ctrl+C gracefully
let isCleaningUp = false;
const cleanup = async () => {
if (isCleaningUp) return;
isCleaningUp = true;
console.log('\n\nStopping log stream...');
try {
await tspmIpcClient.unsubscribe(id);
await tspmIpcClient.disconnect();
} catch (err) {
// Ignore cleanup errors
}
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
// Keep the process alive
await new Promise(() => {}); // Block forever until interrupted
}
} catch (error) {
handleDaemonError(error, 'get logs');
}
},
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) {
handleDaemonError(error, 'start all processes');
}
},
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) {
handleDaemonError(error, 'stop all processes');
}
},
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) {
handleDaemonError(error, 'restart all processes');
}
},
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 manually...');
// Import spawn to start daemon process
const { spawn } = await import('child_process');
const daemonScript = plugins.path.join(
paths.packageDir,
'dist_ts',
'daemon.js',
);
// Start daemon as a regular background process (not detached)
const daemonProcess = spawn(process.execPath, [daemonScript], {
stdio: 'ignore',
env: {
...process.env,
TSPM_DAEMON_MODE: 'true',
},
});
console.log(`Started daemon with PID: ${daemonProcess.pid}`);
// Wait for daemon to be ready
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('For automatic startup, use "tspm enable" instead.');
}
} catch (error) {
console.error('Error starting daemon:', error.message);
process.exit(1);
}
break;
case 'start-service':
// This is called by systemd - start the daemon directly
console.log('Starting TSPM daemon for systemd service...');
const { startDaemon } = await import('./classes.daemon.js');
await startDaemon();
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`);
// Disconnect from daemon after getting status
await tspmIpcClient.disconnect();
} 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: () => {},
});
// Enable command - Enable TSPM daemon as systemd service
smartcliInstance.addCommand('enable').subscribe({
next: async (argvArg: CliArguments) => {
try {
const serviceManager = new TspmServiceManager();
console.log('Enabling TSPM daemon as system service...');
await serviceManager.enableService();
console.log('✓ TSPM daemon enabled and started as system service');
console.log(' The daemon will now start automatically on system boot');
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')) {
console.log('\nNote: You may need to run this command with sudo');
}
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Disable command - Disable TSPM daemon systemd service
smartcliInstance.addCommand('disable').subscribe({
next: async (argvArg: CliArguments) => {
try {
const serviceManager = new TspmServiceManager();
console.log('Disabling TSPM daemon service...');
await serviceManager.disableService();
console.log('✓ TSPM daemon service disabled');
console.log(' The daemon will no longer start on system boot');
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')) {
console.log('\nNote: You may need to run this command with sudo');
}
process.exit(1);
}
},
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,15 @@
import * as plugins from './plugins.js'; export * from './classes.tspm.js';
export * from './classes.processmonitor.js';
export * from './classes.daemon.js';
export * from './classes.ipcclient.js';
export * from './classes.servicemanager.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,
} from './classes.tspm.js';
import type { IProcessLog } from './classes.processwrapper.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');

View File

@@ -1,13 +1,24 @@
// native scope // native scope
import * as childProcess from 'child_process';
import * as path from 'node:path'; import * as path from 'node:path';
export { // Export with explicit module types
path, export { childProcess, path };
}
// @push.rocks scope // @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'; import * as smartpath from '@push.rocks/smartpath';
export { // Export with explicit module types
smartpath, 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 };

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": ".", "baseUrl": ".",
"paths": {} "paths": {}
}, },
"exclude": [ "exclude": ["dist_*/**/*.d.ts"]
"dist_*/**/*.d.ts"
]
} }