Compare commits

...

6 Commits

Author SHA1 Message Date
1a782f0768 3.0.2
Some checks failed
Default (tags) / security (push) Successful in 52s
Default (tags) / test (push) Failing after 1m7s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-28 16:29:41 +00:00
ae4148c82f fix(daemon): Ensure TSPM runtime dir exists and improve daemon startup/debug output 2025-08-28 16:29:41 +00:00
6141b26530 3.0.0
Some checks failed
Default (tags) / security (push) Failing after 11m45s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-08-28 15:52:29 +00:00
e73f4acd63 BREAKING CHANGE(daemon): Refactor daemon and service management: remove IPC auto-spawn, add TspmServiceManager, tighten IPC/client/CLI behavior and tests 2025-08-28 15:52:29 +00:00
8e3cfb624b 2.0.1
Some checks failed
Default (tags) / security (push) Failing after 11m47s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-08-28 15:51:11 +00:00
33fb02733d update 2025-08-28 15:47:59 +00:00
42 changed files with 1556 additions and 1315 deletions

View File

@@ -1,6 +1,27 @@
# Changelog # Changelog
## 2025-08-28 - 3.0.2 - fix(daemon)
Ensure TSPM runtime dir exists and improve daemon startup/debug output
- Create ~/.tspm directory before starting the daemon to avoid missing-directory errors
- Start daemon child process with stdio inherited when TSPM_DEBUG=true to surface startup errors during debugging
- Add warning and troubleshooting guidance when daemon process starts but does not respond (suggest checking socket file and using TSPM_DEBUG)
- Bump package version to 3.0.1
## 2025-08-28 - 3.0.0 - BREAKING CHANGE(daemon)
Refactor daemon and service management: remove IPC auto-spawn, add TspmServiceManager, tighten IPC/client/CLI behavior and tests
- Remove automatic daemon spawn from the IPC client — clients now error with guidance and require the daemon to be started manually or enabled as a system service
- Add TspmServiceManager to manage the daemon as a systemd service (enable/disable/reload/status)
- Update IPC server/client to use SmartIpc.createServer/createClient with heartbeat defaults and explicit onMessage handlers
- Daemon publishes per-process logs to topics (logs.<processId>) and re-emits ProcessMonitor logs for pub/sub
- CLI updated: add enable/disable service commands, adjust daemon start/stop/status workflows and improve user hints when daemon is not running
- Add/adjust integration and unit tests to cover daemon lifecycle, IPC client behavior, log streaming, heartbeat and resource reporting
- Documentation expanded (README, readme.plan.md, changelog) to reflect the refactor and migration notes
- Various code cleanups, formatting fixes and defensive checks across modules
## 2025-08-28 - 2.0.0 - BREAKING CHANGE(daemon) ## 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 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). - Do not auto-spawn the daemon from the IPC client anymore — attempts to connect will now error with instructions to start the daemon manually or enable the system service (breaking change).
@@ -12,6 +33,7 @@ Refactor daemon lifecycle and service management: remove IPC auto-spawn, add Tsp
- Updated development plan/readme (readme.plan.md) to reflect the refactor toward proper SmartDaemon integration and migration notes. - 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) ## 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 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 - Upgrade @push.rocks/smartipc dependency to ^2.1.2
@@ -24,6 +46,7 @@ Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC
- Standardized heartbeat and IPC timing defaults (heartbeatInterval: 5000ms, heartbeatTimeout: 20000ms, heartbeatInitialGracePeriodMs: 10000ms) - Standardized heartbeat and IPC timing defaults (heartbeatInterval: 5000ms, heartbeatTimeout: 20000ms, heartbeatInitialGracePeriodMs: 10000ms)
## 2025-08-25 - 1.7.0 - feat(readme) ## 2025-08-25 - 1.7.0 - feat(readme)
Add comprehensive README with detailed usage, command reference, daemon management, architecture and development instructions 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 - Expanded README from a short placeholder to a full documentation covering: Quick Start, Installation, Command Reference, Daemon Management, Monitoring & Information, Batch Operations, Architecture, Programmatic Usage, Advanced Features, Development, Debugging, Performance, and Legal information
@@ -32,6 +55,7 @@ Add comprehensive README with detailed usage, command reference, daemon manageme
- Improved onboarding instructions: cloning, installing, testing, building, and running the project - Improved onboarding instructions: cloning, installing, testing, building, and running the project
## 2025-08-25 - 1.6.1 - fix(daemon) ## 2025-08-25 - 1.6.1 - fix(daemon)
Fix smartipc integration and add daemon/ipc integration tests Fix smartipc integration and add daemon/ipc integration tests
- Replace direct smartipc server/client construction with SmartIpc.createServer/createClient and set heartbeat: false - Replace direct smartipc server/client construction with SmartIpc.createServer/createClient and set heartbeat: false
@@ -40,6 +64,7 @@ Fix smartipc integration and add daemon/ipc integration tests
- Add comprehensive tests: unit tests for TspmDaemon and TspmIpcClient and full integration tests for daemon lifecycle, process management, error handling, heartbeat and resource reporting - 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) ## 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 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. - Add central daemon implementation (ts/classes.daemon.ts) to manage all processes via a single background service and Unix socket.

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tspm", "name": "@git.zone/tspm",
"version": "2.0.0", "version": "3.0.2",
"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",
@@ -33,7 +33,8 @@
"@push.rocks/smartipc": "^2.1.2", "@push.rocks/smartipc": "^2.1.2",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"pidusage": "^4.0.1", "pidusage": "^4.0.1",
"ps-tree": "^1.2.0" "ps-tree": "^1.2.0",
"tsx": "^4.20.5"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

