Compare commits

..

14 Commits

Author SHA1 Message Date
5036f01516 5.0.0
Some checks failed
Default (tags) / security (push) Successful in 51s
Default (tags) / test (push) Failing after 11m11s
Default (tags) / release (push) Has been cancelled
Default (tags) / metadata (push) Has been cancelled
2025-08-30 13:47:14 +00:00
538f282b62 BREAKING CHANGE(daemon): Introduce persistent log storage, numeric ProcessId type, and improved process monitoring / IPC handling 2025-08-30 13:47:14 +00:00
e507b75c40 4.4.2
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 3m58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 21:22:03 +00:00
97a8377a75 fix(daemon): Fix daemon IPC id handling, reload configs on demand and correct CLI daemon start path 2025-08-29 21:22:03 +00:00
3676bff04c 4.4.1
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 3m58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 21:10:01 +00:00
dfe0677cab fix(cli): Use server-side start-by-id flow for starting processes 2025-08-29 21:10:01 +00:00
611b756670 4.4.0
Some checks failed
Default (tags) / security (push) Successful in 39s
Default (tags) / test (push) Failing after 3m59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 17:27:32 +00:00
2291348774 feat(daemon): Persist desired process states and add daemon restart command 2025-08-29 17:27:32 +00:00
504725043d 4.3.1
Some checks failed
Default (tags) / security (push) Successful in 50s
Default (tags) / test (push) Failing after 3m59s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 17:16:40 +00:00
e16a3fb845 fix(daemon): Fix daemon describe handler to return correct process info and config; bump @push.rocks/smartipc to ^2.2.2 2025-08-29 17:16:40 +00:00
c3d12b287c 4.3.0
Some checks failed
Default (tags) / security (push) Successful in 52s
Default (tags) / test (push) Failing after 3m58s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 16:52:00 +00:00
cbea3f6187 feat(cli): Correct CLI plugin imports and add reset command/IPC to stop processes and clear persisted configs 2025-08-29 16:52:00 +00:00
51aa6eddad 4.2.0
Some checks failed
Default (tags) / security (push) Successful in 54s
Default (tags) / test (push) Failing after 4m1s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 16:22:04 +00:00
5910724b3c feat(cli): Add reset CLI command to stop all processes and clear saved configurations; integrate interactive confirmation and client plugin updates 2025-08-29 16:22:04 +00:00
34 changed files with 1232 additions and 346 deletions

View File

@@ -1,5 +1,68 @@
# 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)
Add 'reset' CLI command to stop all processes and clear saved configurations; integrate interactive confirmation and client plugin updates
- Add new CLI command 'reset' (ts/cli/commands/reset.ts) which stops all processes and removes saved process configurations after an interactive confirmation.
- Use @push.rocks/smartinteract for a confirmation prompt before destructive action.
- Register the new reset command in the CLI bootstrap (ts/cli/index.ts).
- Expose smartinteract from ts/plugins.ts and add @push.rocks/smartinteract to package.json dependencies.
- Introduce a lightweight client plugin shim (ts/client/plugins.ts) and switch tspm.ipcclient to import client plugins from ./plugins.js.
## 2025-08-29 - 4.1.1 - fix(daemon) ## 2025-08-29 - 4.1.1 - fix(daemon)
Bump @push.rocks/smartdaemon to ^2.0.9 Bump @push.rocks/smartdaemon to ^2.0.9

View File

