Compare commits
12 Commits
Author | SHA1 | Date | |
---|---|---|---|
5036f01516 | |||
538f282b62 | |||
e507b75c40 | |||
97a8377a75 | |||
3676bff04c | |||
dfe0677cab | |||
611b756670 | |||
2291348774 | |||
504725043d | |||
e16a3fb845 | |||
c3d12b287c | |||
cbea3f6187 |
0
.tspm_home/.npmextra/kv/@git.zone__tspm.json
Normal file
0
.tspm_home/.npmextra/kv/@git.zone__tspm.json
Normal file
54
changelog.md
54
changelog.md
@@ -1,5 +1,59 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.0.0 - BREAKING CHANGE(daemon)
|
||||||
|
Introduce persistent log storage, numeric ProcessId type, and improved process monitoring / IPC handling
|
||||||
|
|
||||||
|
- Add LogPersistence: persistent on-disk storage for process logs (save/load/delete/cleanup).
|
||||||
|
- Persist logs on process exit/error/stop and trim in-memory buffers to avoid excessive memory usage.
|
||||||
|
- Introduce a branded numeric ProcessId type and toProcessId helpers; migrate IPC types and internal maps from string ids to ProcessId.
|
||||||
|
- ProcessManager refactor: typed maps for processes/configs/info/logs, async start/stop/restart flows, improved PID/uptime/restart tracking, and desired state persistence handling.
|
||||||
|
- ProcessMonitor refactor: async lifecycle (start/stop), load persisted logs on startup, flush logs to disk on exit/error/stop, log memory capping, and improved event emissions.
|
||||||
|
- ProcessWrapper improvements: buffer stdout/stderr remainders, flush partial lines on stream end, clearer debug logging.
|
||||||
|
- IPC client/server changes: handlers now normalize ids with toProcessId, subscribe/unsubscribe accept numeric/string ids, getLogs/start/stop/restart/delete use typed ids.
|
||||||
|
- CLI tweaks: format process id output safely with String() to avoid formatting issues.
|
||||||
|
- Add dependency and plugin export for @push.rocks/smartfile and update package.json accordingly.
|
||||||
|
|
||||||
|
## 2025-08-29 - 4.4.2 - fix(daemon)
|
||||||
|
Fix daemon IPC id handling, reload configs on demand and correct CLI daemon start path
|
||||||
|
|
||||||
|
- Normalize process IDs in daemon IPC handlers (trim strings) to avoid lookup mismatches
|
||||||
|
- Attempt to reload saved process configurations when a startById request cannot find a config (handles races/stale state)
|
||||||
|
- Use normalized IDs in responses and messages for stop/restart/delete/remove/describe handlers
|
||||||
|
- Fix CLI daemon start path to point at dist_ts/daemon/tspm.daemon.js when launching the background daemon
|
||||||
|
- Ensure the IPC client disconnects after showing CLI version/status to avoid leaked connections
|
||||||
|
|
||||||
|
## 2025-08-29 - 4.4.1 - fix(cli)
|
||||||
|
Use server-side start-by-id flow for starting processes
|
||||||
|
|
||||||
|
- CLI: 'tspm start <id>' now calls a new 'startById' IPC method instead of fetching the full config via 'describe' and submitting it back to 'start'.
|
||||||
|
- Daemon: Added server-side handler for 'startById' which resolves the stored process config and starts the process on the daemon.
|
||||||
|
- Protocol: Added StartByIdRequest/StartByIdResponse types and registered 'startById' in the IPC method map.
|
||||||
|
|
||||||
|
## 2025-08-29 - 4.4.0 - feat(daemon)
|
||||||
|
Persist desired process states and add daemon restart command
|
||||||
|
|
||||||
|
- Persist desired process states: ProcessManager now stores desiredStates to user storage (desiredStates key) and reloads them on startup.
|
||||||
|
- Start/stop operations update desired state: IPC handlers in the daemon now set desired state when processes are started, stopped, restarted or when batch start/stop is invoked.
|
||||||
|
- Resume desired state on daemon start: Daemon loads desired states and calls startDesired() to bring processes to their desired 'online' state after startup.
|
||||||
|
- Remove desired state on deletion/reset: Deleting a process or resetting clears its desired state; reset clears all desired states as well.
|
||||||
|
- CLI: Added 'tspm daemon restart' — stops the daemon (gracefully) and restarts it in the foreground for the current session, with checks and informative output.
|
||||||
|
|
||||||
|
## 2025-08-29 - 4.3.1 - fix(daemon)
|
||||||
|
Fix daemon describe handler to return correct process info and config; bump @push.rocks/smartipc to ^2.2.2
|
||||||
|
|
||||||
|
- Corrected the 'describe' IPC handler in the daemon to use ProcessManager.describe(...) result and return { processInfo, config } — this fixes a mismatch between the handler and the ProcessManager.describe() return shape.
|
||||||
|
- Bumped dependency @push.rocks/smartipc to ^2.2.2 in package.json.
|
||||||
|
|
||||||
|
## 2025-08-29 - 4.3.0 - feat(cli)
|
||||||
|
Correct CLI plugin imports and add reset command/IPC to stop processes and clear persisted configs
|
||||||
|
|
||||||
|
- Fixed relative plugin imports in many CLI command modules to use the local CLI plugin wrapper (reduces startup surface and fixes import paths).
|
||||||
|
- Added a lightweight ts/cli/plugins.ts that exposes only the minimal plugin set used by the CLI.
|
||||||
|
- Implemented ProcessManager.reset(): stops running processes, collects per-id stop errors, clears in-memory maps and removes persisted configurations (with fallback to write an empty list on delete failure).
|
||||||
|
- Daemon now exposes a 'reset' IPC handler that delegates to ProcessManager.reset() so CLI can perform a single RPC to reset TSPM state.
|
||||||
|
- Updated shared IPC protocol types to include ResetRequest and ResetResponse.
|
||||||
|
- Refactored the CLI reset command to call the new 'reset' RPC (replaces previous stopAll + per-config removal logic).
|
||||||
|
|
||||||
## 2025-08-29 - 4.2.0 - feat(cli)
|
## 2025-08-29 - 4.2.0 - feat(cli)
|
||||||
Add 'reset' CLI command to stop all processes and clear saved configurations; integrate interactive confirmation and client plugin updates
|
Add 'reset' CLI command to stop all processes and clear saved configurations; integrate interactive confirmation and client plugin updates
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tspm",
|
"name": "@git.zone/tspm",
|
||||||
"version": "4.2.0",
|
"version": "5.0.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a no fuzz process manager",
|
"description": "a no fuzz process manager",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -35,9 +35,10 @@
|
|||||||
"@push.rocks/npmextra": "^5.3.3",
|
"@push.rocks/npmextra": "^5.3.3",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/smartcli": "^4.0.11",
|
"@push.rocks/smartcli": "^4.0.11",
|
||||||
"@push.rocks/smartinteract": "^2.0.16",
|
|
||||||
"@push.rocks/smartdaemon": "^2.0.9",
|
"@push.rocks/smartdaemon": "^2.0.9",
|
||||||
"@push.rocks/smartipc": "^2.2.1",
|
"@push.rocks/smartfile": "^11.2.7",
|
||||||
|
"@push.rocks/smartinteract": "^2.0.16",
|
||||||
|
"@push.rocks/smartipc": "^2.2.2",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"pidusage": "^4.0.1",
|
"pidusage": "^4.0.1",
|
||||||
"ps-tree": "^1.2.0",
|
"ps-tree": "^1.2.0",
|
||||||
|
219
pnpm-lock.yaml
generated
219
pnpm-lock.yaml
generated
@@ -20,12 +20,15 @@ importers:
|
|||||||
'@push.rocks/smartdaemon':
|
'@push.rocks/smartdaemon':
|
||||||
specifier: ^2.0.9
|
specifier: ^2.0.9
|
||||||
version: 2.0.9
|
version: 2.0.9
|
||||||
|
'@push.rocks/smartfile':
|
||||||
|
specifier: ^11.2.7
|
||||||
|
version: 11.2.7
|
||||||
'@push.rocks/smartinteract':
|
'@push.rocks/smartinteract':
|
||||||
specifier: ^2.0.16
|
specifier: ^2.0.16
|
||||||
version: 2.0.16
|
version: 2.0.16
|
||||||
'@push.rocks/smartipc':
|
'@push.rocks/smartipc':
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.2
|
||||||
version: 2.2.1
|
version: 2.2.2
|
||||||
'@push.rocks/smartpath':
|
'@push.rocks/smartpath':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@@ -844,9 +847,6 @@ packages:
|
|||||||
'@push.rocks/smartfile@10.0.41':
|
'@push.rocks/smartfile@10.0.41':
|
||||||
resolution: {integrity: sha512-xOOy0duI34M2qrJZggpk51EHGXmg9+mBL1Q55tNiQKXzfx89P3coY1EAZG8tvmep3qB712QEKe7T+u04t42Kjg==}
|
resolution: {integrity: sha512-xOOy0duI34M2qrJZggpk51EHGXmg9+mBL1Q55tNiQKXzfx89P3coY1EAZG8tvmep3qB712QEKe7T+u04t42Kjg==}
|
||||||
|
|
||||||
'@push.rocks/smartfile@11.2.0':
|
|
||||||
resolution: {integrity: sha512-0Gw6DvCQ2D/BXNN6airSC7hoSBut0p/uNWf2+rqO+D6VLhIJ/QUBvF6xm/LnpPI/zcF8YlDn/GEriInB5DUtEw==}
|
|
||||||
|
|
||||||
'@push.rocks/smartfile@11.2.7':
|
'@push.rocks/smartfile@11.2.7':
|
||||||
resolution: {integrity: sha512-8Yp7/sAgPpWJBHohV92ogHWKzRomI5MEbSG6b5W2n18tqwfAmjMed0rQvsvGrSBlnEWCKgoOrYIIZbLO61+J0Q==}
|
resolution: {integrity: sha512-8Yp7/sAgPpWJBHohV92ogHWKzRomI5MEbSG6b5W2n18tqwfAmjMed0rQvsvGrSBlnEWCKgoOrYIIZbLO61+J0Q==}
|
||||||
|
|
||||||
@@ -865,8 +865,8 @@ packages:
|
|||||||
'@push.rocks/smartinteract@2.0.16':
|
'@push.rocks/smartinteract@2.0.16':
|
||||||
resolution: {integrity: sha512-eltvVRRUKBKd77DSFA4DPY2g4V4teZLNe8A93CDy/WglglYcUjxMoLY/b0DFTWCWKYT+yjk6Fe6p0FRrvX9Yvg==}
|
resolution: {integrity: sha512-eltvVRRUKBKd77DSFA4DPY2g4V4teZLNe8A93CDy/WglglYcUjxMoLY/b0DFTWCWKYT+yjk6Fe6p0FRrvX9Yvg==}
|
||||||
|
|
||||||
'@push.rocks/smartipc@2.2.1':
|
'@push.rocks/smartipc@2.2.2':
|
||||||
resolution: {integrity: sha512-yBFZwJsWRyVdN1YRSiHafRMfn0PYIi2IStcQqPkiU4Srr6XPDMZD3mmIeV2V1WL6bWvRWf+4WF9Y+rLhj4jGdA==}
|
resolution: {integrity: sha512-pkWqp2nQH7p5zD9Efh5KNX2O0+gFWL6bxbdd6SdDh4gP8Gb0b3Sn87Tpedghpc/d+LCVql+1pUf6OlvMQpD5Yw==}
|
||||||
|
|
||||||
'@push.rocks/smartjson@5.0.20':
|
'@push.rocks/smartjson@5.0.20':
|
||||||
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
||||||
@@ -1641,9 +1641,6 @@ packages:
|
|||||||
'@types/node-forge@1.3.14':
|
'@types/node-forge@1.3.14':
|
||||||
resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
|
resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
|
||||||
|
|
||||||
'@types/node-ipc@9.2.3':
|
|
||||||
resolution: {integrity: sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==}
|
|
||||||
|
|
||||||
'@types/node@22.13.10':
|
'@types/node@22.13.10':
|
||||||
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
|
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
|
||||||
|
|
||||||
@@ -2042,9 +2039,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
cliui@7.0.4:
|
|
||||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2141,10 +2135,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==}
|
resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
copyfiles@2.4.1:
|
|
||||||
resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
core-util-is@1.0.3:
|
core-util-is@1.0.3:
|
||||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
@@ -2317,10 +2307,6 @@ packages:
|
|||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
easy-stack@1.0.1:
|
|
||||||
resolution: {integrity: sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==}
|
|
||||||
engines: {node: '>=6.0.0'}
|
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
|
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
|
||||||
|
|
||||||
@@ -2441,10 +2427,6 @@ packages:
|
|||||||
resolution: {integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=}
|
resolution: {integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
event-pubsub@5.0.3:
|
|
||||||
resolution: {integrity: sha512-2QiHxshejKgJrYMzSI9MEHrvhmzxBL+eLyiM5IiyjDBySkgwS2+tdtnO3gbx8pEisu/yOFCIhfCb63gCEu0yBQ==}
|
|
||||||
engines: {node: '>=13.0.0'}
|
|
||||||
|
|
||||||
event-stream@3.3.4:
|
event-stream@3.3.4:
|
||||||
resolution: {integrity: sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=}
|
resolution: {integrity: sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=}
|
||||||
|
|
||||||
@@ -2687,11 +2669,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
glob@11.0.1:
|
|
||||||
resolution: {integrity: sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
glob@11.0.3:
|
glob@11.0.3:
|
||||||
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -2975,9 +2952,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
isarray@0.0.1:
|
|
||||||
resolution: {integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=}
|
|
||||||
|
|
||||||
isarray@1.0.0:
|
isarray@1.0.0:
|
||||||
resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=}
|
resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=}
|
||||||
|
|
||||||
@@ -3010,10 +2984,6 @@ packages:
|
|||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
jackspeak@4.1.0:
|
|
||||||
resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
|
|
||||||
jackspeak@4.1.1:
|
jackspeak@4.1.1:
|
||||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -3041,14 +3011,6 @@ packages:
|
|||||||
js-base64@3.7.7:
|
js-base64@3.7.7:
|
||||||
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
|
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
|
||||||
|
|
||||||
js-message@1.0.7:
|
|
||||||
resolution: {integrity: sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==}
|
|
||||||
engines: {node: '>=0.6.0'}
|
|
||||||
|
|
||||||
js-queue@2.0.2:
|
|
||||||
resolution: {integrity: sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==}
|
|
||||||
engines: {node: '>=1.0.0'}
|
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -3441,10 +3403,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
minimatch@10.0.1:
|
|
||||||
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
|
|
||||||
minimatch@10.0.3:
|
minimatch@10.0.3:
|
||||||
resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
|
resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -3552,13 +3510,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||||
engines: {node: '>= 6.13.0'}
|
engines: {node: '>= 6.13.0'}
|
||||||
|
|
||||||
node-ipc@12.0.0:
|
|
||||||
resolution: {integrity: sha512-QHJ2gAJiqA3cM7cQiRjLsfCOBRB0TwQ6axYD4FSllQWipEbP6i7Se1dP8EzPKk5J1nCe27W69eqPmCoKyQ61Vg==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
|
|
||||||
noms@0.0.0:
|
|
||||||
resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==}
|
|
||||||
|
|
||||||
normalize-newline@4.1.0:
|
normalize-newline@4.1.0:
|
||||||
resolution: {integrity: sha512-ff4jKqMI8Xl50/4Mms/9jPobzAV/UK+kXG2XJ/7AqOmxIx8mqfqTIHYxuAnEgJ2AQeBbLnlbmZ5+38Y9A0w/YA==}
|
resolution: {integrity: sha512-ff4jKqMI8Xl50/4Mms/9jPobzAV/UK+kXG2XJ/7AqOmxIx8mqfqTIHYxuAnEgJ2AQeBbLnlbmZ5+38Y9A0w/YA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -3869,9 +3820,6 @@ packages:
|
|||||||
react-is@18.3.1:
|
react-is@18.3.1:
|
||||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||||
|
|
||||||
readable-stream@1.0.34:
|
|
||||||
resolution: {integrity: sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=}
|
|
||||||
|
|
||||||
readable-stream@2.3.8:
|
readable-stream@2.3.8:
|
||||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||||
|
|
||||||
@@ -4158,9 +4106,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
string_decoder@0.10.31:
|
|
||||||
resolution: {integrity: sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=}
|
|
||||||
|
|
||||||
string_decoder@1.1.1:
|
string_decoder@1.1.1:
|
||||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||||
|
|
||||||
@@ -4200,14 +4145,6 @@ packages:
|
|||||||
strnum@2.1.1:
|
strnum@2.1.1:
|
||||||
resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
|
resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
|
||||||
|
|
||||||
strong-type@0.1.6:
|
|
||||||
resolution: {integrity: sha512-eJe5caH6Pi5oMMeQtIoBPpvNu/s4jiyb63u5tkHNnQXomK+puyQ5i+Z5iTLBr/xUz/pIcps0NSfzzFI34+gAXg==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
strong-type@1.1.0:
|
|
||||||
resolution: {integrity: sha512-X5Z6riticuH5GnhUyzijfDi1SoXas8ODDyN7K8lJeQK+Jfi4dKdoJGL4CXTskY/ATBcN+rz5lROGn1tAUkOX7g==}
|
|
||||||
engines: {node: '>=12.21.0'}
|
|
||||||
|
|
||||||
strtok3@10.3.4:
|
strtok3@10.3.4:
|
||||||
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
|
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4261,9 +4198,6 @@ packages:
|
|||||||
threads@1.7.0:
|
threads@1.7.0:
|
||||||
resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==}
|
resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==}
|
||||||
|
|
||||||
through2@2.0.5:
|
|
||||||
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
|
||||||
|
|
||||||
through2@4.0.2:
|
through2@4.0.2:
|
||||||
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||||
|
|
||||||
@@ -4422,10 +4356,6 @@ packages:
|
|||||||
resolution: {integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=}
|
resolution: {integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
untildify@4.0.0:
|
|
||||||
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
upper-case@1.1.3:
|
upper-case@1.1.3:
|
||||||
resolution: {integrity: sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=}
|
resolution: {integrity: sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=}
|
||||||
|
|
||||||
@@ -4551,26 +4481,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
|
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
xtend@4.0.2:
|
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
|
||||||
engines: {node: '>=0.4'}
|
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
yargs-parser@20.2.9:
|
|
||||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
yargs-parser@21.1.1:
|
yargs-parser@21.1.1:
|
||||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
yargs@16.2.0:
|
|
||||||
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
yargs@17.7.2:
|
yargs@17.7.2:
|
||||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -5734,7 +5652,7 @@ snapshots:
|
|||||||
|
|
||||||
'@git.zone/tsrun@1.3.3':
|
'@git.zone/tsrun@1.3.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartfile': 11.2.0
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartshell': 3.2.3
|
'@push.rocks/smartshell': 3.2.3
|
||||||
tsx: 4.20.5
|
tsx: 4.20.5
|
||||||
|
|
||||||
@@ -6359,25 +6277,6 @@ snapshots:
|
|||||||
glob: 10.4.5
|
glob: 10.4.5
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
|
|
||||||
'@push.rocks/smartfile@11.2.0':
|
|
||||||
dependencies:
|
|
||||||
'@push.rocks/lik': 6.1.0
|
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
|
||||||
'@push.rocks/smartfile-interfaces': 1.0.7
|
|
||||||
'@push.rocks/smarthash': 3.0.4
|
|
||||||
'@push.rocks/smartjson': 5.0.20
|
|
||||||
'@push.rocks/smartmime': 2.0.4
|
|
||||||
'@push.rocks/smartpath': 5.1.0
|
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
|
||||||
'@push.rocks/smartrequest': 2.0.23
|
|
||||||
'@push.rocks/smartstream': 3.2.5
|
|
||||||
'@types/fs-extra': 11.0.4
|
|
||||||
'@types/glob': 8.1.0
|
|
||||||
'@types/js-yaml': 4.0.9
|
|
||||||
fs-extra: 11.3.0
|
|
||||||
glob: 11.0.1
|
|
||||||
js-yaml: 4.1.0
|
|
||||||
|
|
||||||
'@push.rocks/smartfile@11.2.7':
|
'@push.rocks/smartfile@11.2.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
@@ -6427,12 +6326,10 @@ snapshots:
|
|||||||
'@push.rocks/smartpromise': 4.2.3
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
inquirer: 11.1.0
|
inquirer: 11.1.0
|
||||||
|
|
||||||
'@push.rocks/smartipc@2.2.1':
|
'@push.rocks/smartipc@2.2.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@types/node-ipc': 9.2.3
|
|
||||||
node-ipc: 12.0.0
|
|
||||||
|
|
||||||
'@push.rocks/smartjson@5.0.20':
|
'@push.rocks/smartjson@5.0.20':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7689,10 +7586,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.10
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/node-ipc@9.2.3':
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 22.13.10
|
|
||||||
|
|
||||||
'@types/node@22.13.10':
|
'@types/node@22.13.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 6.20.0
|
undici-types: 6.20.0
|
||||||
@@ -8130,12 +8023,6 @@ snapshots:
|
|||||||
|
|
||||||
cli-width@4.1.0: {}
|
cli-width@4.1.0: {}
|
||||||
|
|
||||||
cliui@7.0.4:
|
|
||||||
dependencies:
|
|
||||||
string-width: 4.2.3
|
|
||||||
strip-ansi: 6.0.1
|
|
||||||
wrap-ansi: 7.0.0
|
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
@@ -8227,16 +8114,6 @@ snapshots:
|
|||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
keygrip: 1.1.0
|
keygrip: 1.1.0
|
||||||
|
|
||||||
copyfiles@2.4.1:
|
|
||||||
dependencies:
|
|
||||||
glob: 7.2.3
|
|
||||||
minimatch: 3.1.2
|
|
||||||
mkdirp: 1.0.4
|
|
||||||
noms: 0.0.0
|
|
||||||
through2: 2.0.5
|
|
||||||
untildify: 4.0.0
|
|
||||||
yargs: 16.2.0
|
|
||||||
|
|
||||||
core-util-is@1.0.3: {}
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
cors@2.8.5:
|
cors@2.8.5:
|
||||||
@@ -8373,8 +8250,6 @@ snapshots:
|
|||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
easy-stack@1.0.1: {}
|
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
@@ -8507,11 +8382,6 @@ snapshots:
|
|||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
||||||
event-pubsub@5.0.3:
|
|
||||||
dependencies:
|
|
||||||
copyfiles: 2.4.1
|
|
||||||
strong-type: 0.1.6
|
|
||||||
|
|
||||||
event-stream@3.3.4:
|
event-stream@3.3.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
duplexer: 0.1.2
|
duplexer: 0.1.2
|
||||||
@@ -8834,15 +8704,6 @@ snapshots:
|
|||||||
package-json-from-dist: 1.0.1
|
package-json-from-dist: 1.0.1
|
||||||
path-scurry: 1.11.1
|
path-scurry: 1.11.1
|
||||||
|
|
||||||
glob@11.0.1:
|
|
||||||
dependencies:
|
|
||||||
foreground-child: 3.3.1
|
|
||||||
jackspeak: 4.1.0
|
|
||||||
minimatch: 10.0.1
|
|
||||||
minipass: 7.1.2
|
|
||||||
package-json-from-dist: 1.0.1
|
|
||||||
path-scurry: 2.0.0
|
|
||||||
|
|
||||||
glob@11.0.3:
|
glob@11.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: 3.3.1
|
foreground-child: 3.3.1
|
||||||
@@ -9162,8 +9023,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
|
|
||||||
isarray@0.0.1: {}
|
|
||||||
|
|
||||||
isarray@1.0.0: {}
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
isbinaryfile@5.0.4: {}
|
isbinaryfile@5.0.4: {}
|
||||||
@@ -9193,10 +9052,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@pkgjs/parseargs': 0.11.0
|
'@pkgjs/parseargs': 0.11.0
|
||||||
|
|
||||||
jackspeak@4.1.0:
|
|
||||||
dependencies:
|
|
||||||
'@isaacs/cliui': 8.0.2
|
|
||||||
|
|
||||||
jackspeak@4.1.1:
|
jackspeak@4.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/cliui': 8.0.2
|
'@isaacs/cliui': 8.0.2
|
||||||
@@ -9240,12 +9095,6 @@ snapshots:
|
|||||||
|
|
||||||
js-base64@3.7.7: {}
|
js-base64@3.7.7: {}
|
||||||
|
|
||||||
js-message@1.0.7: {}
|
|
||||||
|
|
||||||
js-queue@2.0.2:
|
|
||||||
dependencies:
|
|
||||||
easy-stack: 1.0.1
|
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@3.14.1:
|
js-yaml@3.14.1:
|
||||||
@@ -9835,10 +9684,6 @@ snapshots:
|
|||||||
|
|
||||||
min-indent@1.0.1: {}
|
min-indent@1.0.1: {}
|
||||||
|
|
||||||
minimatch@10.0.1:
|
|
||||||
dependencies:
|
|
||||||
brace-expansion: 2.0.1
|
|
||||||
|
|
||||||
minimatch@10.0.3:
|
minimatch@10.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/brace-expansion': 5.0.0
|
'@isaacs/brace-expansion': 5.0.0
|
||||||
@@ -9939,18 +9784,6 @@ snapshots:
|
|||||||
|
|
||||||
node-forge@1.3.1: {}
|
node-forge@1.3.1: {}
|
||||||
|
|
||||||
node-ipc@12.0.0:
|
|
||||||
dependencies:
|
|
||||||
event-pubsub: 5.0.3
|
|
||||||
js-message: 1.0.7
|
|
||||||
js-queue: 2.0.2
|
|
||||||
strong-type: 1.1.0
|
|
||||||
|
|
||||||
noms@0.0.0:
|
|
||||||
dependencies:
|
|
||||||
inherits: 2.0.4
|
|
||||||
readable-stream: 1.0.34
|
|
||||||
|
|
||||||
normalize-newline@4.1.0:
|
normalize-newline@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
replace-buffer: 1.2.1
|
replace-buffer: 1.2.1
|
||||||
@@ -10269,13 +10102,6 @@ snapshots:
|
|||||||
|
|
||||||
react-is@18.3.1: {}
|
react-is@18.3.1: {}
|
||||||
|
|
||||||
readable-stream@1.0.34:
|
|
||||||
dependencies:
|
|
||||||
core-util-is: 1.0.3
|
|
||||||
inherits: 2.0.4
|
|
||||||
isarray: 0.0.1
|
|
||||||
string_decoder: 0.10.31
|
|
||||||
|
|
||||||
readable-stream@2.3.8:
|
readable-stream@2.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
core-util-is: 1.0.3
|
core-util-is: 1.0.3
|
||||||
@@ -10673,8 +10499,6 @@ snapshots:
|
|||||||
emoji-regex: 9.2.2
|
emoji-regex: 9.2.2
|
||||||
strip-ansi: 7.1.0
|
strip-ansi: 7.1.0
|
||||||
|
|
||||||
string_decoder@0.10.31: {}
|
|
||||||
|
|
||||||
string_decoder@1.1.1:
|
string_decoder@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.1.2
|
safe-buffer: 5.1.2
|
||||||
@@ -10710,10 +10534,6 @@ snapshots:
|
|||||||
|
|
||||||
strnum@2.1.1: {}
|
strnum@2.1.1: {}
|
||||||
|
|
||||||
strong-type@0.1.6: {}
|
|
||||||
|
|
||||||
strong-type@1.1.0: {}
|
|
||||||
|
|
||||||
strtok3@10.3.4:
|
strtok3@10.3.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tokenizer/token': 0.3.0
|
'@tokenizer/token': 0.3.0
|
||||||
@@ -10778,11 +10598,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
through2@2.0.5:
|
|
||||||
dependencies:
|
|
||||||
readable-stream: 2.3.8
|
|
||||||
xtend: 4.0.2
|
|
||||||
|
|
||||||
through2@4.0.2:
|
through2@4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
@@ -10923,8 +10738,6 @@ snapshots:
|
|||||||
|
|
||||||
unpipe@1.0.0: {}
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
untildify@4.0.0: {}
|
|
||||||
|
|
||||||
upper-case@1.1.3: {}
|
upper-case@1.1.3: {}
|
||||||
|
|
||||||
url@0.11.4:
|
url@0.11.4:
|
||||||
@@ -11031,24 +10844,10 @@ snapshots:
|
|||||||
|
|
||||||
xmlhttprequest-ssl@2.1.2: {}
|
xmlhttprequest-ssl@2.1.2: {}
|
||||||
|
|
||||||
xtend@4.0.2: {}
|
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yargs-parser@20.2.9: {}
|
|
||||||
|
|
||||||
yargs-parser@21.1.1: {}
|
yargs-parser@21.1.1: {}
|
||||||
|
|
||||||
yargs@16.2.0:
|
|
||||||
dependencies:
|
|
||||||
cliui: 7.0.4
|
|
||||||
escalade: 3.2.0
|
|
||||||
get-caller-file: 2.0.5
|
|
||||||
require-directory: 2.1.1
|
|
||||||
string-width: 4.2.3
|
|
||||||
y18n: 5.0.8
|
|
||||||
yargs-parser: 20.2.9
|
|
||||||
|
|
||||||
yargs@17.7.2:
|
yargs@17.7.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
cliui: 8.0.1
|
cliui: 8.0.1
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tspm',
|
name: '@git.zone/tspm',
|
||||||
version: '4.2.0',
|
version: '5.0.0',
|
||||||
description: 'a no fuzz process manager'
|
description: 'a no fuzz process manager'
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
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 type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
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 type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
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 type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
@@ -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';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
||||||
@@ -33,7 +33,8 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
const daemonScript = plugins.path.join(
|
const daemonScript = plugins.path.join(
|
||||||
paths.packageDir,
|
paths.packageDir,
|
||||||
'dist_ts',
|
'dist_ts',
|
||||||
'daemon.js',
|
'daemon',
|
||||||
|
'tspm.daemon.js',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start daemon as a detached background process
|
// Start daemon as a detached background process
|
||||||
@@ -80,6 +81,48 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'restart':
|
||||||
|
try {
|
||||||
|
console.log('Restarting TSPM daemon...');
|
||||||
|
await tspmIpcClient.stopDaemon(true);
|
||||||
|
|
||||||
|
// Reuse the manual start logic from 'start'
|
||||||
|
const statusAfterStop = await tspmIpcClient.getDaemonStatus();
|
||||||
|
if (statusAfterStop) {
|
||||||
|
console.warn('Daemon still appears to be running; proceeding to start anyway.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Starting TSPM daemon manually...');
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const daemonScript = plugins.path.join(
|
||||||
|
paths.packageDir,
|
||||||
|
'dist_ts',
|
||||||
|
'daemon.js',
|
||||||
|
);
|
||||||
|
const daemonProcess = spawn(process.execPath, [daemonScript], {
|
||||||
|
detached: true,
|
||||||
|
stdio: process.env.TSPM_DEBUG === 'true' ? 'inherit' : 'ignore',
|
||||||
|
env: { ...process.env, TSPM_DAEMON_MODE: 'true' },
|
||||||
|
});
|
||||||
|
daemonProcess.unref();
|
||||||
|
console.log(`Started daemon with PID: ${daemonProcess.pid}`);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
const newStatus = await tspmIpcClient.getDaemonStatus();
|
||||||
|
if (newStatus) {
|
||||||
|
console.log('✓ TSPM daemon restarted successfully');
|
||||||
|
console.log(` PID: ${newStatus.pid}`);
|
||||||
|
} else {
|
||||||
|
console.warn('\n⚠️ Warning: Daemon restart attempted but status is unavailable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await tspmIpcClient.disconnect();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restarting daemon:', (error as any).message || String(error));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'start-service':
|
case 'start-service':
|
||||||
// This is called by systemd - start the daemon directly
|
// This is called by systemd - start the daemon directly
|
||||||
console.log('Starting TSPM daemon for systemd service...');
|
console.log('Starting TSPM daemon for systemd service...');
|
||||||
@@ -135,6 +178,7 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
console.log('Usage: tspm daemon <command>');
|
console.log('Usage: tspm daemon <command>');
|
||||||
console.log('\nCommands:');
|
console.log('\nCommands:');
|
||||||
console.log(' start Start the TSPM daemon');
|
console.log(' start Start the TSPM daemon');
|
||||||
|
console.log(' restart Restart the TSPM daemon');
|
||||||
console.log(' stop Stop the TSPM daemon');
|
console.log(' stop Stop the TSPM daemon');
|
||||||
console.log(' status Show daemon status');
|
console.log(' status Show daemon status');
|
||||||
break;
|
break;
|
||||||
|
@@ -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';
|
||||||
import { tspmIpcClient } from '../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../client/tspm.ipcclient.js';
|
||||||
import { Logger } from '../../shared/common/utils.errorhandler.js';
|
import { Logger } from '../../shared/common/utils.errorhandler.js';
|
||||||
@@ -74,7 +74,7 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
const resetColor = '\x1b[0m';
|
const resetColor = '\x1b[0m';
|
||||||
|
|
||||||
console.log(
|
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)} │`,
|
`│ ${pad(String(proc.id), 7)} │ ${pad(String(proc.id), 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad(formatMemory(proc.memory), 9)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
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 type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { parseMemoryString, formatMemory } from '../../helpers/memory.js';
|
import { parseMemoryString, formatMemory } from '../../helpers/memory.js';
|
||||||
@@ -88,4 +88,3 @@ export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
{ actionLabel: 'add process config' },
|
{ actionLabel: 'add process config' },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
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 type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
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 type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
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 type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
@@ -39,7 +39,7 @@ export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
const resetColor = '\x1b[0m';
|
const resetColor = '\x1b[0m';
|
||||||
|
|
||||||
console.log(
|
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)} │`,
|
`│ ${pad(String(proc.id), 7)} │ ${pad(String(proc.id), 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad((proc.pid || '-').toString(), 9)} │ ${pad(formatMemory(proc.memory), 8)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
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 type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
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';
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
|
|
||||||
const id = String(arg);
|
const id = String(arg);
|
||||||
console.log(`Restarting process: ${id}`);
|
console.log(`Restarting process: ${id}`);
|
||||||
const response = await tspmIpcClient.request('restart', { id });
|
const response = await tspmIpcClient.request('restart', { id: toProcessId(id) });
|
||||||
|
|
||||||
console.log(`✓ Process restarted successfully`);
|
console.log(`✓ Process restarted successfully`);
|
||||||
console.log(` ID: ${response.processId}`);
|
console.log(` ID: ${response.processId}`);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
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 type { IProcessConfig } from '../../../shared/protocol/ipc.types.js';
|
import type { IProcessConfig } from '../../../shared/protocol/ipc.types.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
@@ -17,14 +17,8 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const desc = await tspmIpcClient.request('describe', { id }).catch(() => null);
|
console.log(`Starting process id ${id}...`);
|
||||||
if (!desc) {
|
const response = await tspmIpcClient.request('startById', { id });
|
||||||
console.error(`Process with id '${id}' not found. Use 'tspm add' first.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Starting process id ${id} (${desc.config.name || id})...`);
|
|
||||||
const response = await tspmIpcClient.request('start', { config: desc.config });
|
|
||||||
console.log('✓ Process started');
|
console.log('✓ Process started');
|
||||||
console.log(` ID: ${response.processId}`);
|
console.log(` ID: ${response.processId}`);
|
||||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
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 type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { registerIpcCommand } from '../registration/index.js';
|
import { registerIpcCommand } from '../registration/index.js';
|
||||||
import { tspmIpcClient } from '../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../client/tspm.ipcclient.js';
|
||||||
|
|
||||||
@@ -18,33 +18,16 @@ export function registerResetCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop all processes first
|
// Single IPC call to reset
|
||||||
const stopAllRes = await tspmIpcClient.request('stopAll', {});
|
const result = await tspmIpcClient.request('reset', {});
|
||||||
if (stopAllRes.failed.length) {
|
const failedCount = result.failed.length;
|
||||||
console.log(
|
console.log(`Stopped ${result.stopped.length} processes.`);
|
||||||
`Stopped ${stopAllRes.stopped.length}, ${stopAllRes.failed.length} failed to stop. Proceeding with config reset...`,
|
if (failedCount) {
|
||||||
);
|
console.log(`${failedCount} processes failed to stop (configs cleared anyway).`);
|
||||||
} else {
|
|
||||||
console.log(`Stopped ${stopAllRes.stopped.length} processes.`);
|
|
||||||
}
|
}
|
||||||
|
console.log(`Cleared ${result.removed.length} saved configurations.`);
|
||||||
// List all processes/configs currently known
|
console.log('TSPM has been reset.');
|
||||||
const listRes = await tspmIpcClient.request('list', {});
|
|
||||||
|
|
||||||
// Remove each config
|
|
||||||
for (const proc of listRes.processes) {
|
|
||||||
try {
|
|
||||||
// Prefer 'remove' which handles stop-if-running semantics as well
|
|
||||||
await tspmIpcClient.request('remove', { id: proc.id });
|
|
||||||
console.log(`Removed config for process ${proc.id}.`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`Failed to remove config for ${proc.id}:`, (err as Error)?.message || String(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('TSPM has been reset: processes stopped and configs cleared.');
|
|
||||||
},
|
},
|
||||||
{ actionLabel: 'reset TSPM' },
|
{ actionLabel: 'reset TSPM' },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
|
import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
|
||||||
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
|
import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
|
||||||
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
|
@@ -1,4 +1,5 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from './plugins.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';
|
||||||
|
|
||||||
@@ -38,6 +39,24 @@ export const run = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const smartcliInstance = new plugins.smartcli.Smartcli();
|
const smartcliInstance = new plugins.smartcli.Smartcli();
|
||||||
|
// Intercept -v/--version to show CLI and daemon versions
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args.includes('-v') || args.includes('--version')) {
|
||||||
|
const cliVersion = tspmProjectinfo.npm.version;
|
||||||
|
console.log(`tspm CLI: ${cliVersion}`);
|
||||||
|
const status = await tspmIpcClient.getDaemonStatus();
|
||||||
|
if (status) {
|
||||||
|
console.log(
|
||||||
|
`Daemon: running v${status.version || 'unknown'} (pid ${status.pid})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('Daemon: not running');
|
||||||
|
}
|
||||||
|
// Ensure we disconnect any IPC client connection used for status
|
||||||
|
try { await tspmIpcClient.disconnect(); } catch {}
|
||||||
|
return; // do not start parser
|
||||||
|
}
|
||||||
|
// Keep Smartcli version info for help output but not used for -v now
|
||||||
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
|
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
|
||||||
|
|
||||||
// Register all commands
|
// Register all commands
|
||||||
|
8
ts/cli/plugins.ts
Normal file
8
ts/cli/plugins.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Minimal plugin set for the CLI to keep startup light
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as projectinfo from '@push.rocks/projectinfo';
|
||||||
|
import * as smartcli from '@push.rocks/smartcli';
|
||||||
|
import * as smartinteract from '@push.rocks/smartinteract';
|
||||||
|
|
||||||
|
export { path, projectinfo, smartcli, smartinteract };
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import type {
|
import type {
|
||||||
CliArguments,
|
CliArguments,
|
||||||
CommandAction,
|
CommandAction,
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import * as plugins from './plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as paths from '../paths.js';
|
import * as paths from '../paths.js';
|
||||||
|
import { toProcessId } from '../shared/protocol/id.js';
|
||||||
|
import type { ProcessId } from '../shared/protocol/id.js';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IpcMethodMap,
|
IpcMethodMap,
|
||||||
@@ -144,26 +146,28 @@ export class TspmIpcClient {
|
|||||||
* Subscribe to log updates for a specific process
|
* Subscribe to log updates for a specific process
|
||||||
*/
|
*/
|
||||||
public async subscribe(
|
public async subscribe(
|
||||||
processId: string,
|
processId: ProcessId | number | string,
|
||||||
handler: (log: any) => void,
|
handler: (log: any) => void,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.ipcClient || !this.isConnected) {
|
if (!this.ipcClient || !this.isConnected) {
|
||||||
throw new Error('Not connected to daemon');
|
throw new Error('Not connected to daemon');
|
||||||
}
|
}
|
||||||
|
|
||||||
const topic = `logs.${processId}`;
|
const id = toProcessId(processId);
|
||||||
|
const topic = `logs.${id}`;
|
||||||
await this.ipcClient.subscribe(`topic:${topic}`, handler);
|
await this.ipcClient.subscribe(`topic:${topic}`, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from log updates for a specific process
|
* Unsubscribe from log updates for a specific process
|
||||||
*/
|
*/
|
||||||
public async unsubscribe(processId: string): Promise<void> {
|
public async unsubscribe(processId: ProcessId | number | string): Promise<void> {
|
||||||
if (!this.ipcClient || !this.isConnected) {
|
if (!this.ipcClient || !this.isConnected) {
|
||||||
throw new Error('Not connected to daemon');
|
throw new Error('Not connected to daemon');
|
||||||
}
|
}
|
||||||
|
|
||||||
const topic = `logs.${processId}`;
|
const id = toProcessId(processId);
|
||||||
|
const topic = `logs.${id}`;
|
||||||
await this.ipcClient.unsubscribe(`topic:${topic}`);
|
await this.ipcClient.unsubscribe(`topic:${topic}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
117
ts/daemon/logpersistence.ts
Normal file
117
ts/daemon/logpersistence.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as paths from '../paths.js';
|
||||||
|
import type { IProcessLog } from '../shared/protocol/ipc.types.js';
|
||||||
|
import type { ProcessId } from '../shared/protocol/id.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages persistent log storage for processes
|
||||||
|
*/
|
||||||
|
export class LogPersistence {
|
||||||
|
private logsDir: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.logsDir = plugins.path.join(paths.tspmDir, 'logs');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the log file path for a process
|
||||||
|
*/
|
||||||
|
private getLogFilePath(processId: ProcessId): string {
|
||||||
|
return plugins.path.join(this.logsDir, `process-${processId}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the logs directory exists
|
||||||
|
*/
|
||||||
|
private async ensureLogsDir(): Promise<void> {
|
||||||
|
await plugins.smartfile.fs.ensureDir(this.logsDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save logs to disk
|
||||||
|
*/
|
||||||
|
public async saveLogs(processId: ProcessId, logs: IProcessLog[]): Promise<void> {
|
||||||
|
await this.ensureLogsDir();
|
||||||
|
const filePath = this.getLogFilePath(processId);
|
||||||
|
|
||||||
|
// Write logs as JSON
|
||||||
|
await plugins.smartfile.memory.toFs(
|
||||||
|
JSON.stringify(logs, null, 2),
|
||||||
|
filePath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load logs from disk
|
||||||
|
*/
|
||||||
|
public async loadLogs(processId: ProcessId): Promise<IProcessLog[]> {
|
||||||
|
const filePath = this.getLogFilePath(processId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exists = await plugins.smartfile.fs.fileExists(filePath);
|
||||||
|
if (!exists) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await plugins.smartfile.fs.toStringSync(filePath);
|
||||||
|
const logs = JSON.parse(content) as IProcessLog[];
|
||||||
|
|
||||||
|
// Convert date strings back to Date objects
|
||||||
|
return logs.map(log => ({
|
||||||
|
...log,
|
||||||
|
timestamp: new Date(log.timestamp)
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load logs for process ${processId}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete logs from disk after loading
|
||||||
|
*/
|
||||||
|
public async deleteLogs(processId: ProcessId): Promise<void> {
|
||||||
|
const filePath = this.getLogFilePath(processId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exists = await plugins.smartfile.fs.fileExists(filePath);
|
||||||
|
if (exists) {
|
||||||
|
await plugins.smartfile.fs.remove(filePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to delete logs for process ${processId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate approximate memory size of logs in bytes
|
||||||
|
*/
|
||||||
|
public static calculateLogMemorySize(logs: IProcessLog[]): number {
|
||||||
|
// Estimate based on JSON string size
|
||||||
|
// This is an approximation but good enough for our purposes
|
||||||
|
return JSON.stringify(logs).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old log files (for maintenance)
|
||||||
|
*/
|
||||||
|
public async cleanupOldLogs(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.ensureLogsDir();
|
||||||
|
const files = await plugins.smartfile.fs.listFileTree(this.logsDir, '*.json');
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = plugins.path.join(this.logsDir, file);
|
||||||
|
const stats = await plugins.smartfile.fs.stat(filePath);
|
||||||
|
|
||||||
|
// Delete files older than 7 days
|
||||||
|
const ageInDays = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24);
|
||||||
|
if (ageInDays > 7) {
|
||||||
|
await plugins.smartfile.fs.remove(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to cleanup old logs:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as paths from '../paths.js';
|
import * as paths from '../paths.js';
|
||||||
import { ProcessMonitor } from './processmonitor.js';
|
import { ProcessMonitor } from './processmonitor.js';
|
||||||
|
import { LogPersistence } from './logpersistence.js';
|
||||||
import { TspmConfig } from './tspm.config.js';
|
import { TspmConfig } from './tspm.config.js';
|
||||||
import {
|
import {
|
||||||
Logger,
|
Logger,
|
||||||
@@ -16,15 +17,20 @@ import type {
|
|||||||
IProcessLog,
|
IProcessLog,
|
||||||
IMonitorConfig
|
IMonitorConfig
|
||||||
} from '../shared/protocol/ipc.types.js';
|
} from '../shared/protocol/ipc.types.js';
|
||||||
|
import { toProcessId, getNextProcessId } from '../shared/protocol/id.js';
|
||||||
|
import type { ProcessId } from '../shared/protocol/id.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class ProcessManager extends EventEmitter {
|
export class ProcessManager extends EventEmitter {
|
||||||
public processes: Map<string, ProcessMonitor> = new Map();
|
public processes: Map<ProcessId, ProcessMonitor> = new Map();
|
||||||
public processConfigs: Map<string, IProcessConfig> = new Map();
|
public processConfigs: Map<ProcessId, IProcessConfig> = new Map();
|
||||||
public processInfo: Map<string, IProcessInfo> = new Map();
|
public processInfo: Map<ProcessId, IProcessInfo> = new Map();
|
||||||
|
private processLogs: Map<ProcessId, IProcessLog[]> = new Map();
|
||||||
private config: TspmConfig;
|
private config: TspmConfig;
|
||||||
private configStorageKey = 'processes';
|
private configStorageKey = 'processes';
|
||||||
|
private desiredStateStorageKey = 'desiredStates';
|
||||||
|
private desiredStates: Map<ProcessId, IProcessInfo['status']> = new Map();
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -32,18 +38,19 @@ export class ProcessManager extends EventEmitter {
|
|||||||
this.logger = new Logger('Tspm');
|
this.logger = new Logger('Tspm');
|
||||||
this.config = new TspmConfig();
|
this.config = new TspmConfig();
|
||||||
this.loadProcessConfigs();
|
this.loadProcessConfigs();
|
||||||
|
this.loadDesiredStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a process configuration without starting it.
|
* Add a process configuration without starting it.
|
||||||
* Returns the assigned numeric sequential id as string.
|
* Returns the assigned numeric sequential id.
|
||||||
*/
|
*/
|
||||||
public async add(configInput: Omit<IProcessConfig, 'id'> & { id?: string }): Promise<string> {
|
public async add(configInput: Omit<IProcessConfig, 'id'> & { id?: ProcessId }): Promise<ProcessId> {
|
||||||
// Determine next numeric id
|
// Determine next numeric id
|
||||||
const nextId = this.getNextSequentialId();
|
const nextId = this.getNextSequentialId();
|
||||||
|
|
||||||
const config: IProcessConfig = {
|
const config: IProcessConfig = {
|
||||||
id: String(nextId),
|
id: nextId,
|
||||||
name: configInput.name || `process-${nextId}`,
|
name: configInput.name || `process-${nextId}`,
|
||||||
command: configInput.command,
|
command: configInput.command,
|
||||||
args: configInput.args,
|
args: configInput.args,
|
||||||
@@ -67,6 +74,7 @@ export class ProcessManager extends EventEmitter {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await this.saveProcessConfigs();
|
await this.saveProcessConfigs();
|
||||||
|
await this.setDesiredState(config.id, 'stopped');
|
||||||
return config.id;
|
return config.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,7 +115,8 @@ export class ProcessManager extends EventEmitter {
|
|||||||
|
|
||||||
// Create and start process monitor
|
// Create and start process monitor
|
||||||
const monitor = new ProcessMonitor({
|
const monitor = new ProcessMonitor({
|
||||||
name: config.name || config.id,
|
id: config.id, // Pass the ProcessId for log persistence
|
||||||
|
name: config.name || String(config.id),
|
||||||
projectDir: config.projectDir,
|
projectDir: config.projectDir,
|
||||||
command: config.command,
|
command: config.command,
|
||||||
args: config.args,
|
args: config.args,
|
||||||
@@ -121,13 +130,43 @@ export class ProcessManager extends EventEmitter {
|
|||||||
|
|
||||||
// Set up log event handler to re-emit for pub/sub
|
// Set up log event handler to re-emit for pub/sub
|
||||||
monitor.on('log', (log: IProcessLog) => {
|
monitor.on('log', (log: IProcessLog) => {
|
||||||
|
// Store log in our persistent storage
|
||||||
|
if (!this.processLogs.has(config.id)) {
|
||||||
|
this.processLogs.set(config.id, []);
|
||||||
|
}
|
||||||
|
const logs = this.processLogs.get(config.id)!;
|
||||||
|
logs.push(log);
|
||||||
|
|
||||||
|
// Trim logs if they exceed buffer size (default 1000)
|
||||||
|
const bufferSize = config.logBufferSize || 1000;
|
||||||
|
if (logs.length > bufferSize) {
|
||||||
|
this.processLogs.set(config.id, logs.slice(-bufferSize));
|
||||||
|
}
|
||||||
|
|
||||||
this.emit('process:log', { processId: config.id, log });
|
this.emit('process:log', { processId: config.id, log });
|
||||||
});
|
});
|
||||||
|
|
||||||
monitor.start();
|
// Set up event handler to track PID when process starts
|
||||||
|
monitor.on('start', (pid: number) => {
|
||||||
|
this.updateProcessInfo(config.id, { pid });
|
||||||
|
});
|
||||||
|
|
||||||
// Update process info
|
// Set up event handler to clear PID when process exits
|
||||||
this.updateProcessInfo(config.id, { status: 'online' });
|
monitor.on('exit', () => {
|
||||||
|
this.updateProcessInfo(config.id, { pid: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
|
await monitor.start();
|
||||||
|
|
||||||
|
// Wait a moment for the process to spawn and get its PID
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// Update process info with PID
|
||||||
|
const pid = monitor.getPid();
|
||||||
|
this.updateProcessInfo(config.id, {
|
||||||
|
status: 'online',
|
||||||
|
pid: pid || undefined
|
||||||
|
});
|
||||||
|
|
||||||
// Save updated configs
|
// Save updated configs
|
||||||
await this.saveProcessConfigs();
|
await this.saveProcessConfigs();
|
||||||
@@ -161,7 +200,7 @@ export class ProcessManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Stop a process by id
|
* Stop a process by id
|
||||||
*/
|
*/
|
||||||
public async stop(id: string): Promise<void> {
|
public async stop(id: ProcessId): Promise<void> {
|
||||||
this.logger.info(`Stopping process with id '${id}'`);
|
this.logger.info(`Stopping process with id '${id}'`);
|
||||||
|
|
||||||
const monitor = this.processes.get(id);
|
const monitor = this.processes.get(id);
|
||||||
@@ -175,7 +214,7 @@ export class ProcessManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
monitor.stop();
|
await monitor.stop();
|
||||||
this.updateProcessInfo(id, { status: 'stopped' });
|
this.updateProcessInfo(id, { status: 'stopped' });
|
||||||
this.logger.info(`Successfully stopped process with id '${id}'`);
|
this.logger.info(`Successfully stopped process with id '${id}'`);
|
||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
@@ -195,7 +234,7 @@ export class ProcessManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Restart a process by id
|
* Restart a process by id
|
||||||
*/
|
*/
|
||||||
public async restart(id: string): Promise<void> {
|
public async restart(id: ProcessId): Promise<void> {
|
||||||
this.logger.info(`Restarting process with id '${id}'`);
|
this.logger.info(`Restarting process with id '${id}'`);
|
||||||
|
|
||||||
const monitor = this.processes.get(id);
|
const monitor = this.processes.get(id);
|
||||||
@@ -212,11 +251,12 @@ export class ProcessManager extends EventEmitter {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Stop and then start the process
|
// Stop and then start the process
|
||||||
monitor.stop();
|
await monitor.stop();
|
||||||
|
|
||||||
// Create a new monitor instance
|
// Create a new monitor instance
|
||||||
const newMonitor = new ProcessMonitor({
|
const newMonitor = new ProcessMonitor({
|
||||||
name: config.name || config.id,
|
id: config.id, // Pass the ProcessId for log persistence
|
||||||
|
name: config.name || String(config.id),
|
||||||
projectDir: config.projectDir,
|
projectDir: config.projectDir,
|
||||||
command: config.command,
|
command: config.command,
|
||||||
args: config.args,
|
args: config.args,
|
||||||
@@ -226,14 +266,37 @@ export class ProcessManager extends EventEmitter {
|
|||||||
logBufferSize: config.logBufferSize,
|
logBufferSize: config.logBufferSize,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.processes.set(id, newMonitor);
|
// Set up log event handler for the new monitor
|
||||||
newMonitor.start();
|
newMonitor.on('log', (log: IProcessLog) => {
|
||||||
|
// Store log in our persistent storage
|
||||||
|
if (!this.processLogs.has(id)) {
|
||||||
|
this.processLogs.set(id, []);
|
||||||
|
}
|
||||||
|
const logs = this.processLogs.get(id)!;
|
||||||
|
logs.push(log);
|
||||||
|
|
||||||
// Update restart count
|
// Trim logs if they exceed buffer size (default 1000)
|
||||||
|
const bufferSize = config.logBufferSize || 1000;
|
||||||
|
if (logs.length > bufferSize) {
|
||||||
|
this.processLogs.set(id, logs.slice(-bufferSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.emit('process:log', { processId: id, log });
|
||||||
|
});
|
||||||
|
|
||||||
|
this.processes.set(id, newMonitor);
|
||||||
|
await newMonitor.start();
|
||||||
|
|
||||||
|
// Wait a moment for the process to spawn and get its PID
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// Update restart count and PID
|
||||||
const info = this.processInfo.get(id);
|
const info = this.processInfo.get(id);
|
||||||
if (info) {
|
if (info) {
|
||||||
|
const pid = newMonitor.getPid();
|
||||||
this.updateProcessInfo(id, {
|
this.updateProcessInfo(id, {
|
||||||
status: 'online',
|
status: 'online',
|
||||||
|
pid: pid || undefined,
|
||||||
restarts: info.restarts + 1,
|
restarts: info.restarts + 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -253,7 +316,7 @@ export class ProcessManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Delete a process by id
|
* Delete a process by id
|
||||||
*/
|
*/
|
||||||
public async delete(id: string): Promise<void> {
|
public async delete(id: ProcessId): Promise<void> {
|
||||||
this.logger.info(`Deleting process with id '${id}'`);
|
this.logger.info(`Deleting process with id '${id}'`);
|
||||||
|
|
||||||
// Check if process exists
|
// Check if process exists
|
||||||
@@ -276,9 +339,15 @@ export class ProcessManager extends EventEmitter {
|
|||||||
this.processes.delete(id);
|
this.processes.delete(id);
|
||||||
this.processConfigs.delete(id);
|
this.processConfigs.delete(id);
|
||||||
this.processInfo.delete(id);
|
this.processInfo.delete(id);
|
||||||
|
this.processLogs.delete(id);
|
||||||
|
|
||||||
|
// Delete persisted logs from disk
|
||||||
|
const logPersistence = new LogPersistence();
|
||||||
|
await logPersistence.deleteLogs(id);
|
||||||
|
|
||||||
// Save updated configs
|
// Save updated configs
|
||||||
await this.saveProcessConfigs();
|
await this.saveProcessConfigs();
|
||||||
|
await this.removeDesiredState(id);
|
||||||
|
|
||||||
this.logger.info(`Successfully deleted process with id '${id}'`);
|
this.logger.info(`Successfully deleted process with id '${id}'`);
|
||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
@@ -287,7 +356,14 @@ export class ProcessManager extends EventEmitter {
|
|||||||
this.processes.delete(id);
|
this.processes.delete(id);
|
||||||
this.processConfigs.delete(id);
|
this.processConfigs.delete(id);
|
||||||
this.processInfo.delete(id);
|
this.processInfo.delete(id);
|
||||||
|
this.processLogs.delete(id);
|
||||||
|
|
||||||
|
// Delete persisted logs from disk even if stop failed
|
||||||
|
const logPersistence = new LogPersistence();
|
||||||
|
await logPersistence.deleteLogs(id);
|
||||||
|
|
||||||
await this.saveProcessConfigs();
|
await this.saveProcessConfigs();
|
||||||
|
await this.removeDesiredState(id);
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`Successfully deleted process with id '${id}' after stopping failure`,
|
`Successfully deleted process with id '${id}' after stopping failure`,
|
||||||
@@ -308,14 +384,42 @@ export class ProcessManager extends EventEmitter {
|
|||||||
* Get a list of all process infos
|
* Get a list of all process infos
|
||||||
*/
|
*/
|
||||||
public list(): IProcessInfo[] {
|
public list(): IProcessInfo[] {
|
||||||
return Array.from(this.processInfo.values());
|
const infos = Array.from(this.processInfo.values());
|
||||||
|
|
||||||
|
// Enrich with live data from monitors
|
||||||
|
for (const info of infos) {
|
||||||
|
const monitor = this.processes.get(info.id);
|
||||||
|
if (monitor) {
|
||||||
|
// Update with current PID if the monitor is running
|
||||||
|
const pid = monitor.getPid();
|
||||||
|
if (pid) {
|
||||||
|
info.pid = pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update uptime if available
|
||||||
|
const uptime = monitor.getUptime();
|
||||||
|
if (uptime !== null) {
|
||||||
|
info.uptime = uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update restart count
|
||||||
|
info.restarts = monitor.getRestartCount();
|
||||||
|
|
||||||
|
// Update status based on actual running state
|
||||||
|
if (monitor.isRunning()) {
|
||||||
|
info.status = 'online';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return infos;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get detailed info for a specific process
|
* Get detailed info for a specific process
|
||||||
*/
|
*/
|
||||||
public describe(
|
public describe(
|
||||||
id: string,
|
id: ProcessId,
|
||||||
): { config: IProcessConfig; info: IProcessInfo } | null {
|
): { config: IProcessConfig; info: IProcessInfo } | null {
|
||||||
const config = this.processConfigs.get(id);
|
const config = this.processConfigs.get(id);
|
||||||
const info = this.processInfo.get(id);
|
const info = this.processInfo.get(id);
|
||||||
@@ -330,13 +434,21 @@ export class ProcessManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Get process logs
|
* Get process logs
|
||||||
*/
|
*/
|
||||||
public getLogs(id: string, limit?: number): IProcessLog[] {
|
public getLogs(id: ProcessId, limit?: number): IProcessLog[] {
|
||||||
|
// Get logs from the ProcessMonitor instance
|
||||||
const monitor = this.processes.get(id);
|
const monitor = this.processes.get(id);
|
||||||
if (!monitor) {
|
|
||||||
return [];
|
if (monitor) {
|
||||||
|
const logs = monitor.getLogs(limit);
|
||||||
|
return logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
return monitor.getLogs(limit);
|
// Fallback to stored logs if monitor doesn't exist
|
||||||
|
const logs = this.processLogs.get(id) || [];
|
||||||
|
if (limit && limit > 0) {
|
||||||
|
return logs.slice(-limit);
|
||||||
|
}
|
||||||
|
return logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -371,7 +483,7 @@ export class ProcessManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Update the info for a process
|
* Update the info for a process
|
||||||
*/
|
*/
|
||||||
private updateProcessInfo(id: string, update: Partial<IProcessInfo>): void {
|
private updateProcessInfo(id: ProcessId, update: Partial<IProcessInfo>): void {
|
||||||
const info = this.processInfo.get(id);
|
const info = this.processInfo.get(id);
|
||||||
if (info) {
|
if (info) {
|
||||||
this.processInfo.set(id, { ...info, ...update });
|
this.processInfo.set(id, { ...info, ...update });
|
||||||
@@ -381,15 +493,40 @@ export class ProcessManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Compute next sequential numeric id based on existing configs
|
* Compute next sequential numeric id based on existing configs
|
||||||
*/
|
*/
|
||||||
private getNextSequentialId(): number {
|
/**
|
||||||
let maxId = 0;
|
* Sync process stats from monitors to processInfo
|
||||||
for (const id of this.processConfigs.keys()) {
|
*/
|
||||||
const n = parseInt(id, 10);
|
public syncProcessStats(): void {
|
||||||
if (!isNaN(n)) {
|
for (const [id, monitor] of this.processes.entries()) {
|
||||||
maxId = Math.max(maxId, n);
|
const info = this.processInfo.get(id);
|
||||||
|
if (info) {
|
||||||
|
const pid = monitor.getPid();
|
||||||
|
const updates: Partial<IProcessInfo> = {};
|
||||||
|
|
||||||
|
// Update PID if available
|
||||||
|
if (pid) {
|
||||||
|
updates.pid = pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update uptime if available
|
||||||
|
const uptime = monitor.getUptime();
|
||||||
|
if (uptime !== null) {
|
||||||
|
updates.uptime = uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update restart count
|
||||||
|
updates.restarts = monitor.getRestartCount();
|
||||||
|
|
||||||
|
// Update status based on actual running state
|
||||||
|
updates.status = monitor.isRunning() ? 'online' : 'stopped';
|
||||||
|
|
||||||
|
this.updateProcessInfo(id, updates);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return maxId + 1;
|
}
|
||||||
|
|
||||||
|
private getNextSequentialId(): ProcessId {
|
||||||
|
return getNextProcessId(this.processConfigs.keys());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -415,6 +552,82 @@ export class ProcessManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Desired state persistence ===
|
||||||
|
private async saveDesiredStates(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const obj: Record<string, IProcessInfo['status']> = {};
|
||||||
|
for (const [id, state] of this.desiredStates.entries()) {
|
||||||
|
obj[String(id)] = state;
|
||||||
|
}
|
||||||
|
await this.config.writeKey(
|
||||||
|
this.desiredStateStorageKey,
|
||||||
|
JSON.stringify(obj),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to save desired states: ${error?.message || String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async loadDesiredStates(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const raw = await this.config.readKey(this.desiredStateStorageKey);
|
||||||
|
if (raw) {
|
||||||
|
const obj = JSON.parse(raw) as Record<string, IProcessInfo['status']>;
|
||||||
|
this.desiredStates = new Map(
|
||||||
|
Object.entries(obj).map(([k, v]) => [toProcessId(k), v] as const)
|
||||||
|
);
|
||||||
|
this.logger.debug(
|
||||||
|
`Loaded desired states for ${this.desiredStates.size} processes`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to load desired states: ${error?.message || String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setDesiredState(
|
||||||
|
id: ProcessId,
|
||||||
|
state: IProcessInfo['status'],
|
||||||
|
): Promise<void> {
|
||||||
|
this.desiredStates.set(id, state);
|
||||||
|
await this.saveDesiredStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeDesiredState(id: ProcessId): Promise<void> {
|
||||||
|
this.desiredStates.delete(id);
|
||||||
|
await this.saveDesiredStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setDesiredStateForAll(
|
||||||
|
state: IProcessInfo['status'],
|
||||||
|
): Promise<void> {
|
||||||
|
for (const id of this.processConfigs.keys()) {
|
||||||
|
this.desiredStates.set(id, state);
|
||||||
|
}
|
||||||
|
await this.saveDesiredStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async startDesired(): Promise<void> {
|
||||||
|
for (const [id, config] of this.processConfigs.entries()) {
|
||||||
|
const desired = this.desiredStates.get(id);
|
||||||
|
if (desired === 'online' && !this.processes.has(id)) {
|
||||||
|
try {
|
||||||
|
await this.start(config);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to start desired process ${id}: ${
|
||||||
|
(e as Error)?.message || String(e)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load process configurations from config storage
|
* Load process configurations from config storage
|
||||||
*/
|
*/
|
||||||
@@ -425,23 +638,35 @@ export class ProcessManager extends EventEmitter {
|
|||||||
const configsJson = await this.config.readKey(this.configStorageKey);
|
const configsJson = await this.config.readKey(this.configStorageKey);
|
||||||
if (configsJson) {
|
if (configsJson) {
|
||||||
try {
|
try {
|
||||||
const configs = JSON.parse(configsJson) as IProcessConfig[];
|
const parsed = JSON.parse(configsJson) as Array<any>;
|
||||||
this.logger.debug(`Loaded ${configs.length} process configurations`);
|
this.logger.debug(`Loaded ${parsed.length} process configurations`);
|
||||||
|
|
||||||
for (const config of configs) {
|
for (const raw of parsed) {
|
||||||
// Validate config
|
// Convert legacy string IDs to ProcessId
|
||||||
if (!config.id || !config.command || !config.projectDir) {
|
let id: ProcessId;
|
||||||
|
try {
|
||||||
|
id = toProcessId(raw.id);
|
||||||
|
} catch {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Skipping invalid process config for id '${config.id || 'unknown'}'`,
|
`Skipping invalid process config with non-numeric id '${raw.id || 'unknown'}'`,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processConfigs.set(config.id, config);
|
// Validate config
|
||||||
|
if (!id || !raw.command || !raw.projectDir) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Skipping invalid process config for id '${id || 'unknown'}'`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: IProcessConfig = { ...raw, id };
|
||||||
|
this.processConfigs.set(id, config);
|
||||||
|
|
||||||
// Initialize process info
|
// Initialize process info
|
||||||
this.processInfo.set(config.id, {
|
this.processInfo.set(id, {
|
||||||
id: config.id,
|
id: id,
|
||||||
status: 'stopped',
|
status: 'stopped',
|
||||||
memory: 0,
|
memory: 0,
|
||||||
restarts: 0,
|
restarts: 0,
|
||||||
@@ -470,4 +695,49 @@ export class ProcessManager extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset: stop all running processes and clear all saved configurations
|
||||||
|
*/
|
||||||
|
public async reset(): Promise<{
|
||||||
|
stopped: ProcessId[];
|
||||||
|
removed: ProcessId[];
|
||||||
|
failed: Array<{ id: ProcessId; error: string }>;
|
||||||
|
}> {
|
||||||
|
this.logger.info('Resetting TSPM: stopping all processes and clearing configs');
|
||||||
|
|
||||||
|
const removed = Array.from(this.processConfigs.keys());
|
||||||
|
const stopped: ProcessId[] = [];
|
||||||
|
const failed: Array<{ id: ProcessId; error: string }> = [];
|
||||||
|
|
||||||
|
// Attempt to stop all currently running processes with per-id error collection
|
||||||
|
for (const id of Array.from(this.processes.keys())) {
|
||||||
|
try {
|
||||||
|
await this.stop(id);
|
||||||
|
stopped.push(id);
|
||||||
|
} catch (error: any) {
|
||||||
|
failed.push({ id, error: error?.message || String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear in-memory maps regardless of stop outcomes
|
||||||
|
this.processes.clear();
|
||||||
|
this.processInfo.clear();
|
||||||
|
this.processConfigs.clear();
|
||||||
|
this.desiredStates.clear();
|
||||||
|
|
||||||
|
// Remove persisted configs
|
||||||
|
try {
|
||||||
|
await this.config.deleteKey(this.configStorageKey);
|
||||||
|
await this.config.deleteKey(this.desiredStateStorageKey).catch(() => {});
|
||||||
|
this.logger.debug('Cleared persisted process configurations');
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback: write empty list if deleteKey fails for any reason
|
||||||
|
this.logger.warn('deleteKey failed, writing empty process list instead');
|
||||||
|
await this.saveProcessConfigs().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info('TSPM reset complete');
|
||||||
|
return { stopped, removed, failed };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { ProcessWrapper } from './processwrapper.js';
|
import { ProcessWrapper } from './processwrapper.js';
|
||||||
|
import { LogPersistence } from './logpersistence.js';
|
||||||
import { Logger, ProcessError, handleError } from '../shared/common/utils.errorhandler.js';
|
import { Logger, ProcessError, handleError } from '../shared/common/utils.errorhandler.js';
|
||||||
import type { IMonitorConfig, IProcessLog } from '../shared/protocol/ipc.types.js';
|
import type { IMonitorConfig, IProcessLog } from '../shared/protocol/ipc.types.js';
|
||||||
|
import type { ProcessId } from '../shared/protocol/id.js';
|
||||||
|
|
||||||
export class ProcessMonitor extends EventEmitter {
|
export class ProcessMonitor extends EventEmitter {
|
||||||
private processWrapper: ProcessWrapper | null = null;
|
private processWrapper: ProcessWrapper | null = null;
|
||||||
@@ -11,14 +13,36 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
private stopped: boolean = true; // Initially stopped until start() is called
|
private stopped: boolean = true; // Initially stopped until start() is called
|
||||||
private restartCount: number = 0;
|
private restartCount: number = 0;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private logs: IProcessLog[] = [];
|
||||||
|
private logPersistence: LogPersistence;
|
||||||
|
private processId?: ProcessId;
|
||||||
|
private currentLogMemorySize: number = 0;
|
||||||
|
private readonly MAX_LOG_MEMORY_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
|
||||||
constructor(config: IMonitorConfig) {
|
constructor(config: IMonitorConfig & { id?: ProcessId }) {
|
||||||
super();
|
super();
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
|
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
|
||||||
|
this.logs = [];
|
||||||
|
this.logPersistence = new LogPersistence();
|
||||||
|
this.processId = config.id;
|
||||||
|
this.currentLogMemorySize = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(): void {
|
public async start(): Promise<void> {
|
||||||
|
// Load previously persisted logs if available
|
||||||
|
if (this.processId) {
|
||||||
|
const persistedLogs = await this.logPersistence.loadLogs(this.processId);
|
||||||
|
if (persistedLogs.length > 0) {
|
||||||
|
this.logs = persistedLogs;
|
||||||
|
this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs);
|
||||||
|
this.logger.info(`Loaded ${persistedLogs.length} persisted logs from disk`);
|
||||||
|
|
||||||
|
// Delete the persisted file after loading
|
||||||
|
await this.logPersistence.deleteLogs(this.processId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reset the stopped flag so that new processes can spawn.
|
// Reset the stopped flag so that new processes can spawn.
|
||||||
this.stopped = false;
|
this.stopped = false;
|
||||||
this.log(`Starting process monitor.`);
|
this.log(`Starting process monitor.`);
|
||||||
@@ -57,6 +81,22 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
|
|
||||||
// Set up event handlers
|
// Set up event handlers
|
||||||
this.processWrapper.on('log', (log: IProcessLog): void => {
|
this.processWrapper.on('log', (log: IProcessLog): void => {
|
||||||
|
// Store the log in our buffer
|
||||||
|
this.logs.push(log);
|
||||||
|
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}`);
|
||||||
|
|
||||||
|
// Update memory size tracking
|
||||||
|
this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs);
|
||||||
|
|
||||||
|
// Trim logs if they exceed memory limit (10MB)
|
||||||
|
while (this.currentLogMemorySize > this.MAX_LOG_MEMORY_SIZE && this.logs.length > 1) {
|
||||||
|
// Remove oldest logs until we're under the memory limit
|
||||||
|
this.logs.shift();
|
||||||
|
this.currentLogMemorySize = LogPersistence.calculateLogMemorySize(this.logs);
|
||||||
|
}
|
||||||
|
|
||||||
// Re-emit the log event for upstream handlers
|
// Re-emit the log event for upstream handlers
|
||||||
this.emit('log', log);
|
this.emit('log', log);
|
||||||
|
|
||||||
@@ -66,13 +106,31 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Re-emit start event with PID for upstream handlers
|
||||||
|
this.processWrapper.on('start', (pid: number): void => {
|
||||||
|
this.emit('start', pid);
|
||||||
|
});
|
||||||
|
|
||||||
this.processWrapper.on(
|
this.processWrapper.on(
|
||||||
'exit',
|
'exit',
|
||||||
(code: number | null, signal: string | null): void => {
|
async (code: number | null, signal: string | null): Promise<void> => {
|
||||||
const exitMsg = `Process exited with code ${code}, signal ${signal}.`;
|
const exitMsg = `Process exited with code ${code}, signal ${signal}.`;
|
||||||
this.logger.info(exitMsg);
|
this.logger.info(exitMsg);
|
||||||
this.log(exitMsg);
|
this.log(exitMsg);
|
||||||
|
|
||||||
|
// Flush logs to disk on exit
|
||||||
|
if (this.processId && this.logs.length > 0) {
|
||||||
|
try {
|
||||||
|
await this.logPersistence.saveLogs(this.processId, this.logs);
|
||||||
|
this.logger.debug(`Flushed ${this.logs.length} logs to disk on exit`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to flush logs to disk on exit: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-emit exit event for upstream handlers
|
||||||
|
this.emit('exit', code, signal);
|
||||||
|
|
||||||
if (!this.stopped) {
|
if (!this.stopped) {
|
||||||
this.logger.info('Restarting process...');
|
this.logger.info('Restarting process...');
|
||||||
this.log('Restarting process...');
|
this.log('Restarting process...');
|
||||||
@@ -86,7 +144,7 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.processWrapper.on('error', (error: Error | ProcessError): void => {
|
this.processWrapper.on('error', async (error: Error | ProcessError): Promise<void> => {
|
||||||
const errorMsg =
|
const errorMsg =
|
||||||
error instanceof ProcessError
|
error instanceof ProcessError
|
||||||
? `Process error: ${error.toString()}`
|
? `Process error: ${error.toString()}`
|
||||||
@@ -95,6 +153,16 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
this.log(errorMsg);
|
this.log(errorMsg);
|
||||||
|
|
||||||
|
// Flush logs to disk on error
|
||||||
|
if (this.processId && this.logs.length > 0) {
|
||||||
|
try {
|
||||||
|
await this.logPersistence.saveLogs(this.processId, this.logs);
|
||||||
|
this.logger.debug(`Flushed ${this.logs.length} logs to disk on error`);
|
||||||
|
} catch (flushError) {
|
||||||
|
this.logger.error(`Failed to flush logs to disk on error: ${flushError}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.stopped) {
|
if (!this.stopped) {
|
||||||
this.logger.info('Restarting process due to error...');
|
this.logger.info('Restarting process due to error...');
|
||||||
this.log('Restarting process due to error...');
|
this.log('Restarting process due to error...');
|
||||||
@@ -239,9 +307,20 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Stop the monitor and prevent any further respawns.
|
* Stop the monitor and prevent any further respawns.
|
||||||
*/
|
*/
|
||||||
public stop(): void {
|
public async stop(): Promise<void> {
|
||||||
this.log('Stopping process monitor.');
|
this.log('Stopping process monitor.');
|
||||||
this.stopped = true;
|
this.stopped = true;
|
||||||
|
|
||||||
|
// Flush logs to disk before stopping
|
||||||
|
if (this.processId && this.logs.length > 0) {
|
||||||
|
try {
|
||||||
|
await this.logPersistence.saveLogs(this.processId, this.logs);
|
||||||
|
this.logger.info(`Flushed ${this.logs.length} logs to disk on stop`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to flush logs to disk on stop: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.intervalId) {
|
if (this.intervalId) {
|
||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
}
|
}
|
||||||
@@ -254,10 +333,12 @@ 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[] {
|
||||||
if (!this.processWrapper) {
|
console.error(`[ProcessMonitor:${this.config.name}] getLogs called, logs.length=${this.logs.length}, limit=${limit}`);
|
||||||
return [];
|
this.logger.debug(`Getting logs, total stored: ${this.logs.length}`);
|
||||||
|
if (limit && limit > 0) {
|
||||||
|
return this.logs.slice(-limit);
|
||||||
}
|
}
|
||||||
return this.processWrapper.getLogs(limit);
|
return this.logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -21,6 +21,8 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private nextSeq: number = 0;
|
private nextSeq: number = 0;
|
||||||
private runId: string = '';
|
private runId: string = '';
|
||||||
|
private stdoutRemainder: string = '';
|
||||||
|
private stderrRemainder: string = '';
|
||||||
|
|
||||||
constructor(options: IProcessWrapperOptions) {
|
constructor(options: IProcessWrapperOptions) {
|
||||||
super();
|
super();
|
||||||
@@ -66,6 +68,11 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
const exitMessage = `Process exited with code ${code}, signal ${signal}`;
|
const exitMessage = `Process exited with code ${code}, signal ${signal}`;
|
||||||
this.logger.info(exitMessage);
|
this.logger.info(exitMessage);
|
||||||
this.addSystemLog(exitMessage);
|
this.addSystemLog(exitMessage);
|
||||||
|
|
||||||
|
// Clear remainder buffers on exit
|
||||||
|
this.stdoutRemainder = '';
|
||||||
|
this.stderrRemainder = '';
|
||||||
|
|
||||||
this.emit('exit', code, signal);
|
this.emit('exit', code, signal);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,24 +90,57 @@ 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}`);
|
||||||
this.process.stdout.on('data', (data) => {
|
this.process.stdout.on('data', (data) => {
|
||||||
const lines = data.toString().split('\n');
|
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
|
||||||
|
const text = this.stdoutRemainder + data.toString();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
// The last element might be a partial line
|
||||||
|
this.stdoutRemainder = lines.pop() || '';
|
||||||
|
|
||||||
|
// Process complete lines
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.trim()) {
|
console.error(`[ProcessWrapper] Processing stdout line: ${line}`);
|
||||||
this.addLog('stdout', line);
|
this.logger.debug(`Captured stdout: ${line}`);
|
||||||
}
|
this.addLog('stdout', line);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Flush remainder on stream end
|
||||||
|
this.process.stdout.on('end', () => {
|
||||||
|
if (this.stdoutRemainder) {
|
||||||
|
this.logger.debug(`Flushing stdout remainder: ${this.stdoutRemainder}`);
|
||||||
|
this.addLog('stdout', this.stdoutRemainder);
|
||||||
|
this.stdoutRemainder = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.logger.warn('Process stdout is null');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture stderr
|
// Capture stderr
|
||||||
if (this.process.stderr) {
|
if (this.process.stderr) {
|
||||||
this.process.stderr.on('data', (data) => {
|
this.process.stderr.on('data', (data) => {
|
||||||
const lines = data.toString().split('\n');
|
// Add data to remainder buffer and split by newlines
|
||||||
|
const text = this.stderrRemainder + data.toString();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
// The last element might be a partial line
|
||||||
|
this.stderrRemainder = lines.pop() || '';
|
||||||
|
|
||||||
|
// Process complete lines
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.trim()) {
|
this.addLog('stderr', line);
|
||||||
this.addLog('stderr', line);
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
// Flush remainder on stream end
|
||||||
|
this.process.stderr.on('end', () => {
|
||||||
|
if (this.stderrRemainder) {
|
||||||
|
this.addLog('stderr', this.stderrRemainder);
|
||||||
|
this.stderrRemainder = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as paths from '../paths.js';
|
import * as paths from '../paths.js';
|
||||||
|
import { toProcessId } from '../shared/protocol/id.js';
|
||||||
|
import type { ProcessId } from '../shared/protocol/id.js';
|
||||||
import { ProcessManager } from './processmanager.js';
|
import { ProcessManager } from './processmanager.js';
|
||||||
import type {
|
import type {
|
||||||
IpcMethodMap,
|
IpcMethodMap,
|
||||||
@@ -20,12 +22,20 @@ export class TspmDaemon {
|
|||||||
private socketPath: string;
|
private socketPath: string;
|
||||||
private heartbeatInterval: NodeJS.Timeout | null = null;
|
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||||
private daemonPidFile: string;
|
private daemonPidFile: string;
|
||||||
|
private version: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.tspmInstance = new ProcessManager();
|
this.tspmInstance = new ProcessManager();
|
||||||
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
|
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
|
||||||
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
|
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
|
||||||
this.startTime = Date.now();
|
this.startTime = Date.now();
|
||||||
|
// Determine daemon version from package metadata
|
||||||
|
try {
|
||||||
|
const proj = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||||
|
this.version = proj.npm.version || 'unknown';
|
||||||
|
} catch {
|
||||||
|
this.version = 'unknown';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,6 +91,7 @@ export class TspmDaemon {
|
|||||||
|
|
||||||
// Load existing process configurations
|
// Load existing process configurations
|
||||||
await this.tspmInstance.loadProcessConfigs();
|
await this.tspmInstance.loadProcessConfigs();
|
||||||
|
await this.tspmInstance.loadDesiredStates();
|
||||||
|
|
||||||
// Set up log publishing
|
// Set up log publishing
|
||||||
this.tspmInstance.on('process:log', ({ processId, log }) => {
|
this.tspmInstance.on('process:log', ({ processId, log }) => {
|
||||||
@@ -95,6 +106,9 @@ export class TspmDaemon {
|
|||||||
// Set up graceful shutdown handlers
|
// Set up graceful shutdown handlers
|
||||||
this.setupShutdownHandlers();
|
this.setupShutdownHandlers();
|
||||||
|
|
||||||
|
// Start processes that should be online per desired state
|
||||||
|
await this.tspmInstance.startDesired();
|
||||||
|
|
||||||
console.log(`TSPM daemon started successfully on ${this.socketPath}`);
|
console.log(`TSPM daemon started successfully on ${this.socketPath}`);
|
||||||
console.log(`PID: ${process.pid}`);
|
console.log(`PID: ${process.pid}`);
|
||||||
}
|
}
|
||||||
@@ -108,6 +122,7 @@ export class TspmDaemon {
|
|||||||
'start',
|
'start',
|
||||||
async (request: RequestForMethod<'start'>) => {
|
async (request: RequestForMethod<'start'>) => {
|
||||||
try {
|
try {
|
||||||
|
await this.tspmInstance.setDesiredState(request.config.id, 'online');
|
||||||
await this.tspmInstance.start(request.config);
|
await this.tspmInstance.start(request.config);
|
||||||
const processInfo = this.tspmInstance.processInfo.get(
|
const processInfo = this.tspmInstance.processInfo.get(
|
||||||
request.config.id,
|
request.config.id,
|
||||||
@@ -123,14 +138,45 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Start by id (resolve config on server)
|
||||||
|
this.ipcServer.onMessage(
|
||||||
|
'startById',
|
||||||
|
async (request: RequestForMethod<'startById'>) => {
|
||||||
|
try {
|
||||||
|
const id = toProcessId(request.id);
|
||||||
|
let config = this.tspmInstance.processConfigs.get(id);
|
||||||
|
if (!config) {
|
||||||
|
// Try to reload configs if not found (handles races or stale state)
|
||||||
|
await this.tspmInstance.loadProcessConfigs();
|
||||||
|
config = this.tspmInstance.processConfigs.get(id) || null as any;
|
||||||
|
}
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`Process ${id} not found`);
|
||||||
|
}
|
||||||
|
await this.tspmInstance.setDesiredState(id, 'online');
|
||||||
|
await this.tspmInstance.start(config);
|
||||||
|
const processInfo = this.tspmInstance.processInfo.get(id);
|
||||||
|
return {
|
||||||
|
processId: id,
|
||||||
|
pid: processInfo?.pid,
|
||||||
|
status: processInfo?.status || 'stopped',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to start process: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'stop',
|
'stop',
|
||||||
async (request: RequestForMethod<'stop'>) => {
|
async (request: RequestForMethod<'stop'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.stop(request.id);
|
const id = toProcessId(request.id);
|
||||||
|
await this.tspmInstance.setDesiredState(id, 'stopped');
|
||||||
|
await this.tspmInstance.stop(id);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Process ${request.id} stopped successfully`,
|
message: `Process ${id} stopped successfully`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to stop process: ${error.message}`);
|
throw new Error(`Failed to stop process: ${error.message}`);
|
||||||
@@ -142,10 +188,12 @@ export class TspmDaemon {
|
|||||||
'restart',
|
'restart',
|
||||||
async (request: RequestForMethod<'restart'>) => {
|
async (request: RequestForMethod<'restart'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.restart(request.id);
|
const id = toProcessId(request.id);
|
||||||
const processInfo = this.tspmInstance.processInfo.get(request.id);
|
await this.tspmInstance.setDesiredState(id, 'online');
|
||||||
|
await this.tspmInstance.restart(id);
|
||||||
|
const processInfo = this.tspmInstance.processInfo.get(id);
|
||||||
return {
|
return {
|
||||||
processId: request.id,
|
processId: id,
|
||||||
pid: processInfo?.pid,
|
pid: processInfo?.pid,
|
||||||
status: processInfo?.status || 'stopped',
|
status: processInfo?.status || 'stopped',
|
||||||
};
|
};
|
||||||
@@ -159,10 +207,11 @@ export class TspmDaemon {
|
|||||||
'delete',
|
'delete',
|
||||||
async (request: RequestForMethod<'delete'>) => {
|
async (request: RequestForMethod<'delete'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.delete(request.id);
|
const id = toProcessId(request.id);
|
||||||
|
await this.tspmInstance.delete(id);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Process ${request.id} deleted successfully`,
|
message: `Process ${id} deleted successfully`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to delete process: ${error.message}`);
|
throw new Error(`Failed to delete process: ${error.message}`);
|
||||||
@@ -188,8 +237,9 @@ export class TspmDaemon {
|
|||||||
'remove',
|
'remove',
|
||||||
async (request: RequestForMethod<'remove'>) => {
|
async (request: RequestForMethod<'remove'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.delete(request.id);
|
const id = toProcessId(request.id);
|
||||||
return { success: true, message: `Process ${request.id} deleted successfully` };
|
await this.tspmInstance.delete(id);
|
||||||
|
return { success: true, message: `Process ${id} deleted successfully` };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to remove process: ${error.message}`);
|
throw new Error(`Failed to remove process: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -207,16 +257,15 @@ export class TspmDaemon {
|
|||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'describe',
|
'describe',
|
||||||
async (request: RequestForMethod<'describe'>) => {
|
async (request: RequestForMethod<'describe'>) => {
|
||||||
const processInfo = await this.tspmInstance.describe(request.id);
|
const id = toProcessId(request.id);
|
||||||
const config = this.tspmInstance.processConfigs.get(request.id);
|
const result = await this.tspmInstance.describe(id);
|
||||||
|
if (!result) {
|
||||||
if (!processInfo || !config) {
|
throw new Error(`Process ${id} not found`);
|
||||||
throw new Error(`Process ${request.id} not found`);
|
|
||||||
}
|
}
|
||||||
|
// Return correctly shaped response
|
||||||
return {
|
return {
|
||||||
processInfo,
|
processInfo: result.info,
|
||||||
config,
|
config: result.config,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -224,7 +273,7 @@ export class TspmDaemon {
|
|||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'getLogs',
|
'getLogs',
|
||||||
async (request: RequestForMethod<'getLogs'>) => {
|
async (request: RequestForMethod<'getLogs'>) => {
|
||||||
const logs = await this.tspmInstance.getLogs(request.id);
|
const logs = await this.tspmInstance.getLogs(toProcessId(request.id));
|
||||||
return { logs };
|
return { logs };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -233,9 +282,10 @@ export class TspmDaemon {
|
|||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'startAll',
|
'startAll',
|
||||||
async (request: RequestForMethod<'startAll'>) => {
|
async (request: RequestForMethod<'startAll'>) => {
|
||||||
const started: string[] = [];
|
const started: ProcessId[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: ProcessId; error: string }> = [];
|
||||||
|
|
||||||
|
await this.tspmInstance.setDesiredStateForAll('online');
|
||||||
await this.tspmInstance.startAll();
|
await this.tspmInstance.startAll();
|
||||||
|
|
||||||
// Get status of all processes
|
// Get status of all processes
|
||||||
@@ -254,9 +304,10 @@ export class TspmDaemon {
|
|||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'stopAll',
|
'stopAll',
|
||||||
async (request: RequestForMethod<'stopAll'>) => {
|
async (request: RequestForMethod<'stopAll'>) => {
|
||||||
const stopped: string[] = [];
|
const stopped: ProcessId[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: ProcessId; error: string }> = [];
|
||||||
|
|
||||||
|
await this.tspmInstance.setDesiredStateForAll('stopped');
|
||||||
await this.tspmInstance.stopAll();
|
await this.tspmInstance.stopAll();
|
||||||
|
|
||||||
// Get status of all processes
|
// Get status of all processes
|
||||||
@@ -275,8 +326,8 @@ export class TspmDaemon {
|
|||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'restartAll',
|
'restartAll',
|
||||||
async (request: RequestForMethod<'restartAll'>) => {
|
async (request: RequestForMethod<'restartAll'>) => {
|
||||||
const restarted: string[] = [];
|
const restarted: ProcessId[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: ProcessId; error: string }> = [];
|
||||||
|
|
||||||
await this.tspmInstance.restartAll();
|
await this.tspmInstance.restartAll();
|
||||||
|
|
||||||
@@ -293,6 +344,15 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reset handler: stops all and clears configs
|
||||||
|
this.ipcServer.onMessage(
|
||||||
|
'reset',
|
||||||
|
async (request: RequestForMethod<'reset'>) => {
|
||||||
|
const result = await this.tspmInstance.reset();
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Daemon management handlers
|
// Daemon management handlers
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'daemon:status',
|
'daemon:status',
|
||||||
@@ -305,6 +365,7 @@ export class TspmDaemon {
|
|||||||
processCount: this.tspmInstance.processes.size,
|
processCount: this.tspmInstance.processes.size,
|
||||||
memoryUsage: memUsage.heapUsed,
|
memoryUsage: memUsage.heapUsed,
|
||||||
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
|
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
|
||||||
|
version: this.version,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -497,3 +558,11 @@ export const startDaemon = async (): Promise<void> => {
|
|||||||
// Keep the process alive
|
// Keep the process alive
|
||||||
await new Promise(() => {});
|
await new Promise(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If this file is run directly (not imported), start the daemon
|
||||||
|
if (process.env.TSPM_DAEMON_MODE === 'true') {
|
||||||
|
startDaemon().catch((error) => {
|
||||||
|
console.error('Failed to start TSPM daemon:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -10,12 +10,13 @@ import * as npmextra from '@push.rocks/npmextra';
|
|||||||
import * as projectinfo from '@push.rocks/projectinfo';
|
import * as projectinfo from '@push.rocks/projectinfo';
|
||||||
import * as smartcli from '@push.rocks/smartcli';
|
import * as smartcli from '@push.rocks/smartcli';
|
||||||
import * as smartdaemon from '@push.rocks/smartdaemon';
|
import * as smartdaemon from '@push.rocks/smartdaemon';
|
||||||
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
import * as smartipc from '@push.rocks/smartipc';
|
import * as smartipc from '@push.rocks/smartipc';
|
||||||
import * as smartpath from '@push.rocks/smartpath';
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
import * as smartinteract from '@push.rocks/smartinteract';
|
import * as smartinteract from '@push.rocks/smartinteract';
|
||||||
|
|
||||||
// Export with explicit module types
|
// Export with explicit module types
|
||||||
export { npmextra, projectinfo, smartcli, smartdaemon, smartipc, smartpath, smartinteract };
|
export { npmextra, projectinfo, smartcli, smartdaemon, smartfile, smartipc, smartpath, smartinteract };
|
||||||
|
|
||||||
// third-party scope
|
// third-party scope
|
||||||
import psTree from 'ps-tree';
|
import psTree from 'ps-tree';
|
||||||
|
56
ts/shared/protocol/id.ts
Normal file
56
ts/shared/protocol/id.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Branded type for process IDs to ensure type safety
|
||||||
|
*/
|
||||||
|
export type ProcessId = number & { readonly __brand: 'ProcessId' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input type that accepts various ID formats for backward compatibility
|
||||||
|
*/
|
||||||
|
export type ProcessIdInput = ProcessId | number | string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes various ID input formats to a ProcessId
|
||||||
|
* @param input - The ID in various formats (string, number, or ProcessId)
|
||||||
|
* @returns A normalized ProcessId
|
||||||
|
* @throws Error if the input is not a valid process ID
|
||||||
|
*/
|
||||||
|
export function toProcessId(input: ProcessIdInput): ProcessId {
|
||||||
|
let num: number;
|
||||||
|
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!/^\d+$/.test(trimmed)) {
|
||||||
|
throw new Error(`Invalid process ID: "${input}" is not a numeric string`);
|
||||||
|
}
|
||||||
|
num = parseInt(trimmed, 10);
|
||||||
|
} else if (typeof input === 'number') {
|
||||||
|
num = input;
|
||||||
|
} else {
|
||||||
|
// Already a ProcessId
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(num) || num < 1) {
|
||||||
|
throw new Error(`Invalid process ID: ${input} must be a positive integer`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return num as ProcessId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to check if a value is a ProcessId
|
||||||
|
*/
|
||||||
|
export function isProcessId(value: unknown): value is ProcessId {
|
||||||
|
return typeof value === 'number' && Number.isInteger(value) && value >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the next sequential ID given existing IDs
|
||||||
|
*/
|
||||||
|
export function getNextProcessId(existingIds: Iterable<ProcessId>): ProcessId {
|
||||||
|
let maxId = 0;
|
||||||
|
for (const id of existingIds) {
|
||||||
|
maxId = Math.max(maxId, id);
|
||||||
|
}
|
||||||
|
return (maxId + 1) as ProcessId;
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ProcessId } from './id.js';
|
||||||
|
|
||||||
// Process-related interfaces (used in IPC communication)
|
// Process-related interfaces (used in IPC communication)
|
||||||
export interface IMonitorConfig {
|
export interface IMonitorConfig {
|
||||||
name?: string; // Optional name to identify the instance
|
name?: string; // Optional name to identify the instance
|
||||||
@@ -11,14 +13,14 @@ export interface IMonitorConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IProcessConfig extends IMonitorConfig {
|
export interface IProcessConfig extends IMonitorConfig {
|
||||||
id: string; // Unique identifier for the process
|
id: ProcessId; // Unique identifier for the process
|
||||||
autorestart: boolean; // Whether to restart the process automatically on crash
|
autorestart: boolean; // Whether to restart the process automatically on crash
|
||||||
watch?: boolean; // Whether to watch for file changes and restart
|
watch?: boolean; // Whether to watch for file changes and restart
|
||||||
watchPaths?: string[]; // Paths to watch for changes
|
watchPaths?: string[]; // Paths to watch for changes
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProcessInfo {
|
export interface IProcessInfo {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
status: 'online' | 'stopped' | 'errored';
|
status: 'online' | 'stopped' | 'errored';
|
||||||
memory: number;
|
memory: number;
|
||||||
@@ -61,14 +63,25 @@ export interface StartRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StartResponse {
|
export interface StartResponse {
|
||||||
processId: string;
|
processId: ProcessId;
|
||||||
|
pid?: number;
|
||||||
|
status: 'online' | 'stopped' | 'errored';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start by id (server resolves config)
|
||||||
|
export interface StartByIdRequest {
|
||||||
|
id: ProcessId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartByIdResponse {
|
||||||
|
processId: ProcessId;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
status: 'online' | 'stopped' | 'errored';
|
status: 'online' | 'stopped' | 'errored';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop command
|
// Stop command
|
||||||
export interface StopRequest {
|
export interface StopRequest {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StopResponse {
|
export interface StopResponse {
|
||||||
@@ -78,18 +91,18 @@ export interface StopResponse {
|
|||||||
|
|
||||||
// Restart command
|
// Restart command
|
||||||
export interface RestartRequest {
|
export interface RestartRequest {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RestartResponse {
|
export interface RestartResponse {
|
||||||
processId: string;
|
processId: ProcessId;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
status: 'online' | 'stopped' | 'errored';
|
status: 'online' | 'stopped' | 'errored';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete command
|
// Delete command
|
||||||
export interface DeleteRequest {
|
export interface DeleteRequest {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteResponse {
|
export interface DeleteResponse {
|
||||||
@@ -108,7 +121,7 @@ export interface ListResponse {
|
|||||||
|
|
||||||
// Describe command
|
// Describe command
|
||||||
export interface DescribeRequest {
|
export interface DescribeRequest {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DescribeResponse {
|
export interface DescribeResponse {
|
||||||
@@ -118,7 +131,7 @@ export interface DescribeResponse {
|
|||||||
|
|
||||||
// Get logs command
|
// Get logs command
|
||||||
export interface GetLogsRequest {
|
export interface GetLogsRequest {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
lines?: number;
|
lines?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,9 +145,9 @@ export interface StartAllRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StartAllResponse {
|
export interface StartAllResponse {
|
||||||
started: string[];
|
started: ProcessId[];
|
||||||
failed: Array<{
|
failed: Array<{
|
||||||
id: string;
|
id: ProcessId;
|
||||||
error: string;
|
error: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
@@ -145,9 +158,9 @@ export interface StopAllRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StopAllResponse {
|
export interface StopAllResponse {
|
||||||
stopped: string[];
|
stopped: ProcessId[];
|
||||||
failed: Array<{
|
failed: Array<{
|
||||||
id: string;
|
id: ProcessId;
|
||||||
error: string;
|
error: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
@@ -158,9 +171,23 @@ export interface RestartAllRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RestartAllResponse {
|
export interface RestartAllResponse {
|
||||||
restarted: string[];
|
restarted: ProcessId[];
|
||||||
failed: Array<{
|
failed: Array<{
|
||||||
id: string;
|
id: ProcessId;
|
||||||
|
error: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset command (stop all and clear configs)
|
||||||
|
export interface ResetRequest {
|
||||||
|
// No parameters needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetResponse {
|
||||||
|
stopped: ProcessId[];
|
||||||
|
removed: ProcessId[];
|
||||||
|
failed: Array<{
|
||||||
|
id: ProcessId;
|
||||||
error: string;
|
error: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
@@ -177,6 +204,7 @@ export interface DaemonStatusResponse {
|
|||||||
processCount: number;
|
processCount: number;
|
||||||
memoryUsage?: number;
|
memoryUsage?: number;
|
||||||
cpuUsage?: number;
|
cpuUsage?: number;
|
||||||
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Daemon shutdown command
|
// Daemon shutdown command
|
||||||
@@ -203,17 +231,17 @@ export interface HeartbeatResponse {
|
|||||||
// Add (register config without starting)
|
// Add (register config without starting)
|
||||||
export interface AddRequest {
|
export interface AddRequest {
|
||||||
// Optional id is ignored server-side if present; server assigns sequential id
|
// Optional id is ignored server-side if present; server assigns sequential id
|
||||||
config: Omit<IProcessConfig, 'id'> & { id?: string };
|
config: Omit<IProcessConfig, 'id'> & { id?: ProcessId };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddResponse {
|
export interface AddResponse {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
config: IProcessConfig;
|
config: IProcessConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove (delete config and stop if running)
|
// Remove (delete config and stop if running)
|
||||||
export interface RemoveRequest {
|
export interface RemoveRequest {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoveResponse {
|
export interface RemoveResponse {
|
||||||
@@ -224,6 +252,7 @@ export interface RemoveResponse {
|
|||||||
// Type mappings for methods
|
// Type mappings for methods
|
||||||
export type IpcMethodMap = {
|
export type IpcMethodMap = {
|
||||||
start: { request: StartRequest; response: StartResponse };
|
start: { request: StartRequest; response: StartResponse };
|
||||||
|
startById: { request: StartByIdRequest; response: StartByIdResponse };
|
||||||
stop: { request: StopRequest; response: StopResponse };
|
stop: { request: StopRequest; response: StopResponse };
|
||||||
restart: { request: RestartRequest; response: RestartResponse };
|
restart: { request: RestartRequest; response: RestartResponse };
|
||||||
delete: { request: DeleteRequest; response: DeleteResponse };
|
delete: { request: DeleteRequest; response: DeleteResponse };
|
||||||
@@ -235,6 +264,7 @@ export type IpcMethodMap = {
|
|||||||
startAll: { request: StartAllRequest; response: StartAllResponse };
|
startAll: { request: StartAllRequest; response: StartAllResponse };
|
||||||
stopAll: { request: StopAllRequest; response: StopAllResponse };
|
stopAll: { request: StopAllRequest; response: StopAllResponse };
|
||||||
restartAll: { request: RestartAllRequest; response: RestartAllResponse };
|
restartAll: { request: RestartAllRequest; response: RestartAllResponse };
|
||||||
|
reset: { request: ResetRequest; response: ResetResponse };
|
||||||
'daemon:status': {
|
'daemon:status': {
|
||||||
request: DaemonStatusRequest;
|
request: DaemonStatusRequest;
|
||||||
response: DaemonStatusResponse;
|
response: DaemonStatusResponse;
|
||||||
|
Reference in New Issue
Block a user