271
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
ps-tree: ps-tree:
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0 version: 1.2.0
tsx:
specifier: ^4.20.5
version: 4.20.5
devDependencies: devDependencies:
'@git.zone/tsbuild': '@git.zone/tsbuild':
specifier: ^2.6.7 specifier: ^2.6.7
@@ -362,252 +365,126 @@ packages:
'@emnapi/wasi-threads@1.0.4': '@emnapi/wasi-threads@1.0.4':
resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==} resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==}
'@esbuild/aix-ppc64@0.25.0':
resolution: {integrity: sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.25.9': '@esbuild/aix-ppc64@0.25.9':
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==} resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ppc64] cpu: [ppc64]
os: [aix] os: [aix]
'@esbuild/android-arm64@0.25.0':
resolution: {integrity: sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.25.9': '@esbuild/android-arm64@0.25.9':
resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==} resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
'@esbuild/android-arm@0.25.0':
resolution: {integrity: sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.25.9': '@esbuild/android-arm@0.25.9':
resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==} resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
'@esbuild/android-x64@0.25.0':
resolution: {integrity: sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.25.9': '@esbuild/android-x64@0.25.9':
resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==} resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [android] os: [android]
'@esbuild/darwin-arm64@0.25.0':
resolution: {integrity: sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.25.9': '@esbuild/darwin-arm64@0.25.9':
resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==} resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
'@esbuild/darwin-x64@0.25.0':
resolution: {integrity: sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.25.9': '@esbuild/darwin-x64@0.25.9':
resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==} resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
'@esbuild/freebsd-arm64@0.25.0':
resolution: {integrity: sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.25.9': '@esbuild/freebsd-arm64@0.25.9':
resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==} resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [freebsd] os: [freebsd]
'@esbuild/freebsd-x64@0.25.0':
resolution: {integrity: sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.25.9': '@esbuild/freebsd-x64@0.25.9':
resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==} resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
'@esbuild/linux-arm64@0.25.0':
resolution: {integrity: sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.25.9': '@esbuild/linux-arm64@0.25.9':
resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==} resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
'@esbuild/linux-arm@0.25.0':
resolution: {integrity: sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.25.9': '@esbuild/linux-arm@0.25.9':
resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==} resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
'@esbuild/linux-ia32@0.25.0':
resolution: {integrity: sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.25.9': '@esbuild/linux-ia32@0.25.9':
resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==} resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ia32] cpu: [ia32]
os: [linux] os: [linux]
'@esbuild/linux-loong64@0.25.0':
resolution: {integrity: sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.25.9': '@esbuild/linux-loong64@0.25.9':
resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==} resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [loong64] cpu: [loong64]
os: [linux] os: [linux]
'@esbuild/linux-mips64el@0.25.0':
resolution: {integrity: sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.25.9': '@esbuild/linux-mips64el@0.25.9':
resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==} resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [mips64el] cpu: [mips64el]
os: [linux] os: [linux]
'@esbuild/linux-ppc64@0.25.0':
resolution: {integrity: sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.25.9': '@esbuild/linux-ppc64@0.25.9':
resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==} resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ppc64] cpu: [ppc64]
os: [linux] os: [linux]
'@esbuild/linux-riscv64@0.25.0':
resolution: {integrity: sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.25.9': '@esbuild/linux-riscv64@0.25.9':
resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==} resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] os: [linux]
'@esbuild/linux-s390x@0.25.0':
resolution: {integrity: sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.25.9': '@esbuild/linux-s390x@0.25.9':
resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==} resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [s390x] cpu: [s390x]
os: [linux] os: [linux]
'@esbuild/linux-x64@0.25.0':
resolution: {integrity: sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.25.9': '@esbuild/linux-x64@0.25.9':
resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==} resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
'@esbuild/netbsd-arm64@0.25.0':
resolution: {integrity: sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [netbsd]
'@esbuild/netbsd-arm64@0.25.9': '@esbuild/netbsd-arm64@0.25.9':
resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==} resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [netbsd] os: [netbsd]
'@esbuild/netbsd-x64@0.25.0':
resolution: {integrity: sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.25.9': '@esbuild/netbsd-x64@0.25.9':
resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==} resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [netbsd] os: [netbsd]
'@esbuild/openbsd-arm64@0.25.0':
resolution: {integrity: sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-arm64@0.25.9': '@esbuild/openbsd-arm64@0.25.9':
resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==} resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [openbsd] os: [openbsd]
'@esbuild/openbsd-x64@0.25.0':
resolution: {integrity: sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.25.9': '@esbuild/openbsd-x64@0.25.9':
resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==} resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -620,48 +497,24 @@ packages:
cpu: [arm64] cpu: [arm64]
os: [openharmony] os: [openharmony]
'@esbuild/sunos-x64@0.25.0':
resolution: {integrity: sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.25.9': '@esbuild/sunos-x64@0.25.9':
resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==} resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [x64] cpu: [x64]
os: [sunos] os: [sunos]
'@esbuild/win32-arm64@0.25.0':
resolution: {integrity: sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.25.9': '@esbuild/win32-arm64@0.25.9':
resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==} resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
'@esbuild/win32-ia32@0.25.0':
resolution: {integrity: sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.25.9': '@esbuild/win32-ia32@0.25.9':
resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==} resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==}
engines: {node: '>=18'} engines: {node: '>=18'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
'@esbuild/win32-x64@0.25.0':
resolution: {integrity: sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.25.9': '@esbuild/win32-x64@0.25.9':
resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==} resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -2440,11 +2293,6 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
esbuild@0.25.0:
resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==}
engines: {node: '>=18'}
hasBin: true
esbuild@0.25.9: esbuild@0.25.9:
resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==} resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -4355,8 +4203,8 @@ packages:
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
engines: {node: '>=0.6.x'} engines: {node: '>=0.6.x'}
tsx@4.19.3: tsx@4.20.5:
resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==} resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
hasBin: true hasBin: true
@@ -5625,156 +5473,81 @@ snapshots:
tslib: 2.8.1 tslib: 2.8.1
optional: true optional: true
'@esbuild/aix-ppc64@0.25.0':
optional: true
'@esbuild/aix-ppc64@0.25.9': '@esbuild/aix-ppc64@0.25.9':
optional: true optional: true
'@esbuild/android-arm64@0.25.0':
optional: true
'@esbuild/android-arm64@0.25.9': '@esbuild/android-arm64@0.25.9':
optional: true optional: true
'@esbuild/android-arm@0.25.0':
optional: true
'@esbuild/android-arm@0.25.9': '@esbuild/android-arm@0.25.9':
optional: true optional: true
'@esbuild/android-x64@0.25.0':
optional: true
'@esbuild/android-x64@0.25.9': '@esbuild/android-x64@0.25.9':
optional: true optional: true
'@esbuild/darwin-arm64@0.25.0':
optional: true
'@esbuild/darwin-arm64@0.25.9': '@esbuild/darwin-arm64@0.25.9':
optional: true optional: true
'@esbuild/darwin-x64@0.25.0':
optional: true
'@esbuild/darwin-x64@0.25.9': '@esbuild/darwin-x64@0.25.9':
optional: true optional: true
'@esbuild/freebsd-arm64@0.25.0':
optional: true
'@esbuild/freebsd-arm64@0.25.9': '@esbuild/freebsd-arm64@0.25.9':
optional: true optional: true
'@esbuild/freebsd-x64@0.25.0':
optional: true
'@esbuild/freebsd-x64@0.25.9': '@esbuild/freebsd-x64@0.25.9':
optional: true optional: true
'@esbuild/linux-arm64@0.25.0':
optional: true
'@esbuild/linux-arm64@0.25.9': '@esbuild/linux-arm64@0.25.9':
optional: true optional: true
'@esbuild/linux-arm@0.25.0':
optional: true
'@esbuild/linux-arm@0.25.9': '@esbuild/linux-arm@0.25.9':
optional: true optional: true
'@esbuild/linux-ia32@0.25.0':
optional: true
'@esbuild/linux-ia32@0.25.9': '@esbuild/linux-ia32@0.25.9':
optional: true optional: true
'@esbuild/linux-loong64@0.25.0':
optional: true
'@esbuild/linux-loong64@0.25.9': '@esbuild/linux-loong64@0.25.9':
optional: true optional: true
'@esbuild/linux-mips64el@0.25.0':
optional: true
'@esbuild/linux-mips64el@0.25.9': '@esbuild/linux-mips64el@0.25.9':
optional: true optional: true
'@esbuild/linux-ppc64@0.25.0':
optional: true
'@esbuild/linux-ppc64@0.25.9': '@esbuild/linux-ppc64@0.25.9':
optional: true optional: true
'@esbuild/linux-riscv64@0.25.0':
optional: true
'@esbuild/linux-riscv64@0.25.9': '@esbuild/linux-riscv64@0.25.9':
optional: true optional: true
'@esbuild/linux-s390x@0.25.0':
optional: true
'@esbuild/linux-s390x@0.25.9': '@esbuild/linux-s390x@0.25.9':
optional: true optional: true
'@esbuild/linux-x64@0.25.0':
optional: true
'@esbuild/linux-x64@0.25.9': '@esbuild/linux-x64@0.25.9':
optional: true optional: true
'@esbuild/netbsd-arm64@0.25.0':
optional: true
'@esbuild/netbsd-arm64@0.25.9': '@esbuild/netbsd-arm64@0.25.9':
optional: true optional: true
'@esbuild/netbsd-x64@0.25.0':
optional: true
'@esbuild/netbsd-x64@0.25.9': '@esbuild/netbsd-x64@0.25.9':
optional: true optional: true
'@esbuild/openbsd-arm64@0.25.0':
optional: true
'@esbuild/openbsd-arm64@0.25.9': '@esbuild/openbsd-arm64@0.25.9':
optional: true optional: true
'@esbuild/openbsd-x64@0.25.0':
optional: true
'@esbuild/openbsd-x64@0.25.9': '@esbuild/openbsd-x64@0.25.9':
optional: true optional: true
'@esbuild/openharmony-arm64@0.25.9': '@esbuild/openharmony-arm64@0.25.9':
optional: true optional: true
'@esbuild/sunos-x64@0.25.0':
optional: true
'@esbuild/sunos-x64@0.25.9': '@esbuild/sunos-x64@0.25.9':
optional: true optional: true
'@esbuild/win32-arm64@0.25.0':
optional: true
'@esbuild/win32-arm64@0.25.9': '@esbuild/win32-arm64@0.25.9':
optional: true optional: true
'@esbuild/win32-ia32@0.25.0':
optional: true
'@esbuild/win32-ia32@0.25.9': '@esbuild/win32-ia32@0.25.9':
optional: true optional: true
'@esbuild/win32-x64@0.25.0':
optional: true
'@esbuild/win32-x64@0.25.9': '@esbuild/win32-x64@0.25.9':
optional: true optional: true
@@ -5837,7 +5610,7 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartfile': 11.2.0 '@push.rocks/smartfile': 11.2.0
'@push.rocks/smartshell': 3.2.3 '@push.rocks/smartshell': 3.2.3
tsx: 4.19.3 tsx: 4.20.5
'@git.zone/tstest@2.3.5(@aws-sdk/credential-providers@3.758.0)(socks@2.8.7)(typescript@5.9.2)': '@git.zone/tstest@2.3.5(@aws-sdk/credential-providers@3.758.0)(socks@2.8.7)(typescript@5.9.2)':
dependencies: dependencies:
@@ -8366,34 +8139,6 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.2 hasown: 2.0.2
esbuild@0.25.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.0
'@esbuild/android-arm': 0.25.0
'@esbuild/android-arm64': 0.25.0
'@esbuild/android-x64': 0.25.0
'@esbuild/darwin-arm64': 0.25.0
'@esbuild/darwin-x64': 0.25.0
'@esbuild/freebsd-arm64': 0.25.0
'@esbuild/freebsd-x64': 0.25.0
'@esbuild/linux-arm': 0.25.0
'@esbuild/linux-arm64': 0.25.0
'@esbuild/linux-ia32': 0.25.0
'@esbuild/linux-loong64': 0.25.0
'@esbuild/linux-mips64el': 0.25.0
'@esbuild/linux-ppc64': 0.25.0
'@esbuild/linux-riscv64': 0.25.0
'@esbuild/linux-s390x': 0.25.0
'@esbuild/linux-x64': 0.25.0
'@esbuild/netbsd-arm64': 0.25.0
'@esbuild/netbsd-x64': 0.25.0
'@esbuild/openbsd-arm64': 0.25.0
'@esbuild/openbsd-x64': 0.25.0
'@esbuild/sunos-x64': 0.25.0
'@esbuild/win32-arm64': 0.25.0
'@esbuild/win32-ia32': 0.25.0
'@esbuild/win32-x64': 0.25.0
esbuild@0.25.9: esbuild@0.25.9:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.25.9 '@esbuild/aix-ppc64': 0.25.9
@@ -10753,9 +10498,9 @@ snapshots:
tsscmp@1.0.6: {} tsscmp@1.0.6: {}
tsx@4.19.3: tsx@4.20.5:
dependencies: dependencies:
esbuild: 0.25.0 esbuild: 0.25.9
get-tsconfig: 4.10.0 get-tsconfig: 4.10.0
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3