@@ -1,6 +1,6 @@
{ {
"name": "@git.zone/tspm", "name": "@git.zone/tspm",
"version": "4.1.1", "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",
@@ -36,7 +36,9 @@
"@push.rocks/projectinfo": "^5.0.2", "@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartcli": "^4.0.11", "@push.rocks/smartcli": "^4.0.11",
"@push.rocks/smartdaemon": "^2.0.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",

456
pnpm-lock.yaml generated
View File

@@ -20,9 +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':
specifier: ^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
@@ -551,6 +557,62 @@ packages:
resolution: {integrity: sha512-mfOoUlIw8VBiJYPrl5RZfMzkXC/z7gbSpi2ecycrj/gRWLq2CMV+Q+0G+JPjeOmuNFgg0skEIzkVFzVYFP6URw==} resolution: {integrity: sha512-mfOoUlIw8VBiJYPrl5RZfMzkXC/z7gbSpi2ecycrj/gRWLq2CMV+Q+0G+JPjeOmuNFgg0skEIzkVFzVYFP6URw==}
engines: {node: '>=18.0.0'} engines: {node: '>=18.0.0'}
'@inquirer/checkbox@3.0.1':
resolution: {integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==}
engines: {node: '>=18'}
'@inquirer/confirm@4.0.1':
resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==}
engines: {node: '>=18'}
'@inquirer/core@9.2.1':
resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==}
engines: {node: '>=18'}
'@inquirer/editor@3.0.1':
resolution: {integrity: sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==}
engines: {node: '>=18'}
'@inquirer/expand@3.0.1':
resolution: {integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==}
engines: {node: '>=18'}
'@inquirer/figures@1.0.13':
resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==}
engines: {node: '>=18'}
'@inquirer/input@3.0.1':
resolution: {integrity: sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==}
engines: {node: '>=18'}
'@inquirer/number@2.0.1':
resolution: {integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==}
engines: {node: '>=18'}
'@inquirer/password@3.0.1':
resolution: {integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==}
engines: {node: '>=18'}
'@inquirer/prompts@6.0.1':
resolution: {integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==}
engines: {node: '>=18'}
'@inquirer/rawlist@3.0.1':
resolution: {integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==}
engines: {node: '>=18'}
'@inquirer/search@2.0.1':
resolution: {integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==}
engines: {node: '>=18'}
'@inquirer/select@3.0.1':
resolution: {integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==}
engines: {node: '>=18'}
'@inquirer/type@2.0.0':
resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==}
engines: {node: '>=18'}
'@isaacs/balanced-match@4.0.1': '@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@@ -785,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==}
@@ -803,8 +862,11 @@ packages:
'@push.rocks/smarthash@3.2.3': '@push.rocks/smarthash@3.2.3':
resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==} resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==}
'@push.rocks/smartipc@2.2.1': '@push.rocks/smartinteract@2.0.16':
resolution: {integrity: sha512-yBFZwJsWRyVdN1YRSiHafRMfn0PYIi2IStcQqPkiU4Srr6XPDMZD3mmIeV2V1WL6bWvRWf+4WF9Y+rLhj4jGdA==} resolution: {integrity: sha512-eltvVRRUKBKd77DSFA4DPY2g4V4teZLNe8A93CDy/WglglYcUjxMoLY/b0DFTWCWKYT+yjk6Fe6p0FRrvX9Yvg==}
'@push.rocks/smartipc@2.2.2':
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==}
@@ -1573,12 +1635,12 @@ packages:
'@types/ms@2.1.0': '@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/mute-stream@0.0.4':
resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==}
'@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==}
@@ -1660,6 +1722,9 @@ packages:
'@types/which@3.0.4': '@types/which@3.0.4':
resolution: {integrity: sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==} resolution: {integrity: sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==}
'@types/wrap-ansi@3.0.0':
resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==}
'@types/ws@7.4.7': '@types/ws@7.4.7':
resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==}
@@ -1938,6 +2003,9 @@ packages:
character-entities@2.0.2: character-entities@2.0.2:
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
chokidar@4.0.3: chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'} engines: {node: '>= 14.16.0'}
@@ -1967,8 +2035,9 @@ packages:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'} engines: {node: '>=8'}
cliui@7.0.4: cli-width@4.1.0:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
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==}
@@ -2066,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==}
@@ -2242,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=}
@@ -2366,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=}
@@ -2399,6 +2456,10 @@ packages:
extend@3.0.2: extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
external-editor@3.1.0:
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
engines: {node: '>=4'}
extract-zip@2.0.1: extract-zip@2.0.1:
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
engines: {node: '>= 10.17.0'} engines: {node: '>= 10.17.0'}
@@ -2608,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}
@@ -2778,6 +2834,10 @@ packages:
ini@1.3.8: ini@1.3.8:
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
inquirer@11.1.0:
resolution: {integrity: sha512-CmLAZT65GG/v30c+D2Fk8+ceP6pxD6RL+hIUOWAltCmeyEqWYwqu9v76q03OvjyZ3AB0C1Ala2stn1z/rMqGEw==}
engines: {node: '>=18'}
internal-ip@6.2.0: internal-ip@6.2.0:
resolution: {integrity: sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==} resolution: {integrity: sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -2892,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=}
@@ -2927,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}
@@ -2958,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==}
@@ -3358,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}
@@ -3429,6 +3470,10 @@ packages:
ms@2.1.3: ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
mute-stream@1.0.0:
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
nanocolors@0.2.13: nanocolors@0.2.13:
resolution: {integrity: sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==} resolution: {integrity: sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==}
@@ -3465,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'}
@@ -3524,6 +3562,10 @@ packages:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
os-tmpdir@1.0.2:
resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=}
engines: {node: '>=0.10.0'}
p-cancelable@3.0.0: p-cancelable@3.0.0:
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==} resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
@@ -3778,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==}
@@ -3871,6 +3910,10 @@ packages:
rss-parser@3.13.0: rss-parser@3.13.0:
resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==} resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
run-async@3.0.0:
resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==}
engines: {node: '>=0.12.0'}
run-parallel@1.2.0: run-parallel@1.2.0:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -4063,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==}
@@ -4105,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'}
@@ -4166,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==}
@@ -4182,6 +4211,10 @@ packages:
tiny-worker@2.3.0: tiny-worker@2.3.0:
resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==} resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==}
tmp@0.0.33:
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
engines: {node: '>=0.6.0'}
to-regex-range@5.0.1: to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'} engines: {node: '>=8.0'}
@@ -4323,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=}
@@ -4452,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'}
@@ -4487,6 +4504,10 @@ packages:
resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==} resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==}
engines: {node: '>= 4.0.0'} engines: {node: '>= 4.0.0'}
yoctocolors-cjs@2.1.3:
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
engines: {node: '>=18'}
zod@3.25.76: zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
@@ -5631,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
@@ -5686,6 +5707,102 @@ snapshots:
dependencies: dependencies:
happy-dom: 15.11.7 happy-dom: 15.11.7
'@inquirer/checkbox@3.0.1':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/figures': 1.0.13
'@inquirer/type': 2.0.0
ansi-escapes: 4.3.2
yoctocolors-cjs: 2.1.3
'@inquirer/confirm@4.0.1':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 2.0.0
'@inquirer/core@9.2.1':
dependencies:
'@inquirer/figures': 1.0.13
'@inquirer/type': 2.0.0
'@types/mute-stream': 0.0.4
'@types/node': 22.13.10
'@types/wrap-ansi': 3.0.0
ansi-escapes: 4.3.2
cli-width: 4.1.0
mute-stream: 1.0.0
signal-exit: 4.1.0
strip-ansi: 6.0.1
wrap-ansi: 6.2.0
yoctocolors-cjs: 2.1.3
'@inquirer/editor@3.0.1':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 2.0.0
external-editor: 3.1.0
'@inquirer/expand@3.0.1':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 2.0.0
yoctocolors-cjs: 2.1.3
'@inquirer/figures@1.0.13': {}
'@inquirer/input@3.0.1':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 2.0.0
'@inquirer/number@2.0.1':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 2.0.0
'@inquirer/password@3.0.1':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 2.0.0
ansi-escapes: 4.3.2
'@inquirer/prompts@6.0.1':
dependencies:
'@inquirer/checkbox': 3.0.1
'@inquirer/confirm': 4.0.1
'@inquirer/editor': 3.0.1
'@inquirer/expand': 3.0.1
'@inquirer/input': 3.0.1
'@inquirer/number': 2.0.1
'@inquirer/password': 3.0.1
'@inquirer/rawlist': 3.0.1
'@inquirer/search': 2.0.1
'@inquirer/select': 3.0.1
'@inquirer/rawlist@3.0.1':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/type': 2.0.0
yoctocolors-cjs: 2.1.3
'@inquirer/search@2.0.1':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/figures': 1.0.13
'@inquirer/type': 2.0.0
yoctocolors-cjs: 2.1.3
'@inquirer/select@3.0.1':
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/figures': 1.0.13
'@inquirer/type': 2.0.0
ansi-escapes: 4.3.2
yoctocolors-cjs: 2.1.3
'@inquirer/type@2.0.0':
dependencies:
mute-stream: 1.0.0
'@isaacs/balanced-match@4.0.1': {} '@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0': '@isaacs/brace-expansion@5.0.0':
@@ -6160,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
@@ -6221,12 +6319,17 @@ snapshots:
'@types/through2': 2.0.41 '@types/through2': 2.0.41
through2: 4.0.2 through2: 4.0.2
'@push.rocks/smartipc@2.2.1': '@push.rocks/smartinteract@2.0.16':
dependencies:
'@push.rocks/lik': 6.2.2
'@push.rocks/smartobject': 1.0.12
'@push.rocks/smartpromise': 4.2.3
inquirer: 11.1.0
'@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:
@@ -7475,11 +7578,11 @@ snapshots:
'@types/ms@2.1.0': {} '@types/ms@2.1.0': {}
'@types/node-forge@1.3.14': '@types/mute-stream@0.0.4':
dependencies: dependencies:
'@types/node': 22.13.10 '@types/node': 22.13.10
'@types/node-ipc@9.2.3': '@types/node-forge@1.3.14':
dependencies: dependencies:
'@types/node': 22.13.10 '@types/node': 22.13.10
@@ -7561,6 +7664,8 @@ snapshots:
'@types/which@3.0.4': {} '@types/which@3.0.4': {}
'@types/wrap-ansi@3.0.0': {}
'@types/ws@7.4.7': '@types/ws@7.4.7':
dependencies: dependencies:
'@types/node': 22.13.10 '@types/node': 22.13.10
@@ -7888,6 +7993,8 @@ snapshots:
character-entities@2.0.2: {} character-entities@2.0.2: {}
chardet@0.7.0: {}
chokidar@4.0.3: chokidar@4.0.3:
dependencies: dependencies:
readdirp: 4.1.2 readdirp: 4.1.2
@@ -7914,11 +8021,7 @@ snapshots:
dependencies: dependencies:
restore-cursor: 3.1.0 restore-cursor: 3.1.0
cliui@7.0.4: cli-width@4.1.0: {}
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:
@@ -8011,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:
@@ -8157,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: {}
@@ -8291,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
@@ -8370,6 +8456,12 @@ snapshots:
extend@3.0.2: {} extend@3.0.2: {}
external-editor@3.1.0:
dependencies:
chardet: 0.7.0
iconv-lite: 0.4.24
tmp: 0.0.33
extract-zip@2.0.1: extract-zip@2.0.1:
dependencies: dependencies:
debug: 4.4.1 debug: 4.4.1
@@ -8612,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
@@ -8842,6 +8925,17 @@ snapshots:
ini@1.3.8: {} ini@1.3.8: {}
inquirer@11.1.0:
dependencies:
'@inquirer/core': 9.2.1
'@inquirer/prompts': 6.0.1
'@inquirer/type': 2.0.0
'@types/mute-stream': 0.0.4
ansi-escapes: 4.3.2
mute-stream: 1.0.0
run-async: 3.0.0
rxjs: 7.8.2
internal-ip@6.2.0: internal-ip@6.2.0:
dependencies: dependencies:
default-gateway: 6.0.3 default-gateway: 6.0.3
@@ -8929,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: {}
@@ -8960,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
@@ -9007,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:
@@ -9602,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
@@ -9680,6 +9758,8 @@ snapshots:
ms@2.1.3: {} ms@2.1.3: {}
mute-stream@1.0.0: {}
nanocolors@0.2.13: {} nanocolors@0.2.13: {}
nanoid@3.3.11: {} nanoid@3.3.11: {}
@@ -9704,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
@@ -9760,6 +9828,8 @@ snapshots:
is-docker: 2.2.1 is-docker: 2.2.1
is-wsl: 2.2.0 is-wsl: 2.2.0
os-tmpdir@1.0.2: {}
p-cancelable@3.0.0: {} p-cancelable@3.0.0: {}
p-event@4.2.0: p-event@4.2.0:
@@ -10032,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
@@ -10179,6 +10242,8 @@ snapshots:
entities: 2.2.0 entities: 2.2.0
xml2js: 0.5.0 xml2js: 0.5.0
run-async@3.0.0: {}
run-parallel@1.2.0: run-parallel@1.2.0:
dependencies: dependencies:
queue-microtask: 1.2.3 queue-microtask: 1.2.3
@@ -10434,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
@@ -10471,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
@@ -10539,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
@@ -10558,6 +10612,10 @@ snapshots:
dependencies: dependencies:
esm: 3.2.25 esm: 3.2.25
tmp@0.0.33:
dependencies:
os-tmpdir: 1.0.2
to-regex-range@5.0.1: to-regex-range@5.0.1:
dependencies: dependencies:
is-number: 7.0.0 is-number: 7.0.0
@@ -10680,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:
@@ -10788,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
@@ -10828,6 +10870,8 @@ snapshots:
ylru@1.4.0: {} ylru@1.4.0: {}
yoctocolors-cjs@2.1.3: {}
zod@3.25.76: {} zod@3.25.76: {}
zwitch@2.0.4: {} zwitch@2.0.4: {}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import * as plugins from '../../../plugins.js'; import * as plugins from '../../plugins.js';
import * as paths from '../../../paths.js'; import * as paths from '../../../paths.js';
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;

