Compare commits

...

22 Commits

Author SHA1 Message Date
22a43204d4 5.2.0
Some checks failed
Default (tags) / security (push) Successful in 56s
Default (tags) / test (push) Failing after 4m25s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-30 15:11:38 +00:00
699d07ea36 feat(cli): Preserve CLI environment when adding processes, simplify edit flow, and refresh README docs 2025-08-30 15:11:38 +00:00
2b57251f47 5.1.0
Some checks failed
Default (tags) / security (push) Successful in 58s
Default (tags) / test (push) Failing after 4m27s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-30 14:02:23 +00:00
311a536fae feat(cli): Add interactive edit command and update support for process configurations 2025-08-30 14:02:22 +00:00
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
a67d247e9c 4.1.1
Some checks failed
Default (tags) / security (push) Successful in 40s
Default (tags) / test (push) Failing after 3m57s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 13:35:20 +00:00
f7bc56e676 fix(daemon): Bump @push.rocks/smartdaemon to ^2.0.9 2025-08-29 13:35:20 +00:00
7bfda01768 4.1.0
Some checks failed
Default (tags) / security (push) Successful in 51s
Default (tags) / test (push) Failing after 3m57s
Default (tags) / release (push) Has been skipped
Default (tags) / metadata (push) Has been skipped
2025-08-29 12:16:43 +00:00
27384d03c7 feat(cli): Add support for restarting all processes from CLI; improve usage message and reporting 2025-08-29 12:16:43 +00:00
36 changed files with 1770 additions and 503 deletions

View File

@@ -1,5 +1,96 @@
# Changelog
## 2025-08-30 - 5.2.0 - feat(cli)
Preserve CLI environment when adding processes, simplify edit flow, and refresh README docs
- CLI: When adding a process, capture and persist essential environment variables from the CLI (PATH, HOME, USER, SHELL, LANG, LC_ALL, NODE_ENV, NODE_PATH, npm_config_prefix and any TSPM_* variables). Undefined values are removed before storing.
- CLI: Interactive edit flow temporarily disabled. The edit command now displays the current configuration and updates stored environment variables to match the current CLI environment.
- Docs: Major README refresh — reorganized sections, clarified add vs start semantics, expanded examples, added daemon/service usage and programmatic API examples, and improved command reference and output examples.
## 2025-08-30 - 5.1.0 - feat(cli)
Add interactive edit command and update support for process configurations
- Add 'tspm edit' interactive CLI command to modify saved process configurations (prompts for name, command, args, cwd, memory, autorestart, watch, watch paths) with an option to replace stored PATH.
- Implement ProcessManager.update(id, updates) to merge updates, persist them, and return the updated configuration.
- Add 'update' IPC method and daemon handler to allow remote/configurations updates via IPC.
- Persist the current CLI PATH when adding a process so managed processes inherit the same PATH environment.
- Merge provided env with the runtime process.env when spawning processes to avoid fully overriding the runtime environment.
## 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)
Bump @push.rocks/smartdaemon to ^2.0.9
- Update @push.rocks/smartdaemon from ^2.0.8 to ^2.0.9 (dependency version bump)
## 2025-08-29 - 4.1.0 - feat(cli)
Add support for restarting all processes from CLI; improve usage message and reporting
- CLI 'restart' command now accepts 'all' to restart all processes via the daemon (tspm restart all).
- Improved usage/help output when no process id is provided.
- CLI now prints summaries of restarted process IDs and failed restarts and sets a non-zero exit code when any restarts failed.
## 2025-08-29 - 4.0.0 - BREAKING CHANGE(cli)
Add persistent process registration (tspm add), alias remove, and change start to use saved process IDs (breaking CLI behavior)

View File