View File

@@ -64,9 +64,11 @@ tspm restart my-server
### Process Management ### Process Management
#### `tspm start <script> [options]` #### `tspm start <script> [options]`
Start a new process with automatic monitoring and management. Start a new process with automatic monitoring and management.
**Options:** **Options:**
- `--name <name>` - Custom name for the process (default: script name) - `--name <name>` - Custom name for the process (default: script name)
- `--memory <size>` - Memory limit (e.g., "512MB", "2GB", default: 512MB) - `--memory <size>` - Memory limit (e.g., "512MB", "2GB", default: 512MB)
- `--cwd <path>` - Working directory (default: current directory) - `--cwd <path>` - Working directory (default: current directory)
@@ -75,6 +77,7 @@ Start a new process with automatic monitoring and management.
- `--autorestart` - Auto-restart on crash (default: true) - `--autorestart` - Auto-restart on crash (default: true)
**Examples:** **Examples:**
```bash ```bash
# Simple start # Simple start
tspm start server.js tspm start server.js
@@ -90,6 +93,7 @@ tspm start ../other-project/index.js --cwd ../other-project --name other
``` ```
#### `tspm stop <id>` #### `tspm stop <id>`
Gracefully stop a running process (SIGTERM → SIGKILL after timeout). Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
```bash ```bash
@@ -97,6 +101,7 @@ tspm stop my-server
``` ```
#### `tspm restart <id>` #### `tspm restart <id>`
Stop and restart a process with the same configuration. Stop and restart a process with the same configuration.
```bash ```bash
@@ -104,6 +109,7 @@ tspm restart my-server
``` ```
#### `tspm delete <id>` #### `tspm delete <id>`
Stop and remove a process from TSPM management. Stop and remove a process from TSPM management.
```bash ```bash
@@ -113,6 +119,7 @@ tspm delete old-server
### Monitoring & Information ### Monitoring & Information
#### `tspm list` #### `tspm list`
Display all managed processes in a beautiful table. Display all managed processes in a beautiful table.
```bash ```bash
@@ -128,6 +135,7 @@ tspm list
``` ```
#### `tspm describe <id>` #### `tspm describe <id>`
Get detailed information about a specific process. Get detailed information about a specific process.
```bash ```bash
@@ -153,9 +161,11 @@ Watch Paths: src, config
``` ```
#### `tspm logs <id> [options]` #### `tspm logs <id> [options]`
View process logs (stdout and stderr). View process logs (stdout and stderr).
**Options:** **Options:**
- `--lines <n>` - Number of lines to display (default: 50) - `--lines <n>` - Number of lines to display (default: 50)
```bash ```bash
@@ -165,6 +175,7 @@ tspm logs my-server --lines 100
### Batch Operations ### Batch Operations
#### `tspm start-all` #### `tspm start-all`
Start all saved processes at once. Start all saved processes at once.
```bash ```bash
@@ -172,6 +183,7 @@ tspm start-all
``` ```
#### `tspm stop-all` #### `tspm stop-all`
Stop all running processes. Stop all running processes.
```bash ```bash
@@ -179,6 +191,7 @@ tspm stop-all
``` ```
#### `tspm restart-all` #### `tspm restart-all`
Restart all running processes. Restart all running processes.
```bash ```bash
@@ -188,6 +201,7 @@ tspm restart-all
### Daemon Management ### Daemon Management
#### `tspm daemon start` #### `tspm daemon start`
Start the TSPM daemon (happens automatically on first command). Start the TSPM daemon (happens automatically on first command).
```bash ```bash
@@ -195,6 +209,7 @@ tspm daemon start
``` ```
#### `tspm daemon stop` #### `tspm daemon stop`
Stop the TSPM daemon and all managed processes. Stop the TSPM daemon and all managed processes.
```bash ```bash
@@ -202,6 +217,7 @@ tspm daemon stop
``` ```
#### `tspm daemon status` #### `tspm daemon status`
Check daemon health and statistics. Check daemon health and statistics.
```bash ```bash
@@ -245,7 +261,7 @@ const processId = await manager.start({
projectDir: process.cwd(), projectDir: process.cwd(),
memoryLimitBytes: 512 * 1024 * 1024, // 512MB memoryLimitBytes: 512 * 1024 * 1024, // 512MB
autorestart: true, autorestart: true,
watch: false watch: false,
}); });
// Monitor process // Monitor process
@@ -259,18 +275,23 @@ await manager.stop(processId);
## 🔧 Advanced Features ## 🔧 Advanced Features
### Memory Limit Enforcement ### 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. 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 ### 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. Using `ps-tree`, TSPM monitors not just your main process but all child processes it spawns, ensuring complete cleanup on stop/restart.
### Intelligent Logging ### Intelligent Logging
Logs are buffered and managed efficiently, preventing memory issues from excessive output while ensuring you don't lose important information. Logs are buffered and managed efficiently, preventing memory issues from excessive output while ensuring you don't lose important information.
### Graceful Shutdown ### Graceful Shutdown
Processes receive SIGTERM first, allowing them to clean up. After a timeout, SIGKILL ensures termination. Processes receive SIGTERM first, allowing them to clean up. After a timeout, SIGKILL ensures termination.
### Configuration Persistence ### Configuration Persistence
Process configurations are saved, allowing you to restart all processes after a system reboot with a single command. Process configurations are saved, allowing you to restart all processes after a system reboot with a single command.
## 🛠️ Development ## 🛠️ Development
@@ -304,6 +325,7 @@ tspm list
## 📊 Performance ## 📊 Performance
TSPM is designed to be lightweight and efficient: TSPM is designed to be lightweight and efficient:
- Minimal CPU overhead (typically < 0.5%) - Minimal CPU overhead (typically < 0.5%)
- Small memory footprint (~30-50MB for the daemon) - Small memory footprint (~30-50MB for the daemon)
- Fast process startup and shutdown - Fast process startup and shutdown