View File

@@ -1,4 +1,4 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../plugins.js';
import * as paths from '../../paths.js'; import * as paths from '../../paths.js';
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)}`,
); );
} }

View File

@@ -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' },
); );
} }

View File

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

View File

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

View File

@@ -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)}`,
); );
} }

View File

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

View File

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

View File

@@ -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'}`);

View File

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

33
ts/cli/commands/reset.ts Normal file
View File

@@ -0,0 +1,33 @@
import * as plugins from '../plugins.js';
import { registerIpcCommand } from '../registration/index.js';
import { tspmIpcClient } from '../../client/tspm.ipcclient.js';
export function registerResetCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'reset',
async () => {
console.log('This will stop all processes and clear saved configurations.');
const confirmed = await plugins.smartinteract.SmartInteract.getCliConfirmation(
'Are you sure you want to reset TSPM? (stops all and removes configs)',
false,
);
if (!confirmed) {
console.log('Reset cancelled. No changes made.');
return;
}
// Single IPC call to reset
const result = await tspmIpcClient.request('reset', {});
const failedCount = result.failed.length;
console.log(`Stopped ${result.stopped.length} processes.`);
if (failedCount) {
console.log(`${failedCount} processes failed to stop (configs cleared anyway).`);
}
console.log(`Cleared ${result.removed.length} saved configurations.`);
console.log('TSPM has been reset.');
},
{ actionLabel: 'reset TSPM' },
);
}

