Compare commits

..

4 Commits

Author SHA1 Message Date
6141b26530 3.0.0
Some checks failed
Default (tags) / security (push) Failing after 11m45s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-08-28 15:52:29 +00:00
e73f4acd63 BREAKING CHANGE(daemon): Refactor daemon and service management: remove IPC auto-spawn, add TspmServiceManager, tighten IPC/client/CLI behavior and tests 2025-08-28 15:52:29 +00:00
8e3cfb624b 2.0.1
Some checks failed
Default (tags) / security (push) Failing after 11m47s
Default (tags) / test (push) Has been cancelled
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-08-28 15:51:11 +00:00
33fb02733d update 2025-08-28 15:47:59 +00:00
42 changed files with 1536 additions and 1315 deletions

View File

@@ -1,6 +1,19 @@
# Changelog
## 2025-08-28 - 3.0.0 - BREAKING CHANGE(daemon)
Refactor daemon and service management: remove IPC auto-spawn, add TspmServiceManager, tighten IPC/client/CLI behavior and tests
- Remove automatic daemon spawn from the IPC client — clients now error with guidance and require the daemon to be started manually or enabled as a system service
- Add TspmServiceManager to manage the daemon as a systemd service (enable/disable/reload/status)
- Update IPC server/client to use SmartIpc.createServer/createClient with heartbeat defaults and explicit onMessage handlers
- Daemon publishes per-process logs to topics (logs.<processId>) and re-emits ProcessMonitor logs for pub/sub
- CLI updated: add enable/disable service commands, adjust daemon start/stop/status workflows and improve user hints when daemon is not running
- Add/adjust integration and unit tests to cover daemon lifecycle, IPC client behavior, log streaming, heartbeat and resource reporting
- Documentation expanded (README, readme.plan.md, changelog) to reflect the refactor and migration notes
- Various code cleanups, formatting fixes and defensive checks across modules
## 2025-08-28 - 2.0.0 - BREAKING CHANGE(daemon)
Refactor daemon lifecycle and service management: remove IPC auto-spawn, add TspmServiceManager and CLI enable/disable
- Do not auto-spawn the daemon from the IPC client anymore — attempts to connect will now error with instructions to start the daemon manually or enable the system service (breaking change).
@@ -12,6 +25,7 @@ Refactor daemon lifecycle and service management: remove IPC auto-spawn, add Tsp
- Updated development plan/readme (readme.plan.md) to reflect the refactor toward proper SmartDaemon integration and migration notes.
## 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
@@ -24,6 +38,7 @@ Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC
- Standardized heartbeat and IPC timing defaults (heartbeatInterval: 5000ms, heartbeatTimeout: 20000ms, heartbeatInitialGracePeriodMs: 10000ms)
## 2025-08-25 - 1.7.0 - feat(readme)
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
@@ -32,6 +47,7 @@ Add comprehensive README with detailed usage, command reference, daemon manageme
- Improved onboarding instructions: cloning, installing, testing, building, and running the project
## 2025-08-25 - 1.6.1 - fix(daemon)
Fix smartipc integration and add daemon/ipc integration tests
- Replace direct smartipc server/client construction with SmartIpc.createServer/createClient and set heartbeat: false
@@ -40,6 +56,7 @@ Fix smartipc integration and add daemon/ipc integration tests
- Add comprehensive tests: unit tests for TspmDaemon and TspmIpcClient and full integration tests for daemon lifecycle, process management, error handling, heartbeat and resource reporting
## 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 daemon implementation (ts/classes.daemon.ts) to manage all processes via a single background service and Unix socket.

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tspm",
"version": "2.0.0",
"version": "3.0.0",
"private": false,
"description": "a no fuzz process manager",
"main": "dist_ts/index.js",
@@ -33,7 +33,8 @@
"@push.rocks/smartipc": "^2.1.2",
"@push.rocks/smartpath": "^6.0.0",
"pidusage": "^4.0.1",
"ps-tree": "^1.2.0"
"ps-tree": "^1.2.0",
"tsx": "^4.20.5"
},
"repository": {
"type": "git",

271
pnpm-lock.yaml generated
View File

@@ -32,6 +32,9 @@ importers:
ps-tree:
specifier: ^1.2.0
version: 1.2.0
tsx:
specifier: ^4.20.5
version: 4.20.5
devDependencies:
'@git.zone/tsbuild':
specifier: ^2.6.7
@@ -362,252 +365,126 @@ packages:
'@emnapi/wasi-threads@1.0.4':
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':
resolution: {integrity: sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==}
engines: {node: '>=18'}
cpu: [ppc64]
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':
resolution: {integrity: sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==}
engines: {node: '>=18'}
cpu: [arm64]
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':
resolution: {integrity: sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==}
engines: {node: '>=18'}
cpu: [arm]
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':
resolution: {integrity: sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==}
engines: {node: '>=18'}
cpu: [x64]
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':
resolution: {integrity: sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==}
engines: {node: '>=18'}
cpu: [arm64]
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':
resolution: {integrity: sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==}
engines: {node: '>=18'}
cpu: [x64]
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':
resolution: {integrity: sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==}
engines: {node: '>=18'}
cpu: [arm64]
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':
resolution: {integrity: sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==}
engines: {node: '>=18'}
cpu: [x64]
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':
resolution: {integrity: sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==}
engines: {node: '>=18'}
cpu: [arm64]
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':
resolution: {integrity: sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==}
engines: {node: '>=18'}
cpu: [arm]
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':
resolution: {integrity: sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==}
engines: {node: '>=18'}
cpu: [ia32]
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':
resolution: {integrity: sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==}
engines: {node: '>=18'}
cpu: [loong64]
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':
resolution: {integrity: sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==}
engines: {node: '>=18'}
cpu: [mips64el]
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':
resolution: {integrity: sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==}
engines: {node: '>=18'}
cpu: [ppc64]
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':
resolution: {integrity: sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==}
engines: {node: '>=18'}
cpu: [riscv64]
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':
resolution: {integrity: sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==}
engines: {node: '>=18'}
cpu: [s390x]
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':
resolution: {integrity: sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==}
engines: {node: '>=18'}
cpu: [x64]
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':
resolution: {integrity: sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==}
engines: {node: '>=18'}
cpu: [arm64]
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':
resolution: {integrity: sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==}
engines: {node: '>=18'}
cpu: [x64]
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':
resolution: {integrity: sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==}
engines: {node: '>=18'}
cpu: [arm64]
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':
resolution: {integrity: sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==}
engines: {node: '>=18'}
@@ -620,48 +497,24 @@ packages:
cpu: [arm64]
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':
resolution: {integrity: sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==}
engines: {node: '>=18'}
cpu: [x64]
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':
resolution: {integrity: sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==}
engines: {node: '>=18'}
cpu: [arm64]
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':
resolution: {integrity: sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==}
engines: {node: '>=18'}
cpu: [ia32]
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':
resolution: {integrity: sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==}
engines: {node: '>=18'}
@@ -2440,11 +2293,6 @@ packages:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
esbuild@0.25.0:
resolution: {integrity: sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==}
engines: {node: '>=18'}
hasBin: true
esbuild@0.25.9:
resolution: {integrity: sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==}
engines: {node: '>=18'}
@@ -4355,8 +4203,8 @@ packages:
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
engines: {node: '>=0.6.x'}
tsx@4.19.3:
resolution: {integrity: sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==}
tsx@4.20.5:
resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==}
engines: {node: '>=18.0.0'}
hasBin: true
@@ -5625,156 +5473,81 @@ snapshots:
tslib: 2.8.1
optional: true
'@esbuild/aix-ppc64@0.25.0':
optional: true
'@esbuild/aix-ppc64@0.25.9':
optional: true
'@esbuild/android-arm64@0.25.0':
optional: true
'@esbuild/android-arm64@0.25.9':
optional: true
'@esbuild/android-arm@0.25.0':
optional: true
'@esbuild/android-arm@0.25.9':
optional: true
'@esbuild/android-x64@0.25.0':
optional: true
'@esbuild/android-x64@0.25.9':
optional: true
'@esbuild/darwin-arm64@0.25.0':
optional: true
'@esbuild/darwin-arm64@0.25.9':
optional: true
'@esbuild/darwin-x64@0.25.0':
optional: true
'@esbuild/darwin-x64@0.25.9':
optional: true
'@esbuild/freebsd-arm64@0.25.0':
optional: true
'@esbuild/freebsd-arm64@0.25.9':
optional: true
'@esbuild/freebsd-x64@0.25.0':
optional: true
'@esbuild/freebsd-x64@0.25.9':
optional: true
'@esbuild/linux-arm64@0.25.0':
optional: true
'@esbuild/linux-arm64@0.25.9':
optional: true
'@esbuild/linux-arm@0.25.0':
optional: true
'@esbuild/linux-arm@0.25.9':
optional: true
'@esbuild/linux-ia32@0.25.0':
optional: true
'@esbuild/linux-ia32@0.25.9':
optional: true
'@esbuild/linux-loong64@0.25.0':
optional: true
'@esbuild/linux-loong64@0.25.9':
optional: true
'@esbuild/linux-mips64el@0.25.0':
optional: true
'@esbuild/linux-mips64el@0.25.9':
optional: true
'@esbuild/linux-ppc64@0.25.0':
optional: true
'@esbuild/linux-ppc64@0.25.9':
optional: true
'@esbuild/linux-riscv64@0.25.0':
optional: true
'@esbuild/linux-riscv64@0.25.9':
optional: true
'@esbuild/linux-s390x@0.25.0':
optional: true
'@esbuild/linux-s390x@0.25.9':
optional: true
'@esbuild/linux-x64@0.25.0':
optional: true
'@esbuild/linux-x64@0.25.9':
optional: true
'@esbuild/netbsd-arm64@0.25.0':
optional: true
'@esbuild/netbsd-arm64@0.25.9':
optional: true
'@esbuild/netbsd-x64@0.25.0':
optional: true
'@esbuild/netbsd-x64@0.25.9':
optional: true
'@esbuild/openbsd-arm64@0.25.0':
optional: true
'@esbuild/openbsd-arm64@0.25.9':
optional: true
'@esbuild/openbsd-x64@0.25.0':
optional: true
'@esbuild/openbsd-x64@0.25.9':
optional: true
'@esbuild/openharmony-arm64@0.25.9':
optional: true
'@esbuild/sunos-x64@0.25.0':
optional: true
'@esbuild/sunos-x64@0.25.9':
optional: true
'@esbuild/win32-arm64@0.25.0':
optional: true
'@esbuild/win32-arm64@0.25.9':
optional: true
'@esbuild/win32-ia32@0.25.0':
optional: true
'@esbuild/win32-ia32@0.25.9':
optional: true
'@esbuild/win32-x64@0.25.0':
optional: true
'@esbuild/win32-x64@0.25.9':
optional: true
@@ -5837,7 +5610,7 @@ snapshots:
dependencies:
'@push.rocks/smartfile': 11.2.0
'@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)':
dependencies:
@@ -8366,34 +8139,6 @@ snapshots:
has-tostringtag: 1.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:
optionalDependencies:
'@esbuild/aix-ppc64': 0.25.9
@@ -10753,9 +10498,9 @@ snapshots:
tsscmp@1.0.6: {}
tsx@4.19.3:
tsx@4.20.5:
dependencies:
esbuild: 0.25.0
esbuild: 0.25.9
get-tsconfig: 4.10.0
optionalDependencies:
fsevents: 2.3.3