View File

@@ -1,20 +1,24 @@
# TSPM SmartDaemon Service Management Refactor # TSPM SmartDaemon Service Management Refactor
## Problem ## 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. 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 ## Solution
Refactor to use SmartDaemon for proper systemd service integration. Refactor to use SmartDaemon for proper systemd service integration.
## Implementation Tasks ## Implementation Tasks
### Phase 1: Remove Auto-Spawn Behavior ### Phase 1: Remove Auto-Spawn Behavior
- [x] Remove spawn import from ts/classes.ipcclient.ts - [x] Remove spawn import from ts/classes.ipcclient.ts
- [x] Delete startDaemon() method from IpcClient - [x] Delete startDaemon() method from IpcClient
- [x] Update connect() to throw error when daemon not running - [x] Update connect() to throw error when daemon not running
- [x] Remove auto-reconnect logic from request() method - [x] Remove auto-reconnect logic from request() method
### Phase 2: Create Service Manager ### Phase 2: Create Service Manager
- [x] Create new file ts/classes.servicemanager.ts - [x] Create new file ts/classes.servicemanager.ts
- [x] Implement TspmServiceManager class - [x] Implement TspmServiceManager class
- [x] Add getOrCreateService() method - [x] Add getOrCreateService() method
@@ -23,6 +27,7 @@ Refactor to use SmartDaemon for proper systemd service integration.
- [x] Add getServiceStatus() method - [x] Add getServiceStatus() method
### Phase 3: Update CLI Commands ### Phase 3: Update CLI Commands
- [x] Add 'enable' command to CLI - [x] Add 'enable' command to CLI
- [x] Add 'disable' command to CLI - [x] Add 'disable' command to CLI
- [x] Update 'daemon start' to work without systemd - [x] Update 'daemon start' to work without systemd
@@ -31,11 +36,13 @@ Refactor to use SmartDaemon for proper systemd service integration.
- [x] Add proper error messages with hints - [x] Add proper error messages with hints
### Phase 4: Update Documentation ### Phase 4: Update Documentation
- [x] Update help text in CLI - [x] Update help text in CLI
- [ ] Update command descriptions - [ ] Update command descriptions
- [x] Add service management section - [x] Add service management section
### Phase 5: Testing ### Phase 5: Testing
- [x] Test enable command - [x] Test enable command
- [x] Test disable command - [x] Test disable command
- [x] Test daemon commands - [x] Test daemon commands
@@ -43,6 +50,7 @@ Refactor to use SmartDaemon for proper systemd service integration.
- [x] Build and verify TypeScript compilation - [x] Build and verify TypeScript compilation
## Migration Notes ## Migration Notes
- Users will need to run `tspm enable` once after update - Users will need to run `tspm enable` once after update
- Existing daemon instances will stop working - Existing daemon instances will stop working
- Documentation needs updating to explain new behavior - Documentation needs updating to explain new behavior

23
simple-test.ts Normal file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env tsx
console.log('✓ TypeScript execution works!');
// Test TypeScript features
interface TestData {
message: string;
timestamp: Date;
success: boolean;
}
const data: TestData = {
message: 'TSPM can run .ts files directly with tsx!',
timestamp: new Date(),
success: true
};
console.log('Test data:', data);
console.log('✓ TypeScript types and interfaces work');
console.log('✓ Test complete');
// Exit cleanly
process.exit(0);

28
test-script.ts Normal file
View File

@@ -0,0 +1,28 @@
#!/usr/bin/env tsx
console.log('TypeScript test script started!');
// Test TypeScript features
interface TestData {
message: string;
timestamp: Date;
}
const data: TestData = {
message: 'Hello from TypeScript',
timestamp: new Date()
};
console.log(`Message: ${data.message}`);
console.log(`Time: ${data.timestamp.toISOString()}`);
// Keep the process running for a bit
let counter = 0;
const interval = setInterval(() => {
counter++;
console.log(`Counter: ${counter}`);
if (counter >= 5) {
console.log('Test complete!');
clearInterval(interval);
}
}, 1000);

View File

