Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
8e3cfb624b | |||
33fb02733d | |||
1c2310c185 | |||
d33a001edc | |||
35b6a6a8d0 | |||
50c5fdb0ea |
23
changelog.md
23
changelog.md
@@ -1,5 +1,28 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-08-28 - 2.0.0 - BREAKING CHANGE(daemon)
|
||||
Refactor daemon lifecycle and service management: remove IPC auto-spawn, add TspmServiceManager and CLI enable/disable
|
||||
|
||||
- Do not auto-spawn the daemon from the IPC client anymore — attempts to connect will now error with instructions to start the daemon manually or enable the system service (breaking change).
|
||||
- Add TspmServiceManager to manage the daemon as a systemd service via smartdaemon (enable/disable/reload/status helpers).
|
||||
- CLI: add 'enable' and 'disable' commands to install/uninstall the daemon as a system service and add 'daemon start-service' entrypoint used by systemd.
|
||||
- CLI: improve error handling and user hints when the daemon is not running (suggests `tspm daemon start` or `tspm enable`).
|
||||
- IPC client: removed startDaemon() and related auto-reconnect/start logic; request() no longer auto-reconnects or implicitly start the daemon.
|
||||
- Export TspmServiceManager from the package index so service management is part of the public API.
|
||||
- Updated development plan/readme (readme.plan.md) to reflect the refactor toward proper SmartDaemon integration and migration notes.
|
||||
|
||||
## 2025-08-26 - 1.8.0 - feat(daemon)
|
||||
Add real-time log streaming and pub/sub: daemon publishes per-process logs, IPC client subscribe/unsubscribe, CLI --follow streaming, and sequencing for logs
|
||||
|
||||
- Upgrade @push.rocks/smartipc dependency to ^2.1.2
|
||||
- Daemon: initialize SmartIpc server with heartbeat and publish process logs to topic `logs.<processId>`; write PID file and start heartbeat monitoring
|
||||
- Tspm: re-emit monitor log events as 'process:log' so daemon can broadcast logs
|
||||
- ProcessWrapper: include seq and runId on IProcessLog entries and maintain nextSeq/runId (adds sequencing to logs); default log buffer size applied
|
||||
- TspmIpcClient: improved connect options (retries, timeouts, heartbeat handling), add subscribe/unsubscribe for real-time logs, and use SmartIpc.waitForServer when starting daemon
|
||||
- CLI: add --follow flag to `logs` command to stream live logs, detect sequence gaps/duplicates, and handle graceful cleanup on Ctrl+C
|
||||
- ProcessMonitor: now extends EventEmitter and re-emits process logs for upstream consumption
|
||||
- Standardized heartbeat and IPC timing defaults (heartbeatInterval: 5000ms, heartbeatTimeout: 20000ms, heartbeatInitialGracePeriodMs: 10000ms)
|
||||
|
||||
## 2025-08-25 - 1.7.0 - feat(readme)
|
||||
Add comprehensive README with detailed usage, command reference, daemon management, architecture and development instructions
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@git.zone/tspm",
|
||||
"version": "1.7.0",
|
||||
"version": "2.0.1",
|
||||
"private": false,
|
||||
"description": "a no fuzz process manager",
|
||||
"main": "dist_ts/index.js",
|
||||
@@ -30,10 +30,11 @@
|
||||
"@push.rocks/projectinfo": "^5.0.2",
|
||||
"@push.rocks/smartcli": "^4.0.11",
|
||||
"@push.rocks/smartdaemon": "^2.0.8",
|
||||
"@push.rocks/smartipc": "^2.0.3",
|
||||
"@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",
|
||||
|
281
pnpm-lock.yaml
generated
281
pnpm-lock.yaml
generated
@@ -21,8 +21,8 @@ importers:
|
||||
specifier: ^2.0.8
|
||||
version: 2.0.8
|
||||
'@push.rocks/smartipc':
|
||||
specifier: ^2.0.3
|
||||
version: 2.0.3
|
||||
specifier: ^2.1.2
|
||||
version: 2.1.2
|
||||
'@push.rocks/smartpath':
|
||||
specifier: ^6.0.0
|
||||
version: 6.0.0
|
||||
@@ -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'}
|
||||
@@ -950,8 +803,8 @@ packages:
|
||||
'@push.rocks/smarthash@3.2.3':
|
||||
resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==}
|
||||
|
||||
'@push.rocks/smartipc@2.0.3':
|
||||
resolution: {integrity: sha512-Yty+craFj9lYp6dL1dxHwrF1ykeu02o78D9kNGb5XR+4c53Cci7puqgK9+zbSakaHlNMqKHUWICi50ziGuq5xQ==}
|
||||
'@push.rocks/smartipc@2.1.2':
|
||||
resolution: {integrity: sha512-QyFrohq9jq4ISl6DUyeS1uuWgKxQiTrWZAzIqsGZW/BT36FGoqMpGufgjjkVuBvZtYW8e3hl+lcmT+DHfVMfmg==}
|
||||
|
||||
'@push.rocks/smartjson@5.0.20':
|
||||
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
||||
@@ -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:
|
||||
@@ -6425,7 +6198,7 @@ snapshots:
|
||||
'@types/through2': 2.0.41
|
||||
through2: 4.0.2
|
||||
|
||||
'@push.rocks/smartipc@2.0.3':
|
||||
'@push.rocks/smartipc@2.1.2':
|
||||
dependencies:
|
||||
'@push.rocks/smartdelay': 3.0.5
|
||||
'@push.rocks/smartrx': 3.0.10
|
||||
@@ -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
|
||||
|
239
readme.plan.md
239
readme.plan.md
@@ -1,209 +1,48 @@
|
||||
# TSPM Refactoring Plan: Central Daemon Architecture
|
||||
# TSPM SmartDaemon Service Management Refactor
|
||||
|
||||
## Problem Analysis
|
||||
## Problem
|
||||
Currently TSPM auto-spawns the daemon as a detached child process, which is improper daemon management. It should use smartdaemon for all lifecycle management and never spawn processes directly.
|
||||
|
||||
Currently, each `startAsDaemon` creates an isolated tspm instance with no coordination:
|
||||
## Solution
|
||||
Refactor to use SmartDaemon for proper systemd service integration.
|
||||
|
||||
- Multiple daemons reading/writing same config file
|
||||
- No communication between instances
|
||||
- Inconsistent process management
|
||||
- `tspm list` shows all processes but each daemon only manages its own
|
||||
## Implementation Tasks
|
||||
|
||||
## Proposed Architecture
|
||||
### 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
|
||||
|
||||
### 1. Central Daemon Manager (`ts/classes.daemon.ts`)
|
||||
### Phase 2: Create Service Manager
|
||||
- [x] Create new file ts/classes.servicemanager.ts
|
||||
- [x] Implement TspmServiceManager class
|
||||
- [x] Add getOrCreateService() method
|
||||
- [x] Add enableService() method
|
||||
- [x] Add disableService() method
|
||||
- [x] Add getServiceStatus() method
|
||||
|
||||
- Single daemon instance managing ALL processes
|
||||
- Runs continuously in background
|
||||
- Uses Unix socket for IPC at `~/.tspm/tspm.sock`
|
||||
- Maintains single source of truth for process state
|
||||
### 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
|
||||
- [x] Add 'daemon start-service' internal command for systemd
|
||||
- [x] Update all commands to handle missing daemon gracefully
|
||||
- [x] Add proper error messages with hints
|
||||
|
||||
### 2. IPC Communication Layer (`ts/classes.ipc.ts`)
|
||||
### Phase 4: Update Documentation
|
||||
- [x] Update help text in CLI
|
||||
- [ ] Update command descriptions
|
||||
- [x] Add service management section
|
||||
|
||||
- **Framework**: Use `@push.rocks/smartipc` v2.0.1
|
||||
- **Server**: SmartIpc server in daemon using Unix Domain Socket
|
||||
- **Client**: SmartIpc client in CLI for all operations
|
||||
- **Socket Path**: `~/.tspm/tspm.sock` (Unix) or named pipe (Windows)
|
||||
- **Protocol**: Type-safe request/response with SmartIpc's built-in patterns
|
||||
- **Features**:
|
||||
- Automatic reconnection with exponential backoff
|
||||
- Heartbeat monitoring for daemon health
|
||||
- Type-safe message contracts
|
||||
- **Auto-start**: CLI starts daemon if connection fails
|
||||
### Phase 5: Testing
|
||||
- [x] Test enable command
|
||||
- [x] Test disable command
|
||||
- [x] Test daemon commands
|
||||
- [x] Test error handling when daemon not running
|
||||
- [x] Build and verify TypeScript compilation
|
||||
|
||||
### 3. New CLI Commands
|
||||
|
||||
- `tspm enable` - Start central daemon using systemd/launchd
|
||||
- `tspm disable` - Stop and disable central daemon
|
||||
- `tspm status` - Show daemon status
|
||||
- Remove `startAsDaemon` (replaced by daemon + `tspm start`)
|
||||
|
||||
### 4. Refactored CLI (`ts/cli.ts`)
|
||||
|
||||
All commands become daemon clients:
|
||||
|
||||
```typescript
|
||||
// Before: Direct process management
|
||||
await tspm.start(config);
|
||||
|
||||
// After: Send to daemon
|
||||
await ipcClient.request('start', config);
|
||||
```
|
||||
|
||||
### 5. File Structure Changes
|
||||
|
||||
```
|
||||
ts/
|
||||
├── classes.daemon.ts # New: Central daemon server
|
||||
├── classes.ipc.ts # New: IPC client/server
|
||||
├── classes.tspm.ts # Modified: Used by daemon only
|
||||
├── cli.ts # Modified: Becomes thin client
|
||||
└── classes.daemonmanager.ts # New: Systemd/launchd integration
|
||||
```
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Phase 1: Core Infrastructure
|
||||
|
||||
- [ ] Add `@push.rocks/smartipc` dependency (v2.0.1)
|
||||
- [ ] Create IPC message type definitions for all operations
|
||||
- [ ] Implement daemon server with SmartIpc server
|
||||
- [ ] Create IPC client wrapper for CLI
|
||||
- [ ] Add daemon lifecycle management (enable/disable)
|
||||
|
||||
### Phase 2: CLI Refactoring
|
||||
|
||||
- [ ] Convert all CLI commands to SmartIpc client requests
|
||||
- [ ] Add daemon auto-start logic with connection monitoring
|
||||
- [ ] Leverage SmartIpc's built-in reconnection and error handling
|
||||
- [ ] Implement type-safe message contracts for all commands
|
||||
|
||||
### Phase 3: Migration & Cleanup
|
||||
|
||||
- [ ] Migrate existing config to daemon-compatible format
|
||||
- [ ] Remove `startAsDaemon` command
|
||||
- [ ] Add migration guide for users
|
||||
|
||||
## Technical Details
|
||||
|
||||
### IPC Implementation with SmartIpc
|
||||
|
||||
```typescript
|
||||
// Daemon server setup
|
||||
import { SmartIpc } from '@push.rocks/smartipc';
|
||||
|
||||
const ipcServer = SmartIpc.createServer({
|
||||
id: 'tspm-daemon',
|
||||
socketPath: '~/.tspm/tspm.sock', // Unix socket
|
||||
});
|
||||
|
||||
// Message handlers with type safety
|
||||
ipcServer.onMessage<StartRequest, StartResponse>(
|
||||
'start',
|
||||
async (data, clientId) => {
|
||||
const result = await tspmManager.start(data.config);
|
||||
return { success: true, processId: result.pid };
|
||||
},
|
||||
);
|
||||
|
||||
// CLI client setup
|
||||
const ipcClient = SmartIpc.createClient({
|
||||
id: 'tspm-daemon',
|
||||
socketPath: '~/.tspm/tspm.sock',
|
||||
});
|
||||
|
||||
// Type-safe requests
|
||||
const response = await ipcClient.request<StartRequest, StartResponse>('start', {
|
||||
config: processConfig,
|
||||
});
|
||||
```
|
||||
|
||||
### Message Types
|
||||
|
||||
```typescript
|
||||
interface StartRequest {
|
||||
config: ProcessConfig;
|
||||
}
|
||||
|
||||
interface StartResponse {
|
||||
success: boolean;
|
||||
processId?: number;
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### Daemon State File
|
||||
|
||||
`~/.tspm/daemon.state` - PID, socket path, version
|
||||
|
||||
### Process Management
|
||||
|
||||
Daemon maintains all ProcessMonitor instances internally, CLI never directly manages processes.
|
||||
|
||||
## Key Benefits
|
||||
|
||||
### Architecture Benefits
|
||||
|
||||
- Single daemon manages all processes
|
||||
- Consistent state management
|
||||
- Efficient resource usage
|
||||
- Better process coordination
|
||||
- Proper service integration with OS
|
||||
|
||||
### SmartIpc Advantages
|
||||
|
||||
- **Cross-platform**: Unix sockets on Linux/macOS, named pipes on Windows
|
||||
- **Type-safe**: Full TypeScript support with generic message types
|
||||
- **Resilient**: Automatic reconnection with exponential backoff
|
||||
- **Observable**: Built-in metrics and heartbeat monitoring
|
||||
- **Performant**: Low-latency messaging with zero external dependencies
|
||||
- **Secure**: Connection limits and message size restrictions
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
- Keep existing config format
|
||||
- Auto-migrate on first run
|
||||
- Provide clear upgrade instructions
|
||||
|
||||
## Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────┐ IPC ┌──────────────┐
|
||||
│ CLI │◄────────────►│ Daemon │
|
||||
│ (thin client)│ Socket │ (server) │
|
||||
└─────────────┘ └──────────────┘
|
||||
│ │
|
||||
│ ▼
|
||||
│ ┌──────────────┐
|
||||
│ │ Tspm │
|
||||
│ │ Manager │
|
||||
│ └──────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌──────────────┐
|
||||
│ User │ │ProcessMonitor│
|
||||
│ Commands │ │ Instances │
|
||||
└─────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
## Migration Path
|
||||
|
||||
1. **Version 2.0.0-alpha**: Implement daemon with backwards compatibility
|
||||
2. **Version 2.0.0-beta**: Deprecate `startAsDaemon`, encourage daemon mode
|
||||
3. **Version 2.0.0**: Remove legacy code, daemon-only operation
|
||||
4. **Documentation**: Update all examples and guides
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Unix socket permissions (user-only access)
|
||||
- Validate all IPC messages
|
||||
- Rate limiting for IPC requests
|
||||
- Secure daemon shutdown mechanism
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
- Unit tests for IPC layer
|
||||
- Integration tests for daemon lifecycle
|
||||
- Migration tests from current architecture
|
||||
- Performance tests for multiple processes
|
||||
- Stress tests for IPC communication
|
||||
## Migration Notes
|
||||
- Users will need to run `tspm enable` once after update
|
||||
- Existing daemon instances will stop working
|
||||
- Documentation needs updating to explain new behavior
|
23
simple-test.ts
Normal file
23
simple-test.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
console.log('✓ TypeScript execution works!');
|
||||
|
||||
// Test TypeScript features
|
||||
interface TestData {
|
||||
message: string;
|
||||
timestamp: Date;
|
||||
success: boolean;
|
||||
}
|
||||
|
||||
const data: TestData = {
|
||||
message: 'TSPM can run .ts files directly with tsx!',
|
||||
timestamp: new Date(),
|
||||
success: true
|
||||
};
|
||||
|
||||
console.log('Test data:', data);
|
||||
console.log('✓ TypeScript types and interfaces work');
|
||||
console.log('✓ Test complete');
|
||||
|
||||
// Exit cleanly
|
||||
process.exit(0);
|
28
test-script.ts
Normal file
28
test-script.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
console.log('TypeScript test script started!');
|
||||
|
||||
// Test TypeScript features
|
||||
interface TestData {
|
||||
message: string;
|
||||
timestamp: Date;
|
||||
}
|
||||
|
||||
const data: TestData = {
|
||||
message: 'Hello from TypeScript',
|
||||
timestamp: new Date()
|
||||
};
|
||||
|
||||
console.log(`Message: ${data.message}`);
|
||||
console.log(`Time: ${data.timestamp.toISOString()}`);
|
||||
|
||||
// Keep the process running for a bit
|
||||
let counter = 0;
|
||||
const interval = setInterval(() => {
|
||||
counter++;
|
||||
console.log(`Counter: ${counter}`);
|
||||
if (counter >= 5) {
|
||||
console.log('Test complete!');
|
||||
clearInterval(interval);
|
||||
}
|
||||
}, 1000);
|
@@ -3,6 +3,6 @@
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@git.zone/tspm',
|
||||
version: '1.7.0',
|
||||
version: '2.0.0',
|
||||
description: 'a no fuzz process manager'
|
||||
}
|
||||
|
@@ -43,14 +43,19 @@ export class TspmDaemon {
|
||||
this.ipcServer = plugins.smartipc.SmartIpc.createServer({
|
||||
id: 'tspm-daemon',
|
||||
socketPath: this.socketPath,
|
||||
heartbeat: false, // Disable heartbeat for now
|
||||
autoCleanupSocketFile: true, // Clean up stale sockets
|
||||
socketMode: 0o600, // Set proper permissions
|
||||
heartbeat: true,
|
||||
heartbeatInterval: 5000,
|
||||
heartbeatTimeout: 20000,
|
||||
heartbeatInitialGracePeriodMs: 10000 // Grace period for startup
|
||||
});
|
||||
|
||||
// Register message handlers
|
||||
this.registerHandlers();
|
||||
|
||||
// Start the IPC server
|
||||
await this.ipcServer.start();
|
||||
// Start the IPC server and wait until ready to accept connections
|
||||
await this.ipcServer.start({ readyWhen: 'accepting' });
|
||||
|
||||
// Write PID file
|
||||
await this.writePidFile();
|
||||
@@ -61,6 +66,16 @@ export class TspmDaemon {
|
||||
// Load existing process configurations
|
||||
await this.tspmInstance.loadProcessConfigs();
|
||||
|
||||
// Set up log publishing
|
||||
this.tspmInstance.on('process:log', ({ processId, log }) => {
|
||||
// Publish to topic for this process
|
||||
const topic = `logs.${processId}`;
|
||||
// Broadcast to all connected clients subscribed to this topic
|
||||
if (this.ipcServer) {
|
||||
this.ipcServer.broadcast(`topic:${topic}`, log);
|
||||
}
|
||||
});
|
||||
|
||||
// Set up graceful shutdown handlers
|
||||
this.setupShutdownHandlers();
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
import type {
|
||||
IpcMethodMap,
|
||||
RequestForMethod,
|
||||
@@ -34,28 +34,50 @@ export class TspmIpcClient {
|
||||
const daemonRunning = await this.isDaemonRunning();
|
||||
|
||||
if (!daemonRunning) {
|
||||
console.log('Daemon not running, starting it...');
|
||||
await this.startDaemon();
|
||||
// Wait a bit for daemon to initialize
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
throw new Error(
|
||||
'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'
|
||||
);
|
||||
}
|
||||
|
||||
// Create IPC client
|
||||
this.ipcClient = plugins.smartipc.SmartIpc.createClient({
|
||||
id: 'tspm-cli',
|
||||
socketPath: this.socketPath,
|
||||
heartbeat: false, // Disable heartbeat for now
|
||||
clientId: `cli-${process.pid}`,
|
||||
connectRetry: {
|
||||
enabled: true,
|
||||
initialDelay: 100,
|
||||
maxDelay: 2000,
|
||||
maxAttempts: 30,
|
||||
totalTimeout: 15000,
|
||||
},
|
||||
registerTimeoutMs: 8000,
|
||||
heartbeat: true,
|
||||
heartbeatInterval: 5000,
|
||||
heartbeatTimeout: 20000,
|
||||
heartbeatInitialGracePeriodMs: 10000,
|
||||
heartbeatThrowOnTimeout: false // Don't throw, emit events instead
|
||||
});
|
||||
|
||||
// Connect to the daemon
|
||||
try {
|
||||
await this.ipcClient.connect();
|
||||
await this.ipcClient.connect({ waitForReady: true });
|
||||
this.isConnected = true;
|
||||
|
||||
// Handle heartbeat timeouts gracefully
|
||||
this.ipcClient.on('heartbeatTimeout', () => {
|
||||
console.warn('Heartbeat timeout detected, connection may be degraded');
|
||||
this.isConnected = false;
|
||||
});
|
||||
|
||||
console.log('Connected to TSPM daemon');
|
||||
} catch (error) {
|
||||
console.error('Failed to connect to daemon:', error);
|
||||
throw new Error(
|
||||
'Could not connect to TSPM daemon. Please try running "tspm daemon start" manually.',
|
||||
'Could not connect to TSPM daemon. Please try running "tspm daemon start" or "tspm enable".',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -79,6 +101,7 @@ export class TspmIpcClient {
|
||||
params: RequestForMethod<M>,
|
||||
): Promise<ResponseForMethod<M>> {
|
||||
if (!this.isConnected || !this.ipcClient) {
|
||||
// Try to connect first
|
||||
await this.connect();
|
||||
}
|
||||
|
||||
@@ -90,26 +113,35 @@ export class TspmIpcClient {
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Handle connection errors by trying to reconnect once
|
||||
if (
|
||||
error.message?.includes('ECONNREFUSED') ||
|
||||
error.message?.includes('ENOENT')
|
||||
) {
|
||||
console.log('Connection lost, attempting to reconnect...');
|
||||
this.isConnected = false;
|
||||
await this.connect();
|
||||
|
||||
// Retry the request
|
||||
return await this.ipcClient!.request<
|
||||
RequestForMethod<M>,
|
||||
ResponseForMethod<M>
|
||||
>(method, params);
|
||||
}
|
||||
|
||||
// Don't try to auto-reconnect, just throw the error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to log updates for a specific process
|
||||
*/
|
||||
public async subscribe(processId: string, handler: (log: any) => void): Promise<void> {
|
||||
if (!this.ipcClient || !this.isConnected) {
|
||||
throw new Error('Not connected to daemon');
|
||||
}
|
||||
|
||||
const topic = `logs.${processId}`;
|
||||
await this.ipcClient.subscribe(`topic:${topic}`, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from log updates for a specific process
|
||||
*/
|
||||
public async unsubscribe(processId: string): Promise<void> {
|
||||
if (!this.ipcClient || !this.isConnected) {
|
||||
throw new Error('Not connected to daemon');
|
||||
}
|
||||
|
||||
const topic = `logs.${processId}`;
|
||||
await this.ipcClient.unsubscribe(`topic:${topic}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the daemon is running
|
||||
*/
|
||||
@@ -129,14 +161,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(() => {});
|
||||
@@ -151,44 +184,7 @@ export class TspmIpcClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the daemon process
|
||||
*/
|
||||
private async startDaemon(): Promise<void> {
|
||||
const daemonScript = plugins.path.join(
|
||||
paths.packageDir,
|
||||
'dist_ts',
|
||||
'daemon.js',
|
||||
);
|
||||
|
||||
// Spawn the daemon as a detached process
|
||||
const daemonProcess = spawn(process.execPath, [daemonScript], {
|
||||
detached: true,
|
||||
stdio: ['ignore', 'ignore', 'ignore'],
|
||||
env: {
|
||||
...process.env,
|
||||
TSPM_DAEMON_MODE: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
// Unref the process so the parent can exit
|
||||
daemonProcess.unref();
|
||||
|
||||
console.log(`Started daemon process with PID: ${daemonProcess.pid}`);
|
||||
|
||||
// Wait for daemon to be ready (check for socket file)
|
||||
const maxWaitTime = 10000; // 10 seconds
|
||||
const startTime = Date.now();
|
||||
|
||||
while (Date.now() - startTime < maxWaitTime) {
|
||||
if (await this.isDaemonRunning()) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
}
|
||||
|
||||
throw new Error('Daemon failed to start within timeout period');
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the daemon
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { EventEmitter } from 'events';
|
||||
import { ProcessWrapper, type IProcessLog } from './classes.processwrapper.js';
|
||||
import { Logger, ProcessError, handleError } from './utils.errorhandler.js';
|
||||
|
||||
@@ -13,7 +14,7 @@ export interface IMonitorConfig {
|
||||
logBufferSize?: number; // Optional: number of log lines to keep (default: 100)
|
||||
}
|
||||
|
||||
export class ProcessMonitor {
|
||||
export class ProcessMonitor extends EventEmitter {
|
||||
private processWrapper: ProcessWrapper | null = null;
|
||||
private config: IMonitorConfig;
|
||||
private intervalId: NodeJS.Timeout | null = null;
|
||||
@@ -22,6 +23,7 @@ export class ProcessMonitor {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(config: IMonitorConfig) {
|
||||
super();
|
||||
this.config = config;
|
||||
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
|
||||
}
|
||||
@@ -65,8 +67,10 @@ export class ProcessMonitor {
|
||||
|
||||
// Set up event handlers
|
||||
this.processWrapper.on('log', (log: IProcessLog): void => {
|
||||
// Here we could add handlers to send logs somewhere
|
||||
// For now, we just log system messages to the console
|
||||
// Re-emit the log event for upstream handlers
|
||||
this.emit('log', log);
|
||||
|
||||
// Log system messages to the console
|
||||
if (log.type === 'system') {
|
||||
this.log(log.message);
|
||||
}
|
||||
|
@@ -15,6 +15,8 @@ export interface IProcessLog {
|
||||
timestamp: Date;
|
||||
type: 'stdout' | 'stderr' | 'system';
|
||||
message: string;
|
||||
seq: number;
|
||||
runId: string;
|
||||
}
|
||||
|
||||
export class ProcessWrapper extends EventEmitter {
|
||||
@@ -24,12 +26,15 @@ export class ProcessWrapper extends EventEmitter {
|
||||
private logBufferSize: number;
|
||||
private startTime: Date | null = null;
|
||||
private logger: Logger;
|
||||
private nextSeq: number = 0;
|
||||
private runId: string = '';
|
||||
|
||||
constructor(options: IProcessWrapperOptions) {
|
||||
super();
|
||||
this.options = options;
|
||||
this.logBufferSize = options.logBuffer || 100;
|
||||
this.logger = new Logger(`ProcessWrapper:${options.name}`);
|
||||
this.runId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,6 +222,8 @@ export class ProcessWrapper extends EventEmitter {
|
||||
timestamp: new Date(),
|
||||
type,
|
||||
message,
|
||||
seq: this.nextSeq++,
|
||||
runId: this.runId,
|
||||
};
|
||||
|
||||
this.logs.push(log);
|
||||
@@ -238,6 +245,8 @@ export class ProcessWrapper extends EventEmitter {
|
||||
timestamp: new Date(),
|
||||
type: 'system',
|
||||
message,
|
||||
seq: this.nextSeq++,
|
||||
runId: this.runId,
|
||||
};
|
||||
|
||||
this.logs.push(log);
|
||||
|
103
ts/classes.servicemanager.ts
Normal file
103
ts/classes.servicemanager.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
|
||||
/**
|
||||
* Manages TSPM daemon as a systemd service via smartdaemon
|
||||
*/
|
||||
export class TspmServiceManager {
|
||||
private smartDaemon: plugins.smartdaemon.SmartDaemon;
|
||||
private service: any = null; // SmartDaemonService type is not exported
|
||||
|
||||
constructor() {
|
||||
this.smartDaemon = new plugins.smartdaemon.SmartDaemon();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create the TSPM daemon service configuration
|
||||
*/
|
||||
private async getOrCreateService(): Promise<any> {
|
||||
if (!this.service) {
|
||||
const cliPath = plugins.path.join(paths.packageDir, 'cli.js');
|
||||
|
||||
// Create service configuration
|
||||
this.service = await this.smartDaemon.addService({
|
||||
name: 'tspm-daemon',
|
||||
description: 'TSPM Process Manager Daemon',
|
||||
command: `${process.execPath} ${cliPath} daemon start-service`,
|
||||
workingDir: process.env.HOME || process.cwd(),
|
||||
version: '1.0.0'
|
||||
});
|
||||
}
|
||||
return this.service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable the TSPM daemon as a system service
|
||||
*/
|
||||
public async enableService(): Promise<void> {
|
||||
const service = await this.getOrCreateService();
|
||||
|
||||
// Save service configuration
|
||||
await service.save();
|
||||
|
||||
// Enable service to start on boot
|
||||
await service.enable();
|
||||
|
||||
// Start the service immediately
|
||||
await service.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable the TSPM daemon service
|
||||
*/
|
||||
public async disableService(): Promise<void> {
|
||||
const service = await this.getOrCreateService();
|
||||
|
||||
// Stop the service if running
|
||||
try {
|
||||
await service.stop();
|
||||
} catch (error) {
|
||||
// Service might not be running
|
||||
console.log('Service was not running');
|
||||
}
|
||||
|
||||
// Disable service from starting on boot
|
||||
await service.disable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status of the systemd service
|
||||
*/
|
||||
public async getServiceStatus(): Promise<{
|
||||
enabled: boolean;
|
||||
running: boolean;
|
||||
status: string;
|
||||
}> {
|
||||
try {
|
||||
await this.getOrCreateService();
|
||||
|
||||
// Note: SmartDaemon doesn't provide direct status methods,
|
||||
// so we'll need to check via systemctl commands
|
||||
// This is a simplified implementation
|
||||
return {
|
||||
enabled: true, // Would need to check systemctl is-enabled
|
||||
running: true, // Would need to check systemctl is-active
|
||||
status: 'active'
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
enabled: false,
|
||||
running: false,
|
||||
status: 'inactive'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the systemd service configuration
|
||||
*/
|
||||
public async reloadService(): Promise<void> {
|
||||
const service = await this.getOrCreateService();
|
||||
await service.reload();
|
||||
}
|
||||
}
|
@@ -1,9 +1,11 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as paths from './paths.js';
|
||||
import {
|
||||
ProcessMonitor,
|
||||
type IMonitorConfig,
|
||||
} from './classes.processmonitor.js';
|
||||
import { type IProcessLog } from './classes.processwrapper.js';
|
||||
import { TspmConfig } from './classes.config.js';
|
||||
import {
|
||||
Logger,
|
||||
@@ -30,13 +32,9 @@ export interface IProcessInfo {
|
||||
restarts: number;
|
||||
}
|
||||
|
||||
export interface IProcessLog {
|
||||
timestamp: Date;
|
||||
type: 'stdout' | 'stderr' | 'system';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export class Tspm {
|
||||
|
||||
export class Tspm extends EventEmitter {
|
||||
public processes: Map<string, ProcessMonitor> = new Map();
|
||||
public processConfigs: Map<string, IProcessConfig> = new Map();
|
||||
public processInfo: Map<string, IProcessInfo> = new Map();
|
||||
@@ -45,6 +43,7 @@ export class Tspm {
|
||||
private logger: Logger;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.logger = new Logger('Tspm');
|
||||
this.config = new TspmConfig();
|
||||
this.loadProcessConfigs();
|
||||
@@ -98,6 +97,12 @@ export class Tspm {
|
||||
});
|
||||
|
||||
this.processes.set(config.id, monitor);
|
||||
|
||||
// Set up log event handler to re-emit for pub/sub
|
||||
monitor.on('log', (log: IProcessLog) => {
|
||||
this.emit('process:log', { processId: config.id, log });
|
||||
});
|
||||
|
||||
monitor.start();
|
||||
|
||||
// Update process info
|
||||
|
640
ts/cli.ts
640
ts/cli.ts
@@ -1,638 +1,2 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import * as paths from './paths.js';
|
||||
import { tspmIpcClient } from './classes.ipcclient.js';
|
||||
import { Logger, LogLevel } from './utils.errorhandler.js';
|
||||
import type { IProcessConfig } from './classes.tspm.js';
|
||||
|
||||
export interface CliArguments {
|
||||
verbose?: boolean;
|
||||
watch?: boolean;
|
||||
memory?: string;
|
||||
cwd?: string;
|
||||
daemon?: boolean;
|
||||
test?: boolean;
|
||||
name?: string;
|
||||
autorestart?: boolean;
|
||||
watchPaths?: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
// Helper function to parse memory strings (e.g., "512MB", "2GB")
|
||||
function parseMemoryString(memStr: string): number {
|
||||
const units = {
|
||||
KB: 1024,
|
||||
MB: 1024 * 1024,
|
||||
GB: 1024 * 1024 * 1024,
|
||||
};
|
||||
|
||||
const match = memStr.toUpperCase().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Invalid memory format: ${memStr}. Use format like "512MB" or "2GB"`,
|
||||
);
|
||||
}
|
||||
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = (match[2] || 'MB') as keyof typeof units;
|
||||
|
||||
return Math.floor(value * units[unit]);
|
||||
}
|
||||
|
||||
// Helper function to format memory for display
|
||||
function formatMemory(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
} else if (bytes >= 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
} else if (bytes >= 1024) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
} else {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function for padding strings
|
||||
function pad(str: string, length: number): string {
|
||||
return str.length > length
|
||||
? str.substring(0, length - 3) + '...'
|
||||
: str.padEnd(length);
|
||||
}
|
||||
|
||||
export const run = async (): Promise<void> => {
|
||||
const cliLogger = new Logger('CLI');
|
||||
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||
|
||||
// Check if debug mode is enabled
|
||||
const debugMode = process.env.TSPM_DEBUG === 'true';
|
||||
if (debugMode) {
|
||||
cliLogger.setLevel(LogLevel.DEBUG);
|
||||
cliLogger.debug('Debug mode enabled');
|
||||
}
|
||||
|
||||
const smartcliInstance = new plugins.smartcli.Smartcli();
|
||||
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
|
||||
|
||||
// Default command - show help and list processes
|
||||
smartcliInstance.standardCommand().subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
console.log(
|
||||
`TSPM - TypeScript Process Manager v${tspmProjectinfo.npm.version}`,
|
||||
);
|
||||
console.log('Usage: tspm [command] [options]');
|
||||
console.log('\nCommands:');
|
||||
console.log(' start <script> Start a process');
|
||||
console.log(' list List all processes');
|
||||
console.log(' stop <id> Stop a process');
|
||||
console.log(' restart <id> Restart a process');
|
||||
console.log(' delete <id> Delete a process');
|
||||
console.log(' describe <id> Show details for a process');
|
||||
console.log(' logs <id> Show logs for a process');
|
||||
console.log(' start-all Start all saved processes');
|
||||
console.log(' stop-all Stop all processes');
|
||||
console.log(' restart-all Restart all processes');
|
||||
console.log('\nDaemon Commands:');
|
||||
console.log(' daemon start Start the TSPM daemon');
|
||||
console.log(' daemon stop Stop the TSPM daemon');
|
||||
console.log(' daemon status Show daemon status');
|
||||
console.log(
|
||||
'\nUse tspm [command] --help for more information about a command.',
|
||||
);
|
||||
|
||||
// Show current process list
|
||||
console.log('\nProcess List:');
|
||||
|
||||
try {
|
||||
const response = await tspmIpcClient.request('list', {});
|
||||
const processes = response.processes;
|
||||
|
||||
if (processes.length === 0) {
|
||||
console.log(
|
||||
' No processes running. Use "tspm start" to start a process.',
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
'┌─────────┬─────────────┬───────────┬───────────┬──────────┐',
|
||||
);
|
||||
console.log(
|
||||
'│ ID │ Name │ Status │ Memory │ Restarts │',
|
||||
);
|
||||
console.log(
|
||||
'├─────────┼─────────────┼───────────┼───────────┼──────────┤',
|
||||
);
|
||||
|
||||
for (const proc of processes) {
|
||||
const statusColor =
|
||||
proc.status === 'online'
|
||||
? '\x1b[32m'
|
||||
: proc.status === 'errored'
|
||||
? '\x1b[31m'
|
||||
: '\x1b[33m';
|
||||
const resetColor = '\x1b[0m';
|
||||
|
||||
console.log(
|
||||
`│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad(formatMemory(proc.memory), 9)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
'└─────────┴─────────────┴───────────┴───────────┴──────────┘',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
'Error: Could not connect to TSPM daemon. Use "tspm daemon start" to start it.',
|
||||
);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// Start command
|
||||
smartcliInstance.addCommand('start').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
const script = argvArg._[1];
|
||||
if (!script) {
|
||||
console.error('Error: Please provide a script to run');
|
||||
console.log('Usage: tspm start <script> [options]');
|
||||
console.log('\nOptions:');
|
||||
console.log(' --name <name> Name for the process');
|
||||
console.log(
|
||||
' --memory <size> Memory limit (e.g., "512MB", "2GB")',
|
||||
);
|
||||
console.log(' --cwd <path> Working directory');
|
||||
console.log(
|
||||
' --watch Watch for file changes and restart',
|
||||
);
|
||||
console.log(' --watch-paths <paths> Comma-separated paths to watch');
|
||||
console.log(' --autorestart Auto-restart on crash');
|
||||
return;
|
||||
}
|
||||
|
||||
const memoryLimit = argvArg.memory
|
||||
? parseMemoryString(argvArg.memory)
|
||||
: 512 * 1024 * 1024; // Default 512MB
|
||||
const projectDir = argvArg.cwd || process.cwd();
|
||||
const name = argvArg.name || script;
|
||||
const watch = argvArg.watch || false;
|
||||
const autorestart = argvArg.autorestart !== false; // Default true
|
||||
const watchPaths = argvArg.watchPaths
|
||||
? typeof argvArg.watchPaths === 'string'
|
||||
? (argvArg.watchPaths as string).split(',')
|
||||
: argvArg.watchPaths
|
||||
: undefined;
|
||||
|
||||
const processConfig: IProcessConfig = {
|
||||
id: name.replace(/[^a-zA-Z0-9-_]/g, '_'),
|
||||
name,
|
||||
command: script,
|
||||
projectDir,
|
||||
memoryLimitBytes: memoryLimit,
|
||||
autorestart,
|
||||
watch,
|
||||
watchPaths,
|
||||
};
|
||||
|
||||
console.log(`Starting process: ${name}`);
|
||||
console.log(` Command: ${script}`);
|
||||
console.log(` Directory: ${projectDir}`);
|
||||
console.log(` Memory limit: ${formatMemory(memoryLimit)}`);
|
||||
console.log(` Auto-restart: ${autorestart}`);
|
||||
if (watch) {
|
||||
console.log(` Watch mode: enabled`);
|
||||
if (watchPaths) {
|
||||
console.log(` Watch paths: ${watchPaths.join(', ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await tspmIpcClient.request('start', {
|
||||
config: processConfig,
|
||||
});
|
||||
|
||||
console.log(`✓ Process started successfully`);
|
||||
console.log(` ID: ${response.processId}`);
|
||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||
console.log(` Status: ${response.status}`);
|
||||
} catch (error) {
|
||||
console.error('Error starting process:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// Stop command
|
||||
smartcliInstance.addCommand('stop').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm stop <id>');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Stopping process: ${id}`);
|
||||
const response = await tspmIpcClient.request('stop', { id });
|
||||
|
||||
if (response.success) {
|
||||
console.log(`✓ ${response.message}`);
|
||||
} else {
|
||||
console.error(`✗ Failed to stop process: ${response.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error stopping process:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// Restart command
|
||||
smartcliInstance.addCommand('restart').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm restart <id>');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Restarting process: ${id}`);
|
||||
const response = await tspmIpcClient.request('restart', { id });
|
||||
|
||||
console.log(`✓ Process restarted successfully`);
|
||||
console.log(` ID: ${response.processId}`);
|
||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||
console.log(` Status: ${response.status}`);
|
||||
} catch (error) {
|
||||
console.error('Error restarting process:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// Delete command
|
||||
smartcliInstance.addCommand('delete').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm delete <id>');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Deleting process: ${id}`);
|
||||
const response = await tspmIpcClient.request('delete', { id });
|
||||
|
||||
if (response.success) {
|
||||
console.log(`✓ ${response.message}`);
|
||||
} else {
|
||||
console.error(`✗ Failed to delete process: ${response.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error deleting process:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// List command
|
||||
smartcliInstance.addCommand('list').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
const response = await tspmIpcClient.request('list', {});
|
||||
const processes = response.processes;
|
||||
|
||||
if (processes.length === 0) {
|
||||
console.log('No processes running.');
|
||||
} else {
|
||||
console.log('Process List:');
|
||||
console.log(
|
||||
'┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐',
|
||||
);
|
||||
console.log(
|
||||
'│ ID │ Name │ Status │ PID │ Memory │ Restarts │',
|
||||
);
|
||||
console.log(
|
||||
'├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤',
|
||||
);
|
||||
|
||||
for (const proc of processes) {
|
||||
const statusColor =
|
||||
proc.status === 'online'
|
||||
? '\x1b[32m'
|
||||
: proc.status === 'errored'
|
||||
? '\x1b[31m'
|
||||
: '\x1b[33m';
|
||||
const resetColor = '\x1b[0m';
|
||||
|
||||
console.log(
|
||||
`│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad((proc.pid || '-').toString(), 9)} │ ${pad(formatMemory(proc.memory), 8)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
'└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error listing processes:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// Describe command
|
||||
smartcliInstance.addCommand('describe').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm describe <id>');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await tspmIpcClient.request('describe', { id });
|
||||
|
||||
console.log(`Process Details: ${id}`);
|
||||
console.log('─'.repeat(40));
|
||||
console.log(`Status: ${response.processInfo.status}`);
|
||||
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
|
||||
console.log(
|
||||
`Memory: ${formatMemory(response.processInfo.memory)}`,
|
||||
);
|
||||
console.log(
|
||||
`CPU: ${response.processInfo.cpu ? response.processInfo.cpu.toFixed(1) + '%' : 'N/A'}`,
|
||||
);
|
||||
console.log(
|
||||
`Uptime: ${response.processInfo.uptime ? Math.floor(response.processInfo.uptime / 1000) + 's' : 'N/A'}`,
|
||||
);
|
||||
console.log(`Restarts: ${response.processInfo.restarts}`);
|
||||
console.log('\nConfiguration:');
|
||||
console.log(`Command: ${response.config.command}`);
|
||||
console.log(`Directory: ${response.config.projectDir}`);
|
||||
console.log(
|
||||
`Memory Limit: ${formatMemory(response.config.memoryLimitBytes)}`,
|
||||
);
|
||||
console.log(`Auto-restart: ${response.config.autorestart}`);
|
||||
if (response.config.watch) {
|
||||
console.log(`Watch: enabled`);
|
||||
if (response.config.watchPaths) {
|
||||
console.log(
|
||||
`Watch Paths: ${response.config.watchPaths.join(', ')}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error describing process:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// Logs command
|
||||
smartcliInstance.addCommand('logs').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
const id = argvArg._[1];
|
||||
if (!id) {
|
||||
console.error('Error: Please provide a process ID');
|
||||
console.log('Usage: tspm logs <id>');
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = argvArg.lines || 50;
|
||||
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
||||
|
||||
console.log(`Logs for process: ${id} (last ${lines} lines)`);
|
||||
console.log('─'.repeat(60));
|
||||
|
||||
for (const log of response.logs) {
|
||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||
const prefix = log.type === 'stdout' ? '[OUT]' : '[ERR]';
|
||||
console.log(`${timestamp} ${prefix} ${log.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error getting logs:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// Start-all command
|
||||
smartcliInstance.addCommand('start-all').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
console.log('Starting all processes...');
|
||||
const response = await tspmIpcClient.request('startAll', {});
|
||||
|
||||
if (response.started.length > 0) {
|
||||
console.log(`✓ Started ${response.started.length} processes:`);
|
||||
for (const id of response.started) {
|
||||
console.log(` - ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.failed.length > 0) {
|
||||
console.log(`✗ Failed to start ${response.failed.length} processes:`);
|
||||
for (const failure of response.failed) {
|
||||
console.log(` - ${failure.id}: ${failure.error}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting all processes:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// Stop-all command
|
||||
smartcliInstance.addCommand('stop-all').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
console.log('Stopping all processes...');
|
||||
const response = await tspmIpcClient.request('stopAll', {});
|
||||
|
||||
if (response.stopped.length > 0) {
|
||||
console.log(`✓ Stopped ${response.stopped.length} processes:`);
|
||||
for (const id of response.stopped) {
|
||||
console.log(` - ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.failed.length > 0) {
|
||||
console.log(`✗ Failed to stop ${response.failed.length} processes:`);
|
||||
for (const failure of response.failed) {
|
||||
console.log(` - ${failure.id}: ${failure.error}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error stopping all processes:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// Restart-all command
|
||||
smartcliInstance.addCommand('restart-all').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
try {
|
||||
console.log('Restarting all processes...');
|
||||
const response = await tspmIpcClient.request('restartAll', {});
|
||||
|
||||
if (response.restarted.length > 0) {
|
||||
console.log(`✓ Restarted ${response.restarted.length} processes:`);
|
||||
for (const id of response.restarted) {
|
||||
console.log(` - ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (response.failed.length > 0) {
|
||||
console.log(
|
||||
`✗ Failed to restart ${response.failed.length} processes:`,
|
||||
);
|
||||
for (const failure of response.failed) {
|
||||
console.log(` - ${failure.id}: ${failure.error}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error restarting all processes:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// Daemon commands
|
||||
smartcliInstance.addCommand('daemon').subscribe({
|
||||
next: async (argvArg: CliArguments) => {
|
||||
const subCommand = argvArg._[1];
|
||||
|
||||
switch (subCommand) {
|
||||
case 'start':
|
||||
try {
|
||||
const status = await tspmIpcClient.getDaemonStatus();
|
||||
if (status) {
|
||||
console.log('TSPM daemon is already running');
|
||||
console.log(` PID: ${status.pid}`);
|
||||
console.log(
|
||||
` Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
|
||||
);
|
||||
console.log(` Processes: ${status.processCount}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Starting TSPM daemon...');
|
||||
await tspmIpcClient.connect();
|
||||
console.log('✓ TSPM daemon started successfully');
|
||||
|
||||
const newStatus = await tspmIpcClient.getDaemonStatus();
|
||||
if (newStatus) {
|
||||
console.log(` PID: ${newStatus.pid}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error starting daemon:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'stop':
|
||||
try {
|
||||
console.log('Stopping TSPM daemon...');
|
||||
await tspmIpcClient.stopDaemon(true);
|
||||
console.log('✓ TSPM daemon stopped successfully');
|
||||
} catch (error) {
|
||||
console.error('Error stopping daemon:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
try {
|
||||
const status = await tspmIpcClient.getDaemonStatus();
|
||||
if (!status) {
|
||||
console.log('TSPM daemon is not running');
|
||||
console.log('Use "tspm daemon start" to start it');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('TSPM Daemon Status:');
|
||||
console.log('─'.repeat(40));
|
||||
console.log(`Status: ${status.status}`);
|
||||
console.log(`PID: ${status.pid}`);
|
||||
console.log(
|
||||
`Uptime: ${Math.floor((status.uptime || 0) / 1000)}s`,
|
||||
);
|
||||
console.log(`Processes: ${status.processCount}`);
|
||||
console.log(
|
||||
`Memory: ${formatMemory(status.memoryUsage || 0)}`,
|
||||
);
|
||||
console.log(`CPU: ${status.cpuUsage?.toFixed(1) || 0}s`);
|
||||
} catch (error) {
|
||||
console.error('Error getting daemon status:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Usage: tspm daemon <command>');
|
||||
console.log('\nCommands:');
|
||||
console.log(' start Start the TSPM daemon');
|
||||
console.log(' stop Stop the TSPM daemon');
|
||||
console.log(' status Show daemon status');
|
||||
break;
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
cliLogger.error(err);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
|
||||
// Start parsing commands
|
||||
smartcliInstance.startParse();
|
||||
};
|
||||
// Re-export from the new modular CLI structure
|
||||
export * from './cli/index.js';
|
26
ts/cli/commands/batch/restart-all.ts
Normal file
26
ts/cli/commands/batch/restart-all.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
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' });
|
||||
}
|
26
ts/cli/commands/batch/start-all.ts
Normal file
26
ts/cli/commands/batch/start-all.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
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' });
|
||||
}
|
26
ts/cli/commands/batch/stop-all.ts
Normal file
26
ts/cli/commands/batch/stop-all.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
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' });
|
||||
}
|
138
ts/cli/commands/daemon/index.ts
Normal file
138
ts/cli/commands/daemon/index.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
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: () => {},
|
||||
});
|
||||
}
|
96
ts/cli/commands/default.ts
Normal file
96
ts/cli/commands/default.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
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: () => {},
|
||||
});
|
||||
}
|
24
ts/cli/commands/process/delete.ts
Normal file
24
ts/cli/commands/process/delete.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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' });
|
||||
}
|
38
ts/cli/commands/process/describe.ts
Normal file
38
ts/cli/commands/process/describe.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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' });
|
||||
}
|
37
ts/cli/commands/process/list.ts
Normal file
37
ts/cli/commands/process/list.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
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' });
|
||||
}
|
73
ts/cli/commands/process/logs.ts
Normal file
73
ts/cli/commands/process/logs.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
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')
|
||||
});
|
||||
}
|
23
ts/cli/commands/process/restart.ts
Normal file
23
ts/cli/commands/process/restart.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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' });
|
||||
}
|
83
ts/cli/commands/process/start.ts
Normal file
83
ts/cli/commands/process/start.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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' });
|
||||
}
|
24
ts/cli/commands/process/stop.ts
Normal file
24
ts/cli/commands/process/stop.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
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' });
|
||||
}
|
33
ts/cli/commands/service/disable.ts
Normal file
33
ts/cli/commands/service/disable.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
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: () => {},
|
||||
});
|
||||
}
|
33
ts/cli/commands/service/enable.ts
Normal file
33
ts/cli/commands/service/enable.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
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: () => {},
|
||||
});
|
||||
}
|
16
ts/cli/helpers/argv.ts
Normal file
16
ts/cli/helpers/argv.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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;
|
||||
};
|
14
ts/cli/helpers/errors.ts
Normal file
14
ts/cli/helpers/errors.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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);
|
||||
}
|
17
ts/cli/helpers/formatting.ts
Normal file
17
ts/cli/helpers/formatting.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
// Helper function for padding strings
|
||||
export function pad(str: string, length: number): string {
|
||||
return str.length > length
|
||||
? str.substring(0, length - 3) + '...'
|
||||
: str.padEnd(length);
|
||||
}
|
||||
|
||||
// Helper for unknown errors
|
||||
export const unknownError = (err: any) =>
|
||||
(err?.message && typeof err.message === 'string') ? err.message : String(err);
|
||||
|
||||
// Helper function to format log entries
|
||||
export function formatLog(log: any): string {
|
||||
const timestamp = new Date(log.timestamp).toLocaleTimeString();
|
||||
const prefix = log.type === 'stdout' ? '[OUT]' : log.type === 'stderr' ? '[ERR]' : '[SYS]';
|
||||
return `${timestamp} ${prefix} ${log.message}`;
|
||||
}
|
22
ts/cli/helpers/lifecycle.ts
Normal file
22
ts/cli/helpers/lifecycle.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
// Streaming lifecycle helper
|
||||
export function withStreamingLifecycle(
|
||||
setup: () => Promise<void>,
|
||||
teardown: () => Promise<void>,
|
||||
) {
|
||||
let isCleaningUp = false;
|
||||
const cleanup = async () => {
|
||||
if (isCleaningUp) return;
|
||||
isCleaningUp = true;
|
||||
try {
|
||||
await teardown();
|
||||
} finally {
|
||||
process.exit(0);
|
||||
}
|
||||
};
|
||||
process.once('SIGINT', cleanup);
|
||||
process.once('SIGTERM', cleanup);
|
||||
return (async () => {
|
||||
await setup();
|
||||
await new Promise(() => {}); // keep alive
|
||||
})();
|
||||
}
|
33
ts/cli/helpers/memory.ts
Normal file
33
ts/cli/helpers/memory.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
// Helper function to parse memory strings (e.g., "512MB", "2GB")
|
||||
export function parseMemoryString(memStr: string): number {
|
||||
const units = {
|
||||
KB: 1024,
|
||||
MB: 1024 * 1024,
|
||||
GB: 1024 * 1024 * 1024,
|
||||
};
|
||||
|
||||
const match = memStr.toUpperCase().match(/^(\d+(?:\.\d+)?)\s*(KB|MB|GB)?$/);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Invalid memory format: ${memStr}. Use format like "512MB" or "2GB"`,
|
||||
);
|
||||
}
|
||||
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = (match[2] || 'MB') as keyof typeof units;
|
||||
|
||||
return Math.floor(value * units[unit]);
|
||||
}
|
||||
|
||||
// Helper function to format memory for display
|
||||
export function formatMemory(bytes: number): string {
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
||||
} else if (bytes >= 1024 * 1024) {
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
} else if (bytes >= 1024) {
|
||||
return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
} else {
|
||||
return `${bytes} B`;
|
||||
}
|
||||
}
|
68
ts/cli/index.ts
Normal file
68
ts/cli/index.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import * as plugins from '../plugins.js';
|
||||
import * as paths from '../paths.js';
|
||||
import { Logger, LogLevel } from '../utils.errorhandler.js';
|
||||
|
||||
// Import command registration functions
|
||||
import { registerDefaultCommand } from './commands/default.js';
|
||||
import { registerStartCommand } from './commands/process/start.js';
|
||||
import { registerStopCommand } from './commands/process/stop.js';
|
||||
import { registerRestartCommand } from './commands/process/restart.js';
|
||||
import { registerDeleteCommand } from './commands/process/delete.js';
|
||||
import { registerListCommand } from './commands/process/list.js';
|
||||
import { registerDescribeCommand } from './commands/process/describe.js';
|
||||
import { registerLogsCommand } from './commands/process/logs.js';
|
||||
import { registerStartAllCommand } from './commands/batch/start-all.js';
|
||||
import { registerStopAllCommand } from './commands/batch/stop-all.js';
|
||||
import { registerRestartAllCommand } from './commands/batch/restart-all.js';
|
||||
import { registerDaemonCommand } from './commands/daemon/index.js';
|
||||
import { registerEnableCommand } from './commands/service/enable.js';
|
||||
import { registerDisableCommand } from './commands/service/disable.js';
|
||||
|
||||
// Export types for external use
|
||||
export type { CliArguments } from './types.js';
|
||||
|
||||
/**
|
||||
* Main CLI entry point
|
||||
*/
|
||||
export const run = async (): Promise<void> => {
|
||||
const cliLogger = new Logger('CLI');
|
||||
const tspmProjectinfo = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||
|
||||
// Check if debug mode is enabled
|
||||
const debugMode = process.env.TSPM_DEBUG === 'true';
|
||||
if (debugMode) {
|
||||
cliLogger.setLevel(LogLevel.DEBUG);
|
||||
cliLogger.debug('Debug mode enabled');
|
||||
}
|
||||
|
||||
const smartcliInstance = new plugins.smartcli.Smartcli();
|
||||
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
|
||||
|
||||
// Register all commands
|
||||
// Default command (help + list)
|
||||
registerDefaultCommand(smartcliInstance);
|
||||
|
||||
// Process commands
|
||||
registerStartCommand(smartcliInstance);
|
||||
registerStopCommand(smartcliInstance);
|
||||
registerRestartCommand(smartcliInstance);
|
||||
registerDeleteCommand(smartcliInstance);
|
||||
registerListCommand(smartcliInstance);
|
||||
registerDescribeCommand(smartcliInstance);
|
||||
registerLogsCommand(smartcliInstance);
|
||||
|
||||
// Batch commands
|
||||
registerStartAllCommand(smartcliInstance);
|
||||
registerStopAllCommand(smartcliInstance);
|
||||
registerRestartAllCommand(smartcliInstance);
|
||||
|
||||
// Daemon commands
|
||||
registerDaemonCommand(smartcliInstance);
|
||||
|
||||
// Service commands
|
||||
registerEnableCommand(smartcliInstance);
|
||||
registerDisableCommand(smartcliInstance);
|
||||
|
||||
// Start parsing commands
|
||||
smartcliInstance.startParse();
|
||||
};
|
19
ts/cli/registration/daemon-check.ts
Normal file
19
ts/cli/registration/daemon-check.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
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;
|
||||
}
|
87
ts/cli/registration/index.ts
Normal file
87
ts/cli/registration/index.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import * as plugins from '../../plugins.js';
|
||||
import type { CliArguments, CommandAction, IpcCommandOptions } from '../types.js';
|
||||
import { handleDaemonError } from '../helpers/errors.js';
|
||||
import { unknownError } from '../helpers/formatting.js';
|
||||
import { runIpcCommand } from '../utils/ipc.js';
|
||||
import { ensureDaemonOrHint } from './daemon-check.js';
|
||||
|
||||
/**
|
||||
* Add an IPC-based CLI command with:
|
||||
* - optional daemon preflight
|
||||
* - standard error handling
|
||||
* - automatic disconnect via runIpcCommand unless keepAlive is true
|
||||
*/
|
||||
export function registerIpcCommand(
|
||||
smartcli: plugins.smartcli.Smartcli,
|
||||
name: string,
|
||||
action: CommandAction,
|
||||
opts: IpcCommandOptions = {}
|
||||
) {
|
||||
const { actionLabel = name, keepAlive = false, requireDaemon = true } = opts;
|
||||
|
||||
smartcli.addCommand(name).subscribe({
|
||||
next: async (argv: CliArguments) => {
|
||||
// Early preflight for better UX
|
||||
const ok = await ensureDaemonOrHint(requireDaemon, actionLabel);
|
||||
if (!ok) {
|
||||
process.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Evaluate keepAlive - can be boolean or function
|
||||
const shouldKeepAlive = typeof keepAlive === 'function' ? keepAlive(argv) : keepAlive;
|
||||
|
||||
if (shouldKeepAlive) {
|
||||
// Let action manage its own connection/cleanup lifecycle
|
||||
try {
|
||||
await action(argv);
|
||||
} catch (error) {
|
||||
handleDaemonError(error, actionLabel);
|
||||
}
|
||||
} else {
|
||||
// Auto-disconnect pattern for one-shot IPC commands
|
||||
await runIpcCommand(async () => {
|
||||
try {
|
||||
await action(argv);
|
||||
} catch (error) {
|
||||
handleDaemonError(error, actionLabel);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
// Fallback error path (should be rare with try/catch in next)
|
||||
console.error(`Unexpected error in command "${name}":`, unknownError(err));
|
||||
process.exit(1);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register local commands that don't require IPC/daemon connection
|
||||
* Used for daemon lifecycle, service management, and other local operations
|
||||
*/
|
||||
export function registerLocalCommand(
|
||||
smartcli: plugins.smartcli.Smartcli,
|
||||
name: string,
|
||||
action: (argv: CliArguments) => Promise<void>,
|
||||
opts: { actionLabel?: string } = {}
|
||||
) {
|
||||
const { actionLabel = name } = opts;
|
||||
smartcli.addCommand(name).subscribe({
|
||||
next: async (argv: CliArguments) => {
|
||||
try {
|
||||
await action(argv);
|
||||
} catch (error: any) {
|
||||
console.error(`Error ${actionLabel}:`, error?.message || String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(`Unexpected error in command "${name}":`, unknownError(err));
|
||||
process.exit(1);
|
||||
},
|
||||
complete: () => {},
|
||||
});
|
||||
}
|
20
ts/cli/types.ts
Normal file
20
ts/cli/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export interface CliArguments {
|
||||
verbose?: boolean;
|
||||
watch?: boolean;
|
||||
memory?: string;
|
||||
cwd?: string;
|
||||
daemon?: boolean;
|
||||
test?: boolean;
|
||||
name?: string;
|
||||
autorestart?: boolean;
|
||||
watchPaths?: string[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type CommandAction = (argv: CliArguments) => Promise<void>;
|
||||
|
||||
export interface IpcCommandOptions {
|
||||
actionLabel?: string; // used in error message, e.g. "start process"
|
||||
keepAlive?: boolean | ((argv: CliArguments) => boolean); // true for streaming commands (don't auto-disconnect), or function to determine at runtime
|
||||
requireDaemon?: boolean; // default true for IPC-bound commands
|
||||
}
|
14
ts/cli/utils/ipc.ts
Normal file
14
ts/cli/utils/ipc.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { tspmIpcClient } from '../../classes.ipcclient.js';
|
||||
|
||||
// Helper function to run IPC commands with automatic disconnect
|
||||
export async function runIpcCommand<T>(body: () => Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await body();
|
||||
} finally {
|
||||
try {
|
||||
await tspmIpcClient.disconnect();
|
||||
} catch {
|
||||
// Ignore disconnect errors
|
||||
}
|
||||
}
|
||||
}
|
@@ -2,6 +2,7 @@ export * from './classes.tspm.js';
|
||||
export * from './classes.processmonitor.js';
|
||||
export * from './classes.daemon.js';
|
||||
export * from './classes.ipcclient.js';
|
||||
export * from './classes.servicemanager.js';
|
||||
export * from './ipc.types.js';
|
||||
|
||||
import * as cli from './cli.js';
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import type {
|
||||
IProcessConfig,
|
||||
IProcessInfo,
|
||||
IProcessLog,
|
||||
} from './classes.tspm.js';
|
||||
import type { IProcessLog } from './classes.processwrapper.js';
|
||||
|
||||
// Base message types
|
||||
export interface IpcRequest<T = any> {
|
||||
|
Reference in New Issue
Block a user