Compare commits

...

10 Commits

Author SHA1 Message Date
f4cbdd51e1 5.4.1
Some checks failed
Default (tags) / security (push) Successful in 38s
Default (tags) / test (push) Failing after 3m58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-30 22:08:24 +00:00
1340c1c248 fix(processmonitor): Bump tsbuild devDependency and relax ps-tree callback typing in ProcessMonitor 2025-08-30 22:08:24 +00:00
92a6ecac71 5.4.0
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 3m58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-30 22:01:19 +00:00
5e26b0ab5f feat(daemon): Add CLI systemd service refresh on version mismatch and fix daemon memory leak; update dependencies 2025-08-30 22:01:19 +00:00
e09cf38f30 5.3.2
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 3m58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-30 21:50:43 +00:00
c694672438 fix(daemon): Improve daemon log delivery and process monitor memory accounting; gate debug output and update tests to numeric ProcessId 2025-08-30 21:50:43 +00:00
3b21a338fb 5.3.1
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 3m58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-30 21:16:31 +00:00
28680309ad fix(client(tspmIpcClient)): Use bare topic names for IPC client subscribe/unsubscribe to fix log subscription issues 2025-08-30 21:16:31 +00:00
833573eb10 5.3.0
Some checks failed
Default (tags) / security (push) Successful in 54s
Default (tags) / test (push) Failing after 4m23s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-30 16:55:10 +00:00
ebc20a9232 feat(cli/daemon/processmonitor): Add flexible target resolution and search command; improve restart/backoff and error handling 2025-08-30 16:55:10 +00:00
25 changed files with 585 additions and 168 deletions

View File