@@ -97,7 +97,7 @@ tap.test('Daemon uptime calculation', async () => {
const startTime = Date.now(); const startTime = Date.now();
// Wait a bit // Wait a bit
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise((resolve) => setTimeout(resolve, 100));
const uptime = Date.now() - startTime; const uptime = Date.now() - startTime;
expect(uptime).toBeGreaterThanOrEqual(100); expect(uptime).toBeGreaterThanOrEqual(100);

View File

@@ -10,7 +10,7 @@ import { tspmIpcClient } from '../ts/classes.ipcclient.js';
async function ensureDaemonStopped() { async function ensureDaemonStopped() {
try { try {
await tspmIpcClient.stopDaemon(false); await tspmIpcClient.stopDaemon(false);
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) { } catch (error) {
// Ignore errors if daemon is not running // Ignore errors if daemon is not running
} }
@@ -43,7 +43,7 @@ tap.test('Full daemon lifecycle test', async (tools) => {
await tspmIpcClient.connect(); await tspmIpcClient.connect();
// Give daemon time to fully initialize // Give daemon time to fully initialize
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
// Test 3: Check daemon is running // Test 3: Check daemon is running
status = await tspmIpcClient.getDaemonStatus(); status = await tspmIpcClient.getDaemonStatus();
@@ -57,7 +57,7 @@ tap.test('Full daemon lifecycle test', async (tools) => {
await tspmIpcClient.stopDaemon(true); await tspmIpcClient.stopDaemon(true);
// Give daemon time to shutdown // Give daemon time to shutdown
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise((resolve) => setTimeout(resolve, 2000));
// Test 5: Check daemon is stopped // Test 5: Check daemon is stopped
status = await tspmIpcClient.getDaemonStatus(); status = await tspmIpcClient.getDaemonStatus();
@@ -71,7 +71,7 @@ tap.test('Process management through daemon', async (tools) => {
// Ensure daemon is running // Ensure daemon is running
await tspmIpcClient.connect(); await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
// Test 1: List processes (should be empty initially) // Test 1: List processes (should be empty initially)
let listResponse = await tspmIpcClient.request('list', {}); let listResponse = await tspmIpcClient.request('list', {});
@@ -88,7 +88,9 @@ tap.test('Process management through daemon', async (tools) => {
autorestart: false, autorestart: false,
}; };
const startResponse = await tspmIpcClient.request('start', { config: testConfig }); const startResponse = await tspmIpcClient.request('start', {
config: testConfig,
});
expect(startResponse.processId).toEqual('test-echo'); expect(startResponse.processId).toEqual('test-echo');
expect(startResponse.status).toBeDefined(); expect(startResponse.status).toBeDefined();
@@ -96,12 +98,14 @@ tap.test('Process management through daemon', async (tools) => {
listResponse = await tspmIpcClient.request('list', {}); listResponse = await tspmIpcClient.request('list', {});
expect(listResponse.processes.length).toBeGreaterThanOrEqual(1); expect(listResponse.processes.length).toBeGreaterThanOrEqual(1);
const process = listResponse.processes.find(p => p.id === 'test-echo'); const process = listResponse.processes.find((p) => p.id === 'test-echo');
expect(process).toBeDefined(); expect(process).toBeDefined();
expect(process?.id).toEqual('test-echo'); expect(process?.id).toEqual('test-echo');
// Test 4: Describe the process // Test 4: Describe the process
const describeResponse = await tspmIpcClient.request('describe', { id: 'test-echo' }); const describeResponse = await tspmIpcClient.request('describe', {
id: 'test-echo',
});
expect(describeResponse.processInfo).toBeDefined(); expect(describeResponse.processInfo).toBeDefined();
expect(describeResponse.config).toBeDefined(); expect(describeResponse.config).toBeDefined();
expect(describeResponse.config.id).toEqual('test-echo'); expect(describeResponse.config.id).toEqual('test-echo');
@@ -112,12 +116,16 @@ tap.test('Process management through daemon', async (tools) => {
expect(stopResponse.message).toInclude('stopped successfully'); expect(stopResponse.message).toInclude('stopped successfully');
// Test 6: Delete the process // Test 6: Delete the process
const deleteResponse = await tspmIpcClient.request('delete', { id: 'test-echo' }); const deleteResponse = await tspmIpcClient.request('delete', {
id: 'test-echo',
});
expect(deleteResponse.success).toEqual(true); expect(deleteResponse.success).toEqual(true);
// Test 7: Verify process is gone // Test 7: Verify process is gone
listResponse = await tspmIpcClient.request('list', {}); listResponse = await tspmIpcClient.request('list', {});
const deletedProcess = listResponse.processes.find(p => p.id === 'test-echo'); const deletedProcess = listResponse.processes.find(
(p) => p.id === 'test-echo',
);
expect(deletedProcess).toBeUndefined(); expect(deletedProcess).toBeUndefined();
// Cleanup: stop daemon // Cleanup: stop daemon
@@ -131,7 +139,7 @@ tap.test('Batch operations through daemon', async (tools) => {
// Ensure daemon is running // Ensure daemon is running
await tspmIpcClient.connect(); await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
// Add multiple test processes // Add multiple test processes
const testConfigs: tspm.IProcessConfig[] = [ const testConfigs: tspm.IProcessConfig[] = [
@@ -187,7 +195,7 @@ tap.test('Daemon error handling', async (tools) => {
// Ensure daemon is running // Ensure daemon is running
await tspmIpcClient.connect(); await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
// Test 1: Try to stop non-existent process // Test 1: Try to stop non-existent process
try { try {
@@ -224,7 +232,7 @@ tap.test('Daemon heartbeat functionality', async (tools) => {
// Ensure daemon is running // Ensure daemon is running
await tspmIpcClient.connect(); await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
// Test heartbeat // Test heartbeat
const heartbeatResponse = await tspmIpcClient.request('heartbeat', {}); const heartbeatResponse = await tspmIpcClient.request('heartbeat', {});
@@ -242,7 +250,7 @@ tap.test('Daemon memory and CPU reporting', async (tools) => {
// Ensure daemon is running // Ensure daemon is running
await tspmIpcClient.connect(); await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
// Get daemon status // Get daemon status
const status = await tspmIpcClient.getDaemonStatus(); const status = await tspmIpcClient.getDaemonStatus();

View File

@@ -61,7 +61,10 @@ tap.test('IPC client daemon running check - stale PID', async () => {
expect(isRunning).toEqual(false); expect(isRunning).toEqual(false);
// Clean up - the stale PID should be removed // Clean up - the stale PID should be removed
const fileExists = await fs.access(pidFile).then(() => true).catch(() => false); const fileExists = await fs
.access(pidFile)
.then(() => true)
.catch(() => false);
expect(fileExists).toEqual(false); expect(fileExists).toEqual(false);
}); });
@@ -95,7 +98,9 @@ tap.test('IPC client singleton instance', async () => {
expect(tspmIpcClient).toBeInstanceOf(TspmIpcClient); expect(tspmIpcClient).toBeInstanceOf(TspmIpcClient);
// Test that it's the same instance // Test that it's the same instance
const { tspmIpcClient: secondImport } = await import('../ts/classes.ipcclient.js'); const { tspmIpcClient: secondImport } = await import(
'../ts/classes.ipcclient.js'
);
expect(tspmIpcClient).toBe(secondImport); expect(tspmIpcClient).toBe(secondImport);
}); });
@@ -111,7 +116,8 @@ tap.test('IPC client request method type safety', async () => {
}); });
tap.test('IPC client error message formatting', async () => { tap.test('IPC client error message formatting', async () => {
const errorMessage = 'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.'; const errorMessage =
'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.';
expect(errorMessage).toInclude('tspm daemon start'); expect(errorMessage).toInclude('tspm daemon start');
}); });

View File

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

View File

@@ -34,6 +34,10 @@ export class TspmDaemon {
public async start(): Promise<void> { public async start(): Promise<void> {
console.log('Starting TSPM daemon...'); console.log('Starting TSPM daemon...');
// Ensure the TSPM directory exists
const fs = await import('fs/promises');
await fs.mkdir(paths.tspmDir, { recursive: true });
// Check if another daemon is already running // Check if another daemon is already running
if (await this.isDaemonRunning()) { if (await this.isDaemonRunning()) {
throw new Error('Another TSPM daemon instance is already running'); throw new Error('Another TSPM daemon instance is already running');
@@ -43,12 +47,12 @@ export class TspmDaemon {
this.ipcServer = plugins.smartipc.SmartIpc.createServer({ this.ipcServer = plugins.smartipc.SmartIpc.createServer({
id: 'tspm-daemon', id: 'tspm-daemon',
socketPath: this.socketPath, socketPath: this.socketPath,
autoCleanupSocketFile: true, // Clean up stale sockets autoCleanupSocketFile: true, // Clean up stale sockets
socketMode: 0o600, // Set proper permissions socketMode: 0o600, // Set proper permissions
heartbeat: true, heartbeat: true,
heartbeatInterval: 5000, heartbeatInterval: 5000,
heartbeatTimeout: 20000, heartbeatTimeout: 20000,
heartbeatInitialGracePeriodMs: 10000 // Grace period for startup heartbeatInitialGracePeriodMs: 10000, // Grace period for startup
}); });
// Register message handlers // Register message handlers
@@ -122,19 +126,22 @@ export class TspmDaemon {
}, },
); );
this.ipcServer.onMessage('restart', async (request: RequestForMethod<'restart'>) => { this.ipcServer.onMessage(
try { 'restart',
await this.tspmInstance.restart(request.id); async (request: RequestForMethod<'restart'>) => {
const processInfo = this.tspmInstance.processInfo.get(request.id); try {
return { await this.tspmInstance.restart(request.id);
processId: request.id, const processInfo = this.tspmInstance.processInfo.get(request.id);
pid: processInfo?.pid, return {
status: processInfo?.status || 'stopped', processId: request.id,
}; pid: processInfo?.pid,
} catch (error) { status: processInfo?.status || 'stopped',
throw new Error(`Failed to restart process: ${error.message}`); };
} } catch (error) {
}); throw new Error(`Failed to restart process: ${error.message}`);
}
},
);
this.ipcServer.onMessage( this.ipcServer.onMessage(
'delete', 'delete',
@@ -160,124 +167,148 @@ export class TspmDaemon {
}, },
); );
this.ipcServer.onMessage('describe', async (request: RequestForMethod<'describe'>) => { this.ipcServer.onMessage(
const processInfo = await this.tspmInstance.describe(request.id); 'describe',
const config = this.tspmInstance.processConfigs.get(request.id); async (request: RequestForMethod<'describe'>) => {
const processInfo = await this.tspmInstance.describe(request.id);
const config = this.tspmInstance.processConfigs.get(request.id);
if (!processInfo || !config) { if (!processInfo || !config) {
throw new Error(`Process ${request.id} not found`); throw new Error(`Process ${request.id} not found`);
} }
return { return {
processInfo, processInfo,
config, config,
}; };
}); },
);
this.ipcServer.onMessage('getLogs', async (request: RequestForMethod<'getLogs'>) => { this.ipcServer.onMessage(
const logs = await this.tspmInstance.getLogs(request.id); 'getLogs',
return { logs }; async (request: RequestForMethod<'getLogs'>) => {
}); const logs = await this.tspmInstance.getLogs(request.id);
return { logs };
},
);
// Batch operations handlers // Batch operations handlers
this.ipcServer.onMessage('startAll', async (request: RequestForMethod<'startAll'>) => { this.ipcServer.onMessage(
const started: string[] = []; 'startAll',
const failed: Array<{ id: string; error: string }> = []; async (request: RequestForMethod<'startAll'>) => {
const started: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
await this.tspmInstance.startAll(); await this.tspmInstance.startAll();
// Get status of all processes // Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) { for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'online') { if (processInfo.status === 'online') {
started.push(id); started.push(id);
} else { } else {
failed.push({ id, error: 'Failed to start' }); failed.push({ id, error: 'Failed to start' });
}
} }
}
return { started, failed }; return { started, failed };
}); },
);
this.ipcServer.onMessage('stopAll', async (request: RequestForMethod<'stopAll'>) => { this.ipcServer.onMessage(
const stopped: string[] = []; 'stopAll',
const failed: Array<{ id: string; error: string }> = []; async (request: RequestForMethod<'stopAll'>) => {
const stopped: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
await this.tspmInstance.stopAll(); await this.tspmInstance.stopAll();
// Get status of all processes // Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) { for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'stopped') { if (processInfo.status === 'stopped') {
stopped.push(id); stopped.push(id);
} else { } else {
failed.push({ id, error: 'Failed to stop' }); failed.push({ id, error: 'Failed to stop' });
}
} }
}
return { stopped, failed }; return { stopped, failed };
}); },
);
this.ipcServer.onMessage('restartAll', async (request: RequestForMethod<'restartAll'>) => { this.ipcServer.onMessage(
const restarted: string[] = []; 'restartAll',
const failed: Array<{ id: string; error: string }> = []; async (request: RequestForMethod<'restartAll'>) => {
const restarted: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
await this.tspmInstance.restartAll(); await this.tspmInstance.restartAll();
// Get status of all processes // Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) { for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'online') { if (processInfo.status === 'online') {
restarted.push(id); restarted.push(id);
} else { } else {
failed.push({ id, error: 'Failed to restart' }); failed.push({ id, error: 'Failed to restart' });
}
} }
}
return { restarted, failed }; return { restarted, failed };
}); },
);
// Daemon management handlers // Daemon management handlers
this.ipcServer.onMessage('daemon:status', async (request: RequestForMethod<'daemon:status'>) => { this.ipcServer.onMessage(
const memUsage = process.memoryUsage(); 'daemon:status',
return { async (request: RequestForMethod<'daemon:status'>) => {
status: 'running', const memUsage = process.memoryUsage();
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 { return {
success: false, status: 'running',
message: 'Daemon is already shutting down', pid: process.pid,
uptime: Date.now() - this.startTime,
processCount: this.tspmInstance.processes.size,
memoryUsage: memUsage.heapUsed,
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
}; };
} },
);
// Schedule shutdown this.ipcServer.onMessage(
const graceful = request.graceful !== false; 'daemon:shutdown',
const timeout = request.timeout || 10000; async (request: RequestForMethod<'daemon:shutdown'>) => {
if (this.isShuttingDown) {
return {
success: false,
message: 'Daemon is already shutting down',
};
}
if (graceful) { // Schedule shutdown
setTimeout(() => this.shutdown(true), 100); const graceful = request.graceful !== false;
} else { const timeout = request.timeout || 10000;
setTimeout(() => this.shutdown(false), 100);
}
return { if (graceful) {
success: true, setTimeout(() => this.shutdown(true), 100);
message: `Daemon will shutdown ${graceful ? 'gracefully' : 'immediately'} in ${timeout}ms`, } else {
}; setTimeout(() => this.shutdown(false), 100);
}); }
return {
success: true,
message: `Daemon will shutdown ${graceful ? 'gracefully' : 'immediately'} in ${timeout}ms`,
};
},
);
// Heartbeat handler // Heartbeat handler
this.ipcServer.onMessage('heartbeat', async (request: RequestForMethod<'heartbeat'>) => { this.ipcServer.onMessage(
return { 'heartbeat',
timestamp: Date.now(), async (request: RequestForMethod<'heartbeat'>) => {
status: this.isShuttingDown ? 'degraded' : 'healthy', return {
}; timestamp: Date.now(),
}); status: this.isShuttingDown ? 'degraded' : 'healthy',
};
},
);
} }
/** /**

View File

@@ -36,9 +36,9 @@ export class TspmIpcClient {
if (!daemonRunning) { if (!daemonRunning) {
throw new Error( throw new Error(
'TSPM daemon is not running.\n\n' + 'TSPM daemon is not running.\n\n' +
'To start the daemon, run one of:\n' + 'To start the daemon, run one of:\n' +
' tspm daemon start - Start daemon for this session\n' + ' tspm daemon start - Start daemon for this session\n' +
' tspm enable - Enable daemon as system service (recommended)\n' ' tspm enable - Enable daemon as system service (recommended)\n',
); );
} }
@@ -59,7 +59,7 @@ export class TspmIpcClient {
heartbeatInterval: 5000, heartbeatInterval: 5000,
heartbeatTimeout: 20000, heartbeatTimeout: 20000,
heartbeatInitialGracePeriodMs: 10000, heartbeatInitialGracePeriodMs: 10000,
heartbeatThrowOnTimeout: false // Don't throw, emit events instead heartbeatThrowOnTimeout: false, // Don't throw, emit events instead
}); });
// Connect to the daemon // Connect to the daemon
@@ -101,10 +101,8 @@ export class TspmIpcClient {
params: RequestForMethod<M>, params: RequestForMethod<M>,
): Promise<ResponseForMethod<M>> { ): Promise<ResponseForMethod<M>> {
if (!this.isConnected || !this.ipcClient) { if (!this.isConnected || !this.ipcClient) {
throw new Error( // Try to connect first
'Not connected to TSPM daemon.\n' + await this.connect();
'Run "tspm daemon start" or "tspm enable" first.'
);
} }
try { try {
@@ -123,7 +121,10 @@ export class TspmIpcClient {
/** /**
* Subscribe to log updates for a specific process * Subscribe to log updates for a specific process
*/ */
public async subscribe(processId: string, handler: (log: any) => void): Promise<void> { public async subscribe(
processId: string,
handler: (log: any) => void,
): Promise<void> {
if (!this.ipcClient || !this.isConnected) { if (!this.ipcClient || !this.isConnected) {
throw new Error('Not connected to daemon'); throw new Error('Not connected to daemon');
} }
@@ -163,14 +164,15 @@ export class TspmIpcClient {
try { try {
process.kill(pid, 0); process.kill(pid, 0);
// Also check if socket exists and is accessible // PID is alive, daemon is running
// Socket check is advisory only - the connect retry will handle transient socket issues
try { try {
await fs.promises.access(this.socketPath); await fs.promises.access(this.socketPath);
return true;
} catch { } catch {
// Socket doesn't exist, daemon might be starting // Socket might be missing temporarily, but daemon is alive
return false; // Let the connection retry logic handle this
} }
return true;
} catch { } catch {
// Process doesn't exist, clean up stale PID file // Process doesn't exist, clean up stale PID file
await fs.promises.unlink(this.daemonPidFile).catch(() => {}); await fs.promises.unlink(this.daemonPidFile).catch(() => {});
@@ -185,8 +187,6 @@ export class TspmIpcClient {
} }
} }
/** /**
* Stop the daemon * Stop the daemon
*/ */

View File

@@ -25,7 +25,7 @@ export class TspmServiceManager {
description: 'TSPM Process Manager Daemon', description: 'TSPM Process Manager Daemon',
command: `${process.execPath} ${cliPath} daemon start-service`, command: `${process.execPath} ${cliPath} daemon start-service`,
workingDir: process.env.HOME || process.cwd(), workingDir: process.env.HOME || process.cwd(),
version: '1.0.0' version: '1.0.0',
}); });
} }
return this.service; return this.service;
@@ -82,13 +82,13 @@ export class TspmServiceManager {
return { return {
enabled: true, // Would need to check systemctl is-enabled enabled: true, // Would need to check systemctl is-enabled
running: true, // Would need to check systemctl is-active running: true, // Would need to check systemctl is-active
status: 'active' status: 'active',
}; };
} catch (error) { } catch (error) {
return { return {
enabled: false, enabled: false,
running: false, running: false,
status: 'inactive' status: 'inactive',
}; };
} }
} }

View File

@@ -32,8 +32,6 @@ export interface IProcessInfo {
restarts: number; restarts: number;
} }
export class Tspm extends EventEmitter { export class Tspm extends EventEmitter {
public processes: Map<string, ProcessMonitor> = new Map(); public processes: Map<string, ProcessMonitor> = new Map();
public processConfigs: Map<string, IProcessConfig> = new Map(); public processConfigs: Map<string, IProcessConfig> = new Map();

799
ts/cli.ts
View File

@@ -1,797 +1,2 @@
import * as plugins from './plugins.js'; // Re-export from the new modular CLI structure
import * as paths from './paths.js'; export * from './cli/index.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();
};

View File

@@ -0,0 +1,31 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerRestartAllCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'restart-all',
async (_argvArg: CliArguments) => {
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}`);
}
process.exitCode = 1; // Signal partial failure
}
},
{ actionLabel: 'restart all processes' },
);
}

View File

@@ -0,0 +1,31 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStartAllCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'start-all',
async (_argvArg: CliArguments) => {
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}`);
}
process.exitCode = 1; // Signal partial failure
}
},
{ actionLabel: 'start all processes' },
);
}