View File

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

View File

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

23
simple-test.ts Normal file
View File

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

28
test-script.ts Normal file
View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import { tspmIpcClient } from '../ts/classes.ipcclient.js';
async function ensureDaemonStopped() {
try {
await tspmIpcClient.stopDaemon(false);
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
} catch (error) {
// Ignore errors if daemon is not running
}
@@ -43,7 +43,7 @@ tap.test('Full daemon lifecycle test', async (tools) => {
await tspmIpcClient.connect();
// 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
status = await tspmIpcClient.getDaemonStatus();
@@ -57,7 +57,7 @@ tap.test('Full daemon lifecycle test', async (tools) => {
await tspmIpcClient.stopDaemon(true);
// 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
status = await tspmIpcClient.getDaemonStatus();
@@ -71,7 +71,7 @@ tap.test('Process management through daemon', async (tools) => {
// Ensure daemon is running
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)
let listResponse = await tspmIpcClient.request('list', {});
@@ -88,7 +88,9 @@ tap.test('Process management through daemon', async (tools) => {
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.status).toBeDefined();
@@ -96,12 +98,14 @@ tap.test('Process management through daemon', async (tools) => {
listResponse = await tspmIpcClient.request('list', {});
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?.id).toEqual('test-echo');
// 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.config).toBeDefined();
expect(describeResponse.config.id).toEqual('test-echo');
@@ -112,12 +116,16 @@ tap.test('Process management through daemon', async (tools) => {
expect(stopResponse.message).toInclude('stopped successfully');
// 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);
// Test 7: Verify process is gone
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();
// Cleanup: stop daemon
@@ -131,7 +139,7 @@ tap.test('Batch operations through daemon', async (tools) => {
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
// Add multiple test processes
const testConfigs: tspm.IProcessConfig[] = [
@@ -187,7 +195,7 @@ tap.test('Daemon error handling', async (tools) => {
// Ensure daemon is running
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
try {
@@ -224,7 +232,7 @@ tap.test('Daemon heartbeat functionality', async (tools) => {
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
// Test heartbeat
const heartbeatResponse = await tspmIpcClient.request('heartbeat', {});
@@ -242,7 +250,7 @@ tap.test('Daemon memory and CPU reporting', async (tools) => {
// Ensure daemon is running
await tspmIpcClient.connect();
await new Promise(resolve => setTimeout(resolve, 1000));
await new Promise((resolve) => setTimeout(resolve, 1000));
// Get daemon status
const status = await tspmIpcClient.getDaemonStatus();

View File

@@ -61,7 +61,10 @@ tap.test('IPC client daemon running check - stale PID', async () => {
expect(isRunning).toEqual(false);
// 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);
});
@@ -95,7 +98,9 @@ tap.test('IPC client singleton instance', async () => {
expect(tspmIpcClient).toBeInstanceOf(TspmIpcClient);
// 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);
});
@@ -111,7 +116,8 @@ tap.test('IPC client request method type safety', 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');
});

View File

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

View File

@@ -48,7 +48,7 @@ export class TspmDaemon {
heartbeat: true,
heartbeatInterval: 5000,
heartbeatTimeout: 20000,
heartbeatInitialGracePeriodMs: 10000 // Grace period for startup
heartbeatInitialGracePeriodMs: 10000, // Grace period for startup
});
// Register message handlers
@@ -122,7 +122,9 @@ export class TspmDaemon {
},
);
this.ipcServer.onMessage('restart', async (request: RequestForMethod<'restart'>) => {
this.ipcServer.onMessage(
'restart',
async (request: RequestForMethod<'restart'>) => {
try {
await this.tspmInstance.restart(request.id);
const processInfo = this.tspmInstance.processInfo.get(request.id);
@@ -134,7 +136,8 @@ export class TspmDaemon {
} catch (error) {
throw new Error(`Failed to restart process: ${error.message}`);
}
});
},
);
this.ipcServer.onMessage(
'delete',
@@ -160,7 +163,9 @@ export class TspmDaemon {
},
);
this.ipcServer.onMessage('describe', async (request: RequestForMethod<'describe'>) => {
this.ipcServer.onMessage(
'describe',
async (request: RequestForMethod<'describe'>) => {
const processInfo = await this.tspmInstance.describe(request.id);
const config = this.tspmInstance.processConfigs.get(request.id);
@@ -172,15 +177,21 @@ export class TspmDaemon {
processInfo,
config,
};
});
},
);
this.ipcServer.onMessage('getLogs', async (request: RequestForMethod<'getLogs'>) => {
this.ipcServer.onMessage(
'getLogs',
async (request: RequestForMethod<'getLogs'>) => {
const logs = await this.tspmInstance.getLogs(request.id);
return { logs };
});
},
);
// Batch operations handlers
this.ipcServer.onMessage('startAll', async (request: RequestForMethod<'startAll'>) => {
this.ipcServer.onMessage(
'startAll',
async (request: RequestForMethod<'startAll'>) => {
const started: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
@@ -196,9 +207,12 @@ export class TspmDaemon {
}
return { started, failed };
});
},
);
this.ipcServer.onMessage('stopAll', async (request: RequestForMethod<'stopAll'>) => {
this.ipcServer.onMessage(
'stopAll',
async (request: RequestForMethod<'stopAll'>) => {
const stopped: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
@@ -214,9 +228,12 @@ export class TspmDaemon {
}
return { stopped, failed };
});
},
);
this.ipcServer.onMessage('restartAll', async (request: RequestForMethod<'restartAll'>) => {
this.ipcServer.onMessage(
'restartAll',
async (request: RequestForMethod<'restartAll'>) => {
const restarted: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
@@ -232,10 +249,13 @@ export class TspmDaemon {
}
return { restarted, failed };
});
},
);
// Daemon management handlers
this.ipcServer.onMessage('daemon:status', async (request: RequestForMethod<'daemon:status'>) => {
this.ipcServer.onMessage(
'daemon:status',
async (request: RequestForMethod<'daemon:status'>) => {
const memUsage = process.memoryUsage();
return {
status: 'running',
@@ -245,9 +265,12 @@ export class TspmDaemon {
memoryUsage: memUsage.heapUsed,
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
};
});
},
);
this.ipcServer.onMessage('daemon:shutdown', async (request: RequestForMethod<'daemon:shutdown'>) => {
this.ipcServer.onMessage(
'daemon:shutdown',
async (request: RequestForMethod<'daemon:shutdown'>) => {
if (this.isShuttingDown) {
return {
success: false,
@@ -269,15 +292,19 @@ export class TspmDaemon {
success: true,
message: `Daemon will shutdown ${graceful ? 'gracefully' : 'immediately'} in ${timeout}ms`,
};
});
},
);
// Heartbeat handler
this.ipcServer.onMessage('heartbeat', async (request: RequestForMethod<'heartbeat'>) => {
this.ipcServer.onMessage(
'heartbeat',
async (request: RequestForMethod<'heartbeat'>) => {
return {
timestamp: Date.now(),
status: this.isShuttingDown ? 'degraded' : 'healthy',
};
});
},
);
}
/**

View File

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

View File

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

View File

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

799
ts/cli.ts
View File

@@ -1,797 +1,2 @@
import * as plugins from './plugins.js';
import * as paths from './paths.js';
import { tspmIpcClient } from './classes.ipcclient.js';
import { TspmServiceManager } from './classes.servicemanager.js';
import { Logger, LogLevel } from './utils.errorhandler.js';
import type { IProcessConfig } from './classes.tspm.js';
export interface CliArguments {
verbose?: boolean;
watch?: boolean;
memory?: string;
cwd?: string;
daemon?: boolean;
test?: boolean;
name?: string;
autorestart?: boolean;
watchPaths?: string[];
[key: string]: any;
}
// Helper function to parse memory strings (e.g., "512MB", "2GB")
function parseMemoryString(memStr: string): number {
const units = {
KB: 1024,
MB: 1024 * 1024,
GB: 1024 * 1024 * 1024,
};
const match = memStr.toUpperCase().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/);
if (!match) {
throw new Error(
`Invalid memory format: ${memStr}. Use format like "512MB" or "2GB"`,
);
}
const value = parseFloat(match[1]);
const unit = (match[2] || 'MB') as keyof typeof units;
return Math.floor(value * units[unit]);
}
// Helper function to format memory for display
function formatMemory(bytes: number): string {
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
} else if (bytes >= 1024 * 1024) {
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
} else if (bytes >= 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
} else {
return `${bytes} B`;
}
}
// Helper function to handle daemon connection errors
function handleDaemonError(error: any, action: string): void {
if (error.message?.includes('daemon is not running') ||
error.message?.includes('Not connected') ||
error.message?.includes('ECONNREFUSED')) {
console.error(`Error: Cannot ${action} - TSPM daemon is not running.`);
console.log('\nTo start the daemon, run one of:');
console.log(' tspm daemon start - Start for this session only');
console.log(' tspm enable - Enable as system service (recommended)');
} else {
console.error(`Error ${action}:`, error.message);
}
process.exit(1);
}
// Helper function for padding strings
function pad(str: string, length: number): string {
return str.length > length
? str.substring(0, length - 3) + '...'
: str.padEnd(length);
}
export const run = async (): Promise<void> => {
const cliLogger = new Logger('CLI');
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
// Check if debug mode is enabled
const debugMode = process.env.TSPM_DEBUG === 'true';
if (debugMode) {
cliLogger.setLevel(LogLevel.DEBUG);
cliLogger.debug('Debug mode enabled');
}
const smartcliInstance = new plugins.smartcli.Smartcli();
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
// Default command - show help and list processes
smartcliInstance.standardCommand().subscribe({
next: async (argvArg: CliArguments) => {
console.log(
`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`,
);
console.log('Usage: tspm [command] [options]');
console.log('\nService Management:');
console.log(' enable Enable TSPM as system service (systemd)');
console.log(' disable Disable TSPM system service');
console.log('\nProcess Commands:');
console.log(' start <script> Start a process');
console.log(' list List all processes');
console.log(' stop <id> Stop a process');
console.log(' restart <id> Restart a process');
console.log(' delete <id> Delete a process');
console.log(' describe <id> Show details for a process');
console.log(' logs <id> Show logs for a process');
console.log(' start-all Start all saved processes');
console.log(' stop-all Stop all processes');
console.log(' restart-all Restart all processes');
console.log('\nDaemon Commands:');
console.log(' daemon start Start daemon manually (current session)');
console.log(' daemon stop Stop the daemon');
console.log(' daemon status Show daemon status');
console.log(
'\nUse tspm [command] --help for more information about a command.',
);
// Show current process list
console.log('\nProcess List:');
try {
const response = await tspmIpcClient.request('list', {});
const processes = response.processes;
if (processes.length === 0) {
console.log(
' No processes running. Use "tspm start" to start a process.',
);
} else {
console.log(
'┌─────────┬─────────────┬───────────┬───────────┬──────────┐',
);
console.log(
'│ ID │ Name │ Status │ Memory │ Restarts │',
);
console.log(
'├─────────┼─────────────┼───────────┼───────────┼──────────┤',
);
for (const proc of processes) {
const statusColor =
proc.status === 'online'
? '\x1b[32m'
: proc.status === 'errored'
? '\x1b[31m'
: '\x1b[33m';
const resetColor = '\x1b[0m';
console.log(
`${pad(proc.id, 7)}${pad(proc.id, 11)}${statusColor}${pad(proc.status, 9)}${resetColor}${pad(formatMemory(proc.memory), 9)}${pad(proc.restarts.toString(), 8)}`,
);
}
console.log(
'└─────────┴─────────────┴───────────┴───────────┴──────────┘',
);
}
} catch (error) {
console.error('Error: TSPM daemon is not running.');
console.log('\nTo start the daemon, run one of:');
console.log(' tspm daemon start - Start for this session only');
console.log(' tspm enable - Enable as system service (recommended)');
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Start command
smartcliInstance.addCommand('start').subscribe({
next: async (argvArg: CliArguments) => {
try {
const script = argvArg._[1];
if (!script) {
console.error('Error: Please provide a script to run');
console.log('Usage: tspm start <script> [options]');
console.log('\nOptions:');
console.log(' --name <name> Name for the process');
console.log(
' --memory <size> Memory limit (e.g., "512MB", "2GB")',
);
console.log(' --cwd <path> Working directory');
console.log(
' --watch Watch for file changes and restart',
);
console.log(' --watch-paths <paths> Comma-separated paths to watch');
console.log(' --autorestart Auto-restart on crash');
return;
}
const memoryLimit = argvArg.memory
? parseMemoryString(argvArg.memory)
: 512 * 1024 * 1024; // Default 512MB
const projectDir = argvArg.cwd || process.cwd();
const name = argvArg.name || script;
const watch = argvArg.watch || false;
const autorestart = argvArg.autorestart !== false; // Default true
const watchPaths = argvArg.watchPaths
? typeof argvArg.watchPaths === 'string'
? (argvArg.watchPaths as string).split(',')
: argvArg.watchPaths
: undefined;
const processConfig: IProcessConfig = {
id: name.replace(/[^a-zA-Z0-9-_]/g, '_'),
name,
command: script,
projectDir,
memoryLimitBytes: memoryLimit,
autorestart,
watch,
watchPaths,
};
console.log(`Starting process: ${name}`);
console.log(` Command: ${script}`);
console.log(` Directory: ${projectDir}`);
console.log(` Memory limit: ${formatMemory(memoryLimit)}`);
console.log(` Auto-restart: ${autorestart}`);
if (watch) {
console.log(` Watch mode: enabled`);
if (watchPaths) {
console.log(` Watch paths: ${watchPaths.join(', ')}`);
}
}
const response = await tspmIpcClient.request('start', {
config: processConfig,
});
console.log(`✓ Process started successfully`);
console.log(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`);
} catch (error) {
handleDaemonError(error, 'start process');
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Stop command
smartcliInstance.addCommand('stop').subscribe({
next: async (argvArg: CliArguments) => {
try {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm stop <id>');
return;
}
console.log(`Stopping process: ${id}`);
const response = await tspmIpcClient.request('stop', { id });
if (response.success) {
console.log(`${response.message}`);
} else {
console.error(`✗ Failed to stop process: ${response.message}`);
}
} catch (error) {
handleDaemonError(error, 'stop process');
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Restart command
smartcliInstance.addCommand('restart').subscribe({
next: async (argvArg: CliArguments) => {
try {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm restart <id>');
return;
}
console.log(`Restarting process: ${id}`);
const response = await tspmIpcClient.request('restart', { id });
console.log(`✓ Process restarted successfully`);
console.log(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`);
} catch (error) {
handleDaemonError(error, 'restart process');
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Delete command
smartcliInstance.addCommand('delete').subscribe({
next: async (argvArg: CliArguments) => {
try {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm delete <id>');
return;
}
console.log(`Deleting process: ${id}`);
const response = await tspmIpcClient.request('delete', { id });
if (response.success) {
console.log(`${response.message}`);
} else {
console.error(`✗ Failed to delete process: ${response.message}`);
}
} catch (error) {
handleDaemonError(error, 'delete process');
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// List command
smartcliInstance.addCommand('list').subscribe({
next: async (argvArg: CliArguments) => {
try {
const response = await tspmIpcClient.request('list', {});
const processes = response.processes;
if (processes.length === 0) {
console.log('No processes running.');
} else {
console.log('Process List:');
console.log(
'┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐',
);
console.log(
'│ ID │ Name │ Status │ PID │ Memory │ Restarts │',
);
console.log(
'├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤',
);
for (const proc of processes) {
const statusColor =
proc.status === 'online'
? '\x1b[32m'
: proc.status === 'errored'
? '\x1b[31m'
: '\x1b[33m';
const resetColor = '\x1b[0m';
console.log(
`${pad(proc.id, 7)}${pad(proc.id, 11)}${statusColor}${pad(proc.status, 9)}${resetColor}${pad((proc.pid || '-').toString(), 9)}${pad(formatMemory(proc.memory), 8)}${pad(proc.restarts.toString(), 8)}`,
);
}
console.log(
'└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘',
);
}
} catch (error) {
handleDaemonError(error, 'list processes');
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Describe command
smartcliInstance.addCommand('describe').subscribe({
next: async (argvArg: CliArguments) => {
try {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm describe <id>');
return;
}
const response = await tspmIpcClient.request('describe', { id });
console.log(`Process Details: ${id}`);
console.log('─'.repeat(40));
console.log(`Status: ${response.processInfo.status}`);
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
console.log(
`Memory: ${formatMemory(response.processInfo.memory)}`,
);
console.log(
`CPU: ${response.processInfo.cpu ? response.processInfo.cpu.toFixed(1) + '%' : 'N/A'}`,
);
console.log(
`Uptime: ${response.processInfo.uptime ? Math.floor(response.processInfo.uptime / 1000) + 's' : 'N/A'}`,
);
console.log(`Restarts: ${response.processInfo.restarts}`);
console.log('\nConfiguration:');
console.log(`Command: ${response.config.command}`);
console.log(`Directory: ${response.config.projectDir}`);
console.log(
`Memory Limit: ${formatMemory(response.config.memoryLimitBytes)}`,
);
console.log(`Auto-restart: ${response.config.autorestart}`);
if (response.config.watch) {
console.log(`Watch: enabled`);
if (response.config.watchPaths) {
console.log(
`Watch Paths: ${response.config.watchPaths.join(', ')}`,
);
}
}
} catch (error) {
handleDaemonError(error, 'describe process');
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Logs command
smartcliInstance.addCommand('logs').subscribe({
next: async (argvArg: CliArguments) => {
try {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm logs <id> [options]');
console.log('\nOptions:');
console.log(' --lines <n> Number of lines to show (default: 50)');
console.log(' --follow Stream logs in real-time (like tail -f)');
return;
}
const lines = argvArg.lines || 50;
const follow = argvArg.follow || argvArg.f || false;
// Get initial logs
const response = await tspmIpcClient.request('getLogs', { id, lines });
if (!follow) {
// Static log output
console.log(`Logs for process: ${id} (last ${lines} lines)`);
console.log('─'.repeat(60));
for (const log of response.logs) {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
}
} else {
// Streaming log output
console.log(`Logs for process: ${id} (streaming...)`);
console.log('─'.repeat(60));
// Display initial logs
let lastSeq = 0;
for (const log of response.logs) {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
if (log.seq !== undefined) {
lastSeq = Math.max(lastSeq, log.seq);
}
}
// Subscribe to real-time updates
await tspmIpcClient.subscribe(id, (log: any) => {
// Check for sequence gap or duplicate
if (log.seq !== undefined && log.seq <= lastSeq) {
return; // Skip duplicate
}
if (log.seq !== undefined && log.seq > lastSeq + 1) {
console.log(`[WARNING] Log gap detected: expected seq ${lastSeq + 1}, got ${log.seq}`);
}
const timestamp = new Date(log.timestamp).toLocaleTimeString();
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
console.log(`${timestamp} ${prefix} ${log.message}`);
if (log.seq !== undefined) {
lastSeq = log.seq;
}
});
// Handle Ctrl+C gracefully
let isCleaningUp = false;
const cleanup = async () => {
if (isCleaningUp) return;
isCleaningUp = true;
console.log('\n\nStopping log stream...');
try {
await tspmIpcClient.unsubscribe(id);
await tspmIpcClient.disconnect();
} catch (err) {
// Ignore cleanup errors
}
process.exit(0);
};
process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
// Keep the process alive
await new Promise(() => {}); // Block forever until interrupted
}
} catch (error) {
handleDaemonError(error, 'get logs');
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Start-all command
smartcliInstance.addCommand('start-all').subscribe({
next: async (argvArg: CliArguments) => {
try {
console.log('Starting all processes...');
const response = await tspmIpcClient.request('startAll', {});
if (response.started.length > 0) {
console.log(`✓ Started ${response.started.length} processes:`);
for (const id of response.started) {
console.log(` - ${id}`);
}
}
if (response.failed.length > 0) {
console.log(`✗ Failed to start ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
}
} catch (error) {
handleDaemonError(error, 'start all processes');
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Stop-all command
smartcliInstance.addCommand('stop-all').subscribe({
next: async (argvArg: CliArguments) => {
try {
console.log('Stopping all processes...');
const response = await tspmIpcClient.request('stopAll', {});
if (response.stopped.length > 0) {
console.log(`✓ Stopped ${response.stopped.length} processes:`);
for (const id of response.stopped) {
console.log(` - ${id}`);
}
}
if (response.failed.length > 0) {
console.log(`✗ Failed to stop ${response.failed.length} processes:`);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
}
} catch (error) {
handleDaemonError(error, 'stop all processes');
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Restart-all command
smartcliInstance.addCommand('restart-all').subscribe({
next: async (argvArg: CliArguments) => {
try {
console.log('Restarting all processes...');
const response = await tspmIpcClient.request('restartAll', {});
if (response.restarted.length > 0) {
console.log(`✓ Restarted ${response.restarted.length} processes:`);
for (const id of response.restarted) {
console.log(` - ${id}`);
}
}
if (response.failed.length > 0) {
console.log(
`✗ Failed to restart ${response.failed.length} processes:`,
);
for (const failure of response.failed) {
console.log(` - ${failure.id}: ${failure.error}`);
}
}
} catch (error) {
handleDaemonError(error, 'restart all processes');
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Daemon commands
smartcliInstance.addCommand('daemon').subscribe({
next: async (argvArg: CliArguments) => {
const subCommand = argvArg._[1];
switch (subCommand) {
case 'start':
try {
const status = await tspmIpcClient.getDaemonStatus();
if (status) {
console.log('TSPM daemon is already running');
console.log(` PID: ${status.pid}`);
console.log(
` Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
);
console.log(` Processes: ${status.processCount}`);
return;
}
console.log('Starting TSPM daemon manually...');
// Import spawn to start daemon process
const { spawn } = await import('child_process');
const daemonScript = plugins.path.join(
paths.packageDir,
'dist_ts',
'daemon.js',
);
// Start daemon as a regular background process (not detached)
const daemonProcess = spawn(process.execPath, [daemonScript], {
stdio: 'ignore',
env: {
...process.env,
TSPM_DAEMON_MODE: 'true',
},
});
console.log(`Started daemon with PID: ${daemonProcess.pid}`);
// Wait for daemon to be ready
await new Promise(resolve => setTimeout(resolve, 2000));
const newStatus = await tspmIpcClient.getDaemonStatus();
if (newStatus) {
console.log('✓ TSPM daemon started successfully');
console.log(` PID: ${newStatus.pid}`);
console.log('\nNote: This daemon will run until you stop it or logout.');
console.log('For automatic startup, use "tspm enable" instead.');
}
} catch (error) {
console.error('Error starting daemon:', error.message);
process.exit(1);
}
break;
case 'start-service':
// This is called by systemd - start the daemon directly
console.log('Starting TSPM daemon for systemd service...');
const { startDaemon } = await import('./classes.daemon.js');
await startDaemon();
break;
case 'stop':
try {
console.log('Stopping TSPM daemon...');
await tspmIpcClient.stopDaemon(true);
console.log('✓ TSPM daemon stopped successfully');
} catch (error) {
console.error('Error stopping daemon:', error.message);
process.exit(1);
}
break;
case 'status':
try {
const status = await tspmIpcClient.getDaemonStatus();
if (!status) {
console.log('TSPM daemon is not running');
console.log('Use "tspm daemon start" to start it');
return;
}
console.log('TSPM Daemon Status:');
console.log('─'.repeat(40));
console.log(`Status: ${status.status}`);
console.log(`PID: ${status.pid}`);
console.log(
`Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
);
console.log(`Processes: ${status.processCount}`);
console.log(
`Memory: ${formatMemory(status.memoryUsage || 0)}`,
);
console.log(`CPU: ${status.cpuUsage?.toFixed(1) || 0}s`);
// Disconnect from daemon after getting status
await tspmIpcClient.disconnect();
} catch (error) {
console.error('Error getting daemon status:', error.message);
process.exit(1);
}
break;
default:
console.log('Usage: tspm daemon <command>');
console.log('\nCommands:');
console.log(' start Start the TSPM daemon');
console.log(' stop Stop the TSPM daemon');
console.log(' status Show daemon status');
break;
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Enable command - Enable TSPM daemon as systemd service
smartcliInstance.addCommand('enable').subscribe({
next: async (argvArg: CliArguments) => {
try {
const serviceManager = new TspmServiceManager();
console.log('Enabling TSPM daemon as system service...');
await serviceManager.enableService();
console.log('✓ TSPM daemon enabled and started as system service');
console.log(' The daemon will now start automatically on system boot');
console.log(' Use "tspm disable" to remove the service');
} catch (error) {
console.error('Error enabling service:', error.message);
if (error.message.includes('permission') || error.message.includes('denied')) {
console.log('\nNote: You may need to run this command with sudo');
}
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Disable command - Disable TSPM daemon systemd service
smartcliInstance.addCommand('disable').subscribe({
next: async (argvArg: CliArguments) => {
try {
const serviceManager = new TspmServiceManager();
console.log('Disabling TSPM daemon service...');
await serviceManager.disableService();
console.log('✓ TSPM daemon service disabled');
console.log(' The daemon will no longer start on system boot');
console.log(' Use "tspm enable" to re-enable the service');
} catch (error) {
console.error('Error disabling service:', error.message);
if (error.message.includes('permission') || error.message.includes('denied')) {
console.log('\nNote: You may need to run this command with sudo');
}
process.exit(1);
}
},
error: (err) => {
cliLogger.error(err);
},
complete: () => {},
});
// Start parsing commands
smartcliInstance.startParse();
};
// Re-export from the new modular CLI structure
export * from './cli/index.js';

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,102 @@
import * as plugins from '../../../plugins.js';
import { tspmIpcClient } from '../../../classes.ipcclient.js';
import type { IProcessConfig } from '../../../classes.tspm.js';
import type { CliArguments } from '../../types.js';
import { parseMemoryString, formatMemory } from '../../helpers/memory.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'start',
async (argvArg: CliArguments) => {
const script = argvArg._[1];
if (!script) {
console.error('Error: Please provide a script to run');
console.log('Usage: tspm start <script> [options]');
console.log('\nOptions:');
console.log(' --name <name> Name for the process');
console.log(
' --memory <size> Memory limit (e.g., "512MB", "2GB")',
);
console.log(' --cwd <path> Working directory');
console.log(
' --watch Watch for file changes and restart',
);
console.log(' --watch-paths <paths> Comma-separated paths to watch');
console.log(' --autorestart Auto-restart on crash');
return;
}
const memoryLimit = argvArg.memory
? parseMemoryString(argvArg.memory)
: 512 * 1024 * 1024;
const projectDir = argvArg.cwd || process.cwd();
// Direct .ts support via tsx (bundled with TSPM)
let actualCommand = script;
let commandArgs: string[] | undefined = undefined;
if (script.endsWith('.ts')) {
try {
const tsxPath = await (async () => {
const { createRequire } = await import('module');
const require = createRequire(import.meta.url);
return require.resolve('tsx/dist/cli.mjs');
})();
const scriptPath = plugins.path.isAbsolute(script)
? script
: plugins.path.join(projectDir, script);
actualCommand = tsxPath;
commandArgs = [scriptPath];
} catch {
actualCommand = 'tsx';
commandArgs = [script];
}
}
const name = argvArg.name || script;
const watch = argvArg.watch || false;
const autorestart = argvArg.autorestart !== false; // default true
const watchPaths = argvArg.watchPaths
? typeof argvArg.watchPaths === 'string'
? (argvArg.watchPaths as string).split(',')
: argvArg.watchPaths
: undefined;
const processConfig: IProcessConfig = {
id: name.replace(/[^a-zA-Z0-9-_]/g, '_'),
name,
command: actualCommand,
args: commandArgs,
projectDir,
memoryLimitBytes: memoryLimit,
autorestart,
watch,
watchPaths,
};
console.log(`Starting process: ${name}`);
console.log(
` Command: ${script}${script.endsWith('.ts') ? ' (via tsx)' : ''}`,
);
console.log(` Directory: ${projectDir}`);
console.log(` Memory limit: ${formatMemory(memoryLimit)}`);
console.log(` Auto-restart: ${autorestart}`);
if (watch) {
console.log(` Watch mode: enabled`);
if (watchPaths) console.log(` Watch paths: ${watchPaths.join(', ')}`);
}
const response = await tspmIpcClient.request('start', {
config: processConfig,
});
console.log(`✓ Process started successfully`);
console.log(` ID: ${response.processId}`);
console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`);
},
{ actionLabel: 'start process' },
);
}

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

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

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

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

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

View File

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