Compare commits

...

13 Commits

Author SHA1 Message Date
529a403c4b 3.1.0
Some checks failed
Default (tags) / security (push) Successful in 1m1s
Default (tags) / test (push) Failing after 1m25s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-28 18:17:41 +00:00
ece16b75e2 feat(daemon): Reorganize and refactor core into client/daemon/shared modules; add IPC protocol and tests 2025-08-28 18:17:41 +00:00
1516185c4d prepare refactor 2025-08-28 18:10:33 +00:00
1a782f0768 3.0.2
Some checks failed
Default (tags) / security (push) Successful in 52s
Default (tags) / test (push) Failing after 1m7s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-28 16:29:41 +00:00
ae4148c82f fix(daemon): Ensure TSPM runtime dir exists and improve daemon startup/debug output 2025-08-28 16:29:41 +00:00
6141b26530 3.0.0
Some checks failed
Default (tags) / security (push) Failing after 11m45s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-08-28 15:52:29 +00:00
e73f4acd63 BREAKING CHANGE(daemon): Refactor daemon and service management: remove IPC auto-spawn, add TspmServiceManager, tighten IPC/client/CLI behavior and tests 2025-08-28 15:52:29 +00:00
8e3cfb624b 2.0.1
Some checks failed
Default (tags) / security (push) Failing after 11m47s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-08-28 15:51:11 +00:00
33fb02733d update 2025-08-28 15:47:59 +00:00
1c2310c185 2.0.0
Some checks failed
Default (tags) / security (push) Successful in 1m2s
Default (tags) / test (push) Failing after 1m31s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-28 10:39:35 +00:00
d33a001edc BREAKING CHANGE(daemon): Refactor daemon lifecycle and service management: remove IPC auto-spawn, add TspmServiceManager and CLI enable/disable 2025-08-28 10:39:35 +00:00
35b6a6a8d0 1.8.0
Some checks failed
Default (tags) / security (push) Failing after 10m29s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-08-26 15:00:15 +00:00
50c5fdb0ea feat(daemon): Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC client subscribe/unsubscribe, CLI --follow streaming, and sequencing for logs 2025-08-26 15:00:15 +00:00
51 changed files with 2047 additions and 1388 deletions

View File

@@ -1,6 +1,64 @@
# Changelog # Changelog
## 2025-08-28 - 3.1.0 - feat(daemon)
Reorganize and refactor core into client/daemon/shared modules; add IPC protocol and tests
- Reorganized core code: split daemon and client logic into ts/daemon and ts/client directories
- Moved process management into ProcessManager, ProcessMonitor and ProcessWrapper under ts/daemon
- Added a dedicated IPC client and service manager under ts/client (tspm.ipcclient, tspm.servicemanager)
- Introduced shared protocol and error handling: ts/shared/protocol/ipc.types.ts, protocol.version.ts and ts/shared/common/utils.errorhandler.ts
- Updated CLI to import Logger from shared/common utils and updated related helpers
- Added daemon entrypoint at ts/daemon/index.ts and reorganized daemon startup/shutdown/heartbeat handling
- Added test assets (test/testassets/simple-test.ts, simple-script2.ts) and expanded test files under test/
- Removed legacy top-level class files (classes.*) in favor of the new structured layout
## 2025-08-28 - 3.0.2 - fix(daemon)
Ensure TSPM runtime dir exists and improve daemon startup/debug output
- Create ~/.tspm directory before starting the daemon to avoid missing-directory errors
- Start daemon child process with stdio inherited when TSPM_DEBUG=true to surface startup errors during debugging
- Add warning and troubleshooting guidance when daemon process starts but does not respond (suggest checking socket file and using TSPM_DEBUG)
- Bump package version to 3.0.1
## 2025-08-28 - 3.0.0 - BREAKING CHANGE(daemon)
Refactor daemon and service management: remove IPC auto-spawn, add TspmServiceManager, tighten IPC/client/CLI behavior and tests
- Remove automatic daemon spawn from the IPC client — clients now error with guidance and require the daemon to be started manually or enabled as a system service
- Add TspmServiceManager to manage the daemon as a systemd service (enable/disable/reload/status)
- Update IPC server/client to use SmartIpc.createServer/createClient with heartbeat defaults and explicit onMessage handlers
- Daemon publishes per-process logs to topics (logs.<processId>) and re-emits ProcessMonitor logs for pub/sub
- CLI updated: add enable/disable service commands, adjust daemon start/stop/status workflows and improve user hints when daemon is not running
- Add/adjust integration and unit tests to cover daemon lifecycle, IPC client behavior, log streaming, heartbeat and resource reporting
- Documentation expanded (README, readme.plan.md, changelog) to reflect the refactor and migration notes
- Various code cleanups, formatting fixes and defensive checks across modules
## 2025-08-28 - 2.0.0 - BREAKING CHANGE(daemon)
Refactor daemon lifecycle and service management: remove IPC auto-spawn, add TspmServiceManager and CLI enable/disable
- Do not auto-spawn the daemon from the IPC client anymore — attempts to connect will now error with instructions to start the daemon manually or enable the system service (breaking change).
- Add TspmServiceManager to manage the daemon as a systemd service via smartdaemon (enable/disable/reload/status helpers).
- CLI: add 'enable' and 'disable' commands to install/uninstall the daemon as a system service and add 'daemon start-service' entrypoint used by systemd.
- CLI: improve error handling and user hints when the daemon is not running (suggests `tspm daemon start` or `tspm enable`).
- IPC client: removed startDaemon() and related auto-reconnect/start logic; request() no longer auto-reconnects or implicitly start the daemon.
- Export TspmServiceManager from the package index so service management is part of the public API.
- Updated development plan/readme (readme.plan.md) to reflect the refactor toward proper SmartDaemon integration and migration notes.
## 2025-08-26 - 1.8.0 - feat(daemon)
Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC client subscribe/unsubscribe, CLI --follow streaming, and sequencing for logs
- Upgrade @push.rocks/smartipc dependency to ^2.1.2
- Daemon: initialize SmartIpc server with heartbeat and publish process logs to topic `logs.<processId>`; write PID file and start heartbeat monitoring
- Tspm: re-emit monitor log events as 'process:log' so daemon can broadcast logs
- ProcessWrapper: include seq and runId on IProcessLog entries and maintain nextSeq/runId (adds sequencing to logs); default log buffer size applied
- TspmIpcClient: improved connect options (retries, timeouts, heartbeat handling), add subscribe/unsubscribe for real-time logs, and use SmartIpc.waitForServer when starting daemon
- CLI: add --follow flag to `logs` command to stream live logs, detect sequence gaps/duplicates, and handle graceful cleanup on Ctrl+C
- ProcessMonitor: now extends EventEmitter and re-emits process logs for upstream consumption
- Standardized heartbeat and IPC timing defaults (heartbeatInterval: 5000ms, heartbeatTimeout: 20000ms, heartbeatInitialGracePeriodMs: 10000ms)
## 2025-08-25 - 1.7.0 - feat(readme) ## 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
@@ -9,6 +67,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
@@ -17,6 +76,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.

0
cli.js Normal file → Executable file
View File

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tspm", "name": "@git.zone/tspm",
"version": "1.7.0", "version": "3.1.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",
@@ -30,10 +30,11 @@
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartcli": "^4.0.11", "@push.rocks/smartcli": "^4.0.11",
"@push.rocks/smartdaemon": "^2.0.8", "@push.rocks/smartdaemon": "^2.0.8",
"@push.rocks/smartipc": "^2.0.3", "@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",

281
pnpm-lock.yaml generated
View File

@@ -21,8 +21,8 @@ importers:
specifier: ^2.0.8 specifier: ^2.0.8
version: 2.0.8 version: 2.0.8
'@push.rocks/smartipc': '@push.rocks/smartipc':
specifier: ^2.0.3 specifier: ^2.1.2
version: 2.0.3 version: 2.1.2
'@push.rocks/smartpath': '@push.rocks/smartpath':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
@@ -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'}
@@ -950,8 +803,8 @@ packages:
'@push.rocks/smarthash@3.2.3': '@push.rocks/smarthash@3.2.3':
resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==} resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==}
'@push.rocks/smartipc@2.0.3': '@push.rocks/smartipc@2.1.2':
resolution: {integrity: sha512-Yty+craFj9lYp6dL1dxHwrF1ykeu02o78D9kNGb5XR+4c53Cci7puqgK9+zbSakaHlNMqKHUWICi50ziGuq5xQ==} resolution: {integrity: sha512-QyFrohq9jq4ISl6DUyeS1uuWgKxQiTrWZAzIqsGZW/BT36FGoqMpGufgjjkVuBvZtYW8e3hl+lcmT+DHfVMfmg==}
'@push.rocks/smartjson@5.0.20': '@push.rocks/smartjson@5.0.20':
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==} resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
@@ -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:
@@ -6425,7 +6198,7 @@ snapshots:
'@types/through2': 2.0.41 '@types/through2': 2.0.41
through2: 4.0.2 through2: 4.0.2
'@push.rocks/smartipc@2.0.3': '@push.rocks/smartipc@2.1.2':
dependencies: dependencies:
'@push.rocks/smartdelay': 3.0.5 '@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartrx': 3.0.10 '@push.rocks/smartrx': 3.0.10
@@ -8366,34 +8139,6 @@ snapshots:
has-tostringtag: 1.0.2 has-tostringtag: 1.0.2
hasown: 2.0.2 hasown: 2.0.2
esbuild@0.25.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.0
'@esbuild/android-arm': 0.25.0
'@esbuild/android-arm64': 0.25.0
'@esbuild/android-x64': 0.25.0
'@esbuild/darwin-arm64': 0.25.0
'@esbuild/darwin-x64': 0.25.0
'@esbuild/freebsd-arm64': 0.25.0
'@esbuild/freebsd-x64': 0.25.0
'@esbuild/linux-arm': 0.25.0
'@esbuild/linux-arm64': 0.25.0
'@esbuild/linux-ia32': 0.25.0
'@esbuild/linux-loong64': 0.25.0
'@esbuild/linux-mips64el': 0.25.0
'@esbuild/linux-ppc64': 0.25.0
'@esbuild/linux-riscv64': 0.25.0
'@esbuild/linux-s390x': 0.25.0
'@esbuild/linux-x64': 0.25.0
'@esbuild/netbsd-arm64': 0.25.0
'@esbuild/netbsd-x64': 0.25.0
'@esbuild/openbsd-arm64': 0.25.0
'@esbuild/openbsd-x64': 0.25.0
'@esbuild/sunos-x64': 0.25.0
'@esbuild/win32-arm64': 0.25.0
'@esbuild/win32-ia32': 0.25.0
'@esbuild/win32-x64': 0.25.0
esbuild@0.25.9: esbuild@0.25.9:
optionalDependencies: optionalDependencies:
'@esbuild/aix-ppc64': 0.25.9 '@esbuild/aix-ppc64': 0.25.9
@@ -10753,9 +10498,9 @@ snapshots:
tsscmp@1.0.6: {} tsscmp@1.0.6: {}
tsx@4.19.3: tsx@4.20.5:
dependencies: dependencies:
esbuild: 0.25.0 esbuild: 0.25.9
get-tsconfig: 4.10.0 get-tsconfig: 4.10.0
optionalDependencies: optionalDependencies:
fsevents: 2.3.3 fsevents: 2.3.3

View File