@@ -1,5 +1,50 @@
# Changelog # Changelog
## 2025-08-30 - 5.4.1 - fix(processmonitor)
Bump tsbuild devDependency and relax ps-tree callback typing in ProcessMonitor
- Update devDependency @git.zone/tsbuild from ^2.6.7 to ^2.6.8
- Change psTree callback types in ts/daemon/processmonitor.ts to accept any error and ReadonlyArray for children to improve type compatibility
## 2025-08-30 - 5.4.0 - feat(daemon)
Add CLI systemd service refresh on version mismatch and fix daemon memory leak; update dependencies
- CLI: when client and daemon versions differ, prompt to refresh the systemd service and optionally disable/enable the service automatically
- Daemon: clear pidusage state for PIDs on process exit/stop to prevent memory leaks in long-running monitors
- Client: expose smartdaemon in client plugin exports and fix import path for tspm.servicemanager
- Package: tighten dependency ranges (set specific versions) and add @types for pidusage and ps-tree
- Misc: ensure IPC disconnects and PID/socket handling improvements were integrated alongside the above changes
## 2025-08-30 - 5.3.2 - fix(daemon)
Improve daemon log delivery and process monitor memory accounting; gate debug output and update tests to numeric ProcessId
- Deliver process logs only to subscribed clients instead of broadcasting to all connections (reduce unnecessary IPC traffic and noise)
- Implement incremental log memory accounting in ProcessMonitor using an estimateLogSize helper and WeakMap to avoid repeated JSON.stringify and reduce CPU/memory overhead
- Seed the incremental size map when loading persisted logs so memory accounting is accurate after restart
- Trim logs incrementally by subtracting estimated sizes of removed entries (avoids O(n) recalculation)
- Gate verbose console/debug output behind TSPM_DEBUG to prevent spamming in normal runs (applies to ProcessWrapper and ProcessMonitor)
- Improve process wrapper stdout/stderr debug logging to be conditional on debug mode
- Update tests to use numeric ProcessId via toProcessId(...) for consistency with typed IDs
## 2025-08-30 - 5.3.1 - fix(client(tspmIpcClient))
Use bare topic names for IPC client subscribe/unsubscribe to fix log subscription issues
- Updated ts/client/tspm.ipcclient.ts to call ipcClient.subscribe/unsubscribe with the bare topic (e.g. 'logs.<id>') instead of prefixed 'topic:<...>'.
- Added comments clarifying that the IpcClient registers the 'topic:' prefix internally.
- Fixes incorrect topic registration that could prevent log streaming handlers from receiving messages.
## 2025-08-30 - 5.3.0 - feat(cli/daemon/processmonitor)
Add flexible target resolution and search command; improve restart/backoff and error handling
- Add new cli command `search` to find processes by id or name fragment.
- Allow flexible process targets in CLI commands (accepts numeric id, id:<n>, or name:<label>) for start/stop/restart/delete/describe/logs/edit commands.
- Introduce a new daemon IPC method `resolveTarget` to normalize user-provided targets to ProcessId (supports id:<n>, name:<label>, or bare numeric id).
- Keep `remove` as a CLI alias but daemon exposes `delete` only; CLI resolves targets and always calls daemon `delete`.
- Implement scheduled restart/backoff in ProcessMonitor with incremental debounce, max retries, and a 1-hour reset window.
- Emit a `failed` event from ProcessMonitor when max restart attempts are exceeded; ProcessManager listens and marks processes as `errored` and clears pid.
- Ensure desired state is set to `stopped` before deleting a process to avoid race conditions.
- Improve cli output messages to include resolved names alongside numeric ids where available.
## 2025-08-30 - 5.2.0 - feat(cli) ## 2025-08-30 - 5.2.0 - feat(cli)
Preserve CLI environment when adding processes, simplify edit flow, and refresh README docs Preserve CLI environment when adding processes, simplify edit flow, and refresh README docs

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tspm", "name": "@git.zone/tspm",
"version": "5.2.0", "version": "5.4.1",
"private": false, "private": false,
"description": "a no fuzz process manager", "description": "a no fuzz process manager",
"main": "dist_ts/index.js", "main": "dist_ts/index.js",
@@ -24,7 +24,7 @@
"tspm": "./cli.js" "tspm": "./cli.js"
}, },
"devDependencies": { "devDependencies": {
"@git.zone/tsbuild": "^2.6.7", "@git.zone/tsbuild": "^2.6.8",
"@git.zone/tsbundle": "^2.5.1", "@git.zone/tsbundle": "^2.5.1",
"@git.zone/tsrun": "^1.2.46", "@git.zone/tsrun": "^1.2.46",
"@git.zone/tstest": "^2.3.5", "@git.zone/tstest": "^2.3.5",
@@ -40,6 +40,8 @@
"@push.rocks/smartinteract": "^2.0.16", "@push.rocks/smartinteract": "^2.0.16",
"@push.rocks/smartipc": "^2.2.2", "@push.rocks/smartipc": "^2.2.2",
"@push.rocks/smartpath": "^6.0.0", "@push.rocks/smartpath": "^6.0.0",
"@types/pidusage": "^2.0.5",
"@types/ps-tree": "^1.1.6",
"pidusage": "^4.0.1", "pidusage": "^4.0.1",
"ps-tree": "^1.2.0", "ps-tree": "^1.2.0",
"tsx": "^4.20.5" "tsx": "^4.20.5"

74
pnpm-lock.yaml generated
View File

@@ -32,6 +32,12 @@ importers:
'@push.rocks/smartpath': '@push.rocks/smartpath':
specifier: ^6.0.0 specifier: ^6.0.0
version: 6.0.0 version: 6.0.0
'@types/pidusage':
specifier: ^2.0.5
version: 2.0.5
'@types/ps-tree':
specifier: ^1.1.6
version: 1.1.6
pidusage: pidusage:
specifier: ^4.0.1 specifier: ^4.0.1
version: 4.0.1 version: 4.0.1
@@ -43,8 +49,8 @@ importers:
version: 4.20.5 version: 4.20.5
devDependencies: devDependencies:
'@git.zone/tsbuild': '@git.zone/tsbuild':
specifier: ^2.6.7 specifier: ^2.6.8
version: 2.6.7 version: 2.6.8
'@git.zone/tsbundle': '@git.zone/tsbundle':
specifier: ^2.5.1 specifier: ^2.5.1
version: 2.5.1 version: 2.5.1
@@ -530,8 +536,8 @@ packages:
'@esm-bundle/chai@4.3.4-fix.0': '@esm-bundle/chai@4.3.4-fix.0':
resolution: {integrity: sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==} resolution: {integrity: sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==}
'@git.zone/tsbuild@2.6.7': '@git.zone/tsbuild@2.6.8':
resolution: {integrity: sha512-nLRYk1V4gxdEAp5mbLYNdr/in9mFA26L4MPKBKqzASID4lXSYya5sDbLRdDTv+mD0ZRBgdn6e+WMylA0SU4hSw==} resolution: {integrity: sha512-g1z7+MxiYD0xMfuqn8NSWitbfK1OaF0Qolmw7WOmUsHmNF60T1AR02Lo4DtNmnjSpchA+xzDFAQzL1xTcQA39w==}
hasBin: true hasBin: true
'@git.zone/tsbundle@2.5.1': '@git.zone/tsbundle@2.5.1':
@@ -769,8 +775,8 @@ packages:
'@push.rocks/isounique@1.0.5': '@push.rocks/isounique@1.0.5':
resolution: {integrity: sha512-Z0BVqZZOCif1THTbIKWMgg0wxCzt9CyBtBBqQJiZ+jJ0KlQFrQHNHrPt81/LXe/L4x0cxWsn0bpL6W5DNSvNLw==} resolution: {integrity: sha512-Z0BVqZZOCif1THTbIKWMgg0wxCzt9CyBtBBqQJiZ+jJ0KlQFrQHNHrPt81/LXe/L4x0cxWsn0bpL6W5DNSvNLw==}
'@push.rocks/levelcache@3.1.1': '@push.rocks/levelcache@3.2.0':
resolution: {integrity: sha512-+JpDNEt+EuvmbtADGH9SkODxBy+slHDDzs43mAbuMbwpVvi6uNuMK0Mkhrfz9UFpxUSp+cJE/jl/OxdpD0xL1A==} resolution: {integrity: sha512-Ch0Oguta2I0SVi704kHghhBcgfyfS92ua1elRu9d8X1/9LMRYuqvvBAnyXyFxQzI3S8q8QC6EkRdd8CAAYSzRg==}
'@push.rocks/lik@6.1.0': '@push.rocks/lik@6.1.0':
resolution: {integrity: sha512-BoSAIRFNryQ8Sd5EP+35ZBj6vAQ1C60/XjZIO2O65XDyLG8xz7xJ+u5Wm8/fjIJ0WX3h8GkkaCz2tJM34nFT3A==} resolution: {integrity: sha512-BoSAIRFNryQ8Sd5EP+35ZBj6vAQ1C60/XjZIO2O65XDyLG8xz7xJ+u5Wm8/fjIJ0WX3h8GkkaCz2tJM34nFT3A==}
@@ -805,6 +811,9 @@ packages:
'@push.rocks/smartcache@1.0.16': '@push.rocks/smartcache@1.0.16':
resolution: {integrity: sha512-UAXf74eDuH4/RebJhydIbHlYVR3ACYJjniEY/9ZePblu7bIPgwFZqLBE9g1lcKVogbH9yY62dk3rSpgBzenyfQ==} resolution: {integrity: sha512-UAXf74eDuH4/RebJhydIbHlYVR3ACYJjniEY/9ZePblu7bIPgwFZqLBE9g1lcKVogbH9yY62dk3rSpgBzenyfQ==}
'@push.rocks/smartcache@1.0.18':
resolution: {integrity: sha512-3+cmLu9chbnmi4yD4kjlFP/Tn4NReaZIoicEcGTtwbcokTrSDMs3YPdJzIpDZkAs83PW7OcVSHa3Ak5KU5OWzA==}
'@push.rocks/smartchok@1.1.1': '@push.rocks/smartchok@1.1.1':
resolution: {integrity: sha512-WmNigGmn1muBJMANVuJb4F8x3TzgYrnn6YZm6ixTsG+0WFbYevivEwp+J4S7npobLHsR7ynf+Ky8LxRYmsL50A==} resolution: {integrity: sha512-WmNigGmn1muBJMANVuJb4F8x3TzgYrnn6YZm6ixTsG+0WFbYevivEwp+J4S7npobLHsR7ynf+Ky8LxRYmsL50A==}
@@ -832,6 +841,9 @@ packages:
'@push.rocks/smartenv@5.0.13': '@push.rocks/smartenv@5.0.13':
resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==} resolution: {integrity: sha512-ACXmUcHZHl2CF2jnVuRw9saRRrZvJblCRs2d+K5aLR1DfkYFX3eA21kcMlKeLisI3aGNbIj9vz/rowN5qkRkfA==}
'@push.rocks/smarterror@2.0.1':
resolution: {integrity: sha512-iCcH1D8tlDJgMFsaJ6lhdOTKhbU0KoprNv9MRP9o7691QOx4JEDXiHtr/lNtxVo8BUtdb9CF6kazaknO9KuORA==}
'@push.rocks/smartexit@1.0.23': '@push.rocks/smartexit@1.0.23':
resolution: {integrity: sha512-WmwKYcwbHBByoABhHHB+PAjr5475AtD/xBh1mDcqPrFsOOUOZq3BBUdpq25wI3ccu/SZB5IwaimiVzadls6HkA==} resolution: {integrity: sha512-WmwKYcwbHBByoABhHHB+PAjr5475AtD/xBh1mDcqPrFsOOUOZq3BBUdpq25wI3ccu/SZB5IwaimiVzadls6HkA==}
@@ -1012,6 +1024,9 @@ packages:
'@push.rocks/tapbundle@6.0.3': '@push.rocks/tapbundle@6.0.3':
resolution: {integrity: sha512-SuP14V6TPdtd1y1CYTvwTKJdpHa7EzY55NfaaEMxW4oRKvHgJiOiPEiR/IrtL9tSiDMSfrx12waTMgZheYaBug==} resolution: {integrity: sha512-SuP14V6TPdtd1y1CYTvwTKJdpHa7EzY55NfaaEMxW4oRKvHgJiOiPEiR/IrtL9tSiDMSfrx12waTMgZheYaBug==}
'@push.rocks/taskbuffer@3.1.10':
resolution: {integrity: sha512-jT+FxRSk0+IP17q9LD1/Ks8GJBn5TZWgLtfnKRHW/LAZ1bHX/2ARZvAV8fm1T4WMU5s7PyId+y4fkoohG/5Nkg==}
'@push.rocks/taskbuffer@3.1.7': '@push.rocks/taskbuffer@3.1.7':
resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==} resolution: {integrity: sha512-QktGVJPucqQmW/QNGnscf4FAigT1H7JWKFGFdRuDEaOHKFh9qN+PXG3QY7DtZ4jfXdGLxPN4yAufDuPSAJYFnw==}
@@ -1647,9 +1662,15 @@ packages:
'@types/parse5@6.0.3': '@types/parse5@6.0.3':
resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==} resolution: {integrity: sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==}
'@types/pidusage@2.0.5':
resolution: {integrity: sha512-MIiyZI4/MK9UGUXWt0jJcCZhVw7YdhBuTOuqP/BjuLDLZ2PmmViMIQgZiWxtaMicQfAz/kMrZ5T7PKxFSkTeUA==}
'@types/ping@0.4.4': '@types/ping@0.4.4':
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==} resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
'@types/ps-tree@1.1.6':
resolution: {integrity: sha512-PtrlVaOaI44/3pl3cvnlK+GxOM3re2526TJvPvh7W+keHIXdV4TE0ylpPBAcvFQCbGitaTXwL9u+RF7qtVeazQ==}
'@types/qs@6.14.0': '@types/qs@6.14.0':
resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==}
@@ -5599,7 +5620,7 @@ snapshots:
dependencies: dependencies:
'@types/chai': 4.3.20 '@types/chai': 4.3.20
'@git.zone/tsbuild@2.6.7': '@git.zone/tsbuild@2.6.8':
dependencies: dependencies:
'@git.zone/tspublish': 1.10.3 '@git.zone/tspublish': 1.10.3
'@push.rocks/early': 4.0.4 '@push.rocks/early': 4.0.4
@@ -6015,21 +6036,21 @@ snapshots:
'@push.rocks/isounique@1.0.5': {} '@push.rocks/isounique@1.0.5': {}
'@push.rocks/levelcache@3.1.1': '@push.rocks/levelcache@3.2.0':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
'@push.rocks/smartbucket': 3.3.10 '@push.rocks/smartbucket': 3.3.10
'@push.rocks/smartcache': 1.0.16 '@push.rocks/smartcache': 1.0.18
'@push.rocks/smartenv': 5.0.13 '@push.rocks/smartenv': 5.0.13
'@push.rocks/smartexit': 1.0.23 '@push.rocks/smartexit': 1.0.23
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartjson': 5.0.20 '@push.rocks/smartjson': 5.0.20
'@push.rocks/smartpath': 5.1.0 '@push.rocks/smartpath': 6.0.0
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartstring': 4.0.15 '@push.rocks/smartstring': 4.0.15
'@push.rocks/smartunique': 3.0.9 '@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer': 3.1.7 '@push.rocks/taskbuffer': 3.1.10
'@tsclass/tsclass': 4.4.4 '@tsclass/tsclass': 9.2.0
transitivePeerDependencies: transitivePeerDependencies:
- aws-crt - aws-crt
@@ -6158,6 +6179,14 @@ snapshots:
'@pushrocks/smartpromise': 3.1.10 '@pushrocks/smartpromise': 3.1.10
'@pushrocks/smarttime': 4.0.1 '@pushrocks/smarttime': 4.0.1
'@push.rocks/smartcache@1.0.18':
dependencies:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smarterror': 2.0.1
'@push.rocks/smarthash': 3.2.3
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smarttime': 4.1.1
'@push.rocks/smartchok@1.1.1': '@push.rocks/smartchok@1.1.1':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
@@ -6237,6 +6266,11 @@ snapshots:
dependencies: dependencies:
'@push.rocks/smartpromise': 4.2.3 '@push.rocks/smartpromise': 4.2.3
'@push.rocks/smarterror@2.0.1':
dependencies:
clean-stack: 1.3.0
make-error-cause: 2.3.0
'@push.rocks/smartexit@1.0.23': '@push.rocks/smartexit@1.0.23':
dependencies: dependencies:
'@push.rocks/lik': 6.1.0 '@push.rocks/lik': 6.1.0
@@ -6443,7 +6477,7 @@ snapshots:
'@push.rocks/smartnpm@2.0.6': '@push.rocks/smartnpm@2.0.6':
dependencies: dependencies:
'@push.rocks/consolecolor': 2.0.3 '@push.rocks/consolecolor': 2.0.3
'@push.rocks/levelcache': 3.1.1 '@push.rocks/levelcache': 3.2.0
'@push.rocks/smartarchive': 4.2.2 '@push.rocks/smartarchive': 4.2.2
'@push.rocks/smartfile': 11.2.7 '@push.rocks/smartfile': 11.2.7
'@push.rocks/smartpath': 6.0.0 '@push.rocks/smartpath': 6.0.0
@@ -6745,6 +6779,16 @@ snapshots:
- supports-color - supports-color
- utf-8-validate - utf-8-validate
'@push.rocks/taskbuffer@3.1.10':
dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartlog': 3.1.8
'@push.rocks/smartpromise': 4.2.3
'@push.rocks/smartrx': 3.0.10
'@push.rocks/smarttime': 4.1.1
'@push.rocks/smartunique': 3.0.9
'@push.rocks/taskbuffer@3.1.7': '@push.rocks/taskbuffer@3.1.7':
dependencies: dependencies:
'@push.rocks/lik': 6.2.2 '@push.rocks/lik': 6.2.2
@@ -7592,8 +7636,12 @@ snapshots:
'@types/parse5@6.0.3': {} '@types/parse5@6.0.3': {}
'@types/pidusage@2.0.5': {}
'@types/ping@0.4.4': {} '@types/ping@0.4.4': {}
'@types/ps-tree@1.1.6': {}
'@types/qs@6.14.0': {} '@types/qs@6.14.0': {}
'@types/randomatic@3.1.5': {} '@types/randomatic@3.1.5': {}