@@ -1,6 +1,6 @@
{
"name": "@git.zone/tspm",
"version": "4.0.0",
"version": "5.2.0",
"private": false,
"description": "a no fuzz process manager",
"main": "dist_ts/index.js",
@@ -35,8 +35,10 @@
"@push.rocks/npmextra": "^5.3.3",
"@push.rocks/projectinfo": "^5.0.2",
"@push.rocks/smartcli": "^4.0.11",
"@push.rocks/smartdaemon": "^2.0.8",
"@push.rocks/smartipc": "^2.2.1",
"@push.rocks/smartdaemon": "^2.0.9",
"@push.rocks/smartfile": "^11.2.7",
"@push.rocks/smartinteract": "^2.0.16",
"@push.rocks/smartipc": "^2.2.2",
"@push.rocks/smartpath": "^6.0.0",
"pidusage": "^4.0.1",
"ps-tree": "^1.2.0",

533
pnpm-lock.yaml generated
View File

@@ -18,11 +18,17 @@ importers:
specifier: ^4.0.11
version: 4.0.11
'@push.rocks/smartdaemon':
specifier: ^2.0.8
version: 2.0.8
specifier: ^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':
specifier: ^2.2.1
version: 2.2.1
specifier: ^2.2.2
version: 2.2.2
'@push.rocks/smartpath':
specifier: ^6.0.0
version: 6.0.0
@@ -551,6 +557,62 @@ packages:
resolution: {integrity: sha512-mfOoUlIw8VBiJYPrl5RZfMzkXC/z7gbSpi2ecycrj/gRWLq2CMV+Q+0G+JPjeOmuNFgg0skEIzkVFzVYFP6URw==}
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':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22}
@@ -755,8 +817,8 @@ packages:
'@push.rocks/smartcrypto@2.0.4':
resolution: {integrity: sha512-1+/5bsjyataf5uUkUNnnVXGRAt+gHVk1KDzozjTqgqJxHvQk1d9fVDohL6CxUhUucTPtu5VR5xNBiV8YCDuGyw==}
'@push.rocks/smartdaemon@2.0.8':
resolution: {integrity: sha512-92qCS8XqGhQrCBDrz5L+WrWzlAggy93mXacVx9zEzGK41QwxRxZSMfxEMTxq4FO9YD4Kymffesav7S3ivCuJeQ==}
'@push.rocks/smartdaemon@2.0.9':
resolution: {integrity: sha512-TJd2N/vMAY3qpuy7ub0btNsSqdy7oU/hF/D+BbmfJVAiTKpvlgtCXKE5POwfuee03SONyh8LuH5Ey1ycIpsEHA==}
'@push.rocks/smartdata@5.16.4':
resolution: {integrity: sha512-COiKw8yk9iAcLN44WmZHG8Gi0v+HGkgM8Osoq7Cns+UsOA+grPepqbN2r0XPG1fm5vOdJcaydi2ZU0xrnbGVvQ==}
@@ -785,9 +847,6 @@ packages:
'@push.rocks/smartfile@10.0.41':
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':
resolution: {integrity: sha512-8Yp7/sAgPpWJBHohV92ogHWKzRomI5MEbSG6b5W2n18tqwfAmjMed0rQvsvGrSBlnEWCKgoOrYIIZbLO61+J0Q==}
@@ -803,8 +862,11 @@ packages:
'@push.rocks/smarthash@3.2.3':
resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==}
'@push.rocks/smartipc@2.2.1':
resolution: {integrity: sha512-yBFZwJsWRyVdN1YRSiHafRMfn0PYIi2IStcQqPkiU4Srr6XPDMZD3mmIeV2V1WL6bWvRWf+4WF9Y+rLhj4jGdA==}
'@push.rocks/smartinteract@2.0.16':
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':
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
@@ -842,6 +904,9 @@ packages:
'@push.rocks/smartmongo@2.0.12':
resolution: {integrity: sha512-NglYiO14BikxnlvW6JF18FtopBtaWQEGAtPxHmmSCbyhU8Mi0aEFO7VgCasE9Kguba/wcR597qhcDEdcpBg1eQ==}
'@push.rocks/smartnetwork@3.0.2':
resolution: {integrity: sha512-s6CNGzQ1n/d/6cOKXbxeW6/tO//dr1woLqI01g7XhqTriw0nsm2G2kWaZh2J0VOguGNWBgQVCIpR0LjdRNWb3g==}
'@push.rocks/smartnetwork@4.1.2':
resolution: {integrity: sha512-TjucG72ooHgzAUpNu2LAv4iFoettmZq2aEWhhzIa7AKcOvt4yxsk3Vl73guhKRohTfhdRauPcH5OHISLUHJbYA==}
@@ -923,8 +988,8 @@ packages:
'@push.rocks/smartstring@4.0.15':
resolution: {integrity: sha512-NTNeOjWyg+aHtBTiQEyXamr7oTvYZ3wS1fudHo9ua7CLrykpK+i+RxFyJaLg1zB5x9xQF3NLEQecB14HPFX8Cg==}
'@push.rocks/smartsystem@3.0.1':
resolution: {integrity: sha512-+W9AiSJWcRAjthqDFT8rDli2+5k3bk8c9Psndy3uKN2YbaQkMZwWptZRI3WgpXMG9NhsjF8XrkyiH/xHv9AxzQ==}
'@push.rocks/smartsystem@3.0.7':
resolution: {integrity: sha512-FSzrJKY+pAIxlPR1cQgUd/Edy82UDusl4n2aA+Fe564Qf7KHfFY9sTapjX1JJU6zP/hmBKWzApKa7/m+qF6Tog==}
'@push.rocks/smarttime@4.1.1':
resolution: {integrity: sha512-Ha/3J/G+zfTl4ahpZgF6oUOZnUjpLhrBja0OQ2cloFxF9sKT8I1COaSqIfBGDtoK2Nly4UD4aTJ3JcJNOg/kgA==}
@@ -998,10 +1063,6 @@ packages:
resolution: {integrity: sha512-PLvBNVeuY9BERNLq3PFDkhnHHc0RpilEGHd4aUI5XRFlZF++LETdLxPbxw+DHbvHlkUf/nep09f7rrL9Tqub1Q==}
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartmatch
'@pushrocks/smartnetwork@3.0.2':
resolution: {integrity: sha512-XKVeTzf22IRgAvY9m8naFlsjh5yYVCU4/Dqi7XnxQUVfrnrcNIJVo+9JIYjQetLbHiUOHAnthlZVP5yXppOxyw==}
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartnetwork
'@pushrocks/smartping@1.0.8':
resolution: {integrity: sha512-VM2gfS1sTuycj/jHyDa0lDntkPe7/JT0b2kGsy265RkichAJZkoEp3fboRJH/WAdzM8T4Du64JYgZkc8v2HHQg==}
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartping
@@ -1574,12 +1635,12 @@ packages:
'@types/ms@2.1.0':
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':
resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
'@types/node-ipc@9.2.3':
resolution: {integrity: sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==}
'@types/node@22.13.10':
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
@@ -1661,6 +1722,9 @@ packages:
'@types/which@3.0.4':
resolution: {integrity: sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==}
'@types/wrap-ansi@3.0.0':
resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==}
'@types/ws@7.4.7':
resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==}
@@ -1939,6 +2003,9 @@ packages:
character-entities@2.0.2:
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
chardet@0.7.0:
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -1968,8 +2035,9 @@ packages:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
cli-width@4.1.0:
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
engines: {node: '>= 12'}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
@@ -2067,10 +2135,6 @@ packages:
resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==}
engines: {node: '>= 0.8'}
copyfiles@2.4.1:
resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==}
hasBin: true
core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
@@ -2243,10 +2307,6 @@ packages:
eastasianwidth@0.2.0:
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:
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
@@ -2367,10 +2427,6 @@ packages:
resolution: {integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=}
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:
resolution: {integrity: sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=}
@@ -2400,6 +2456,10 @@ packages:
extend@3.0.2:
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:
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
engines: {node: '>= 10.17.0'}
@@ -2609,11 +2669,6 @@ packages:
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
hasBin: true
glob@11.0.1:
resolution: {integrity: sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==}
engines: {node: 20 || >=22}
hasBin: true
glob@11.0.3:
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
engines: {node: 20 || >=22}
@@ -2779,6 +2834,10 @@ packages:
ini@1.3.8:
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:
resolution: {integrity: sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==}
engines: {node: '>=10'}
@@ -2893,9 +2952,6 @@ packages:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
isarray@0.0.1:
resolution: {integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=}
isarray@1.0.0:
resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=}
@@ -2928,10 +2984,6 @@ packages:
jackspeak@3.4.3:
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:
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
engines: {node: 20 || >=22}
@@ -2959,14 +3011,6 @@ packages:
js-base64@3.7.7:
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:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -3359,10 +3403,6 @@ packages:
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
engines: {node: '>=4'}
minimatch@10.0.1:
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
engines: {node: 20 || >=22}
minimatch@10.0.3:
resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
engines: {node: 20 || >=22}
@@ -3430,6 +3470,10 @@ packages:
ms@2.1.3:
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:
resolution: {integrity: sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==}
@@ -3466,13 +3510,6 @@ packages:
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
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:
resolution: {integrity: sha512-ff4jKqMI8Xl50/4Mms/9jPobzAV/UK+kXG2XJ/7AqOmxIx8mqfqTIHYxuAnEgJ2AQeBbLnlbmZ5+38Y9A0w/YA==}
engines: {node: '>=12'}
@@ -3525,6 +3562,10 @@ packages:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
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:
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
engines: {node: '>=12.20'}
@@ -3779,9 +3820,6 @@ packages:
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
readable-stream@1.0.34:
resolution: {integrity: sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=}
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
@@ -3872,6 +3910,10 @@ packages:
rss-parser@3.13.0:
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:
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
@@ -4064,9 +4106,6 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
string_decoder@0.10.31:
resolution: {integrity: sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=}
string_decoder@1.1.1:
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
@@ -4106,14 +4145,6 @@ packages:
strnum@2.1.1:
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:
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
engines: {node: '>=18'}
@@ -4140,14 +4171,14 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
systeminformation@5.25.11:
resolution: {integrity: sha512-jI01fn/t47rrLTQB0FTlMCC+5dYx8o0RRF+R4BPiUNsvg5OdY0s9DKMFmJGrx5SwMZQ4cag0Gl6v8oycso9b/g==}
systeminformation@5.27.7:
resolution: {integrity: sha512-saaqOoVEEFaux4v0K8Q7caiauRwjXC4XbD2eH60dxHXbpKxQ8kH9Rf7Jh+nryKpOUSEFxtCdBlSUx0/lO6rwRg==}
engines: {node: '>=8.0.0'}
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
hasBin: true
systeminformation@5.27.7:
resolution: {integrity: sha512-saaqOoVEEFaux4v0K8Q7caiauRwjXC4XbD2eH60dxHXbpKxQ8kH9Rf7Jh+nryKpOUSEFxtCdBlSUx0/lO6rwRg==}
systeminformation@5.27.8:
resolution: {integrity: sha512-d3Z0gaQO1MlUxzDUKsmXz5y4TOBCMZ8IyijzaYOykV3AcNOTQ7mT+tpndUOXYNSxzLK3la8G32xiUFvZ0/s6PA==}
engines: {node: '>=8.0.0'}
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
hasBin: true
@@ -4167,9 +4198,6 @@ packages:
threads@1.7.0:
resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==}
through2@2.0.5:
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
through2@4.0.2:
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
@@ -4183,6 +4211,10 @@ packages:
tiny-worker@2.3.0:
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:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
@@ -4324,10 +4356,6 @@ packages:
resolution: {integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=}
engines: {node: '>= 0.8'}
untildify@4.0.0:
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
engines: {node: '>=8'}
upper-case@1.1.3:
resolution: {integrity: sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=}
@@ -4453,26 +4481,14 @@ packages:
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
engines: {node: '>=0.4.0'}
xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
yargs-parser@20.2.9:
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
engines: {node: '>=10'}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs@16.2.0:
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
engines: {node: '>=10'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
@@ -4488,6 +4504,10 @@ packages:
resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==}
engines: {node: '>= 4.0.0'}
yoctocolors-cjs@2.1.3:
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
engines: {node: '>=18'}
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
@@ -5632,7 +5652,7 @@ snapshots:
'@git.zone/tsrun@1.3.3':
dependencies:
'@push.rocks/smartfile': 11.2.0
'@push.rocks/smartfile': 11.2.7
'@push.rocks/smartshell': 3.2.3
tsx: 4.20.5
@@ -5687,6 +5707,102 @@ snapshots:
dependencies:
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/brace-expansion@5.0.0':
@@ -6074,16 +6190,16 @@ snapshots:
'@types/node-forge': 1.3.14
node-forge: 1.3.1
'@push.rocks/smartdaemon@2.0.8':
'@push.rocks/smartdaemon@2.0.9':
dependencies:
'@push.rocks/lik': 6.1.0
'@push.rocks/smartfile': 11.2.0
'@push.rocks/lik': 6.2.2
'@push.rocks/smartfile': 11.2.7
'@push.rocks/smartfm': 2.2.2
'@push.rocks/smartlog': 3.0.7
'@push.rocks/smartlog': 3.1.8
'@push.rocks/smartlog-destination-local': 9.0.2
'@push.rocks/smartpath': 5.1.0
'@push.rocks/smartshell': 3.2.3
'@push.rocks/smartsystem': 3.0.1
'@push.rocks/smartpath': 6.0.0
'@push.rocks/smartshell': 3.3.0
'@push.rocks/smartsystem': 3.0.7
'@push.rocks/smartdata@5.16.4(@aws-sdk/credential-providers@3.758.0)(socks@2.8.7)':
dependencies:
@@ -6161,25 +6277,6 @@ snapshots:
glob: 10.4.5
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':
dependencies:
'@push.rocks/lik': 6.2.2
@@ -6222,12 +6319,17 @@ snapshots:
'@types/through2': 2.0.41
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:
'@push.rocks/smartdelay': 3.0.5
'@push.rocks/smartrx': 3.0.10
'@types/node-ipc': 9.2.3
node-ipc: 12.0.0
'@push.rocks/smartjson@5.0.20':
dependencies:
@@ -6318,6 +6420,16 @@ snapshots:
- socks
- supports-color
'@push.rocks/smartnetwork@3.0.2':
dependencies:
'@pushrocks/smartping': 1.0.8
'@pushrocks/smartpromise': 3.1.10
'@pushrocks/smartstring': 4.0.7
'@types/default-gateway': 3.0.1
isopen: 1.3.0
public-ip: 6.0.2
systeminformation: 5.27.8
'@push.rocks/smartnetwork@4.1.2':
dependencies:
'@push.rocks/smartping': 1.0.8
@@ -6559,13 +6671,13 @@ snapshots:
strip-indent: 4.0.0
url: 0.11.4
'@push.rocks/smartsystem@3.0.1':
'@push.rocks/smartsystem@3.0.7':
dependencies:
'@pushrocks/lik': 6.0.2
'@pushrocks/smartenv': 5.0.5
'@pushrocks/smartnetwork': 3.0.2
'@pushrocks/smartpromise': 3.1.10
systeminformation: 5.25.11
'@push.rocks/lik': 6.2.2
'@push.rocks/smartenv': 5.0.13
'@push.rocks/smartnetwork': 3.0.2
'@push.rocks/smartpromise': 4.2.3
systeminformation: 5.27.8
'@push.rocks/smarttime@4.1.1':
dependencies:
@@ -6721,16 +6833,6 @@ snapshots:
dependencies:
matcher: 5.0.0
'@pushrocks/smartnetwork@3.0.2':
dependencies:
'@pushrocks/smartping': 1.0.8
'@pushrocks/smartpromise': 3.1.10
'@pushrocks/smartstring': 4.0.7
'@types/default-gateway': 3.0.1
isopen: 1.3.0
public-ip: 6.0.2
systeminformation: 5.25.11
'@pushrocks/smartping@1.0.8':
dependencies:
'@types/ping': 0.4.4
@@ -7476,11 +7578,11 @@ snapshots:
'@types/ms@2.1.0': {}
'@types/node-forge@1.3.14':
'@types/mute-stream@0.0.4':
dependencies:
'@types/node': 22.13.10
'@types/node-ipc@9.2.3':
'@types/node-forge@1.3.14':
dependencies:
'@types/node': 22.13.10
@@ -7562,6 +7664,8 @@ snapshots:
'@types/which@3.0.4': {}
'@types/wrap-ansi@3.0.0': {}
'@types/ws@7.4.7':
dependencies:
'@types/node': 22.13.10
@@ -7889,6 +7993,8 @@ snapshots:
character-entities@2.0.2: {}
chardet@0.7.0: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
@@ -7915,11 +8021,7 @@ snapshots:
dependencies:
restore-cursor: 3.1.0
cliui@7.0.4:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
cli-width@4.1.0: {}
cliui@8.0.1:
dependencies:
@@ -8012,16 +8114,6 @@ snapshots:
depd: 2.0.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: {}
cors@2.8.5:
@@ -8158,8 +8250,6 @@ snapshots:
eastasianwidth@0.2.0: {}
easy-stack@1.0.1: {}
ee-first@1.1.1: {}
emoji-regex@8.0.0: {}
@@ -8292,11 +8382,6 @@ snapshots:
etag@1.8.1: {}
event-pubsub@5.0.3:
dependencies:
copyfiles: 2.4.1
strong-type: 0.1.6
event-stream@3.3.4:
dependencies:
duplexer: 0.1.2
@@ -8371,6 +8456,12 @@ snapshots:
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:
dependencies:
debug: 4.4.1
@@ -8613,15 +8704,6 @@ snapshots:
package-json-from-dist: 1.0.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:
dependencies:
foreground-child: 3.3.1
@@ -8843,6 +8925,17 @@ snapshots:
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:
dependencies:
default-gateway: 6.0.3
@@ -8930,8 +9023,6 @@ snapshots:
dependencies:
is-docker: 2.2.1
isarray@0.0.1: {}
isarray@1.0.0: {}
isbinaryfile@5.0.4: {}
@@ -8961,10 +9052,6 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
jackspeak@4.1.0:
dependencies:
'@isaacs/cliui': 8.0.2
jackspeak@4.1.1:
dependencies:
'@isaacs/cliui': 8.0.2
@@ -9008,12 +9095,6 @@ snapshots:
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-yaml@3.14.1:
@@ -9603,10 +9684,6 @@ snapshots:
min-indent@1.0.1: {}
minimatch@10.0.1:
dependencies:
brace-expansion: 2.0.1
minimatch@10.0.3:
dependencies:
'@isaacs/brace-expansion': 5.0.0
@@ -9681,6 +9758,8 @@ snapshots:
ms@2.1.3: {}
mute-stream@1.0.0: {}
nanocolors@0.2.13: {}
nanoid@3.3.11: {}
@@ -9705,18 +9784,6 @@ snapshots:
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:
dependencies:
replace-buffer: 1.2.1
@@ -9761,6 +9828,8 @@ snapshots:
is-docker: 2.2.1
is-wsl: 2.2.0
os-tmpdir@1.0.2: {}
p-cancelable@3.0.0: {}
p-event@4.2.0:
@@ -10033,13 +10102,6 @@ snapshots:
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:
dependencies:
core-util-is: 1.0.3
@@ -10180,6 +10242,8 @@ snapshots:
entities: 2.2.0
xml2js: 0.5.0
run-async@3.0.0: {}
run-parallel@1.2.0:
dependencies:
queue-microtask: 1.2.3
@@ -10435,8 +10499,6 @@ snapshots:
emoji-regex: 9.2.2
strip-ansi: 7.1.0
string_decoder@0.10.31: {}
string_decoder@1.1.1:
dependencies:
safe-buffer: 5.1.2
@@ -10472,10 +10534,6 @@ snapshots:
strnum@2.1.1: {}
strong-type@0.1.6: {}
strong-type@1.1.0: {}
strtok3@10.3.4:
dependencies:
'@tokenizer/token': 0.3.0
@@ -10503,10 +10561,10 @@ snapshots:
symbol-tree@3.2.4: {}
systeminformation@5.25.11: {}
systeminformation@5.27.7: {}
systeminformation@5.27.8: {}
tar-fs@3.1.0:
dependencies:
pump: 3.0.3
@@ -10540,11 +10598,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
through2@2.0.5:
dependencies:
readable-stream: 2.3.8
xtend: 4.0.2
through2@4.0.2:
dependencies:
readable-stream: 3.6.2
@@ -10559,6 +10612,10 @@ snapshots:
dependencies:
esm: 3.2.25
tmp@0.0.33:
dependencies:
os-tmpdir: 1.0.2
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0
@@ -10681,8 +10738,6 @@ snapshots:
unpipe@1.0.0: {}
untildify@4.0.0: {}
upper-case@1.1.3: {}
url@0.11.4:
@@ -10789,24 +10844,10 @@ snapshots:
xmlhttprequest-ssl@2.1.2: {}
xtend@4.0.2: {}
y18n@5.0.8: {}
yargs-parser@20.2.9: {}
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:
dependencies:
cliui: 8.0.1
@@ -10829,6 +10870,8 @@ snapshots:
ylru@1.4.0: {}
yoctocolors-cjs@2.1.3: {}
zod@3.25.76: {}
zwitch@2.0.4: {}

