Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
833573eb10 | |||
ebc20a9232 | |||
22a43204d4 | |||
699d07ea36 |
19
changelog.md
19
changelog.md
@@ -1,5 +1,24 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-08-30 - 5.3.0 - feat(cli/daemon/processmonitor)
|
||||
Add flexible target resolution and search command; improve restart/backoff and error handling
|
||||
|
||||
- Add new cli command `search` to find processes by id or name fragment.
|
||||
- Allow flexible process targets in CLI commands (accepts numeric id, id:<n>, or name:<label>) for start/stop/restart/delete/describe/logs/edit commands.
|
||||
- Introduce a new daemon IPC method `resolveTarget` to normalize user-provided targets to ProcessId (supports id:<n>, name:<label>, or bare numeric id).
|
||||
- Keep `remove` as a CLI alias but daemon exposes `delete` only; CLI resolves targets and always calls daemon `delete`.
|
||||
- Implement scheduled restart/backoff in ProcessMonitor with incremental debounce, max retries, and a 1-hour reset window.
|
||||
- Emit a `failed` event from ProcessMonitor when max restart attempts are exceeded; ProcessManager listens and marks processes as `errored` and clears pid.
|
||||
- Ensure desired state is set to `stopped` before deleting a process to avoid race conditions.
|
||||
- Improve cli output messages to include resolved names alongside numeric ids where available.
|
||||
|
||||
## 2025-08-30 - 5.2.0 - feat(cli)
|
||||
Preserve CLI environment when adding processes, simplify edit flow, and refresh README docs
|
||||
|
||||
- CLI: When adding a process, capture and persist essential environment variables from the CLI (PATH, HOME, USER, SHELL, LANG, LC_ALL, NODE_ENV, NODE_PATH, npm_config_prefix and any TSPM_* variables). Undefined values are removed before storing.
|
||||
- CLI: Interactive edit flow temporarily disabled. The edit command now displays the current configuration and updates stored environment variables to match the current CLI environment.
|
||||
- Docs: Major README refresh — reorganized sections, clarified add vs start semantics, expanded examples, added daemon/service usage and programmatic API examples, and improved command reference and output examples.
|
||||
|
||||
## 2025-08-30 - 5.1.0 - feat(cli)
|
||||
Add interactive edit command and update support for process configurations
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tspm",
|
||||
"version": "5.1.0",
|
||||
"version": "5.3.0",
|
||||
"private": false,
|
||||
"description": "a no fuzz process manager",
|
||||
"main": "dist_ts/index.js",
|
||||
|
453
readme.md
453
readme.md
@@ -2,118 +2,133 @@
|
||||
|
||||
**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.
|
||||
|
||||
## 🎯 What TSPM Does
|
||||
## 🎯 What is TSPM?
|
||||
|
||||
TSPM is your production-ready process manager that handles the hard parts of running Node.js applications:
|
||||
TSPM (TypeScript Process Manager) is your production-ready process manager that handles the hard parts of running Node.js applications. It's like PM2, but built from the ground up for the modern TypeScript ecosystem with better memory management, intelligent logging, and a cleaner architecture.
|
||||
|
||||
- **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
|
||||
### ✨ Key Features
|
||||
|
||||
- **🧠 Smart Memory Management** - Tracks memory including child processes, enforces limits, and auto-restarts when exceeded
|
||||
- **💾 Persistent Log Storage** - Keeps 10MB of logs in memory, persists to disk on restart/stop/error
|
||||
- **🔄 Intelligent Auto-Restart** - Automatically restarts crashed processes with configurable policies
|
||||
- **👀 File Watching** - Auto-restart on file changes for seamless development
|
||||
- **🌳 Process Group Tracking** - Monitors parent and all child processes as a unit
|
||||
- **🏗️ Daemon Architecture** - Survives terminal sessions with Unix socket IPC
|
||||
- **📊 Beautiful CLI** - Clean, informative output with real-time status updates
|
||||
- **📝 Structured Logging** - Captures stdout/stderr with timestamps and metadata
|
||||
- **⚡ Zero Config** - Works out of the box, customize when needed
|
||||
- **🔌 System Service** - Run as systemd service for production deployments
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
```bash
|
||||
# Install globally
|
||||
# Install globally (recommended)
|
||||
npm install -g @git.zone/tspm
|
||||
|
||||
# Or with pnpm (recommended)
|
||||
# Or with pnpm
|
||||
pnpm add -g @git.zone/tspm
|
||||
|
||||
# Or use in your project
|
||||
# Or as a dev dependency
|
||||
npm install --save-dev @git.zone/tspm
|
||||
```
|
||||
|
||||
## 🚀 Quick Start
|
||||
|
||||
```bash
|
||||
# Start the daemon (happens automatically on first use)
|
||||
tspm daemon start
|
||||
# Add a process (creates config without starting)
|
||||
tspm add "node server.js" --name my-server --memory 1GB
|
||||
|
||||
# Start a process
|
||||
tspm start server.js --name my-server
|
||||
# Start the process (by name or id)
|
||||
tspm start name:my-server
|
||||
# or
|
||||
tspm start id:1
|
||||
|
||||
# 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
|
||||
# Or add and start in one go
|
||||
tspm add "node app.js" --name my-app
|
||||
tspm start name:my-app
|
||||
|
||||
# List all processes
|
||||
tspm list
|
||||
|
||||
# Check process details
|
||||
tspm describe my-server
|
||||
|
||||
# View logs
|
||||
tspm logs my-server --lines 100
|
||||
tspm logs name:my-app
|
||||
|
||||
# Stop a process
|
||||
tspm stop my-server
|
||||
|
||||
# Restart a process
|
||||
tspm restart my-server
|
||||
tspm stop name:my-app
|
||||
```
|
||||
|
||||
## 📋 Command Reference
|
||||
## 📋 Commands
|
||||
|
||||
### Process Management
|
||||
|
||||
#### `tspm start <script> [options]`
|
||||
#### `tspm add <command> [options]`
|
||||
|
||||
Start a new process with automatic monitoring and management.
|
||||
Add a new process configuration without starting it. This is the recommended way to register processes.
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--name <name>` - Custom name for the process (default: script name)
|
||||
- `--name <name>` - Custom name for the process (required)
|
||||
- `--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)
|
||||
- `--watch-paths <paths>` - Comma-separated paths to watch
|
||||
- `--autorestart` - Auto-restart on crash (default: true)
|
||||
|
||||
**Examples:**
|
||||
|
||||
```bash
|
||||
# Simple start
|
||||
tspm start server.js
|
||||
# Add a simple Node.js app
|
||||
tspm add "node server.js" --name api-server
|
||||
|
||||
# Production setup with 2GB memory
|
||||
tspm start app.js --name production-api --memory 2GB
|
||||
# Add with 2GB memory limit
|
||||
tspm add "node app.js" --name production-api --memory 2GB
|
||||
|
||||
# Development with watching
|
||||
tspm start dev-server.js --watch --watch-paths "src,config" --name dev
|
||||
# Add TypeScript app with watching
|
||||
tspm add "tsx watch src/index.ts" --name dev-server --watch --watch-paths "src,config"
|
||||
|
||||
# Custom working directory
|
||||
tspm start ../other-project/index.js --cwd ../other-project --name other
|
||||
# Add without auto-restart
|
||||
tspm add "node worker.js" --name one-time-job --autorestart false
|
||||
```
|
||||
|
||||
#### `tspm stop <id>`
|
||||
#### `tspm start <id|id:N|name:LABEL>`
|
||||
|
||||
Start a previously added process by its ID or name.
|
||||
|
||||
```bash
|
||||
tspm start name:my-server
|
||||
tspm start id:1 # Or a bare numeric id: tspm start 1
|
||||
```
|
||||
|
||||
#### `tspm stop <id|id:N|name:LABEL>`
|
||||
|
||||
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
|
||||
|
||||
```bash
|
||||
tspm stop my-server
|
||||
tspm stop name:my-server
|
||||
```
|
||||
|
||||
#### `tspm restart <id>`
|
||||
#### `tspm restart <id|id:N|name:LABEL>`
|
||||
|
||||
Stop and restart a process with the same configuration.
|
||||
|
||||
```bash
|
||||
tspm restart my-server
|
||||
tspm restart name:my-server
|
||||
```
|
||||
|
||||
#### `tspm delete <id>`
|
||||
#### `tspm delete <id|id:N|name:LABEL>` / `tspm remove <id|id:N|name:LABEL>`
|
||||
|
||||
Stop and remove a process from TSPM management.
|
||||
Stop and remove a process from TSPM management. Also deletes persisted logs.
|
||||
|
||||
```bash
|
||||
tspm delete old-server
|
||||
tspm delete name:old-server
|
||||
tspm remove name:old-server # Alias for delete (daemon handles delete)
|
||||
```
|
||||
|
||||
#### `tspm edit <id>`
|
||||
|
||||
Interactively edit a process configuration.
|
||||
|
||||
```bash
|
||||
tspm edit my-server
|
||||
# Opens interactive prompts to modify name, command, memory, etc.
|
||||
```
|
||||
|
||||
### Monitoring & Information
|
||||
@@ -126,20 +141,21 @@ Display all managed processes in a beautiful table.
|
||||
tspm list
|
||||
|
||||
# Output:
|
||||
┌─────────┬─────────────┬───────────┬───────────┬──────────┐
|
||||
│ ID │ Name │ Status │ Memory │ Restarts │
|
||||
├─────────┼─────────────┼───────────┼───────────┼──────────┤
|
||||
│ my-app │ my-app │ online │ 245.3 MB │ 0 │
|
||||
│ worker │ worker │ online │ 128.7 MB │ 2 │
|
||||
└─────────┴─────────────┴───────────┴───────────┴──────────┘
|
||||
┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐
|
||||
│ ID │ Name │ Status │ PID │ Memory │ Restarts │
|
||||
├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤
|
||||
│ 1 │ my-app │ online │ 45123 │ 245.3 MB │ 0 │
|
||||
│ 2 │ worker │ online │ 45456 │ 128.7 MB │ 2 │
|
||||
│ 3 │ api-server │ stopped │ - │ 0 B │ 5 │
|
||||
└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘
|
||||
```
|
||||
|
||||
#### `tspm describe <id>`
|
||||
#### `tspm describe <id|id:N|name:LABEL>`
|
||||
|
||||
Get detailed information about a specific process.
|
||||
|
||||
```bash
|
||||
tspm describe my-server
|
||||
tspm describe name:my-server
|
||||
|
||||
# Output:
|
||||
Process Details: my-server
|
||||
@@ -147,29 +163,35 @@ Process Details: my-server
|
||||
Status: online
|
||||
PID: 45123
|
||||
Memory: 245.3 MB
|
||||
CPU: 2.3%
|
||||
Uptime: 3600s
|
||||
Restarts: 0
|
||||
|
||||
Configuration:
|
||||
Command: server.js
|
||||
────────────────────────────────────────
|
||||
Command: node server.js
|
||||
Directory: /home/user/project
|
||||
Memory Limit: 2 GB
|
||||
Auto-restart: true
|
||||
Watch: enabled
|
||||
Watch Paths: src, config
|
||||
Watch: disabled
|
||||
```
|
||||
|
||||
#### `tspm logs <id> [options]`
|
||||
#### `tspm logs <id|id:N|name:LABEL> [options]`
|
||||
|
||||
View process logs (stdout and stderr).
|
||||
View process logs (stdout and stderr combined).
|
||||
|
||||
**Options:**
|
||||
|
||||
- `--lines <n>` - Number of lines to display (default: 50)
|
||||
- `--follow` - Stream logs in real-time (like `tail -f`)
|
||||
|
||||
```bash
|
||||
tspm logs my-server --lines 100
|
||||
# View last 50 lines
|
||||
tspm logs name:my-server
|
||||
|
||||
# View last 100 lines
|
||||
tspm logs name:my-server --lines 100
|
||||
|
||||
# Follow logs in real-time
|
||||
tspm logs name:my-server --follow
|
||||
```
|
||||
|
||||
### Batch Operations
|
||||
@@ -180,6 +202,10 @@ Start all saved processes at once.
|
||||
|
||||
```bash
|
||||
tspm start-all
|
||||
# ✓ Started 3 processes:
|
||||
# - my-app
|
||||
# - worker
|
||||
# - api-server
|
||||
```
|
||||
|
||||
#### `tspm stop-all`
|
||||
@@ -188,6 +214,7 @@ Stop all running processes.
|
||||
|
||||
```bash
|
||||
tspm stop-all
|
||||
# ✓ Stopped 3 processes
|
||||
```
|
||||
|
||||
#### `tspm restart-all`
|
||||
@@ -196,24 +223,49 @@ Restart all running processes.
|
||||
|
||||
```bash
|
||||
tspm restart-all
|
||||
# ✓ Restarted 3 processes
|
||||
```
|
||||
|
||||
#### `tspm reset`
|
||||
|
||||
**⚠️ Dangerous:** Stop all processes and clear all configurations.
|
||||
|
||||
```bash
|
||||
tspm reset
|
||||
# Are you sure? (y/N)
|
||||
# Stopped 3 processes.
|
||||
# Cleared all configurations.
|
||||
```
|
||||
|
||||
### Daemon Management
|
||||
|
||||
The TSPM daemon runs in the background and manages all your processes. It starts automatically when needed.
|
||||
|
||||
#### `tspm daemon start`
|
||||
|
||||
Start the TSPM daemon (happens automatically on first command).
|
||||
Manually start the TSPM daemon (usually automatic).
|
||||
|
||||
```bash
|
||||
tspm daemon start
|
||||
# ✓ TSPM daemon started successfully
|
||||
```
|
||||
|
||||
#### `tspm daemon stop`
|
||||
|
||||
Stop the TSPM daemon and all managed processes.
|
||||
Stop the daemon and all managed processes.
|
||||
|
||||
```bash
|
||||
tspm daemon stop
|
||||
# ✓ TSPM daemon stopped successfully
|
||||
```
|
||||
|
||||
#### `tspm daemon restart`
|
||||
|
||||
Restart the daemon (preserves running processes).
|
||||
|
||||
```bash
|
||||
tspm daemon restart
|
||||
# ✓ TSPM daemon restarted successfully
|
||||
```
|
||||
|
||||
#### `tspm daemon status`
|
||||
@@ -230,75 +282,175 @@ Status: running
|
||||
PID: 12345
|
||||
Uptime: 86400s
|
||||
Processes: 5
|
||||
Memory: 45.2 MB
|
||||
CPU: 0.1%
|
||||
Socket: /home/user/.tspm/tspm.sock
|
||||
```
|
||||
|
||||
### System Service Management
|
||||
|
||||
Run TSPM as a system service (systemd) for production deployments.
|
||||
|
||||
#### `tspm enable`
|
||||
|
||||
Enable TSPM as a system service that starts on boot.
|
||||
|
||||
```bash
|
||||
sudo tspm enable
|
||||
# ✓ TSPM daemon enabled and started as system service
|
||||
# The daemon will now start automatically on system boot
|
||||
```
|
||||
|
||||
#### `tspm disable`
|
||||
|
||||
Disable the TSPM system service.
|
||||
|
||||
```bash
|
||||
sudo tspm disable
|
||||
# ✓ TSPM daemon service disabled
|
||||
# The daemon will no longer start on system boot
|
||||
```
|
||||
|
||||
## 🏗️ Architecture
|
||||
|
||||
TSPM uses a three-tier architecture for maximum reliability:
|
||||
TSPM uses a robust three-tier architecture:
|
||||
|
||||
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
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ CLI Interface │
|
||||
│ (tspm commands) │
|
||||
└────────────────┬────────────────────────┘
|
||||
│ Unix Socket IPC
|
||||
┌────────────────▼────────────────────────┐
|
||||
│ TSPM Daemon │
|
||||
│ (Background Service) │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
│ │ ProcessManager │ │
|
||||
│ │ - Configuration persistence │ │
|
||||
│ │ - Process lifecycle │ │
|
||||
│ │ - Desired state management │ │
|
||||
│ └────────────┬─────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────▼─────────────────────┐ │
|
||||
│ │ ProcessMonitor │ │
|
||||
│ │ - Memory tracking & limits │ │
|
||||
│ │ - Auto-restart logic │ │
|
||||
│ │ - Log persistence (10MB) │ │
|
||||
│ │ - File watching │ │
|
||||
│ └────────────┬─────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────▼─────────────────────┐ │
|
||||
│ │ ProcessWrapper │ │
|
||||
│ │ - Process spawning │ │
|
||||
│ │ - Stream handling │ │
|
||||
│ │ - Signal management │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
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.
|
||||
### Key Components
|
||||
|
||||
## 🎮 Programmatic Usage
|
||||
- **CLI** - Lightweight client that communicates with daemon via IPC
|
||||
- **Daemon** - Persistent background service managing all processes
|
||||
- **ProcessManager** - High-level orchestration and configuration
|
||||
- **ProcessMonitor** - Adds monitoring, limits, and auto-restart
|
||||
- **ProcessWrapper** - Low-level process lifecycle and streams
|
||||
|
||||
TSPM can also be used as a library in your Node.js applications:
|
||||
## 🎮 Programmatic API
|
||||
|
||||
Use TSPM as a library in your Node.js applications:
|
||||
|
||||
```typescript
|
||||
import { Tspm } from '@git.zone/tspm';
|
||||
import { TspmIpcClient } from '@git.zone/tspm/client';
|
||||
|
||||
const manager = new Tspm();
|
||||
const client = new TspmIpcClient();
|
||||
await client.connect();
|
||||
|
||||
// Start a process
|
||||
const processId = await manager.start({
|
||||
id: 'worker',
|
||||
name: 'Background Worker',
|
||||
// Add and start a process
|
||||
const { id } = await client.request('add', {
|
||||
command: 'node worker.js',
|
||||
name: 'background-worker',
|
||||
projectDir: process.cwd(),
|
||||
memoryLimitBytes: 512 * 1024 * 1024, // 512MB
|
||||
memoryLimit: 512 * 1024 * 1024, // 512MB in bytes
|
||||
autorestart: true,
|
||||
watch: false,
|
||||
});
|
||||
|
||||
// Monitor process
|
||||
const info = await manager.getProcessInfo(processId);
|
||||
console.log(`Process ${info.id} is ${info.status}`);
|
||||
await client.request('start', { id });
|
||||
|
||||
// Stop process
|
||||
await manager.stop(processId);
|
||||
// Get process info
|
||||
const { processInfo } = await client.request('describe', { id });
|
||||
console.log(`Worker status: ${processInfo.status}`);
|
||||
console.log(`Memory usage: ${processInfo.memory} bytes`);
|
||||
|
||||
// Get logs
|
||||
const { logs } = await client.request('logs', { id, limit: 100 });
|
||||
logs.forEach(log => {
|
||||
console.log(`[${log.timestamp}] ${log.message}`);
|
||||
});
|
||||
|
||||
// Clean up
|
||||
await client.request('stop', { id });
|
||||
await client.disconnect();
|
||||
```
|
||||
|
||||
## 🔧 Advanced Features
|
||||
|
||||
### Memory Limit Enforcement
|
||||
### Memory Management
|
||||
|
||||
TSPM tracks memory usage including all child processes spawned by your application. When a process exceeds its memory limit, it's gracefully restarted.
|
||||
TSPM tracks total memory usage including all child processes:
|
||||
- Uses `ps-tree` to discover child processes
|
||||
- Calculates combined memory usage
|
||||
- Gracefully restarts when limit exceeded
|
||||
- Prevents memory leaks in production
|
||||
|
||||
### Process Group Tracking
|
||||
### Log Persistence
|
||||
|
||||
Using `ps-tree`, TSPM monitors not just your main process but all child processes it spawns, ensuring complete cleanup on stop/restart.
|
||||
Intelligent log management system:
|
||||
- Keeps 10MB of logs in memory per process
|
||||
- Automatically flushes to disk on stop/restart/error
|
||||
- Loads previous logs on process restart
|
||||
- Cleans up persisted logs after loading
|
||||
- Prevents disk space issues
|
||||
|
||||
### Intelligent Logging
|
||||
### Process Groups
|
||||
|
||||
Logs are buffered and managed efficiently, preventing memory issues from excessive output while ensuring you don't lose important information.
|
||||
Full process tree management:
|
||||
- Tracks parent and all child processes
|
||||
- Ensures complete cleanup on stop
|
||||
- Accurate memory tracking across process trees
|
||||
- No orphaned processes
|
||||
|
||||
### Graceful Shutdown
|
||||
|
||||
Processes receive SIGTERM first, allowing them to clean up. After a timeout, SIGKILL ensures termination.
|
||||
Multi-stage shutdown process:
|
||||
1. Send SIGTERM for graceful shutdown
|
||||
2. Wait for process to clean up (5 seconds)
|
||||
3. Send SIGKILL if still running
|
||||
4. Clean up all child processes
|
||||
|
||||
### Configuration Persistence
|
||||
### File Watching
|
||||
|
||||
Process configurations are saved, allowing you to restart all processes after a system reboot with a single command.
|
||||
Development-friendly auto-restart:
|
||||
- Watch specific directories or files
|
||||
- Ignore `node_modules` by default
|
||||
- Debounced restart on changes
|
||||
- Configurable watch paths
|
||||
|
||||
## 📊 Performance
|
||||
|
||||
TSPM is designed for production efficiency:
|
||||
|
||||
- **CPU Usage**: < 0.5% overhead per managed process
|
||||
- **Memory**: ~30-50MB for daemon, ~5-10MB per managed process
|
||||
- **Startup Time**: < 100ms to spawn new process
|
||||
- **IPC Latency**: < 1ms for command execution
|
||||
- **Log Performance**: Efficient ring buffer with automatic trimming
|
||||
|
||||
## 🛠️ Development
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://code.foss.global/git.zone/tspm.git
|
||||
cd tspm
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
@@ -309,42 +461,109 @@ pnpm test
|
||||
# Build the project
|
||||
pnpm build
|
||||
|
||||
# Start development
|
||||
# Run in development
|
||||
pnpm start
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
tspm/
|
||||
├── ts/
|
||||
│ ├── cli/ # CLI commands and interface
|
||||
│ ├── client/ # IPC client for daemon communication
|
||||
│ ├── daemon/ # Daemon server and process management
|
||||
│ └── shared/ # Shared types and protocols
|
||||
├── test/ # Test files
|
||||
└── dist_ts/ # Compiled JavaScript
|
||||
```
|
||||
|
||||
## 🐛 Debugging
|
||||
|
||||
Enable debug mode for verbose logging:
|
||||
Enable verbose logging for troubleshooting:
|
||||
|
||||
```bash
|
||||
# Enable debug mode
|
||||
export TSPM_DEBUG=true
|
||||
tspm list
|
||||
|
||||
# Check daemon logs
|
||||
tail -f /tmp/daemon-stderr.log
|
||||
|
||||
# Force daemon restart
|
||||
tspm daemon restart
|
||||
```
|
||||
|
||||
## 📊 Performance
|
||||
Common issues:
|
||||
|
||||
TSPM is designed to be lightweight and efficient:
|
||||
- **"Daemon not running"**: Run `tspm daemon start` or `tspm enable`
|
||||
- **"Permission denied"**: Check socket permissions in `~/.tspm/`
|
||||
- **"Process won't start"**: Check logs with `tspm logs <id|id:N|name:LABEL>`
|
||||
|
||||
- Minimal CPU overhead (typically < 0.5%)
|
||||
- Small memory footprint (~30-50MB for the daemon)
|
||||
- Fast process startup and shutdown
|
||||
- Efficient log buffering and rotation
|
||||
## 🎯 Targeting Processes (IDs and Names)
|
||||
|
||||
## 🤝 Why TSPM?
|
||||
Most process commands accept the following target formats:
|
||||
|
||||
Unlike general-purpose process managers, TSPM is built specifically for the TypeScript/Node.js ecosystem:
|
||||
- Numeric ID: `tspm start 1`
|
||||
- Explicit ID: `tspm start id:1`
|
||||
- Explicit name: `tspm start name:api-server`
|
||||
|
||||
- **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
|
||||
Notes:
|
||||
- Names must be used with the `name:` prefix.
|
||||
- If multiple processes share the same name, the CLI will report the ambiguous matches. Use `id:N` to disambiguate.
|
||||
- Use `tspm search <query>` to discover IDs by name or ID fragments.
|
||||
|
||||
### `tspm search <query>`
|
||||
|
||||
Search processes by name or ID substring and print matching IDs (and names when available):
|
||||
|
||||
```bash
|
||||
tspm search api
|
||||
# Matches for "api":
|
||||
# - id:3 name:api-server
|
||||
```
|
||||
|
||||
- **"Memory limit exceeded"**: Increase limit with `tspm edit <id>`
|
||||
|
||||
## 🤝 Why Choose TSPM?
|
||||
|
||||
### TSPM vs PM2
|
||||
|
||||
| Feature | TSPM | PM2 |
|
||||
|---------|------|-----|
|
||||
| TypeScript Native | ✅ Built in TS | ❌ JavaScript |
|
||||
| Memory Tracking | ✅ Including children | ⚠️ Main process only |
|
||||
| Log Management | ✅ Smart 10MB buffer | ⚠️ Can grow unlimited |
|
||||
| Architecture | ✅ Clean 3-tier | ❌ Monolithic |
|
||||
| Dependencies | ✅ Minimal | ❌ Heavy |
|
||||
| ESM Support | ✅ Native | ⚠️ Partial |
|
||||
| Config Format | ✅ Simple JSON | ❌ Complex ecosystem |
|
||||
|
||||
### Perfect For
|
||||
|
||||
### Restart Backoff and Failure Handling
|
||||
|
||||
TSPM automatically restarts crashed processes with an incremental backoff:
|
||||
|
||||
- Debounce delay grows linearly from 1s up to 10s for consecutive retries.
|
||||
- After the 10th retry, the process is marked as failed (status: "errored") and auto-restarts stop.
|
||||
- The retry counter resets if no retry happens for 1 hour since the last attempt.
|
||||
|
||||
You can manually restart a failed process at any time:
|
||||
|
||||
```bash
|
||||
tspm restart id:1
|
||||
```
|
||||
|
||||
- 🚀 **Production Node.js apps** - Reliable process management
|
||||
- 🔧 **Microservices** - Manage multiple services easily
|
||||
- 👨💻 **Development** - File watching and auto-restart
|
||||
- 🏭 **Worker processes** - Queue workers, cron jobs
|
||||
- 📊 **Resource-constrained environments** - Memory limits prevent OOM
|
||||
|
||||
## 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.
|
||||
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.
|
||||
|
||||
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tspm',
|
||||
version: '5.1.0',
|
||||
version: '5.3.0',
|
||||
description: 'a no fuzz process manager'
|
||||
}
|
||||
|
@@ -22,13 +22,14 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
);
|
||||
console.log(' disable Disable TSPM system service');
|
||||
console.log('\nProcess Commands:');
|
||||
console.log(' start <script> Start a process');
|
||||
console.log(' start <id|id:N|name:LBL> 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(' stop <id|id:N|name:LBL> Stop a process');
|
||||
console.log(' restart <id|id:N|name:LBL> Restart a process');
|
||||
console.log(' delete <id|id:N|name:LBL> Delete a process');
|
||||
console.log(' describe <id|id:N|name:LBL> Show details for a process');
|
||||
console.log(' logs <id|id:N|name:LBL> Show logs for a process');
|
||||
console.log(' search <query> Find processes by id/name');
|
||||
console.log(' start-all Start all saved processes');
|
||||
console.log(' stop-all Stop all processes');
|
||||
console.log(' restart-all Restart all processes');
|
||||
|
@@ -69,6 +69,33 @@ export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
if (watchPaths) console.log(` Watch paths: ${watchPaths.join(',')}`);
|
||||
}
|
||||
|
||||
// Capture essential environment variables from the CLI environment
|
||||
// so processes have access to the same environment they were added with
|
||||
const essentialEnvVars: NodeJS.ProcessEnv = {
|
||||
PATH: process.env.PATH || '',
|
||||
HOME: process.env.HOME,
|
||||
USER: process.env.USER,
|
||||
SHELL: process.env.SHELL,
|
||||
LANG: process.env.LANG,
|
||||
LC_ALL: process.env.LC_ALL,
|
||||
// Node.js specific
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
NODE_PATH: process.env.NODE_PATH,
|
||||
// npm/pnpm/yarn paths
|
||||
npm_config_prefix: process.env.npm_config_prefix,
|
||||
// Include any TSPM_ prefixed vars
|
||||
...Object.fromEntries(
|
||||
Object.entries(process.env).filter(([key]) => key.startsWith('TSPM_'))
|
||||
),
|
||||
};
|
||||
|
||||
// Remove undefined values
|
||||
Object.keys(essentialEnvVars).forEach(key => {
|
||||
if (essentialEnvVars[key] === undefined) {
|
||||
delete essentialEnvVars[key];
|
||||
}
|
||||
});
|
||||
|
||||
const response = await tspmIpcClient.request('add', {
|
||||
config: {
|
||||
name,
|
||||
@@ -76,9 +103,7 @@ export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
args: cmdArgs,
|
||||
projectDir,
|
||||
memoryLimitBytes: memoryLimit,
|
||||
// Persist the PATH from the current CLI environment so managed
|
||||
// processes see the same PATH they had when added.
|
||||
env: { PATH: process.env.PATH || '' },
|
||||
env: essentialEnvVars,
|
||||
autorestart,
|
||||
watch,
|
||||
watchPaths,
|
||||
|
@@ -8,23 +8,25 @@ export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
smartcli,
|
||||
['delete', 'remove'],
|
||||
async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm delete <id> | tspm remove <id>');
|
||||
const target = argvArg._[1];
|
||||
if (!target) {
|
||||
console.error('Error: Please provide a process target');
|
||||
console.log('Usage: tspm delete <id|id:N|name:LABEL> | tspm remove <id|id:N|name:LABEL>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine if command was 'remove' to use the new IPC route, otherwise 'delete'
|
||||
const cmd = String(argvArg._[0]);
|
||||
const useRemove = cmd === 'remove';
|
||||
console.log(`${useRemove ? 'Removing' : 'Deleting'} process: ${id}`);
|
||||
const response = await tspmIpcClient.request(useRemove ? 'remove' : 'delete', { id } as any);
|
||||
const isRemoveAlias = cmd === 'remove';
|
||||
console.log(`${isRemoveAlias ? 'Removing' : 'Deleting'} process: ${target}`);
|
||||
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||
// Always call daemon 'delete'; 'remove' is CLI alias only
|
||||
const response = await tspmIpcClient.request('delete', { id: resolved.id } as any);
|
||||
|
||||
if (response.success) {
|
||||
console.log(`✓ ${response.message || (useRemove ? 'Removed successfully' : 'Deleted successfully')}`);
|
||||
console.log(`✓ ${response.message || (isRemoveAlias ? 'Removed successfully' : 'Deleted successfully')}`);
|
||||
} else {
|
||||
console.error(`✗ Failed to ${useRemove ? 'remove' : 'delete'} process: ${response.message}`);
|
||||
console.error(`✗ Failed to ${isRemoveAlias ? 'remove' : 'delete'} process: ${response.message}`);
|
||||
}
|
||||
},
|
||||
{ actionLabel: 'delete/remove process' },
|
||||
|
@@ -9,16 +9,17 @@ export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
smartcli,
|
||||
'describe',
|
||||
async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm describe <id>');
|
||||
const target = argvArg._[1];
|
||||
if (!target) {
|
||||
console.error('Error: Please provide a process target');
|
||||
console.log('Usage: tspm describe <id | id:N | name:LABEL>');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await tspmIpcClient.request('describe', { id });
|
||||
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||
const response = await tspmIpcClient.request('describe', { id: resolved.id });
|
||||
|
||||
console.log(`Process Details: ${id}`);
|
||||
console.log(`Process Details: ${response.config.name || resolved.id}`);
|
||||
console.log('─'.repeat(40));
|
||||
console.log(`Status: ${response.processInfo.status}`);
|
||||
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
|
||||
|
@@ -9,109 +9,66 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
smartcli,
|
||||
'edit',
|
||||
async (argvArg: CliArguments) => {
|
||||
const idRaw = argvArg._[1];
|
||||
if (!idRaw) {
|
||||
console.error('Error: Please provide a process ID to edit');
|
||||
console.log('Usage: tspm edit <id>');
|
||||
const target = argvArg._[1];
|
||||
if (!target) {
|
||||
console.error('Error: Please provide a process target to edit');
|
||||
console.log('Usage: tspm edit <id | id:N | name:LABEL>');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = idRaw;
|
||||
// Resolve and load current config
|
||||
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||
const { config } = await tspmIpcClient.request('describe', { id: resolved.id });
|
||||
|
||||
// Load current config
|
||||
const { config } = await tspmIpcClient.request('describe', { id });
|
||||
// Interactive editing is temporarily disabled - needs smartinteract API update
|
||||
console.log('Interactive editing is temporarily disabled.');
|
||||
console.log('Current configuration:');
|
||||
console.log(` Name: ${config.name}`);
|
||||
console.log(` Command: ${config.command}`);
|
||||
console.log(` Directory: ${config.projectDir}`);
|
||||
console.log(` Memory: ${formatMemory(config.memoryLimitBytes)}`);
|
||||
console.log(` Auto-restart: ${config.autorestart}`);
|
||||
console.log(` Watch: ${config.watch ? 'enabled' : 'disabled'}`);
|
||||
|
||||
// For now, just update environment variables to current
|
||||
const essentialEnvVars: NodeJS.ProcessEnv = {
|
||||
PATH: process.env.PATH || '',
|
||||
HOME: process.env.HOME,
|
||||
USER: process.env.USER,
|
||||
SHELL: process.env.SHELL,
|
||||
LANG: process.env.LANG,
|
||||
LC_ALL: process.env.LC_ALL,
|
||||
// Node.js specific
|
||||
NODE_ENV: process.env.NODE_ENV,
|
||||
NODE_PATH: process.env.NODE_PATH,
|
||||
// npm/pnpm/yarn paths
|
||||
npm_config_prefix: process.env.npm_config_prefix,
|
||||
// Include any TSPM_ prefixed vars
|
||||
...Object.fromEntries(
|
||||
Object.entries(process.env).filter(([key]) => key.startsWith('TSPM_'))
|
||||
),
|
||||
};
|
||||
|
||||
const si = plugins.smartinteract;
|
||||
// Remove undefined values
|
||||
Object.keys(essentialEnvVars).forEach(key => {
|
||||
if (essentialEnvVars[key] === undefined) {
|
||||
delete essentialEnvVars[key];
|
||||
}
|
||||
});
|
||||
|
||||
const answers: any = {};
|
||||
answers.name = await si.question.text(
|
||||
`Name [${config.name || ''}]`,
|
||||
config.name || '',
|
||||
);
|
||||
answers.command = await si.question.text(
|
||||
`Command [${config.command}]`,
|
||||
config.command,
|
||||
);
|
||||
const currentArgs = (config.args || []).join(' ');
|
||||
const argsStr = await si.question.text(
|
||||
`Args (space separated) [${currentArgs}]`,
|
||||
currentArgs,
|
||||
);
|
||||
answers.args = argsStr.trim() ? argsStr.split(/\s+/) : [];
|
||||
answers.projectDir = await si.question.text(
|
||||
`Working directory [${config.projectDir}]`,
|
||||
config.projectDir,
|
||||
);
|
||||
const memStrDefault = formatMemory(config.memoryLimitBytes);
|
||||
const memStr = await si.question.text(
|
||||
`Memory limit [${memStrDefault}]`,
|
||||
memStrDefault,
|
||||
);
|
||||
answers.memoryLimitBytes = parseMemoryString(memStr || memStrDefault);
|
||||
answers.autorestart = await si.question.confirm(
|
||||
`Autorestart? [${config.autorestart ? 'Y' : 'N'}]`,
|
||||
!!config.autorestart,
|
||||
);
|
||||
const watchEnabled = await si.question.confirm(
|
||||
`Watch for changes? [${config.watch ? 'Y' : 'N'}]`,
|
||||
!!config.watch,
|
||||
);
|
||||
answers.watch = watchEnabled;
|
||||
if (watchEnabled) {
|
||||
const existingWatch = (config.watchPaths || []).join(',');
|
||||
const watchStr = await si.question.text(
|
||||
`Watch paths (comma separated) [${existingWatch}]`,
|
||||
existingWatch,
|
||||
);
|
||||
answers.watchPaths = watchStr
|
||||
.split(',')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
// Update environment variables
|
||||
const updates = {
|
||||
env: { ...(config.env || {}), ...essentialEnvVars }
|
||||
};
|
||||
|
||||
const replacePath = await si.question.confirm(
|
||||
'Replace stored PATH with current PATH?',
|
||||
false,
|
||||
);
|
||||
|
||||
const updates: any = {};
|
||||
if (answers.name !== config.name) updates.name = answers.name;
|
||||
if (answers.command !== config.command) updates.command = answers.command;
|
||||
if (JSON.stringify(answers.args) !== JSON.stringify(config.args || []))
|
||||
updates.args = answers.args.length ? answers.args : undefined;
|
||||
if (answers.projectDir !== config.projectDir)
|
||||
updates.projectDir = answers.projectDir;
|
||||
if (answers.memoryLimitBytes !== config.memoryLimitBytes)
|
||||
updates.memoryLimitBytes = answers.memoryLimitBytes;
|
||||
if (answers.autorestart !== config.autorestart)
|
||||
updates.autorestart = answers.autorestart;
|
||||
if (answers.watch !== config.watch) updates.watch = answers.watch;
|
||||
if (answers.watch && JSON.stringify(answers.watchPaths || []) !== JSON.stringify(config.watchPaths || []))
|
||||
updates.watchPaths = answers.watchPaths;
|
||||
|
||||
if (replacePath) {
|
||||
updates.env = { ...(config.env || {}), PATH: process.env.PATH || '' };
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length === 0) {
|
||||
console.log('No changes. Nothing to update.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { config: newConfig } = await tspmIpcClient.request('update', {
|
||||
id,
|
||||
const updateResponse = await tspmIpcClient.request('update', {
|
||||
id: resolved.id,
|
||||
updates,
|
||||
} as any);
|
||||
});
|
||||
|
||||
console.log('✓ Updated process configuration');
|
||||
console.log(` ID: ${newConfig.id}`);
|
||||
console.log(` Command: ${newConfig.command}`);
|
||||
console.log(` CWD: ${newConfig.projectDir}`);
|
||||
if (newConfig.env?.PATH) {
|
||||
console.log(' PATH: [stored]');
|
||||
}
|
||||
console.log('✓ Environment variables updated');
|
||||
console.log(' Process configuration updated successfully');
|
||||
},
|
||||
{ actionLabel: 'edit process config' },
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -11,10 +11,10 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
smartcli,
|
||||
'logs',
|
||||
async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm logs <id> [options]');
|
||||
const target = argvArg._[1];
|
||||
if (!target) {
|
||||
console.error('Error: Please provide a process target');
|
||||
console.log('Usage: tspm logs <id | id:N | name:LABEL> [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)');
|
||||
@@ -24,6 +24,8 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
const lines = getNumber(argvArg, 'lines', 50);
|
||||
const follow = getBool(argvArg, 'follow', 'f');
|
||||
|
||||
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||
const id = resolved.id;
|
||||
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
||||
|
||||
if (!follow) {
|
||||
@@ -44,7 +46,7 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
}
|
||||
|
||||
// Streaming mode
|
||||
console.log(`Logs for process: ${id} (streaming...)`);
|
||||
console.log(`Logs for process: ${resolved.name || id} (streaming...)`);
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
let lastSeq = 0;
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||
import { toProcessId } from '../../../shared/protocol/id.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
|
||||
@@ -11,9 +10,9 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
async (argvArg: CliArguments) => {
|
||||
const arg = argvArg._[1];
|
||||
if (!arg) {
|
||||
console.error('Error: Please provide a process ID or "all"');
|
||||
console.error('Error: Please provide a process target or "all"');
|
||||
console.log('Usage:');
|
||||
console.log(' tspm restart <id>');
|
||||
console.log(' tspm restart <id | id:N | name:LABEL>');
|
||||
console.log(' tspm restart all');
|
||||
return;
|
||||
}
|
||||
@@ -33,12 +32,13 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
return;
|
||||
}
|
||||
|
||||
const id = String(arg);
|
||||
console.log(`Restarting process: ${id}`);
|
||||
const response = await tspmIpcClient.request('restart', { id: toProcessId(id) });
|
||||
const target = String(arg);
|
||||
console.log(`Restarting process: ${target}`);
|
||||
const resolved = await tspmIpcClient.request('resolveTarget', { target });
|
||||
const response = await tspmIpcClient.request('restart', { id: resolved.id });
|
||||
|
||||
console.log(`✓ Process restarted successfully`);
|
||||
console.log(` ID: ${response.processId}`);
|
||||
console.log(` ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
|
||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||
console.log(` Status: ${response.status}`);
|
||||
},
|
||||
|
62
ts/cli/commands/process/search.ts
Normal file
62
ts/cli/commands/process/search.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||
import type { CliArguments } from '../../types.js';
|
||||
import { registerIpcCommand } from '../../registration/index.js';
|
||||
|
||||
export function registerSearchCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
registerIpcCommand(
|
||||
smartcli,
|
||||
'search',
|
||||
async (argvArg: CliArguments) => {
|
||||
const query = String(argvArg._[1] || '').trim();
|
||||
if (!query) {
|
||||
console.error('Error: Please provide a search query');
|
||||
console.log('Usage: tspm search <name-fragment | id-fragment>');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch list of processes, then enrich with names via describe
|
||||
const listRes = await tspmIpcClient.request('list', {});
|
||||
const processes = listRes.processes;
|
||||
|
||||
// If there are no processes, short-circuit
|
||||
if (processes.length === 0) {
|
||||
console.log('No processes found.');
|
||||
return;
|
||||
}
|
||||
|
||||
const lowerQ = query.toLowerCase();
|
||||
const matches: Array<{ id: number; name?: string }> = [];
|
||||
|
||||
// Collect describe calls to obtain names
|
||||
for (const proc of processes) {
|
||||
try {
|
||||
const desc = await tspmIpcClient.request('describe', { id: proc.id });
|
||||
const name = desc.config.name || '';
|
||||
const idStr = String(proc.id);
|
||||
if (name.toLowerCase().includes(lowerQ) || idStr.includes(query)) {
|
||||
matches.push({ id: proc.id, name });
|
||||
}
|
||||
} catch {
|
||||
// Ignore describe errors for individual processes
|
||||
}
|
||||
}
|
||||
|
||||
if (matches.length === 0) {
|
||||
console.log(`No matches for "${query}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Matches for "${query}":`);
|
||||
for (const m of matches) {
|
||||
if (m.name) {
|
||||
console.log(`- id:${m.id}\tname:${m.name}`);
|
||||
} else {
|
||||
console.log(`- id:${m.id}`);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ actionLabel: 'search processes' },
|
||||
);
|
||||
}
|
||||
|
@@ -10,17 +10,18 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
smartcli,
|
||||
'start',
|
||||
async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID to start');
|
||||
console.log('Usage: tspm start <id>');
|
||||
const target = argvArg._[1];
|
||||
if (!target) {
|
||||
console.error('Error: Please provide a process target to start');
|
||||
console.log('Usage: tspm start <id | id:N | name:LABEL>');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Starting process id ${id}...`);
|
||||
const response = await tspmIpcClient.request('startById', { id });
|
||||
console.log(`Starting process: ${target}...`);
|
||||
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||
const response = await tspmIpcClient.request('startById', { id: resolved.id });
|
||||
console.log('✓ Process started');
|
||||
console.log(` ID: ${response.processId}`);
|
||||
console.log(` ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
|
||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||
console.log(` Status: ${response.status}`);
|
||||
},
|
||||
|
@@ -8,15 +8,16 @@ export function registerStopCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||
smartcli,
|
||||
'stop',
|
||||
async (argvArg: CliArguments) => {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm stop <id>');
|
||||
const target = argvArg._[1];
|
||||
if (!target) {
|
||||
console.error('Error: Please provide a process target');
|
||||
console.log('Usage: tspm stop <id | id:N | name:LABEL>');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Stopping process: ${id}`);
|
||||
const response = await tspmIpcClient.request('stop', { id });
|
||||
console.log(`Stopping process: ${target}`);
|
||||
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||
const response = await tspmIpcClient.request('stop', { id: resolved.id });
|
||||
|
||||
if (response.success) {
|
||||
console.log(`✓ ${response.message}`);
|
||||
|
@@ -10,6 +10,7 @@ import { registerAddCommand } from './commands/process/add.js';
|
||||
import { registerStopCommand } from './commands/process/stop.js';
|
||||
import { registerRestartCommand } from './commands/process/restart.js';
|
||||
import { registerDeleteCommand } from './commands/process/delete.js';
|
||||
import { registerSearchCommand } from './commands/process/search.js';
|
||||
import { registerListCommand } from './commands/process/list.js';
|
||||
import { registerDescribeCommand } from './commands/process/describe.js';
|
||||
import { registerLogsCommand } from './commands/process/logs.js';
|
||||
@@ -74,6 +75,7 @@ export const run = async (): Promise<void> => {
|
||||
registerDescribeCommand(smartcliInstance);
|
||||
registerLogsCommand(smartcliInstance);
|
||||
registerEditCommand(smartcliInstance);
|
||||
registerSearchCommand(smartcliInstance);
|
||||
|
||||
// Batch commands
|
||||
registerStartAllCommand(smartcliInstance);
|
||||
|
@@ -156,6 +156,11 @@ export class ProcessManager extends EventEmitter {
|
||||
this.updateProcessInfo(config.id, { pid: undefined });
|
||||
});
|
||||
|
||||
// Set up failure handler to mark process as errored
|
||||
monitor.on('failed', () => {
|
||||
this.updateProcessInfo(config.id, { status: 'errored', pid: undefined });
|
||||
});
|
||||
|
||||
await monitor.start();
|
||||
|
||||
// Wait a moment for the process to spawn and get its PID
|
||||
@@ -327,6 +332,11 @@ export class ProcessManager extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
// Mark errored on failure events
|
||||
newMonitor.on('failed', () => {
|
||||
this.updateProcessInfo(id, { status: 'errored', pid: undefined });
|
||||
});
|
||||
|
||||
this.logger.info(`Successfully restarted process with id '${id}'`);
|
||||
} catch (error: Error | unknown) {
|
||||
const processError = new ProcessError(
|
||||
|
@@ -18,6 +18,10 @@ export class ProcessMonitor extends EventEmitter {
|
||||
private processId?: ProcessId;
|
||||
private currentLogMemorySize: number = 0;
|
||||
private readonly MAX_LOG_MEMORY_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
private restartTimer: NodeJS.Timeout | null = null;
|
||||
private lastRetryAt: number | null = null;
|
||||
private readonly MAX_RETRIES = 10;
|
||||
private readonly RESET_WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
constructor(config: IMonitorConfig & { id?: ProcessId }) {
|
||||
super();
|
||||
@@ -132,10 +136,7 @@ export class ProcessMonitor extends EventEmitter {
|
||||
this.emit('exit', code, signal);
|
||||
|
||||
if (!this.stopped) {
|
||||
this.logger.info('Restarting process...');
|
||||
this.log('Restarting process...');
|
||||
this.restartCount++;
|
||||
this.spawnProcess();
|
||||
this.scheduleRestart('exit');
|
||||
} else {
|
||||
this.logger.debug(
|
||||
'Not restarting process because monitor is stopped',
|
||||
@@ -164,10 +165,7 @@ export class ProcessMonitor extends EventEmitter {
|
||||
}
|
||||
|
||||
if (!this.stopped) {
|
||||
this.logger.info('Restarting process due to error...');
|
||||
this.log('Restarting process due to error...');
|
||||
this.restartCount++;
|
||||
this.spawnProcess();
|
||||
this.scheduleRestart('error');
|
||||
} else {
|
||||
this.logger.debug('Not restarting process because monitor is stopped');
|
||||
}
|
||||
@@ -185,6 +183,49 @@ export class ProcessMonitor extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a restart with incremental debounce and failure cutoff.
|
||||
*/
|
||||
private scheduleRestart(reason: 'exit' | 'error'): void {
|
||||
const now = Date.now();
|
||||
// Reset window: if last retry was more than 1 hour ago, reset counter
|
||||
if (this.lastRetryAt && now - this.lastRetryAt >= this.RESET_WINDOW_MS) {
|
||||
this.logger.info('Resetting retry counter after 1 hour window');
|
||||
this.restartCount = 0;
|
||||
}
|
||||
|
||||
// Already at or above max retries?
|
||||
if (this.restartCount >= this.MAX_RETRIES) {
|
||||
const msg = 'Maximum restart attempts reached. Marking process as failed.';
|
||||
this.logger.warn(msg);
|
||||
this.log(msg);
|
||||
this.stopped = true;
|
||||
// Emit a specific event so manager can set status to errored
|
||||
this.emit('failed');
|
||||
return;
|
||||
}
|
||||
|
||||
// Increment and compute delay (1..10 seconds)
|
||||
this.restartCount++;
|
||||
const delaySec = Math.min(this.restartCount, 10);
|
||||
const msg = `Restarting process in ${delaySec}s (attempt ${this.restartCount}/${this.MAX_RETRIES}) due to ${reason}...`;
|
||||
this.logger.info(msg);
|
||||
this.log(msg);
|
||||
|
||||
// Clear existing timer if any, then schedule
|
||||
if (this.restartTimer) {
|
||||
clearTimeout(this.restartTimer);
|
||||
}
|
||||
this.lastRetryAt = now;
|
||||
this.restartTimer = setTimeout(() => {
|
||||
// If stopped in the meantime, do not spawn
|
||||
if (this.stopped) {
|
||||
return;
|
||||
}
|
||||
this.spawnProcess();
|
||||
}, delaySec * 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
|
@@ -208,6 +208,8 @@ export class TspmDaemon {
|
||||
async (request: RequestForMethod<'delete'>) => {
|
||||
try {
|
||||
const id = toProcessId(request.id);
|
||||
// Ensure desired state reflects stopped before deletion
|
||||
await this.tspmInstance.setDesiredState(id, 'stopped');
|
||||
await this.tspmInstance.delete(id);
|
||||
return {
|
||||
success: true,
|
||||
@@ -246,18 +248,7 @@ export class TspmDaemon {
|
||||
},
|
||||
);
|
||||
|
||||
this.ipcServer.onMessage(
|
||||
'remove',
|
||||
async (request: RequestForMethod<'remove'>) => {
|
||||
try {
|
||||
const id = toProcessId(request.id);
|
||||
await this.tspmInstance.delete(id);
|
||||
return { success: true, message: `Process ${id} deleted successfully` };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to remove process: ${error.message}`);
|
||||
}
|
||||
},
|
||||
);
|
||||
// Note: 'remove' is only a CLI alias. Daemon exposes 'delete' only.
|
||||
|
||||
this.ipcServer.onMessage(
|
||||
'list',
|
||||
@@ -291,6 +282,58 @@ export class TspmDaemon {
|
||||
},
|
||||
);
|
||||
|
||||
// Resolve target (id:n | name:foo | numeric string) to ProcessId
|
||||
this.ipcServer.onMessage(
|
||||
'resolveTarget',
|
||||
async (request: RequestForMethod<'resolveTarget'>) => {
|
||||
const raw = String(request.target || '').trim();
|
||||
if (!raw) {
|
||||
throw new Error('Empty target');
|
||||
}
|
||||
|
||||
// id:<n>
|
||||
if (/^id:\s*\d+$/i.test(raw)) {
|
||||
const idNum = raw.split(':')[1].trim();
|
||||
const id = toProcessId(idNum);
|
||||
const config = this.tspmInstance.processConfigs.get(id);
|
||||
if (!config) throw new Error(`Process ${id} not found`);
|
||||
return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
|
||||
}
|
||||
|
||||
// name:<label>
|
||||
if (/^name:/i.test(raw)) {
|
||||
const name = raw.slice(raw.indexOf(':') + 1).trim();
|
||||
if (!name) throw new Error('Missing name after name:');
|
||||
const matches = Array.from(this.tspmInstance.processConfigs.values()).filter(
|
||||
(c) => (c.name || '').trim() === name,
|
||||
);
|
||||
if (matches.length === 0) {
|
||||
throw new Error(`No process found with name "${name}"`);
|
||||
}
|
||||
if (matches.length > 1) {
|
||||
const ids = matches.map((c) => String(c.id)).join(', ');
|
||||
throw new Error(
|
||||
`Multiple processes found with name "${name}": ids [${ids}]. Please use id:<n>.`,
|
||||
);
|
||||
}
|
||||
return { id: matches[0].id, name } as ResponseForMethod<'resolveTarget'>;
|
||||
}
|
||||
|
||||
// bare numeric id
|
||||
if (/^\d+$/.test(raw)) {
|
||||
const id = toProcessId(raw);
|
||||
const config = this.tspmInstance.processConfigs.get(id);
|
||||
if (!config) throw new Error(`Process ${id} not found`);
|
||||
return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
|
||||
}
|
||||
|
||||
// Unknown format
|
||||
throw new Error(
|
||||
'Unsupported target format. Use numeric id (e.g. 1), id:<n> (e.g. id:1), or name:<label> (e.g. name:api).',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// Batch operations handlers
|
||||
this.ipcServer.onMessage(
|
||||
'startAll',
|
||||
|
@@ -240,14 +240,6 @@ export interface AddResponse {
|
||||
}
|
||||
|
||||
// Remove (delete config and stop if running)
|
||||
export interface RemoveRequest {
|
||||
id: ProcessId;
|
||||
}
|
||||
|
||||
export interface RemoveResponse {
|
||||
success: boolean;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
// Update (modify existing config)
|
||||
export interface UpdateRequest {
|
||||
@@ -260,6 +252,16 @@ export interface UpdateResponse {
|
||||
config: IProcessConfig;
|
||||
}
|
||||
|
||||
// Resolve a user-provided target (id:n or name:foo or numeric string) to a ProcessId
|
||||
export interface ResolveTargetRequest {
|
||||
target: string;
|
||||
}
|
||||
|
||||
export interface ResolveTargetResponse {
|
||||
id: ProcessId;
|
||||
name?: string;
|
||||
}
|
||||
|
||||
// Type mappings for methods
|
||||
export type IpcMethodMap = {
|
||||
start: { request: StartRequest; response: StartResponse };
|
||||
@@ -269,7 +271,6 @@ export type IpcMethodMap = {
|
||||
delete: { request: DeleteRequest; response: DeleteResponse };
|
||||
add: { request: AddRequest; response: AddResponse };
|
||||
update: { request: UpdateRequest; response: UpdateResponse };
|
||||
remove: { request: RemoveRequest; response: RemoveResponse };
|
||||
list: { request: ListRequest; response: ListResponse };
|
||||
describe: { request: DescribeRequest; response: DescribeResponse };
|
||||
getLogs: { request: GetLogsRequest; response: GetLogsResponse };
|
||||
@@ -286,6 +287,7 @@ export type IpcMethodMap = {
|
||||
response: DaemonShutdownResponse;
|
||||
};
|
||||
heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse };
|
||||
resolveTarget: { request: ResolveTargetRequest; response: ResolveTargetResponse };
|
||||
};
|
||||
|
||||
// Helper type to extract request type for a method
|
||||
|
Reference in New Issue
Block a user