View File

@@ -0,0 +1,31 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStopAllCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'stop-all',
async (_argvArg: CliArguments) => {
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}`);
}
process.exitCode = 1; // Signal partial failure
}
},
{ actionLabel: 'stop all processes' },
);
}

View File

@@ -0,0 +1,148 @@
import * as plugins from '../../../plugins.js';
import * as paths from '../../../paths.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import { Logger } from '../../../utils.errorhandler.js';
import type { CliArguments } from '../../types.js';
import { formatMemory } from '../../helpers/memory.js';
export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
const cliLogger = new Logger('CLI');
smartcli.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 detached background process
// Use 'inherit' for stdio to see any startup errors when debugging
const daemonProcess = spawn(process.execPath, [daemonScript], {
detached: true,
stdio: process.env.TSPM_DEBUG === 'true' ? 'inherit' : 'ignore',
env: {
...process.env,
TSPM_DAEMON_MODE: 'true',
},
});
// Detach the daemon so it continues running after CLI exits
daemonProcess.unref();
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.');
} else {
console.warn('\n⚠ Warning: Daemon process started but is not responding.');
console.log('The daemon may have crashed on startup.');
console.log('\nTo debug, try:');
console.log(' TSPM_DEBUG=true tspm daemon start');
console.log('\nOr check if the socket file exists:');
console.log(` ls -la ~/.tspm/tspm.sock`);
}
// Disconnect from the daemon after starting
await tspmIpcClient.disconnect();
} 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');
// Disconnect from the daemon after stopping
await tspmIpcClient.disconnect();
} 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: () => {},
});
}

102
ts/cli/commands/default.ts Normal file
View File

@@ -0,0 +1,102 @@
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { tspmIpcClient } from '../../classes.ipcclient.js';
import { Logger } from '../../utils.errorhandler.js';
import type { CliArguments } from '../types.js';
import { pad } from '../helpers/formatting.js';
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) => {
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(
'└─────────┴─────────────┴───────────┴───────────┴──────────┘',
);
}
// Disconnect from daemon after getting list
await tspmIpcClient.disconnect();
} 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: () => {},
});
}

View File

@@ -0,0 +1,29 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'delete',
async (argvArg: CliArguments) => {
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}`);
}
},
{ actionLabel: 'delete process' },
);
}