401
readme.md
View File

@@ -2,94 +2,97 @@
**TypeScript Process Manager** - A robust, no-fuss process manager designed specifically for TypeScript and Node.js applications. Built for developers who need reliable process management without the complexity.
## 🎯 What TSPM Does
## 🎯 What is TSPM?
TSPM is your production-ready process manager that handles the hard parts of running Node.js applications:
TSPM (TypeScript Process Manager) is your production-ready process manager that handles the hard parts of running Node.js applications. It's like PM2, but built from the ground up for the modern TypeScript ecosystem with better memory management, intelligent logging, and a cleaner architecture.
- **Automatic Memory Management** - Set memory limits and let TSPM handle the rest
- **Smart Auto-Restart** - Crashed processes come back automatically (when you want them to)
- **File Watching** - Auto-restart on file changes during development
- **Process Groups** - Track parent and child processes together
- **Daemon Architecture** - Survives terminal sessions with a persistent background daemon
- **Beautiful CLI** - Clean, informative terminal output with real-time status
- **Structured Logging** - Capture and manage stdout/stderr with intelligent buffering
- **Zero Config** - Works out of the box, customize when you need to
### ✨ Key Features
- **🧠 Smart Memory Management** - Tracks memory including child processes, enforces limits, and auto-restarts when exceeded
- **💾 Persistent Log Storage** - Keeps 10MB of logs in memory, persists to disk on restart/stop/error
- **🔄 Intelligent Auto-Restart** - Automatically restarts crashed processes with configurable policies
- **👀 File Watching** - Auto-restart on file changes for seamless development
- **🌳 Process Group Tracking** - Monitors parent and all child processes as a unit
- **🏗️ Daemon Architecture** - Survives terminal sessions with Unix socket IPC
- **📊 Beautiful CLI** - Clean, informative output with real-time status updates
- **📝 Structured Logging** - Captures stdout/stderr with timestamps and metadata
- **⚡ Zero Config** - Works out of the box, customize when needed
- **🔌 System Service** - Run as systemd service for production deployments
## 📦 Installation
```bash
# Install globally
# Install globally (recommended)
npm install -g @git.zone/tspm
# Or with pnpm (recommended)
# Or with pnpm
pnpm add -g @git.zone/tspm
# Or use in your project
# Or as a dev dependency
npm install --save-dev @git.zone/tspm
```
## 🚀 Quick Start
```bash
# Start the daemon (happens automatically on first use)
tspm daemon start
# Add a process (creates config without starting)
tspm add "node server.js" --name my-server --memory 1GB
# Start a process
tspm start server.js --name my-server
# Start the process
tspm start my-server
# Start with memory limit
tspm start app.js --memory 512MB --name my-app
# Start with file watching (great for development)
tspm start dev.js --watch --name dev-server
# Or add and start in one go
tspm add "node app.js" --name my-app
tspm start my-app
# List all processes
tspm list
# Check process details
tspm describe my-server
# View logs
tspm logs my-server --lines 100
tspm logs my-app
# Stop a process
tspm stop my-server
# Restart a process
tspm restart my-server
tspm stop my-app
```
## 📋 Command Reference
## 📋 Commands
### Process Management
#### `tspm start <script> [options]`
#### `tspm add <command> [options]`
Start a new process with automatic monitoring and management.
Add a new process configuration without starting it. This is the recommended way to register processes.
**Options:**
- `--name <name>` - Custom name for the process (default: script name)
- `--name <name>` - Custom name for the process (required)
- `--memory <size>` - Memory limit (e.g., "512MB", "2GB", default: 512MB)
- `--cwd <path>` - Working directory (default: current directory)
- `--watch` - Enable file watching for auto-restart
- `--watch-paths <paths>` - Comma-separated paths to watch (with --watch)
- `--watch-paths <paths>` - Comma-separated paths to watch
- `--autorestart` - Auto-restart on crash (default: true)
**Examples:**
```bash
# Add a simple Node.js app
tspm add "node server.js" --name api-server
# Add with 2GB memory limit
tspm add "node app.js" --name production-api --memory 2GB
# Add TypeScript app with watching
tspm add "tsx watch src/index.ts" --name dev-server --watch --watch-paths "src,config"
# Add without auto-restart
tspm add "node worker.js" --name one-time-job --autorestart false
```
#### `tspm start <id>`
Start a previously added process by its ID or name.
```bash
# Simple start
tspm start server.js
# Production setup with 2GB memory
tspm start app.js --name production-api --memory 2GB
# Development with watching
tspm start dev-server.js --watch --watch-paths "src,config" --name dev
# Custom working directory
tspm start ../other-project/index.js --cwd ../other-project --name other
tspm start my-server
tspm start 1 # Can also use numeric ID
```
#### `tspm stop <id>`
@@ -108,12 +111,22 @@ Stop and restart a process with the same configuration.
tspm restart my-server
```
#### `tspm delete <id>`
#### `tspm delete <id>` / `tspm remove <id>`
Stop and remove a process from TSPM management.
Stop and remove a process from TSPM management. Also deletes persisted logs.
```bash
tspm delete old-server
tspm remove old-server # Alias for delete
```
#### `tspm edit <id>`
Interactively edit a process configuration.
```bash
tspm edit my-server
# Opens interactive prompts to modify name, command, memory, etc.
```
### Monitoring & Information
@@ -126,12 +139,13 @@ Display all managed processes in a beautiful table.
tspm list
# Output:
┌─────────┬─────────────┬───────────┬───────────┬──────────┐
│ ID │ Name │ Status │ Memory │ Restarts │
├─────────┼─────────────┼───────────┼───────────┼──────────┤
my-app │ my-app │ online │ 245.3 MB 0
worker │ worker │ online │ 128.7 MB 2
└─────────┴─────────────┴───────────┴───────────┴──────────┘
┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────
│ ID │ Name │ Status │ PID │ Memory │ Restarts │
├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────
1 │ my-app │ online │ 45123 245.3 MB │ 0
2 │ worker │ online │ 45456 128.7 MB │ 2
3 │ api-server │ stopped │ - │ 0 B │ 5
└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘
```
#### `tspm describe <id>`
@@ -147,29 +161,35 @@ Process Details: my-server
Status: online
PID: 45123
Memory: 245.3 MB
CPU: 2.3%
Uptime: 3600s
Restarts: 0
Configuration:
Command: server.js
────────────────────────────────────────
Command: node server.js
Directory: /home/user/project
Memory Limit: 2 GB
Auto-restart: true
Watch: enabled
Watch Paths: src, config
Watch: disabled
```
#### `tspm logs <id> [options]`
View process logs (stdout and stderr).
View process logs (stdout and stderr combined).
**Options:**
- `--lines <n>` - Number of lines to display (default: 50)
- `--follow` - Stream logs in real-time (like `tail -f`)
```bash
# View last 50 lines
tspm logs my-server
# View last 100 lines
tspm logs my-server --lines 100
# Follow logs in real-time
tspm logs my-server --follow
```
### Batch Operations
@@ -180,6 +200,10 @@ Start all saved processes at once.
```bash
tspm start-all
# ✓ Started 3 processes:
# - my-app
# - worker
# - api-server
```
#### `tspm stop-all`
@@ -188,6 +212,7 @@ Stop all running processes.
```bash
tspm stop-all
# ✓ Stopped 3 processes
```
#### `tspm restart-all`
@@ -196,24 +221,49 @@ Restart all running processes.
```bash
tspm restart-all
# ✓ Restarted 3 processes
```
#### `tspm reset`
**⚠️ Dangerous:** Stop all processes and clear all configurations.
```bash
tspm reset
# Are you sure? (y/N)
# Stopped 3 processes.
# Cleared all configurations.
```
### Daemon Management
The TSPM daemon runs in the background and manages all your processes. It starts automatically when needed.
#### `tspm daemon start`
Start the TSPM daemon (happens automatically on first command).
Manually start the TSPM daemon (usually automatic).
```bash
tspm daemon start
# ✓ TSPM daemon started successfully
```
#### `tspm daemon stop`
Stop the TSPM daemon and all managed processes.
Stop the daemon and all managed processes.
```bash
tspm daemon stop
# ✓ TSPM daemon stopped successfully
```
#### `tspm daemon restart`
Restart the daemon (preserves running processes).
```bash
tspm daemon restart
# ✓ TSPM daemon restarted successfully
```
#### `tspm daemon status`
@@ -230,75 +280,175 @@ Status: running
PID: 12345
Uptime: 86400s
Processes: 5
Memory: 45.2 MB
CPU: 0.1%
Socket: /home/user/.tspm/tspm.sock
```
### System Service Management
Run TSPM as a system service (systemd) for production deployments.
#### `tspm enable`
Enable TSPM as a system service that starts on boot.
```bash
sudo tspm enable
# ✓ TSPM daemon enabled and started as system service
# The daemon will now start automatically on system boot
```
#### `tspm disable`
Disable the TSPM system service.
```bash
sudo tspm disable
# ✓ TSPM daemon service disabled
# The daemon will no longer start on system boot
```
## 🏗️ Architecture
TSPM uses a three-tier architecture for maximum reliability:
TSPM uses a robust three-tier architecture:
1. **ProcessWrapper** - Low-level process management with stream handling
2. **ProcessMonitor** - Adds monitoring, memory limits, and auto-restart logic
3. **Tspm Core** - High-level orchestration with configuration persistence
```
┌─────────────────────────────────────────┐
│ CLI Interface │
│ (tspm commands) │
└────────────────┬────────────────────────┘
│ Unix Socket IPC
┌────────────────▼────────────────────────┐
│ TSPM Daemon │
│ (Background Service) │
│ ┌──────────────────────────────────┐ │
│ │ ProcessManager │ │
│ │ - Configuration persistence │ │
│ │ - Process lifecycle │ │
│ │ - Desired state management │ │
│ └────────────┬─────────────────────┘ │
│ │ │
│ ┌────────────▼─────────────────────┐ │
│ │ ProcessMonitor │ │
│ │ - Memory tracking & limits │ │
│ │ - Auto-restart logic │ │
│ │ - Log persistence (10MB) │ │
│ │ - File watching │ │
│ └────────────┬─────────────────────┘ │
│ │ │
│ ┌────────────▼─────────────────────┐ │
│ │ ProcessWrapper │ │
│ │ - Process spawning │ │
│ │ - Stream handling │ │
│ │ - Signal management │ │
│ └──────────────────────────────────┘ │
└─────────────────────────────────────────┘
```
The daemon architecture ensures your processes keep running even after you close your terminal. All process communication happens through a robust IPC (Inter-Process Communication) system.
### Key Components
## 🎮 Programmatic Usage
- **CLI** - Lightweight client that communicates with daemon via IPC
- **Daemon** - Persistent background service managing all processes
- **ProcessManager** - High-level orchestration and configuration
- **ProcessMonitor** - Adds monitoring, limits, and auto-restart
- **ProcessWrapper** - Low-level process lifecycle and streams
TSPM can also be used as a library in your Node.js applications:
## 🎮 Programmatic API
Use TSPM as a library in your Node.js applications:
```typescript
import { Tspm } from '@git.zone/tspm';
import { TspmIpcClient } from '@git.zone/tspm/client';
const manager = new Tspm();
const client = new TspmIpcClient();
await client.connect();
// Start a process
const processId = await manager.start({
id: 'worker',
name: 'Background Worker',
// Add and start a process
const { id } = await client.request('add', {
command: 'node worker.js',
name: 'background-worker',
projectDir: process.cwd(),
memoryLimitBytes: 512 * 1024 * 1024, // 512MB
memoryLimit: 512 * 1024 * 1024, // 512MB in bytes
autorestart: true,
watch: false,
});
// Monitor process
const info = await manager.getProcessInfo(processId);
console.log(`Process ${info.id} is ${info.status}`);
await client.request('start', { id });
// Stop process
await manager.stop(processId);
// Get process info
const { processInfo } = await client.request('describe', { id });
console.log(`Worker status: ${processInfo.status}`);
console.log(`Memory usage: ${processInfo.memory} bytes`);
// Get logs
const { logs } = await client.request('logs', { id, limit: 100 });
logs.forEach(log => {
console.log(`[${log.timestamp}] ${log.message}`);
});
// Clean up
await client.request('stop', { id });
await client.disconnect();
```
## 🔧 Advanced Features
### Memory Limit Enforcement
### Memory Management
TSPM tracks memory usage including all child processes spawned by your application. When a process exceeds its memory limit, it's gracefully restarted.
TSPM tracks total memory usage including all child processes:
- Uses `ps-tree` to discover child processes
- Calculates combined memory usage
- Gracefully restarts when limit exceeded
- Prevents memory leaks in production
### Process Group Tracking
### Log Persistence
Using `ps-tree`, TSPM monitors not just your main process but all child processes it spawns, ensuring complete cleanup on stop/restart.
Intelligent log management system:
- Keeps 10MB of logs in memory per process
- Automatically flushes to disk on stop/restart/error
- Loads previous logs on process restart
- Cleans up persisted logs after loading
- Prevents disk space issues
### Intelligent Logging
### Process Groups
Logs are buffered and managed efficiently, preventing memory issues from excessive output while ensuring you don't lose important information.
Full process tree management:
- Tracks parent and all child processes
- Ensures complete cleanup on stop
- Accurate memory tracking across process trees
- No orphaned processes
### Graceful Shutdown
Processes receive SIGTERM first, allowing them to clean up. After a timeout, SIGKILL ensures termination.
Multi-stage shutdown process:
1. Send SIGTERM for graceful shutdown
2. Wait for process to clean up (5 seconds)
3. Send SIGKILL if still running
4. Clean up all child processes
### Configuration Persistence
### File Watching
Process configurations are saved, allowing you to restart all processes after a system reboot with a single command.
Development-friendly auto-restart:
- Watch specific directories or files
- Ignore `node_modules` by default
- Debounced restart on changes
- Configurable watch paths
## 📊 Performance
TSPM is designed for production efficiency:
- **CPU Usage**: < 0.5% overhead per managed process
- **Memory**: ~30-50MB for daemon, ~5-10MB per managed process
- **Startup Time**: < 100ms to spawn new process
- **IPC Latency**: < 1ms for command execution
- **Log Performance**: Efficient ring buffer with automatic trimming
## 🛠️ Development
```bash
# Clone the repository
git clone https://code.foss.global/git.zone/tspm.git
cd tspm
# Install dependencies
pnpm install
@@ -309,42 +459,71 @@ pnpm test
# Build the project
pnpm build
# Start development
# Run in development
pnpm start
```
### Project Structure
```
tspm/
├── ts/
│ ├── cli/ # CLI commands and interface
│ ├── client/ # IPC client for daemon communication
│ ├── daemon/ # Daemon server and process management
│ └── shared/ # Shared types and protocols
├── test/ # Test files
└── dist_ts/ # Compiled JavaScript
```
## 🐛 Debugging
Enable debug mode for verbose logging:
Enable verbose logging for troubleshooting:
```bash
# Enable debug mode
export TSPM_DEBUG=true
tspm list
# Check daemon logs
tail -f /tmp/daemon-stderr.log
# Force daemon restart
tspm daemon restart
```
## 📊 Performance
Common issues:
TSPM is designed to be lightweight and efficient:
- **"Daemon not running"**: Run `tspm daemon start` or `tspm enable`
- **"Permission denied"**: Check socket permissions in `~/.tspm/`
- **"Process won't start"**: Check logs with `tspm logs <id>`
- **"Memory limit exceeded"**: Increase limit with `tspm edit <id>`
- Minimal CPU overhead (typically < 0.5%)
- Small memory footprint (~30-50MB for the daemon)
- Fast process startup and shutdown
- Efficient log buffering and rotation
## 🤝 Why Choose TSPM?
## 🤝 Why TSPM?
### TSPM vs PM2
Unlike general-purpose process managers, TSPM is built specifically for the TypeScript/Node.js ecosystem:
| Feature | TSPM | PM2 |
|---------|------|-----|
| TypeScript Native | Built in TS | JavaScript |
| Memory Tracking | Including children | Main process only |
| Log Management | Smart 10MB buffer | Can grow unlimited |
| Architecture | Clean 3-tier | Monolithic |
| Dependencies | Minimal | Heavy |
| ESM Support | Native | Partial |
| Config Format | Simple JSON | Complex ecosystem |
- **TypeScript First** - Written in TypeScript, for TypeScript projects
- **ESM Native** - Full support for ES modules
- **Developer Friendly** - Beautiful CLI output and helpful error messages
- **Production Ready** - Battle-tested memory management and error handling
- **No Configuration Required** - Sensible defaults that just work
- **Modern Architecture** - Async/await throughout, no callback hell
### Perfect For
- 🚀 **Production Node.js apps** - Reliable process management
- 🔧 **Microservices** - Manage multiple services easily
- 👨💻 **Development** - File watching and auto-restart
- 🏭 **Worker processes** - Queue workers, cron jobs
- 📊 **Resource-constrained environments** - Memory limits prevent OOM
## License and Legal Information
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
@@ -359,4 +538,4 @@ Registered at District court Bremen HRB 35230 HB, Germany
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