@@ -64,9 +64,11 @@ tspm restart my-server
### Process Management ### Process Management
#### `tspm start <script> [options]` #### `tspm start <script> [options]`
Start a new process with automatic monitoring and management. Start a new process with automatic monitoring and management.
**Options:** **Options:**
- `--name <name>` - Custom name for the process (default: script name) - `--name <name>` - Custom name for the process (default: script name)
- `--memory <size>` - Memory limit (e.g., "512MB", "2GB", default: 512MB) - `--memory <size>` - Memory limit (e.g., "512MB", "2GB", default: 512MB)
- `--cwd <path>` - Working directory (default: current directory) - `--cwd <path>` - Working directory (default: current directory)
@@ -75,6 +77,7 @@ Start a new process with automatic monitoring and management.
- `--autorestart` - Auto-restart on crash (default: true) - `--autorestart` - Auto-restart on crash (default: true)
**Examples:** **Examples:**
```bash ```bash
# Simple start # Simple start
tspm start server.js tspm start server.js
@@ -90,6 +93,7 @@ tspm start ../other-project/index.js --cwd ../other-project --name other
``` ```
#### `tspm stop <id>` #### `tspm stop <id>`
Gracefully stop a running process (SIGTERM → SIGKILL after timeout). Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
```bash ```bash
@@ -97,6 +101,7 @@ tspm stop my-server
``` ```
#### `tspm restart <id>` #### `tspm restart <id>`
Stop and restart a process with the same configuration. Stop and restart a process with the same configuration.
```bash ```bash
@@ -104,6 +109,7 @@ tspm restart my-server
``` ```
#### `tspm delete <id>` #### `tspm delete <id>`
Stop and remove a process from TSPM management. Stop and remove a process from TSPM management.
```bash ```bash
@@ -113,6 +119,7 @@ tspm delete old-server
### Monitoring & Information ### Monitoring & Information
#### `tspm list` #### `tspm list`
Display all managed processes in a beautiful table. Display all managed processes in a beautiful table.
```bash ```bash
@@ -128,6 +135,7 @@ tspm list
``` ```
#### `tspm describe <id>` #### `tspm describe <id>`
Get detailed information about a specific process. Get detailed information about a specific process.
```bash ```bash
@@ -153,9 +161,11 @@ Watch Paths: src, config
``` ```
#### `tspm logs <id> [options]` #### `tspm logs <id> [options]`
View process logs (stdout and stderr). View process logs (stdout and stderr).
**Options:** **Options:**
- `--lines <n>` - Number of lines to display (default: 50) - `--lines <n>` - Number of lines to display (default: 50)
```bash ```bash
@@ -165,6 +175,7 @@ tspm logs my-server --lines 100
### Batch Operations ### Batch Operations
#### `tspm start-all` #### `tspm start-all`
Start all saved processes at once. Start all saved processes at once.
```bash ```bash
@@ -172,6 +183,7 @@ tspm start-all
``` ```
#### `tspm stop-all` #### `tspm stop-all`
Stop all running processes. Stop all running processes.
```bash ```bash
@@ -179,6 +191,7 @@ tspm stop-all
``` ```
#### `tspm restart-all` #### `tspm restart-all`
Restart all running processes. Restart all running processes.
```bash ```bash
@@ -188,6 +201,7 @@ tspm restart-all
### Daemon Management ### Daemon Management
#### `tspm daemon start` #### `tspm daemon start`
Start the TSPM daemon (happens automatically on first command). Start the TSPM daemon (happens automatically on first command).
```bash ```bash
@@ -195,6 +209,7 @@ tspm daemon start
``` ```
#### `tspm daemon stop` #### `tspm daemon stop`
Stop the TSPM daemon and all managed processes. Stop the TSPM daemon and all managed processes.
```bash ```bash
@@ -202,6 +217,7 @@ tspm daemon stop
``` ```
#### `tspm daemon status` #### `tspm daemon status`
Check daemon health and statistics. Check daemon health and statistics.
```bash ```bash
@@ -245,7 +261,7 @@ const processId = await manager.start({
projectDir: process.cwd(), projectDir: process.cwd(),
memoryLimitBytes: 512 * 1024 * 1024, // 512MB memoryLimitBytes: 512 * 1024 * 1024, // 512MB
autorestart: true, autorestart: true,
watch: false watch: false,
}); });
// Monitor process // Monitor process
@@ -259,18 +275,23 @@ await manager.stop(processId);
## 🔧 Advanced Features ## 🔧 Advanced Features
### Memory Limit Enforcement ### Memory Limit Enforcement
TSPM tracks memory usage including all child processes spawned by your application. When a process exceeds its memory limit, it's gracefully restarted. TSPM tracks memory usage including all child processes spawned by your application. When a process exceeds its memory limit, it's gracefully restarted.
### Process Group Tracking ### Process Group Tracking
Using `ps-tree`, TSPM monitors not just your main process but all child processes it spawns, ensuring complete cleanup on stop/restart. Using `ps-tree`, TSPM monitors not just your main process but all child processes it spawns, ensuring complete cleanup on stop/restart.
### Intelligent Logging ### Intelligent Logging
Logs are buffered and managed efficiently, preventing memory issues from excessive output while ensuring you don't lose important information. Logs are buffered and managed efficiently, preventing memory issues from excessive output while ensuring you don't lose important information.
### Graceful Shutdown ### Graceful Shutdown
Processes receive SIGTERM first, allowing them to clean up. After a timeout, SIGKILL ensures termination. Processes receive SIGTERM first, allowing them to clean up. After a timeout, SIGKILL ensures termination.
### Configuration Persistence ### Configuration Persistence
Process configurations are saved, allowing you to restart all processes after a system reboot with a single command. Process configurations are saved, allowing you to restart all processes after a system reboot with a single command.
## 🛠️ Development ## 🛠️ Development
@@ -304,6 +325,7 @@ tspm list
## 📊 Performance ## 📊 Performance
TSPM is designed to be lightweight and efficient: TSPM is designed to be lightweight and efficient:
- Minimal CPU overhead (typically < 0.5%) - Minimal CPU overhead (typically < 0.5%)
- Small memory footprint (~30-50MB for the daemon) - Small memory footprint (~30-50MB for the daemon)
- Fast process startup and shutdown - Fast process startup and shutdown
@@ -322,7 +344,7 @@ Unlike general-purpose process managers, TSPM is built specifically for the Type
## License and Legal Information ## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file. **Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
@@ -337,4 +359,4 @@ Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc. For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works. By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -1,209 +1,249 @@
# TSPM Refactoring Plan: Central Daemon Architecture # TSPM Architecture Refactoring Plan
## Problem Analysis ## Current Problems
The current architecture has several issues that make the codebase confusing:
Currently, each `startAsDaemon` creates an isolated tspm instance with no coordination: 1. **Flat structure confusion**: All classes are mixed together in the `ts/` directory with a `classes.` prefix naming convention
2. **Unclear boundaries**: It's hard to tell what code runs in the daemon vs the client
3. **Misleading naming**: The `Tspm` class is actually the core ProcessManager, not the overall system
4. **Coupling risk**: Client code could accidentally import daemon internals, bloating bundles
5. **No architectural enforcement**: Nothing prevents cross-boundary imports
- Multiple daemons reading/writing same config file ## Goal
- No communication between instances Refactor into a clean 3-folder architecture (daemon/client/shared) with proper separation of concerns and enforced boundaries.
- Inconsistent process management
- `tspm list` shows all processes but each daemon only manages its own
## Proposed Architecture ## Key Insights from Architecture Review
### 1. Central Daemon Manager (`ts/classes.daemon.ts`) ### Why This Separation Makes Sense
After discussion with GPT-5, we identified that:
- Single daemon instance managing ALL processes 1. **ProcessManager/Monitor/Wrapper are daemon-only**: These classes actually spawn and manage processes. Clients never need them - they only communicate via IPC.
- Runs continuously in background
- Uses Unix socket for IPC at `~/.tspm/tspm.sock`
- Maintains single source of truth for process state
### 2. IPC Communication Layer (`ts/classes.ipc.ts`) 2. **The client is just an IPC bridge**: The client (CLI and library users) only needs to send messages to the daemon and receive responses. It should never directly manage processes.
- **Framework**: Use `@push.rocks/smartipc` v2.0.1 3. **Shared should be minimal**: Only the IPC protocol types and pure utilities should be shared. No Node.js APIs, no file system access.
- **Server**: SmartIpc server in daemon using Unix Domain Socket
- **Client**: SmartIpc client in CLI for all operations
- **Socket Path**: `~/.tspm/tspm.sock` (Unix) or named pipe (Windows)
- **Protocol**: Type-safe request/response with SmartIpc's built-in patterns
- **Features**:
- Automatic reconnection with exponential backoff
- Heartbeat monitoring for daemon health
- Type-safe message contracts
- **Auto-start**: CLI starts daemon if connection fails
### 3. New CLI Commands 4. **Protocol is the contract**: The IPC types are the only coupling between client and daemon. This allows independent evolution.
- `tspm enable` - Start central daemon using systemd/launchd ## Architecture Overview
- `tspm disable` - Stop and disable central daemon
- `tspm status` - Show daemon status
- Remove `startAsDaemon` (replaced by daemon + `tspm start`)
### 4. Refactored CLI (`ts/cli.ts`) ### Folder Structure
- **ts/daemon/** - Process orchestration (runs in daemon process only)
- Contains all process management logic
- Spawns and monitors actual system processes
- Manages configuration and state
- Never imported by client code
All commands become daemon clients: - **ts/client/** - IPC communication (runs in CLI/client process)
- Only knows how to talk to the daemon via IPC
- Lightweight - no process management logic
- What library users import when they use TSPM
- Can work in any Node.js environment (or potentially browser)
```typescript - **ts/shared/** - Minimal shared contract (protocol & pure utilities)
// Before: Direct process management - **protocol/** - IPC request/response types, error codes, version
await tspm.start(config); - **common/** - Pure utilities with no environment dependencies
- No fs, net, child_process, or Node-specific APIs
- Keep as small as possible to minimize coupling
// After: Send to daemon ## File Organization Rationale
await ipcClient.request('start', config);
```
### 5. File Structure Changes ### What Goes in Daemon
These files are daemon-only because they actually manage processes:
- `processmanager.ts` (was Tspm) - Core process orchestration logic
- `processmonitor.ts` - Monitors memory and restarts processes
- `processwrapper.ts` - Wraps child processes with logging
- `tspm.config.ts` - Persists process configurations to disk
- `tspm.daemon.ts` - Wires everything together, handles IPC requests
``` ### What Goes in Client
ts/ These files are client-only because they just communicate:
├── classes.daemon.ts # New: Central daemon server - `tspm.ipcclient.ts` - Sends requests to daemon via Unix socket
├── classes.ipc.ts # New: IPC client/server - `tspm.servicemanager.ts` - Manages systemd service (delegates to smartdaemon)
├── classes.tspm.ts # Modified: Used by daemon only - CLI files - Command-line interface that uses the IPC client
├── cli.ts # Modified: Becomes thin client
└── classes.daemonmanager.ts # New: Systemd/launchd integration
```
## Implementation Steps ### What Goes in Shared
Only the absolute minimum needed by both:
- `protocol/ipc.types.ts` - Request/response type definitions
- `protocol/error.codes.ts` - Standardized error codes
- `common/utils.errorhandler.ts` - If it's pure (no I/O)
- Parts of `paths.ts` - Constants like socket path (not OS-specific resolution)
- Plugin interfaces only (not loading logic)
### Phase 1: Core Infrastructure ### Critical Design Decisions
- [ ] Add `@push.rocks/smartipc` dependency (v2.0.1) 1. **Rename Tspm to ProcessManager**: The class name should reflect what it does
- [ ] Create IPC message type definitions for all operations 2. **No process management in shared**: ProcessManager, ProcessMonitor, ProcessWrapper are daemon-only
- [ ] Implement daemon server with SmartIpc server 3. **Protocol versioning**: Add version to allow client/daemon compatibility
- [ ] Create IPC client wrapper for CLI 4. **Enforce boundaries**: Use TypeScript project references to prevent violations
- [ ] Add daemon lifecycle management (enable/disable) 5. **Control exports**: Package.json exports map ensures library users can't import daemon code
### Phase 2: CLI Refactoring ## Detailed Task List
- [ ] Convert all CLI commands to SmartIpc client requests ### Phase 1: Create New Structure
- [ ] Add daemon auto-start logic with connection monitoring - [ ] Create directory `ts/daemon/`
- [ ] Leverage SmartIpc's built-in reconnection and error handling - [ ] Create directory `ts/client/`
- [ ] Implement type-safe message contracts for all commands - [ ] Create directory `ts/shared/`
- [ ] Create directory `ts/shared/protocol/`
- [ ] Create directory `ts/shared/common/`
### Phase 3: Migration & Cleanup ### Phase 2: Move Daemon Files
- [ ] Move `ts/daemon.ts``ts/daemon/index.ts`
- [ ] Move `ts/classes.daemon.ts``ts/daemon/tspm.daemon.ts`
- [ ] Move `ts/classes.tspm.ts``ts/daemon/processmanager.ts`
- [ ] Move `ts/classes.processmonitor.ts``ts/daemon/processmonitor.ts`
- [ ] Move `ts/classes.processwrapper.ts``ts/daemon/processwrapper.ts`
- [ ] Move `ts/classes.config.ts``ts/daemon/tspm.config.ts`
- [ ] Migrate existing config to daemon-compatible format ### Phase 3: Move Client Files
- [ ] Remove `startAsDaemon` command - [ ] Move `ts/classes.ipcclient.ts``ts/client/tspm.ipcclient.ts`
- [ ] Add migration guide for users - [ ] Move `ts/classes.servicemanager.ts``ts/client/tspm.servicemanager.ts`
- [ ] Create `ts/client/index.ts` barrel export file
## Technical Details ### Phase 4: Move Shared Files
- [ ] Move `ts/ipc.types.ts``ts/shared/protocol/ipc.types.ts`
- [ ] Create `ts/shared/protocol/protocol.version.ts` with version constant
- [ ] Create `ts/shared/protocol/error.codes.ts` with standardized error codes
- [ ] Move `ts/utils.errorhandler.ts``ts/shared/common/utils.errorhandler.ts`
- [ ] Analyze `ts/paths.ts` - split into constants (shared) vs resolvers (daemon)
- [ ] Move/split `ts/plugins.ts` - interfaces to shared, loaders to daemon
### IPC Implementation with SmartIpc ### Phase 5: Rename Classes
- [ ] In `processmanager.ts`: Rename class `Tspm``ProcessManager`
- [ ] Update all references to `Tspm` class to use `ProcessManager`
- [ ] Update constructor in `tspm.daemon.ts` to use `ProcessManager`
```typescript ### Phase 6: Update Imports - Daemon Files
// Daemon server setup - [ ] Update imports in `ts/daemon/index.ts`
import { SmartIpc } from '@push.rocks/smartipc'; - [ ] Update imports in `ts/daemon/tspm.daemon.ts`
- [ ] Change `'./classes.tspm.js'``'./processmanager.js'`
- [ ] Change `'./paths.js'` → appropriate shared/daemon path
- [ ] Change `'./ipc.types.js'``'../shared/protocol/ipc.types.js'`
- [ ] Update imports in `ts/daemon/processmanager.ts`
- [ ] Change `'./classes.processmonitor.js'``'./processmonitor.js'`
- [ ] Change `'./classes.processwrapper.js'``'./processwrapper.js'`
- [ ] Change `'./classes.config.js'``'./tspm.config.js'`
- [ ] Change `'./utils.errorhandler.js'``'../shared/common/utils.errorhandler.js'`
- [ ] Update imports in `ts/daemon/processmonitor.ts`
- [ ] Change `'./classes.processwrapper.js'``'./processwrapper.js'`
- [ ] Update imports in `ts/daemon/processwrapper.ts`
- [ ] Update imports in `ts/daemon/tspm.config.ts`
const ipcServer = SmartIpc.createServer({ ### Phase 7: Update Imports - Client Files
id: 'tspm-daemon', - [ ] Update imports in `ts/client/tspm.ipcclient.ts`
socketPath: '~/.tspm/tspm.sock', // Unix socket - [ ] Change `'./paths.js'` → appropriate shared/daemon path
}); - [ ] Change `'./ipc.types.js'``'../shared/protocol/ipc.types.js'`
- [ ] Update imports in `ts/client/tspm.servicemanager.ts`
- [ ] Change `'./paths.js'` → appropriate shared/daemon path
- [ ] Create exports in `ts/client/index.ts`
- [ ] Export TspmIpcClient
- [ ] Export TspmServiceManager
// Message handlers with type safety ### Phase 8: Update Imports - CLI Files
ipcServer.onMessage<StartRequest, StartResponse>( - [ ] Update imports in `ts/cli/index.ts`
'start', - [ ] Change `'../classes.ipcclient.js'``'../client/tspm.ipcclient.js'`
async (data, clientId) => { - [ ] Update imports in `ts/cli/commands/service/enable.ts`
const result = await tspmManager.start(data.config); - [ ] Change `'../../../classes.servicemanager.js'``'../../../client/tspm.servicemanager.js'`
return { success: true, processId: result.pid }; - [ ] Update imports in `ts/cli/commands/service/disable.ts`
}, - [ ] Change `'../../../classes.servicemanager.js'``'../../../client/tspm.servicemanager.js'`
); - [ ] Update imports in `ts/cli/commands/daemon/index.ts`
- [ ] Change `'../../../classes.daemon.js'``'../../../daemon/tspm.daemon.js'`
- [ ] Change `'../../../classes.ipcclient.js'``'../../../client/tspm.ipcclient.js'`
- [ ] Update imports in `ts/cli/commands/process/*.ts` files
- [ ] Change all `'../../../classes.ipcclient.js'``'../../../client/tspm.ipcclient.js'`
- [ ] Change all `'../../../classes.tspm.js'``'../../../shared/protocol/ipc.types.js'` (for types)
- [ ] Update imports in `ts/cli/registration/index.ts`
- [ ] Change `'../../classes.ipcclient.js'``'../../client/tspm.ipcclient.js'`
// CLI client setup ### Phase 9: Update Main Exports
const ipcClient = SmartIpc.createClient({ - [ ] Update `ts/index.ts`
id: 'tspm-daemon', - [ ] Remove `export * from './classes.tspm.js'`
socketPath: '~/.tspm/tspm.sock', - [ ] Remove `export * from './classes.processmonitor.js'`
}); - [ ] Remove `export * from './classes.processwrapper.js'`
- [ ] Remove `export * from './classes.daemon.js'`
- [ ] Remove `export * from './classes.ipcclient.js'`
- [ ] Remove `export * from './classes.servicemanager.js'`
- [ ] Add `export * from './client/index.js'`
- [ ] Add `export * from './shared/protocol/ipc.types.js'`
- [ ] Add `export { startDaemon } from './daemon/index.js'`
// Type-safe requests ### Phase 10: Update Package.json
const response = await ipcClient.request<StartRequest, StartResponse>('start', { - [ ] Add exports map to package.json:
config: processConfig, ```json
}); "exports": {
``` ".": "./dist_ts/client/index.js",
"./client": "./dist_ts/client/index.js",
"./daemon": "./dist_ts/daemon/index.js",
"./protocol": "./dist_ts/shared/protocol/ipc.types.js"
}
```
### Message Types ### Phase 11: TypeScript Configuration
- [ ] Create `tsconfig.base.json` with common settings
- [ ] Create `tsconfig.shared.json` for shared code
- [ ] Create `tsconfig.client.json` with reference to shared
- [ ] Create `tsconfig.daemon.json` with reference to shared
- [ ] Update main `tsconfig.json` to use references
```typescript ### Phase 12: Testing
interface StartRequest { - [ ] Run `pnpm run build` and fix any compilation errors
config: ProcessConfig; - [ ] Test daemon startup: `./cli.js daemon start`
} - [ ] Test process management: `./cli.js start "echo test"`
- [ ] Test client commands: `./cli.js list`
- [ ] Run existing tests: `pnpm test`
- [ ] Update test imports if needed
interface StartResponse { ### Phase 13: Documentation
success: boolean; - [ ] Update README.md if needed
processId?: number; - [ ] Document the new architecture in a comment at top of ts/index.ts
error?: string; - [ ] Add comments explaining the separation in each index.ts file
}
```
### Daemon State File ### Phase 14: Cleanup
- [ ] Delete empty directories from old structure
- [ ] Verify no broken imports remain
- [ ] Run linter and fix any issues
- [ ] Commit with message: "refactor(architecture): reorganize into daemon/client/shared structure"
`~/.tspm/daemon.state` - PID, socket path, version ## Benefits After Completion
### Process Management ### Immediate Benefits
- **Clear separation**: Instantly obvious what runs where (daemon vs client)
- **Smaller client bundles**: Client code won't accidentally include ProcessMonitor, ProcessWrapper, etc.
- **Better testing**: Can test client and daemon independently
- **Cleaner imports**: No more confusing `classes.` prefix on everything
Daemon maintains all ProcessMonitor instances internally, CLI never directly manages processes. ### Architecture Benefits
- **Enforced boundaries**: TypeScript project references prevent cross-imports
- **Protocol as contract**: Client and daemon can evolve independently
- **Version compatibility**: Protocol versioning allows client/daemon version skew
- **Security**: Internal daemon errors don't leak to clients over IPC
## Key Benefits ### Future Benefits
- **Browser support**: Clean client could potentially work in browser
- **Embedded mode**: Could add option to run ProcessManager in-process
- **Plugin system**: Clear boundary for plugin interfaces vs implementation
- **Multi-language clients**: Other languages only need to implement IPC protocol
### Architecture Benefits ## Implementation Safeguards (from GPT-5 Review)
- Single daemon manages all processes ### Boundary Enforcement
- Consistent state management - **TypeScript project references**: Separate tsconfig files prevent illegal imports
- Efficient resource usage - **ESLint rules**: Use `import/no-restricted-paths` to catch violations
- Better process coordination - **Package.json exports**: Control what external consumers can import
- Proper service integration with OS
### SmartIpc Advantages ### Keep Shared Minimal
- **No Node.js APIs**: No fs, net, child_process in shared
- **No environment access**: No process.env, no OS-specific code
- **Pure functions only**: Shared utilities must be environment-agnostic
- **Protocol-focused**: Mainly type definitions and constants
- **Cross-platform**: Unix sockets on Linux/macOS, named pipes on Windows ### Prevent Environment Bleed
- **Type-safe**: Full TypeScript support with generic message types - **Split paths.ts**: Constants (shared) vs OS-specific resolution (daemon)
- **Resilient**: Automatic reconnection with exponential backoff - **Plugin interfaces only**: Loading/discovery stays in daemon
- **Observable**: Built-in metrics and heartbeat monitoring - **No dynamic imports**: Keep shared statically analyzable
- **Performant**: Low-latency messaging with zero external dependencies
- **Secure**: Connection limits and message size restrictions
## Backwards Compatibility ### Future-Proofing
- **Protocol versioning**: Add version field for compatibility
- Keep existing config format - **Error codes**: Standardized errors instead of string messages
- Auto-migrate on first run - **Capability negotiation**: Client can query daemon capabilities
- Provide clear upgrade instructions - **Subpath exports**: Different entry points for different use cases
## Architecture Diagram
```
┌─────────────┐ IPC ┌──────────────┐
│ CLI │◄────────────►│ Daemon │
│ (thin client)│ Socket │ (server) │
└─────────────┘ └──────────────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ Tspm │
│ │ Manager │
│ └──────────────┘
│ │
▼ ▼
┌─────────────┐ ┌──────────────┐
│ User │ │ProcessMonitor│
│ Commands │ │ Instances │
└─────────────┘ └──────────────┘
```
## Migration Path
1. **Version 2.0.0-alpha**: Implement daemon with backwards compatibility
2. **Version 2.0.0-beta**: Deprecate `startAsDaemon`, encourage daemon mode
3. **Version 2.0.0**: Remove legacy code, daemon-only operation
4. **Documentation**: Update all examples and guides
## Security Considerations
- Unix socket permissions (user-only access)
- Validate all IPC messages
- Rate limiting for IPC requests
- Secure daemon shutdown mechanism
## Testing Requirements
- Unit tests for IPC layer
- Integration tests for daemon lifecycle
- Migration tests from current architecture
- Performance tests for multiple processes
- Stress tests for IPC communication

View File

@@ -13,18 +13,18 @@ tap.test('TspmDaemon creation', async () => {
tap.test('Daemon PID file management', async (tools) => { tap.test('Daemon PID file management', async (tools) => {
const testDir = path.join(process.cwd(), '.nogit'); const testDir = path.join(process.cwd(), '.nogit');
const testPidFile = path.join(testDir, 'test-daemon.pid'); const testPidFile = path.join(testDir, 'test-daemon.pid');
// Create directory if it doesn't exist // Create directory if it doesn't exist
await fs.mkdir(testDir, { recursive: true }); await fs.mkdir(testDir, { recursive: true });
// Clean up any existing test file // Clean up any existing test file
await fs.unlink(testPidFile).catch(() => {}); await fs.unlink(testPidFile).catch(() => {});
// Test writing PID file // Test writing PID file
await fs.writeFile(testPidFile, process.pid.toString()); await fs.writeFile(testPidFile, process.pid.toString());
const pidContent = await fs.readFile(testPidFile, 'utf-8'); const pidContent = await fs.readFile(testPidFile, 'utf-8');
expect(parseInt(pidContent)).toEqual(process.pid); expect(parseInt(pidContent)).toEqual(process.pid);
// Clean up // Clean up
await fs.unlink(testPidFile); await fs.unlink(testPidFile);
}); });
@@ -38,11 +38,11 @@ tap.test('Daemon socket path generation', async () => {
tap.test('Daemon shutdown handlers', async (tools) => { tap.test('Daemon shutdown handlers', async (tools) => {
const daemon = new TspmDaemon(); const daemon = new TspmDaemon();
// Test that shutdown handlers are registered // Test that shutdown handlers are registered
const sigintListeners = process.listeners('SIGINT'); const sigintListeners = process.listeners('SIGINT');
const sigtermListeners = process.listeners('SIGTERM'); const sigtermListeners = process.listeners('SIGTERM');
// We expect at least one listener for each signal // We expect at least one listener for each signal
// (Note: in actual test we won't start the daemon to avoid side effects) // (Note: in actual test we won't start the daemon to avoid side effects)
expect(sigintListeners.length).toBeGreaterThanOrEqual(0); expect(sigintListeners.length).toBeGreaterThanOrEqual(0);
@@ -52,7 +52,7 @@ tap.test('Daemon shutdown handlers', async (tools) => {
tap.test('Daemon process info tracking', async () => { tap.test('Daemon process info tracking', async () => {
const daemon = new TspmDaemon(); const daemon = new TspmDaemon();
const tspmInstance = (daemon as any).tspmInstance; const tspmInstance = (daemon as any).tspmInstance;
expect(tspmInstance).toBeDefined(); expect(tspmInstance).toBeDefined();
expect(tspmInstance.processes).toBeInstanceOf(Map); expect(tspmInstance.processes).toBeInstanceOf(Map);
expect(tspmInstance.processConfigs).toBeInstanceOf(Map); expect(tspmInstance.processConfigs).toBeInstanceOf(Map);
@@ -61,7 +61,7 @@ tap.test('Daemon process info tracking', async () => {
tap.test('Daemon heartbeat monitoring setup', async (tools) => { tap.test('Daemon heartbeat monitoring setup', async (tools) => {
const daemon = new TspmDaemon(); const daemon = new TspmDaemon();
// Test heartbeat interval property exists // Test heartbeat interval property exists
const heartbeatInterval = (daemon as any).heartbeatInterval; const heartbeatInterval = (daemon as any).heartbeatInterval;
expect(heartbeatInterval).toEqual(null); // Should be null before start expect(heartbeatInterval).toEqual(null); // Should be null before start
@@ -70,13 +70,13 @@ tap.test('Daemon heartbeat monitoring setup', async (tools) => {
tap.test('Daemon shutdown state management', async () => { tap.test('Daemon shutdown state management', async () => {
const daemon = new TspmDaemon(); const daemon = new TspmDaemon();
const isShuttingDown = (daemon as any).isShuttingDown; const isShuttingDown = (daemon as any).isShuttingDown;
expect(isShuttingDown).toEqual(false); expect(isShuttingDown).toEqual(false);
}); });
tap.test('Daemon memory usage reporting', async () => { tap.test('Daemon memory usage reporting', async () => {
const memUsage = process.memoryUsage(); const memUsage = process.memoryUsage();
expect(memUsage.heapUsed).toBeGreaterThan(0); expect(memUsage.heapUsed).toBeGreaterThan(0);
expect(memUsage.heapTotal).toBeGreaterThan(0); expect(memUsage.heapTotal).toBeGreaterThan(0);
expect(memUsage.rss).toBeGreaterThan(0); expect(memUsage.rss).toBeGreaterThan(0);
@@ -84,10 +84,10 @@ tap.test('Daemon memory usage reporting', async () => {
tap.test('Daemon CPU usage calculation', async () => { tap.test('Daemon CPU usage calculation', async () => {
const cpuUsage = process.cpuUsage(); const cpuUsage = process.cpuUsage();
expect(cpuUsage.user).toBeGreaterThanOrEqual(0); expect(cpuUsage.user).toBeGreaterThanOrEqual(0);
expect(cpuUsage.system).toBeGreaterThanOrEqual(0); expect(cpuUsage.system).toBeGreaterThanOrEqual(0);
// Test conversion to seconds // Test conversion to seconds
const cpuSeconds = cpuUsage.user / 1000000; const cpuSeconds = cpuUsage.user / 1000000;
expect(cpuSeconds).toBeGreaterThanOrEqual(0); expect(cpuSeconds).toBeGreaterThanOrEqual(0);
@@ -95,13 +95,13 @@ tap.test('Daemon CPU usage calculation', async () => {
tap.test('Daemon uptime calculation', async () => { 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);
expect(uptime).toBeLessThan(200); expect(uptime).toBeLessThan(200);
}); });
export default tap.start(); export default tap.start();

View File

@@ -10,7 +10,7 @@ import { tspmIpcClient } from '../ts/classes.ipcclient.js';
async function ensureDaemonStopped() { async function ensureDaemonStopped() {
try { try {
await tspmIpcClient.stopDaemon(false); await tspmIpcClient.stopDaemon(false);
await new Promise(resolve => setTimeout(resolve, 1000)); await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) { } catch (error) {
// Ignore errors if daemon is not running // Ignore errors if daemon is not running
} }
@@ -21,7 +21,7 @@ async function cleanupTestFiles() {
const tspmDir = path.join(os.homedir(), '.tspm'); const tspmDir = path.join(os.homedir(), '.tspm');
const pidFile = path.join(tspmDir, 'daemon.pid'); const pidFile = path.join(tspmDir, 'daemon.pid');
const socketFile = path.join(tspmDir, 'tspm.sock'); const socketFile = path.join(tspmDir, 'tspm.sock');
await fs.unlink(pidFile).catch(() => {}); await fs.unlink(pidFile).catch(() => {});
await fs.unlink(socketFile).catch(() => {}); await fs.unlink(socketFile).catch(() => {});
} }
@@ -29,55 +29,55 @@ async function cleanupTestFiles() {
// Integration tests for daemon-client communication // Integration tests for daemon-client communication
tap.test('Full daemon lifecycle test', async (tools) => { tap.test('Full daemon lifecycle test', async (tools) => {
const done = tools.defer(); const done = tools.defer();
// Ensure clean state // Ensure clean state
await ensureDaemonStopped(); await ensureDaemonStopped();
await cleanupTestFiles(); await cleanupTestFiles();
// Test 1: Check daemon is not running // Test 1: Check daemon is not running
let status = await tspmIpcClient.getDaemonStatus(); let status = await tspmIpcClient.getDaemonStatus();
expect(status).toEqual(null); expect(status).toEqual(null);
// Test 2: Start daemon // Test 2: Start daemon
console.log('Starting daemon...'); console.log('Starting daemon...');
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();
expect(status).toBeDefined(); expect(status).toBeDefined();
expect(status?.status).toEqual('running'); expect(status?.status).toEqual('running');
expect(status?.pid).toBeGreaterThan(0); expect(status?.pid).toBeGreaterThan(0);
expect(status?.processCount).toBeGreaterThanOrEqual(0); expect(status?.processCount).toBeGreaterThanOrEqual(0);
// Test 4: Stop daemon // Test 4: Stop daemon
console.log('Stopping daemon...'); console.log('Stopping daemon...');
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();
expect(status).toEqual(null); expect(status).toEqual(null);
done.resolve(); done.resolve();
}); });
tap.test('Process management through daemon', async (tools) => { tap.test('Process management through daemon', async (tools) => {
const done = tools.defer(); const done = tools.defer();
// 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', {});
expect(listResponse.processes).toBeArray(); expect(listResponse.processes).toBeArray();
expect(listResponse.processes.length).toEqual(0); expect(listResponse.processes.length).toEqual(0);
// Test 2: Start a test process // Test 2: Start a test process
const testConfig: tspm.IProcessConfig = { const testConfig: tspm.IProcessConfig = {
id: 'test-echo', id: 'test-echo',
@@ -87,52 +87,60 @@ tap.test('Process management through daemon', async (tools) => {
memoryLimitBytes: 50 * 1024 * 1024, memoryLimitBytes: 50 * 1024 * 1024,
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();
// Test 3: List processes (should have one process) // Test 3: List processes (should have one process)
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');
// Test 5: Stop the process // Test 5: Stop the process
const stopResponse = await tspmIpcClient.request('stop', { id: 'test-echo' }); const stopResponse = await tspmIpcClient.request('stop', { id: 'test-echo' });
expect(stopResponse.success).toEqual(true); expect(stopResponse.success).toEqual(true);
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
await tspmIpcClient.stopDaemon(true); await tspmIpcClient.stopDaemon(true);
done.resolve(); done.resolve();
}); });
tap.test('Batch operations through daemon', async (tools) => { tap.test('Batch operations through daemon', async (tools) => {
const done = tools.defer(); const done = tools.defer();
// 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[] = [
{ {
@@ -152,43 +160,43 @@ tap.test('Batch operations through daemon', async (tools) => {
autorestart: false, autorestart: false,
}, },
]; ];
// Start processes // Start processes
for (const config of testConfigs) { for (const config of testConfigs) {
await tspmIpcClient.request('start', { config }); await tspmIpcClient.request('start', { config });
} }
// Test 1: Stop all processes // Test 1: Stop all processes
const stopAllResponse = await tspmIpcClient.request('stopAll', {}); const stopAllResponse = await tspmIpcClient.request('stopAll', {});
expect(stopAllResponse.stopped).toBeArray(); expect(stopAllResponse.stopped).toBeArray();
expect(stopAllResponse.stopped.length).toBeGreaterThanOrEqual(2); expect(stopAllResponse.stopped.length).toBeGreaterThanOrEqual(2);
// Test 2: Start all processes // Test 2: Start all processes
const startAllResponse = await tspmIpcClient.request('startAll', {}); const startAllResponse = await tspmIpcClient.request('startAll', {});
expect(startAllResponse.started).toBeArray(); expect(startAllResponse.started).toBeArray();
// Test 3: Restart all processes // Test 3: Restart all processes
const restartAllResponse = await tspmIpcClient.request('restartAll', {}); const restartAllResponse = await tspmIpcClient.request('restartAll', {});
expect(restartAllResponse.restarted).toBeArray(); expect(restartAllResponse.restarted).toBeArray();
// Cleanup: delete all test processes // Cleanup: delete all test processes
for (const config of testConfigs) { for (const config of testConfigs) {
await tspmIpcClient.request('delete', { id: config.id }).catch(() => {}); await tspmIpcClient.request('delete', { id: config.id }).catch(() => {});
} }
// Stop daemon // Stop daemon
await tspmIpcClient.stopDaemon(true); await tspmIpcClient.stopDaemon(true);
done.resolve(); done.resolve();
}); });
tap.test('Daemon error handling', async (tools) => { tap.test('Daemon error handling', async (tools) => {
const done = tools.defer(); const done = tools.defer();
// 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 {
await tspmIpcClient.request('stop', { id: 'non-existent-process' }); await tspmIpcClient.request('stop', { id: 'non-existent-process' });
@@ -196,7 +204,7 @@ tap.test('Daemon error handling', async (tools) => {
} catch (error) { } catch (error) {
expect(error.message).toInclude('Failed to stop process'); expect(error.message).toInclude('Failed to stop process');
} }
// Test 2: Try to describe non-existent process // Test 2: Try to describe non-existent process
try { try {
await tspmIpcClient.request('describe', { id: 'non-existent-process' }); await tspmIpcClient.request('describe', { id: 'non-existent-process' });
@@ -204,7 +212,7 @@ tap.test('Daemon error handling', async (tools) => {
} catch (error) { } catch (error) {
expect(error.message).toInclude('not found'); expect(error.message).toInclude('not found');
} }
// Test 3: Try to restart non-existent process // Test 3: Try to restart non-existent process
try { try {
await tspmIpcClient.request('restart', { id: 'non-existent-process' }); await tspmIpcClient.request('restart', { id: 'non-existent-process' });
@@ -212,48 +220,48 @@ tap.test('Daemon error handling', async (tools) => {
} catch (error) { } catch (error) {
expect(error.message).toInclude('Failed to restart process'); expect(error.message).toInclude('Failed to restart process');
} }
// Stop daemon // Stop daemon
await tspmIpcClient.stopDaemon(true); await tspmIpcClient.stopDaemon(true);
done.resolve(); done.resolve();
}); });
tap.test('Daemon heartbeat functionality', async (tools) => { tap.test('Daemon heartbeat functionality', async (tools) => {
const done = tools.defer(); const done = tools.defer();
// 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', {});
expect(heartbeatResponse.timestamp).toBeGreaterThan(0); expect(heartbeatResponse.timestamp).toBeGreaterThan(0);
expect(heartbeatResponse.status).toEqual('healthy'); expect(heartbeatResponse.status).toEqual('healthy');
// Stop daemon // Stop daemon
await tspmIpcClient.stopDaemon(true); await tspmIpcClient.stopDaemon(true);
done.resolve(); done.resolve();
}); });
tap.test('Daemon memory and CPU reporting', async (tools) => { tap.test('Daemon memory and CPU reporting', async (tools) => {
const done = tools.defer(); const done = tools.defer();
// 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();
expect(status).toBeDefined(); expect(status).toBeDefined();
expect(status?.memoryUsage).toBeGreaterThan(0); expect(status?.memoryUsage).toBeGreaterThan(0);
expect(status?.cpuUsage).toBeGreaterThanOrEqual(0); expect(status?.cpuUsage).toBeGreaterThanOrEqual(0);
expect(status?.uptime).toBeGreaterThan(0); expect(status?.uptime).toBeGreaterThan(0);
// Stop daemon // Stop daemon
await tspmIpcClient.stopDaemon(true); await tspmIpcClient.stopDaemon(true);
done.resolve(); done.resolve();
}); });
@@ -263,4 +271,4 @@ tap.test('Final cleanup', async () => {
await cleanupTestFiles(); await cleanupTestFiles();
}); });
export default tap.start(); export default tap.start();

View File

@@ -14,7 +14,7 @@ tap.test('TspmIpcClient creation', async () => {
tap.test('IPC client socket path', async () => { tap.test('IPC client socket path', async () => {
const client = new TspmIpcClient(); const client = new TspmIpcClient();
const socketPath = (client as any).socketPath; const socketPath = (client as any).socketPath;
expect(socketPath).toInclude('.tspm'); expect(socketPath).toInclude('.tspm');
expect(socketPath).toInclude('tspm.sock'); expect(socketPath).toInclude('tspm.sock');
}); });
@@ -22,7 +22,7 @@ tap.test('IPC client socket path', async () => {
tap.test('IPC client daemon PID file path', async () => { tap.test('IPC client daemon PID file path', async () => {
const client = new TspmIpcClient(); const client = new TspmIpcClient();
const daemonPidFile = (client as any).daemonPidFile; const daemonPidFile = (client as any).daemonPidFile;
expect(daemonPidFile).toInclude('.tspm'); expect(daemonPidFile).toInclude('.tspm');
expect(daemonPidFile).toInclude('daemon.pid'); expect(daemonPidFile).toInclude('daemon.pid');
}); });
@@ -30,7 +30,7 @@ tap.test('IPC client daemon PID file path', async () => {
tap.test('IPC client connection state', async () => { tap.test('IPC client connection state', async () => {
const client = new TspmIpcClient(); const client = new TspmIpcClient();
const isConnected = (client as any).isConnected; const isConnected = (client as any).isConnected;
expect(isConnected).toEqual(false); // Should be false initially expect(isConnected).toEqual(false); // Should be false initially
}); });
@@ -38,10 +38,10 @@ tap.test('IPC client daemon running check - no daemon', async () => {
const client = new TspmIpcClient(); const client = new TspmIpcClient();
const tspmDir = path.join(os.homedir(), '.tspm'); const tspmDir = path.join(os.homedir(), '.tspm');
const pidFile = path.join(tspmDir, 'daemon.pid'); const pidFile = path.join(tspmDir, 'daemon.pid');
// Ensure no PID file exists for this test // Ensure no PID file exists for this test
await fs.unlink(pidFile).catch(() => {}); await fs.unlink(pidFile).catch(() => {});
const isRunning = await (client as any).isDaemonRunning(); const isRunning = await (client as any).isDaemonRunning();
expect(isRunning).toEqual(false); expect(isRunning).toEqual(false);
}); });
@@ -50,18 +50,21 @@ tap.test('IPC client daemon running check - stale PID', async () => {
const client = new TspmIpcClient(); const client = new TspmIpcClient();
const tspmDir = path.join(os.homedir(), '.tspm'); const tspmDir = path.join(os.homedir(), '.tspm');
const pidFile = path.join(tspmDir, 'daemon.pid'); const pidFile = path.join(tspmDir, 'daemon.pid');
// Create directory if it doesn't exist // Create directory if it doesn't exist
await fs.mkdir(tspmDir, { recursive: true }); await fs.mkdir(tspmDir, { recursive: true });
// Write a fake PID that doesn't exist // Write a fake PID that doesn't exist
await fs.writeFile(pidFile, '99999999'); await fs.writeFile(pidFile, '99999999');
const isRunning = await (client as any).isDaemonRunning(); const isRunning = await (client as any).isDaemonRunning();
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);
}); });
@@ -70,19 +73,19 @@ tap.test('IPC client daemon running check - current process', async () => {
const tspmDir = path.join(os.homedir(), '.tspm'); const tspmDir = path.join(os.homedir(), '.tspm');
const pidFile = path.join(tspmDir, 'daemon.pid'); const pidFile = path.join(tspmDir, 'daemon.pid');
const socketFile = path.join(tspmDir, 'tspm.sock'); const socketFile = path.join(tspmDir, 'tspm.sock');
// Create directory if it doesn't exist // Create directory if it doesn't exist
await fs.mkdir(tspmDir, { recursive: true }); await fs.mkdir(tspmDir, { recursive: true });
// Write current process PID (simulating daemon is this process) // Write current process PID (simulating daemon is this process)
await fs.writeFile(pidFile, process.pid.toString()); await fs.writeFile(pidFile, process.pid.toString());
// Create a fake socket file // Create a fake socket file
await fs.writeFile(socketFile, ''); await fs.writeFile(socketFile, '');
const isRunning = await (client as any).isDaemonRunning(); const isRunning = await (client as any).isDaemonRunning();
expect(isRunning).toEqual(true); expect(isRunning).toEqual(true);
// Clean up // Clean up
await fs.unlink(pidFile).catch(() => {}); await fs.unlink(pidFile).catch(() => {});
await fs.unlink(socketFile).catch(() => {}); await fs.unlink(socketFile).catch(() => {});
@@ -91,17 +94,19 @@ tap.test('IPC client daemon running check - current process', async () => {
tap.test('IPC client singleton instance', async () => { tap.test('IPC client singleton instance', async () => {
// Import the singleton // Import the singleton
const { tspmIpcClient } = await import('../ts/classes.ipcclient.js'); const { tspmIpcClient } = await import('../ts/classes.ipcclient.js');
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);
}); });
tap.test('IPC client request method type safety', async () => { tap.test('IPC client request method type safety', async () => {
const client = new TspmIpcClient(); const client = new TspmIpcClient();
// Test that request method exists // Test that request method exists
expect(client.request).toBeInstanceOf(Function); expect(client.request).toBeInstanceOf(Function);
expect(client.connect).toBeInstanceOf(Function); expect(client.connect).toBeInstanceOf(Function);
@@ -111,17 +116,18 @@ 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');
}); });
tap.test('IPC client reconnection logic', async () => { tap.test('IPC client reconnection logic', async () => {
const client = new TspmIpcClient(); const client = new TspmIpcClient();
// Test reconnection error conditions // Test reconnection error conditions
const econnrefusedError = new Error('ECONNREFUSED'); const econnrefusedError = new Error('ECONNREFUSED');
expect(econnrefusedError.message).toInclude('ECONNREFUSED'); expect(econnrefusedError.message).toInclude('ECONNREFUSED');
const enoentError = new Error('ENOENT'); const enoentError = new Error('ENOENT');
expect(enoentError.message).toInclude('ENOENT'); expect(enoentError.message).toInclude('ENOENT');
}); });
@@ -129,7 +135,7 @@ tap.test('IPC client reconnection logic', async () => {
tap.test('IPC client daemon start timeout', async () => { tap.test('IPC client daemon start timeout', async () => {
const maxWaitTime = 10000; // 10 seconds const maxWaitTime = 10000; // 10 seconds
const checkInterval = 500; // 500ms const checkInterval = 500; // 500ms
const maxChecks = maxWaitTime / checkInterval; const maxChecks = maxWaitTime / checkInterval;
expect(maxChecks).toEqual(20); expect(maxChecks).toEqual(20);
}); });
@@ -137,9 +143,9 @@ tap.test('IPC client daemon start timeout', async () => {
tap.test('IPC client daemon stop timeout', async () => { tap.test('IPC client daemon stop timeout', async () => {
const maxWaitTime = 15000; // 15 seconds const maxWaitTime = 15000; // 15 seconds
const checkInterval = 500; // 500ms const checkInterval = 500; // 500ms
const maxChecks = maxWaitTime / checkInterval; const maxChecks = maxWaitTime / checkInterval;
expect(maxChecks).toEqual(30); expect(maxChecks).toEqual(30);
}); });
export default tap.start(); export default tap.start();

View File

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

View File

@@ -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);

View File

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

640
ts/cli.ts
View File

@@ -1,638 +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>');
return;
}
const lines = argvArg.lines || 50;
const response = await tspmIpcClient.request('getLogs', { id, lines });
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]' : '[ERR]';
console.log(`${timestamp} ${prefix} ${log.message}`);
}
} 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();
};

View File

@@ -0,0 +1,31 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerRestartAllCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'restart-all',
async (_argvArg: CliArguments) => {
console.log('Restarting all processes...');
const response = await tspmIpcClient.request('restartAll', {});
if (response.restarted.length > 0) {
console.log(`✓ Restarted ${response.restarted.length} processes:`);
for (const id of response.restarted) {
console.log(` - ${id}`);
}
}
if (response.failed.length > 0) {
console.log(`✗ Failed to restart ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
process.exitCode = 1; // Signal partial failure
}
},
{ actionLabel: 'restart all processes' },
);
}

View File

@@ -0,0 +1,31 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStartAllCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'start-all',
async (_argvArg: CliArguments) => {
console.log('Starting all processes...');
const response = await tspmIpcClient.request('startAll', {});
if (response.started.length > 0) {
console.log(`✓ Started ${response.started.length} processes:`);
for (const id of response.started) {
console.log(` - ${id}`);
}
}
if (response.failed.length > 0) {
console.log(`✗ Failed to start ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
process.exitCode = 1; // Signal partial failure
}
},
{ actionLabel: 'start all processes' },
);
}

View File

@@ -0,0 +1,31 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStopAllCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'stop-all',
async (_argvArg: CliArguments) => {
console.log('Stopping all processes...');
const response = await tspmIpcClient.request('stopAll', {});
if (response.stopped.length > 0) {
console.log(`✓ Stopped ${response.stopped.length} processes:`);
for (const id of response.stopped) {
console.log(` - ${id}`);
}
}
if (response.failed.length > 0) {
console.log(`✗ Failed to stop ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
process.exitCode = 1; // Signal partial failure
}
},
{ actionLabel: 'stop all processes' },
);
}

View File

@@ -0,0 +1,148 @@
import * as plugins from '../../../plugins.js';
import * as paths from '../../../paths.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import { Logger } from '../../../utils.errorhandler.js';
import type { CliArguments } from '../../types.js';
import { formatMemory } from '../../helpers/memory.js';
export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
const cliLogger = new Logger('CLI');
smartcli.addCommand('daemon').subscribe({
next: async (argvArg: CliArguments) => {
const subCommand = argvArg._[1];
switch (subCommand) {
case 'start':
try {
const status = await tspmIpcClient.getDaemonStatus();
if (status) {
console.log('TSPM daemon is already running');
console.log(` PID: ${status.pid}`);
console.log(
` Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
);
console.log(` Processes: ${status.processCount}`);
return;
}
console.log('Starting TSPM daemon manually...');
// Import spawn to start daemon process
const { spawn } = await import('child_process');
const daemonScript = plugins.path.join(
paths.packageDir,
'dist_ts',
'daemon.js',
);
// Start daemon as a detached background process
// Use 'inherit' for stdio to see any startup errors when debugging
const daemonProcess = spawn(process.execPath, [daemonScript], {
detached: true,
stdio: process.env.TSPM_DEBUG === 'true' ? 'inherit' : 'ignore',
env: {
...process.env,
TSPM_DAEMON_MODE: 'true',
},
});
// Detach the daemon so it continues running after CLI exits
daemonProcess.unref();
console.log(`Started daemon with PID: ${daemonProcess.pid}`);
// Wait for daemon to be ready
await new Promise((resolve) => setTimeout(resolve, 2000));
const newStatus = await tspmIpcClient.getDaemonStatus();
if (newStatus) {
console.log('✓ TSPM daemon started successfully');
console.log(` PID: ${newStatus.pid}`);
console.log(
'\nNote: This daemon will run until you stop it or logout.',
);
console.log('For automatic startup, use "tspm enable" instead.');
} else {
console.warn('\n⚠ Warning: Daemon process started but is not responding.');
console.log('The daemon may have crashed on startup.');
console.log('\nTo debug, try:');
console.log(' TSPM_DEBUG=true tspm daemon start');
console.log('\nOr check if the socket file exists:');
console.log(` ls -la ~/.tspm/tspm.sock`);
}
// Disconnect from the daemon after starting
await tspmIpcClient.disconnect();
} catch (error) {
console.error('Error starting daemon:', error.message);
process.exit(1);
}
break;
case 'start-service':
// This is called by systemd - start the daemon directly
console.log('Starting TSPM daemon for systemd service...');
const { startDaemon } = await import('../../../classes.daemon.js');
await startDaemon();
break;
case 'stop':
try {
console.log('Stopping TSPM daemon...');
await tspmIpcClient.stopDaemon(true);
console.log('✓ TSPM daemon stopped successfully');
// Disconnect from the daemon after stopping
await tspmIpcClient.disconnect();
} catch (error) {
console.error('Error stopping daemon:', error.message);
process.exit(1);
}
break;
case 'status':
try {
const status = await tspmIpcClient.getDaemonStatus();
if (!status) {
console.log('TSPM daemon is not running');
console.log('Use "tspm daemon start" to start it');
return;
}
console.log('TSPM Daemon Status:');
console.log('─'.repeat(40));
console.log(`Status: ${status.status}`);
console.log(`PID: ${status.pid}`);
console.log(
`Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
);
console.log(`Processes: ${status.processCount}`);
console.log(
`Memory: ${formatMemory(status.memoryUsage || 0)}`,
);
console.log(`CPU: ${status.cpuUsage?.toFixed(1) || 0}s`);
// Disconnect from daemon after getting status
await tspmIpcClient.disconnect();
} catch (error) {
console.error('Error getting daemon status:', error.message);
process.exit(1);
}
break;
default:
console.log('Usage: tspm daemon <command>');
console.log('\nCommands:');
console.log(' start Start the TSPM daemon');
console.log(' stop Stop the TSPM daemon');
console.log(' status Show daemon status');
break;
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
}

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

@@ -0,0 +1,102 @@
import * as plugins from '../../plugins.js';
import * as paths from '../../paths.js';
import { tspmIpcClient } from '../../classes.ipcclient.js';
import { Logger } from '../../utils.errorhandler.js';
import type { CliArguments } from '../types.js';
import { pad } from '../helpers/formatting.js';
import { formatMemory } from '../helpers/memory.js';
export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
const cliLogger = new Logger('CLI');
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
smartcli.standardCommand().subscribe({
next: async (argvArg: CliArguments) => {
console.log(
`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`,
);
console.log('Usage: tspm [command] [options]');
console.log('\nService Management:');
console.log(
' enable Enable TSPM as system service (systemd)',
);
console.log(' disable Disable TSPM system service');
console.log('\nProcess Commands:');
console.log(' start <script> Start a process');
console.log(' list List all processes');
console.log(' stop <id> Stop a process');
console.log(' restart <id> Restart a process');
console.log(' delete <id> Delete a process');
console.log(' describe <id> Show details for a process');
console.log(' logs <id> Show logs for a process');
console.log(' start-all Start all saved processes');
console.log(' stop-all Stop all processes');
console.log(' restart-all Restart all processes');
console.log('\nDaemon Commands:');
console.log(
' daemon start Start daemon manually (current session)',
);
console.log(' daemon stop Stop the daemon');
console.log(' daemon status Show daemon status');
console.log(
'\nUse tspm [command] --help for more information about a command.',
);
// Show current process list
console.log('\nProcess List:');
try {
const response = await tspmIpcClient.request('list', {});
const processes = response.processes;
if (processes.length === 0) {
console.log(
' No processes running. Use "tspm start" to start a process.',
);
} else {
console.log(
'┌─────────┬─────────────┬───────────┬───────────┬──────────┐',
);
console.log(
'│ ID │ Name │ Status │ Memory │ Restarts │',
);
console.log(
'├─────────┼─────────────┼───────────┼───────────┼──────────┤',
);
for (const proc of processes) {
const statusColor =
proc.status === 'online'
? '\x1b[32m'
: proc.status === 'errored'
? '\x1b[31m'
: '\x1b[33m';
const resetColor = '\x1b[0m';
console.log(
`${pad(proc.id, 7)}${pad(proc.id, 11)}${statusColor}${pad(proc.status, 9)}${resetColor}${pad(formatMemory(proc.memory), 9)}${pad(proc.restarts.toString(), 8)}`,
);
}
console.log(
'└─────────┴─────────────┴───────────┴───────────┴──────────┘',
);
}
// Disconnect from daemon after getting list
await tspmIpcClient.disconnect();
} catch (error) {
console.error('Error: TSPM daemon is not running.');
console.log('\nTo start the daemon, run one of:');
console.log(' tspm daemon start - Start for this session only');
console.log(
' tspm enable - Enable as system service (recommended)',
);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
}

View File

@@ -0,0 +1,29 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'delete',
async (argvArg: CliArguments) => {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm delete <id>');
return;
}
console.log(`Deleting process: ${id}`);
const response = await tspmIpcClient.request('delete', { id });
if (response.success) {
console.log(`${response.message}`);
} else {
console.error(`✗ Failed to delete process: ${response.message}`);
}
},
{ actionLabel: 'delete process' },
);
}

View File

@@ -0,0 +1,49 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
import { formatMemory } from '../../helpers/memory.js';
export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'describe',
async (argvArg: CliArguments) => {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm describe <id>');
return;
}
const response = await tspmIpcClient.request('describe', { id });
console.log(`Process Details: ${id}`);
console.log('─'.repeat(40));
console.log(`Status: ${response.processInfo.status}`);
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
console.log(`Memory: ${formatMemory(response.processInfo.memory)}`);
console.log(
`CPU: ${response.processInfo.cpu ? response.processInfo.cpu.toFixed(1) + '%' : 'N/A'}`,
);
console.log(
`Uptime: ${response.processInfo.uptime ? Math.floor(response.processInfo.uptime / 1000) + 's' : 'N/A'}`,
);
console.log(`Restarts: ${response.processInfo.restarts}`);
console.log('\nConfiguration:');
console.log(`Command: ${response.config.command}`);
console.log(`Directory: ${response.config.projectDir}`);
console.log(
`Memory Limit: ${formatMemory(response.config.memoryLimitBytes)}`,
);
console.log(`Auto-restart: ${response.config.autorestart}`);
if (response.config.watch) {
console.log(`Watch: enabled`);
if (response.config.watchPaths) {
console.log(`Watch Paths: ${response.config.watchPaths.join(', ')}`);
}
}
},
{ actionLabel: 'describe process' },
);
}

View File

@@ -0,0 +1,52 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
import { pad } from '../../helpers/formatting.js';
import { formatMemory } from '../../helpers/memory.js';
export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'list',
async (_argvArg: CliArguments) => {
const response = await tspmIpcClient.request('list', {});
const processes = response.processes;
if (processes.length === 0) {
console.log('No processes running.');
return;
}
console.log('Process List:');
console.log(
'┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐',
);
console.log(
'│ ID │ Name │ Status │ PID │ Memory │ Restarts │',
);
console.log(
'├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤',
);
for (const proc of processes) {
const statusColor =
proc.status === 'online'
? '\x1b[32m'
: proc.status === 'errored'
? '\x1b[31m'
: '\x1b[33m';
const resetColor = '\x1b[0m';
console.log(
`${pad(proc.id, 7)}${pad(proc.id, 11)}${statusColor}${pad(proc.status, 9)}${resetColor}${pad((proc.pid || '-').toString(), 9)}${pad(formatMemory(proc.memory), 8)}${pad(proc.restarts.toString(), 8)}`,
);
}
console.log(
'└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘',
);
},
{ actionLabel: 'list processes' },
);
}

View File

@@ -0,0 +1,99 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
import { getBool, getNumber } from '../../helpers/argv.js';
import { formatLog } from '../../helpers/formatting.js';
import { withStreamingLifecycle } from '../../helpers/lifecycle.js';
export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'logs',
async (argvArg: CliArguments) => {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm logs <id> [options]');
console.log('\nOptions:');
console.log(' --lines <n> Number of lines to show (default: 50)');
console.log(' --follow Stream logs in real-time (like tail -f)');
return;
}
const lines = getNumber(argvArg, 'lines', 50);
const follow = getBool(argvArg, 'follow', 'f');
const response = await tspmIpcClient.request('getLogs', { id, lines });
if (!follow) {
// One-shot mode - auto-disconnect handled by registerIpcCommand
console.log(`Logs for process: ${id} (last ${lines} lines)`);
console.log('─'.repeat(60));
for (const log of response.logs) {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix =
log.type === 'stdout'
? '[OUT]'
: log.type === 'stderr'
? '[ERR]'
: '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
}
return;
}
// Streaming mode
console.log(`Logs for process: ${id} (streaming...)`);
console.log('─'.repeat(60));
let lastSeq = 0;
for (const log of response.logs) {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix =
log.type === 'stdout'
? '[OUT]'
: log.type === 'stderr'
? '[ERR]'
: '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
if (log.seq !== undefined) lastSeq = Math.max(lastSeq, log.seq);
}
await withStreamingLifecycle(
async () => {
await tspmIpcClient.subscribe(id, (log: any) => {
if (log.seq !== undefined && log.seq <= lastSeq) return;
if (log.seq !== undefined && log.seq > lastSeq + 1) {
console.log(
`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`,
);
}
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix =
log.type === 'stdout'
? '[OUT]'
: log.type === 'stderr'
? '[ERR]'
: '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
if (log.seq !== undefined) lastSeq = log.seq;
});
},
async () => {
console.log('\n\nStopping log stream...');
try {
await tspmIpcClient.unsubscribe(id);
} catch {}
try {
await tspmIpcClient.disconnect();
} catch {}
},
);
},
{
actionLabel: 'get logs',
keepAlive: (argv) => getBool(argv, 'follow', 'f'),
},
);
}

View File

@@ -0,0 +1,28 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'restart',
async (argvArg: CliArguments) => {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm restart <id>');
return;
}
console.log(`Restarting process: ${id}`);
const response = await tspmIpcClient.request('restart', { id });
console.log(`✓ Process restarted successfully`);
console.log(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`);
},
{ actionLabel: 'restart process' },
);
}

View File

@@ -0,0 +1,121 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../client/tspm.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) => {
// Get all arguments after 'start' command
const commandArgs = argvArg._.slice(1);
if (commandArgs.length === 0) {
console.error('Error: Please provide a command to run');
console.log('Usage: tspm start <command> [options]');
console.log('\nExamples:');
console.log(' tspm start "npm run dev"');
console.log(' tspm start pnpm start');
console.log(' tspm start node server.js');
console.log(' tspm start script.ts');
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;
}
// Join all command parts to form the full command
const script = commandArgs.join(' ');
const memoryLimit = argvArg.memory
? parseMemoryString(argvArg.memory)
: 512 * 1024 * 1024;
const projectDir = argvArg.cwd || process.cwd();
// Parse the command to determine if we need to handle .ts files
let actualCommand: string;
let processArgs: string[] | undefined = undefined;
// Split the script to check if it's a single .ts file or a full command
const scriptParts = script.split(' ');
const firstPart = scriptParts[0];
// Check if this is a direct .ts file execution (single argument ending in .ts)
if (scriptParts.length === 1 && firstPart.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(firstPart)
? firstPart
: plugins.path.join(projectDir, firstPart);
actualCommand = tsxPath;
processArgs = [scriptPath];
} catch {
actualCommand = 'tsx';
processArgs = [firstPart];
}
} else {
// For multi-word commands, use the entire script as the command
// This handles cases like "pnpm start", "npm run dev", etc.
actualCommand = script;
processArgs = undefined;
}
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: processArgs,
projectDir,
memoryLimitBytes: memoryLimit,
autorestart,
watch,
watchPaths,
};
console.log(`Starting process: ${name}`);
console.log(
` Command: ${script}${scriptParts.length === 1 && firstPart.endsWith('.ts') ? ' (via tsx)' : ''}`,
);
console.log(` Directory: ${projectDir}`);
console.log(` Memory limit: ${formatMemory(memoryLimit)}`);
console.log(` Auto-restart: ${autorestart}`);
if (watch) {
console.log(` Watch mode: enabled`);
if (watchPaths) console.log(` Watch paths: ${watchPaths.join(', ')}`);
}
const response = await tspmIpcClient.request('start', {
config: processConfig,
});
console.log(`✓ Process started successfully`);
console.log(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`);
},
{ actionLabel: 'start process' },
);
}

View File

@@ -0,0 +1,29 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStopCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'stop',
async (argvArg: CliArguments) => {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm stop <id>');
return;
}
console.log(`Stopping process: ${id}`);
const response = await tspmIpcClient.request('stop', { id });
if (response.success) {
console.log(`${response.message}`);
} else {
console.error(`✗ Failed to stop process: ${response.message}`);
}
},
{ actionLabel: 'stop process' },
);
}

View File

@@ -0,0 +1,36 @@
import * as plugins from '../../../plugins.js';
import { TspmServiceManager } from '../../../classes.servicemanager.js';
import { Logger } from '../../../utils.errorhandler.js';
import type { CliArguments } from '../../types.js';
export function registerDisableCommand(smartcli: plugins.smartcli.Smartcli) {
const cliLogger = new Logger('CLI');
smartcli.addCommand('disable').subscribe({
next: async (argvArg: CliArguments) => {
try {
const serviceManager = new TspmServiceManager();
console.log('Disabling TSPM daemon service...');
await serviceManager.disableService();
console.log('✓ TSPM daemon service disabled');
console.log(' The daemon will no longer start on system boot');
console.log(' Use "tspm enable" to re-enable the service');
} catch (error) {
console.error('Error disabling service:', error.message);
if (
error.message.includes('permission') ||
error.message.includes('denied')
) {
console.log('\nNote: You may need to run this command with sudo');
}
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
}

View File

@@ -0,0 +1,36 @@
import * as plugins from '../../../plugins.js';
import { TspmServiceManager } from '../../../classes.servicemanager.js';
import { Logger } from '../../../utils.errorhandler.js';
import type { CliArguments } from '../../types.js';
export function registerEnableCommand(smartcli: plugins.smartcli.Smartcli) {
const cliLogger = new Logger('CLI');
smartcli.addCommand('enable').subscribe({
next: async (argvArg: CliArguments) => {
try {
const serviceManager = new TspmServiceManager();
console.log('Enabling TSPM daemon as system service...');
await serviceManager.enableService();
console.log('✓ TSPM daemon enabled and started as system service');
console.log(' The daemon will now start automatically on system boot');
console.log(' Use "tspm disable" to remove the service');
} catch (error) {
console.error('Error enabling service:', error.message);
if (
error.message.includes('permission') ||
error.message.includes('denied')
) {
console.log('\nNote: You may need to run this command with sudo');
}
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
}

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

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

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

@@ -0,0 +1,18 @@
// Helper function to handle daemon connection errors
export function handleDaemonError(error: any, action: string): void {
if (
error.message?.includes('daemon is not running') ||
error.message?.includes('Not connected') ||
error.message?.includes('ECONNREFUSED')
) {
console.error(`Error: Cannot ${action} - TSPM daemon is not running.`);
console.log('\nTo start the daemon, run one of:');
console.log(' tspm daemon start - Start for this session only');
console.log(
' tspm enable - Enable as system service (recommended)',
);
} else {
console.error(`Error ${action}:`, error.message);
}
process.exit(1);
}

View File

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

View File

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

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

@@ -0,0 +1,33 @@
// Helper function to parse memory strings (e.g., "512MB", "2GB")
export function parseMemoryString(memStr: string): number {
const units = {
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
};
const match = memStr.toUpperCase().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/);
if (!match) {
throw new Error(
`Invalid memory format: ${memStr}. Use format like "512MB" or "2GB"`,
);
}
const value = parseFloat(match[1]);
const unit = (match[2] || 'MB') as keyof typeof units;
return Math.floor(value * units[unit]);
}
// Helper function to format memory for display
export function formatMemory(bytes: number): string {
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
} else if (bytes >= 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
} else if (bytes >= 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
} else {
return `${bytes} B`;
}
}

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

@@ -0,0 +1,68 @@
import * as plugins from '../plugins.js';
import * as paths from '../paths.js';
import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js';
// Import command registration functions
import { registerDefaultCommand } from './commands/default.js';
import { registerStartCommand } from './commands/process/start.js';
import { registerStopCommand } from './commands/process/stop.js';
import { registerRestartCommand } from './commands/process/restart.js';
import { registerDeleteCommand } from './commands/process/delete.js';
import { registerListCommand } from './commands/process/list.js';
import { registerDescribeCommand } from './commands/process/describe.js';
import { registerLogsCommand } from './commands/process/logs.js';
import { registerStartAllCommand } from './commands/batch/start-all.js';
import { registerStopAllCommand } from './commands/batch/stop-all.js';
import { registerRestartAllCommand } from './commands/batch/restart-all.js';
import { registerDaemonCommand } from './commands/daemon/index.js';
import { registerEnableCommand } from './commands/service/enable.js';
import { registerDisableCommand } from './commands/service/disable.js';
// Export types for external use
export type { CliArguments } from './types.js';
/**
* Main CLI entry point
*/
export const run = async (): Promise<void> => {
const cliLogger = new Logger('CLI');
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
// Check if debug mode is enabled
const debugMode = process.env.TSPM_DEBUG === 'true';
if (debugMode) {
cliLogger.setLevel(LogLevel.DEBUG);
cliLogger.debug('Debug mode enabled');
}
const smartcliInstance = new plugins.smartcli.Smartcli();
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
// Register all commands
// Default command (help + list)
registerDefaultCommand(smartcliInstance);
// Process commands
registerStartCommand(smartcliInstance);
registerStopCommand(smartcliInstance);
registerRestartCommand(smartcliInstance);
registerDeleteCommand(smartcliInstance);
registerListCommand(smartcliInstance);
registerDescribeCommand(smartcliInstance);
registerLogsCommand(smartcliInstance);
// Batch commands
registerStartAllCommand(smartcliInstance);
registerStopAllCommand(smartcliInstance);
registerRestartAllCommand(smartcliInstance);
// Daemon commands
registerDaemonCommand(smartcliInstance);
// Service commands
registerEnableCommand(smartcliInstance);
registerDisableCommand(smartcliInstance);
// Start parsing commands
smartcliInstance.startParse();
};