View File

@@ -0,0 +1,49 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
import { formatMemory } from '../../helpers/memory.js';
export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'describe',
async (argvArg: CliArguments) => {
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(', ')}`);
}
}
},
{ actionLabel: 'describe process' },
);
}

View File

@@ -0,0 +1,52 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
import { pad } from '../../helpers/formatting.js';
import { formatMemory } from '../../helpers/memory.js';
export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'list',
async (_argvArg: CliArguments) => {
const response = await tspmIpcClient.request('list', {});
const processes = response.processes;
if (processes.length === 0) {
console.log('No processes running.');
return;
}
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(
'└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘',
);
},
{ actionLabel: 'list processes' },
);
}

View File

@@ -0,0 +1,99 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
import { getBool, getNumber } from '../../helpers/argv.js';
import { formatLog } from '../../helpers/formatting.js';
import { withStreamingLifecycle } from '../../helpers/lifecycle.js';
export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'logs',
async (argvArg: CliArguments) => {
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 = getNumber(argvArg, 'lines', 50);
const follow = getBool(argvArg, 'follow', 'f');
const response = await tspmIpcClient.request('getLogs', { id, lines });
if (!follow) {
// One-shot mode - auto-disconnect handled by registerIpcCommand
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}`);
}
return;
}
// Streaming mode
console.log(`Logs for process: ${id} (streaming...)`);
console.log('─'.repeat(60));
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);
}
await withStreamingLifecycle(
async () => {
await tspmIpcClient.subscribe(id, (log: any) => {
if (log.seq !== undefined && log.seq <= lastSeq) return;
if (log.seq !== undefined && log.seq > lastSeq + 1) {
console.log(
`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`,
);
}
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;
});
},
async () => {
console.log('\n\nStopping log stream...');
try {
await tspmIpcClient.unsubscribe(id);
} catch {}
try {
await tspmIpcClient.disconnect();
} catch {}
},
);
},
{
actionLabel: 'get logs',
keepAlive: (argv) => getBool(argv, 'follow', 'f'),
},
);
}

View File

@@ -0,0 +1,28 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'restart',
async (argvArg: CliArguments) => {
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}`);
},
{ actionLabel: 'restart process' },
);
}

View File

@@ -0,0 +1,102 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { IProcessConfig } from '../../../classes.tspm.js';
import type { CliArguments } from '../../types.js';
import { parseMemoryString, formatMemory } from '../../helpers/memory.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'start',
async (argvArg: CliArguments) => {
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;
const projectDir = argvArg.cwd || process.cwd();
// Direct .ts support via tsx (bundled with TSPM)
let actualCommand = script;
let commandArgs: string[] | undefined = undefined;
if (script.endsWith('.ts')) {
try {
const tsxPath = await (async () => {
const { createRequire } = await import('module');
const require = createRequire(import.meta.url);
return require.resolve('tsx/dist/cli.mjs');
})();
const scriptPath = plugins.path.isAbsolute(script)
? script
: plugins.path.join(projectDir, script);
actualCommand = tsxPath;
commandArgs = [scriptPath];
} catch {
actualCommand = 'tsx';
commandArgs = [script];
}
}
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: actualCommand,
args: commandArgs,
projectDir,
memoryLimitBytes: memoryLimit,
autorestart,
watch,
watchPaths,
};
console.log(`Starting process: ${name}`);
console.log(
` Command: ${script}${script.endsWith('.ts') ? ' (via tsx)' : ''}`,
);
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}`);
},
{ actionLabel: 'start process' },
);
}

View File

@@ -0,0 +1,29 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStopCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'stop',
async (argvArg: CliArguments) => {
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}`);
}
},
{ actionLabel: 'stop process' },
);
}

View File

@@ -0,0 +1,36 @@
import * as plugins from '../../../plugins.js';
import { TspmServiceManager } from '../../../classes.servicemanager.js';
import { Logger } from '../../../utils.errorhandler.js';
import type { CliArguments } from '../../types.js';
export function registerDisableCommand(smartcli: plugins.smartcli.Smartcli) {
const cliLogger = new Logger('CLI');
smartcli.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: () => {},
});
}

View File

@@ -0,0 +1,36 @@
import * as plugins from '../../../plugins.js';
import { TspmServiceManager } from '../../../classes.servicemanager.js';
import { Logger } from '../../../utils.errorhandler.js';
import type { CliArguments } from '../../types.js';
export function registerEnableCommand(smartcli: plugins.smartcli.Smartcli) {
const cliLogger = new Logger('CLI');
smartcli.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: () => {},
});
}

24
ts/cli/helpers/argv.ts Normal file
View File

@@ -0,0 +1,24 @@
import type { CliArguments } from '../types.js';
// Argument parsing helpers
export const getBool = (argv: CliArguments, ...keys: string[]) =>
keys.some((k) => Boolean((argv as any)[k]));
export const getNumber = (
argv: CliArguments,
key: string,
fallback: number,
) => {
const v = (argv as any)[key];
const n = typeof v === 'string' ? Number(v) : v;
return Number.isFinite(n) ? n : fallback;
};
export const getString = (
argv: CliArguments,
key: string,
fallback?: string,
) => {
const v = (argv as any)[key];
return typeof v === 'string' ? v : fallback;
};

18
ts/cli/helpers/errors.ts Normal file
View File

@@ -0,0 +1,18 @@
// Helper function to handle daemon connection errors
export 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);
}

View File

@@ -0,0 +1,18 @@
// Helper function for padding strings
export function pad(str: string, length: number): string {
return str.length > length
? str.substring(0, length - 3) + '...'
: str.padEnd(length);
}
// Helper for unknown errors
export const unknownError = (err: any) =>
err?.message && typeof err.message === 'string' ? err.message : String(err);
// Helper function to format log entries
export function formatLog(log: any): string {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix =
log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
return `${timestamp} ${prefix} ${log.message}`;
}

View File

@@ -0,0 +1,22 @@
// Streaming lifecycle helper
export function withStreamingLifecycle(
setup: () => Promise<void>,
teardown: () => Promise<void>,
) {
let isCleaningUp = false;
const cleanup = async () => {
if (isCleaningUp) return;
isCleaningUp = true;
try {
await teardown();
} finally {
process.exit(0);
}
};
process.once('SIGINT', cleanup);
process.once('SIGTERM', cleanup);
return (async () => {
await setup();
await new Promise(() => {}); // keep alive
})();
}

33
ts/cli/helpers/memory.ts Normal file
View File

@@ -0,0 +1,33 @@
// Helper function to parse memory strings (e.g., "512MB", "2GB")
export 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
export 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`;
}
}