View File

@@ -3,6 +3,6 @@
*/
export const commitinfo = {
name: '@git.zone/tspm',
version: '4.0.0',
version: '5.2.0',
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 type { CliArguments } from '../../types.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 type { CliArguments } from '../../types.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 type { CliArguments } from '../../types.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 { tspmIpcClient } from '../../../client/tspm.ipcclient.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(
paths.packageDir,
'dist_ts',
'daemon.js',
'daemon',
'tspm.daemon.js',
);
// Start daemon as a detached background process
@@ -80,6 +81,48 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
}
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':
// This is called by systemd - start the daemon directly
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('\nCommands:');
console.log(' start Start the TSPM daemon');
console.log(' restart Restart the TSPM daemon');
console.log(' stop Stop the TSPM daemon');
console.log(' status Show daemon status');
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 { tspmIpcClient } from '../../client/tspm.ipcclient.js';
import { Logger } from '../../shared/common/utils.errorhandler.js';
@@ -74,7 +74,7 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
const resetColor = '\x1b[0m';
console.log(
`${pad(proc.id, 7)}${pad(proc.id, 11)}${statusColor}${pad(proc.status, 9)}${resetColor}${pad(formatMemory(proc.memory), 9)}${pad(proc.restarts.toString(), 8)}`,
`${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 type { CliArguments } from '../../types.js';
import { parseMemoryString, formatMemory } from '../../helpers/memory.js';
@@ -69,6 +69,33 @@ export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
if (watchPaths) console.log(` Watch paths: ${watchPaths.join(',')}`);
}
// Capture essential environment variables from the CLI environment
// so processes have access to the same environment they were added with
const essentialEnvVars: NodeJS.ProcessEnv = {
PATH: process.env.PATH || '',
HOME: process.env.HOME,
USER: process.env.USER,
SHELL: process.env.SHELL,
LANG: process.env.LANG,
LC_ALL: process.env.LC_ALL,
// Node.js specific
NODE_ENV: process.env.NODE_ENV,
NODE_PATH: process.env.NODE_PATH,
// npm/pnpm/yarn paths
npm_config_prefix: process.env.npm_config_prefix,
// Include any TSPM_ prefixed vars
...Object.fromEntries(
Object.entries(process.env).filter(([key]) => key.startsWith('TSPM_'))
),
};
// Remove undefined values
Object.keys(essentialEnvVars).forEach(key => {
if (essentialEnvVars[key] === undefined) {
delete essentialEnvVars[key];
}
});
const response = await tspmIpcClient.request('add', {
config: {
name,
@@ -76,6 +103,7 @@ export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
args: cmdArgs,
projectDir,
memoryLimitBytes: memoryLimit,
env: essentialEnvVars,
autorestart,
watch,
watchPaths,
@@ -88,4 +116,3 @@ export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
{ 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 type { CliArguments } from '../../types.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 type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';

View File

@@ -0,0 +1,76 @@
import * as plugins from '../../plugins.js';
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
import { formatMemory, parseMemoryString } from '../../helpers/memory.js';
export function registerEditCommand(smartcli: plugins.smartcli.Smartcli) {
registerIpcCommand(
smartcli,
'edit',
async (argvArg: CliArguments) => {
const idRaw = argvArg._[1];
if (!idRaw) {
console.error('Error: Please provide a process ID to edit');
console.log('Usage: tspm edit <id>');
return;
}
const id = idRaw;
// Load current config
const { config } = await tspmIpcClient.request('describe', { id });
// Interactive editing is temporarily disabled - needs smartinteract API update
console.log('Interactive editing is temporarily disabled.');
console.log('Current configuration:');
console.log(` Name: ${config.name}`);
console.log(` Command: ${config.command}`);
console.log(` Directory: ${config.projectDir}`);
console.log(` Memory: ${formatMemory(config.memoryLimitBytes)}`);
console.log(` Auto-restart: ${config.autorestart}`);
console.log(` Watch: ${config.watch ? 'enabled' : 'disabled'}`);
// For now, just update environment variables to current
const essentialEnvVars: NodeJS.ProcessEnv = {
PATH: process.env.PATH || '',
HOME: process.env.HOME,
USER: process.env.USER,
SHELL: process.env.SHELL,
LANG: process.env.LANG,
LC_ALL: process.env.LC_ALL,
// Node.js specific
NODE_ENV: process.env.NODE_ENV,
NODE_PATH: process.env.NODE_PATH,
// npm/pnpm/yarn paths
npm_config_prefix: process.env.npm_config_prefix,
// Include any TSPM_ prefixed vars
...Object.fromEntries(
Object.entries(process.env).filter(([key]) => key.startsWith('TSPM_'))
),
};
// Remove undefined values
Object.keys(essentialEnvVars).forEach(key => {
if (essentialEnvVars[key] === undefined) {
delete essentialEnvVars[key];
}
});
// Update environment variables
const updates = {
env: { ...(config.env || {}), ...essentialEnvVars }
};
const updateResponse = await tspmIpcClient.request('update', {
id,
updates,
});
console.log('✓ Environment variables updated');
console.log(' Process configuration updated successfully');
},
{ actionLabel: 'edit 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 type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
@@ -39,7 +39,7 @@ export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
const resetColor = '\x1b[0m';
console.log(
`${pad(proc.id, 7)}${pad(proc.id, 11)}${statusColor}${pad(proc.status, 9)}${resetColor}${pad((proc.pid || '-').toString(), 9)}${pad(formatMemory(proc.memory), 8)}${pad(proc.restarts.toString(), 8)}`,
`${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 type { CliArguments } from '../../types.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 { toProcessId } from '../../../shared/protocol/id.js';
import type { CliArguments } from '../../types.js';
import { registerIpcCommand } from '../../registration/index.js';
@@ -8,15 +9,33 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
smartcli,
'restart',
async (argvArg: CliArguments) => {
const id = argvArg._[1];
if (!id) {
console.error('Error: Please provide a process ID');
console.log('Usage: tspm restart <id>');
const arg = argvArg._[1];
if (!arg) {
console.error('Error: Please provide a process ID or "all"');
console.log('Usage:');
console.log(' tspm restart <id>');
console.log(' tspm restart all');
return;
}
if (String(arg).toLowerCase() === 'all') {
console.log('Restarting all processes...');
const res = await tspmIpcClient.request('restartAll', {});
if (res.restarted.length > 0) {
console.log(`✓ Restarted ${res.restarted.length} processes:`);
for (const id of res.restarted) console.log(` - ${id}`);
}
if (res.failed.length > 0) {
console.log(`✗ Failed to restart ${res.failed.length} processes:`);
for (const f of res.failed) console.log(` - ${f.id}: ${f.error}`);
process.exitCode = 1;
}
return;
}
const id = String(arg);
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(` 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 type { IProcessConfig } from '../../../shared/protocol/ipc.types.js';
import type { CliArguments } from '../../types.js';
@@ -17,14 +17,8 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
return;
}
const desc = await tspmIpcClient.request('describe', { id }).catch(() => null);
if (!desc) {
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(`Starting process id ${id}...`);
const response = await tspmIpcClient.request('startById', { id });
console.log('✓ Process started');
console.log(` ID: ${response.processId}`);
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 type { CliArguments } from '../../types.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 { Logger } from '../../../shared/common/utils.errorhandler.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 { Logger } from '../../../shared/common/utils.errorhandler.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 { Logger, LogLevel } from '../shared/common/utils.errorhandler.js';
@@ -12,12 +13,14 @@ import { registerDeleteCommand } from './commands/process/delete.js';
import { registerListCommand } from './commands/process/list.js';
import { registerDescribeCommand } from './commands/process/describe.js';
import { registerLogsCommand } from './commands/process/logs.js';
import { registerEditCommand } from './commands/process/edit.js';
import { registerStartAllCommand } from './commands/batch/start-all.js';
import { registerStopAllCommand } from './commands/batch/stop-all.js';
import { registerRestartAllCommand } from './commands/batch/restart-all.js';
import { registerDaemonCommand } from './commands/daemon/index.js';
import { registerEnableCommand } from './commands/service/enable.js';
import { registerDisableCommand } from './commands/service/disable.js';
import { registerResetCommand } from './commands/reset.js';
// Export types for external use
export type { CliArguments } from './types.js';
@@ -37,6 +40,24 @@ export const run = async (): Promise<void> => {
}
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);
// Register all commands
@@ -52,6 +73,7 @@ export const run = async (): Promise<void> => {
registerListCommand(smartcliInstance);
registerDescribeCommand(smartcliInstance);
registerLogsCommand(smartcliInstance);
registerEditCommand(smartcliInstance);
// Batch commands
registerStartAllCommand(smartcliInstance);
@@ -65,6 +87,9 @@ export const run = async (): Promise<void> => {
registerEnableCommand(smartcliInstance);
registerDisableCommand(smartcliInstance);
// Maintenance commands
registerResetCommand(smartcliInstance);
// Start parsing commands
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 {
CliArguments,
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 { toProcessId } from '../shared/protocol/id.js';
import type { ProcessId } from '../shared/protocol/id.js';
import type {
IpcMethodMap,
@@ -144,26 +146,28 @@ export class TspmIpcClient {
* Subscribe to log updates for a specific process
*/
public async subscribe(
processId: string,
processId: ProcessId | number | string,
handler: (log: any) => void,
): Promise<void> {
if (!this.ipcClient || !this.isConnected) {
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);
}
/**
* 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) {
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}`);
}

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 * as paths from '../paths.js';
import { ProcessMonitor } from './processmonitor.js';
import { LogPersistence } from './logpersistence.js';
import { TspmConfig } from './tspm.config.js';
import {
Logger,
@@ -16,15 +17,20 @@ import type {
IProcessLog,
IMonitorConfig
} 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 {
public processes: Map<string, ProcessMonitor> = new Map();
public processConfigs: Map<string, IProcessConfig> = new Map();
public processInfo: Map<string, IProcessInfo> = new Map();
public processes: Map<ProcessId, ProcessMonitor> = new Map();
public processConfigs: Map<ProcessId, IProcessConfig> = new Map();
public processInfo: Map<ProcessId, IProcessInfo> = new Map();
private processLogs: Map<ProcessId, IProcessLog[]> = new Map();
private config: TspmConfig;
private configStorageKey = 'processes';
private desiredStateStorageKey = 'desiredStates';
private desiredStates: Map<ProcessId, IProcessInfo['status']> = new Map();
private logger: Logger;
constructor() {
@@ -32,18 +38,19 @@ export class ProcessManager extends EventEmitter {
this.logger = new Logger('Tspm');
this.config = new TspmConfig();
this.loadProcessConfigs();
this.loadDesiredStates();
}
/**
* 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
const nextId = this.getNextSequentialId();
const config: IProcessConfig = {
id: String(nextId),
id: nextId,
name: configInput.name || `process-${nextId}`,
command: configInput.command,
args: configInput.args,
@@ -67,6 +74,7 @@ export class ProcessManager extends EventEmitter {
});
await this.saveProcessConfigs();
await this.setDesiredState(config.id, 'stopped');
return config.id;
}
@@ -107,7 +115,8 @@ export class ProcessManager extends EventEmitter {
// Create and start process monitor
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,
command: config.command,
args: config.args,
@@ -121,13 +130,43 @@ export class ProcessManager extends EventEmitter {
// Set up log event handler to re-emit for pub/sub
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 });
});
// Set up event handler to track PID when process starts
monitor.on('start', (pid: number) => {
this.updateProcessInfo(config.id, { pid });
});
// Set up event handler to clear PID when process exits
monitor.on('exit', () => {
this.updateProcessInfo(config.id, { pid: undefined });
});
monitor.start();
await monitor.start();
// Update process info
this.updateProcessInfo(config.id, { status: 'online' });
// 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
await this.saveProcessConfigs();
@@ -158,10 +197,36 @@ export class ProcessManager extends EventEmitter {
}
}
/**
* Update an existing process configuration
*/
public async update(
id: ProcessId,
updates: Partial<Omit<IProcessConfig, 'id'>>,
): Promise<IProcessConfig> {
const existing = this.processConfigs.get(id);
if (!existing) {
throw new ValidationError(
`Process with id '${id}' does not exist`,
'ERR_PROCESS_NOT_FOUND',
);
}
// Shallow merge; keep id intact
const merged: IProcessConfig = {
...existing,
...updates,
} as IProcessConfig;
this.processConfigs.set(id, merged);
await this.saveProcessConfigs();
return merged;
}
/**
* 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}'`);
const monitor = this.processes.get(id);
@@ -175,7 +240,7 @@ export class ProcessManager extends EventEmitter {
}
try {
monitor.stop();
await monitor.stop();
this.updateProcessInfo(id, { status: 'stopped' });
this.logger.info(`Successfully stopped process with id '${id}'`);
} catch (error: Error | unknown) {
@@ -195,7 +260,7 @@ export class ProcessManager extends EventEmitter {
/**
* 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}'`);
const monitor = this.processes.get(id);
@@ -212,11 +277,12 @@ export class ProcessManager extends EventEmitter {
try {
// Stop and then start the process
monitor.stop();
await monitor.stop();
// Create a new monitor instance
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,
command: config.command,
args: config.args,
@@ -226,14 +292,37 @@ export class ProcessManager extends EventEmitter {
logBufferSize: config.logBufferSize,
});
// Set up log event handler for the new monitor
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);
// 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);
newMonitor.start();
await newMonitor.start();
// Update restart count
// 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);
if (info) {
const pid = newMonitor.getPid();
this.updateProcessInfo(id, {
status: 'online',
pid: pid || undefined,
restarts: info.restarts + 1,
});
}
@@ -253,7 +342,7 @@ export class ProcessManager extends EventEmitter {
/**
* 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}'`);
// Check if process exists
@@ -276,9 +365,15 @@ export class ProcessManager extends EventEmitter {
this.processes.delete(id);
this.processConfigs.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
await this.saveProcessConfigs();
await this.removeDesiredState(id);
this.logger.info(`Successfully deleted process with id '${id}'`);
} catch (error: Error | unknown) {
@@ -287,7 +382,14 @@ export class ProcessManager extends EventEmitter {
this.processes.delete(id);
this.processConfigs.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.removeDesiredState(id);
this.logger.info(
`Successfully deleted process with id '${id}' after stopping failure`,
@@ -308,14 +410,42 @@ export class ProcessManager extends EventEmitter {
* Get a list of all process infos
*/
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
*/
public describe(
id: string,
id: ProcessId,
): { config: IProcessConfig; info: IProcessInfo } | null {
const config = this.processConfigs.get(id);
const info = this.processInfo.get(id);
@@ -330,13 +460,21 @@ export class ProcessManager extends EventEmitter {
/**
* 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);
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 +509,7 @@ export class ProcessManager extends EventEmitter {
/**
* 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);
if (info) {
this.processInfo.set(id, { ...info, ...update });
@@ -381,15 +519,40 @@ export class ProcessManager extends EventEmitter {
/**
* Compute next sequential numeric id based on existing configs
*/
private getNextSequentialId(): number {
let maxId = 0;
for (const id of this.processConfigs.keys()) {
const n = parseInt(id, 10);
if (!isNaN(n)) {
maxId = Math.max(maxId, n);
/**
* Sync process stats from monitors to processInfo
*/
public syncProcessStats(): void {
for (const [id, monitor] of this.processes.entries()) {
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 +578,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
*/
@@ -425,23 +664,35 @@ export class ProcessManager extends EventEmitter {
const configsJson = await this.config.readKey(this.configStorageKey);
if (configsJson) {
try {
const configs = JSON.parse(configsJson) as IProcessConfig[];
this.logger.debug(`Loaded ${configs.length} process configurations`);
const parsed = JSON.parse(configsJson) as Array<any>;
this.logger.debug(`Loaded ${parsed.length} process configurations`);
for (const config of configs) {
// Validate config
if (!config.id || !config.command || !config.projectDir) {
for (const raw of parsed) {
// Convert legacy string IDs to ProcessId
let id: ProcessId;
try {
id = toProcessId(raw.id);
} catch {
this.logger.warn(
`Skipping invalid process config for id '${config.id || 'unknown'}'`,
`Skipping invalid process config with non-numeric id '${raw.id || 'unknown'}'`,
);
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
this.processInfo.set(config.id, {
id: config.id,
this.processInfo.set(id, {
id: id,
status: 'stopped',
memory: 0,
restarts: 0,
@@ -470,4 +721,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 { EventEmitter } from 'events';
import { ProcessWrapper } from './processwrapper.js';
import { LogPersistence } from './logpersistence.js';
import { Logger, ProcessError, handleError } from '../shared/common/utils.errorhandler.js';
import type { IMonitorConfig, IProcessLog } from '../shared/protocol/ipc.types.js';
import type { ProcessId } from '../shared/protocol/id.js';
export class ProcessMonitor extends EventEmitter {
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 restartCount: number = 0;
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();
this.config = config;
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.
this.stopped = false;
this.log(`Starting process monitor.`);
@@ -57,6 +81,22 @@ export class ProcessMonitor extends EventEmitter {
// Set up event handlers
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
this.emit('log', log);
@@ -65,13 +105,31 @@ export class ProcessMonitor extends EventEmitter {
this.log(log.message);
}
});
// Re-emit start event with PID for upstream handlers
this.processWrapper.on('start', (pid: number): void => {
this.emit('start', pid);
});
this.processWrapper.on(
'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}.`;
this.logger.info(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) {
this.logger.info('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 =
error instanceof ProcessError
? `Process error: ${error.toString()}`
@@ -95,6 +153,16 @@ export class ProcessMonitor extends EventEmitter {
this.logger.error(error);
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) {
this.logger.info('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.
*/
public stop(): void {
public async stop(): Promise<void> {
this.log('Stopping process monitor.');
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) {
clearInterval(this.intervalId);
}
@@ -254,10 +333,12 @@ export class ProcessMonitor extends EventEmitter {
* Get the current logs from the process
*/
public getLogs(limit?: number): IProcessLog[] {
if (!this.processWrapper) {
return [];
console.error(`[ProcessMonitor:${this.config.name}] getLogs called, logs.length=${this.logs.length}, limit=${limit}`);
this.logger.debug(`Getting logs, total stored: ${this.logs.length}`);
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 nextSeq: number = 0;
private runId: string = '';
private stdoutRemainder: string = '';
private stderrRemainder: string = '';
constructor(options: IProcessWrapperOptions) {
super();
@@ -45,7 +47,7 @@ export class ProcessWrapper extends EventEmitter {
this.options.args,
{
cwd: this.options.cwd,
env: this.options.env || process.env,
env: { ...process.env, ...(this.options.env || {}) },
stdio: ['ignore', 'pipe', 'pipe'], // We need to pipe stdout and stderr
},
);
@@ -53,7 +55,7 @@ export class ProcessWrapper extends EventEmitter {
// Use shell mode to allow a full command string
this.process = plugins.childProcess.spawn(this.options.command, {
cwd: this.options.cwd,
env: this.options.env || process.env,
env: { ...process.env, ...(this.options.env || {}) },
stdio: ['ignore', 'pipe', 'pipe'], // We need to pipe stdout and stderr
shell: true,
});
@@ -66,6 +68,11 @@ export class ProcessWrapper extends EventEmitter {
const exitMessage = `Process exited with code ${code}, signal ${signal}`;
this.logger.info(exitMessage);
this.addSystemLog(exitMessage);
// Clear remainder buffers on exit
this.stdoutRemainder = '';
this.stderrRemainder = '';
this.emit('exit', code, signal);
});
@@ -83,24 +90,57 @@ export class ProcessWrapper extends EventEmitter {
// Capture stdout
if (this.process.stdout) {
console.error(`[ProcessWrapper] Setting up stdout listener for process ${this.process.pid}`);
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) {
if (line.trim()) {
this.addLog('stdout', line);
}
console.error(`[ProcessWrapper] Processing stdout line: ${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
if (this.process.stderr) {
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) {
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 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 type {
IpcMethodMap,
@@ -20,12 +22,20 @@ export class TspmDaemon {
private socketPath: string;
private heartbeatInterval: NodeJS.Timeout | null = null;
private daemonPidFile: string;
private version: string;
constructor() {
this.tspmInstance = new ProcessManager();
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
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
await this.tspmInstance.loadProcessConfigs();
await this.tspmInstance.loadDesiredStates();
// Set up log publishing
this.tspmInstance.on('process:log', ({ processId, log }) => {
@@ -95,6 +106,9 @@ export class TspmDaemon {
// Set up graceful shutdown handlers
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(`PID: ${process.pid}`);
}
@@ -108,6 +122,7 @@ export class TspmDaemon {
'start',
async (request: RequestForMethod<'start'>) => {
try {
await this.tspmInstance.setDesiredState(request.config.id, 'online');
await this.tspmInstance.start(request.config);
const processInfo = this.tspmInstance.processInfo.get(
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(
'stop',
async (request: RequestForMethod<'stop'>) => {
try {
await this.tspmInstance.stop(request.id);
const id = toProcessId(request.id);
await this.tspmInstance.setDesiredState(id, 'stopped');
await this.tspmInstance.stop(id);
return {
success: true,
message: `Process ${request.id} stopped successfully`,
message: `Process ${id} stopped successfully`,
};
} catch (error) {
throw new Error(`Failed to stop process: ${error.message}`);
@@ -142,10 +188,12 @@ export class TspmDaemon {
'restart',
async (request: RequestForMethod<'restart'>) => {
try {
await this.tspmInstance.restart(request.id);
const processInfo = this.tspmInstance.processInfo.get(request.id);
const id = toProcessId(request.id);
await this.tspmInstance.setDesiredState(id, 'online');
await this.tspmInstance.restart(id);
const processInfo = this.tspmInstance.processInfo.get(id);
return {
processId: request.id,
processId: id,
pid: processInfo?.pid,
status: processInfo?.status || 'stopped',
};
@@ -159,10 +207,11 @@ export class TspmDaemon {
'delete',
async (request: RequestForMethod<'delete'>) => {
try {
await this.tspmInstance.delete(request.id);
const id = toProcessId(request.id);
await this.tspmInstance.delete(id);
return {
success: true,
message: `Process ${request.id} deleted successfully`,
message: `Process ${id} deleted successfully`,
};
} catch (error) {
throw new Error(`Failed to delete process: ${error.message}`);
@@ -184,12 +233,26 @@ export class TspmDaemon {
},
);
this.ipcServer.onMessage(
'update',
async (request: RequestForMethod<'update'>) => {
try {
const id = toProcessId(request.id);
const updated = await this.tspmInstance.update(id, request.updates as any);
return { id, config: updated };
} catch (error) {
throw new Error(`Failed to update process: ${error.message}`);
}
},
);
this.ipcServer.onMessage(
'remove',
async (request: RequestForMethod<'remove'>) => {
try {
await this.tspmInstance.delete(request.id);
return { success: true, message: `Process ${request.id} deleted successfully` };
const id = toProcessId(request.id);
await this.tspmInstance.delete(id);
return { success: true, message: `Process ${id} deleted successfully` };
} catch (error) {
throw new Error(`Failed to remove process: ${error.message}`);
}
@@ -207,16 +270,15 @@ export class TspmDaemon {
this.ipcServer.onMessage(
'describe',
async (request: RequestForMethod<'describe'>) => {
const processInfo = await this.tspmInstance.describe(request.id);
const config = this.tspmInstance.processConfigs.get(request.id);
if (!processInfo || !config) {
throw new Error(`Process ${request.id} not found`);
const id = toProcessId(request.id);
const result = await this.tspmInstance.describe(id);
if (!result) {
throw new Error(`Process ${id} not found`);
}
// Return correctly shaped response
return {
processInfo,
config,
processInfo: result.info,
config: result.config,
};
},
);
@@ -224,7 +286,7 @@ export class TspmDaemon {
this.ipcServer.onMessage(
'getLogs',
async (request: RequestForMethod<'getLogs'>) => {
const logs = await this.tspmInstance.getLogs(request.id);
const logs = await this.tspmInstance.getLogs(toProcessId(request.id));
return { logs };
},
);
@@ -233,9 +295,10 @@ export class TspmDaemon {
this.ipcServer.onMessage(
'startAll',
async (request: RequestForMethod<'startAll'>) => {
const started: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
const started: ProcessId[] = [];
const failed: Array<{ id: ProcessId; error: string }> = [];
await this.tspmInstance.setDesiredStateForAll('online');
await this.tspmInstance.startAll();
// Get status of all processes
@@ -254,9 +317,10 @@ export class TspmDaemon {
this.ipcServer.onMessage(
'stopAll',
async (request: RequestForMethod<'stopAll'>) => {
const stopped: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
const stopped: ProcessId[] = [];
const failed: Array<{ id: ProcessId; error: string }> = [];
await this.tspmInstance.setDesiredStateForAll('stopped');
await this.tspmInstance.stopAll();
// Get status of all processes
@@ -275,8 +339,8 @@ export class TspmDaemon {
this.ipcServer.onMessage(
'restartAll',
async (request: RequestForMethod<'restartAll'>) => {
const restarted: string[] = [];
const failed: Array<{ id: string; error: string }> = [];
const restarted: ProcessId[] = [];
const failed: Array<{ id: ProcessId; error: string }> = [];
await this.tspmInstance.restartAll();
@@ -293,6 +357,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
this.ipcServer.onMessage(
'daemon:status',
@@ -305,6 +378,7 @@ export class TspmDaemon {
processCount: this.tspmInstance.processes.size,
memoryUsage: memUsage.heapUsed,
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
version: this.version,
};
},
);
@@ -497,3 +571,11 @@ export const startDaemon = async (): Promise<void> => {
// Keep the process alive
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 smartcli from '@push.rocks/smartcli';
import * as smartdaemon from '@push.rocks/smartdaemon';
import * as smartfile from '@push.rocks/smartfile';
import * as smartipc from '@push.rocks/smartipc';
import * as smartpath from '@push.rocks/smartpath';
import * as smartinteract from '@push.rocks/smartinteract';
// Export with explicit module types
export { npmextra, projectinfo, smartcli, smartdaemon, smartipc, smartpath };
export { npmextra, projectinfo, smartcli, smartdaemon, smartfile, smartipc, smartpath, smartinteract };
// third-party scope
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)
export interface IMonitorConfig {
name?: string; // Optional name to identify the instance
@@ -11,14 +13,14 @@ export interface 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
watch?: boolean; // Whether to watch for file changes and restart
watchPaths?: string[]; // Paths to watch for changes
}
export interface IProcessInfo {
id: string;
id: ProcessId;
pid?: number;
status: 'online' | 'stopped' | 'errored';
memory: number;
@@ -61,14 +63,25 @@ export interface StartRequest {
}
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;
status: 'online' | 'stopped' | 'errored';
}
// Stop command
export interface StopRequest {
id: string;
id: ProcessId;
}
export interface StopResponse {
@@ -78,18 +91,18 @@ export interface StopResponse {
// Restart command
export interface RestartRequest {
id: string;
id: ProcessId;
}
export interface RestartResponse {
processId: string;
processId: ProcessId;
pid?: number;
status: 'online' | 'stopped' | 'errored';
}
// Delete command
export interface DeleteRequest {
id: string;
id: ProcessId;
}
export interface DeleteResponse {
@@ -108,7 +121,7 @@ export interface ListResponse {
// Describe command
export interface DescribeRequest {
id: string;
id: ProcessId;
}
export interface DescribeResponse {
@@ -118,7 +131,7 @@ export interface DescribeResponse {
// Get logs command
export interface GetLogsRequest {
id: string;
id: ProcessId;
lines?: number;
}
@@ -132,9 +145,9 @@ export interface StartAllRequest {
}
export interface StartAllResponse {
started: string[];
started: ProcessId[];
failed: Array<{
id: string;
id: ProcessId;
error: string;
}>;
}
@@ -145,9 +158,9 @@ export interface StopAllRequest {
}
export interface StopAllResponse {
stopped: string[];
stopped: ProcessId[];
failed: Array<{
id: string;
id: ProcessId;
error: string;
}>;
}
@@ -158,9 +171,23 @@ export interface RestartAllRequest {
}
export interface RestartAllResponse {
restarted: string[];
restarted: ProcessId[];
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;
}>;
}
@@ -177,6 +204,7 @@ export interface DaemonStatusResponse {
processCount: number;
memoryUsage?: number;
cpuUsage?: number;
version?: string;
}
// Daemon shutdown command
@@ -203,17 +231,17 @@ export interface HeartbeatResponse {
// Add (register config without starting)
export interface AddRequest {
// 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 {
id: string;
id: ProcessId;
config: IProcessConfig;
}
// Remove (delete config and stop if running)
export interface RemoveRequest {
id: string;
id: ProcessId;
}
export interface RemoveResponse {
@@ -221,13 +249,26 @@ export interface RemoveResponse {
message?: string;
}
// Update (modify existing config)
export interface UpdateRequest {
id: ProcessId;
updates: Partial<Omit<IProcessConfig, 'id'>>;
}
export interface UpdateResponse {
id: ProcessId;
config: IProcessConfig;
}
// Type mappings for methods
export type IpcMethodMap = {
start: { request: StartRequest; response: StartResponse };
startById: { request: StartByIdRequest; response: StartByIdResponse };
stop: { request: StopRequest; response: StopResponse };
restart: { request: RestartRequest; response: RestartResponse };
delete: { request: DeleteRequest; response: DeleteResponse };
add: { request: AddRequest; response: AddResponse };
update: { request: UpdateRequest; response: UpdateResponse };
remove: { request: RemoveRequest; response: RemoveResponse };
list: { request: ListRequest; response: ListResponse };
describe: { request: DescribeRequest; response: DescribeResponse };
@@ -235,6 +276,7 @@ export type IpcMethodMap = {
startAll: { request: StartAllRequest; response: StartAllResponse };
stopAll: { request: StopAllRequest; response: StopAllResponse };
restartAll: { request: RestartAllRequest; response: RestartAllResponse };
reset: { request: ResetRequest; response: ResetResponse };
'daemon:status': {
request: DaemonStatusRequest;
response: DaemonStatusResponse;