View File

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

View File

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

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

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

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

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

8
ts/client/index.ts Normal file
View File

@@ -0,0 +1,8 @@
/**
* Client-side exports for TSPM
* These are the only components that client applications should use
* They only communicate with the daemon via IPC, never directly manage processes
*/
export * from './tspm.ipcclient.js';
export * from './tspm.servicemanager.js';

View File

@@ -1,11 +1,11 @@
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,
ResponseForMethod, ResponseForMethod,
} from './ipc.types.js'; } from '../shared/protocol/ipc.types.js';
/** /**
* IPC client for communicating with the TSPM daemon * IPC client for communicating with the TSPM daemon
@@ -34,28 +34,50 @@ 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
this.ipcClient = plugins.smartipc.SmartIpc.createClient({ this.ipcClient = plugins.smartipc.SmartIpc.createClient({
id: 'tspm-cli', id: 'tspm-cli',
socketPath: this.socketPath, socketPath: this.socketPath,
heartbeat: false, // Disable heartbeat for now clientId: `cli-${process.pid}`,
connectRetry: {
enabled: true,
initialDelay: 100,
maxDelay: 2000,
maxAttempts: 30,
totalTimeout: 15000,
},
registerTimeoutMs: 8000,
heartbeat: true,
heartbeatInterval: 5000,
heartbeatTimeout: 20000,
heartbeatInitialGracePeriodMs: 10000,
heartbeatThrowOnTimeout: false, // Don't throw, emit events instead
}); });
// Connect to the daemon // Connect to the daemon
try { try {
await this.ipcClient.connect(); await this.ipcClient.connect({ waitForReady: true });
this.isConnected = true; this.isConnected = true;
// Handle heartbeat timeouts gracefully
this.ipcClient.on('heartbeatTimeout', () => {
console.warn('Heartbeat timeout detected, connection may be degraded');
this.isConnected = false;
});
console.log('Connected to TSPM daemon'); console.log('Connected to TSPM daemon');
} 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".',
); );
} }
} }
@@ -79,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();
} }
@@ -90,26 +113,38 @@ 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;
} }
} }
/**
* Subscribe to log updates for a specific process
*/
public async subscribe(
processId: string,
handler: (log: any) => void,
): Promise<void> {
if (!this.ipcClient || !this.isConnected) {
throw new Error('Not connected to daemon');
}
const topic = `logs.${processId}`;
await this.ipcClient.subscribe(`topic:${topic}`, handler);
}
/**
* Unsubscribe from log updates for a specific process
*/
public async unsubscribe(processId: string): Promise<void> {
if (!this.ipcClient || !this.isConnected) {
throw new Error('Not connected to daemon');
}
const topic = `logs.${processId}`;
await this.ipcClient.unsubscribe(`topic:${topic}`);
}
/** /**
* Check if the daemon is running * Check if the daemon is running
*/ */
@@ -129,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(() => {});
@@ -151,45 +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 (check for socket file)
const maxWaitTime = 10000; // 10 seconds
const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) {
if (await this.isDaemonRunning()) {
return;
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error('Daemon failed to start within timeout period');
}
/** /**
* Stop the daemon * Stop the daemon
*/ */

View File

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

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
import { startDaemon } from './classes.daemon.js'; import { startDaemon } from './tspm.daemon.js';
// Start the daemon // Start the daemon
startDaemon().catch((error) => { startDaemon().catch((error) => {

View File

@@ -1,17 +1,19 @@
import * as plugins from './plugins.js'; import * as plugins from '../plugins.js';
import * as paths from './paths.js'; import { EventEmitter } from 'events';
import * as paths from '../paths.js';
import { import {
ProcessMonitor, ProcessMonitor,
type IMonitorConfig, type IMonitorConfig,
} from './classes.processmonitor.js'; } from './processmonitor.js';
import { TspmConfig } from './classes.config.js'; import { type IProcessLog } from './processwrapper.js';
import { TspmConfig } from './tspm.config.js';
import { import {
Logger, Logger,
ProcessError, ProcessError,
ConfigError, ConfigError,
ValidationError, ValidationError,
handleError, handleError,
} from './utils.errorhandler.js'; } from '../shared/common/utils.errorhandler.js';
export interface IProcessConfig extends IMonitorConfig { export interface IProcessConfig extends IMonitorConfig {
id: string; // Unique identifier for the process id: string; // Unique identifier for the process
@@ -30,13 +32,7 @@ export interface IProcessInfo {
restarts: number; restarts: number;
} }
export interface IProcessLog { export class ProcessManager extends EventEmitter {
timestamp: Date;
type: 'stdout' | 'stderr' | 'system';
message: string;
}
export class Tspm {
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();
public processInfo: Map<string, IProcessInfo> = new Map(); public processInfo: Map<string, IProcessInfo> = new Map();
@@ -45,6 +41,7 @@ export class Tspm {
private logger: Logger; private logger: Logger;
constructor() { constructor() {
super();
this.logger = new Logger('Tspm'); this.logger = new Logger('Tspm');
this.config = new TspmConfig(); this.config = new TspmConfig();
this.loadProcessConfigs(); this.loadProcessConfigs();
@@ -98,6 +95,12 @@ export class Tspm {
}); });
this.processes.set(config.id, monitor); this.processes.set(config.id, monitor);
// Set up log event handler to re-emit for pub/sub
monitor.on('log', (log: IProcessLog) => {
this.emit('process:log', { processId: config.id, log });
});
monitor.start(); monitor.start();
// Update process info // Update process info

View File

@@ -1,6 +1,7 @@
import * as plugins from './plugins.js'; import * as plugins from '../plugins.js';
import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js'; import { EventEmitter } from 'events';
import { Logger, ProcessError, handleError } from './utils.errorhandler.js'; import { ProcessWrapper, type IProcessLog } from './processwrapper.js';
import { Logger, ProcessError, handleError } from '../shared/common/utils.errorhandler.js';
export interface IMonitorConfig { export interface IMonitorConfig {
name?: string; // Optional name to identify the instance name?: string; // Optional name to identify the instance
@@ -13,7 +14,7 @@ export interface IMonitorConfig {
logBufferSize?: number; // Optional: number of log lines to keep (default: 100) logBufferSize?: number; // Optional: number of log lines to keep (default: 100)
} }
export class ProcessMonitor { export class ProcessMonitor extends EventEmitter {
private processWrapper: ProcessWrapper | null = null; private processWrapper: ProcessWrapper | null = null;
private config: IMonitorConfig; private config: IMonitorConfig;
private intervalId: NodeJS.Timeout | null = null; private intervalId: NodeJS.Timeout | null = null;
@@ -22,6 +23,7 @@ export class ProcessMonitor {
private logger: Logger; private logger: Logger;
constructor(config: IMonitorConfig) { constructor(config: IMonitorConfig) {
super();
this.config = config; this.config = config;
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`); this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
} }
@@ -65,8 +67,10 @@ export class ProcessMonitor {
// Set up event handlers // Set up event handlers
this.processWrapper.on('log', (log: IProcessLog): void => { this.processWrapper.on('log', (log: IProcessLog): void => {
// Here we could add handlers to send logs somewhere // Re-emit the log event for upstream handlers
// For now, we just log system messages to the console this.emit('log', log);
// Log system messages to the console
if (log.type === 'system') { if (log.type === 'system') {
this.log(log.message); this.log(log.message);
} }

View File

@@ -1,6 +1,6 @@
import * as plugins from './plugins.js'; import * as plugins from '../plugins.js';
import { EventEmitter } from 'events'; import { EventEmitter } from 'events';
import { Logger, ProcessError, handleError } from './utils.errorhandler.js'; import { Logger, ProcessError, handleError } from '../shared/common/utils.errorhandler.js';
export interface IProcessWrapperOptions { export interface IProcessWrapperOptions {
command: string; command: string;
@@ -15,6 +15,8 @@ export interface IProcessLog {
timestamp: Date; timestamp: Date;
type: 'stdout' | 'stderr' | 'system'; type: 'stdout' | 'stderr' | 'system';
message: string; message: string;
seq: number;
runId: string;
} }
export class ProcessWrapper extends EventEmitter { export class ProcessWrapper extends EventEmitter {
@@ -24,12 +26,15 @@ export class ProcessWrapper extends EventEmitter {
private logBufferSize: number; private logBufferSize: number;
private startTime: Date | null = null; private startTime: Date | null = null;
private logger: Logger; private logger: Logger;
private nextSeq: number = 0;
private runId: string = '';
constructor(options: IProcessWrapperOptions) { constructor(options: IProcessWrapperOptions) {
super(); super();
this.options = options; this.options = options;
this.logBufferSize = options.logBuffer || 100; this.logBufferSize = options.logBuffer || 100;
this.logger = new Logger(`ProcessWrapper:${options.name}`); this.logger = new Logger(`ProcessWrapper:${options.name}`);
this.runId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
} }
/** /**
@@ -217,6 +222,8 @@ export class ProcessWrapper extends EventEmitter {
timestamp: new Date(), timestamp: new Date(),
type, type,
message, message,
seq: this.nextSeq++,
runId: this.runId,
}; };
this.logs.push(log); this.logs.push(log);
@@ -238,6 +245,8 @@ export class ProcessWrapper extends EventEmitter {
timestamp: new Date(), timestamp: new Date(),
type: 'system', type: 'system',
message, message,
seq: this.nextSeq++,
runId: this.runId,
}; };
this.logs.push(log); this.logs.push(log);

View File

@@ -1,4 +1,4 @@
import * as plugins from './plugins.js'; import * as plugins from '../plugins.js';
export class TspmConfig { export class TspmConfig {
public npmextraInstance = new plugins.npmextra.KeyValueStore({ public npmextraInstance = new plugins.npmextra.KeyValueStore({

View File

@@ -1,19 +1,19 @@
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 { Tspm } from './classes.tspm.js'; import { ProcessManager } from './processmanager.js';
import type { import type {
IpcMethodMap, IpcMethodMap,
RequestForMethod, RequestForMethod,
ResponseForMethod, ResponseForMethod,
DaemonStatusResponse, DaemonStatusResponse,
HeartbeatResponse, HeartbeatResponse,
} from './ipc.types.js'; } from '../shared/protocol/ipc.types.js';
/** /**
* Central daemon server that manages all TSPM processes * Central daemon server that manages all TSPM processes
*/ */
export class TspmDaemon { export class TspmDaemon {
private tspmInstance: Tspm; private tspmInstance: ProcessManager;
private ipcServer: plugins.smartipc.IpcServer; private ipcServer: plugins.smartipc.IpcServer;
private startTime: number; private startTime: number;
private isShuttingDown: boolean = false; private isShuttingDown: boolean = false;
@@ -22,7 +22,7 @@ export class TspmDaemon {
private daemonPidFile: string; private daemonPidFile: string;
constructor() { constructor() {
this.tspmInstance = new Tspm(); this.tspmInstance = new ProcessManager();
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock'); this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid'); this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
this.startTime = Date.now(); this.startTime = Date.now();
@@ -34,6 +34,10 @@ export class TspmDaemon {
public async start(): Promise<void> { public async start(): Promise<void> {
console.log('Starting TSPM daemon...'); console.log('Starting TSPM daemon...');
// Ensure the TSPM directory exists
const fs = await import('fs/promises');
await fs.mkdir(paths.tspmDir, { recursive: true });
// Check if another daemon is already running // Check if another daemon is already running
if (await this.isDaemonRunning()) { if (await this.isDaemonRunning()) {
throw new Error('Another TSPM daemon instance is already running'); throw new Error('Another TSPM daemon instance is already running');
@@ -43,14 +47,19 @@ export class TspmDaemon {
this.ipcServer = plugins.smartipc.SmartIpc.createServer({ this.ipcServer = plugins.smartipc.SmartIpc.createServer({
id: 'tspm-daemon', id: 'tspm-daemon',
socketPath: this.socketPath, socketPath: this.socketPath,
heartbeat: false, // Disable heartbeat for now autoCleanupSocketFile: true, // Clean up stale sockets
socketMode: 0o600, // Set proper permissions
heartbeat: true,
heartbeatInterval: 5000,
heartbeatTimeout: 20000,
heartbeatInitialGracePeriodMs: 10000, // Grace period for startup
}); });
// Register message handlers // Register message handlers
this.registerHandlers(); this.registerHandlers();
// Start the IPC server // Start the IPC server and wait until ready to accept connections
await this.ipcServer.start(); await this.ipcServer.start({ readyWhen: 'accepting' });
// Write PID file // Write PID file
await this.writePidFile(); await this.writePidFile();
@@ -61,6 +70,16 @@ export class TspmDaemon {
// Load existing process configurations // Load existing process configurations
await this.tspmInstance.loadProcessConfigs(); await this.tspmInstance.loadProcessConfigs();
// Set up log publishing
this.tspmInstance.on('process:log', ({ processId, log }) => {
// Publish to topic for this process
const topic = `logs.${processId}`;
// Broadcast to all connected clients subscribed to this topic
if (this.ipcServer) {
this.ipcServer.broadcast(`topic:${topic}`, log);
}
});
// Set up graceful shutdown handlers // Set up graceful shutdown handlers
this.setupShutdownHandlers(); this.setupShutdownHandlers();
@@ -107,19 +126,22 @@ export class TspmDaemon {
}, },
); );
this.ipcServer.onMessage('restart', async (request: RequestForMethod<'restart'>) => { this.ipcServer.onMessage(
try { 'restart',
await this.tspmInstance.restart(request.id); async (request: RequestForMethod<'restart'>) => {
const processInfo = this.tspmInstance.processInfo.get(request.id); try {
return { await this.tspmInstance.restart(request.id);
processId: request.id, const processInfo = this.tspmInstance.processInfo.get(request.id);
pid: processInfo?.pid, return {
status: processInfo?.status || 'stopped', processId: request.id,
}; pid: processInfo?.pid,
} catch (error) { status: processInfo?.status || 'stopped',
throw new Error(`Failed to restart process: ${error.message}`); };
} } catch (error) {
}); throw new Error(`Failed to restart process: ${error.message}`);
}
},
);
this.ipcServer.onMessage( this.ipcServer.onMessage(
'delete', 'delete',
@@ -145,124 +167,148 @@ export class TspmDaemon {
}, },
); );
this.ipcServer.onMessage('describe', async (request: RequestForMethod<'describe'>) => { this.ipcServer.onMessage(
const processInfo = await this.tspmInstance.describe(request.id); 'describe',
const config = this.tspmInstance.processConfigs.get(request.id); async (request: RequestForMethod<'describe'>) => {
const processInfo = await this.tspmInstance.describe(request.id);
const config = this.tspmInstance.processConfigs.get(request.id);
if (!processInfo || !config) { if (!processInfo || !config) {
throw new Error(`Process ${request.id} not found`); throw new Error(`Process ${request.id} not found`);
} }
return { return {
processInfo, processInfo,
config, config,
}; };
}); },
);
this.ipcServer.onMessage('getLogs', async (request: RequestForMethod<'getLogs'>) => { this.ipcServer.onMessage(
const logs = await this.tspmInstance.getLogs(request.id); 'getLogs',
return { logs }; async (request: RequestForMethod<'getLogs'>) => {
}); const logs = await this.tspmInstance.getLogs(request.id);
return { logs };
},
);
// Batch operations handlers // Batch operations handlers
this.ipcServer.onMessage('startAll', async (request: RequestForMethod<'startAll'>) => { this.ipcServer.onMessage(
const started: string[] = []; 'startAll',
const failed: Array<{ id: string; error: string }> = []; async (request: RequestForMethod<'startAll'>) => {
const started: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
await this.tspmInstance.startAll(); await this.tspmInstance.startAll();
// Get status of all processes // Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) { for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'online') { if (processInfo.status === 'online') {
started.push(id); started.push(id);
} else { } else {
failed.push({ id, error: 'Failed to start' }); failed.push({ id, error: 'Failed to start' });
}
} }
}
return { started, failed }; return { started, failed };
}); },
);
this.ipcServer.onMessage('stopAll', async (request: RequestForMethod<'stopAll'>) => { this.ipcServer.onMessage(
const stopped: string[] = []; 'stopAll',
const failed: Array<{ id: string; error: string }> = []; async (request: RequestForMethod<'stopAll'>) => {
const stopped: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
await this.tspmInstance.stopAll(); await this.tspmInstance.stopAll();
// Get status of all processes // Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) { for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'stopped') { if (processInfo.status === 'stopped') {
stopped.push(id); stopped.push(id);
} else { } else {
failed.push({ id, error: 'Failed to stop' }); failed.push({ id, error: 'Failed to stop' });
}
} }
}
return { stopped, failed }; return { stopped, failed };
}); },
);
this.ipcServer.onMessage('restartAll', async (request: RequestForMethod<'restartAll'>) => { this.ipcServer.onMessage(
const restarted: string[] = []; 'restartAll',
const failed: Array<{ id: string; error: string }> = []; async (request: RequestForMethod<'restartAll'>) => {
const restarted: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
await this.tspmInstance.restartAll(); await this.tspmInstance.restartAll();
// Get status of all processes // Get status of all processes
for (const [id, processInfo] of this.tspmInstance.processInfo) { for (const [id, processInfo] of this.tspmInstance.processInfo) {
if (processInfo.status === 'online') { if (processInfo.status === 'online') {
restarted.push(id); restarted.push(id);
} else { } else {
failed.push({ id, error: 'Failed to restart' }); failed.push({ id, error: 'Failed to restart' });
}
} }
}
return { restarted, failed }; return { restarted, failed };
}); },
);
// Daemon management handlers // Daemon management handlers
this.ipcServer.onMessage('daemon:status', async (request: RequestForMethod<'daemon:status'>) => { this.ipcServer.onMessage(
const memUsage = process.memoryUsage(); 'daemon:status',
return { async (request: RequestForMethod<'daemon:status'>) => {
status: 'running', const memUsage = process.memoryUsage();
pid: process.pid,
uptime: Date.now() - this.startTime,
processCount: this.tspmInstance.processes.size,
memoryUsage: memUsage.heapUsed,
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
};
});
this.ipcServer.onMessage('daemon:shutdown', async (request: RequestForMethod<'daemon:shutdown'>) => {
if (this.isShuttingDown) {
return { return {
success: false, status: 'running',
message: 'Daemon is already shutting down', pid: process.pid,
uptime: Date.now() - this.startTime,
processCount: this.tspmInstance.processes.size,
memoryUsage: memUsage.heapUsed,
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
}; };
} },
);
// Schedule shutdown this.ipcServer.onMessage(
const graceful = request.graceful !== false; 'daemon:shutdown',
const timeout = request.timeout || 10000; async (request: RequestForMethod<'daemon:shutdown'>) => {
if (this.isShuttingDown) {
return {
success: false,
message: 'Daemon is already shutting down',
};
}
if (graceful) { // Schedule shutdown
setTimeout(() => this.shutdown(true), 100); const graceful = request.graceful !== false;
} else { const timeout = request.timeout || 10000;
setTimeout(() => this.shutdown(false), 100);
}
return { if (graceful) {
success: true, setTimeout(() => this.shutdown(true), 100);
message: `Daemon will shutdown ${graceful ? 'gracefully' : 'immediately'} in ${timeout}ms`, } else {
}; setTimeout(() => this.shutdown(false), 100);
}); }
return {
success: true,
message: `Daemon will shutdown ${graceful ? 'gracefully' : 'immediately'} in ${timeout}ms`,
};
},
);
// Heartbeat handler // Heartbeat handler
this.ipcServer.onMessage('heartbeat', async (request: RequestForMethod<'heartbeat'>) => { this.ipcServer.onMessage(
return { 'heartbeat',
timestamp: Date.now(), async (request: RequestForMethod<'heartbeat'>) => {
status: this.isShuttingDown ? 'degraded' : 'healthy', return {
}; timestamp: Date.now(),
}); status: this.isShuttingDown ? 'degraded' : 'healthy',
};
},
);
} }
/** /**

View File

@@ -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';

View File

@@ -0,0 +1,26 @@
/**
* Standardized error codes for IPC communication
* These are used instead of string messages for better error handling
*/
export enum ErrorCode {
// General errors
UNKNOWN_ERROR = 'UNKNOWN_ERROR',
INVALID_REQUEST = 'INVALID_REQUEST',
// Process errors
PROCESS_NOT_FOUND = 'PROCESS_NOT_FOUND',
PROCESS_ALREADY_EXISTS = 'PROCESS_ALREADY_EXISTS',
PROCESS_START_FAILED = 'PROCESS_START_FAILED',
PROCESS_STOP_FAILED = 'PROCESS_STOP_FAILED',
// Daemon errors
DAEMON_NOT_RUNNING = 'DAEMON_NOT_RUNNING',
DAEMON_ALREADY_RUNNING = 'DAEMON_ALREADY_RUNNING',
// Memory errors
MEMORY_LIMIT_EXCEEDED = 'MEMORY_LIMIT_EXCEEDED',
// Config errors
CONFIG_INVALID = 'CONFIG_INVALID',
CONFIG_SAVE_FAILED = 'CONFIG_SAVE_FAILED',
}

View File

@@ -1,8 +1,5 @@
import type { import type { IProcessConfig, IProcessInfo } from './classes.tspm.js';
IProcessConfig, import type { IProcessLog } from './classes.processwrapper.js';
IProcessInfo,
IProcessLog,
} from './classes.tspm.js';
// Base message types // Base message types
export interface IpcRequest<T = any> { export interface IpcRequest<T = any> {

View File

@@ -0,0 +1,5 @@
/**
* Protocol version for client-daemon communication
* This allows for version compatibility checks between client and daemon
*/
export const PROTOCOL_VERSION = '1.0.0';