Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
6141b26530 | |||
e73f4acd63 | |||
8e3cfb624b | |||
33fb02733d | |||
1c2310c185 | |||
d33a001edc |
28
changelog.md
28
changelog.md
@@ -1,6 +1,31 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 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)
|
||||||
|
|
||||||
|
Refactor daemon lifecycle and service management: remove IPC auto-spawn, add TspmServiceManager and CLI enable/disable
|
||||||
|
|
||||||
|
- Do not auto-spawn the daemon from the IPC client anymore — attempts to connect will now error with instructions to start the daemon manually or enable the system service (breaking change).
|
||||||
|
- Add TspmServiceManager to manage the daemon as a systemd service via smartdaemon (enable/disable/reload/status helpers).
|
||||||
|
- CLI: add 'enable' and 'disable' commands to install/uninstall the daemon as a system service and add 'daemon start-service' entrypoint used by systemd.
|
||||||
|
- CLI: improve error handling and user hints when the daemon is not running (suggests `tspm daemon start` or `tspm enable`).
|
||||||
|
- IPC client: removed startDaemon() and related auto-reconnect/start logic; request() no longer auto-reconnects or implicitly start the daemon.
|
||||||
|
- Export TspmServiceManager from the package index so service management is part of the public API.
|
||||||
|
- Updated development plan/readme (readme.plan.md) to reflect the refactor toward proper SmartDaemon integration and migration notes.
|
||||||
|
|
||||||
## 2025-08-26 - 1.8.0 - feat(daemon)
|
## 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
|
||||||
@@ -13,6 +38,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
|
||||||
@@ -21,6 +47,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
|
||||||
@@ -29,6 +56,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.
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tspm",
|
"name": "@git.zone/tspm",
|
||||||
"version": "1.8.0",
|
"version": "3.0.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a no fuzz process manager",
|
"description": "a no fuzz process manager",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -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
271
pnpm-lock.yaml
generated
@@ -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
|
||||||
|
24
readme.md
24
readme.md
@@ -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
|
||||||
|
@@ -1,56 +1,56 @@
|
|||||||
# TSPM Real-Time Log Streaming Implementation Plan
|
# TSPM SmartDaemon Service Management Refactor
|
||||||
|
|
||||||
## Overview
|
## Problem
|
||||||
Implementing real-time log streaming (tailing) functionality for TSPM using SmartIPC's pub/sub capabilities.
|
|
||||||
|
|
||||||
## Approach: Hybrid Request + Subscribe
|
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.
|
||||||
1. Initial getLogs request to fetch historical logs up to current point
|
|
||||||
2. Subscribe to pub/sub channel for real-time updates
|
## Solution
|
||||||
3. Use sequence numbers to detect and handle gaps/duplicates
|
|
||||||
4. Per-process topics for granular subscriptions
|
Refactor to use SmartDaemon for proper systemd service integration.
|
||||||
|
|
||||||
## Implementation Tasks
|
## Implementation Tasks
|
||||||
|
|
||||||
### Core Changes
|
### Phase 1: Remove Auto-Spawn Behavior
|
||||||
- [x] Update IProcessLog interface with seq and runId fields
|
|
||||||
- [x] Add nextSeq and runId fields to ProcessWrapper class
|
|
||||||
- [x] Update addLog() methods to include sequencing
|
|
||||||
- [x] Implement pub/sub publishing in daemon
|
|
||||||
|
|
||||||
### IPC Client Updates
|
- [x] Remove spawn import from ts/classes.ipcclient.ts
|
||||||
- [x] Add subscribe/unsubscribe methods to TspmIpcClient
|
- [x] Delete startDaemon() method from IpcClient
|
||||||
- [ ] Implement log streaming handler
|
- [x] Update connect() to throw error when daemon not running
|
||||||
- [ ] Add connection state management for subscriptions
|
- [x] Remove auto-reconnect logic from request() method
|
||||||
|
|
||||||
### CLI Enhancement
|
### Phase 2: Create Service Manager
|
||||||
- [x] Add --follow flag to logs command
|
|
||||||
- [x] Implement streaming output with proper formatting
|
|
||||||
- [x] Handle Ctrl+C gracefully to unsubscribe
|
|
||||||
|
|
||||||
### Reliability Features
|
- [x] Create new file ts/classes.servicemanager.ts
|
||||||
- [x] Add backpressure handling (drop oldest when buffer full)
|
- [x] Implement TspmServiceManager class
|
||||||
- [x] Implement gap detection and recovery
|
- [x] Add getOrCreateService() method
|
||||||
- [x] Add process restart detection via runId
|
- [x] Add enableService() method
|
||||||
|
- [x] Add disableService() method
|
||||||
|
- [x] Add getServiceStatus() method
|
||||||
|
|
||||||
### Testing
|
### Phase 3: Update CLI Commands
|
||||||
- [x] Test basic log streaming
|
|
||||||
- [x] Test gap recovery
|
|
||||||
- [x] Test high-volume logging scenarios
|
|
||||||
- [x] Test process restart handling
|
|
||||||
|
|
||||||
## Technical Details
|
- [x] Add 'enable' command to CLI
|
||||||
|
- [x] Add 'disable' command to CLI
|
||||||
|
- [x] Update 'daemon start' to work without systemd
|
||||||
|
- [x] Add 'daemon start-service' internal command for systemd
|
||||||
|
- [x] Update all commands to handle missing daemon gracefully
|
||||||
|
- [x] Add proper error messages with hints
|
||||||
|
|
||||||
### Sequence Numbering
|
### Phase 4: Update Documentation
|
||||||
- Each log entry gets incrementing seq number per process
|
|
||||||
- runId changes on process restart
|
|
||||||
- Client tracks lastSeq to detect gaps
|
|
||||||
|
|
||||||
### Topic Structure
|
- [x] Update help text in CLI
|
||||||
- Format: `logs.<processId>`
|
- [ ] Update command descriptions
|
||||||
- Daemon publishes to topic on new log entries
|
- [x] Add service management section
|
||||||
- Clients subscribe to specific process topics
|
|
||||||
|
|
||||||
### Backpressure Strategy
|
### Phase 5: Testing
|
||||||
- Circular buffer of 10,000 entries per process
|
|
||||||
- Drop oldest entries when buffer full
|
- [x] Test enable command
|
||||||
- Client can detect gaps via sequence numbers
|
- [x] Test disable command
|
||||||
|
- [x] Test daemon commands
|
||||||
|
- [x] Test error handling when daemon not running
|
||||||
|
- [x] Build and verify TypeScript compilation
|
||||||
|
|
||||||
|
## Migration Notes
|
||||||
|
|
||||||
|
- Users will need to run `tspm enable` once after update
|
||||||
|
- Existing daemon instances will stop working
|
||||||
|
- Documentation needs updating to explain new behavior
|
||||||
|
23
simple-test.ts
Normal file
23
simple-test.ts
Normal 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
28
test-script.ts
Normal 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);
|
@@ -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);
|
||||||
|
@@ -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();
|
||||||
|
@@ -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');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tspm',
|
name: '@git.zone/tspm',
|
||||||
version: '1.8.0',
|
version: '3.0.0',
|
||||||
description: 'a no fuzz process manager'
|
description: 'a no fuzz process manager'
|
||||||
}
|
}
|
||||||
|
@@ -48,7 +48,7 @@ export class TspmDaemon {
|
|||||||
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,7 +122,9 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ipcServer.onMessage('restart', async (request: RequestForMethod<'restart'>) => {
|
this.ipcServer.onMessage(
|
||||||
|
'restart',
|
||||||
|
async (request: RequestForMethod<'restart'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.restart(request.id);
|
await this.tspmInstance.restart(request.id);
|
||||||
const processInfo = this.tspmInstance.processInfo.get(request.id);
|
const processInfo = this.tspmInstance.processInfo.get(request.id);
|
||||||
@@ -134,7 +136,8 @@ export class TspmDaemon {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to restart process: ${error.message}`);
|
throw new Error(`Failed to restart process: ${error.message}`);
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'delete',
|
'delete',
|
||||||
@@ -160,7 +163,9 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.ipcServer.onMessage('describe', async (request: RequestForMethod<'describe'>) => {
|
this.ipcServer.onMessage(
|
||||||
|
'describe',
|
||||||
|
async (request: RequestForMethod<'describe'>) => {
|
||||||
const processInfo = await this.tspmInstance.describe(request.id);
|
const processInfo = await this.tspmInstance.describe(request.id);
|
||||||
const config = this.tspmInstance.processConfigs.get(request.id);
|
const config = this.tspmInstance.processConfigs.get(request.id);
|
||||||
|
|
||||||
@@ -172,15 +177,21 @@ export class TspmDaemon {
|
|||||||
processInfo,
|
processInfo,
|
||||||
config,
|
config,
|
||||||
};
|
};
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.ipcServer.onMessage('getLogs', async (request: RequestForMethod<'getLogs'>) => {
|
this.ipcServer.onMessage(
|
||||||
|
'getLogs',
|
||||||
|
async (request: RequestForMethod<'getLogs'>) => {
|
||||||
const logs = await this.tspmInstance.getLogs(request.id);
|
const logs = await this.tspmInstance.getLogs(request.id);
|
||||||
return { logs };
|
return { logs };
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Batch operations handlers
|
// Batch operations handlers
|
||||||
this.ipcServer.onMessage('startAll', async (request: RequestForMethod<'startAll'>) => {
|
this.ipcServer.onMessage(
|
||||||
|
'startAll',
|
||||||
|
async (request: RequestForMethod<'startAll'>) => {
|
||||||
const started: string[] = [];
|
const started: string[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: string; error: string }> = [];
|
||||||
|
|
||||||
@@ -196,9 +207,12 @@ export class TspmDaemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { started, failed };
|
return { started, failed };
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.ipcServer.onMessage('stopAll', async (request: RequestForMethod<'stopAll'>) => {
|
this.ipcServer.onMessage(
|
||||||
|
'stopAll',
|
||||||
|
async (request: RequestForMethod<'stopAll'>) => {
|
||||||
const stopped: string[] = [];
|
const stopped: string[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: string; error: string }> = [];
|
||||||
|
|
||||||
@@ -214,9 +228,12 @@ export class TspmDaemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return { stopped, failed };
|
return { stopped, failed };
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.ipcServer.onMessage('restartAll', async (request: RequestForMethod<'restartAll'>) => {
|
this.ipcServer.onMessage(
|
||||||
|
'restartAll',
|
||||||
|
async (request: RequestForMethod<'restartAll'>) => {
|
||||||
const restarted: string[] = [];
|
const restarted: string[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: string; error: string }> = [];
|
||||||
|
|
||||||
@@ -232,10 +249,13 @@ export class TspmDaemon {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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(
|
||||||
|
'daemon:status',
|
||||||
|
async (request: RequestForMethod<'daemon:status'>) => {
|
||||||
const memUsage = process.memoryUsage();
|
const memUsage = process.memoryUsage();
|
||||||
return {
|
return {
|
||||||
status: 'running',
|
status: 'running',
|
||||||
@@ -245,9 +265,12 @@ export class TspmDaemon {
|
|||||||
memoryUsage: memUsage.heapUsed,
|
memoryUsage: memUsage.heapUsed,
|
||||||
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
|
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
|
||||||
};
|
};
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.ipcServer.onMessage('daemon:shutdown', async (request: RequestForMethod<'daemon:shutdown'>) => {
|
this.ipcServer.onMessage(
|
||||||
|
'daemon:shutdown',
|
||||||
|
async (request: RequestForMethod<'daemon:shutdown'>) => {
|
||||||
if (this.isShuttingDown) {
|
if (this.isShuttingDown) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -269,15 +292,19 @@ export class TspmDaemon {
|
|||||||
success: true,
|
success: true,
|
||||||
message: `Daemon will shutdown ${graceful ? 'gracefully' : 'immediately'} in ${timeout}ms`,
|
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(
|
||||||
|
'heartbeat',
|
||||||
|
async (request: RequestForMethod<'heartbeat'>) => {
|
||||||
return {
|
return {
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
status: this.isShuttingDown ? 'degraded' : 'healthy',
|
status: this.isShuttingDown ? 'degraded' : 'healthy',
|
||||||
};
|
};
|
||||||
});
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as paths from './paths.js';
|
import * as paths from './paths.js';
|
||||||
import { spawn } from 'child_process';
|
|
||||||
import type {
|
import type {
|
||||||
IpcMethodMap,
|
IpcMethodMap,
|
||||||
RequestForMethod,
|
RequestForMethod,
|
||||||
@@ -34,10 +34,12 @@ export class TspmIpcClient {
|
|||||||
const daemonRunning = await this.isDaemonRunning();
|
const daemonRunning = await this.isDaemonRunning();
|
||||||
|
|
||||||
if (!daemonRunning) {
|
if (!daemonRunning) {
|
||||||
console.log('Daemon not running, starting it...');
|
throw new Error(
|
||||||
await this.startDaemon();
|
'TSPM daemon is not running.\n\n' +
|
||||||
// Wait a bit for daemon to initialize
|
'To start the daemon, run one of:\n' +
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
' tspm daemon start - Start daemon for this session\n' +
|
||||||
|
' tspm enable - Enable daemon as system service (recommended)\n',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create IPC client
|
// Create IPC client
|
||||||
@@ -57,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
|
||||||
@@ -75,7 +77,7 @@ export class TspmIpcClient {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to connect to daemon:', error);
|
console.error('Failed to connect to daemon:', error);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.',
|
'Could not connect to TSPM daemon. Please try running "tspm daemon start" or "tspm enable".',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,6 +101,7 @@ 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) {
|
||||||
|
// Try to connect first
|
||||||
await this.connect();
|
await this.connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,22 +113,7 @@ export class TspmIpcClient {
|
|||||||
|
|
||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle connection errors by trying to reconnect once
|
// Don't try to auto-reconnect, just throw the error
|
||||||
if (
|
|
||||||
error.message?.includes('ECONNREFUSED') ||
|
|
||||||
error.message?.includes('ENOENT')
|
|
||||||
) {
|
|
||||||
console.log('Connection lost, attempting to reconnect...');
|
|
||||||
this.isConnected = false;
|
|
||||||
await this.connect();
|
|
||||||
|
|
||||||
// Retry the request
|
|
||||||
return await this.ipcClient!.request<
|
|
||||||
RequestForMethod<M>,
|
|
||||||
ResponseForMethod<M>
|
|
||||||
>(method, params);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,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');
|
||||||
}
|
}
|
||||||
@@ -173,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(() => {});
|
||||||
@@ -195,42 +187,6 @@ export class TspmIpcClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the daemon process
|
|
||||||
*/
|
|
||||||
private async startDaemon(): Promise<void> {
|
|
||||||
const daemonScript = plugins.path.join(
|
|
||||||
paths.packageDir,
|
|
||||||
'dist_ts',
|
|
||||||
'daemon.js',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Spawn the daemon as a detached process
|
|
||||||
const daemonProcess = spawn(process.execPath, [daemonScript], {
|
|
||||||
detached: true,
|
|
||||||
stdio: ['ignore', 'ignore', 'ignore'],
|
|
||||||
env: {
|
|
||||||
...process.env,
|
|
||||||
TSPM_DAEMON_MODE: 'true',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Unref the process so the parent can exit
|
|
||||||
daemonProcess.unref();
|
|
||||||
|
|
||||||
console.log(`Started daemon process with PID: ${daemonProcess.pid}`);
|
|
||||||
|
|
||||||
// Wait for daemon to be ready using SmartIPC's helper
|
|
||||||
try {
|
|
||||||
await plugins.smartipc.SmartIpc.waitForServer({
|
|
||||||
socketPath: this.socketPath,
|
|
||||||
timeoutMs: 15000,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Daemon failed to start: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop the daemon
|
* Stop the daemon
|
||||||
*/
|
*/
|
||||||
|
103
ts/classes.servicemanager.ts
Normal file
103
ts/classes.servicemanager.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import * as plugins from './plugins.js';
|
||||||
|
import * as paths from './paths.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages TSPM daemon as a systemd service via smartdaemon
|
||||||
|
*/
|
||||||
|
export class TspmServiceManager {
|
||||||
|
private smartDaemon: plugins.smartdaemon.SmartDaemon;
|
||||||
|
private service: any = null; // SmartDaemonService type is not exported
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.smartDaemon = new plugins.smartdaemon.SmartDaemon();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get or create the TSPM daemon service configuration
|
||||||
|
*/
|
||||||
|
private async getOrCreateService(): Promise<any> {
|
||||||
|
if (!this.service) {
|
||||||
|
const cliPath = plugins.path.join(paths.packageDir, 'cli.js');
|
||||||
|
|
||||||
|
// Create service configuration
|
||||||
|
this.service = await this.smartDaemon.addService({
|
||||||
|
name: 'tspm-daemon',
|
||||||
|
description: 'TSPM Process Manager Daemon',
|
||||||
|
command: `${process.execPath} ${cliPath} daemon start-service`,
|
||||||
|
workingDir: process.env.HOME || process.cwd(),
|
||||||
|
version: '1.0.0',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this.service;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable the TSPM daemon as a system service
|
||||||
|
*/
|
||||||
|
public async enableService(): Promise<void> {
|
||||||
|
const service = await this.getOrCreateService();
|
||||||
|
|
||||||
|
// Save service configuration
|
||||||
|
await service.save();
|
||||||
|
|
||||||
|
// Enable service to start on boot
|
||||||
|
await service.enable();
|
||||||
|
|
||||||
|
// Start the service immediately
|
||||||
|
await service.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disable the TSPM daemon service
|
||||||
|
*/
|
||||||
|
public async disableService(): Promise<void> {
|
||||||
|
const service = await this.getOrCreateService();
|
||||||
|
|
||||||
|
// Stop the service if running
|
||||||
|
try {
|
||||||
|
await service.stop();
|
||||||
|
} catch (error) {
|
||||||
|
// Service might not be running
|
||||||
|
console.log('Service was not running');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable service from starting on boot
|
||||||
|
await service.disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current status of the systemd service
|
||||||
|
*/
|
||||||
|
public async getServiceStatus(): Promise<{
|
||||||
|
enabled: boolean;
|
||||||
|
running: boolean;
|
||||||
|
status: string;
|
||||||
|
}> {
|
||||||
|
try {
|
||||||
|
await this.getOrCreateService();
|
||||||
|
|
||||||
|
// Note: SmartDaemon doesn't provide direct status methods,
|
||||||
|
// so we'll need to check via systemctl commands
|
||||||
|
// This is a simplified implementation
|
||||||
|
return {
|
||||||
|
enabled: true, // Would need to check systemctl is-enabled
|
||||||
|
running: true, // Would need to check systemctl is-active
|
||||||
|
status: 'active',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
enabled: false,
|
||||||
|
running: false,
|
||||||
|
status: 'inactive',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reload the systemd service configuration
|
||||||
|
*/
|
||||||
|
public async reloadService(): Promise<void> {
|
||||||
|
const service = await this.getOrCreateService();
|
||||||
|
await service.reload();
|
||||||
|
}
|
||||||
|
}
|
@@ -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();
|
||||||
|
704
ts/cli.ts
704
ts/cli.ts
@@ -1,702 +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 { Logger, LogLevel } from './utils.errorhandler.js';
|
|
||||||
import type { IProcessConfig } from './classes.tspm.js';
|
|
||||||
|
|
||||||
export interface CliArguments {
|
|
||||||
verbose?: boolean;
|
|
||||||
watch?: boolean;
|
|
||||||
memory?: string;
|
|
||||||
cwd?: string;
|
|
||||||
daemon?: boolean;
|
|
||||||
test?: boolean;
|
|
||||||
name?: string;
|
|
||||||
autorestart?: boolean;
|
|
||||||
watchPaths?: string[];
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to parse memory strings (e.g., "512MB", "2GB")
|
|
||||||
function parseMemoryString(memStr: string): number {
|
|
||||||
const units = {
|
|
||||||
KB: 1024,
|
|
||||||
MB: 1024 * 1024,
|
|
||||||
GB: 1024 * 1024 * 1024,
|
|
||||||
};
|
|
||||||
|
|
||||||
const match = memStr.toUpperCase().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/);
|
|
||||||
if (!match) {
|
|
||||||
throw new Error(
|
|
||||||
`Invalid memory format: ${memStr}. Use format like "512MB" or "2GB"`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const value = parseFloat(match[1]);
|
|
||||||
const unit = (match[2] || 'MB') as keyof typeof units;
|
|
||||||
|
|
||||||
return Math.floor(value * units[unit]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to format memory for display
|
|
||||||
function formatMemory(bytes: number): string {
|
|
||||||
if (bytes >= 1024 * 1024 * 1024) {
|
|
||||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
||||||
} else if (bytes >= 1024 * 1024) {
|
|
||||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
||||||
} else if (bytes >= 1024) {
|
|
||||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
||||||
} else {
|
|
||||||
return `${bytes} B`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function for padding strings
|
|
||||||
function pad(str: string, length: number): string {
|
|
||||||
return str.length > length
|
|
||||||
? str.substring(0, length - 3) + '...'
|
|
||||||
: str.padEnd(length);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const run = async (): Promise<void> => {
|
|
||||||
const cliLogger = new Logger('CLI');
|
|
||||||
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
|
||||||
|
|
||||||
// Check if debug mode is enabled
|
|
||||||
const debugMode = process.env.TSPM_DEBUG === 'true';
|
|
||||||
if (debugMode) {
|
|
||||||
cliLogger.setLevel(LogLevel.DEBUG);
|
|
||||||
cliLogger.debug('Debug mode enabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
const smartcliInstance = new plugins.smartcli.Smartcli();
|
|
||||||
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
|
|
||||||
|
|
||||||
// Default command - show help and list processes
|
|
||||||
smartcliInstance.standardCommand().subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
console.log(
|
|
||||||
`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`,
|
|
||||||
);
|
|
||||||
console.log('Usage: tspm [command] [options]');
|
|
||||||
console.log('\nCommands:');
|
|
||||||
console.log(' start <script> Start a process');
|
|
||||||
console.log(' list List all processes');
|
|
||||||
console.log(' stop <id> Stop a process');
|
|
||||||
console.log(' restart <id> Restart a process');
|
|
||||||
console.log(' delete <id> Delete a process');
|
|
||||||
console.log(' describe <id> Show details for a process');
|
|
||||||
console.log(' logs <id> Show logs for a process');
|
|
||||||
console.log(' start-all Start all saved processes');
|
|
||||||
console.log(' stop-all Stop all processes');
|
|
||||||
console.log(' restart-all Restart all processes');
|
|
||||||
console.log('\nDaemon Commands:');
|
|
||||||
console.log(' daemon start Start the TSPM daemon');
|
|
||||||
console.log(' daemon stop Stop the TSPM daemon');
|
|
||||||
console.log(' daemon status Show daemon status');
|
|
||||||
console.log(
|
|
||||||
'\nUse tspm [command] --help for more information about a command.',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Show current process list
|
|
||||||
console.log('\nProcess List:');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await tspmIpcClient.request('list', {});
|
|
||||||
const processes = response.processes;
|
|
||||||
|
|
||||||
if (processes.length === 0) {
|
|
||||||
console.log(
|
|
||||||
' No processes running. Use "tspm start" to start a process.',
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.log(
|
|
||||||
'┌─────────┬─────────────┬───────────┬───────────┬──────────┐',
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'│ ID │ Name │ Status │ Memory │ Restarts │',
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'├─────────┼─────────────┼───────────┼───────────┼──────────┤',
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const proc of processes) {
|
|
||||||
const statusColor =
|
|
||||||
proc.status === 'online'
|
|
||||||
? '\x1b[32m'
|
|
||||||
: proc.status === 'errored'
|
|
||||||
? '\x1b[31m'
|
|
||||||
: '\x1b[33m';
|
|
||||||
const resetColor = '\x1b[0m';
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad(formatMemory(proc.memory), 9)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'└─────────┴─────────────┴───────────┴───────────┴──────────┘',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
'Error: Could not connect to TSPM daemon. Use "tspm daemon start" to start it.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start command
|
|
||||||
smartcliInstance.addCommand('start').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
try {
|
|
||||||
const script = argvArg._[1];
|
|
||||||
if (!script) {
|
|
||||||
console.error('Error: Please provide a script to run');
|
|
||||||
console.log('Usage: tspm start <script> [options]');
|
|
||||||
console.log('\nOptions:');
|
|
||||||
console.log(' --name <name> Name for the process');
|
|
||||||
console.log(
|
|
||||||
' --memory <size> Memory limit (e.g., "512MB", "2GB")',
|
|
||||||
);
|
|
||||||
console.log(' --cwd <path> Working directory');
|
|
||||||
console.log(
|
|
||||||
' --watch Watch for file changes and restart',
|
|
||||||
);
|
|
||||||
console.log(' --watch-paths <paths> Comma-separated paths to watch');
|
|
||||||
console.log(' --autorestart Auto-restart on crash');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const memoryLimit = argvArg.memory
|
|
||||||
? parseMemoryString(argvArg.memory)
|
|
||||||
: 512 * 1024 * 1024; // Default 512MB
|
|
||||||
const projectDir = argvArg.cwd || process.cwd();
|
|
||||||
const name = argvArg.name || script;
|
|
||||||
const watch = argvArg.watch || false;
|
|
||||||
const autorestart = argvArg.autorestart !== false; // Default true
|
|
||||||
const watchPaths = argvArg.watchPaths
|
|
||||||
? typeof argvArg.watchPaths === 'string'
|
|
||||||
? (argvArg.watchPaths as string).split(',')
|
|
||||||
: argvArg.watchPaths
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const processConfig: IProcessConfig = {
|
|
||||||
id: name.replace(/[^a-zA-Z0-9-_]/g, '_'),
|
|
||||||
name,
|
|
||||||
command: script,
|
|
||||||
projectDir,
|
|
||||||
memoryLimitBytes: memoryLimit,
|
|
||||||
autorestart,
|
|
||||||
watch,
|
|
||||||
watchPaths,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`Starting process: ${name}`);
|
|
||||||
console.log(` Command: ${script}`);
|
|
||||||
console.log(` Directory: ${projectDir}`);
|
|
||||||
console.log(` Memory limit: ${formatMemory(memoryLimit)}`);
|
|
||||||
console.log(` Auto-restart: ${autorestart}`);
|
|
||||||
if (watch) {
|
|
||||||
console.log(` Watch mode: enabled`);
|
|
||||||
if (watchPaths) {
|
|
||||||
console.log(` Watch paths: ${watchPaths.join(', ')}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await tspmIpcClient.request('start', {
|
|
||||||
config: processConfig,
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(`✓ Process started successfully`);
|
|
||||||
console.log(` ID: ${response.processId}`);
|
|
||||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
|
||||||
console.log(` Status: ${response.status}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error starting process:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stop command
|
|
||||||
smartcliInstance.addCommand('stop').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
try {
|
|
||||||
const id = argvArg._[1];
|
|
||||||
if (!id) {
|
|
||||||
console.error('Error: Please provide a process ID');
|
|
||||||
console.log('Usage: tspm stop <id>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Stopping process: ${id}`);
|
|
||||||
const response = await tspmIpcClient.request('stop', { id });
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
console.log(`✓ ${response.message}`);
|
|
||||||
} else {
|
|
||||||
console.error(`✗ Failed to stop process: ${response.message}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error stopping process:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restart command
|
|
||||||
smartcliInstance.addCommand('restart').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
try {
|
|
||||||
const id = argvArg._[1];
|
|
||||||
if (!id) {
|
|
||||||
console.error('Error: Please provide a process ID');
|
|
||||||
console.log('Usage: tspm restart <id>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Restarting process: ${id}`);
|
|
||||||
const response = await tspmIpcClient.request('restart', { id });
|
|
||||||
|
|
||||||
console.log(`✓ Process restarted successfully`);
|
|
||||||
console.log(` ID: ${response.processId}`);
|
|
||||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
|
||||||
console.log(` Status: ${response.status}`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error restarting process:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete command
|
|
||||||
smartcliInstance.addCommand('delete').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
try {
|
|
||||||
const id = argvArg._[1];
|
|
||||||
if (!id) {
|
|
||||||
console.error('Error: Please provide a process ID');
|
|
||||||
console.log('Usage: tspm delete <id>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Deleting process: ${id}`);
|
|
||||||
const response = await tspmIpcClient.request('delete', { id });
|
|
||||||
|
|
||||||
if (response.success) {
|
|
||||||
console.log(`✓ ${response.message}`);
|
|
||||||
} else {
|
|
||||||
console.error(`✗ Failed to delete process: ${response.message}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting process:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// List command
|
|
||||||
smartcliInstance.addCommand('list').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
try {
|
|
||||||
const response = await tspmIpcClient.request('list', {});
|
|
||||||
const processes = response.processes;
|
|
||||||
|
|
||||||
if (processes.length === 0) {
|
|
||||||
console.log('No processes running.');
|
|
||||||
} else {
|
|
||||||
console.log('Process List:');
|
|
||||||
console.log(
|
|
||||||
'┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐',
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'│ ID │ Name │ Status │ PID │ Memory │ Restarts │',
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤',
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const proc of processes) {
|
|
||||||
const statusColor =
|
|
||||||
proc.status === 'online'
|
|
||||||
? '\x1b[32m'
|
|
||||||
: proc.status === 'errored'
|
|
||||||
? '\x1b[31m'
|
|
||||||
: '\x1b[33m';
|
|
||||||
const resetColor = '\x1b[0m';
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad((proc.pid || '-').toString(), 9)} │ ${pad(formatMemory(proc.memory), 8)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error listing processes:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Describe command
|
|
||||||
smartcliInstance.addCommand('describe').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
try {
|
|
||||||
const id = argvArg._[1];
|
|
||||||
if (!id) {
|
|
||||||
console.error('Error: Please provide a process ID');
|
|
||||||
console.log('Usage: tspm describe <id>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await tspmIpcClient.request('describe', { id });
|
|
||||||
|
|
||||||
console.log(`Process Details: ${id}`);
|
|
||||||
console.log('─'.repeat(40));
|
|
||||||
console.log(`Status: ${response.processInfo.status}`);
|
|
||||||
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
|
|
||||||
console.log(
|
|
||||||
`Memory: ${formatMemory(response.processInfo.memory)}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`CPU: ${response.processInfo.cpu ? response.processInfo.cpu.toFixed(1) + '%' : 'N/A'}`,
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`Uptime: ${response.processInfo.uptime ? Math.floor(response.processInfo.uptime / 1000) + 's' : 'N/A'}`,
|
|
||||||
);
|
|
||||||
console.log(`Restarts: ${response.processInfo.restarts}`);
|
|
||||||
console.log('\nConfiguration:');
|
|
||||||
console.log(`Command: ${response.config.command}`);
|
|
||||||
console.log(`Directory: ${response.config.projectDir}`);
|
|
||||||
console.log(
|
|
||||||
`Memory Limit: ${formatMemory(response.config.memoryLimitBytes)}`,
|
|
||||||
);
|
|
||||||
console.log(`Auto-restart: ${response.config.autorestart}`);
|
|
||||||
if (response.config.watch) {
|
|
||||||
console.log(`Watch: enabled`);
|
|
||||||
if (response.config.watchPaths) {
|
|
||||||
console.log(
|
|
||||||
`Watch Paths: ${response.config.watchPaths.join(', ')}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error describing process:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Logs command
|
|
||||||
smartcliInstance.addCommand('logs').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
try {
|
|
||||||
const id = argvArg._[1];
|
|
||||||
if (!id) {
|
|
||||||
console.error('Error: Please provide a process ID');
|
|
||||||
console.log('Usage: tspm logs <id> [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) {
|
|
||||||
console.error('Error getting logs:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start-all command
|
|
||||||
smartcliInstance.addCommand('start-all').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
try {
|
|
||||||
console.log('Starting all processes...');
|
|
||||||
const response = await tspmIpcClient.request('startAll', {});
|
|
||||||
|
|
||||||
if (response.started.length > 0) {
|
|
||||||
console.log(`✓ Started ${response.started.length} processes:`);
|
|
||||||
for (const id of response.started) {
|
|
||||||
console.log(` - ${id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.failed.length > 0) {
|
|
||||||
console.log(`✗ Failed to start ${response.failed.length} processes:`);
|
|
||||||
for (const failure of response.failed) {
|
|
||||||
console.log(` - ${failure.id}: ${failure.error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error starting all processes:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Stop-all command
|
|
||||||
smartcliInstance.addCommand('stop-all').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
try {
|
|
||||||
console.log('Stopping all processes...');
|
|
||||||
const response = await tspmIpcClient.request('stopAll', {});
|
|
||||||
|
|
||||||
if (response.stopped.length > 0) {
|
|
||||||
console.log(`✓ Stopped ${response.stopped.length} processes:`);
|
|
||||||
for (const id of response.stopped) {
|
|
||||||
console.log(` - ${id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.failed.length > 0) {
|
|
||||||
console.log(`✗ Failed to stop ${response.failed.length} processes:`);
|
|
||||||
for (const failure of response.failed) {
|
|
||||||
console.log(` - ${failure.id}: ${failure.error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error stopping all processes:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restart-all command
|
|
||||||
smartcliInstance.addCommand('restart-all').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
try {
|
|
||||||
console.log('Restarting all processes...');
|
|
||||||
const response = await tspmIpcClient.request('restartAll', {});
|
|
||||||
|
|
||||||
if (response.restarted.length > 0) {
|
|
||||||
console.log(`✓ Restarted ${response.restarted.length} processes:`);
|
|
||||||
for (const id of response.restarted) {
|
|
||||||
console.log(` - ${id}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.failed.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`✗ Failed to restart ${response.failed.length} processes:`,
|
|
||||||
);
|
|
||||||
for (const failure of response.failed) {
|
|
||||||
console.log(` - ${failure.id}: ${failure.error}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error restarting all processes:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Daemon commands
|
|
||||||
smartcliInstance.addCommand('daemon').subscribe({
|
|
||||||
next: async (argvArg: CliArguments) => {
|
|
||||||
const subCommand = argvArg._[1];
|
|
||||||
|
|
||||||
switch (subCommand) {
|
|
||||||
case 'start':
|
|
||||||
try {
|
|
||||||
const status = await tspmIpcClient.getDaemonStatus();
|
|
||||||
if (status) {
|
|
||||||
console.log('TSPM daemon is already running');
|
|
||||||
console.log(` PID: ${status.pid}`);
|
|
||||||
console.log(
|
|
||||||
` Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
|
|
||||||
);
|
|
||||||
console.log(` Processes: ${status.processCount}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('Starting TSPM daemon...');
|
|
||||||
await tspmIpcClient.connect();
|
|
||||||
console.log('✓ TSPM daemon started successfully');
|
|
||||||
|
|
||||||
const newStatus = await tspmIpcClient.getDaemonStatus();
|
|
||||||
if (newStatus) {
|
|
||||||
console.log(` PID: ${newStatus.pid}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error starting daemon:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'stop':
|
|
||||||
try {
|
|
||||||
console.log('Stopping TSPM daemon...');
|
|
||||||
await tspmIpcClient.stopDaemon(true);
|
|
||||||
console.log('✓ TSPM daemon stopped successfully');
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error stopping daemon:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'status':
|
|
||||||
try {
|
|
||||||
const status = await tspmIpcClient.getDaemonStatus();
|
|
||||||
if (!status) {
|
|
||||||
console.log('TSPM daemon is not running');
|
|
||||||
console.log('Use "tspm daemon start" to start it');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('TSPM Daemon Status:');
|
|
||||||
console.log('─'.repeat(40));
|
|
||||||
console.log(`Status: ${status.status}`);
|
|
||||||
console.log(`PID: ${status.pid}`);
|
|
||||||
console.log(
|
|
||||||
`Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
|
|
||||||
);
|
|
||||||
console.log(`Processes: ${status.processCount}`);
|
|
||||||
console.log(
|
|
||||||
`Memory: ${formatMemory(status.memoryUsage || 0)}`,
|
|
||||||
);
|
|
||||||
console.log(`CPU: ${status.cpuUsage?.toFixed(1) || 0}s`);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting daemon status:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log('Usage: tspm daemon <command>');
|
|
||||||
console.log('\nCommands:');
|
|
||||||
console.log(' start Start the TSPM daemon');
|
|
||||||
console.log(' stop Stop the TSPM daemon');
|
|
||||||
console.log(' status Show daemon status');
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
error: (err) => {
|
|
||||||
cliLogger.error(err);
|
|
||||||
},
|
|
||||||
complete: () => {},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Start parsing commands
|
|
||||||
smartcliInstance.startParse();
|
|
||||||
};
|
|
||||||
|
31
ts/cli/commands/batch/restart-all.ts
Normal file
31
ts/cli/commands/batch/restart-all.ts
Normal 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' },
|
||||||
|
);
|
||||||
|
}
|
31
ts/cli/commands/batch/start-all.ts
Normal file
31
ts/cli/commands/batch/start-all.ts
Normal 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' },
|
||||||
|
);
|
||||||
|
}
|
31
ts/cli/commands/batch/stop-all.ts
Normal file
31
ts/cli/commands/batch/stop-all.ts
Normal 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' },
|
||||||
|
);
|
||||||
|
}
|
140
ts/cli/commands/daemon/index.ts
Normal file
140
ts/cli/commands/daemon/index.ts
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
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
|
||||||
|
const daemonProcess = spawn(process.execPath, [daemonScript], {
|
||||||
|
detached: true,
|
||||||
|
stdio: '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.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
102
ts/cli/commands/default.ts
Normal 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: () => {},
|
||||||
|
});
|
||||||
|
}
|
29
ts/cli/commands/process/delete.ts
Normal file
29
ts/cli/commands/process/delete.ts
Normal 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' },
|
||||||
|
);
|
||||||
|
}
|
49
ts/cli/commands/process/describe.ts
Normal file
49
ts/cli/commands/process/describe.ts
Normal 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' },
|
||||||
|
);
|
||||||
|
}
|
52
ts/cli/commands/process/list.ts
Normal file
52
ts/cli/commands/process/list.ts
Normal 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' },
|
||||||
|
);
|
||||||
|
}
|
99
ts/cli/commands/process/logs.ts
Normal file
99
ts/cli/commands/process/logs.ts
Normal 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'),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
28
ts/cli/commands/process/restart.ts
Normal file
28
ts/cli/commands/process/restart.ts
Normal 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' },
|
||||||
|
);
|
||||||
|
}
|
102
ts/cli/commands/process/start.ts
Normal file
102
ts/cli/commands/process/start.ts
Normal 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' },
|
||||||
|
);
|
||||||
|
}
|
29
ts/cli/commands/process/stop.ts
Normal file
29
ts/cli/commands/process/stop.ts
Normal 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' },
|
||||||
|
);
|
||||||
|
}
|
36
ts/cli/commands/service/disable.ts
Normal file
36
ts/cli/commands/service/disable.ts
Normal 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: () => {},
|
||||||
|
});
|
||||||
|
}
|
36
ts/cli/commands/service/enable.ts
Normal file
36
ts/cli/commands/service/enable.ts
Normal 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
24
ts/cli/helpers/argv.ts
Normal 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
18
ts/cli/helpers/errors.ts
Normal 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);
|
||||||
|
}
|
18
ts/cli/helpers/formatting.ts
Normal file
18
ts/cli/helpers/formatting.ts
Normal 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}`;
|
||||||
|
}
|
22
ts/cli/helpers/lifecycle.ts
Normal file
22
ts/cli/helpers/lifecycle.ts
Normal 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
33
ts/cli/helpers/memory.ts
Normal 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
68
ts/cli/index.ts
Normal 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();
|
||||||
|
};
|
26
ts/cli/registration/daemon-check.ts
Normal file
26
ts/cli/registration/daemon-check.ts
Normal 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;
|
||||||
|
}
|
98
ts/cli/registration/index.ts
Normal file
98
ts/cli/registration/index.ts
Normal 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
20
ts/cli/types.ts
Normal 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
14
ts/cli/utils/ipc.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,7 @@ export * from './classes.tspm.js';
|
|||||||
export * from './classes.processmonitor.js';
|
export * from './classes.processmonitor.js';
|
||||||
export * from './classes.daemon.js';
|
export * from './classes.daemon.js';
|
||||||
export * from './classes.ipcclient.js';
|
export * from './classes.ipcclient.js';
|
||||||
|
export * from './classes.servicemanager.js';
|
||||||
export * from './ipc.types.js';
|
export * from './ipc.types.js';
|
||||||
|
|
||||||
import * as cli from './cli.js';
|
import * as cli from './cli.js';
|
||||||
|
@@ -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
|
||||||
|
Reference in New Issue
Block a user