View File

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

View File

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

View File

@@ -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';
@@ -18,6 +19,7 @@ import { registerRestartAllCommand } from './commands/batch/restart-all.js';
import { registerDaemonCommand } from './commands/daemon/index.js'; import { registerDaemonCommand } from './commands/daemon/index.js';
import { registerEnableCommand } from './commands/service/enable.js'; import { registerEnableCommand } from './commands/service/enable.js';
import { registerDisableCommand } from './commands/service/disable.js'; import { registerDisableCommand } from './commands/service/disable.js';
import { registerResetCommand } from './commands/reset.js';
// Export types for external use // Export types for external use
export type { CliArguments } from './types.js'; export type { CliArguments } from './types.js';
@@ -37,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
@@ -65,6 +85,9 @@ export const run = async (): Promise<void> => {
registerEnableCommand(smartcliInstance); registerEnableCommand(smartcliInstance);
registerDisableCommand(smartcliInstance); registerDisableCommand(smartcliInstance);
// Maintenance commands
registerResetCommand(smartcliInstance);
// Start parsing commands // Start parsing commands
smartcliInstance.startParse(); smartcliInstance.startParse();
}; };

8
ts/cli/plugins.ts Normal file
View 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 };

View File

@@ -1,4 +1,4 @@
import * as plugins from '../../plugins.js'; import * as plugins from '../plugins.js';
import type { import type {
CliArguments, CliArguments,
CommandAction, CommandAction,

6
ts/client/plugins.ts Normal file
View File

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

View File

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

View File

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

View File

@@ -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;
} }
/** /**

View File

@@ -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 = '';
} }
}); });
} }

View File

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

View File

@@ -10,11 +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';
// Export with explicit module types // Export with explicit module types
export { npmextra, projectinfo, smartcli, smartdaemon, smartipc, smartpath }; 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
View 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;
}

View File

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