fix(cli): improve project metadata loading and normalize CLI error handling

This commit is contained in:
2026-05-09 12:34:58 +00:00
parent c7c1bbb460
commit adb13b027d
13 changed files with 3328 additions and 5222 deletions
+7
View File
@@ -1,5 +1,12 @@
# Changelog
## 2026-05-09 - 5.10.5 - fix(cli)
improve project metadata loading and normalize CLI error handling
- load package metadata asynchronously via ProjectInfo.create in the CLI and daemon to avoid initialization issues
- use handleError when reporting daemon and service command failures for safer error messages
- tighten interactive edit validation for required text fields and refresh documentation to match current CLI behavior
## 2026-03-24 - 5.10.4 - fix(crash-logging)
migrate filesystem persistence to smartfs and stabilize crash log tests
+10 -12
View File
@@ -24,19 +24,18 @@
"tspm": "./cli.js"
},
"devDependencies": {
"@git.zone/tsbuild": "^4.3.0",
"@git.zone/tsbundle": "^2.9.3",
"@git.zone/tsrun": "^2.0.1",
"@git.zone/tstest": "^3.5.1",
"@push.rocks/tapbundle": "^6.0.3",
"@types/node": "^25.5.0"
"@git.zone/tsbuild": "^4.4.0",
"@git.zone/tsbundle": "^2.10.1",
"@git.zone/tsrun": "^2.0.3",
"@git.zone/tstest": "^3.6.5",
"@types/node": "^25.6.2"
},
"dependencies": {
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartcli": "^4.0.20",
"@push.rocks/smartconfig": "^6.0.1",
"@push.rocks/smartdaemon": "^2.1.0",
"@push.rocks/smartfs": "^1.5.0",
"@push.rocks/projectinfo": "^5.1.0",
"@push.rocks/smartcli": "^4.0.21",
"@push.rocks/smartconfig": "^6.1.1",
"@push.rocks/smartdaemon": "^2.1.1",
"@push.rocks/smartfs": "^1.5.1",
"@push.rocks/smartinteract": "^2.0.16",
"@push.rocks/smartipc": "^2.3.0",
"@push.rocks/smartpath": "^6.0.0",
@@ -67,7 +66,6 @@
"readme.md"
],
"pnpm": {
"overrides": {},
"onlyBuiltDependencies": [
"esbuild",
"mongodb-memory-server",
+3242 -5143
View File
File diff suppressed because it is too large Load Diff
+29 -29
View File
@@ -15,7 +15,7 @@ TSPM is your production-ready process manager that handles the hard parts of run
- 🧠 **Smart Memory Management** — Tracks memory across entire process trees (parent + children), enforces limits, and auto-restarts on OOM
- 💾 **Persistent Log Storage** — 10MB in-memory ring buffer per process, auto-persists to disk on stop/restart/crash
- 🔄 **Intelligent Auto-Restart** — Crashed processes restart with incremental backoff (1s → 10s), auto-stop after 10 consecutive failures
- 👀 **File Watching** — Auto-restart on file changes for seamless development workflows
- 🧾 **Persistent Process Configs** — Saved process definitions and desired states survive daemon restarts
- 🌳 **Process Tree Tracking** — Monitors parent and all child processes as a unit — no orphans, ever
- 🏗️ **Daemon Architecture** — Persistent background service that survives terminal sessions via Unix socket IPC
- 📊 **Beautiful CLI** — Clean, informative output with table views, real-time log streaming, and search
@@ -83,8 +83,8 @@ Register a new process configuration (without starting it).
| `--name <name>` | Process name | command string |
| `--memory <size>` | Memory limit (e.g. `512MB`, `2GB`) | `512MB` |
| `--cwd <path>` | Working directory | current directory |
| `--watch` | Enable file watching | `false` |
| `--watch-paths <paths>` | Comma-separated watch paths | — |
| `--watch` | Store watch intent in the process config | `false` |
| `--watch-paths <paths>` | Comma-separated watch paths stored with the config | — |
| `--autorestart` | Auto-restart on crash | `true` |
| `-i, --interactive` | Enter interactive edit after adding | — |
@@ -95,8 +95,8 @@ tspm add "node server.js" --name api-server
# TypeScript with 2GB memory limit
tspm add "tsx src/index.ts" --name production-api --memory 2GB
# Dev mode with file watching
tspm add "tsx watch src/index.ts" --name dev-server --watch --watch-paths "src,config"
# TypeScript entry file (runs through tsx automatically)
tspm add ./src/index.ts --name ts-worker --memory 2GB
# One-shot worker (no auto-restart)
tspm add "node worker.js" --name batch-job --autorestart false
@@ -131,12 +131,13 @@ Stop and restart a process, preserving its configuration.
tspm restart name:my-server
```
#### `tspm delete <target>`
#### `tspm delete <target>` / `tspm remove <target>`
Stop, remove from management, and delete persisted logs.
```bash
tspm delete name:old-server
tspm remove id:3
```
#### `tspm edit <target>`
@@ -161,16 +162,16 @@ tspm search api
#### `tspm list`
Display all managed processes in a table.
Display all managed processes with live runtime stats. Use `tspm describe <target>` for saved command, directory, and full process configuration details.
```
┌─────┬─────────────┬──────────┬───────┬──────────┬──────────┐
│ 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 │
└─────┴─────────────┴─────────────────┴──────────┴──────────┘
┌─────────┬─────────────┬──────────┬───────────┬──────────┬──────────┬─────────
│ ID │ Name │ Status │ PID │ Memory │ CPU │ Restarts │
├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┼──────────┤
│ 1 │ 1 │ online │ 45123 │ 245.3 MB │ 2.1% │ 0 │
│ 2 │ 2 │ online │ 45456 │ 128.7 MB │ 0.5% │ 2 │
│ 3 │ 3 │ stopped │ - │ 0 B │ - │ 5 │
└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┴──────────┘
```
#### `tspm describe <target>`
@@ -194,7 +195,6 @@ tspm describe name:my-server
# Directory: /home/user/project
# Memory Limit: 2 GB
# Auto-restart: true
# Watch: disabled
```
#### `tspm logs <target> [options]`
@@ -230,18 +230,20 @@ tspm logs name:my-server --since 10m --ndjson
tspm start-all # Start all saved processes
tspm stop-all # Stop all running processes
tspm restart-all # Restart all running processes
tspm restart all # Alternate all-process restart form
tspm reset # ⚠️ Stop all + clear all configs (prompts for confirmation)
```
### Daemon Management
The daemon is a persistent background service that manages all processes. It starts automatically when needed.
The daemon is a persistent background service that manages all processes. Start it explicitly for the current session, or install it as a systemd service for boot startup.
```bash
tspm daemon start # Start the daemon
tspm daemon stop # Stop daemon + all managed processes
tspm daemon restart # Restart daemon (preserves processes)
tspm daemon status # Check daemon health + stats
tspm stats # Detailed daemon stats + process table
```
### System Service (systemd)
@@ -285,7 +287,7 @@ TSPM uses a clean three-tier architecture:
│ │ - Memory tracking & limits │ │
│ │ - Auto-restart logic │ │
│ │ - Log persistence (10MB) │ │
│ │ - File watching │ │
│ │ - Desired state restoration │ │
│ └────────────┬─────────────────────┘ │
│ │ │
│ ┌────────────▼─────────────────────┐ │
@@ -310,7 +312,7 @@ TSPM uses a clean three-tier architecture:
| **CLI** | Lightweight client that sends commands to daemon via IPC |
| **Daemon** | Persistent background service managing all processes |
| **ProcessManager** | High-level orchestration, config persistence, state management |
| **ProcessMonitor** | Memory limits, auto-restart with backoff, log persistence, file watching |
| **ProcessMonitor** | Memory limits, auto-restart with backoff, log persistence, process-tree stats |
| **ProcessWrapper** | Low-level process lifecycle, stream handling, signal management |
| **CrashLogManager** | Detailed crash reports with metadata and log history |
@@ -399,14 +401,9 @@ Multi-stage shutdown for reliability:
3. Send **SIGKILL** if still alive
4. Clean up **all child processes** in the tree
### File Watching
### TypeScript Entry Files
Development-friendly auto-restart:
- Watch specific directories or files
- `node_modules` ignored by default
- Debounced restart on file changes
- Configurable via `--watch-paths`
If `tspm add` receives a single `.ts` file, TSPM resolves it through `tsx` automatically and stores the resolved command plus file argument in the process config. Full command strings are still executed through the shell, so existing `node`, `tsx`, `pnpm`, or custom commands work as expected.
## 🐛 Debugging
@@ -417,8 +414,11 @@ tspm daemon status
# View process logs
tspm logs name:my-app --lines 200
# Check daemon stderr
tail -f /tmp/daemon-stderr.log
# Start with verbose daemon diagnostics
TSPM_DEBUG=true tspm daemon start
# For systemd-managed daemon logs
journalctl -u smartdaemon_tspm-daemon -f
# Force daemon restart
tspm daemon restart
@@ -449,13 +449,13 @@ tspm daemon restart
- 🚀 **Production Node.js apps** — Reliable process management with memory guards
- 🔧 **Microservices** — Manage multiple services from a single tool
- 👨‍💻 **Development**File watching and instant auto-restart
- 👨‍💻 **Development**TypeScript entry files, local workers, and quick daemon lifecycle commands
- 🏭 **Workers & Jobs** — Queue workers, cron jobs, background tasks
- 📊 **Resource-constrained environments** — Memory limits prevent OOM kills
## License and Legal Information
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [LICENSE](./license) file.
This repository contains open-source code licensed under the MIT License. A copy of the license can be found in the [license](./license) file.
**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.
+1 -1
View File
@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tspm',
version: '5.10.4',
version: '5.10.5',
description: 'a no fuzz process manager'
}
+4 -4
View File
@@ -1,7 +1,7 @@
import * as plugins from '../../plugins.js';
import * as paths from '../../../paths.js';
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import { Logger } from '../../../shared/common/utils.errorhandler.js';
import { handleError, Logger } from '../../../shared/common/utils.errorhandler.js';
import type { CliArguments } from '../../types.js';
import { formatMemory } from '../../helpers/memory.js';
@@ -76,7 +76,7 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
// Disconnect from the daemon after starting
await tspmIpcClient.disconnect();
} catch (error) {
console.error('Error starting daemon:', error.message);
console.error('Error starting daemon:', handleError(error).message);
process.exit(1);
}
break;
@@ -139,7 +139,7 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
// Disconnect from the daemon after stopping
await tspmIpcClient.disconnect();
} catch (error) {
console.error('Error stopping daemon:', error.message);
console.error('Error stopping daemon:', handleError(error).message);
process.exit(1);
}
break;
@@ -169,7 +169,7 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
// Disconnect from daemon after getting status
await tspmIpcClient.disconnect();
} catch (error) {
console.error('Error getting daemon status:', error.message);
console.error('Error getting daemon status:', handleError(error).message);
process.exit(1);
}
break;
+1 -1
View File
@@ -8,10 +8,10 @@ import { formatMemory } from '../helpers/memory.js';
export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
const cliLogger = new Logger('CLI');
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
smartcli.standardCommand().subscribe({
next: async (argvArg: CliArguments) => {
const tspmProjectinfo = await plugins.projectinfo.ProjectInfo.create(paths.packageDir);
console.log(
`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`,
);
+5 -4
View File
@@ -1,6 +1,6 @@
import * as plugins from '../../plugins.js';
import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
import { Logger } from '../../../shared/common/utils.errorhandler.js';
import { handleError, Logger } from '../../../shared/common/utils.errorhandler.js';
import type { CliArguments } from '../../types.js';
export function registerDisableCommand(smartcli: plugins.smartcli.Smartcli) {
@@ -18,10 +18,11 @@ export function registerDisableCommand(smartcli: plugins.smartcli.Smartcli) {
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);
const errorMessage = handleError(error).message;
console.error('Error disabling service:', errorMessage);
if (
error.message.includes('permission') ||
error.message.includes('denied')
errorMessage.includes('permission') ||
errorMessage.includes('denied')
) {
console.log('\nNote: You may need to run this command with sudo');
}
+5 -4
View File
@@ -1,6 +1,6 @@
import * as plugins from '../../plugins.js';
import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
import { Logger } from '../../../shared/common/utils.errorhandler.js';
import { handleError, Logger } from '../../../shared/common/utils.errorhandler.js';
import type { CliArguments } from '../../types.js';
export function registerEnableCommand(smartcli: plugins.smartcli.Smartcli) {
@@ -18,10 +18,11 @@ export function registerEnableCommand(smartcli: plugins.smartcli.Smartcli) {
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);
const errorMessage = handleError(error).message;
console.error('Error enabling service:', errorMessage);
if (
error.message.includes('permission') ||
error.message.includes('denied')
errorMessage.includes('permission') ||
errorMessage.includes('denied')
) {
console.log('\nNote: You may need to run this command with sudo');
}
+3 -3
View File
@@ -14,7 +14,7 @@ export async function interactiveEditProcess(processId: number): Promise<void> {
message: 'Process name:',
default: config.name,
validate: (input: string) => {
return input && input.trim() !== '';
return input.trim() !== '';
}
},
{
@@ -23,7 +23,7 @@ export async function interactiveEditProcess(processId: number): Promise<void> {
message: 'Command to execute:',
default: config.command,
validate: (input: string) => {
return input && input.trim() !== '';
return input.trim() !== '';
}
},
{
@@ -32,7 +32,7 @@ export async function interactiveEditProcess(processId: number): Promise<void> {
message: 'Working directory:',
default: config.projectDir,
validate: (input: string) => {
return input && input.trim() !== '';
return input.trim() !== '';
}
},
{
+1 -1
View File
@@ -33,7 +33,7 @@ export type { CliArguments } from './types.js';
*/
export const run = async (): Promise<void> => {
const cliLogger = new Logger('CLI');
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
const tspmProjectinfo = await plugins.projectinfo.ProjectInfo.create(paths.packageDir);
// Check if debug mode is enabled
const debugMode = process.env.TSPM_DEBUG === 'true';
+17 -15
View File
@@ -3,6 +3,7 @@ import * as paths from '../paths.js';
import { toProcessId } from '../shared/protocol/id.js';
import type { ProcessId } from '../shared/protocol/id.js';
import { ProcessManager } from './processmanager.js';
import { handleError } from '../shared/common/utils.errorhandler.js';
import type {
IpcMethodMap,
RequestForMethod,
@@ -17,7 +18,7 @@ import { LogPersistence } from './logpersistence.js';
*/
export class TspmDaemon {
private tspmInstance: ProcessManager;
private ipcServer: plugins.smartipc.IpcServer;
private ipcServer!: plugins.smartipc.IpcServer;
private startTime: number;
private isShuttingDown: boolean = false;
private socketPath: string;
@@ -30,13 +31,7 @@ export class TspmDaemon {
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
this.startTime = Date.now();
// Determine daemon version from package metadata
try {
const proj = new plugins.projectinfo.ProjectInfo(paths.packageDir);
this.version = proj.npm.version || 'unknown';
} catch {
this.version = 'unknown';
}
this.version = 'unknown';
}
/**
@@ -45,6 +40,13 @@ export class TspmDaemon {
public async start(): Promise<void> {
console.log('Starting TSPM daemon...');
try {
const proj = await plugins.projectinfo.ProjectInfo.create(paths.packageDir);
this.version = proj.npm.version || 'unknown';
} catch {
this.version = 'unknown';
}
// Ensure the TSPM directory exists
const fs = await import('fs/promises');
await fs.mkdir(paths.tspmDir, { recursive: true });
@@ -150,7 +152,7 @@ export class TspmDaemon {
status: processInfo?.status || 'stopped',
};
} catch (error) {
throw new Error(`Failed to start process: ${error.message}`);
throw new Error(`Failed to start process: ${handleError(error).message}`);
}
},
);
@@ -194,7 +196,7 @@ export class TspmDaemon {
status: processInfo?.status || 'stopped',
};
} catch (error) {
throw new Error(`Failed to start process: ${error.message}`);
throw new Error(`Failed to start process: ${handleError(error).message}`);
}
},
);
@@ -211,7 +213,7 @@ export class TspmDaemon {
message: `Process ${id} stopped successfully`,
};
} catch (error) {
throw new Error(`Failed to stop process: ${error.message}`);
throw new Error(`Failed to stop process: ${handleError(error).message}`);
}
},
);
@@ -230,7 +232,7 @@ export class TspmDaemon {
status: processInfo?.status || 'stopped',
};
} catch (error) {
throw new Error(`Failed to restart process: ${error.message}`);
throw new Error(`Failed to restart process: ${handleError(error).message}`);
}
},
);
@@ -248,7 +250,7 @@ export class TspmDaemon {
message: `Process ${id} deleted successfully`,
};
} catch (error) {
throw new Error(`Failed to delete process: ${error.message}`);
throw new Error(`Failed to delete process: ${handleError(error).message}`);
}
},
);
@@ -262,7 +264,7 @@ export class TspmDaemon {
const config = this.tspmInstance.processConfigs.get(id)!;
return { id, config };
} catch (error) {
throw new Error(`Failed to add process: ${error.message}`);
throw new Error(`Failed to add process: ${handleError(error).message}`);
}
},
);
@@ -275,7 +277,7 @@ export class TspmDaemon {
const updated = await this.tspmInstance.update(id, request.updates as any);
return { id, config: updated };
} catch (error) {
throw new Error(`Failed to update process: ${error.message}`);
throw new Error(`Failed to update process: ${handleError(error).message}`);
}
},
);
+1 -3
View File
@@ -7,9 +7,7 @@
"module": "NodeNext",
"moduleResolution": "NodeNext",
"esModuleInterop": true,
"verbatimModuleSyntax": true,
"baseUrl": ".",
"paths": {}
"verbatimModuleSyntax": true
},
"exclude": ["dist_*/**/*.d.ts"]
}