View File

@@ -38,21 +38,23 @@ npm install --save-dev @git.zone/tspm
# Add a process (creates config without starting) # Add a process (creates config without starting)
tspm add "node server.js" --name my-server --memory 1GB tspm add "node server.js" --name my-server --memory 1GB
# Start the process # Start the process (by name or id)
tspm start my-server tspm start name:my-server
# or
tspm start id:1
# Or add and start in one go # Or add and start in one go
tspm add "node app.js" --name my-app tspm add "node app.js" --name my-app
tspm start my-app tspm start name:my-app
# List all processes # List all processes
tspm list tspm list
# View logs # View logs
tspm logs my-app tspm logs name:my-app
# Stop a process # Stop a process
tspm stop my-app tspm stop name:my-app
``` ```
## 📋 Commands ## 📋 Commands
@@ -86,38 +88,38 @@ tspm add "tsx watch src/index.ts" --name dev-server --watch --watch-paths "src,c
tspm add "node worker.js" --name one-time-job --autorestart false tspm add "node worker.js" --name one-time-job --autorestart false
``` ```
#### `tspm start <id>` #### `tspm start <id|id:N|name:LABEL>`
Start a previously added process by its ID or name. Start a previously added process by its ID or name.
```bash ```bash
tspm start my-server tspm start name:my-server
tspm start 1 # Can also use numeric ID tspm start id:1 # Or a bare numeric id: tspm start 1
``` ```
#### `tspm stop <id>` #### `tspm stop <id|id:N|name:LABEL>`
Gracefully stop a running process (SIGTERM → SIGKILL after timeout). Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
```bash ```bash
tspm stop my-server tspm stop name:my-server
``` ```
#### `tspm restart <id>` #### `tspm restart <id|id:N|name:LABEL>`
Stop and restart a process with the same configuration. Stop and restart a process with the same configuration.
```bash ```bash
tspm restart my-server tspm restart name:my-server
``` ```
#### `tspm delete <id>` / `tspm remove <id>` #### `tspm delete <id|id:N|name:LABEL>` / `tspm remove <id|id:N|name:LABEL>`
Stop and remove a process from TSPM management. Also deletes persisted logs. Stop and remove a process from TSPM management. Also deletes persisted logs.
```bash ```bash
tspm delete old-server tspm delete name:old-server
tspm remove old-server # Alias for delete tspm remove name:old-server # Alias for delete (daemon handles delete)
``` ```
#### `tspm edit <id>` #### `tspm edit <id>`
@@ -148,12 +150,12 @@ tspm list
└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘ └─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘
``` ```
#### `tspm describe <id>` #### `tspm describe <id|id:N|name:LABEL>`
Get detailed information about a specific process. Get detailed information about a specific process.
```bash ```bash
tspm describe my-server tspm describe name:my-server
# Output: # Output:
Process Details: my-server Process Details: my-server
@@ -173,7 +175,7 @@ Auto-restart: true
Watch: disabled Watch: disabled
``` ```
#### `tspm logs <id> [options]` #### `tspm logs <id|id:N|name:LABEL> [options]`
View process logs (stdout and stderr combined). View process logs (stdout and stderr combined).
@@ -183,13 +185,13 @@ View process logs (stdout and stderr combined).
```bash ```bash
# View last 50 lines # View last 50 lines
tspm logs my-server tspm logs name:my-server
# View last 100 lines # View last 100 lines
tspm logs my-server --lines 100 tspm logs name:my-server --lines 100
# Follow logs in real-time # Follow logs in real-time
tspm logs my-server --follow tspm logs name:my-server --follow
``` ```
### Batch Operations ### Batch Operations
@@ -496,7 +498,31 @@ Common issues:
- **"Daemon not running"**: Run `tspm daemon start` or `tspm enable` - **"Daemon not running"**: Run `tspm daemon start` or `tspm enable`
- **"Permission denied"**: Check socket permissions in `~/.tspm/` - **"Permission denied"**: Check socket permissions in `~/.tspm/`
- **"Process won't start"**: Check logs with `tspm logs <id>` - **"Process won't start"**: Check logs with `tspm logs <id|id:N|name:LABEL>`
## 🎯 Targeting Processes (IDs and Names)
Most process commands accept the following target formats:
- Numeric ID: `tspm start 1`
- Explicit ID: `tspm start id:1`
- Explicit name: `tspm start name:api-server`
Notes:
- Names must be used with the `name:` prefix.
- If multiple processes share the same name, the CLI will report the ambiguous matches. Use `id:N` to disambiguate.
- Use `tspm search <query>` to discover IDs by name or ID fragments.
### `tspm search <query>`
Search processes by name or ID substring and print matching IDs (and names when available):
```bash
tspm search api
# Matches for "api":
# - id:3 name:api-server
```
- **"Memory limit exceeded"**: Increase limit with `tspm edit <id>` - **"Memory limit exceeded"**: Increase limit with `tspm edit <id>`
## 🤝 Why Choose TSPM? ## 🤝 Why Choose TSPM?
@@ -515,6 +541,20 @@ Common issues:
### Perfect For ### Perfect For
### Restart Backoff and Failure Handling
TSPM automatically restarts crashed processes with an incremental backoff:
- Debounce delay grows linearly from 1s up to 10s for consecutive retries.
- After the 10th retry, the process is marked as failed (status: "errored") and auto-restarts stop.
- The retry counter resets if no retry happens for 1 hour since the last attempt.
You can manually restart a failed process at any time:
```bash
tspm restart id:1
```
- 🚀 **Production Node.js apps** - Reliable process management - 🚀 **Production Node.js apps** - Reliable process management
- 🔧 **Microservices** - Manage multiple services easily - 🔧 **Microservices** - Manage multiple services easily
- 👨💻 **Development** - File watching and auto-restart - 👨💻 **Development** - File watching and auto-restart

View File