68
ts/cli/index.ts Normal file
View File

@@ -0,0 +1,68 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { Logger, LogLevel } from '../utils.errorhandler.js';
// Import command registration functions
import { registerDefaultCommand } from './commands/default.js';
import { registerStartCommand } from './commands/process/start.js';
import { registerStopCommand } from './commands/process/stop.js';
import { registerRestartCommand } from './commands/process/restart.js';
import { registerDeleteCommand } from './commands/process/delete.js';
import { registerListCommand } from './commands/process/list.js';
import { registerDescribeCommand } from './commands/process/describe.js';
import { registerLogsCommand } from './commands/process/logs.js';
import { registerStartAllCommand } from './commands/batch/start-all.js';
import { registerStopAllCommand } from './commands/batch/stop-all.js';
import { registerRestartAllCommand } from './commands/batch/restart-all.js';
import { registerDaemonCommand } from './commands/daemon/index.js';
import { registerEnableCommand } from './commands/service/enable.js';
import { registerDisableCommand } from './commands/service/disable.js';
// Export types for external use
export type { CliArguments } from './types.js';
/**
* Main CLI entry point
*/
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);
// Register all commands
// Default command (help + list)
registerDefaultCommand(smartcliInstance);
// Process commands
registerStartCommand(smartcliInstance);
registerStopCommand(smartcliInstance);
registerRestartCommand(smartcliInstance);
registerDeleteCommand(smartcliInstance);
registerListCommand(smartcliInstance);
registerDescribeCommand(smartcliInstance);
registerLogsCommand(smartcliInstance);
// Batch commands
registerStartAllCommand(smartcliInstance);
registerStopAllCommand(smartcliInstance);
registerRestartAllCommand(smartcliInstance);
// Daemon commands
registerDaemonCommand(smartcliInstance);
// Service commands
registerEnableCommand(smartcliInstance);
registerDisableCommand(smartcliInstance);
// Start parsing commands
smartcliInstance.startParse();
};

View File

@@ -0,0 +1,26 @@
import { tspmIpcClient } from '../../classes.ipcclient.js';
/**
* Preflight the daemon if required. Uses getDaemonStatus() which is safe and cheap:
* it only connects if the PID file is valid.
*/
export async function ensureDaemonOrHint(
requireDaemon: boolean | undefined,
actionLabel?: string,
): Promise<boolean> {
if (requireDaemon === false) return true; // command does not require daemon
const status = await tspmIpcClient.getDaemonStatus();
if (!status) {
// Same hint as handleDaemonError, but early and consistent
console.error(
`Error: Cannot ${actionLabel || 'perform action'} - TSPM daemon is not running.`,
);
console.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)',
);
return false;
}
return true;
}

View File

@@ -0,0 +1,98 @@
import * as plugins from '../../plugins.js';
import type {
CliArguments,
CommandAction,
IpcCommandOptions,
} from '../types.js';
import { handleDaemonError } from '../helpers/errors.js';
import { unknownError } from '../helpers/formatting.js';
import { runIpcCommand } from '../utils/ipc.js';
import { ensureDaemonOrHint } from './daemon-check.js';
/**
* Add an IPC-based CLI command with:
* - optional daemon preflight
* - standard error handling
* - automatic disconnect via runIpcCommand unless keepAlive is true
*/
export function registerIpcCommand(
smartcli: plugins.smartcli.Smartcli,
name: string,
action: CommandAction,
opts: IpcCommandOptions = {},
) {
const { actionLabel = name, keepAlive = false, requireDaemon = true } = opts;
smartcli.addCommand(name).subscribe({
next: async (argv: CliArguments) => {
// Early preflight for better UX
const ok = await ensureDaemonOrHint(requireDaemon, actionLabel);
if (!ok) {
process.exit(1);
return;
}
// Evaluate keepAlive - can be boolean or function
const shouldKeepAlive =
typeof keepAlive === 'function' ? keepAlive(argv) : keepAlive;
if (shouldKeepAlive) {
// Let action manage its own connection/cleanup lifecycle
try {
await action(argv);
} catch (error) {
handleDaemonError(error, actionLabel);
}
} else {
// Auto-disconnect pattern for one-shot IPC commands
await runIpcCommand(async () => {
try {
await action(argv);
} catch (error) {
handleDaemonError(error, actionLabel);
}
});
}
},
error: (err) => {
// Fallback error path (should be rare with try/catch in next)
console.error(
`Unexpected error in command "${name}":`,
unknownError(err),
);
process.exit(1);
},
complete: () => {},
});
}
/**
* Register local commands that don't require IPC/daemon connection
* Used for daemon lifecycle, service management, and other local operations
*/
export function registerLocalCommand(
smartcli: plugins.smartcli.Smartcli,
name: string,
action: (argv: CliArguments) => Promise<void>,
opts: { actionLabel?: string } = {},
) {
const { actionLabel = name } = opts;
smartcli.addCommand(name).subscribe({
next: async (argv: CliArguments) => {
try {
await action(argv);
} catch (error: any) {
console.error(`Error ${actionLabel}:`, error?.message || String(error));
process.exit(1);
}
},
error: (err) => {
console.error(
`Unexpected error in command "${name}":`,
unknownError(err),
);
process.exit(1);
},
complete: () => {},
});
}

20
ts/cli/types.ts Normal file
View File

@@ -0,0 +1,20 @@
export interface CliArguments {
verbose?: boolean;
watch?: boolean;
memory?: string;
cwd?: string;
daemon?: boolean;
test?: boolean;
name?: string;
autorestart?: boolean;
watchPaths?: string[];
[key: string]: any;
}
export type CommandAction = (argv: CliArguments) => Promise<void>;
export interface IpcCommandOptions {
actionLabel?: string; // used in error message, e.g. "start process"
keepAlive?: boolean | ((argv: CliArguments) => boolean); // true for streaming commands (don't auto-disconnect), or function to determine at runtime
requireDaemon?: boolean; // default true for IPC-bound commands
}

14
ts/cli/utils/ipc.ts Normal file
View File

@@ -0,0 +1,14 @@
import { tspmIpcClient } from '../../classes.ipcclient.js';
// Helper function to run IPC commands with automatic disconnect
export async function runIpcCommand<T>(body: () => Promise<T>): Promise<T> {
try {
return await body();
} finally {
try {
await tspmIpcClient.disconnect();
} catch {
// Ignore disconnect errors
}
}
}

View File

@@ -1,7 +1,4 @@
import type { import type { IProcessConfig, IProcessInfo } from './classes.tspm.js';
IProcessConfig,
IProcessInfo,
} from './classes.tspm.js';
import type { IProcessLog } from './classes.processwrapper.js'; import type { IProcessLog } from './classes.processwrapper.js';
// Base message types // Base message types