@@ -5,6 +5,7 @@ import * as fs from 'fs/promises';
import * as os from 'os'; import * as os from 'os';
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import { tspmIpcClient, TspmIpcClient } from '../ts/client/tspm.ipcclient.js'; import { tspmIpcClient, TspmIpcClient } from '../ts/client/tspm.ipcclient.js';
import { toProcessId } from '../ts/shared/protocol/id.js';
// Helper to ensure daemon is stopped before tests // Helper to ensure daemon is stopped before tests
async function ensureDaemonStopped() { async function ensureDaemonStopped() {
@@ -160,7 +161,7 @@ tap.test('Process management through daemon', async (tools) => {
// Test 2: Start a test process // Test 2: Start a test process
const testConfig: tspm.IProcessConfig = { const testConfig: tspm.IProcessConfig = {
id: 'test-echo', id: toProcessId(1001),
name: 'Test Echo Process', name: 'Test Echo Process',
command: 'echo "Test process"', command: 'echo "Test process"',
projectDir: process.cwd(), projectDir: process.cwd(),
@@ -172,7 +173,7 @@ tap.test('Process management through daemon', async (tools) => {
config: testConfig, config: testConfig,
}); });
console.log('Start response:', startResponse); console.log('Start response:', startResponse);
expect(startResponse.processId).toEqual('test-echo'); expect(startResponse.processId).toEqual(1001);
expect(startResponse.status).toBeDefined(); expect(startResponse.status).toBeDefined();
// Test 3: List processes (should have one process) // Test 3: List processes (should have one process)
@@ -180,27 +181,27 @@ tap.test('Process management through daemon', async (tools) => {
console.log('List after start:', listResponse); console.log('List after start:', listResponse);
expect(listResponse.processes.length).toBeGreaterThanOrEqual(1); expect(listResponse.processes.length).toBeGreaterThanOrEqual(1);
const procInfo = listResponse.processes.find((p) => p.id === 'test-echo'); const procInfo = listResponse.processes.find((p) => p.id === toProcessId(1001));
expect(procInfo).toBeDefined(); expect(procInfo).toBeDefined();
expect(procInfo?.id).toEqual('test-echo'); expect(procInfo?.id).toEqual(1001);
// Test 4: Describe the process // Test 4: Describe the process
const describeResponse = await tspmIpcClient.request('describe', { const describeResponse = await tspmIpcClient.request('describe', {
id: 'test-echo', id: toProcessId(1001),
}); });
console.log('Describe:', describeResponse); console.log('Describe:', describeResponse);
expect(describeResponse.processInfo).toBeDefined(); expect(describeResponse.processInfo).toBeDefined();
expect(describeResponse.config).toBeDefined(); expect(describeResponse.config).toBeDefined();
expect(describeResponse.config.id).toEqual('test-echo'); expect(describeResponse.config.id).toEqual(1001);
// Test 5: Stop the process // Test 5: Stop the process
const stopResponse = await tspmIpcClient.request('stop', { id: 'test-echo' }); const stopResponse = await tspmIpcClient.request('stop', { id: toProcessId(1001) });
console.log('Stop response:', stopResponse); console.log('Stop response:', stopResponse);
expect(stopResponse.success).toEqual(true); expect(stopResponse.success).toEqual(true);
// Test 6: Delete the process // Test 6: Delete the process
const deleteResponse = await tspmIpcClient.request('delete', { const deleteResponse = await tspmIpcClient.request('delete', {
id: 'test-echo', id: toProcessId(1001),
}); });
console.log('Delete response:', deleteResponse); console.log('Delete response:', deleteResponse);
expect(deleteResponse.success).toEqual(true); expect(deleteResponse.success).toEqual(true);
@@ -208,9 +209,7 @@ tap.test('Process management through daemon', async (tools) => {
// Test 7: Verify process is gone // Test 7: Verify process is gone
listResponse = await tspmIpcClient.request('list', {}); listResponse = await tspmIpcClient.request('list', {});
console.log('List after delete:', listResponse); console.log('List after delete:', listResponse);
const deletedProcess = listResponse.processes.find( const deletedProcess = listResponse.processes.find((p) => p.id === toProcessId(1001));
(p) => p.id === 'test-echo',
);
expect(deletedProcess).toBeUndefined(); expect(deletedProcess).toBeUndefined();
// Cleanup: stop daemon // Cleanup: stop daemon
@@ -241,7 +240,7 @@ tap.test('Batch operations through daemon', async (tools) => {
// Add multiple test processes // Add multiple test processes
const testConfigs: tspm.IProcessConfig[] = [ const testConfigs: tspm.IProcessConfig[] = [
{ {
id: 'batch-test-1', id: toProcessId(1101),
name: 'Batch Test 1', name: 'Batch Test 1',
command: 'echo "Process 1"', command: 'echo "Process 1"',
projectDir: process.cwd(), projectDir: process.cwd(),
@@ -249,7 +248,7 @@ tap.test('Batch operations through daemon', async (tools) => {
autorestart: false, autorestart: false,
}, },
{ {
id: 'batch-test-2', id: toProcessId(1102),
name: 'Batch Test 2', name: 'Batch Test 2',
command: 'echo "Process 2"', command: 'echo "Process 2"',
projectDir: process.cwd(), projectDir: process.cwd(),
@@ -308,7 +307,7 @@ tap.test('Daemon error handling', async (tools) => {
// Test 1: Try to stop non-existent process // Test 1: Try to stop non-existent process
try { try {
await tspmIpcClient.request('stop', { id: 'non-existent-process' }); await tspmIpcClient.request('stop', { id: toProcessId(99999) });
expect(false).toEqual(true); // Should not reach here expect(false).toEqual(true); // Should not reach here
} catch (error) { } catch (error) {
expect(error.message).toInclude('Failed to stop process'); expect(error.message).toInclude('Failed to stop process');
@@ -316,7 +315,7 @@ tap.test('Daemon error handling', async (tools) => {
// Test 2: Try to describe non-existent process // Test 2: Try to describe non-existent process
try { try {
await tspmIpcClient.request('describe', { id: 'non-existent-process' }); await tspmIpcClient.request('describe', { id: toProcessId(99999) });
expect(false).toEqual(true); // Should not reach here expect(false).toEqual(true); // Should not reach here
} catch (error) { } catch (error) {
expect(error.message).toInclude('not found'); expect(error.message).toInclude('not found');
@@ -324,7 +323,7 @@ tap.test('Daemon error handling', async (tools) => {
// Test 3: Try to restart non-existent process // Test 3: Try to restart non-existent process
try { try {
await tspmIpcClient.request('restart', { id: 'non-existent-process' }); await tspmIpcClient.request('restart', { id: toProcessId(99999) });
expect(false).toEqual(true); // Should not reach here expect(false).toEqual(true); // Should not reach here
} catch (error) { } catch (error) {
expect(error.message).toInclude('Failed to restart process'); expect(error.message).toInclude('Failed to restart process');

View File

@@ -1,5 +1,6 @@
import { expect, tap } from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as tspm from '../ts/index.js'; import * as tspm from '../ts/index.js';
import { toProcessId } from '../ts/shared/protocol/id.js';
import { join } from 'path'; import { join } from 'path';
// Basic module import test // Basic module import test
@@ -51,7 +52,7 @@ async function exampleUsingIpcClient() {
// Start a process using the request method // Start a process using the request method
await client.request('start', { await client.request('start', {
config: { config: {
id: 'web-server', id: toProcessId(2001),
name: 'Web Server', name: 'Web Server',
projectDir: '/path/to/web/project', projectDir: '/path/to/web/project',
command: 'npm run serve', command: 'npm run serve',
@@ -65,7 +66,7 @@ async function exampleUsingIpcClient() {
// Start another process // Start another process
await client.request('start', { await client.request('start', {
config: { config: {
id: 'api-server', id: toProcessId(2002),
name: 'API Server', name: 'API Server',
projectDir: '/path/to/api/project', projectDir: '/path/to/api/project',
command: 'npm run api', command: 'npm run api',
@@ -80,13 +81,13 @@ async function exampleUsingIpcClient() {
// Get logs from a process // Get logs from a process
const logs = await client.request('getLogs', { const logs = await client.request('getLogs', {
id: 'web-server', id: toProcessId(2001),
lines: 20, lines: 20,
}); });
console.log('Web server logs:', logs.logs); console.log('Web server logs:', logs.logs);
// Stop a process // Stop a process
await client.request('stop', { id: 'api-server' }); await client.request('stop', { id: toProcessId(2002) });
// Handle graceful shutdown // Handle graceful shutdown
process.on('SIGINT', async () => { process.on('SIGINT', async () => {

View File

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

View File

@@ -22,13 +22,14 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
); );
console.log(' disable Disable TSPM system service'); console.log(' disable Disable TSPM system service');
console.log('\nProcess Commands:'); console.log('\nProcess Commands:');
console.log(' start <script> Start a process'); console.log(' start <id|id:N|name:LBL> Start a process');
console.log(' list List all processes'); console.log(' list List all processes');
console.log(' stop <id> Stop a process'); console.log(' stop <id|id:N|name:LBL> Stop a process');
console.log(' restart <id> Restart a process'); console.log(' restart <id|id:N|name:LBL> Restart a process');
console.log(' delete <id> Delete a process'); console.log(' delete <id|id:N|name:LBL> Delete a process');
console.log(' describe <id> Show details for a process'); console.log(' describe <id|id:N|name:LBL> Show details for a process');
console.log(' logs <id> Show logs for a process'); console.log(' logs <id|id:N|name:LBL> Show logs for a process');
console.log(' search <query> Find processes by id/name');
console.log(' start-all Start all saved processes'); console.log(' start-all Start all saved processes');
console.log(' stop-all Stop all processes'); console.log(' stop-all Stop all processes');
console.log(' restart-all Restart all processes'); console.log(' restart-all Restart all processes');

View File

@@ -8,23 +8,25 @@ export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
smartcli, smartcli,
['delete', 'remove'], ['delete', 'remove'],
async (argvArg: CliArguments) => { async (argvArg: CliArguments) => {
const id = argvArg._[1]; const target = argvArg._[1];
if (!id) { if (!target) {
console.error('Error: Please provide a process ID'); console.error('Error: Please provide a process target');
console.log('Usage: tspm delete <id> | tspm remove <id>'); console.log('Usage: tspm delete <id|id:N|name:LABEL> | tspm remove <id|id:N|name:LABEL>');
return; return;
} }
// Determine if command was 'remove' to use the new IPC route, otherwise 'delete' // Determine if command was 'remove' to use the new IPC route, otherwise 'delete'
const cmd = String(argvArg._[0]); const cmd = String(argvArg._[0]);
const useRemove = cmd === 'remove'; const isRemoveAlias = cmd === 'remove';
console.log(`${useRemove ? 'Removing' : 'Deleting'} process: ${id}`); console.log(`${isRemoveAlias ? 'Removing' : 'Deleting'} process: ${target}`);
const response = await tspmIpcClient.request(useRemove ? 'remove' : 'delete', { id } as any); const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
// Always call daemon 'delete'; 'remove' is CLI alias only
const response = await tspmIpcClient.request('delete', { id: resolved.id } as any);
if (response.success) { if (response.success) {
console.log(`${response.message || (useRemove ? 'Removed successfully' : 'Deleted successfully')}`); console.log(`${response.message || (isRemoveAlias ? 'Removed successfully' : 'Deleted successfully')}`);
} else { } else {
console.error(`✗ Failed to ${useRemove ? 'remove' : 'delete'} process: ${response.message}`); console.error(`✗ Failed to ${isRemoveAlias ? 'remove' : 'delete'} process: ${response.message}`);
} }
}, },
{ actionLabel: 'delete/remove process' }, { actionLabel: 'delete/remove process' },

View File

@@ -9,16 +9,17 @@ export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
smartcli, smartcli,
'describe', 'describe',
async (argvArg: CliArguments) => { async (argvArg: CliArguments) => {
const id = argvArg._[1]; const target = argvArg._[1];
if (!id) { if (!target) {
console.error('Error: Please provide a process ID'); console.error('Error: Please provide a process target');
console.log('Usage: tspm describe <id>'); console.log('Usage: tspm describe <id | id:N | name:LABEL>');
return; return;
} }
const response = await tspmIpcClient.request('describe', { id }); const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
const response = await tspmIpcClient.request('describe', { id: resolved.id });
console.log(`Process Details: ${id}`); console.log(`Process Details: ${response.config.name || resolved.id}`);
console.log('─'.repeat(40)); console.log('─'.repeat(40));
console.log(`Status: ${response.processInfo.status}`); console.log(`Status: ${response.processInfo.status}`);
console.log(`PID: ${response.processInfo.pid || 'N/A'}`); console.log(`PID: ${response.processInfo.pid || 'N/A'}`);

View File

@@ -9,17 +9,16 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
smartcli, smartcli,
'edit', 'edit',
async (argvArg: CliArguments) => { async (argvArg: CliArguments) => {
const idRaw = argvArg._[1]; const target = argvArg._[1];
if (!idRaw) { if (!target) {
console.error('Error: Please provide a process ID to edit'); console.error('Error: Please provide a process target to edit');
console.log('Usage: tspm edit <id>'); console.log('Usage: tspm edit <id | id:N | name:LABEL>');
return; return;
} }
const id = idRaw; // Resolve and load current config
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
// Load current config const { config } = await tspmIpcClient.request('describe', { id: resolved.id });
const { config } = await tspmIpcClient.request('describe', { id });
// Interactive editing is temporarily disabled - needs smartinteract API update // Interactive editing is temporarily disabled - needs smartinteract API update
console.log('Interactive editing is temporarily disabled.'); console.log('Interactive editing is temporarily disabled.');
@@ -63,7 +62,7 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
}; };
const updateResponse = await tspmIpcClient.request('update', { const updateResponse = await tspmIpcClient.request('update', {
id, id: resolved.id,
updates, updates,
}); });
@@ -73,4 +72,3 @@ export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
{ actionLabel: 'edit process config' }, { actionLabel: 'edit process config' },
); );
} }

View File

@@ -11,10 +11,10 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
smartcli, smartcli,
'logs', 'logs',
async (argvArg: CliArguments) => { async (argvArg: CliArguments) => {
const id = argvArg._[1]; const target = argvArg._[1];
if (!id) { if (!target) {
console.error('Error: Please provide a process ID'); console.error('Error: Please provide a process target');
console.log('Usage: tspm logs <id> [options]'); console.log('Usage: tspm logs <id | id:N | name:LABEL> [options]');
console.log('\nOptions:'); console.log('\nOptions:');
console.log(' --lines <n> Number of lines to show (default: 50)'); console.log(' --lines <n> Number of lines to show (default: 50)');
console.log(' --follow Stream logs in real-time (like tail -f)'); console.log(' --follow Stream logs in real-time (like tail -f)');
@@ -24,6 +24,8 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
const lines = getNumber(argvArg, 'lines', 50); const lines = getNumber(argvArg, 'lines', 50);
const follow = getBool(argvArg, 'follow', 'f'); const follow = getBool(argvArg, 'follow', 'f');
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
const id = resolved.id;
const response = await tspmIpcClient.request('getLogs', { id, lines }); const response = await tspmIpcClient.request('getLogs', { id, lines });
if (!follow) { if (!follow) {
@@ -44,7 +46,7 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
} }
// Streaming mode // Streaming mode
console.log(`Logs for process: ${id} (streaming...)`); console.log(`Logs for process: ${resolved.name || id} (streaming...)`);
console.log('─'.repeat(60)); console.log('─'.repeat(60));
let lastSeq = 0; let lastSeq = 0;

View File

@@ -1,6 +1,5 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../../plugins.js';
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js'; import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import { toProcessId } from '../../../shared/protocol/id.js';
import type { CliArguments } from '../../types.js'; import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js'; import { registerIpcCommand } from '../../registration/index.js';
@@ -11,9 +10,9 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
async (argvArg: CliArguments) => { async (argvArg: CliArguments) => {
const arg = argvArg._[1]; const arg = argvArg._[1];
if (!arg) { if (!arg) {
console.error('Error: Please provide a process ID or "all"'); console.error('Error: Please provide a process target or "all"');
console.log('Usage:'); console.log('Usage:');
console.log(' tspm restart <id>'); console.log(' tspm restart <id | id:N | name:LABEL>');
console.log(' tspm restart all'); console.log(' tspm restart all');
return; return;
} }
@@ -33,12 +32,13 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
return; return;
} }
const id = String(arg); const target = String(arg);
console.log(`Restarting process: ${id}`); console.log(`Restarting process: ${target}`);
const response = await tspmIpcClient.request('restart', { id: toProcessId(id) }); const resolved = await tspmIpcClient.request('resolveTarget', { target });
const response = await tspmIpcClient.request('restart', { id: resolved.id });
console.log(`✓ Process restarted successfully`); console.log(`✓ Process restarted successfully`);
console.log(` ID: ${response.processId}`); console.log(` ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
console.log(` PID: ${response.pid || 'N/A'}`); console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`); console.log(` Status: ${response.status}`);
}, },

View File

@@ -0,0 +1,62 @@
import * as plugins from '../../plugins.js';
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
export function registerSearchCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'search',
async (argvArg: CliArguments) => {
const query = String(argvArg._[1] || '').trim();
if (!query) {
console.error('Error: Please provide a search query');
console.log('Usage: tspm search <name-fragment | id-fragment>');
return;
}
// Fetch list of processes, then enrich with names via describe
const listRes = await tspmIpcClient.request('list', {});
const processes = listRes.processes;
// If there are no processes, short-circuit
if (processes.length === 0) {
console.log('No processes found.');
return;
}
const lowerQ = query.toLowerCase();
const matches: Array<{ id: number; name?: string }> = [];
// Collect describe calls to obtain names
for (const proc of processes) {
try {
const desc = await tspmIpcClient.request('describe', { id: proc.id });
const name = desc.config.name || '';
const idStr = String(proc.id);
if (name.toLowerCase().includes(lowerQ) || idStr.includes(query)) {
matches.push({ id: proc.id, name });
}
} catch {
// Ignore describe errors for individual processes
}
}
if (matches.length === 0) {
console.log(`No matches for "${query}"`);
return;
}
console.log(`Matches for "${query}":`);
for (const m of matches) {
if (m.name) {
console.log(`- id:${m.id}\tname:${m.name}`);
} else {
console.log(`- id:${m.id}`);
}
}
},
{ actionLabel: 'search processes' },
);
}

View File

@@ -10,17 +10,18 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
smartcli, smartcli,
'start', 'start',
async (argvArg: CliArguments) => { async (argvArg: CliArguments) => {
const id = argvArg._[1]; const target = argvArg._[1];
if (!id) { if (!target) {
console.error('Error: Please provide a process ID to start'); console.error('Error: Please provide a process target to start');
console.log('Usage: tspm start <id>'); console.log('Usage: tspm start <id | id:N | name:LABEL>');
return; return;
} }
console.log(`Starting process id ${id}...`); console.log(`Starting process: ${target}...`);
const response = await tspmIpcClient.request('startById', { id }); const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
const response = await tspmIpcClient.request('startById', { id: resolved.id });
console.log('✓ Process started'); console.log('✓ Process started');
console.log(` ID: ${response.processId}`); console.log(` ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
console.log(` PID: ${response.pid || 'N/A'}`); console.log(` PID: ${response.pid || 'N/A'}`);
console.log(` Status: ${response.status}`); console.log(` Status: ${response.status}`);
}, },

View File

@@ -8,15 +8,16 @@ export function registerStopCommand(smartcli: plugins.smartcli.Smartcli) {
smartcli, smartcli,
'stop', 'stop',
async (argvArg: CliArguments) => { async (argvArg: CliArguments) => {
const id = argvArg._[1]; const target = argvArg._[1];
if (!id) { if (!target) {
console.error('Error: Please provide a process ID'); console.error('Error: Please provide a process target');
console.log('Usage: tspm stop <id>'); console.log('Usage: tspm stop <id | id:N | name:LABEL>');
return; return;
} }
console.log(`Stopping process: ${id}`); console.log(`Stopping process: ${target}`);
const response = await tspmIpcClient.request('stop', { id }); const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
const response = await tspmIpcClient.request('stop', { id: resolved.id });
if (response.success) { if (response.success) {
console.log(`${response.message}`); console.log(`${response.message}`);

View File

@@ -2,6 +2,7 @@ import * as plugins from './plugins.js';
import { tspmIpcClient } from '../client/tspm.ipcclient.js'; import { tspmIpcClient } from '../client/tspm.ipcclient.js';
import * as paths from '../paths.js'; import * as paths from '../paths.js';
import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js'; import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js';
import { TspmServiceManager } from '../client/tspm.servicemanager.js';
// Import command registration functions // Import command registration functions
import { registerDefaultCommand } from './commands/default.js'; import { registerDefaultCommand } from './commands/default.js';
@@ -10,6 +11,7 @@ import { registerAddCommand } from './commands/process/add.js';
import { registerStopCommand } from './commands/process/stop.js'; import { registerStopCommand } from './commands/process/stop.js';
import { registerRestartCommand } from './commands/process/restart.js'; import { registerRestartCommand } from './commands/process/restart.js';
import { registerDeleteCommand } from './commands/process/delete.js'; import { registerDeleteCommand } from './commands/process/delete.js';
import { registerSearchCommand } from './commands/process/search.js';
import { registerListCommand } from './commands/process/list.js'; import { registerListCommand } from './commands/process/list.js';
import { registerDescribeCommand } from './commands/process/describe.js'; import { registerDescribeCommand } from './commands/process/describe.js';
import { registerLogsCommand } from './commands/process/logs.js'; import { registerLogsCommand } from './commands/process/logs.js';
@@ -50,6 +52,38 @@ export const run = async (): Promise<void> => {
console.log( console.log(
`Daemon: running v${status.version || 'unknown'} (pid ${status.pid})`, `Daemon: running v${status.version || 'unknown'} (pid ${status.pid})`,
); );
// If versions mismatch, offer to refresh the systemd service
if (status.version && status.version !== cliVersion) {
console.log('\nVersion mismatch detected:');
console.log(` CLI: v${cliVersion}`);
console.log(` Daemon: v${status.version}`);
console.log(
'\nThis can happen after upgrading tspm. The systemd service may still point to an older version.\n' +
'You can refresh the service (equivalent to "tspm disable" then "tspm enable").',
);
// Ask the user for confirmation
const confirm = await plugins.smartinteract.SmartInteract.getCliConfirmation(
'Refresh the systemd service now?',
true,
);
if (confirm) {
try {
const sm = new TspmServiceManager();
console.log('Refreshing TSPM system service...');
await sm.disableService();
await sm.enableService();
console.log('✓ Service refreshed. Daemon restarted via systemd.');
} catch (err: any) {
console.error(
'Failed to refresh service automatically. You can try manually:\n tspm disable && tspm enable',
);
console.error(err?.message || String(err));
}
} else {
console.log('Skipped service refresh.');
}
}
} else { } else {
console.log('Daemon: not running'); console.log('Daemon: not running');
} }
@@ -74,6 +108,7 @@ export const run = async (): Promise<void> => {
registerDescribeCommand(smartcliInstance); registerDescribeCommand(smartcliInstance);
registerLogsCommand(smartcliInstance); registerLogsCommand(smartcliInstance);
registerEditCommand(smartcliInstance); registerEditCommand(smartcliInstance);
registerSearchCommand(smartcliInstance);
// Batch commands // Batch commands
registerStartAllCommand(smartcliInstance); registerStartAllCommand(smartcliInstance);

View File

@@ -1,6 +1,7 @@
// Minimal plugin set for lightweight client startup // Minimal plugin set for lightweight client startup
import * as path from 'node:path'; import * as path from 'node:path';
import * as smartdaemon from '@push.rocks/smartdaemon';
import * as smartipc from '@push.rocks/smartipc'; import * as smartipc from '@push.rocks/smartipc';
export { path, smartipc }; export { path, smartdaemon, smartipc };

View File

@@ -155,7 +155,9 @@ export class TspmIpcClient {
const id = toProcessId(processId); const id = toProcessId(processId);
const topic = `logs.${id}`; const topic = `logs.${id}`;
await this.ipcClient.subscribe(`topic:${topic}`, handler); // Note: IpcClient.subscribe expects the bare topic (without the 'topic:' prefix)
// and will register a handler for 'topic:<topic>' internally.
await this.ipcClient.subscribe(topic, handler);
} }
/** /**
@@ -168,7 +170,8 @@ export class TspmIpcClient {
const id = toProcessId(processId); const id = toProcessId(processId);
const topic = `logs.${id}`; const topic = `logs.${id}`;
await this.ipcClient.unsubscribe(`topic:${topic}`); // Pass bare topic; client handles 'topic:' prefix internally
await this.ipcClient.unsubscribe(topic);
} }
/** /**

View File

@@ -1,4 +1,4 @@
import * as plugins from '../plugins.js'; import * as plugins from './plugins.js';
import * as paths from '../paths.js'; import * as paths from '../paths.js';
/** /**

View File

@@ -156,6 +156,11 @@ export class ProcessManager extends EventEmitter {
this.updateProcessInfo(config.id, { pid: undefined }); this.updateProcessInfo(config.id, { pid: undefined });
}); });
// Set up failure handler to mark process as errored
monitor.on('failed', () => {
this.updateProcessInfo(config.id, { status: 'errored', pid: undefined });
});
await monitor.start(); await monitor.start();
// Wait a moment for the process to spawn and get its PID // Wait a moment for the process to spawn and get its PID
@@ -327,6 +332,11 @@ export class ProcessManager extends EventEmitter {
}); });
} }
// Mark errored on failure events
newMonitor.on('failed', () => {
this.updateProcessInfo(id, { status: 'errored', pid: undefined });
});
this.logger.info(`Successfully restarted process with id '${id}'`); this.logger.info(`Successfully restarted process with id '${id}'`);
} catch (error: Error | unknown) { } catch (error: Error | unknown) {
const processError = new ProcessError( const processError = new ProcessError(

View File

@@ -18,6 +18,12 @@ export class ProcessMonitor extends EventEmitter {
private processId?: ProcessId; private processId?: ProcessId;
private currentLogMemorySize: number = 0; private currentLogMemorySize: number = 0;
private readonly MAX_LOG_MEMORY_SIZE = 10 * 1024 * 1024; // 10MB private readonly MAX_LOG_MEMORY_SIZE = 10 * 1024 * 1024; // 10MB
// Track approximate size per log to avoid O(n) JSON stringify on every update
private logSizeMap: WeakMap<IProcessLog, number> = new WeakMap();
private restartTimer: NodeJS.Timeout | null = null;
private lastRetryAt: number | null = null;
private readonly MAX_RETRIES = 10;
private readonly RESET_WINDOW_MS = 60 * 60 * 1000; // 1 hour
constructor(config: IMonitorConfig & { id?: ProcessId }) { constructor(config: IMonitorConfig & { id?: ProcessId }) {
super(); super();
@@ -35,7 +41,13 @@ export class ProcessMonitor extends EventEmitter {
const persistedLogs = await this.logPersistence.loadLogs(this.processId); const persistedLogs = await this.logPersistence.loadLogs(this.processId);
if (persistedLogs.length > 0) { if (persistedLogs.length > 0) {
this.logs = persistedLogs; this.logs = persistedLogs;
this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs); // Recalculate size once from scratch and seed the size map
this.currentLogMemorySize = 0;
for (const log of this.logs) {
const size = this.estimateLogSize(log);
this.logSizeMap.set(log, size);
this.currentLogMemorySize += size;
}
this.logger.info(`Loaded ${persistedLogs.length} persisted logs from disk`); this.logger.info(`Loaded ${persistedLogs.length} persisted logs from disk`);
// Delete the persisted file after loading // Delete the persisted file after loading
@@ -83,18 +95,27 @@ export class ProcessMonitor extends EventEmitter {
this.processWrapper.on('log', (log: IProcessLog): void => { this.processWrapper.on('log', (log: IProcessLog): void => {
// Store the log in our buffer // Store the log in our buffer
this.logs.push(log); this.logs.push(log);
console.error(`[ProcessMonitor:${this.config.name}] Received log (type=${log.type}): ${log.message}`); if (process.env.TSPM_DEBUG) {
console.error(`[ProcessMonitor:${this.config.name}] Logs array now has ${this.logs.length} items`); console.error(
`[ProcessMonitor:${this.config.name}] Received log (type=${log.type}): ${log.message}`,
);
console.error(
`[ProcessMonitor:${this.config.name}] Logs array now has ${this.logs.length} items`,
);
}
this.logger.debug(`ProcessMonitor received log: ${log.message}`); this.logger.debug(`ProcessMonitor received log: ${log.message}`);
// Update memory size tracking // Update memory size tracking incrementally
this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs); const approxSize = this.estimateLogSize(log);
this.logSizeMap.set(log, approxSize);
this.currentLogMemorySize += approxSize;
// Trim logs if they exceed memory limit (10MB) // Trim logs if they exceed memory limit (10MB)
while (this.currentLogMemorySize > this.MAX_LOG_MEMORY_SIZE && this.logs.length > 1) { while (this.currentLogMemorySize > this.MAX_LOG_MEMORY_SIZE && this.logs.length > 1) {
// Remove oldest logs until we're under the memory limit // Remove oldest logs until we're under the memory limit
this.logs.shift(); const removed = this.logs.shift()!;
this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs); const removedSize = this.logSizeMap.get(removed) ?? this.estimateLogSize(removed);
this.currentLogMemorySize -= removedSize;
} }
// Re-emit the log event for upstream handlers // Re-emit the log event for upstream handlers
@@ -118,6 +139,14 @@ export class ProcessMonitor extends EventEmitter {
this.logger.info(exitMsg); this.logger.info(exitMsg);
this.log(exitMsg); this.log(exitMsg);
// Clear pidusage internal state for this PID to prevent memory leaks
try {
const pidToClear = this.processWrapper?.getPid();
if (pidToClear) {
(plugins.pidusage as any)?.clear?.(pidToClear);
}
} catch {}
// Flush logs to disk on exit // Flush logs to disk on exit
if (this.processId && this.logs.length > 0) { if (this.processId && this.logs.length > 0) {
try { try {
@@ -132,10 +161,7 @@ export class ProcessMonitor extends EventEmitter {
this.emit('exit', code, signal); this.emit('exit', code, signal);
if (!this.stopped) { if (!this.stopped) {
this.logger.info('Restarting process...'); this.scheduleRestart('exit');
this.log('Restarting process...');
this.restartCount++;
this.spawnProcess();
} else { } else {
this.logger.debug( this.logger.debug(
'Not restarting process because monitor is stopped', 'Not restarting process because monitor is stopped',
@@ -164,10 +190,7 @@ export class ProcessMonitor extends EventEmitter {
} }
if (!this.stopped) { if (!this.stopped) {
this.logger.info('Restarting process due to error...'); this.scheduleRestart('error');
this.log('Restarting process due to error...');
this.restartCount++;
this.spawnProcess();
} else { } else {
this.logger.debug('Not restarting process because monitor is stopped'); this.logger.debug('Not restarting process because monitor is stopped');
} }
@@ -185,6 +208,49 @@ export class ProcessMonitor extends EventEmitter {
} }
} }
/**
* Schedule a restart with incremental debounce and failure cutoff.
*/
private scheduleRestart(reason: 'exit' | 'error'): void {
const now = Date.now();
// Reset window: if last retry was more than 1 hour ago, reset counter
if (this.lastRetryAt && now - this.lastRetryAt >= this.RESET_WINDOW_MS) {
this.logger.info('Resetting retry counter after 1 hour window');
this.restartCount = 0;
}
// Already at or above max retries?
if (this.restartCount >= this.MAX_RETRIES) {
const msg = 'Maximum restart attempts reached. Marking process as failed.';
this.logger.warn(msg);
this.log(msg);
this.stopped = true;
// Emit a specific event so manager can set status to errored
this.emit('failed');
return;
}
// Increment and compute delay (1..10 seconds)
this.restartCount++;
const delaySec = Math.min(this.restartCount, 10);
const msg = `Restarting process in ${delaySec}s (attempt ${this.restartCount}/${this.MAX_RETRIES}) due to ${reason}...`;
this.logger.info(msg);
this.log(msg);
// Clear existing timer if any, then schedule
if (this.restartTimer) {
clearTimeout(this.restartTimer);
}
this.lastRetryAt = now;
this.restartTimer = setTimeout(() => {
// If stopped in the meantime, do not spawn
if (this.stopped) {
return;
}
this.spawnProcess();
}, delaySec * 1000);
}
/** /**
* Monitor the process group's memory usage. If the total memory exceeds the limit, * Monitor the process group's memory usage. If the total memory exceeds the limit,
* kill the process group so that the 'exit' handler can restart it. * kill the process group so that the 'exit' handler can restart it.
@@ -200,12 +266,14 @@ export class ProcessMonitor extends EventEmitter {
`Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`, `Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`,
); );
// Only log to the process log at longer intervals to avoid spamming // Only log memory usage in debug mode to avoid spamming
if (process.env.TSPM_DEBUG) {
this.log( this.log(
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes( `Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
memoryUsage, memoryUsage,
)} (${memoryUsage} bytes)`, )} (${memoryUsage} bytes)`,
); );
}
if (memoryUsage > memoryLimit) { if (memoryUsage > memoryLimit) {
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes( const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
@@ -243,7 +311,7 @@ export class ProcessMonitor extends EventEmitter {
plugins.psTree( plugins.psTree(
pid, pid,
(err: Error | null, children: Array<{ PID: string }>) => { (err: any, children: ReadonlyArray<{ PID: string }>) => {
if (err) { if (err) {
const processError = new ProcessError( const processError = new ProcessError(
`Failed to get process tree: ${err.message}`, `Failed to get process tree: ${err.message}`,
@@ -325,6 +393,13 @@ export class ProcessMonitor extends EventEmitter {
clearInterval(this.intervalId); clearInterval(this.intervalId);
} }
if (this.processWrapper) { if (this.processWrapper) {
// Clear pidusage state for current PID before stopping to avoid leaks
try {
const pidToClear = this.processWrapper.getPid();
if (pidToClear) {
(plugins.pidusage as any)?.clear?.(pidToClear);
}
} catch {}
this.processWrapper.stop(); this.processWrapper.stop();
} }
} }
@@ -333,7 +408,11 @@ export class ProcessMonitor extends EventEmitter {
* Get the current logs from the process * Get the current logs from the process
*/ */
public getLogs(limit?: number): IProcessLog[] { public getLogs(limit?: number): IProcessLog[] {
console.error(`[ProcessMonitor:${this.config.name}] getLogs called, logs.length=${this.logs.length}, limit=${limit}`); if (process.env.TSPM_DEBUG) {
console.error(
`[ProcessMonitor:${this.config.name}] getLogs called, logs.length=${this.logs.length}, limit=${limit}`,
);
}
this.logger.debug(`Getting logs, total stored: ${this.logs.length}`); this.logger.debug(`Getting logs, total stored: ${this.logs.length}`);
if (limit && limit > 0) { if (limit && limit > 0) {
return this.logs.slice(-limit); return this.logs.slice(-limit);
@@ -376,4 +455,17 @@ export class ProcessMonitor extends EventEmitter {
const prefix = this.config.name ? `[${this.config.name}] ` : ''; const prefix = this.config.name ? `[${this.config.name}] ` : '';
console.log(prefix + message); console.log(prefix + message);
} }
/**
* Estimate approximate memory size in bytes for a log entry.
* Keeps CPU low by avoiding JSON.stringify on the full array.
*/
private estimateLogSize(log: IProcessLog): number {
const messageBytes = Buffer.byteLength(log.message || '', 'utf8');
const typeBytes = Buffer.byteLength(log.type || '', 'utf8');
const runIdBytes = Buffer.byteLength((log as any).runId || '', 'utf8');
// Rough overhead for object structure, keys, timestamp/seq values
const overhead = 64;
return messageBytes + typeBytes + runIdBytes + overhead;
}
} }

View File

@@ -90,9 +90,19 @@ export class ProcessWrapper extends EventEmitter {
// Capture stdout // Capture stdout
if (this.process.stdout) { if (this.process.stdout) {
console.error(`[ProcessWrapper] Setting up stdout listener for process ${this.process.pid}`); if (process.env.TSPM_DEBUG) {
console.error(
`[ProcessWrapper] Setting up stdout listener for process ${this.process.pid}`,
);
}
this.process.stdout.on('data', (data) => { this.process.stdout.on('data', (data) => {
console.error(`[ProcessWrapper] Received stdout data from PID ${this.process?.pid}: ${data.toString().substring(0, 100)}`); if (process.env.TSPM_DEBUG) {
console.error(
`[ProcessWrapper] Received stdout data from PID ${this.process?.pid}: ${data
.toString()
.substring(0, 100)}`,
);
}
// Add data to remainder buffer and split by newlines // Add data to remainder buffer and split by newlines
const text = this.stdoutRemainder + data.toString(); const text = this.stdoutRemainder + data.toString();
const lines = text.split('\n'); const lines = text.split('\n');
@@ -102,7 +112,9 @@ export class ProcessWrapper extends EventEmitter {
// Process complete lines // Process complete lines
for (const line of lines) { for (const line of lines) {
if (process.env.TSPM_DEBUG) {
console.error(`[ProcessWrapper] Processing stdout line: ${line}`); console.error(`[ProcessWrapper] Processing stdout line: ${line}`);
}
this.logger.debug(`Captured stdout: ${line}`); this.logger.debug(`Captured stdout: ${line}`);
this.addLog('stdout', line); this.addLog('stdout', line);
} }

View File

@@ -97,9 +97,25 @@ export class TspmDaemon {
this.tspmInstance.on('process:log', ({ processId, log }) => { this.tspmInstance.on('process:log', ({ processId, log }) => {
// Publish to topic for this process // Publish to topic for this process
const topic = `logs.${processId}`; const topic = `logs.${processId}`;
// Broadcast to all connected clients subscribed to this topic // Deliver only to subscribed clients
if (this.ipcServer) { if (this.ipcServer) {
this.ipcServer.broadcast(`topic:${topic}`, log); try {
const topicIndex = (this.ipcServer as any).topicIndex as Map<string, Set<string>> | undefined;
const subscribers = topicIndex?.get(topic);
if (subscribers && subscribers.size > 0) {
// Send directly to subscribers for this topic
for (const clientId of subscribers) {
this.ipcServer
.sendToClient(clientId, `topic:${topic}`, log)
.catch((err: any) => {
// Surface but don't fail the loop
console.error('[IPC] sendToClient error:', err?.message || err);
});
}
}
} catch (err: any) {
console.error('[IPC] Topic delivery error:', err?.message || err);
}
} }
}); });
@@ -208,6 +224,8 @@ export class TspmDaemon {
async (request: RequestForMethod<'delete'>) => { async (request: RequestForMethod<'delete'>) => {
try { try {
const id = toProcessId(request.id); const id = toProcessId(request.id);
// Ensure desired state reflects stopped before deletion
await this.tspmInstance.setDesiredState(id, 'stopped');
await this.tspmInstance.delete(id); await this.tspmInstance.delete(id);
return { return {
success: true, success: true,
@@ -246,18 +264,7 @@ export class TspmDaemon {
}, },
); );
this.ipcServer.onMessage( // Note: 'remove' is only a CLI alias. Daemon exposes 'delete' only.
'remove',
async (request: RequestForMethod<'remove'>) => {
try {
const id = toProcessId(request.id);
await this.tspmInstance.delete(id);
return { success: true, message: `Process ${id} deleted successfully` };
} catch (error) {
throw new Error(`Failed to remove process: ${error.message}`);
}
},
);
this.ipcServer.onMessage( this.ipcServer.onMessage(
'list', 'list',
@@ -291,6 +298,58 @@ export class TspmDaemon {
}, },
); );
// Resolve target (id:n | name:foo | numeric string) to ProcessId
this.ipcServer.onMessage(
'resolveTarget',
async (request: RequestForMethod<'resolveTarget'>) => {
const raw = String(request.target || '').trim();
if (!raw) {
throw new Error('Empty target');
}
// id:<n>
if (/^id:\s*\d+$/i.test(raw)) {
const idNum = raw.split(':')[1].trim();
const id = toProcessId(idNum);
const config = this.tspmInstance.processConfigs.get(id);
if (!config) throw new Error(`Process ${id} not found`);
return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
}
// name:<label>
if (/^name:/i.test(raw)) {
const name = raw.slice(raw.indexOf(':') + 1).trim();
if (!name) throw new Error('Missing name after name:');
const matches = Array.from(this.tspmInstance.processConfigs.values()).filter(
(c) => (c.name || '').trim() === name,
);
if (matches.length === 0) {
throw new Error(`No process found with name "${name}"`);
}
if (matches.length > 1) {
const ids = matches.map((c) => String(c.id)).join(', ');
throw new Error(
`Multiple processes found with name "${name}": ids [${ids}]. Please use id:<n>.`,
);
}
return { id: matches[0].id, name } as ResponseForMethod<'resolveTarget'>;
}
// bare numeric id
if (/^\d+$/.test(raw)) {
const id = toProcessId(raw);
const config = this.tspmInstance.processConfigs.get(id);
if (!config) throw new Error(`Process ${id} not found`);
return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
}
// Unknown format
throw new Error(
'Unsupported target format. Use numeric id (e.g. 1), id:<n> (e.g. id:1), or name:<label> (e.g. name:api).',
);
},
);
// Batch operations handlers // Batch operations handlers
this.ipcServer.onMessage( this.ipcServer.onMessage(
'startAll', 'startAll',

View File

@@ -240,14 +240,6 @@ export interface AddResponse {
} }
// Remove (delete config and stop if running) // Remove (delete config and stop if running)
export interface RemoveRequest {
id: ProcessId;
}
export interface RemoveResponse {
success: boolean;
message?: string;
}
// Update (modify existing config) // Update (modify existing config)
export interface UpdateRequest { export interface UpdateRequest {
@@ -260,6 +252,16 @@ export interface UpdateResponse {
config: IProcessConfig; config: IProcessConfig;
} }
// Resolve a user-provided target (id:n or name:foo or numeric string) to a ProcessId
export interface ResolveTargetRequest {
target: string;
}
export interface ResolveTargetResponse {
id: ProcessId;
name?: string;
}
// Type mappings for methods // Type mappings for methods
export type IpcMethodMap = { export type IpcMethodMap = {
start: { request: StartRequest; response: StartResponse }; start: { request: StartRequest; response: StartResponse };
@@ -269,7 +271,6 @@ export type IpcMethodMap = {
delete: { request: DeleteRequest; response: DeleteResponse }; delete: { request: DeleteRequest; response: DeleteResponse };
add: { request: AddRequest; response: AddResponse }; add: { request: AddRequest; response: AddResponse };
update: { request: UpdateRequest; response: UpdateResponse }; update: { request: UpdateRequest; response: UpdateResponse };
remove: { request: RemoveRequest; response: RemoveResponse };
list: { request: ListRequest; response: ListResponse }; list: { request: ListRequest; response: ListResponse };
describe: { request: DescribeRequest; response: DescribeResponse }; describe: { request: DescribeRequest; response: DescribeResponse };
getLogs: { request: GetLogsRequest; response: GetLogsResponse }; getLogs: { request: GetLogsRequest; response: GetLogsResponse };
@@ -286,6 +287,7 @@ export type IpcMethodMap = {
response: DaemonShutdownResponse; response: DaemonShutdownResponse;
}; };
heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse }; heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse };
resolveTarget: { request: ResolveTargetRequest; response: ResolveTargetResponse };
}; };
// Helper type to extract request type for a method // Helper type to extract request type for a method