Compare commits
30 Commits
Author | SHA1 | Date | |
---|---|---|---|
e09cf38f30 | |||
c694672438 | |||
3b21a338fb | |||
28680309ad | |||
833573eb10 | |||
ebc20a9232 | |||
22a43204d4 | |||
699d07ea36 | |||
2b57251f47 | |||
311a536fae | |||
5036f01516 | |||
538f282b62 | |||
e507b75c40 | |||
97a8377a75 | |||
3676bff04c | |||
dfe0677cab | |||
611b756670 | |||
2291348774 | |||
504725043d | |||
e16a3fb845 | |||
c3d12b287c | |||
cbea3f6187 | |||
51aa6eddad | |||
5910724b3c | |||
a67d247e9c | |||
f7bc56e676 | |||
7bfda01768 | |||
27384d03c7 | |||
47afd4739a | |||
4db128edaf |
0
.tspm_home/.npmextra/kv/@git.zone__tspm.json
Normal file
0
.tspm_home/.npmextra/kv/@git.zone__tspm.json
Normal file
131
changelog.md
131
changelog.md
@@ -1,5 +1,136 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.3.2 - fix(daemon)
|
||||||
|
Improve daemon log delivery and process monitor memory accounting; gate debug output and update tests to numeric ProcessId
|
||||||
|
|
||||||
|
- Deliver process logs only to subscribed clients instead of broadcasting to all connections (reduce unnecessary IPC traffic and noise)
|
||||||
|
- Implement incremental log memory accounting in ProcessMonitor using an estimateLogSize helper and WeakMap to avoid repeated JSON.stringify and reduce CPU/memory overhead
|
||||||
|
- Seed the incremental size map when loading persisted logs so memory accounting is accurate after restart
|
||||||
|
- Trim logs incrementally by subtracting estimated sizes of removed entries (avoids O(n) recalculation)
|
||||||
|
- Gate verbose console/debug output behind TSPM_DEBUG to prevent spamming in normal runs (applies to ProcessWrapper and ProcessMonitor)
|
||||||
|
- Improve process wrapper stdout/stderr debug logging to be conditional on debug mode
|
||||||
|
- Update tests to use numeric ProcessId via toProcessId(...) for consistency with typed IDs
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.3.1 - fix(client(tspmIpcClient))
|
||||||
|
Use bare topic names for IPC client subscribe/unsubscribe to fix log subscription issues
|
||||||
|
|
||||||
|
- Updated ts/client/tspm.ipcclient.ts to call ipcClient.subscribe/unsubscribe with the bare topic (e.g. 'logs.<id>') instead of prefixed 'topic:<...>'.
|
||||||
|
- Added comments clarifying that the IpcClient registers the 'topic:' prefix internally.
|
||||||
|
- Fixes incorrect topic registration that could prevent log streaming handlers from receiving messages.
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.3.0 - feat(cli/daemon/processmonitor)
|
||||||
|
Add flexible target resolution and search command; improve restart/backoff and error handling
|
||||||
|
|
||||||
|
- Add new cli command `search` to find processes by id or name fragment.
|
||||||
|
- Allow flexible process targets in CLI commands (accepts numeric id, id:<n>, or name:<label>) for start/stop/restart/delete/describe/logs/edit commands.
|
||||||
|
- Introduce a new daemon IPC method `resolveTarget` to normalize user-provided targets to ProcessId (supports id:<n>, name:<label>, or bare numeric id).
|
||||||
|
- Keep `remove` as a CLI alias but daemon exposes `delete` only; CLI resolves targets and always calls daemon `delete`.
|
||||||
|
- Implement scheduled restart/backoff in ProcessMonitor with incremental debounce, max retries, and a 1-hour reset window.
|
||||||
|
- Emit a `failed` event from ProcessMonitor when max restart attempts are exceeded; ProcessManager listens and marks processes as `errored` and clears pid.
|
||||||
|
- Ensure desired state is set to `stopped` before deleting a process to avoid race conditions.
|
||||||
|
- Improve cli output messages to include resolved names alongside numeric ids where available.
|
||||||
|
|
||||||
|
## 2025-08-30 - 5.2.0 - feat(cli)
|
||||||
|
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)
|
||||||
|
|
||||||
|
- Add a new CLI command `tspm add` that registers a process configuration without starting it; daemon assigns a sequential numeric ID and returns the stored config.
|
||||||
|
- Change `tspm start` to accept a process ID and start the saved configuration instead of accepting ad-hoc commands/files. This is a breaking change to the CLI contract.
|
||||||
|
- Add `remove` as an alias for the existing `delete` command; both CLI and daemon now support `remove` which stops and deletes the stored process.
|
||||||
|
- Daemon and IPC protocol updated to support `add` and `remove` methods; shared IPC types extended accordingly.
|
||||||
|
- ProcessManager: implemented add() and getNextSequentialId() to persist configs and produce numeric IDs.
|
||||||
|
- CLI registration updated (registerIpcCommand) to accept multiple command names, enabling aliases for commands.
|
||||||
|
|
||||||
## 2025-08-29 - 3.1.3 - fix(client)
|
## 2025-08-29 - 3.1.3 - fix(client)
|
||||||
Improve IPC client robustness and daemon debug logging; update tests and package metadata
|
Improve IPC client robustness and daemon debug logging; update tests and package metadata
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@git.zone/tspm",
|
"name": "@git.zone/tspm",
|
||||||
"version": "3.1.3",
|
"version": "5.3.2",
|
||||||
"private": false,
|
"private": false,
|
||||||
"description": "a no fuzz process manager",
|
"description": "a no fuzz process manager",
|
||||||
"main": "dist_ts/index.js",
|
"main": "dist_ts/index.js",
|
||||||
@@ -35,8 +35,10 @@
|
|||||||
"@push.rocks/npmextra": "^5.3.3",
|
"@push.rocks/npmextra": "^5.3.3",
|
||||||
"@push.rocks/projectinfo": "^5.0.2",
|
"@push.rocks/projectinfo": "^5.0.2",
|
||||||
"@push.rocks/smartcli": "^4.0.11",
|
"@push.rocks/smartcli": "^4.0.11",
|
||||||
"@push.rocks/smartdaemon": "^2.0.8",
|
"@push.rocks/smartdaemon": "^2.0.9",
|
||||||
"@push.rocks/smartipc": "^2.2.1",
|
"@push.rocks/smartfile": "^11.2.7",
|
||||||
|
"@push.rocks/smartinteract": "^2.0.16",
|
||||||
|
"@push.rocks/smartipc": "^2.2.2",
|
||||||
"@push.rocks/smartpath": "^6.0.0",
|
"@push.rocks/smartpath": "^6.0.0",
|
||||||
"pidusage": "^4.0.1",
|
"pidusage": "^4.0.1",
|
||||||
"ps-tree": "^1.2.0",
|
"ps-tree": "^1.2.0",
|
||||||
|
533
pnpm-lock.yaml
generated
533
pnpm-lock.yaml
generated
@@ -18,11 +18,17 @@ importers:
|
|||||||
specifier: ^4.0.11
|
specifier: ^4.0.11
|
||||||
version: 4.0.11
|
version: 4.0.11
|
||||||
'@push.rocks/smartdaemon':
|
'@push.rocks/smartdaemon':
|
||||||
specifier: ^2.0.8
|
specifier: ^2.0.9
|
||||||
version: 2.0.8
|
version: 2.0.9
|
||||||
|
'@push.rocks/smartfile':
|
||||||
|
specifier: ^11.2.7
|
||||||
|
version: 11.2.7
|
||||||
|
'@push.rocks/smartinteract':
|
||||||
|
specifier: ^2.0.16
|
||||||
|
version: 2.0.16
|
||||||
'@push.rocks/smartipc':
|
'@push.rocks/smartipc':
|
||||||
specifier: ^2.2.1
|
specifier: ^2.2.2
|
||||||
version: 2.2.1
|
version: 2.2.2
|
||||||
'@push.rocks/smartpath':
|
'@push.rocks/smartpath':
|
||||||
specifier: ^6.0.0
|
specifier: ^6.0.0
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
@@ -551,6 +557,62 @@ packages:
|
|||||||
resolution: {integrity: sha512-mfOoUlIw8VBiJYPrl5RZfMzkXC/z7gbSpi2ecycrj/gRWLq2CMV+Q+0G+JPjeOmuNFgg0skEIzkVFzVYFP6URw==}
|
resolution: {integrity: sha512-mfOoUlIw8VBiJYPrl5RZfMzkXC/z7gbSpi2ecycrj/gRWLq2CMV+Q+0G+JPjeOmuNFgg0skEIzkVFzVYFP6URw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
|
'@inquirer/checkbox@3.0.1':
|
||||||
|
resolution: {integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/confirm@4.0.1':
|
||||||
|
resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/core@9.2.1':
|
||||||
|
resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/editor@3.0.1':
|
||||||
|
resolution: {integrity: sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/expand@3.0.1':
|
||||||
|
resolution: {integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/figures@1.0.13':
|
||||||
|
resolution: {integrity: sha512-lGPVU3yO9ZNqA7vTYz26jny41lE7yoQansmqdMLBEfqaGsmdg7V3W9mK9Pvb5IL4EVZ9GnSDGMO/cJXud5dMaw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/input@3.0.1':
|
||||||
|
resolution: {integrity: sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/number@2.0.1':
|
||||||
|
resolution: {integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/password@3.0.1':
|
||||||
|
resolution: {integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/prompts@6.0.1':
|
||||||
|
resolution: {integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/rawlist@3.0.1':
|
||||||
|
resolution: {integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/search@2.0.1':
|
||||||
|
resolution: {integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/select@3.0.1':
|
||||||
|
resolution: {integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@inquirer/type@2.0.0':
|
||||||
|
resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
'@isaacs/balanced-match@4.0.1':
|
'@isaacs/balanced-match@4.0.1':
|
||||||
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -755,8 +817,8 @@ packages:
|
|||||||
'@push.rocks/smartcrypto@2.0.4':
|
'@push.rocks/smartcrypto@2.0.4':
|
||||||
resolution: {integrity: sha512-1+/5bsjyataf5uUkUNnnVXGRAt+gHVk1KDzozjTqgqJxHvQk1d9fVDohL6CxUhUucTPtu5VR5xNBiV8YCDuGyw==}
|
resolution: {integrity: sha512-1+/5bsjyataf5uUkUNnnVXGRAt+gHVk1KDzozjTqgqJxHvQk1d9fVDohL6CxUhUucTPtu5VR5xNBiV8YCDuGyw==}
|
||||||
|
|
||||||
'@push.rocks/smartdaemon@2.0.8':
|
'@push.rocks/smartdaemon@2.0.9':
|
||||||
resolution: {integrity: sha512-92qCS8XqGhQrCBDrz5L+WrWzlAggy93mXacVx9zEzGK41QwxRxZSMfxEMTxq4FO9YD4Kymffesav7S3ivCuJeQ==}
|
resolution: {integrity: sha512-TJd2N/vMAY3qpuy7ub0btNsSqdy7oU/hF/D+BbmfJVAiTKpvlgtCXKE5POwfuee03SONyh8LuH5Ey1ycIpsEHA==}
|
||||||
|
|
||||||
'@push.rocks/smartdata@5.16.4':
|
'@push.rocks/smartdata@5.16.4':
|
||||||
resolution: {integrity: sha512-COiKw8yk9iAcLN44WmZHG8Gi0v+HGkgM8Osoq7Cns+UsOA+grPepqbN2r0XPG1fm5vOdJcaydi2ZU0xrnbGVvQ==}
|
resolution: {integrity: sha512-COiKw8yk9iAcLN44WmZHG8Gi0v+HGkgM8Osoq7Cns+UsOA+grPepqbN2r0XPG1fm5vOdJcaydi2ZU0xrnbGVvQ==}
|
||||||
@@ -785,9 +847,6 @@ packages:
|
|||||||
'@push.rocks/smartfile@10.0.41':
|
'@push.rocks/smartfile@10.0.41':
|
||||||
resolution: {integrity: sha512-xOOy0duI34M2qrJZggpk51EHGXmg9+mBL1Q55tNiQKXzfx89P3coY1EAZG8tvmep3qB712QEKe7T+u04t42Kjg==}
|
resolution: {integrity: sha512-xOOy0duI34M2qrJZggpk51EHGXmg9+mBL1Q55tNiQKXzfx89P3coY1EAZG8tvmep3qB712QEKe7T+u04t42Kjg==}
|
||||||
|
|
||||||
'@push.rocks/smartfile@11.2.0':
|
|
||||||
resolution: {integrity: sha512-0Gw6DvCQ2D/BXNN6airSC7hoSBut0p/uNWf2+rqO+D6VLhIJ/QUBvF6xm/LnpPI/zcF8YlDn/GEriInB5DUtEw==}
|
|
||||||
|
|
||||||
'@push.rocks/smartfile@11.2.7':
|
'@push.rocks/smartfile@11.2.7':
|
||||||
resolution: {integrity: sha512-8Yp7/sAgPpWJBHohV92ogHWKzRomI5MEbSG6b5W2n18tqwfAmjMed0rQvsvGrSBlnEWCKgoOrYIIZbLO61+J0Q==}
|
resolution: {integrity: sha512-8Yp7/sAgPpWJBHohV92ogHWKzRomI5MEbSG6b5W2n18tqwfAmjMed0rQvsvGrSBlnEWCKgoOrYIIZbLO61+J0Q==}
|
||||||
|
|
||||||
@@ -803,8 +862,11 @@ packages:
|
|||||||
'@push.rocks/smarthash@3.2.3':
|
'@push.rocks/smarthash@3.2.3':
|
||||||
resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==}
|
resolution: {integrity: sha512-fBPQCGYtOlfLORm9tI3MyoJVT8bixs3MNTAfDDGBw91UKfOVOrPk5jBU+PwVnqZl7IE5mc9b+4wqAJn3giqEpw==}
|
||||||
|
|
||||||
'@push.rocks/smartipc@2.2.1':
|
'@push.rocks/smartinteract@2.0.16':
|
||||||
resolution: {integrity: sha512-yBFZwJsWRyVdN1YRSiHafRMfn0PYIi2IStcQqPkiU4Srr6XPDMZD3mmIeV2V1WL6bWvRWf+4WF9Y+rLhj4jGdA==}
|
resolution: {integrity: sha512-eltvVRRUKBKd77DSFA4DPY2g4V4teZLNe8A93CDy/WglglYcUjxMoLY/b0DFTWCWKYT+yjk6Fe6p0FRrvX9Yvg==}
|
||||||
|
|
||||||
|
'@push.rocks/smartipc@2.2.2':
|
||||||
|
resolution: {integrity: sha512-pkWqp2nQH7p5zD9Efh5KNX2O0+gFWL6bxbdd6SdDh4gP8Gb0b3Sn87Tpedghpc/d+LCVql+1pUf6OlvMQpD5Yw==}
|
||||||
|
|
||||||
'@push.rocks/smartjson@5.0.20':
|
'@push.rocks/smartjson@5.0.20':
|
||||||
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
resolution: {integrity: sha512-ogGBLyOTluphZVwBYNyjhm5sziPGuiAwWihW07OSRxD4HQUyqj9Ek6r1pqH07JUG5EbtRYivM1Yt1cCwnu3JVQ==}
|
||||||
@@ -842,6 +904,9 @@ packages:
|
|||||||
'@push.rocks/smartmongo@2.0.12':
|
'@push.rocks/smartmongo@2.0.12':
|
||||||
resolution: {integrity: sha512-NglYiO14BikxnlvW6JF18FtopBtaWQEGAtPxHmmSCbyhU8Mi0aEFO7VgCasE9Kguba/wcR597qhcDEdcpBg1eQ==}
|
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':
|
'@push.rocks/smartnetwork@4.1.2':
|
||||||
resolution: {integrity: sha512-TjucG72ooHgzAUpNu2LAv4iFoettmZq2aEWhhzIa7AKcOvt4yxsk3Vl73guhKRohTfhdRauPcH5OHISLUHJbYA==}
|
resolution: {integrity: sha512-TjucG72ooHgzAUpNu2LAv4iFoettmZq2aEWhhzIa7AKcOvt4yxsk3Vl73guhKRohTfhdRauPcH5OHISLUHJbYA==}
|
||||||
|
|
||||||
@@ -923,8 +988,8 @@ packages:
|
|||||||
'@push.rocks/smartstring@4.0.15':
|
'@push.rocks/smartstring@4.0.15':
|
||||||
resolution: {integrity: sha512-NTNeOjWyg+aHtBTiQEyXamr7oTvYZ3wS1fudHo9ua7CLrykpK+i+RxFyJaLg1zB5x9xQF3NLEQecB14HPFX8Cg==}
|
resolution: {integrity: sha512-NTNeOjWyg+aHtBTiQEyXamr7oTvYZ3wS1fudHo9ua7CLrykpK+i+RxFyJaLg1zB5x9xQF3NLEQecB14HPFX8Cg==}
|
||||||
|
|
||||||
'@push.rocks/smartsystem@3.0.1':
|
'@push.rocks/smartsystem@3.0.7':
|
||||||
resolution: {integrity: sha512-+W9AiSJWcRAjthqDFT8rDli2+5k3bk8c9Psndy3uKN2YbaQkMZwWptZRI3WgpXMG9NhsjF8XrkyiH/xHv9AxzQ==}
|
resolution: {integrity: sha512-FSzrJKY+pAIxlPR1cQgUd/Edy82UDusl4n2aA+Fe564Qf7KHfFY9sTapjX1JJU6zP/hmBKWzApKa7/m+qF6Tog==}
|
||||||
|
|
||||||
'@push.rocks/smarttime@4.1.1':
|
'@push.rocks/smarttime@4.1.1':
|
||||||
resolution: {integrity: sha512-Ha/3J/G+zfTl4ahpZgF6oUOZnUjpLhrBja0OQ2cloFxF9sKT8I1COaSqIfBGDtoK2Nly4UD4aTJ3JcJNOg/kgA==}
|
resolution: {integrity: sha512-Ha/3J/G+zfTl4ahpZgF6oUOZnUjpLhrBja0OQ2cloFxF9sKT8I1COaSqIfBGDtoK2Nly4UD4aTJ3JcJNOg/kgA==}
|
||||||
@@ -998,10 +1063,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-PLvBNVeuY9BERNLq3PFDkhnHHc0RpilEGHd4aUI5XRFlZF++LETdLxPbxw+DHbvHlkUf/nep09f7rrL9Tqub1Q==}
|
resolution: {integrity: sha512-PLvBNVeuY9BERNLq3PFDkhnHHc0RpilEGHd4aUI5XRFlZF++LETdLxPbxw+DHbvHlkUf/nep09f7rrL9Tqub1Q==}
|
||||||
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartmatch
|
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':
|
'@pushrocks/smartping@1.0.8':
|
||||||
resolution: {integrity: sha512-VM2gfS1sTuycj/jHyDa0lDntkPe7/JT0b2kGsy265RkichAJZkoEp3fboRJH/WAdzM8T4Du64JYgZkc8v2HHQg==}
|
resolution: {integrity: sha512-VM2gfS1sTuycj/jHyDa0lDntkPe7/JT0b2kGsy265RkichAJZkoEp3fboRJH/WAdzM8T4Du64JYgZkc8v2HHQg==}
|
||||||
deprecated: This package has been deprecated in favour of the new package at @push.rocks/smartping
|
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':
|
'@types/ms@2.1.0':
|
||||||
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
|
||||||
|
|
||||||
|
'@types/mute-stream@0.0.4':
|
||||||
|
resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==}
|
||||||
|
|
||||||
'@types/node-forge@1.3.14':
|
'@types/node-forge@1.3.14':
|
||||||
resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
|
resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==}
|
||||||
|
|
||||||
'@types/node-ipc@9.2.3':
|
|
||||||
resolution: {integrity: sha512-/MvSiF71fYf3+zwqkh/zkVkZj1hl1Uobre9EMFy08mqfJNAmpR0vmPgOUdEIDVgifxHj6G1vYMPLSBLLxoDACQ==}
|
|
||||||
|
|
||||||
'@types/node@22.13.10':
|
'@types/node@22.13.10':
|
||||||
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
|
resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
|
||||||
|
|
||||||
@@ -1661,6 +1722,9 @@ packages:
|
|||||||
'@types/which@3.0.4':
|
'@types/which@3.0.4':
|
||||||
resolution: {integrity: sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==}
|
resolution: {integrity: sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==}
|
||||||
|
|
||||||
|
'@types/wrap-ansi@3.0.0':
|
||||||
|
resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==}
|
||||||
|
|
||||||
'@types/ws@7.4.7':
|
'@types/ws@7.4.7':
|
||||||
resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==}
|
resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==}
|
||||||
|
|
||||||
@@ -1939,6 +2003,9 @@ packages:
|
|||||||
character-entities@2.0.2:
|
character-entities@2.0.2:
|
||||||
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
|
||||||
|
|
||||||
|
chardet@0.7.0:
|
||||||
|
resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
engines: {node: '>= 14.16.0'}
|
engines: {node: '>= 14.16.0'}
|
||||||
@@ -1968,8 +2035,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
cliui@7.0.4:
|
cli-width@4.1.0:
|
||||||
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
|
resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==}
|
||||||
|
engines: {node: '>= 12'}
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
|
||||||
@@ -2067,10 +2135,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==}
|
resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
copyfiles@2.4.1:
|
|
||||||
resolution: {integrity: sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg==}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
core-util-is@1.0.3:
|
core-util-is@1.0.3:
|
||||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||||
|
|
||||||
@@ -2243,10 +2307,6 @@ packages:
|
|||||||
eastasianwidth@0.2.0:
|
eastasianwidth@0.2.0:
|
||||||
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
|
||||||
|
|
||||||
easy-stack@1.0.1:
|
|
||||||
resolution: {integrity: sha512-wK2sCs4feiiJeFXn3zvY0p41mdU5VUgbgs1rNsc/y5ngFUijdWd+iIN8eoyuZHKB8xN6BL4PdWmzqFmxNg6V2w==}
|
|
||||||
engines: {node: '>=6.0.0'}
|
|
||||||
|
|
||||||
ee-first@1.1.1:
|
ee-first@1.1.1:
|
||||||
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
|
resolution: {integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=}
|
||||||
|
|
||||||
@@ -2367,10 +2427,6 @@ packages:
|
|||||||
resolution: {integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=}
|
resolution: {integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
event-pubsub@5.0.3:
|
|
||||||
resolution: {integrity: sha512-2QiHxshejKgJrYMzSI9MEHrvhmzxBL+eLyiM5IiyjDBySkgwS2+tdtnO3gbx8pEisu/yOFCIhfCb63gCEu0yBQ==}
|
|
||||||
engines: {node: '>=13.0.0'}
|
|
||||||
|
|
||||||
event-stream@3.3.4:
|
event-stream@3.3.4:
|
||||||
resolution: {integrity: sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=}
|
resolution: {integrity: sha1-SrTJoPWlTbkzi0w02Gv86PSzVXE=}
|
||||||
|
|
||||||
@@ -2400,6 +2456,10 @@ packages:
|
|||||||
extend@3.0.2:
|
extend@3.0.2:
|
||||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||||
|
|
||||||
|
external-editor@3.1.0:
|
||||||
|
resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
extract-zip@2.0.1:
|
extract-zip@2.0.1:
|
||||||
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
|
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
|
||||||
engines: {node: '>= 10.17.0'}
|
engines: {node: '>= 10.17.0'}
|
||||||
@@ -2609,11 +2669,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
glob@11.0.1:
|
|
||||||
resolution: {integrity: sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
hasBin: true
|
|
||||||
|
|
||||||
glob@11.0.3:
|
glob@11.0.3:
|
||||||
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
resolution: {integrity: sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -2779,6 +2834,10 @@ packages:
|
|||||||
ini@1.3.8:
|
ini@1.3.8:
|
||||||
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==}
|
||||||
|
|
||||||
|
inquirer@11.1.0:
|
||||||
|
resolution: {integrity: sha512-CmLAZT65GG/v30c+D2Fk8+ceP6pxD6RL+hIUOWAltCmeyEqWYwqu9v76q03OvjyZ3AB0C1Ala2stn1z/rMqGEw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
internal-ip@6.2.0:
|
internal-ip@6.2.0:
|
||||||
resolution: {integrity: sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==}
|
resolution: {integrity: sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -2893,9 +2952,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
isarray@0.0.1:
|
|
||||||
resolution: {integrity: sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=}
|
|
||||||
|
|
||||||
isarray@1.0.0:
|
isarray@1.0.0:
|
||||||
resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=}
|
resolution: {integrity: sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=}
|
||||||
|
|
||||||
@@ -2928,10 +2984,6 @@ packages:
|
|||||||
jackspeak@3.4.3:
|
jackspeak@3.4.3:
|
||||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||||
|
|
||||||
jackspeak@4.1.0:
|
|
||||||
resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
|
|
||||||
jackspeak@4.1.1:
|
jackspeak@4.1.1:
|
||||||
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
resolution: {integrity: sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -2959,14 +3011,6 @@ packages:
|
|||||||
js-base64@3.7.7:
|
js-base64@3.7.7:
|
||||||
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
|
resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==}
|
||||||
|
|
||||||
js-message@1.0.7:
|
|
||||||
resolution: {integrity: sha512-efJLHhLjIyKRewNS9EGZ4UpI8NguuL6fKkhRxVuMmrGV2xN/0APGdQYwLFky5w9naebSZ0OwAGp0G6/2Cg90rA==}
|
|
||||||
engines: {node: '>=0.6.0'}
|
|
||||||
|
|
||||||
js-queue@2.0.2:
|
|
||||||
resolution: {integrity: sha512-pbKLsbCfi7kriM3s1J4DDCo7jQkI58zPLHi0heXPzPlj0hjUsm+FesPUbE0DSbIVIK503A36aUBoCN7eMFedkA==}
|
|
||||||
engines: {node: '>=1.0.0'}
|
|
||||||
|
|
||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
@@ -3359,10 +3403,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
minimatch@10.0.1:
|
|
||||||
resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
|
|
||||||
engines: {node: 20 || >=22}
|
|
||||||
|
|
||||||
minimatch@10.0.3:
|
minimatch@10.0.3:
|
||||||
resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
|
resolution: {integrity: sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
@@ -3430,6 +3470,10 @@ packages:
|
|||||||
ms@2.1.3:
|
ms@2.1.3:
|
||||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||||
|
|
||||||
|
mute-stream@1.0.0:
|
||||||
|
resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==}
|
||||||
|
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
|
||||||
|
|
||||||
nanocolors@0.2.13:
|
nanocolors@0.2.13:
|
||||||
resolution: {integrity: sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==}
|
resolution: {integrity: sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==}
|
||||||
|
|
||||||
@@ -3466,13 +3510,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
resolution: {integrity: sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==}
|
||||||
engines: {node: '>= 6.13.0'}
|
engines: {node: '>= 6.13.0'}
|
||||||
|
|
||||||
node-ipc@12.0.0:
|
|
||||||
resolution: {integrity: sha512-QHJ2gAJiqA3cM7cQiRjLsfCOBRB0TwQ6axYD4FSllQWipEbP6i7Se1dP8EzPKk5J1nCe27W69eqPmCoKyQ61Vg==}
|
|
||||||
engines: {node: '>=14'}
|
|
||||||
|
|
||||||
noms@0.0.0:
|
|
||||||
resolution: {integrity: sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow==}
|
|
||||||
|
|
||||||
normalize-newline@4.1.0:
|
normalize-newline@4.1.0:
|
||||||
resolution: {integrity: sha512-ff4jKqMI8Xl50/4Mms/9jPobzAV/UK+kXG2XJ/7AqOmxIx8mqfqTIHYxuAnEgJ2AQeBbLnlbmZ5+38Y9A0w/YA==}
|
resolution: {integrity: sha512-ff4jKqMI8Xl50/4Mms/9jPobzAV/UK+kXG2XJ/7AqOmxIx8mqfqTIHYxuAnEgJ2AQeBbLnlbmZ5+38Y9A0w/YA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -3525,6 +3562,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
os-tmpdir@1.0.2:
|
||||||
|
resolution: {integrity: sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
p-cancelable@3.0.0:
|
p-cancelable@3.0.0:
|
||||||
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
|
resolution: {integrity: sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==}
|
||||||
engines: {node: '>=12.20'}
|
engines: {node: '>=12.20'}
|
||||||
@@ -3779,9 +3820,6 @@ packages:
|
|||||||
react-is@18.3.1:
|
react-is@18.3.1:
|
||||||
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
|
||||||
|
|
||||||
readable-stream@1.0.34:
|
|
||||||
resolution: {integrity: sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=}
|
|
||||||
|
|
||||||
readable-stream@2.3.8:
|
readable-stream@2.3.8:
|
||||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||||
|
|
||||||
@@ -3872,6 +3910,10 @@ packages:
|
|||||||
rss-parser@3.13.0:
|
rss-parser@3.13.0:
|
||||||
resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
|
resolution: {integrity: sha512-7jWUBV5yGN3rqMMj7CZufl/291QAhvrrGpDNE4k/02ZchL0npisiYYqULF71jCEKoIiHvK/Q2e6IkDwPziT7+w==}
|
||||||
|
|
||||||
|
run-async@3.0.0:
|
||||||
|
resolution: {integrity: sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==}
|
||||||
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||||
|
|
||||||
@@ -4064,9 +4106,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
string_decoder@0.10.31:
|
|
||||||
resolution: {integrity: sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=}
|
|
||||||
|
|
||||||
string_decoder@1.1.1:
|
string_decoder@1.1.1:
|
||||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||||
|
|
||||||
@@ -4106,14 +4145,6 @@ packages:
|
|||||||
strnum@2.1.1:
|
strnum@2.1.1:
|
||||||
resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
|
resolution: {integrity: sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==}
|
||||||
|
|
||||||
strong-type@0.1.6:
|
|
||||||
resolution: {integrity: sha512-eJe5caH6Pi5oMMeQtIoBPpvNu/s4jiyb63u5tkHNnQXomK+puyQ5i+Z5iTLBr/xUz/pIcps0NSfzzFI34+gAXg==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
strong-type@1.1.0:
|
|
||||||
resolution: {integrity: sha512-X5Z6riticuH5GnhUyzijfDi1SoXas8ODDyN7K8lJeQK+Jfi4dKdoJGL4CXTskY/ATBcN+rz5lROGn1tAUkOX7g==}
|
|
||||||
engines: {node: '>=12.21.0'}
|
|
||||||
|
|
||||||
strtok3@10.3.4:
|
strtok3@10.3.4:
|
||||||
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
|
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -4140,14 +4171,14 @@ packages:
|
|||||||
symbol-tree@3.2.4:
|
symbol-tree@3.2.4:
|
||||||
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
|
||||||
|
|
||||||
systeminformation@5.25.11:
|
systeminformation@5.27.7:
|
||||||
resolution: {integrity: sha512-jI01fn/t47rrLTQB0FTlMCC+5dYx8o0RRF+R4BPiUNsvg5OdY0s9DKMFmJGrx5SwMZQ4cag0Gl6v8oycso9b/g==}
|
resolution: {integrity: sha512-saaqOoVEEFaux4v0K8Q7caiauRwjXC4XbD2eH60dxHXbpKxQ8kH9Rf7Jh+nryKpOUSEFxtCdBlSUx0/lO6rwRg==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
|
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
systeminformation@5.27.7:
|
systeminformation@5.27.8:
|
||||||
resolution: {integrity: sha512-saaqOoVEEFaux4v0K8Q7caiauRwjXC4XbD2eH60dxHXbpKxQ8kH9Rf7Jh+nryKpOUSEFxtCdBlSUx0/lO6rwRg==}
|
resolution: {integrity: sha512-d3Z0gaQO1MlUxzDUKsmXz5y4TOBCMZ8IyijzaYOykV3AcNOTQ7mT+tpndUOXYNSxzLK3la8G32xiUFvZ0/s6PA==}
|
||||||
engines: {node: '>=8.0.0'}
|
engines: {node: '>=8.0.0'}
|
||||||
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
|
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -4167,9 +4198,6 @@ packages:
|
|||||||
threads@1.7.0:
|
threads@1.7.0:
|
||||||
resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==}
|
resolution: {integrity: sha512-Mx5NBSHX3sQYR6iI9VYbgHKBLisyB+xROCBGjjWm1O9wb9vfLxdaGtmT/KCjUqMsSNW6nERzCW3T6H43LqjDZQ==}
|
||||||
|
|
||||||
through2@2.0.5:
|
|
||||||
resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==}
|
|
||||||
|
|
||||||
through2@4.0.2:
|
through2@4.0.2:
|
||||||
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==}
|
||||||
|
|
||||||
@@ -4183,6 +4211,10 @@ packages:
|
|||||||
tiny-worker@2.3.0:
|
tiny-worker@2.3.0:
|
||||||
resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==}
|
resolution: {integrity: sha512-pJ70wq5EAqTAEl9IkGzA+fN0836rycEuz2Cn6yeZ6FRzlVS5IDOkFHpIoEsksPRQV34GDqXm65+OlnZqUSyK2g==}
|
||||||
|
|
||||||
|
tmp@0.0.33:
|
||||||
|
resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==}
|
||||||
|
engines: {node: '>=0.6.0'}
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
@@ -4324,10 +4356,6 @@ packages:
|
|||||||
resolution: {integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=}
|
resolution: {integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
untildify@4.0.0:
|
|
||||||
resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
upper-case@1.1.3:
|
upper-case@1.1.3:
|
||||||
resolution: {integrity: sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=}
|
resolution: {integrity: sha1-9rRQHC7EzdJrp4vnIilh3ndiFZg=}
|
||||||
|
|
||||||
@@ -4453,26 +4481,14 @@ packages:
|
|||||||
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
|
resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==}
|
||||||
engines: {node: '>=0.4.0'}
|
engines: {node: '>=0.4.0'}
|
||||||
|
|
||||||
xtend@4.0.2:
|
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
|
||||||
engines: {node: '>=0.4'}
|
|
||||||
|
|
||||||
y18n@5.0.8:
|
y18n@5.0.8:
|
||||||
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
yargs-parser@20.2.9:
|
|
||||||
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
yargs-parser@21.1.1:
|
yargs-parser@21.1.1:
|
||||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
yargs@16.2.0:
|
|
||||||
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
|
|
||||||
engines: {node: '>=10'}
|
|
||||||
|
|
||||||
yargs@17.7.2:
|
yargs@17.7.2:
|
||||||
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -4488,6 +4504,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==}
|
resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==}
|
||||||
engines: {node: '>= 4.0.0'}
|
engines: {node: '>= 4.0.0'}
|
||||||
|
|
||||||
|
yoctocolors-cjs@2.1.3:
|
||||||
|
resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
zod@3.25.76:
|
zod@3.25.76:
|
||||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
@@ -5632,7 +5652,7 @@ snapshots:
|
|||||||
|
|
||||||
'@git.zone/tsrun@1.3.3':
|
'@git.zone/tsrun@1.3.3':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartfile': 11.2.0
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartshell': 3.2.3
|
'@push.rocks/smartshell': 3.2.3
|
||||||
tsx: 4.20.5
|
tsx: 4.20.5
|
||||||
|
|
||||||
@@ -5687,6 +5707,102 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
happy-dom: 15.11.7
|
happy-dom: 15.11.7
|
||||||
|
|
||||||
|
'@inquirer/checkbox@3.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/core': 9.2.1
|
||||||
|
'@inquirer/figures': 1.0.13
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
ansi-escapes: 4.3.2
|
||||||
|
yoctocolors-cjs: 2.1.3
|
||||||
|
|
||||||
|
'@inquirer/confirm@4.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/core': 9.2.1
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
|
||||||
|
'@inquirer/core@9.2.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/figures': 1.0.13
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
'@types/mute-stream': 0.0.4
|
||||||
|
'@types/node': 22.13.10
|
||||||
|
'@types/wrap-ansi': 3.0.0
|
||||||
|
ansi-escapes: 4.3.2
|
||||||
|
cli-width: 4.1.0
|
||||||
|
mute-stream: 1.0.0
|
||||||
|
signal-exit: 4.1.0
|
||||||
|
strip-ansi: 6.0.1
|
||||||
|
wrap-ansi: 6.2.0
|
||||||
|
yoctocolors-cjs: 2.1.3
|
||||||
|
|
||||||
|
'@inquirer/editor@3.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/core': 9.2.1
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
external-editor: 3.1.0
|
||||||
|
|
||||||
|
'@inquirer/expand@3.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/core': 9.2.1
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
yoctocolors-cjs: 2.1.3
|
||||||
|
|
||||||
|
'@inquirer/figures@1.0.13': {}
|
||||||
|
|
||||||
|
'@inquirer/input@3.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/core': 9.2.1
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
|
||||||
|
'@inquirer/number@2.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/core': 9.2.1
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
|
||||||
|
'@inquirer/password@3.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/core': 9.2.1
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
ansi-escapes: 4.3.2
|
||||||
|
|
||||||
|
'@inquirer/prompts@6.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/checkbox': 3.0.1
|
||||||
|
'@inquirer/confirm': 4.0.1
|
||||||
|
'@inquirer/editor': 3.0.1
|
||||||
|
'@inquirer/expand': 3.0.1
|
||||||
|
'@inquirer/input': 3.0.1
|
||||||
|
'@inquirer/number': 2.0.1
|
||||||
|
'@inquirer/password': 3.0.1
|
||||||
|
'@inquirer/rawlist': 3.0.1
|
||||||
|
'@inquirer/search': 2.0.1
|
||||||
|
'@inquirer/select': 3.0.1
|
||||||
|
|
||||||
|
'@inquirer/rawlist@3.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/core': 9.2.1
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
yoctocolors-cjs: 2.1.3
|
||||||
|
|
||||||
|
'@inquirer/search@2.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/core': 9.2.1
|
||||||
|
'@inquirer/figures': 1.0.13
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
yoctocolors-cjs: 2.1.3
|
||||||
|
|
||||||
|
'@inquirer/select@3.0.1':
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/core': 9.2.1
|
||||||
|
'@inquirer/figures': 1.0.13
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
ansi-escapes: 4.3.2
|
||||||
|
yoctocolors-cjs: 2.1.3
|
||||||
|
|
||||||
|
'@inquirer/type@2.0.0':
|
||||||
|
dependencies:
|
||||||
|
mute-stream: 1.0.0
|
||||||
|
|
||||||
'@isaacs/balanced-match@4.0.1': {}
|
'@isaacs/balanced-match@4.0.1': {}
|
||||||
|
|
||||||
'@isaacs/brace-expansion@5.0.0':
|
'@isaacs/brace-expansion@5.0.0':
|
||||||
@@ -6074,16 +6190,16 @@ snapshots:
|
|||||||
'@types/node-forge': 1.3.14
|
'@types/node-forge': 1.3.14
|
||||||
node-forge: 1.3.1
|
node-forge: 1.3.1
|
||||||
|
|
||||||
'@push.rocks/smartdaemon@2.0.8':
|
'@push.rocks/smartdaemon@2.0.9':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.1.0
|
'@push.rocks/lik': 6.2.2
|
||||||
'@push.rocks/smartfile': 11.2.0
|
'@push.rocks/smartfile': 11.2.7
|
||||||
'@push.rocks/smartfm': 2.2.2
|
'@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/smartlog-destination-local': 9.0.2
|
||||||
'@push.rocks/smartpath': 5.1.0
|
'@push.rocks/smartpath': 6.0.0
|
||||||
'@push.rocks/smartshell': 3.2.3
|
'@push.rocks/smartshell': 3.3.0
|
||||||
'@push.rocks/smartsystem': 3.0.1
|
'@push.rocks/smartsystem': 3.0.7
|
||||||
|
|
||||||
'@push.rocks/smartdata@5.16.4(@aws-sdk/credential-providers@3.758.0)(socks@2.8.7)':
|
'@push.rocks/smartdata@5.16.4(@aws-sdk/credential-providers@3.758.0)(socks@2.8.7)':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6161,25 +6277,6 @@ snapshots:
|
|||||||
glob: 10.4.5
|
glob: 10.4.5
|
||||||
js-yaml: 4.1.0
|
js-yaml: 4.1.0
|
||||||
|
|
||||||
'@push.rocks/smartfile@11.2.0':
|
|
||||||
dependencies:
|
|
||||||
'@push.rocks/lik': 6.1.0
|
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
|
||||||
'@push.rocks/smartfile-interfaces': 1.0.7
|
|
||||||
'@push.rocks/smarthash': 3.0.4
|
|
||||||
'@push.rocks/smartjson': 5.0.20
|
|
||||||
'@push.rocks/smartmime': 2.0.4
|
|
||||||
'@push.rocks/smartpath': 5.1.0
|
|
||||||
'@push.rocks/smartpromise': 4.2.3
|
|
||||||
'@push.rocks/smartrequest': 2.0.23
|
|
||||||
'@push.rocks/smartstream': 3.2.5
|
|
||||||
'@types/fs-extra': 11.0.4
|
|
||||||
'@types/glob': 8.1.0
|
|
||||||
'@types/js-yaml': 4.0.9
|
|
||||||
fs-extra: 11.3.0
|
|
||||||
glob: 11.0.1
|
|
||||||
js-yaml: 4.1.0
|
|
||||||
|
|
||||||
'@push.rocks/smartfile@11.2.7':
|
'@push.rocks/smartfile@11.2.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/lik': 6.2.2
|
'@push.rocks/lik': 6.2.2
|
||||||
@@ -6222,12 +6319,17 @@ snapshots:
|
|||||||
'@types/through2': 2.0.41
|
'@types/through2': 2.0.41
|
||||||
through2: 4.0.2
|
through2: 4.0.2
|
||||||
|
|
||||||
'@push.rocks/smartipc@2.2.1':
|
'@push.rocks/smartinteract@2.0.16':
|
||||||
|
dependencies:
|
||||||
|
'@push.rocks/lik': 6.2.2
|
||||||
|
'@push.rocks/smartobject': 1.0.12
|
||||||
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
|
inquirer: 11.1.0
|
||||||
|
|
||||||
|
'@push.rocks/smartipc@2.2.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartdelay': 3.0.5
|
'@push.rocks/smartdelay': 3.0.5
|
||||||
'@push.rocks/smartrx': 3.0.10
|
'@push.rocks/smartrx': 3.0.10
|
||||||
'@types/node-ipc': 9.2.3
|
|
||||||
node-ipc: 12.0.0
|
|
||||||
|
|
||||||
'@push.rocks/smartjson@5.0.20':
|
'@push.rocks/smartjson@5.0.20':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6318,6 +6420,16 @@ snapshots:
|
|||||||
- socks
|
- socks
|
||||||
- supports-color
|
- 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':
|
'@push.rocks/smartnetwork@4.1.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@push.rocks/smartping': 1.0.8
|
'@push.rocks/smartping': 1.0.8
|
||||||
@@ -6559,13 +6671,13 @@ snapshots:
|
|||||||
strip-indent: 4.0.0
|
strip-indent: 4.0.0
|
||||||
url: 0.11.4
|
url: 0.11.4
|
||||||
|
|
||||||
'@push.rocks/smartsystem@3.0.1':
|
'@push.rocks/smartsystem@3.0.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@pushrocks/lik': 6.0.2
|
'@push.rocks/lik': 6.2.2
|
||||||
'@pushrocks/smartenv': 5.0.5
|
'@push.rocks/smartenv': 5.0.13
|
||||||
'@pushrocks/smartnetwork': 3.0.2
|
'@push.rocks/smartnetwork': 3.0.2
|
||||||
'@pushrocks/smartpromise': 3.1.10
|
'@push.rocks/smartpromise': 4.2.3
|
||||||
systeminformation: 5.25.11
|
systeminformation: 5.27.8
|
||||||
|
|
||||||
'@push.rocks/smarttime@4.1.1':
|
'@push.rocks/smarttime@4.1.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6721,16 +6833,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
matcher: 5.0.0
|
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':
|
'@pushrocks/smartping@1.0.8':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/ping': 0.4.4
|
'@types/ping': 0.4.4
|
||||||
@@ -7476,11 +7578,11 @@ snapshots:
|
|||||||
|
|
||||||
'@types/ms@2.1.0': {}
|
'@types/ms@2.1.0': {}
|
||||||
|
|
||||||
'@types/node-forge@1.3.14':
|
'@types/mute-stream@0.0.4':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.10
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
'@types/node-ipc@9.2.3':
|
'@types/node-forge@1.3.14':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.10
|
'@types/node': 22.13.10
|
||||||
|
|
||||||
@@ -7562,6 +7664,8 @@ snapshots:
|
|||||||
|
|
||||||
'@types/which@3.0.4': {}
|
'@types/which@3.0.4': {}
|
||||||
|
|
||||||
|
'@types/wrap-ansi@3.0.0': {}
|
||||||
|
|
||||||
'@types/ws@7.4.7':
|
'@types/ws@7.4.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 22.13.10
|
'@types/node': 22.13.10
|
||||||
@@ -7889,6 +7993,8 @@ snapshots:
|
|||||||
|
|
||||||
character-entities@2.0.2: {}
|
character-entities@2.0.2: {}
|
||||||
|
|
||||||
|
chardet@0.7.0: {}
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
@@ -7915,11 +8021,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
restore-cursor: 3.1.0
|
restore-cursor: 3.1.0
|
||||||
|
|
||||||
cliui@7.0.4:
|
cli-width@4.1.0: {}
|
||||||
dependencies:
|
|
||||||
string-width: 4.2.3
|
|
||||||
strip-ansi: 6.0.1
|
|
||||||
wrap-ansi: 7.0.0
|
|
||||||
|
|
||||||
cliui@8.0.1:
|
cliui@8.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -8012,16 +8114,6 @@ snapshots:
|
|||||||
depd: 2.0.0
|
depd: 2.0.0
|
||||||
keygrip: 1.1.0
|
keygrip: 1.1.0
|
||||||
|
|
||||||
copyfiles@2.4.1:
|
|
||||||
dependencies:
|
|
||||||
glob: 7.2.3
|
|
||||||
minimatch: 3.1.2
|
|
||||||
mkdirp: 1.0.4
|
|
||||||
noms: 0.0.0
|
|
||||||
through2: 2.0.5
|
|
||||||
untildify: 4.0.0
|
|
||||||
yargs: 16.2.0
|
|
||||||
|
|
||||||
core-util-is@1.0.3: {}
|
core-util-is@1.0.3: {}
|
||||||
|
|
||||||
cors@2.8.5:
|
cors@2.8.5:
|
||||||
@@ -8158,8 +8250,6 @@ snapshots:
|
|||||||
|
|
||||||
eastasianwidth@0.2.0: {}
|
eastasianwidth@0.2.0: {}
|
||||||
|
|
||||||
easy-stack@1.0.1: {}
|
|
||||||
|
|
||||||
ee-first@1.1.1: {}
|
ee-first@1.1.1: {}
|
||||||
|
|
||||||
emoji-regex@8.0.0: {}
|
emoji-regex@8.0.0: {}
|
||||||
@@ -8292,11 +8382,6 @@ snapshots:
|
|||||||
|
|
||||||
etag@1.8.1: {}
|
etag@1.8.1: {}
|
||||||
|
|
||||||
event-pubsub@5.0.3:
|
|
||||||
dependencies:
|
|
||||||
copyfiles: 2.4.1
|
|
||||||
strong-type: 0.1.6
|
|
||||||
|
|
||||||
event-stream@3.3.4:
|
event-stream@3.3.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
duplexer: 0.1.2
|
duplexer: 0.1.2
|
||||||
@@ -8371,6 +8456,12 @@ snapshots:
|
|||||||
|
|
||||||
extend@3.0.2: {}
|
extend@3.0.2: {}
|
||||||
|
|
||||||
|
external-editor@3.1.0:
|
||||||
|
dependencies:
|
||||||
|
chardet: 0.7.0
|
||||||
|
iconv-lite: 0.4.24
|
||||||
|
tmp: 0.0.33
|
||||||
|
|
||||||
extract-zip@2.0.1:
|
extract-zip@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 4.4.1
|
debug: 4.4.1
|
||||||
@@ -8613,15 +8704,6 @@ snapshots:
|
|||||||
package-json-from-dist: 1.0.1
|
package-json-from-dist: 1.0.1
|
||||||
path-scurry: 1.11.1
|
path-scurry: 1.11.1
|
||||||
|
|
||||||
glob@11.0.1:
|
|
||||||
dependencies:
|
|
||||||
foreground-child: 3.3.1
|
|
||||||
jackspeak: 4.1.0
|
|
||||||
minimatch: 10.0.1
|
|
||||||
minipass: 7.1.2
|
|
||||||
package-json-from-dist: 1.0.1
|
|
||||||
path-scurry: 2.0.0
|
|
||||||
|
|
||||||
glob@11.0.3:
|
glob@11.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
foreground-child: 3.3.1
|
foreground-child: 3.3.1
|
||||||
@@ -8843,6 +8925,17 @@ snapshots:
|
|||||||
|
|
||||||
ini@1.3.8: {}
|
ini@1.3.8: {}
|
||||||
|
|
||||||
|
inquirer@11.1.0:
|
||||||
|
dependencies:
|
||||||
|
'@inquirer/core': 9.2.1
|
||||||
|
'@inquirer/prompts': 6.0.1
|
||||||
|
'@inquirer/type': 2.0.0
|
||||||
|
'@types/mute-stream': 0.0.4
|
||||||
|
ansi-escapes: 4.3.2
|
||||||
|
mute-stream: 1.0.0
|
||||||
|
run-async: 3.0.0
|
||||||
|
rxjs: 7.8.2
|
||||||
|
|
||||||
internal-ip@6.2.0:
|
internal-ip@6.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
default-gateway: 6.0.3
|
default-gateway: 6.0.3
|
||||||
@@ -8930,8 +9023,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
|
|
||||||
isarray@0.0.1: {}
|
|
||||||
|
|
||||||
isarray@1.0.0: {}
|
isarray@1.0.0: {}
|
||||||
|
|
||||||
isbinaryfile@5.0.4: {}
|
isbinaryfile@5.0.4: {}
|
||||||
@@ -8961,10 +9052,6 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@pkgjs/parseargs': 0.11.0
|
'@pkgjs/parseargs': 0.11.0
|
||||||
|
|
||||||
jackspeak@4.1.0:
|
|
||||||
dependencies:
|
|
||||||
'@isaacs/cliui': 8.0.2
|
|
||||||
|
|
||||||
jackspeak@4.1.1:
|
jackspeak@4.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/cliui': 8.0.2
|
'@isaacs/cliui': 8.0.2
|
||||||
@@ -9008,12 +9095,6 @@ snapshots:
|
|||||||
|
|
||||||
js-base64@3.7.7: {}
|
js-base64@3.7.7: {}
|
||||||
|
|
||||||
js-message@1.0.7: {}
|
|
||||||
|
|
||||||
js-queue@2.0.2:
|
|
||||||
dependencies:
|
|
||||||
easy-stack: 1.0.1
|
|
||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
js-yaml@3.14.1:
|
js-yaml@3.14.1:
|
||||||
@@ -9603,10 +9684,6 @@ snapshots:
|
|||||||
|
|
||||||
min-indent@1.0.1: {}
|
min-indent@1.0.1: {}
|
||||||
|
|
||||||
minimatch@10.0.1:
|
|
||||||
dependencies:
|
|
||||||
brace-expansion: 2.0.1
|
|
||||||
|
|
||||||
minimatch@10.0.3:
|
minimatch@10.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@isaacs/brace-expansion': 5.0.0
|
'@isaacs/brace-expansion': 5.0.0
|
||||||
@@ -9681,6 +9758,8 @@ snapshots:
|
|||||||
|
|
||||||
ms@2.1.3: {}
|
ms@2.1.3: {}
|
||||||
|
|
||||||
|
mute-stream@1.0.0: {}
|
||||||
|
|
||||||
nanocolors@0.2.13: {}
|
nanocolors@0.2.13: {}
|
||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
@@ -9705,18 +9784,6 @@ snapshots:
|
|||||||
|
|
||||||
node-forge@1.3.1: {}
|
node-forge@1.3.1: {}
|
||||||
|
|
||||||
node-ipc@12.0.0:
|
|
||||||
dependencies:
|
|
||||||
event-pubsub: 5.0.3
|
|
||||||
js-message: 1.0.7
|
|
||||||
js-queue: 2.0.2
|
|
||||||
strong-type: 1.1.0
|
|
||||||
|
|
||||||
noms@0.0.0:
|
|
||||||
dependencies:
|
|
||||||
inherits: 2.0.4
|
|
||||||
readable-stream: 1.0.34
|
|
||||||
|
|
||||||
normalize-newline@4.1.0:
|
normalize-newline@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
replace-buffer: 1.2.1
|
replace-buffer: 1.2.1
|
||||||
@@ -9761,6 +9828,8 @@ snapshots:
|
|||||||
is-docker: 2.2.1
|
is-docker: 2.2.1
|
||||||
is-wsl: 2.2.0
|
is-wsl: 2.2.0
|
||||||
|
|
||||||
|
os-tmpdir@1.0.2: {}
|
||||||
|
|
||||||
p-cancelable@3.0.0: {}
|
p-cancelable@3.0.0: {}
|
||||||
|
|
||||||
p-event@4.2.0:
|
p-event@4.2.0:
|
||||||
@@ -10033,13 +10102,6 @@ snapshots:
|
|||||||
|
|
||||||
react-is@18.3.1: {}
|
react-is@18.3.1: {}
|
||||||
|
|
||||||
readable-stream@1.0.34:
|
|
||||||
dependencies:
|
|
||||||
core-util-is: 1.0.3
|
|
||||||
inherits: 2.0.4
|
|
||||||
isarray: 0.0.1
|
|
||||||
string_decoder: 0.10.31
|
|
||||||
|
|
||||||
readable-stream@2.3.8:
|
readable-stream@2.3.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
core-util-is: 1.0.3
|
core-util-is: 1.0.3
|
||||||
@@ -10180,6 +10242,8 @@ snapshots:
|
|||||||
entities: 2.2.0
|
entities: 2.2.0
|
||||||
xml2js: 0.5.0
|
xml2js: 0.5.0
|
||||||
|
|
||||||
|
run-async@3.0.0: {}
|
||||||
|
|
||||||
run-parallel@1.2.0:
|
run-parallel@1.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
queue-microtask: 1.2.3
|
queue-microtask: 1.2.3
|
||||||
@@ -10435,8 +10499,6 @@ snapshots:
|
|||||||
emoji-regex: 9.2.2
|
emoji-regex: 9.2.2
|
||||||
strip-ansi: 7.1.0
|
strip-ansi: 7.1.0
|
||||||
|
|
||||||
string_decoder@0.10.31: {}
|
|
||||||
|
|
||||||
string_decoder@1.1.1:
|
string_decoder@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer: 5.1.2
|
safe-buffer: 5.1.2
|
||||||
@@ -10472,10 +10534,6 @@ snapshots:
|
|||||||
|
|
||||||
strnum@2.1.1: {}
|
strnum@2.1.1: {}
|
||||||
|
|
||||||
strong-type@0.1.6: {}
|
|
||||||
|
|
||||||
strong-type@1.1.0: {}
|
|
||||||
|
|
||||||
strtok3@10.3.4:
|
strtok3@10.3.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tokenizer/token': 0.3.0
|
'@tokenizer/token': 0.3.0
|
||||||
@@ -10503,10 +10561,10 @@ snapshots:
|
|||||||
|
|
||||||
symbol-tree@3.2.4: {}
|
symbol-tree@3.2.4: {}
|
||||||
|
|
||||||
systeminformation@5.25.11: {}
|
|
||||||
|
|
||||||
systeminformation@5.27.7: {}
|
systeminformation@5.27.7: {}
|
||||||
|
|
||||||
|
systeminformation@5.27.8: {}
|
||||||
|
|
||||||
tar-fs@3.1.0:
|
tar-fs@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
pump: 3.0.3
|
pump: 3.0.3
|
||||||
@@ -10540,11 +10598,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
through2@2.0.5:
|
|
||||||
dependencies:
|
|
||||||
readable-stream: 2.3.8
|
|
||||||
xtend: 4.0.2
|
|
||||||
|
|
||||||
through2@4.0.2:
|
through2@4.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
@@ -10559,6 +10612,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
esm: 3.2.25
|
esm: 3.2.25
|
||||||
|
|
||||||
|
tmp@0.0.33:
|
||||||
|
dependencies:
|
||||||
|
os-tmpdir: 1.0.2
|
||||||
|
|
||||||
to-regex-range@5.0.1:
|
to-regex-range@5.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
@@ -10681,8 +10738,6 @@ snapshots:
|
|||||||
|
|
||||||
unpipe@1.0.0: {}
|
unpipe@1.0.0: {}
|
||||||
|
|
||||||
untildify@4.0.0: {}
|
|
||||||
|
|
||||||
upper-case@1.1.3: {}
|
upper-case@1.1.3: {}
|
||||||
|
|
||||||
url@0.11.4:
|
url@0.11.4:
|
||||||
@@ -10789,24 +10844,10 @@ snapshots:
|
|||||||
|
|
||||||
xmlhttprequest-ssl@2.1.2: {}
|
xmlhttprequest-ssl@2.1.2: {}
|
||||||
|
|
||||||
xtend@4.0.2: {}
|
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
||||||
yargs-parser@20.2.9: {}
|
|
||||||
|
|
||||||
yargs-parser@21.1.1: {}
|
yargs-parser@21.1.1: {}
|
||||||
|
|
||||||
yargs@16.2.0:
|
|
||||||
dependencies:
|
|
||||||
cliui: 7.0.4
|
|
||||||
escalade: 3.2.0
|
|
||||||
get-caller-file: 2.0.5
|
|
||||||
require-directory: 2.1.1
|
|
||||||
string-width: 4.2.3
|
|
||||||
y18n: 5.0.8
|
|
||||||
yargs-parser: 20.2.9
|
|
||||||
|
|
||||||
yargs@17.7.2:
|
yargs@17.7.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
cliui: 8.0.1
|
cliui: 8.0.1
|
||||||
@@ -10829,6 +10870,8 @@ snapshots:
|
|||||||
|
|
||||||
ylru@1.4.0: {}
|
ylru@1.4.0: {}
|
||||||
|
|
||||||
|
yoctocolors-cjs@2.1.3: {}
|
||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|
||||||
zwitch@2.0.4: {}
|
zwitch@2.0.4: {}
|
||||||
|
453
readme.md
453
readme.md
@@ -2,118 +2,133 @@
|
|||||||
|
|
||||||
**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.
|
**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
|
### ✨ Key Features
|
||||||
- **Smart Auto-Restart** - Crashed processes come back automatically (when you want them to)
|
|
||||||
- **File Watching** - Auto-restart on file changes during development
|
- **🧠 Smart Memory Management** - Tracks memory including child processes, enforces limits, and auto-restarts when exceeded
|
||||||
- **Process Groups** - Track parent and child processes together
|
- **💾 Persistent Log Storage** - Keeps 10MB of logs in memory, persists to disk on restart/stop/error
|
||||||
- **Daemon Architecture** - Survives terminal sessions with a persistent background daemon
|
- **🔄 Intelligent Auto-Restart** - Automatically restarts crashed processes with configurable policies
|
||||||
- **Beautiful CLI** - Clean, informative terminal output with real-time status
|
- **👀 File Watching** - Auto-restart on file changes for seamless development
|
||||||
- **Structured Logging** - Capture and manage stdout/stderr with intelligent buffering
|
- **🌳 Process Group Tracking** - Monitors parent and all child processes as a unit
|
||||||
- **Zero Config** - Works out of the box, customize when you need to
|
- **🏗️ 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
|
## 📦 Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install globally
|
# Install globally (recommended)
|
||||||
npm install -g @git.zone/tspm
|
npm install -g @git.zone/tspm
|
||||||
|
|
||||||
# Or with pnpm (recommended)
|
# Or with pnpm
|
||||||
pnpm add -g @git.zone/tspm
|
pnpm add -g @git.zone/tspm
|
||||||
|
|
||||||
# Or use in your project
|
# Or as a dev dependency
|
||||||
npm install --save-dev @git.zone/tspm
|
npm install --save-dev @git.zone/tspm
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🚀 Quick Start
|
## 🚀 Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start the daemon (happens automatically on first use)
|
# Add a process (creates config without starting)
|
||||||
tspm daemon start
|
tspm add "node server.js" --name my-server --memory 1GB
|
||||||
|
|
||||||
# Start a process
|
# Start the process (by name or id)
|
||||||
tspm start server.js --name my-server
|
tspm start name:my-server
|
||||||
|
# or
|
||||||
|
tspm start id:1
|
||||||
|
|
||||||
# Start with memory limit
|
# Or add and start in one go
|
||||||
tspm start app.js --memory 512MB --name my-app
|
tspm add "node app.js" --name my-app
|
||||||
|
tspm start name:my-app
|
||||||
# Start with file watching (great for development)
|
|
||||||
tspm start dev.js --watch --name dev-server
|
|
||||||
|
|
||||||
# List all processes
|
# List all processes
|
||||||
tspm list
|
tspm list
|
||||||
|
|
||||||
# Check process details
|
|
||||||
tspm describe my-server
|
|
||||||
|
|
||||||
# View logs
|
# View logs
|
||||||
tspm logs my-server --lines 100
|
tspm logs name:my-app
|
||||||
|
|
||||||
# Stop a process
|
# Stop a process
|
||||||
tspm stop my-server
|
tspm stop name:my-app
|
||||||
|
|
||||||
# Restart a process
|
|
||||||
tspm restart my-server
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📋 Command Reference
|
## 📋 Commands
|
||||||
|
|
||||||
### Process Management
|
### 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:**
|
**Options:**
|
||||||
|
- `--name <name>` - Custom name for the process (required)
|
||||||
- `--name <name>` - Custom name for the process (default: script name)
|
|
||||||
- `--memory <size>` - Memory limit (e.g., "512MB", "2GB", default: 512MB)
|
- `--memory <size>` - Memory limit (e.g., "512MB", "2GB", default: 512MB)
|
||||||
- `--cwd <path>` - Working directory (default: current directory)
|
- `--cwd <path>` - Working directory (default: current directory)
|
||||||
- `--watch` - Enable file watching for auto-restart
|
- `--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)
|
- `--autorestart` - Auto-restart on crash (default: true)
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Simple start
|
# Add a simple Node.js app
|
||||||
tspm start server.js
|
tspm add "node server.js" --name api-server
|
||||||
|
|
||||||
# Production setup with 2GB memory
|
# Add with 2GB memory limit
|
||||||
tspm start app.js --name production-api --memory 2GB
|
tspm add "node app.js" --name production-api --memory 2GB
|
||||||
|
|
||||||
# Development with watching
|
# Add TypeScript app with watching
|
||||||
tspm start dev-server.js --watch --watch-paths "src,config" --name dev
|
tspm add "tsx watch src/index.ts" --name dev-server --watch --watch-paths "src,config"
|
||||||
|
|
||||||
# Custom working directory
|
# Add without auto-restart
|
||||||
tspm start ../other-project/index.js --cwd ../other-project --name other
|
tspm add "node worker.js" --name one-time-job --autorestart false
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm stop <id>`
|
#### `tspm start <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
|
Start a previously added process by its ID or name.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm start name:my-server
|
||||||
|
tspm start id:1 # Or a bare numeric id: tspm start 1
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `tspm stop <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
|
Gracefully stop a running process (SIGTERM → SIGKILL after timeout).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm stop my-server
|
tspm stop name:my-server
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm restart <id>`
|
#### `tspm restart <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Stop and restart a process with the same configuration.
|
Stop and restart a process with the same configuration.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm restart my-server
|
tspm restart name:my-server
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm delete <id>`
|
#### `tspm delete <id|id:N|name:LABEL>` / `tspm remove <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Stop and remove a process from TSPM management.
|
Stop and remove a process from TSPM management. Also deletes persisted logs.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm delete old-server
|
tspm delete name:old-server
|
||||||
|
tspm remove name:old-server # Alias for delete (daemon handles 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
|
### Monitoring & Information
|
||||||
@@ -126,20 +141,21 @@ Display all managed processes in a beautiful table.
|
|||||||
tspm list
|
tspm list
|
||||||
|
|
||||||
# Output:
|
# Output:
|
||||||
┌─────────┬─────────────┬───────────┬───────────┬──────────┐
|
┌─────────┬─────────────┬───────────┬───────────┬──────────┬──────────┐
|
||||||
│ ID │ Name │ Status │ Memory │ Restarts │
|
│ ID │ Name │ Status │ PID │ Memory │ Restarts │
|
||||||
├─────────┼─────────────┼───────────┼───────────┼──────────┤
|
├─────────┼─────────────┼───────────┼───────────┼──────────┼──────────┤
|
||||||
│ my-app │ my-app │ online │ 245.3 MB │ 0 │
|
│ 1 │ my-app │ online │ 45123 │ 245.3 MB │ 0 │
|
||||||
│ worker │ worker │ online │ 128.7 MB │ 2 │
|
│ 2 │ worker │ online │ 45456 │ 128.7 MB │ 2 │
|
||||||
└─────────┴─────────────┴───────────┴───────────┴──────────┘
|
│ 3 │ api-server │ stopped │ - │ 0 B │ 5 │
|
||||||
|
└─────────┴─────────────┴───────────┴───────────┴──────────┴──────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm describe <id>`
|
#### `tspm describe <id|id:N|name:LABEL>`
|
||||||
|
|
||||||
Get detailed information about a specific process.
|
Get detailed information about a specific process.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm describe my-server
|
tspm describe name:my-server
|
||||||
|
|
||||||
# Output:
|
# Output:
|
||||||
Process Details: my-server
|
Process Details: my-server
|
||||||
@@ -147,29 +163,35 @@ Process Details: my-server
|
|||||||
Status: online
|
Status: online
|
||||||
PID: 45123
|
PID: 45123
|
||||||
Memory: 245.3 MB
|
Memory: 245.3 MB
|
||||||
CPU: 2.3%
|
|
||||||
Uptime: 3600s
|
Uptime: 3600s
|
||||||
Restarts: 0
|
Restarts: 0
|
||||||
|
|
||||||
Configuration:
|
Configuration:
|
||||||
Command: server.js
|
────────────────────────────────────────
|
||||||
|
Command: node server.js
|
||||||
Directory: /home/user/project
|
Directory: /home/user/project
|
||||||
Memory Limit: 2 GB
|
Memory Limit: 2 GB
|
||||||
Auto-restart: true
|
Auto-restart: true
|
||||||
Watch: enabled
|
Watch: disabled
|
||||||
Watch Paths: src, config
|
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm logs <id> [options]`
|
#### `tspm logs <id|id:N|name:LABEL> [options]`
|
||||||
|
|
||||||
View process logs (stdout and stderr).
|
View process logs (stdout and stderr combined).
|
||||||
|
|
||||||
**Options:**
|
**Options:**
|
||||||
|
|
||||||
- `--lines <n>` - Number of lines to display (default: 50)
|
- `--lines <n>` - Number of lines to display (default: 50)
|
||||||
|
- `--follow` - Stream logs in real-time (like `tail -f`)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm logs my-server --lines 100
|
# View last 50 lines
|
||||||
|
tspm logs name:my-server
|
||||||
|
|
||||||
|
# View last 100 lines
|
||||||
|
tspm logs name:my-server --lines 100
|
||||||
|
|
||||||
|
# Follow logs in real-time
|
||||||
|
tspm logs name:my-server --follow
|
||||||
```
|
```
|
||||||
|
|
||||||
### Batch Operations
|
### Batch Operations
|
||||||
@@ -180,6 +202,10 @@ Start all saved processes at once.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm start-all
|
tspm start-all
|
||||||
|
# ✓ Started 3 processes:
|
||||||
|
# - my-app
|
||||||
|
# - worker
|
||||||
|
# - api-server
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm stop-all`
|
#### `tspm stop-all`
|
||||||
@@ -188,6 +214,7 @@ Stop all running processes.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm stop-all
|
tspm stop-all
|
||||||
|
# ✓ Stopped 3 processes
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm restart-all`
|
#### `tspm restart-all`
|
||||||
@@ -196,24 +223,49 @@ Restart all running processes.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm restart-all
|
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
|
### Daemon Management
|
||||||
|
|
||||||
|
The TSPM daemon runs in the background and manages all your processes. It starts automatically when needed.
|
||||||
|
|
||||||
#### `tspm daemon start`
|
#### `tspm daemon start`
|
||||||
|
|
||||||
Start the TSPM daemon (happens automatically on first command).
|
Manually start the TSPM daemon (usually automatic).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm daemon start
|
tspm daemon start
|
||||||
|
# ✓ TSPM daemon started successfully
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `tspm daemon stop`
|
#### `tspm daemon stop`
|
||||||
|
|
||||||
Stop the TSPM daemon and all managed processes.
|
Stop the daemon and all managed processes.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
tspm daemon stop
|
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`
|
#### `tspm daemon status`
|
||||||
@@ -230,75 +282,175 @@ Status: running
|
|||||||
PID: 12345
|
PID: 12345
|
||||||
Uptime: 86400s
|
Uptime: 86400s
|
||||||
Processes: 5
|
Processes: 5
|
||||||
Memory: 45.2 MB
|
Socket: /home/user/.tspm/tspm.sock
|
||||||
CPU: 0.1%
|
```
|
||||||
|
|
||||||
|
### 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
|
## 🏗️ 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
|
```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
|
// Add and start a process
|
||||||
const processId = await manager.start({
|
const { id } = await client.request('add', {
|
||||||
id: 'worker',
|
|
||||||
name: 'Background Worker',
|
|
||||||
command: 'node worker.js',
|
command: 'node worker.js',
|
||||||
|
name: 'background-worker',
|
||||||
projectDir: process.cwd(),
|
projectDir: process.cwd(),
|
||||||
memoryLimitBytes: 512 * 1024 * 1024, // 512MB
|
memoryLimit: 512 * 1024 * 1024, // 512MB in bytes
|
||||||
autorestart: true,
|
autorestart: true,
|
||||||
watch: false,
|
watch: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Monitor process
|
await client.request('start', { id });
|
||||||
const info = await manager.getProcessInfo(processId);
|
|
||||||
console.log(`Process ${info.id} is ${info.status}`);
|
|
||||||
|
|
||||||
// Stop process
|
// Get process info
|
||||||
await manager.stop(processId);
|
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
|
## 🔧 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
|
### 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
|
## 🛠️ Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Clone the repository
|
# Clone the repository
|
||||||
git clone https://code.foss.global/git.zone/tspm.git
|
git clone https://code.foss.global/git.zone/tspm.git
|
||||||
|
cd tspm
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
pnpm install
|
pnpm install
|
||||||
@@ -309,42 +461,109 @@ pnpm test
|
|||||||
# Build the project
|
# Build the project
|
||||||
pnpm build
|
pnpm build
|
||||||
|
|
||||||
# Start development
|
# Run in development
|
||||||
pnpm start
|
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
|
## 🐛 Debugging
|
||||||
|
|
||||||
Enable debug mode for verbose logging:
|
Enable verbose logging for troubleshooting:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Enable debug mode
|
||||||
export TSPM_DEBUG=true
|
export TSPM_DEBUG=true
|
||||||
tspm list
|
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|id:N|name:LABEL>`
|
||||||
|
|
||||||
- Minimal CPU overhead (typically < 0.5%)
|
## 🎯 Targeting Processes (IDs and Names)
|
||||||
- Small memory footprint (~30-50MB for the daemon)
|
|
||||||
- Fast process startup and shutdown
|
|
||||||
- Efficient log buffering and rotation
|
|
||||||
|
|
||||||
## 🤝 Why TSPM?
|
Most process commands accept the following target formats:
|
||||||
|
|
||||||
Unlike general-purpose process managers, TSPM is built specifically for the TypeScript/Node.js ecosystem:
|
- Numeric ID: `tspm start 1`
|
||||||
|
- Explicit ID: `tspm start id:1`
|
||||||
|
- Explicit name: `tspm start name:api-server`
|
||||||
|
|
||||||
- **TypeScript First** - Written in TypeScript, for TypeScript projects
|
Notes:
|
||||||
- **ESM Native** - Full support for ES modules
|
- Names must be used with the `name:` prefix.
|
||||||
- **Developer Friendly** - Beautiful CLI output and helpful error messages
|
- If multiple processes share the same name, the CLI will report the ambiguous matches. Use `id:N` to disambiguate.
|
||||||
- **Production Ready** - Battle-tested memory management and error handling
|
- Use `tspm search <query>` to discover IDs by name or ID fragments.
|
||||||
- **No Configuration Required** - Sensible defaults that just work
|
|
||||||
- **Modern Architecture** - Async/await throughout, no callback hell
|
### `tspm search <query>`
|
||||||
|
|
||||||
|
Search processes by name or ID substring and print matching IDs (and names when available):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm search api
|
||||||
|
# Matches for "api":
|
||||||
|
# - id:3 name:api-server
|
||||||
|
```
|
||||||
|
|
||||||
|
- **"Memory limit exceeded"**: Increase limit with `tspm edit <id>`
|
||||||
|
|
||||||
|
## 🤝 Why Choose TSPM?
|
||||||
|
|
||||||
|
### TSPM vs PM2
|
||||||
|
|
||||||
|
| 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 |
|
||||||
|
|
||||||
|
### Perfect For
|
||||||
|
|
||||||
|
### Restart Backoff and Failure Handling
|
||||||
|
|
||||||
|
TSPM automatically restarts crashed processes with an incremental backoff:
|
||||||
|
|
||||||
|
- Debounce delay grows linearly from 1s up to 10s for consecutive retries.
|
||||||
|
- After the 10th retry, the process is marked as failed (status: "errored") and auto-restarts stop.
|
||||||
|
- The retry counter resets if no retry happens for 1 hour since the last attempt.
|
||||||
|
|
||||||
|
You can manually restart a failed process at any time:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tspm restart id:1
|
||||||
|
```
|
||||||
|
|
||||||
|
- 🚀 **Production Node.js apps** - Reliable process management
|
||||||
|
- 🔧 **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
|
## 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.
|
**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.
|
||||||
|
|
||||||
|
@@ -5,6 +5,7 @@ import * as fs from 'fs/promises';
|
|||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import { spawn } from 'child_process';
|
import { spawn } from 'child_process';
|
||||||
import { tspmIpcClient, TspmIpcClient } from '../ts/client/tspm.ipcclient.js';
|
import { tspmIpcClient, TspmIpcClient } from '../ts/client/tspm.ipcclient.js';
|
||||||
|
import { toProcessId } from '../ts/shared/protocol/id.js';
|
||||||
|
|
||||||
// Helper to ensure daemon is stopped before tests
|
// Helper to ensure daemon is stopped before tests
|
||||||
async function ensureDaemonStopped() {
|
async function ensureDaemonStopped() {
|
||||||
@@ -160,7 +161,7 @@ tap.test('Process management through daemon', async (tools) => {
|
|||||||
|
|
||||||
// Test 2: Start a test process
|
// Test 2: Start a test process
|
||||||
const testConfig: tspm.IProcessConfig = {
|
const testConfig: tspm.IProcessConfig = {
|
||||||
id: 'test-echo',
|
id: toProcessId(1001),
|
||||||
name: 'Test Echo Process',
|
name: 'Test Echo Process',
|
||||||
command: 'echo "Test process"',
|
command: 'echo "Test process"',
|
||||||
projectDir: process.cwd(),
|
projectDir: process.cwd(),
|
||||||
@@ -172,7 +173,7 @@ tap.test('Process management through daemon', async (tools) => {
|
|||||||
config: testConfig,
|
config: testConfig,
|
||||||
});
|
});
|
||||||
console.log('Start response:', startResponse);
|
console.log('Start response:', startResponse);
|
||||||
expect(startResponse.processId).toEqual('test-echo');
|
expect(startResponse.processId).toEqual(1001);
|
||||||
expect(startResponse.status).toBeDefined();
|
expect(startResponse.status).toBeDefined();
|
||||||
|
|
||||||
// Test 3: List processes (should have one process)
|
// Test 3: List processes (should have one process)
|
||||||
@@ -180,27 +181,27 @@ tap.test('Process management through daemon', async (tools) => {
|
|||||||
console.log('List after start:', listResponse);
|
console.log('List after start:', listResponse);
|
||||||
expect(listResponse.processes.length).toBeGreaterThanOrEqual(1);
|
expect(listResponse.processes.length).toBeGreaterThanOrEqual(1);
|
||||||
|
|
||||||
const procInfo = listResponse.processes.find((p) => p.id === 'test-echo');
|
const procInfo = listResponse.processes.find((p) => p.id === toProcessId(1001));
|
||||||
expect(procInfo).toBeDefined();
|
expect(procInfo).toBeDefined();
|
||||||
expect(procInfo?.id).toEqual('test-echo');
|
expect(procInfo?.id).toEqual(1001);
|
||||||
|
|
||||||
// Test 4: Describe the process
|
// Test 4: Describe the process
|
||||||
const describeResponse = await tspmIpcClient.request('describe', {
|
const describeResponse = await tspmIpcClient.request('describe', {
|
||||||
id: 'test-echo',
|
id: toProcessId(1001),
|
||||||
});
|
});
|
||||||
console.log('Describe:', describeResponse);
|
console.log('Describe:', describeResponse);
|
||||||
expect(describeResponse.processInfo).toBeDefined();
|
expect(describeResponse.processInfo).toBeDefined();
|
||||||
expect(describeResponse.config).toBeDefined();
|
expect(describeResponse.config).toBeDefined();
|
||||||
expect(describeResponse.config.id).toEqual('test-echo');
|
expect(describeResponse.config.id).toEqual(1001);
|
||||||
|
|
||||||
// Test 5: Stop the process
|
// Test 5: Stop the process
|
||||||
const stopResponse = await tspmIpcClient.request('stop', { id: 'test-echo' });
|
const stopResponse = await tspmIpcClient.request('stop', { id: toProcessId(1001) });
|
||||||
console.log('Stop response:', stopResponse);
|
console.log('Stop response:', stopResponse);
|
||||||
expect(stopResponse.success).toEqual(true);
|
expect(stopResponse.success).toEqual(true);
|
||||||
|
|
||||||
// Test 6: Delete the process
|
// Test 6: Delete the process
|
||||||
const deleteResponse = await tspmIpcClient.request('delete', {
|
const deleteResponse = await tspmIpcClient.request('delete', {
|
||||||
id: 'test-echo',
|
id: toProcessId(1001),
|
||||||
});
|
});
|
||||||
console.log('Delete response:', deleteResponse);
|
console.log('Delete response:', deleteResponse);
|
||||||
expect(deleteResponse.success).toEqual(true);
|
expect(deleteResponse.success).toEqual(true);
|
||||||
@@ -208,9 +209,7 @@ tap.test('Process management through daemon', async (tools) => {
|
|||||||
// Test 7: Verify process is gone
|
// Test 7: Verify process is gone
|
||||||
listResponse = await tspmIpcClient.request('list', {});
|
listResponse = await tspmIpcClient.request('list', {});
|
||||||
console.log('List after delete:', listResponse);
|
console.log('List after delete:', listResponse);
|
||||||
const deletedProcess = listResponse.processes.find(
|
const deletedProcess = listResponse.processes.find((p) => p.id === toProcessId(1001));
|
||||||
(p) => p.id === 'test-echo',
|
|
||||||
);
|
|
||||||
expect(deletedProcess).toBeUndefined();
|
expect(deletedProcess).toBeUndefined();
|
||||||
|
|
||||||
// Cleanup: stop daemon
|
// Cleanup: stop daemon
|
||||||
@@ -241,7 +240,7 @@ tap.test('Batch operations through daemon', async (tools) => {
|
|||||||
// Add multiple test processes
|
// Add multiple test processes
|
||||||
const testConfigs: tspm.IProcessConfig[] = [
|
const testConfigs: tspm.IProcessConfig[] = [
|
||||||
{
|
{
|
||||||
id: 'batch-test-1',
|
id: toProcessId(1101),
|
||||||
name: 'Batch Test 1',
|
name: 'Batch Test 1',
|
||||||
command: 'echo "Process 1"',
|
command: 'echo "Process 1"',
|
||||||
projectDir: process.cwd(),
|
projectDir: process.cwd(),
|
||||||
@@ -249,7 +248,7 @@ tap.test('Batch operations through daemon', async (tools) => {
|
|||||||
autorestart: false,
|
autorestart: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'batch-test-2',
|
id: toProcessId(1102),
|
||||||
name: 'Batch Test 2',
|
name: 'Batch Test 2',
|
||||||
command: 'echo "Process 2"',
|
command: 'echo "Process 2"',
|
||||||
projectDir: process.cwd(),
|
projectDir: process.cwd(),
|
||||||
@@ -308,7 +307,7 @@ tap.test('Daemon error handling', async (tools) => {
|
|||||||
|
|
||||||
// Test 1: Try to stop non-existent process
|
// Test 1: Try to stop non-existent process
|
||||||
try {
|
try {
|
||||||
await tspmIpcClient.request('stop', { id: 'non-existent-process' });
|
await tspmIpcClient.request('stop', { id: toProcessId(99999) });
|
||||||
expect(false).toEqual(true); // Should not reach here
|
expect(false).toEqual(true); // Should not reach here
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toInclude('Failed to stop process');
|
expect(error.message).toInclude('Failed to stop process');
|
||||||
@@ -316,7 +315,7 @@ tap.test('Daemon error handling', async (tools) => {
|
|||||||
|
|
||||||
// Test 2: Try to describe non-existent process
|
// Test 2: Try to describe non-existent process
|
||||||
try {
|
try {
|
||||||
await tspmIpcClient.request('describe', { id: 'non-existent-process' });
|
await tspmIpcClient.request('describe', { id: toProcessId(99999) });
|
||||||
expect(false).toEqual(true); // Should not reach here
|
expect(false).toEqual(true); // Should not reach here
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toInclude('not found');
|
expect(error.message).toInclude('not found');
|
||||||
@@ -324,7 +323,7 @@ tap.test('Daemon error handling', async (tools) => {
|
|||||||
|
|
||||||
// Test 3: Try to restart non-existent process
|
// Test 3: Try to restart non-existent process
|
||||||
try {
|
try {
|
||||||
await tspmIpcClient.request('restart', { id: 'non-existent-process' });
|
await tspmIpcClient.request('restart', { id: toProcessId(99999) });
|
||||||
expect(false).toEqual(true); // Should not reach here
|
expect(false).toEqual(true); // Should not reach here
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.message).toInclude('Failed to restart process');
|
expect(error.message).toInclude('Failed to restart process');
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||||
import * as tspm from '../ts/index.js';
|
import * as tspm from '../ts/index.js';
|
||||||
|
import { toProcessId } from '../ts/shared/protocol/id.js';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
// Basic module import test
|
// Basic module import test
|
||||||
@@ -51,7 +52,7 @@ async function exampleUsingIpcClient() {
|
|||||||
// Start a process using the request method
|
// Start a process using the request method
|
||||||
await client.request('start', {
|
await client.request('start', {
|
||||||
config: {
|
config: {
|
||||||
id: 'web-server',
|
id: toProcessId(2001),
|
||||||
name: 'Web Server',
|
name: 'Web Server',
|
||||||
projectDir: '/path/to/web/project',
|
projectDir: '/path/to/web/project',
|
||||||
command: 'npm run serve',
|
command: 'npm run serve',
|
||||||
@@ -65,7 +66,7 @@ async function exampleUsingIpcClient() {
|
|||||||
// Start another process
|
// Start another process
|
||||||
await client.request('start', {
|
await client.request('start', {
|
||||||
config: {
|
config: {
|
||||||
id: 'api-server',
|
id: toProcessId(2002),
|
||||||
name: 'API Server',
|
name: 'API Server',
|
||||||
projectDir: '/path/to/api/project',
|
projectDir: '/path/to/api/project',
|
||||||
command: 'npm run api',
|
command: 'npm run api',
|
||||||
@@ -80,13 +81,13 @@ async function exampleUsingIpcClient() {
|
|||||||
|
|
||||||
// Get logs from a process
|
// Get logs from a process
|
||||||
const logs = await client.request('getLogs', {
|
const logs = await client.request('getLogs', {
|
||||||
id: 'web-server',
|
id: toProcessId(2001),
|
||||||
lines: 20,
|
lines: 20,
|
||||||
});
|
});
|
||||||
console.log('Web server logs:', logs.logs);
|
console.log('Web server logs:', logs.logs);
|
||||||
|
|
||||||
// Stop a process
|
// Stop a process
|
||||||
await client.request('stop', { id: 'api-server' });
|
await client.request('stop', { id: toProcessId(2002) });
|
||||||
|
|
||||||
// Handle graceful shutdown
|
// Handle graceful shutdown
|
||||||
process.on('SIGINT', async () => {
|
process.on('SIGINT', async () => {
|
||||||
|
@@ -3,6 +3,6 @@
|
|||||||
*/
|
*/
|
||||||
export const commitinfo = {
|
export const commitinfo = {
|
||||||
name: '@git.zone/tspm',
|
name: '@git.zone/tspm',
|
||||||
version: '3.1.3',
|
version: '5.3.2',
|
||||||
description: 'a no fuzz process manager'
|
description: 'a no fuzz process manager'
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import * as paths from '../../../paths.js';
|
import * as paths from '../../../paths.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
||||||
@@ -33,7 +33,8 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
const daemonScript = plugins.path.join(
|
const daemonScript = plugins.path.join(
|
||||||
paths.packageDir,
|
paths.packageDir,
|
||||||
'dist_ts',
|
'dist_ts',
|
||||||
'daemon.js',
|
'daemon',
|
||||||
|
'tspm.daemon.js',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Start daemon as a detached background process
|
// Start daemon as a detached background process
|
||||||
@@ -80,6 +81,48 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'restart':
|
||||||
|
try {
|
||||||
|
console.log('Restarting TSPM daemon...');
|
||||||
|
await tspmIpcClient.stopDaemon(true);
|
||||||
|
|
||||||
|
// Reuse the manual start logic from 'start'
|
||||||
|
const statusAfterStop = await tspmIpcClient.getDaemonStatus();
|
||||||
|
if (statusAfterStop) {
|
||||||
|
console.warn('Daemon still appears to be running; proceeding to start anyway.');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Starting TSPM daemon manually...');
|
||||||
|
const { spawn } = await import('child_process');
|
||||||
|
const daemonScript = plugins.path.join(
|
||||||
|
paths.packageDir,
|
||||||
|
'dist_ts',
|
||||||
|
'daemon.js',
|
||||||
|
);
|
||||||
|
const daemonProcess = spawn(process.execPath, [daemonScript], {
|
||||||
|
detached: true,
|
||||||
|
stdio: process.env.TSPM_DEBUG === 'true' ? 'inherit' : 'ignore',
|
||||||
|
env: { ...process.env, TSPM_DAEMON_MODE: 'true' },
|
||||||
|
});
|
||||||
|
daemonProcess.unref();
|
||||||
|
console.log(`Started daemon with PID: ${daemonProcess.pid}`);
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||||
|
const newStatus = await tspmIpcClient.getDaemonStatus();
|
||||||
|
if (newStatus) {
|
||||||
|
console.log('✓ TSPM daemon restarted successfully');
|
||||||
|
console.log(` PID: ${newStatus.pid}`);
|
||||||
|
} else {
|
||||||
|
console.warn('\n⚠️ Warning: Daemon restart attempted but status is unavailable.');
|
||||||
|
}
|
||||||
|
|
||||||
|
await tspmIpcClient.disconnect();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restarting daemon:', (error as any).message || String(error));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case 'start-service':
|
case 'start-service':
|
||||||
// This is called by systemd - start the daemon directly
|
// This is called by systemd - start the daemon directly
|
||||||
console.log('Starting TSPM daemon for systemd service...');
|
console.log('Starting TSPM daemon for systemd service...');
|
||||||
@@ -135,6 +178,7 @@ export function registerDaemonCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
console.log('Usage: tspm daemon <command>');
|
console.log('Usage: tspm daemon <command>');
|
||||||
console.log('\nCommands:');
|
console.log('\nCommands:');
|
||||||
console.log(' start Start the TSPM daemon');
|
console.log(' start Start the TSPM daemon');
|
||||||
|
console.log(' restart Restart the TSPM daemon');
|
||||||
console.log(' stop Stop the TSPM daemon');
|
console.log(' stop Stop the TSPM daemon');
|
||||||
console.log(' status Show daemon status');
|
console.log(' status Show daemon status');
|
||||||
break;
|
break;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as paths from '../../paths.js';
|
import * as paths from '../../paths.js';
|
||||||
import { tspmIpcClient } from '../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../client/tspm.ipcclient.js';
|
||||||
import { Logger } from '../../shared/common/utils.errorhandler.js';
|
import { Logger } from '../../shared/common/utils.errorhandler.js';
|
||||||
@@ -22,13 +22,14 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
);
|
);
|
||||||
console.log(' disable Disable TSPM system service');
|
console.log(' disable Disable TSPM system service');
|
||||||
console.log('\nProcess Commands:');
|
console.log('\nProcess Commands:');
|
||||||
console.log(' start <script> Start a process');
|
console.log(' start <id|id:N|name:LBL> Start a process');
|
||||||
console.log(' list List all processes');
|
console.log(' list List all processes');
|
||||||
console.log(' stop <id> Stop a process');
|
console.log(' stop <id|id:N|name:LBL> Stop a process');
|
||||||
console.log(' restart <id> Restart a process');
|
console.log(' restart <id|id:N|name:LBL> Restart a process');
|
||||||
console.log(' delete <id> Delete a process');
|
console.log(' delete <id|id:N|name:LBL> Delete a process');
|
||||||
console.log(' describe <id> Show details for a process');
|
console.log(' describe <id|id:N|name:LBL> Show details for a process');
|
||||||
console.log(' logs <id> Show logs for a process');
|
console.log(' logs <id|id:N|name:LBL> Show logs for a process');
|
||||||
|
console.log(' search <query> Find processes by id/name');
|
||||||
console.log(' start-all Start all saved processes');
|
console.log(' start-all Start all saved processes');
|
||||||
console.log(' stop-all Stop all processes');
|
console.log(' stop-all Stop all processes');
|
||||||
console.log(' restart-all Restart all processes');
|
console.log(' restart-all Restart all processes');
|
||||||
@@ -74,7 +75,7 @@ export function registerDefaultCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
const resetColor = '\x1b[0m';
|
const resetColor = '\x1b[0m';
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad(formatMemory(proc.memory), 9)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
`│ ${pad(String(proc.id), 7)} │ ${pad(String(proc.id), 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad(formatMemory(proc.memory), 9)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
118
ts/cli/commands/process/add.ts
Normal file
118
ts/cli/commands/process/add.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
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';
|
||||||
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
|
||||||
|
export function registerAddCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||||
|
registerIpcCommand(
|
||||||
|
smartcli,
|
||||||
|
'add',
|
||||||
|
async (argvArg: CliArguments) => {
|
||||||
|
const args = argvArg._.slice(1);
|
||||||
|
if (args.length === 0) {
|
||||||
|
console.error('Error: Please provide a command or .ts file');
|
||||||
|
console.log('Usage: tspm add <command|file.ts> [options]');
|
||||||
|
console.log('\nOptions:');
|
||||||
|
console.log(' --name <name> Optional name');
|
||||||
|
console.log(' --memory <size> Memory limit (e.g., 512MB, 2GB)');
|
||||||
|
console.log(' --cwd <path> Working directory');
|
||||||
|
console.log(' --watch Watch for file changes');
|
||||||
|
console.log(' --watch-paths <paths> Comma-separated paths');
|
||||||
|
console.log(' --autorestart Auto-restart on crash (default true)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const script = args.join(' ');
|
||||||
|
const projectDir = argvArg.cwd || process.cwd();
|
||||||
|
const memoryLimit = argvArg.memory
|
||||||
|
? parseMemoryString(argvArg.memory)
|
||||||
|
: 512 * 1024 * 1024;
|
||||||
|
|
||||||
|
// Resolve .ts single-file execution via tsx if needed
|
||||||
|
const parts = script.split(' ');
|
||||||
|
const first = parts[0];
|
||||||
|
let command = script;
|
||||||
|
let cmdArgs: string[] | undefined;
|
||||||
|
if (parts.length === 1 && first.endsWith('.ts')) {
|
||||||
|
try {
|
||||||
|
const { createRequire } = await import('module');
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const tsxPath = require.resolve('tsx/dist/cli.mjs');
|
||||||
|
const filePath = plugins.path.isAbsolute(first)
|
||||||
|
? first
|
||||||
|
: plugins.path.join(projectDir, first);
|
||||||
|
command = tsxPath;
|
||||||
|
cmdArgs = [filePath];
|
||||||
|
} catch {
|
||||||
|
command = 'tsx';
|
||||||
|
cmdArgs = [first];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = argvArg.name || script;
|
||||||
|
const watch = argvArg.watch || false;
|
||||||
|
const autorestart = argvArg.autorestart !== false;
|
||||||
|
const watchPaths = argvArg.watchPaths
|
||||||
|
? typeof argvArg.watchPaths === 'string'
|
||||||
|
? (argvArg.watchPaths as string).split(',')
|
||||||
|
: argvArg.watchPaths
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
console.log('Adding process configuration:');
|
||||||
|
console.log(` Command: ${script}${parts.length === 1 && first.endsWith('.ts') ? ' (via tsx)' : ''}`);
|
||||||
|
console.log(` Directory: ${projectDir}`);
|
||||||
|
console.log(` Memory limit: ${formatMemory(memoryLimit)}`);
|
||||||
|
console.log(` Auto-restart: ${autorestart}`);
|
||||||
|
if (watch) {
|
||||||
|
console.log(` Watch: enabled`);
|
||||||
|
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,
|
||||||
|
command,
|
||||||
|
args: cmdArgs,
|
||||||
|
projectDir,
|
||||||
|
memoryLimitBytes: memoryLimit,
|
||||||
|
env: essentialEnvVars,
|
||||||
|
autorestart,
|
||||||
|
watch,
|
||||||
|
watchPaths,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✓ Added');
|
||||||
|
console.log(` Assigned ID: ${response.id}`);
|
||||||
|
},
|
||||||
|
{ actionLabel: 'add process config' },
|
||||||
|
);
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
@@ -6,24 +6,29 @@ import { registerIpcCommand } from '../../registration/index.js';
|
|||||||
export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
|
export function registerDeleteCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||||
registerIpcCommand(
|
registerIpcCommand(
|
||||||
smartcli,
|
smartcli,
|
||||||
'delete',
|
['delete', 'remove'],
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm delete <id>');
|
console.log('Usage: tspm delete <id|id:N|name:LABEL> | tspm remove <id|id:N|name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Deleting process: ${id}`);
|
// Determine if command was 'remove' to use the new IPC route, otherwise 'delete'
|
||||||
const response = await tspmIpcClient.request('delete', { id });
|
const cmd = String(argvArg._[0]);
|
||||||
|
const isRemoveAlias = cmd === 'remove';
|
||||||
|
console.log(`${isRemoveAlias ? 'Removing' : 'Deleting'} process: ${target}`);
|
||||||
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
// Always call daemon 'delete'; 'remove' is CLI alias only
|
||||||
|
const response = await tspmIpcClient.request('delete', { id: resolved.id } as any);
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log(`✓ ${response.message}`);
|
console.log(`✓ ${response.message || (isRemoveAlias ? 'Removed successfully' : 'Deleted successfully')}`);
|
||||||
} else {
|
} else {
|
||||||
console.error(`✗ Failed to delete process: ${response.message}`);
|
console.error(`✗ Failed to ${isRemoveAlias ? 'remove' : 'delete'} process: ${response.message}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ actionLabel: 'delete process' },
|
{ actionLabel: 'delete/remove process' },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
@@ -9,16 +9,17 @@ export function registerDescribeCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'describe',
|
'describe',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm describe <id>');
|
console.log('Usage: tspm describe <id | id:N | name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await tspmIpcClient.request('describe', { id });
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const response = await tspmIpcClient.request('describe', { id: resolved.id });
|
||||||
|
|
||||||
console.log(`Process Details: ${id}`);
|
console.log(`Process Details: ${response.config.name || resolved.id}`);
|
||||||
console.log('─'.repeat(40));
|
console.log('─'.repeat(40));
|
||||||
console.log(`Status: ${response.processInfo.status}`);
|
console.log(`Status: ${response.processInfo.status}`);
|
||||||
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
|
console.log(`PID: ${response.processInfo.pid || 'N/A'}`);
|
||||||
|
74
ts/cli/commands/process/edit.ts
Normal file
74
ts/cli/commands/process/edit.ts
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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 target = argvArg._[1];
|
||||||
|
if (!target) {
|
||||||
|
console.error('Error: Please provide a process target to edit');
|
||||||
|
console.log('Usage: tspm edit <id | id:N | name:LABEL>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve and load current config
|
||||||
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const { config } = await tspmIpcClient.request('describe', { id: resolved.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: resolved.id,
|
||||||
|
updates,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('✓ Environment variables updated');
|
||||||
|
console.log(' Process configuration updated successfully');
|
||||||
|
},
|
||||||
|
{ actionLabel: 'edit process config' },
|
||||||
|
);
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
@@ -39,7 +39,7 @@ export function registerListCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
const resetColor = '\x1b[0m';
|
const resetColor = '\x1b[0m';
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`│ ${pad(proc.id, 7)} │ ${pad(proc.id, 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad((proc.pid || '-').toString(), 9)} │ ${pad(formatMemory(proc.memory), 8)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
`│ ${pad(String(proc.id), 7)} │ ${pad(String(proc.id), 11)} │ ${statusColor}${pad(proc.status, 9)}${resetColor} │ ${pad((proc.pid || '-').toString(), 9)} │ ${pad(formatMemory(proc.memory), 8)} │ ${pad(proc.restarts.toString(), 8)} │`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
@@ -11,10 +11,10 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'logs',
|
'logs',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm logs <id> [options]');
|
console.log('Usage: tspm logs <id | id:N | name:LABEL> [options]');
|
||||||
console.log('\nOptions:');
|
console.log('\nOptions:');
|
||||||
console.log(' --lines <n> Number of lines to show (default: 50)');
|
console.log(' --lines <n> Number of lines to show (default: 50)');
|
||||||
console.log(' --follow Stream logs in real-time (like tail -f)');
|
console.log(' --follow Stream logs in real-time (like tail -f)');
|
||||||
@@ -24,6 +24,8 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
const lines = getNumber(argvArg, 'lines', 50);
|
const lines = getNumber(argvArg, 'lines', 50);
|
||||||
const follow = getBool(argvArg, 'follow', 'f');
|
const follow = getBool(argvArg, 'follow', 'f');
|
||||||
|
|
||||||
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const id = resolved.id;
|
||||||
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
const response = await tspmIpcClient.request('getLogs', { id, lines });
|
||||||
|
|
||||||
if (!follow) {
|
if (!follow) {
|
||||||
@@ -44,7 +46,7 @@ export function registerLogsCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Streaming mode
|
// Streaming mode
|
||||||
console.log(`Logs for process: ${id} (streaming...)`);
|
console.log(`Logs for process: ${resolved.name || id} (streaming...)`);
|
||||||
console.log('─'.repeat(60));
|
console.log('─'.repeat(60));
|
||||||
|
|
||||||
let lastSeq = 0;
|
let lastSeq = 0;
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
@@ -8,18 +8,37 @@ export function registerRestartCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'restart',
|
'restart',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const arg = argvArg._[1];
|
||||||
if (!id) {
|
if (!arg) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target or "all"');
|
||||||
console.log('Usage: tspm restart <id>');
|
console.log('Usage:');
|
||||||
|
console.log(' tspm restart <id | id:N | name:LABEL>');
|
||||||
|
console.log(' tspm restart all');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Restarting process: ${id}`);
|
if (String(arg).toLowerCase() === 'all') {
|
||||||
const response = await tspmIpcClient.request('restart', { id });
|
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 target = String(arg);
|
||||||
|
console.log(`Restarting process: ${target}`);
|
||||||
|
const resolved = await tspmIpcClient.request('resolveTarget', { target });
|
||||||
|
const response = await tspmIpcClient.request('restart', { id: resolved.id });
|
||||||
|
|
||||||
console.log(`✓ Process restarted successfully`);
|
console.log(`✓ Process restarted successfully`);
|
||||||
console.log(` ID: ${response.processId}`);
|
console.log(` ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
|
||||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||||
console.log(` Status: ${response.status}`);
|
console.log(` Status: ${response.status}`);
|
||||||
},
|
},
|
||||||
|
62
ts/cli/commands/process/search.ts
Normal file
62
ts/cli/commands/process/search.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import * as plugins from '../../plugins.js';
|
||||||
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
|
import type { CliArguments } from '../../types.js';
|
||||||
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
|
|
||||||
|
export function registerSearchCommand(smartcli: plugins.smartcli.Smartcli) {
|
||||||
|
registerIpcCommand(
|
||||||
|
smartcli,
|
||||||
|
'search',
|
||||||
|
async (argvArg: CliArguments) => {
|
||||||
|
const query = String(argvArg._[1] || '').trim();
|
||||||
|
if (!query) {
|
||||||
|
console.error('Error: Please provide a search query');
|
||||||
|
console.log('Usage: tspm search <name-fragment | id-fragment>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch list of processes, then enrich with names via describe
|
||||||
|
const listRes = await tspmIpcClient.request('list', {});
|
||||||
|
const processes = listRes.processes;
|
||||||
|
|
||||||
|
// If there are no processes, short-circuit
|
||||||
|
if (processes.length === 0) {
|
||||||
|
console.log('No processes found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lowerQ = query.toLowerCase();
|
||||||
|
const matches: Array<{ id: number; name?: string }> = [];
|
||||||
|
|
||||||
|
// Collect describe calls to obtain names
|
||||||
|
for (const proc of processes) {
|
||||||
|
try {
|
||||||
|
const desc = await tspmIpcClient.request('describe', { id: proc.id });
|
||||||
|
const name = desc.config.name || '';
|
||||||
|
const idStr = String(proc.id);
|
||||||
|
if (name.toLowerCase().includes(lowerQ) || idStr.includes(query)) {
|
||||||
|
matches.push({ id: proc.id, name });
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore describe errors for individual processes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
console.log(`No matches for "${query}"`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Matches for "${query}":`);
|
||||||
|
for (const m of matches) {
|
||||||
|
if (m.name) {
|
||||||
|
console.log(`- id:${m.id}\tname:${m.name}`);
|
||||||
|
} else {
|
||||||
|
console.log(`- id:${m.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ actionLabel: 'search processes' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import type { IProcessConfig } from '../../../shared/protocol/ipc.types.js';
|
import type { IProcessConfig } from '../../../shared/protocol/ipc.types.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
@@ -10,109 +10,18 @@ export function registerStartCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'start',
|
'start',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
// Get all arguments after 'start' command
|
const target = argvArg._[1];
|
||||||
const commandArgs = argvArg._.slice(1);
|
if (!target) {
|
||||||
if (commandArgs.length === 0) {
|
console.error('Error: Please provide a process target to start');
|
||||||
console.error('Error: Please provide a command to run');
|
console.log('Usage: tspm start <id | id:N | name:LABEL>');
|
||||||
console.log('Usage: tspm start <command> [options]');
|
|
||||||
console.log('\nExamples:');
|
|
||||||
console.log(' tspm start "npm run dev"');
|
|
||||||
console.log(' tspm start pnpm start');
|
|
||||||
console.log(' tspm start node server.js');
|
|
||||||
console.log(' tspm start script.ts');
|
|
||||||
console.log('\nOptions:');
|
|
||||||
console.log(' --name <name> Name for the process');
|
|
||||||
console.log(
|
|
||||||
' --memory <size> Memory limit (e.g., "512MB", "2GB")',
|
|
||||||
);
|
|
||||||
console.log(' --cwd <path> Working directory');
|
|
||||||
console.log(
|
|
||||||
' --watch Watch for file changes and restart',
|
|
||||||
);
|
|
||||||
console.log(' --watch-paths <paths> Comma-separated paths to watch');
|
|
||||||
console.log(' --autorestart Auto-restart on crash');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Join all command parts to form the full command
|
|
||||||
const script = commandArgs.join(' ');
|
|
||||||
|
|
||||||
const memoryLimit = argvArg.memory
|
console.log(`Starting process: ${target}...`);
|
||||||
? parseMemoryString(argvArg.memory)
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
: 512 * 1024 * 1024;
|
const response = await tspmIpcClient.request('startById', { id: resolved.id });
|
||||||
const projectDir = argvArg.cwd || process.cwd();
|
console.log('✓ Process started');
|
||||||
|
console.log(` ID: ${response.processId}${resolved.name ? ` (name: ${resolved.name})` : ''}`);
|
||||||
// Parse the command to determine if we need to handle .ts files
|
|
||||||
let actualCommand: string;
|
|
||||||
let processArgs: string[] | undefined = undefined;
|
|
||||||
|
|
||||||
// Split the script to check if it's a single .ts file or a full command
|
|
||||||
const scriptParts = script.split(' ');
|
|
||||||
const firstPart = scriptParts[0];
|
|
||||||
|
|
||||||
// Check if this is a direct .ts file execution (single argument ending in .ts)
|
|
||||||
if (scriptParts.length === 1 && firstPart.endsWith('.ts')) {
|
|
||||||
try {
|
|
||||||
const tsxPath = await (async () => {
|
|
||||||
const { createRequire } = await import('module');
|
|
||||||
const require = createRequire(import.meta.url);
|
|
||||||
return require.resolve('tsx/dist/cli.mjs');
|
|
||||||
})();
|
|
||||||
|
|
||||||
const scriptPath = plugins.path.isAbsolute(firstPart)
|
|
||||||
? firstPart
|
|
||||||
: plugins.path.join(projectDir, firstPart);
|
|
||||||
actualCommand = tsxPath;
|
|
||||||
processArgs = [scriptPath];
|
|
||||||
} catch {
|
|
||||||
actualCommand = 'tsx';
|
|
||||||
processArgs = [firstPart];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// For multi-word commands, use the entire script as the command
|
|
||||||
// This handles cases like "pnpm start", "npm run dev", etc.
|
|
||||||
actualCommand = script;
|
|
||||||
processArgs = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = argvArg.name || script;
|
|
||||||
const watch = argvArg.watch || false;
|
|
||||||
const autorestart = argvArg.autorestart !== false; // default true
|
|
||||||
const watchPaths = argvArg.watchPaths
|
|
||||||
? typeof argvArg.watchPaths === 'string'
|
|
||||||
? (argvArg.watchPaths as string).split(',')
|
|
||||||
: argvArg.watchPaths
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
const processConfig: IProcessConfig = {
|
|
||||||
id: name.replace(/[^a-zA-Z0-9-_]/g, '_'),
|
|
||||||
name,
|
|
||||||
command: actualCommand,
|
|
||||||
args: processArgs,
|
|
||||||
projectDir,
|
|
||||||
memoryLimitBytes: memoryLimit,
|
|
||||||
autorestart,
|
|
||||||
watch,
|
|
||||||
watchPaths,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(`Starting process: ${name}`);
|
|
||||||
console.log(
|
|
||||||
` Command: ${script}${scriptParts.length === 1 && firstPart.endsWith('.ts') ? ' (via tsx)' : ''}`,
|
|
||||||
);
|
|
||||||
console.log(` Directory: ${projectDir}`);
|
|
||||||
console.log(` Memory limit: ${formatMemory(memoryLimit)}`);
|
|
||||||
console.log(` Auto-restart: ${autorestart}`);
|
|
||||||
if (watch) {
|
|
||||||
console.log(` Watch mode: enabled`);
|
|
||||||
if (watchPaths) console.log(` Watch paths: ${watchPaths.join(', ')}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await tspmIpcClient.request('start', {
|
|
||||||
config: processConfig,
|
|
||||||
});
|
|
||||||
console.log(`✓ Process started successfully`);
|
|
||||||
console.log(` ID: ${response.processId}`);
|
|
||||||
console.log(` PID: ${response.pid || 'N/A'}`);
|
console.log(` PID: ${response.pid || 'N/A'}`);
|
||||||
console.log(` Status: ${response.status}`);
|
console.log(` Status: ${response.status}`);
|
||||||
},
|
},
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
import { tspmIpcClient } from '../../../client/tspm.ipcclient.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
import { registerIpcCommand } from '../../registration/index.js';
|
import { registerIpcCommand } from '../../registration/index.js';
|
||||||
@@ -8,15 +8,16 @@ export function registerStopCommand(smartcli: plugins.smartcli.Smartcli) {
|
|||||||
smartcli,
|
smartcli,
|
||||||
'stop',
|
'stop',
|
||||||
async (argvArg: CliArguments) => {
|
async (argvArg: CliArguments) => {
|
||||||
const id = argvArg._[1];
|
const target = argvArg._[1];
|
||||||
if (!id) {
|
if (!target) {
|
||||||
console.error('Error: Please provide a process ID');
|
console.error('Error: Please provide a process target');
|
||||||
console.log('Usage: tspm stop <id>');
|
console.log('Usage: tspm stop <id | id:N | name:LABEL>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Stopping process: ${id}`);
|
console.log(`Stopping process: ${target}`);
|
||||||
const response = await tspmIpcClient.request('stop', { id });
|
const resolved = await tspmIpcClient.request('resolveTarget', { target: String(target) });
|
||||||
|
const response = await tspmIpcClient.request('stop', { id: resolved.id });
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
console.log(`✓ ${response.message}`);
|
console.log(`✓ ${response.message}`);
|
||||||
|
33
ts/cli/commands/reset.ts
Normal file
33
ts/cli/commands/reset.ts
Normal 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' },
|
||||||
|
);
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
|
import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
|
||||||
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../../plugins.js';
|
import * as plugins from '../../plugins.js';
|
||||||
import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
|
import { TspmServiceManager } from '../../../client/tspm.servicemanager.js';
|
||||||
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
import { Logger } from '../../../shared/common/utils.errorhandler.js';
|
||||||
import type { CliArguments } from '../../types.js';
|
import type { CliArguments } from '../../types.js';
|
||||||
|
@@ -1,22 +1,27 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
|
import { tspmIpcClient } from '../client/tspm.ipcclient.js';
|
||||||
import * as paths from '../paths.js';
|
import * as paths from '../paths.js';
|
||||||
import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js';
|
import { Logger, LogLevel } from '../shared/common/utils.errorhandler.js';
|
||||||
|
|
||||||
// Import command registration functions
|
// Import command registration functions
|
||||||
import { registerDefaultCommand } from './commands/default.js';
|
import { registerDefaultCommand } from './commands/default.js';
|
||||||
import { registerStartCommand } from './commands/process/start.js';
|
import { registerStartCommand } from './commands/process/start.js';
|
||||||
|
import { registerAddCommand } from './commands/process/add.js';
|
||||||
import { registerStopCommand } from './commands/process/stop.js';
|
import { registerStopCommand } from './commands/process/stop.js';
|
||||||
import { registerRestartCommand } from './commands/process/restart.js';
|
import { registerRestartCommand } from './commands/process/restart.js';
|
||||||
import { registerDeleteCommand } from './commands/process/delete.js';
|
import { registerDeleteCommand } from './commands/process/delete.js';
|
||||||
|
import { registerSearchCommand } from './commands/process/search.js';
|
||||||
import { registerListCommand } from './commands/process/list.js';
|
import { registerListCommand } from './commands/process/list.js';
|
||||||
import { registerDescribeCommand } from './commands/process/describe.js';
|
import { registerDescribeCommand } from './commands/process/describe.js';
|
||||||
import { registerLogsCommand } from './commands/process/logs.js';
|
import { registerLogsCommand } from './commands/process/logs.js';
|
||||||
|
import { registerEditCommand } from './commands/process/edit.js';
|
||||||
import { registerStartAllCommand } from './commands/batch/start-all.js';
|
import { registerStartAllCommand } from './commands/batch/start-all.js';
|
||||||
import { registerStopAllCommand } from './commands/batch/stop-all.js';
|
import { registerStopAllCommand } from './commands/batch/stop-all.js';
|
||||||
import { registerRestartAllCommand } from './commands/batch/restart-all.js';
|
import { registerRestartAllCommand } from './commands/batch/restart-all.js';
|
||||||
import { registerDaemonCommand } from './commands/daemon/index.js';
|
import { registerDaemonCommand } from './commands/daemon/index.js';
|
||||||
import { registerEnableCommand } from './commands/service/enable.js';
|
import { registerEnableCommand } from './commands/service/enable.js';
|
||||||
import { registerDisableCommand } from './commands/service/disable.js';
|
import { registerDisableCommand } from './commands/service/disable.js';
|
||||||
|
import { registerResetCommand } from './commands/reset.js';
|
||||||
|
|
||||||
// Export types for external use
|
// Export types for external use
|
||||||
export type { CliArguments } from './types.js';
|
export type { CliArguments } from './types.js';
|
||||||
@@ -36,6 +41,24 @@ export const run = async (): Promise<void> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const smartcliInstance = new plugins.smartcli.Smartcli();
|
const smartcliInstance = new plugins.smartcli.Smartcli();
|
||||||
|
// Intercept -v/--version to show CLI and daemon versions
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args.includes('-v') || args.includes('--version')) {
|
||||||
|
const cliVersion = tspmProjectinfo.npm.version;
|
||||||
|
console.log(`tspm CLI: ${cliVersion}`);
|
||||||
|
const status = await tspmIpcClient.getDaemonStatus();
|
||||||
|
if (status) {
|
||||||
|
console.log(
|
||||||
|
`Daemon: running v${status.version || 'unknown'} (pid ${status.pid})`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('Daemon: not running');
|
||||||
|
}
|
||||||
|
// Ensure we disconnect any IPC client connection used for status
|
||||||
|
try { await tspmIpcClient.disconnect(); } catch {}
|
||||||
|
return; // do not start parser
|
||||||
|
}
|
||||||
|
// Keep Smartcli version info for help output but not used for -v now
|
||||||
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
|
smartcliInstance.addVersion(tspmProjectinfo.npm.version);
|
||||||
|
|
||||||
// Register all commands
|
// Register all commands
|
||||||
@@ -43,6 +66,7 @@ export const run = async (): Promise<void> => {
|
|||||||
registerDefaultCommand(smartcliInstance);
|
registerDefaultCommand(smartcliInstance);
|
||||||
|
|
||||||
// Process commands
|
// Process commands
|
||||||
|
registerAddCommand(smartcliInstance);
|
||||||
registerStartCommand(smartcliInstance);
|
registerStartCommand(smartcliInstance);
|
||||||
registerStopCommand(smartcliInstance);
|
registerStopCommand(smartcliInstance);
|
||||||
registerRestartCommand(smartcliInstance);
|
registerRestartCommand(smartcliInstance);
|
||||||
@@ -50,6 +74,8 @@ export const run = async (): Promise<void> => {
|
|||||||
registerListCommand(smartcliInstance);
|
registerListCommand(smartcliInstance);
|
||||||
registerDescribeCommand(smartcliInstance);
|
registerDescribeCommand(smartcliInstance);
|
||||||
registerLogsCommand(smartcliInstance);
|
registerLogsCommand(smartcliInstance);
|
||||||
|
registerEditCommand(smartcliInstance);
|
||||||
|
registerSearchCommand(smartcliInstance);
|
||||||
|
|
||||||
// Batch commands
|
// Batch commands
|
||||||
registerStartAllCommand(smartcliInstance);
|
registerStartAllCommand(smartcliInstance);
|
||||||
@@ -63,6 +89,9 @@ export const run = async (): Promise<void> => {
|
|||||||
registerEnableCommand(smartcliInstance);
|
registerEnableCommand(smartcliInstance);
|
||||||
registerDisableCommand(smartcliInstance);
|
registerDisableCommand(smartcliInstance);
|
||||||
|
|
||||||
|
// Maintenance commands
|
||||||
|
registerResetCommand(smartcliInstance);
|
||||||
|
|
||||||
// Start parsing commands
|
// Start parsing commands
|
||||||
smartcliInstance.startParse();
|
smartcliInstance.startParse();
|
||||||
};
|
};
|
||||||
|
8
ts/cli/plugins.ts
Normal file
8
ts/cli/plugins.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Minimal plugin set for the CLI to keep startup light
|
||||||
|
import * as path from 'node:path';
|
||||||
|
import * as projectinfo from '@push.rocks/projectinfo';
|
||||||
|
import * as smartcli from '@push.rocks/smartcli';
|
||||||
|
import * as smartinteract from '@push.rocks/smartinteract';
|
||||||
|
|
||||||
|
export { path, projectinfo, smartcli, smartinteract };
|
||||||
|
|
@@ -1,4 +1,4 @@
|
|||||||
import * as plugins from '../../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import type {
|
import type {
|
||||||
CliArguments,
|
CliArguments,
|
||||||
CommandAction,
|
CommandAction,
|
||||||
@@ -17,53 +17,56 @@ import { ensureDaemonOrHint } from './daemon-check.js';
|
|||||||
*/
|
*/
|
||||||
export function registerIpcCommand(
|
export function registerIpcCommand(
|
||||||
smartcli: plugins.smartcli.Smartcli,
|
smartcli: plugins.smartcli.Smartcli,
|
||||||
name: string,
|
name: string | string[],
|
||||||
action: CommandAction,
|
action: CommandAction,
|
||||||
opts: IpcCommandOptions = {},
|
opts: IpcCommandOptions = {},
|
||||||
) {
|
) {
|
||||||
const { actionLabel = name, keepAlive = false, requireDaemon = true } = opts;
|
const names = Array.isArray(name) ? name : [name];
|
||||||
|
for (const singleName of names) {
|
||||||
|
const { actionLabel = singleName, keepAlive = false, requireDaemon = true } = opts;
|
||||||
|
|
||||||
smartcli.addCommand(name).subscribe({
|
smartcli.addCommand(singleName).subscribe({
|
||||||
next: async (argv: CliArguments) => {
|
next: async (argv: CliArguments) => {
|
||||||
// Early preflight for better UX
|
// Early preflight for better UX
|
||||||
const ok = await ensureDaemonOrHint(requireDaemon, actionLabel);
|
const ok = await ensureDaemonOrHint(requireDaemon, actionLabel);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate keepAlive - can be boolean or function
|
|
||||||
const shouldKeepAlive =
|
|
||||||
typeof keepAlive === 'function' ? keepAlive(argv) : keepAlive;
|
|
||||||
|
|
||||||
if (shouldKeepAlive) {
|
|
||||||
// Let action manage its own connection/cleanup lifecycle
|
|
||||||
try {
|
|
||||||
await action(argv);
|
|
||||||
} catch (error) {
|
|
||||||
handleDaemonError(error, actionLabel);
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Auto-disconnect pattern for one-shot IPC commands
|
// Evaluate keepAlive - can be boolean or function
|
||||||
await runIpcCommand(async () => {
|
const shouldKeepAlive =
|
||||||
|
typeof keepAlive === 'function' ? keepAlive(argv) : keepAlive;
|
||||||
|
|
||||||
|
if (shouldKeepAlive) {
|
||||||
|
// Let action manage its own connection/cleanup lifecycle
|
||||||
try {
|
try {
|
||||||
await action(argv);
|
await action(argv);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleDaemonError(error, actionLabel);
|
handleDaemonError(error, actionLabel);
|
||||||
}
|
}
|
||||||
});
|
} else {
|
||||||
}
|
// Auto-disconnect pattern for one-shot IPC commands
|
||||||
},
|
await runIpcCommand(async () => {
|
||||||
error: (err) => {
|
try {
|
||||||
// Fallback error path (should be rare with try/catch in next)
|
await action(argv);
|
||||||
console.error(
|
} catch (error) {
|
||||||
`Unexpected error in command "${name}":`,
|
handleDaemonError(error, actionLabel);
|
||||||
unknownError(err),
|
}
|
||||||
);
|
});
|
||||||
process.exit(1);
|
}
|
||||||
},
|
},
|
||||||
complete: () => {},
|
error: (err) => {
|
||||||
});
|
// Fallback error path (should be rare with try/catch in next)
|
||||||
|
console.error(
|
||||||
|
`Unexpected error in command "${singleName}":`,
|
||||||
|
unknownError(err),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
},
|
||||||
|
complete: () => {},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
6
ts/client/plugins.ts
Normal file
6
ts/client/plugins.ts
Normal 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 };
|
||||||
|
|
@@ -1,5 +1,7 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from './plugins.js';
|
||||||
import * as paths from '../paths.js';
|
import * as paths from '../paths.js';
|
||||||
|
import { toProcessId } from '../shared/protocol/id.js';
|
||||||
|
import type { ProcessId } from '../shared/protocol/id.js';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IpcMethodMap,
|
IpcMethodMap,
|
||||||
@@ -144,27 +146,32 @@ export class TspmIpcClient {
|
|||||||
* Subscribe to log updates for a specific process
|
* Subscribe to log updates for a specific process
|
||||||
*/
|
*/
|
||||||
public async subscribe(
|
public async subscribe(
|
||||||
processId: string,
|
processId: ProcessId | number | string,
|
||||||
handler: (log: any) => void,
|
handler: (log: any) => void,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.ipcClient || !this.isConnected) {
|
if (!this.ipcClient || !this.isConnected) {
|
||||||
throw new Error('Not connected to daemon');
|
throw new Error('Not connected to daemon');
|
||||||
}
|
}
|
||||||
|
|
||||||
const topic = `logs.${processId}`;
|
const id = toProcessId(processId);
|
||||||
await this.ipcClient.subscribe(`topic:${topic}`, handler);
|
const topic = `logs.${id}`;
|
||||||
|
// Note: IpcClient.subscribe expects the bare topic (without the 'topic:' prefix)
|
||||||
|
// and will register a handler for 'topic:<topic>' internally.
|
||||||
|
await this.ipcClient.subscribe(topic, handler);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from log updates for a specific process
|
* Unsubscribe from log updates for a specific process
|
||||||
*/
|
*/
|
||||||
public async unsubscribe(processId: string): Promise<void> {
|
public async unsubscribe(processId: ProcessId | number | string): Promise<void> {
|
||||||
if (!this.ipcClient || !this.isConnected) {
|
if (!this.ipcClient || !this.isConnected) {
|
||||||
throw new Error('Not connected to daemon');
|
throw new Error('Not connected to daemon');
|
||||||
}
|
}
|
||||||
|
|
||||||
const topic = `logs.${processId}`;
|
const id = toProcessId(processId);
|
||||||
await this.ipcClient.unsubscribe(`topic:${topic}`);
|
const topic = `logs.${id}`;
|
||||||
|
// Pass bare topic; client handles 'topic:' prefix internally
|
||||||
|
await this.ipcClient.unsubscribe(topic);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
117
ts/daemon/logpersistence.ts
Normal file
117
ts/daemon/logpersistence.ts
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import * as plugins from '../plugins.js';
|
||||||
|
import * as paths from '../paths.js';
|
||||||
|
import type { IProcessLog } from '../shared/protocol/ipc.types.js';
|
||||||
|
import type { ProcessId } from '../shared/protocol/id.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages persistent log storage for processes
|
||||||
|
*/
|
||||||
|
export class LogPersistence {
|
||||||
|
private logsDir: string;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.logsDir = plugins.path.join(paths.tspmDir, 'logs');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the log file path for a process
|
||||||
|
*/
|
||||||
|
private getLogFilePath(processId: ProcessId): string {
|
||||||
|
return plugins.path.join(this.logsDir, `process-${processId}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the logs directory exists
|
||||||
|
*/
|
||||||
|
private async ensureLogsDir(): Promise<void> {
|
||||||
|
await plugins.smartfile.fs.ensureDir(this.logsDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save logs to disk
|
||||||
|
*/
|
||||||
|
public async saveLogs(processId: ProcessId, logs: IProcessLog[]): Promise<void> {
|
||||||
|
await this.ensureLogsDir();
|
||||||
|
const filePath = this.getLogFilePath(processId);
|
||||||
|
|
||||||
|
// Write logs as JSON
|
||||||
|
await plugins.smartfile.memory.toFs(
|
||||||
|
JSON.stringify(logs, null, 2),
|
||||||
|
filePath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load logs from disk
|
||||||
|
*/
|
||||||
|
public async loadLogs(processId: ProcessId): Promise<IProcessLog[]> {
|
||||||
|
const filePath = this.getLogFilePath(processId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exists = await plugins.smartfile.fs.fileExists(filePath);
|
||||||
|
if (!exists) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = await plugins.smartfile.fs.toStringSync(filePath);
|
||||||
|
const logs = JSON.parse(content) as IProcessLog[];
|
||||||
|
|
||||||
|
// Convert date strings back to Date objects
|
||||||
|
return logs.map(log => ({
|
||||||
|
...log,
|
||||||
|
timestamp: new Date(log.timestamp)
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to load logs for process ${processId}:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete logs from disk after loading
|
||||||
|
*/
|
||||||
|
public async deleteLogs(processId: ProcessId): Promise<void> {
|
||||||
|
const filePath = this.getLogFilePath(processId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const exists = await plugins.smartfile.fs.fileExists(filePath);
|
||||||
|
if (exists) {
|
||||||
|
await plugins.smartfile.fs.remove(filePath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to delete logs for process ${processId}:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate approximate memory size of logs in bytes
|
||||||
|
*/
|
||||||
|
public static calculateLogMemorySize(logs: IProcessLog[]): number {
|
||||||
|
// Estimate based on JSON string size
|
||||||
|
// This is an approximation but good enough for our purposes
|
||||||
|
return JSON.stringify(logs).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old log files (for maintenance)
|
||||||
|
*/
|
||||||
|
public async cleanupOldLogs(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await this.ensureLogsDir();
|
||||||
|
const files = await plugins.smartfile.fs.listFileTree(this.logsDir, '*.json');
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = plugins.path.join(this.logsDir, file);
|
||||||
|
const stats = await plugins.smartfile.fs.stat(filePath);
|
||||||
|
|
||||||
|
// Delete files older than 7 days
|
||||||
|
const ageInDays = (Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24);
|
||||||
|
if (ageInDays > 7) {
|
||||||
|
await plugins.smartfile.fs.remove(filePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to cleanup old logs:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
|
|||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import * as paths from '../paths.js';
|
import * as paths from '../paths.js';
|
||||||
import { ProcessMonitor } from './processmonitor.js';
|
import { ProcessMonitor } from './processmonitor.js';
|
||||||
|
import { LogPersistence } from './logpersistence.js';
|
||||||
import { TspmConfig } from './tspm.config.js';
|
import { TspmConfig } from './tspm.config.js';
|
||||||
import {
|
import {
|
||||||
Logger,
|
Logger,
|
||||||
@@ -16,15 +17,20 @@ import type {
|
|||||||
IProcessLog,
|
IProcessLog,
|
||||||
IMonitorConfig
|
IMonitorConfig
|
||||||
} from '../shared/protocol/ipc.types.js';
|
} from '../shared/protocol/ipc.types.js';
|
||||||
|
import { toProcessId, getNextProcessId } from '../shared/protocol/id.js';
|
||||||
|
import type { ProcessId } from '../shared/protocol/id.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class ProcessManager extends EventEmitter {
|
export class ProcessManager extends EventEmitter {
|
||||||
public processes: Map<string, ProcessMonitor> = new Map();
|
public processes: Map<ProcessId, ProcessMonitor> = new Map();
|
||||||
public processConfigs: Map<string, IProcessConfig> = new Map();
|
public processConfigs: Map<ProcessId, IProcessConfig> = new Map();
|
||||||
public processInfo: Map<string, IProcessInfo> = new Map();
|
public processInfo: Map<ProcessId, IProcessInfo> = new Map();
|
||||||
|
private processLogs: Map<ProcessId, IProcessLog[]> = new Map();
|
||||||
private config: TspmConfig;
|
private config: TspmConfig;
|
||||||
private configStorageKey = 'processes';
|
private configStorageKey = 'processes';
|
||||||
|
private desiredStateStorageKey = 'desiredStates';
|
||||||
|
private desiredStates: Map<ProcessId, IProcessInfo['status']> = new Map();
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -32,6 +38,44 @@ export class ProcessManager extends EventEmitter {
|
|||||||
this.logger = new Logger('Tspm');
|
this.logger = new Logger('Tspm');
|
||||||
this.config = new TspmConfig();
|
this.config = new TspmConfig();
|
||||||
this.loadProcessConfigs();
|
this.loadProcessConfigs();
|
||||||
|
this.loadDesiredStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a process configuration without starting it.
|
||||||
|
* Returns the assigned numeric sequential id.
|
||||||
|
*/
|
||||||
|
public async add(configInput: Omit<IProcessConfig, 'id'> & { id?: ProcessId }): Promise<ProcessId> {
|
||||||
|
// Determine next numeric id
|
||||||
|
const nextId = this.getNextSequentialId();
|
||||||
|
|
||||||
|
const config: IProcessConfig = {
|
||||||
|
id: nextId,
|
||||||
|
name: configInput.name || `process-${nextId}`,
|
||||||
|
command: configInput.command,
|
||||||
|
args: configInput.args,
|
||||||
|
projectDir: configInput.projectDir,
|
||||||
|
memoryLimitBytes: configInput.memoryLimitBytes || 512 * 1024 * 1024,
|
||||||
|
monitorIntervalMs: configInput.monitorIntervalMs,
|
||||||
|
env: configInput.env,
|
||||||
|
logBufferSize: configInput.logBufferSize,
|
||||||
|
autorestart: configInput.autorestart ?? true,
|
||||||
|
watch: configInput.watch,
|
||||||
|
watchPaths: configInput.watchPaths,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Store config and initial info
|
||||||
|
this.processConfigs.set(config.id, config);
|
||||||
|
this.processInfo.set(config.id, {
|
||||||
|
id: config.id,
|
||||||
|
status: 'stopped',
|
||||||
|
memory: 0,
|
||||||
|
restarts: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.saveProcessConfigs();
|
||||||
|
await this.setDesiredState(config.id, 'stopped');
|
||||||
|
return config.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,7 +115,8 @@ export class ProcessManager extends EventEmitter {
|
|||||||
|
|
||||||
// Create and start process monitor
|
// Create and start process monitor
|
||||||
const monitor = new ProcessMonitor({
|
const monitor = new ProcessMonitor({
|
||||||
name: config.name || config.id,
|
id: config.id, // Pass the ProcessId for log persistence
|
||||||
|
name: config.name || String(config.id),
|
||||||
projectDir: config.projectDir,
|
projectDir: config.projectDir,
|
||||||
command: config.command,
|
command: config.command,
|
||||||
args: config.args,
|
args: config.args,
|
||||||
@@ -85,13 +130,48 @@ export class ProcessManager extends EventEmitter {
|
|||||||
|
|
||||||
// Set up log event handler to re-emit for pub/sub
|
// Set up log event handler to re-emit for pub/sub
|
||||||
monitor.on('log', (log: IProcessLog) => {
|
monitor.on('log', (log: IProcessLog) => {
|
||||||
|
// Store log in our persistent storage
|
||||||
|
if (!this.processLogs.has(config.id)) {
|
||||||
|
this.processLogs.set(config.id, []);
|
||||||
|
}
|
||||||
|
const logs = this.processLogs.get(config.id)!;
|
||||||
|
logs.push(log);
|
||||||
|
|
||||||
|
// Trim logs if they exceed buffer size (default 1000)
|
||||||
|
const bufferSize = config.logBufferSize || 1000;
|
||||||
|
if (logs.length > bufferSize) {
|
||||||
|
this.processLogs.set(config.id, logs.slice(-bufferSize));
|
||||||
|
}
|
||||||
|
|
||||||
this.emit('process:log', { processId: config.id, log });
|
this.emit('process:log', { processId: config.id, log });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 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();
|
// Set up failure handler to mark process as errored
|
||||||
|
monitor.on('failed', () => {
|
||||||
|
this.updateProcessInfo(config.id, { status: 'errored', pid: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
// Update process info
|
await monitor.start();
|
||||||
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
|
// Save updated configs
|
||||||
await this.saveProcessConfigs();
|
await this.saveProcessConfigs();
|
||||||
@@ -122,10 +202,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
|
* Stop a process by id
|
||||||
*/
|
*/
|
||||||
public async stop(id: string): Promise<void> {
|
public async stop(id: ProcessId): Promise<void> {
|
||||||
this.logger.info(`Stopping process with id '${id}'`);
|
this.logger.info(`Stopping process with id '${id}'`);
|
||||||
|
|
||||||
const monitor = this.processes.get(id);
|
const monitor = this.processes.get(id);
|
||||||
@@ -139,7 +245,7 @@ export class ProcessManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
monitor.stop();
|
await monitor.stop();
|
||||||
this.updateProcessInfo(id, { status: 'stopped' });
|
this.updateProcessInfo(id, { status: 'stopped' });
|
||||||
this.logger.info(`Successfully stopped process with id '${id}'`);
|
this.logger.info(`Successfully stopped process with id '${id}'`);
|
||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
@@ -159,7 +265,7 @@ export class ProcessManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Restart a process by id
|
* Restart a process by id
|
||||||
*/
|
*/
|
||||||
public async restart(id: string): Promise<void> {
|
public async restart(id: ProcessId): Promise<void> {
|
||||||
this.logger.info(`Restarting process with id '${id}'`);
|
this.logger.info(`Restarting process with id '${id}'`);
|
||||||
|
|
||||||
const monitor = this.processes.get(id);
|
const monitor = this.processes.get(id);
|
||||||
@@ -176,11 +282,12 @@ export class ProcessManager extends EventEmitter {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Stop and then start the process
|
// Stop and then start the process
|
||||||
monitor.stop();
|
await monitor.stop();
|
||||||
|
|
||||||
// Create a new monitor instance
|
// Create a new monitor instance
|
||||||
const newMonitor = new ProcessMonitor({
|
const newMonitor = new ProcessMonitor({
|
||||||
name: config.name || config.id,
|
id: config.id, // Pass the ProcessId for log persistence
|
||||||
|
name: config.name || String(config.id),
|
||||||
projectDir: config.projectDir,
|
projectDir: config.projectDir,
|
||||||
command: config.command,
|
command: config.command,
|
||||||
args: config.args,
|
args: config.args,
|
||||||
@@ -190,18 +297,46 @@ export class ProcessManager extends EventEmitter {
|
|||||||
logBufferSize: config.logBufferSize,
|
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);
|
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);
|
const info = this.processInfo.get(id);
|
||||||
if (info) {
|
if (info) {
|
||||||
|
const pid = newMonitor.getPid();
|
||||||
this.updateProcessInfo(id, {
|
this.updateProcessInfo(id, {
|
||||||
status: 'online',
|
status: 'online',
|
||||||
|
pid: pid || undefined,
|
||||||
restarts: info.restarts + 1,
|
restarts: info.restarts + 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark errored on failure events
|
||||||
|
newMonitor.on('failed', () => {
|
||||||
|
this.updateProcessInfo(id, { status: 'errored', pid: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
this.logger.info(`Successfully restarted process with id '${id}'`);
|
this.logger.info(`Successfully restarted process with id '${id}'`);
|
||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
const processError = new ProcessError(
|
const processError = new ProcessError(
|
||||||
@@ -217,7 +352,7 @@ export class ProcessManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Delete a process by id
|
* Delete a process by id
|
||||||
*/
|
*/
|
||||||
public async delete(id: string): Promise<void> {
|
public async delete(id: ProcessId): Promise<void> {
|
||||||
this.logger.info(`Deleting process with id '${id}'`);
|
this.logger.info(`Deleting process with id '${id}'`);
|
||||||
|
|
||||||
// Check if process exists
|
// Check if process exists
|
||||||
@@ -240,9 +375,15 @@ export class ProcessManager extends EventEmitter {
|
|||||||
this.processes.delete(id);
|
this.processes.delete(id);
|
||||||
this.processConfigs.delete(id);
|
this.processConfigs.delete(id);
|
||||||
this.processInfo.delete(id);
|
this.processInfo.delete(id);
|
||||||
|
this.processLogs.delete(id);
|
||||||
|
|
||||||
|
// Delete persisted logs from disk
|
||||||
|
const logPersistence = new LogPersistence();
|
||||||
|
await logPersistence.deleteLogs(id);
|
||||||
|
|
||||||
// Save updated configs
|
// Save updated configs
|
||||||
await this.saveProcessConfigs();
|
await this.saveProcessConfigs();
|
||||||
|
await this.removeDesiredState(id);
|
||||||
|
|
||||||
this.logger.info(`Successfully deleted process with id '${id}'`);
|
this.logger.info(`Successfully deleted process with id '${id}'`);
|
||||||
} catch (error: Error | unknown) {
|
} catch (error: Error | unknown) {
|
||||||
@@ -251,7 +392,14 @@ export class ProcessManager extends EventEmitter {
|
|||||||
this.processes.delete(id);
|
this.processes.delete(id);
|
||||||
this.processConfigs.delete(id);
|
this.processConfigs.delete(id);
|
||||||
this.processInfo.delete(id);
|
this.processInfo.delete(id);
|
||||||
|
this.processLogs.delete(id);
|
||||||
|
|
||||||
|
// Delete persisted logs from disk even if stop failed
|
||||||
|
const logPersistence = new LogPersistence();
|
||||||
|
await logPersistence.deleteLogs(id);
|
||||||
|
|
||||||
await this.saveProcessConfigs();
|
await this.saveProcessConfigs();
|
||||||
|
await this.removeDesiredState(id);
|
||||||
|
|
||||||
this.logger.info(
|
this.logger.info(
|
||||||
`Successfully deleted process with id '${id}' after stopping failure`,
|
`Successfully deleted process with id '${id}' after stopping failure`,
|
||||||
@@ -272,14 +420,42 @@ export class ProcessManager extends EventEmitter {
|
|||||||
* Get a list of all process infos
|
* Get a list of all process infos
|
||||||
*/
|
*/
|
||||||
public list(): IProcessInfo[] {
|
public list(): IProcessInfo[] {
|
||||||
return Array.from(this.processInfo.values());
|
const infos = Array.from(this.processInfo.values());
|
||||||
|
|
||||||
|
// Enrich with live data from monitors
|
||||||
|
for (const info of infos) {
|
||||||
|
const monitor = this.processes.get(info.id);
|
||||||
|
if (monitor) {
|
||||||
|
// Update with current PID if the monitor is running
|
||||||
|
const pid = monitor.getPid();
|
||||||
|
if (pid) {
|
||||||
|
info.pid = pid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update uptime if available
|
||||||
|
const uptime = monitor.getUptime();
|
||||||
|
if (uptime !== null) {
|
||||||
|
info.uptime = uptime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update restart count
|
||||||
|
info.restarts = monitor.getRestartCount();
|
||||||
|
|
||||||
|
// Update status based on actual running state
|
||||||
|
if (monitor.isRunning()) {
|
||||||
|
info.status = 'online';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return infos;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get detailed info for a specific process
|
* Get detailed info for a specific process
|
||||||
*/
|
*/
|
||||||
public describe(
|
public describe(
|
||||||
id: string,
|
id: ProcessId,
|
||||||
): { config: IProcessConfig; info: IProcessInfo } | null {
|
): { config: IProcessConfig; info: IProcessInfo } | null {
|
||||||
const config = this.processConfigs.get(id);
|
const config = this.processConfigs.get(id);
|
||||||
const info = this.processInfo.get(id);
|
const info = this.processInfo.get(id);
|
||||||
@@ -294,13 +470,21 @@ export class ProcessManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Get process logs
|
* Get process logs
|
||||||
*/
|
*/
|
||||||
public getLogs(id: string, limit?: number): IProcessLog[] {
|
public getLogs(id: ProcessId, limit?: number): IProcessLog[] {
|
||||||
|
// Get logs from the ProcessMonitor instance
|
||||||
const monitor = this.processes.get(id);
|
const monitor = this.processes.get(id);
|
||||||
if (!monitor) {
|
|
||||||
return [];
|
if (monitor) {
|
||||||
|
const logs = monitor.getLogs(limit);
|
||||||
|
return logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
return monitor.getLogs(limit);
|
// Fallback to stored logs if monitor doesn't exist
|
||||||
|
const logs = this.processLogs.get(id) || [];
|
||||||
|
if (limit && limit > 0) {
|
||||||
|
return logs.slice(-limit);
|
||||||
|
}
|
||||||
|
return logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -335,13 +519,52 @@ export class ProcessManager extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Update the info for a process
|
* Update the info for a process
|
||||||
*/
|
*/
|
||||||
private updateProcessInfo(id: string, update: Partial<IProcessInfo>): void {
|
private updateProcessInfo(id: ProcessId, update: Partial<IProcessInfo>): void {
|
||||||
const info = this.processInfo.get(id);
|
const info = this.processInfo.get(id);
|
||||||
if (info) {
|
if (info) {
|
||||||
this.processInfo.set(id, { ...info, ...update });
|
this.processInfo.set(id, { ...info, ...update });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute next sequential numeric id based on existing configs
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getNextSequentialId(): ProcessId {
|
||||||
|
return getNextProcessId(this.processConfigs.keys());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save all process configurations to config storage
|
* Save all process configurations to config storage
|
||||||
*/
|
*/
|
||||||
@@ -365,6 +588,82 @@ export class ProcessManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// === Desired state persistence ===
|
||||||
|
private async saveDesiredStates(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const obj: Record<string, IProcessInfo['status']> = {};
|
||||||
|
for (const [id, state] of this.desiredStates.entries()) {
|
||||||
|
obj[String(id)] = state;
|
||||||
|
}
|
||||||
|
await this.config.writeKey(
|
||||||
|
this.desiredStateStorageKey,
|
||||||
|
JSON.stringify(obj),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to save desired states: ${error?.message || String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async loadDesiredStates(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const raw = await this.config.readKey(this.desiredStateStorageKey);
|
||||||
|
if (raw) {
|
||||||
|
const obj = JSON.parse(raw) as Record<string, IProcessInfo['status']>;
|
||||||
|
this.desiredStates = new Map(
|
||||||
|
Object.entries(obj).map(([k, v]) => [toProcessId(k), v] as const)
|
||||||
|
);
|
||||||
|
this.logger.debug(
|
||||||
|
`Loaded desired states for ${this.desiredStates.size} processes`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to load desired states: ${error?.message || String(error)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setDesiredState(
|
||||||
|
id: ProcessId,
|
||||||
|
state: IProcessInfo['status'],
|
||||||
|
): Promise<void> {
|
||||||
|
this.desiredStates.set(id, state);
|
||||||
|
await this.saveDesiredStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async removeDesiredState(id: ProcessId): Promise<void> {
|
||||||
|
this.desiredStates.delete(id);
|
||||||
|
await this.saveDesiredStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setDesiredStateForAll(
|
||||||
|
state: IProcessInfo['status'],
|
||||||
|
): Promise<void> {
|
||||||
|
for (const id of this.processConfigs.keys()) {
|
||||||
|
this.desiredStates.set(id, state);
|
||||||
|
}
|
||||||
|
await this.saveDesiredStates();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async startDesired(): Promise<void> {
|
||||||
|
for (const [id, config] of this.processConfigs.entries()) {
|
||||||
|
const desired = this.desiredStates.get(id);
|
||||||
|
if (desired === 'online' && !this.processes.has(id)) {
|
||||||
|
try {
|
||||||
|
await this.start(config);
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Failed to start desired process ${id}: ${
|
||||||
|
(e as Error)?.message || String(e)
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load process configurations from config storage
|
* Load process configurations from config storage
|
||||||
*/
|
*/
|
||||||
@@ -375,23 +674,35 @@ export class ProcessManager extends EventEmitter {
|
|||||||
const configsJson = await this.config.readKey(this.configStorageKey);
|
const configsJson = await this.config.readKey(this.configStorageKey);
|
||||||
if (configsJson) {
|
if (configsJson) {
|
||||||
try {
|
try {
|
||||||
const configs = JSON.parse(configsJson) as IProcessConfig[];
|
const parsed = JSON.parse(configsJson) as Array<any>;
|
||||||
this.logger.debug(`Loaded ${configs.length} process configurations`);
|
this.logger.debug(`Loaded ${parsed.length} process configurations`);
|
||||||
|
|
||||||
for (const config of configs) {
|
for (const raw of parsed) {
|
||||||
// Validate config
|
// Convert legacy string IDs to ProcessId
|
||||||
if (!config.id || !config.command || !config.projectDir) {
|
let id: ProcessId;
|
||||||
|
try {
|
||||||
|
id = toProcessId(raw.id);
|
||||||
|
} catch {
|
||||||
this.logger.warn(
|
this.logger.warn(
|
||||||
`Skipping invalid process config for id '${config.id || 'unknown'}'`,
|
`Skipping invalid process config with non-numeric id '${raw.id || 'unknown'}'`,
|
||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.processConfigs.set(config.id, config);
|
// Validate config
|
||||||
|
if (!id || !raw.command || !raw.projectDir) {
|
||||||
|
this.logger.warn(
|
||||||
|
`Skipping invalid process config for id '${id || 'unknown'}'`,
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config: IProcessConfig = { ...raw, id };
|
||||||
|
this.processConfigs.set(id, config);
|
||||||
|
|
||||||
// Initialize process info
|
// Initialize process info
|
||||||
this.processInfo.set(config.id, {
|
this.processInfo.set(id, {
|
||||||
id: config.id,
|
id: id,
|
||||||
status: 'stopped',
|
status: 'stopped',
|
||||||
memory: 0,
|
memory: 0,
|
||||||
restarts: 0,
|
restarts: 0,
|
||||||
@@ -420,4 +731,49 @@ export class ProcessManager extends EventEmitter {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset: stop all running processes and clear all saved configurations
|
||||||
|
*/
|
||||||
|
public async reset(): Promise<{
|
||||||
|
stopped: ProcessId[];
|
||||||
|
removed: ProcessId[];
|
||||||
|
failed: Array<{ id: ProcessId; error: string }>;
|
||||||
|
}> {
|
||||||
|
this.logger.info('Resetting TSPM: stopping all processes and clearing configs');
|
||||||
|
|
||||||
|
const removed = Array.from(this.processConfigs.keys());
|
||||||
|
const stopped: ProcessId[] = [];
|
||||||
|
const failed: Array<{ id: ProcessId; error: string }> = [];
|
||||||
|
|
||||||
|
// Attempt to stop all currently running processes with per-id error collection
|
||||||
|
for (const id of Array.from(this.processes.keys())) {
|
||||||
|
try {
|
||||||
|
await this.stop(id);
|
||||||
|
stopped.push(id);
|
||||||
|
} catch (error: any) {
|
||||||
|
failed.push({ id, error: error?.message || String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear in-memory maps regardless of stop outcomes
|
||||||
|
this.processes.clear();
|
||||||
|
this.processInfo.clear();
|
||||||
|
this.processConfigs.clear();
|
||||||
|
this.desiredStates.clear();
|
||||||
|
|
||||||
|
// Remove persisted configs
|
||||||
|
try {
|
||||||
|
await this.config.deleteKey(this.configStorageKey);
|
||||||
|
await this.config.deleteKey(this.desiredStateStorageKey).catch(() => {});
|
||||||
|
this.logger.debug('Cleared persisted process configurations');
|
||||||
|
} catch (error) {
|
||||||
|
// Fallback: write empty list if deleteKey fails for any reason
|
||||||
|
this.logger.warn('deleteKey failed, writing empty process list instead');
|
||||||
|
await this.saveProcessConfigs().catch(() => {});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info('TSPM reset complete');
|
||||||
|
return { stopped, removed, failed };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import { EventEmitter } from 'events';
|
import { EventEmitter } from 'events';
|
||||||
import { ProcessWrapper } from './processwrapper.js';
|
import { ProcessWrapper } from './processwrapper.js';
|
||||||
|
import { LogPersistence } from './logpersistence.js';
|
||||||
import { Logger, ProcessError, handleError } from '../shared/common/utils.errorhandler.js';
|
import { Logger, ProcessError, handleError } from '../shared/common/utils.errorhandler.js';
|
||||||
import type { IMonitorConfig, IProcessLog } from '../shared/protocol/ipc.types.js';
|
import type { IMonitorConfig, IProcessLog } from '../shared/protocol/ipc.types.js';
|
||||||
|
import type { ProcessId } from '../shared/protocol/id.js';
|
||||||
|
|
||||||
export class ProcessMonitor extends EventEmitter {
|
export class ProcessMonitor extends EventEmitter {
|
||||||
private processWrapper: ProcessWrapper | null = null;
|
private processWrapper: ProcessWrapper | null = null;
|
||||||
@@ -11,14 +13,48 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
private stopped: boolean = true; // Initially stopped until start() is called
|
private stopped: boolean = true; // Initially stopped until start() is called
|
||||||
private restartCount: number = 0;
|
private restartCount: number = 0;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
private logs: IProcessLog[] = [];
|
||||||
|
private logPersistence: LogPersistence;
|
||||||
|
private processId?: ProcessId;
|
||||||
|
private currentLogMemorySize: number = 0;
|
||||||
|
private readonly MAX_LOG_MEMORY_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
// Track approximate size per log to avoid O(n) JSON stringify on every update
|
||||||
|
private logSizeMap: WeakMap<IProcessLog, number> = new WeakMap();
|
||||||
|
private restartTimer: NodeJS.Timeout | null = null;
|
||||||
|
private lastRetryAt: number | null = null;
|
||||||
|
private readonly MAX_RETRIES = 10;
|
||||||
|
private readonly RESET_WINDOW_MS = 60 * 60 * 1000; // 1 hour
|
||||||
|
|
||||||
constructor(config: IMonitorConfig) {
|
constructor(config: IMonitorConfig & { id?: ProcessId }) {
|
||||||
super();
|
super();
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
|
this.logger = new Logger(`ProcessMonitor:${config.name || 'unnamed'}`);
|
||||||
|
this.logs = [];
|
||||||
|
this.logPersistence = new LogPersistence();
|
||||||
|
this.processId = config.id;
|
||||||
|
this.currentLogMemorySize = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public start(): void {
|
public async start(): Promise<void> {
|
||||||
|
// Load previously persisted logs if available
|
||||||
|
if (this.processId) {
|
||||||
|
const persistedLogs = await this.logPersistence.loadLogs(this.processId);
|
||||||
|
if (persistedLogs.length > 0) {
|
||||||
|
this.logs = persistedLogs;
|
||||||
|
// Recalculate size once from scratch and seed the size map
|
||||||
|
this.currentLogMemorySize = 0;
|
||||||
|
for (const log of this.logs) {
|
||||||
|
const size = this.estimateLogSize(log);
|
||||||
|
this.logSizeMap.set(log, size);
|
||||||
|
this.currentLogMemorySize += size;
|
||||||
|
}
|
||||||
|
this.logger.info(`Loaded ${persistedLogs.length} persisted logs from disk`);
|
||||||
|
|
||||||
|
// Delete the persisted file after loading
|
||||||
|
await this.logPersistence.deleteLogs(this.processId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Reset the stopped flag so that new processes can spawn.
|
// Reset the stopped flag so that new processes can spawn.
|
||||||
this.stopped = false;
|
this.stopped = false;
|
||||||
this.log(`Starting process monitor.`);
|
this.log(`Starting process monitor.`);
|
||||||
@@ -57,6 +93,31 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
|
|
||||||
// Set up event handlers
|
// Set up event handlers
|
||||||
this.processWrapper.on('log', (log: IProcessLog): void => {
|
this.processWrapper.on('log', (log: IProcessLog): void => {
|
||||||
|
// Store the log in our buffer
|
||||||
|
this.logs.push(log);
|
||||||
|
if (process.env.TSPM_DEBUG) {
|
||||||
|
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 incrementally
|
||||||
|
const approxSize = this.estimateLogSize(log);
|
||||||
|
this.logSizeMap.set(log, approxSize);
|
||||||
|
this.currentLogMemorySize += approxSize;
|
||||||
|
|
||||||
|
// 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
|
||||||
|
const removed = this.logs.shift()!;
|
||||||
|
const removedSize = this.logSizeMap.get(removed) ?? this.estimateLogSize(removed);
|
||||||
|
this.currentLogMemorySize -= removedSize;
|
||||||
|
}
|
||||||
|
|
||||||
// Re-emit the log event for upstream handlers
|
// Re-emit the log event for upstream handlers
|
||||||
this.emit('log', log);
|
this.emit('log', log);
|
||||||
|
|
||||||
@@ -65,19 +126,34 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
this.log(log.message);
|
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(
|
this.processWrapper.on(
|
||||||
'exit',
|
'exit',
|
||||||
(code: number | null, signal: string | null): void => {
|
async (code: number | null, signal: string | null): Promise<void> => {
|
||||||
const exitMsg = `Process exited with code ${code}, signal ${signal}.`;
|
const exitMsg = `Process exited with code ${code}, signal ${signal}.`;
|
||||||
this.logger.info(exitMsg);
|
this.logger.info(exitMsg);
|
||||||
this.log(exitMsg);
|
this.log(exitMsg);
|
||||||
|
|
||||||
|
// Flush logs to disk on exit
|
||||||
|
if (this.processId && this.logs.length > 0) {
|
||||||
|
try {
|
||||||
|
await this.logPersistence.saveLogs(this.processId, this.logs);
|
||||||
|
this.logger.debug(`Flushed ${this.logs.length} logs to disk on exit`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to flush logs to disk on exit: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-emit exit event for upstream handlers
|
||||||
|
this.emit('exit', code, signal);
|
||||||
|
|
||||||
if (!this.stopped) {
|
if (!this.stopped) {
|
||||||
this.logger.info('Restarting process...');
|
this.scheduleRestart('exit');
|
||||||
this.log('Restarting process...');
|
|
||||||
this.restartCount++;
|
|
||||||
this.spawnProcess();
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
'Not restarting process because monitor is stopped',
|
'Not restarting process because monitor is stopped',
|
||||||
@@ -86,7 +162,7 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
this.processWrapper.on('error', (error: Error | ProcessError): void => {
|
this.processWrapper.on('error', async (error: Error | ProcessError): Promise<void> => {
|
||||||
const errorMsg =
|
const errorMsg =
|
||||||
error instanceof ProcessError
|
error instanceof ProcessError
|
||||||
? `Process error: ${error.toString()}`
|
? `Process error: ${error.toString()}`
|
||||||
@@ -95,11 +171,18 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
this.logger.error(error);
|
this.logger.error(error);
|
||||||
this.log(errorMsg);
|
this.log(errorMsg);
|
||||||
|
|
||||||
|
// Flush logs to disk on error
|
||||||
|
if (this.processId && this.logs.length > 0) {
|
||||||
|
try {
|
||||||
|
await this.logPersistence.saveLogs(this.processId, this.logs);
|
||||||
|
this.logger.debug(`Flushed ${this.logs.length} logs to disk on error`);
|
||||||
|
} catch (flushError) {
|
||||||
|
this.logger.error(`Failed to flush logs to disk on error: ${flushError}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.stopped) {
|
if (!this.stopped) {
|
||||||
this.logger.info('Restarting process due to error...');
|
this.scheduleRestart('error');
|
||||||
this.log('Restarting process due to error...');
|
|
||||||
this.restartCount++;
|
|
||||||
this.spawnProcess();
|
|
||||||
} else {
|
} else {
|
||||||
this.logger.debug('Not restarting process because monitor is stopped');
|
this.logger.debug('Not restarting process because monitor is stopped');
|
||||||
}
|
}
|
||||||
@@ -117,6 +200,49 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Schedule a restart with incremental debounce and failure cutoff.
|
||||||
|
*/
|
||||||
|
private scheduleRestart(reason: 'exit' | 'error'): void {
|
||||||
|
const now = Date.now();
|
||||||
|
// Reset window: if last retry was more than 1 hour ago, reset counter
|
||||||
|
if (this.lastRetryAt && now - this.lastRetryAt >= this.RESET_WINDOW_MS) {
|
||||||
|
this.logger.info('Resetting retry counter after 1 hour window');
|
||||||
|
this.restartCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Already at or above max retries?
|
||||||
|
if (this.restartCount >= this.MAX_RETRIES) {
|
||||||
|
const msg = 'Maximum restart attempts reached. Marking process as failed.';
|
||||||
|
this.logger.warn(msg);
|
||||||
|
this.log(msg);
|
||||||
|
this.stopped = true;
|
||||||
|
// Emit a specific event so manager can set status to errored
|
||||||
|
this.emit('failed');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment and compute delay (1..10 seconds)
|
||||||
|
this.restartCount++;
|
||||||
|
const delaySec = Math.min(this.restartCount, 10);
|
||||||
|
const msg = `Restarting process in ${delaySec}s (attempt ${this.restartCount}/${this.MAX_RETRIES}) due to ${reason}...`;
|
||||||
|
this.logger.info(msg);
|
||||||
|
this.log(msg);
|
||||||
|
|
||||||
|
// Clear existing timer if any, then schedule
|
||||||
|
if (this.restartTimer) {
|
||||||
|
clearTimeout(this.restartTimer);
|
||||||
|
}
|
||||||
|
this.lastRetryAt = now;
|
||||||
|
this.restartTimer = setTimeout(() => {
|
||||||
|
// If stopped in the meantime, do not spawn
|
||||||
|
if (this.stopped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.spawnProcess();
|
||||||
|
}, delaySec * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monitor the process group's memory usage. If the total memory exceeds the limit,
|
* Monitor the process group's memory usage. If the total memory exceeds the limit,
|
||||||
* kill the process group so that the 'exit' handler can restart it.
|
* kill the process group so that the 'exit' handler can restart it.
|
||||||
@@ -132,12 +258,14 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
`Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`,
|
`Memory usage for PID ${pid}: ${this.humanReadableBytes(memoryUsage)} (${memoryUsage} bytes)`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only log to the process log at longer intervals to avoid spamming
|
// Only log memory usage in debug mode to avoid spamming
|
||||||
this.log(
|
if (process.env.TSPM_DEBUG) {
|
||||||
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
|
this.log(
|
||||||
memoryUsage,
|
`Current memory usage for process group (PID ${pid}): ${this.humanReadableBytes(
|
||||||
)} (${memoryUsage} bytes)`,
|
memoryUsage,
|
||||||
);
|
)} (${memoryUsage} bytes)`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (memoryUsage > memoryLimit) {
|
if (memoryUsage > memoryLimit) {
|
||||||
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
|
const memoryLimitMsg = `Memory usage ${this.humanReadableBytes(
|
||||||
@@ -239,9 +367,20 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
/**
|
/**
|
||||||
* Stop the monitor and prevent any further respawns.
|
* Stop the monitor and prevent any further respawns.
|
||||||
*/
|
*/
|
||||||
public stop(): void {
|
public async stop(): Promise<void> {
|
||||||
this.log('Stopping process monitor.');
|
this.log('Stopping process monitor.');
|
||||||
this.stopped = true;
|
this.stopped = true;
|
||||||
|
|
||||||
|
// Flush logs to disk before stopping
|
||||||
|
if (this.processId && this.logs.length > 0) {
|
||||||
|
try {
|
||||||
|
await this.logPersistence.saveLogs(this.processId, this.logs);
|
||||||
|
this.logger.info(`Flushed ${this.logs.length} logs to disk on stop`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Failed to flush logs to disk on stop: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (this.intervalId) {
|
if (this.intervalId) {
|
||||||
clearInterval(this.intervalId);
|
clearInterval(this.intervalId);
|
||||||
}
|
}
|
||||||
@@ -254,10 +393,16 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
* Get the current logs from the process
|
* Get the current logs from the process
|
||||||
*/
|
*/
|
||||||
public getLogs(limit?: number): IProcessLog[] {
|
public getLogs(limit?: number): IProcessLog[] {
|
||||||
if (!this.processWrapper) {
|
if (process.env.TSPM_DEBUG) {
|
||||||
return [];
|
console.error(
|
||||||
|
`[ProcessMonitor:${this.config.name}] getLogs called, logs.length=${this.logs.length}, limit=${limit}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return this.processWrapper.getLogs(limit);
|
this.logger.debug(`Getting logs, total stored: ${this.logs.length}`);
|
||||||
|
if (limit && limit > 0) {
|
||||||
|
return this.logs.slice(-limit);
|
||||||
|
}
|
||||||
|
return this.logs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -295,4 +440,17 @@ export class ProcessMonitor extends EventEmitter {
|
|||||||
const prefix = this.config.name ? `[${this.config.name}] ` : '';
|
const prefix = this.config.name ? `[${this.config.name}] ` : '';
|
||||||
console.log(prefix + message);
|
console.log(prefix + message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimate approximate memory size in bytes for a log entry.
|
||||||
|
* Keeps CPU low by avoiding JSON.stringify on the full array.
|
||||||
|
*/
|
||||||
|
private estimateLogSize(log: IProcessLog): number {
|
||||||
|
const messageBytes = Buffer.byteLength(log.message || '', 'utf8');
|
||||||
|
const typeBytes = Buffer.byteLength(log.type || '', 'utf8');
|
||||||
|
const runIdBytes = Buffer.byteLength((log as any).runId || '', 'utf8');
|
||||||
|
// Rough overhead for object structure, keys, timestamp/seq values
|
||||||
|
const overhead = 64;
|
||||||
|
return messageBytes + typeBytes + runIdBytes + overhead;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,8 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
private nextSeq: number = 0;
|
private nextSeq: number = 0;
|
||||||
private runId: string = '';
|
private runId: string = '';
|
||||||
|
private stdoutRemainder: string = '';
|
||||||
|
private stderrRemainder: string = '';
|
||||||
|
|
||||||
constructor(options: IProcessWrapperOptions) {
|
constructor(options: IProcessWrapperOptions) {
|
||||||
super();
|
super();
|
||||||
@@ -45,7 +47,7 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
this.options.args,
|
this.options.args,
|
||||||
{
|
{
|
||||||
cwd: this.options.cwd,
|
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
|
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
|
// Use shell mode to allow a full command string
|
||||||
this.process = plugins.childProcess.spawn(this.options.command, {
|
this.process = plugins.childProcess.spawn(this.options.command, {
|
||||||
cwd: this.options.cwd,
|
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
|
stdio: ['ignore', 'pipe', 'pipe'], // We need to pipe stdout and stderr
|
||||||
shell: true,
|
shell: true,
|
||||||
});
|
});
|
||||||
@@ -66,6 +68,11 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
const exitMessage = `Process exited with code ${code}, signal ${signal}`;
|
const exitMessage = `Process exited with code ${code}, signal ${signal}`;
|
||||||
this.logger.info(exitMessage);
|
this.logger.info(exitMessage);
|
||||||
this.addSystemLog(exitMessage);
|
this.addSystemLog(exitMessage);
|
||||||
|
|
||||||
|
// Clear remainder buffers on exit
|
||||||
|
this.stdoutRemainder = '';
|
||||||
|
this.stderrRemainder = '';
|
||||||
|
|
||||||
this.emit('exit', code, signal);
|
this.emit('exit', code, signal);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -83,24 +90,69 @@ export class ProcessWrapper extends EventEmitter {
|
|||||||
|
|
||||||
// Capture stdout
|
// Capture stdout
|
||||||
if (this.process.stdout) {
|
if (this.process.stdout) {
|
||||||
|
if (process.env.TSPM_DEBUG) {
|
||||||
|
console.error(
|
||||||
|
`[ProcessWrapper] Setting up stdout listener for process ${this.process.pid}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
this.process.stdout.on('data', (data) => {
|
this.process.stdout.on('data', (data) => {
|
||||||
const lines = data.toString().split('\n');
|
if (process.env.TSPM_DEBUG) {
|
||||||
|
console.error(
|
||||||
|
`[ProcessWrapper] Received stdout data from PID ${this.process?.pid}: ${data
|
||||||
|
.toString()
|
||||||
|
.substring(0, 100)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Add data to remainder buffer and split by newlines
|
||||||
|
const text = this.stdoutRemainder + data.toString();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
// The last element might be a partial line
|
||||||
|
this.stdoutRemainder = lines.pop() || '';
|
||||||
|
|
||||||
|
// Process complete lines
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.trim()) {
|
if (process.env.TSPM_DEBUG) {
|
||||||
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
|
// Capture stderr
|
||||||
if (this.process.stderr) {
|
if (this.process.stderr) {
|
||||||
this.process.stderr.on('data', (data) => {
|
this.process.stderr.on('data', (data) => {
|
||||||
const lines = data.toString().split('\n');
|
// Add data to remainder buffer and split by newlines
|
||||||
|
const text = this.stderrRemainder + data.toString();
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
// The last element might be a partial line
|
||||||
|
this.stderrRemainder = lines.pop() || '';
|
||||||
|
|
||||||
|
// Process complete lines
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.trim()) {
|
this.addLog('stderr', line);
|
||||||
this.addLog('stderr', line);
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
|
// Flush remainder on stream end
|
||||||
|
this.process.stderr.on('end', () => {
|
||||||
|
if (this.stderrRemainder) {
|
||||||
|
this.addLog('stderr', this.stderrRemainder);
|
||||||
|
this.stderrRemainder = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,7 @@
|
|||||||
import * as plugins from '../plugins.js';
|
import * as plugins from '../plugins.js';
|
||||||
import * as paths from '../paths.js';
|
import * as paths from '../paths.js';
|
||||||
|
import { toProcessId } from '../shared/protocol/id.js';
|
||||||
|
import type { ProcessId } from '../shared/protocol/id.js';
|
||||||
import { ProcessManager } from './processmanager.js';
|
import { ProcessManager } from './processmanager.js';
|
||||||
import type {
|
import type {
|
||||||
IpcMethodMap,
|
IpcMethodMap,
|
||||||
@@ -20,12 +22,20 @@ export class TspmDaemon {
|
|||||||
private socketPath: string;
|
private socketPath: string;
|
||||||
private heartbeatInterval: NodeJS.Timeout | null = null;
|
private heartbeatInterval: NodeJS.Timeout | null = null;
|
||||||
private daemonPidFile: string;
|
private daemonPidFile: string;
|
||||||
|
private version: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.tspmInstance = new ProcessManager();
|
this.tspmInstance = new ProcessManager();
|
||||||
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
|
this.socketPath = plugins.path.join(paths.tspmDir, 'tspm.sock');
|
||||||
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
|
this.daemonPidFile = plugins.path.join(paths.tspmDir, 'daemon.pid');
|
||||||
this.startTime = Date.now();
|
this.startTime = Date.now();
|
||||||
|
// Determine daemon version from package metadata
|
||||||
|
try {
|
||||||
|
const proj = new plugins.projectinfo.ProjectInfo(paths.packageDir);
|
||||||
|
this.version = proj.npm.version || 'unknown';
|
||||||
|
} catch {
|
||||||
|
this.version = 'unknown';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,20 +91,40 @@ export class TspmDaemon {
|
|||||||
|
|
||||||
// Load existing process configurations
|
// Load existing process configurations
|
||||||
await this.tspmInstance.loadProcessConfigs();
|
await this.tspmInstance.loadProcessConfigs();
|
||||||
|
await this.tspmInstance.loadDesiredStates();
|
||||||
|
|
||||||
// Set up log publishing
|
// Set up log publishing
|
||||||
this.tspmInstance.on('process:log', ({ processId, log }) => {
|
this.tspmInstance.on('process:log', ({ processId, log }) => {
|
||||||
// Publish to topic for this process
|
// Publish to topic for this process
|
||||||
const topic = `logs.${processId}`;
|
const topic = `logs.${processId}`;
|
||||||
// Broadcast to all connected clients subscribed to this topic
|
// Deliver only to subscribed clients
|
||||||
if (this.ipcServer) {
|
if (this.ipcServer) {
|
||||||
this.ipcServer.broadcast(`topic:${topic}`, log);
|
try {
|
||||||
|
const topicIndex = (this.ipcServer as any).topicIndex as Map<string, Set<string>> | undefined;
|
||||||
|
const subscribers = topicIndex?.get(topic);
|
||||||
|
if (subscribers && subscribers.size > 0) {
|
||||||
|
// Send directly to subscribers for this topic
|
||||||
|
for (const clientId of subscribers) {
|
||||||
|
this.ipcServer
|
||||||
|
.sendToClient(clientId, `topic:${topic}`, log)
|
||||||
|
.catch((err: any) => {
|
||||||
|
// Surface but don't fail the loop
|
||||||
|
console.error('[IPC] sendToClient error:', err?.message || err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('[IPC] Topic delivery error:', err?.message || err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set up graceful shutdown handlers
|
// Set up graceful shutdown handlers
|
||||||
this.setupShutdownHandlers();
|
this.setupShutdownHandlers();
|
||||||
|
|
||||||
|
// Start processes that should be online per desired state
|
||||||
|
await this.tspmInstance.startDesired();
|
||||||
|
|
||||||
console.log(`TSPM daemon started successfully on ${this.socketPath}`);
|
console.log(`TSPM daemon started successfully on ${this.socketPath}`);
|
||||||
console.log(`PID: ${process.pid}`);
|
console.log(`PID: ${process.pid}`);
|
||||||
}
|
}
|
||||||
@@ -108,6 +138,7 @@ export class TspmDaemon {
|
|||||||
'start',
|
'start',
|
||||||
async (request: RequestForMethod<'start'>) => {
|
async (request: RequestForMethod<'start'>) => {
|
||||||
try {
|
try {
|
||||||
|
await this.tspmInstance.setDesiredState(request.config.id, 'online');
|
||||||
await this.tspmInstance.start(request.config);
|
await this.tspmInstance.start(request.config);
|
||||||
const processInfo = this.tspmInstance.processInfo.get(
|
const processInfo = this.tspmInstance.processInfo.get(
|
||||||
request.config.id,
|
request.config.id,
|
||||||
@@ -123,14 +154,45 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Start by id (resolve config on server)
|
||||||
|
this.ipcServer.onMessage(
|
||||||
|
'startById',
|
||||||
|
async (request: RequestForMethod<'startById'>) => {
|
||||||
|
try {
|
||||||
|
const id = toProcessId(request.id);
|
||||||
|
let config = this.tspmInstance.processConfigs.get(id);
|
||||||
|
if (!config) {
|
||||||
|
// Try to reload configs if not found (handles races or stale state)
|
||||||
|
await this.tspmInstance.loadProcessConfigs();
|
||||||
|
config = this.tspmInstance.processConfigs.get(id) || null as any;
|
||||||
|
}
|
||||||
|
if (!config) {
|
||||||
|
throw new Error(`Process ${id} not found`);
|
||||||
|
}
|
||||||
|
await this.tspmInstance.setDesiredState(id, 'online');
|
||||||
|
await this.tspmInstance.start(config);
|
||||||
|
const processInfo = this.tspmInstance.processInfo.get(id);
|
||||||
|
return {
|
||||||
|
processId: id,
|
||||||
|
pid: processInfo?.pid,
|
||||||
|
status: processInfo?.status || 'stopped',
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to start process: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'stop',
|
'stop',
|
||||||
async (request: RequestForMethod<'stop'>) => {
|
async (request: RequestForMethod<'stop'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.stop(request.id);
|
const id = toProcessId(request.id);
|
||||||
|
await this.tspmInstance.setDesiredState(id, 'stopped');
|
||||||
|
await this.tspmInstance.stop(id);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Process ${request.id} stopped successfully`,
|
message: `Process ${id} stopped successfully`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to stop process: ${error.message}`);
|
throw new Error(`Failed to stop process: ${error.message}`);
|
||||||
@@ -142,10 +204,12 @@ export class TspmDaemon {
|
|||||||
'restart',
|
'restart',
|
||||||
async (request: RequestForMethod<'restart'>) => {
|
async (request: RequestForMethod<'restart'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.restart(request.id);
|
const id = toProcessId(request.id);
|
||||||
const processInfo = this.tspmInstance.processInfo.get(request.id);
|
await this.tspmInstance.setDesiredState(id, 'online');
|
||||||
|
await this.tspmInstance.restart(id);
|
||||||
|
const processInfo = this.tspmInstance.processInfo.get(id);
|
||||||
return {
|
return {
|
||||||
processId: request.id,
|
processId: id,
|
||||||
pid: processInfo?.pid,
|
pid: processInfo?.pid,
|
||||||
status: processInfo?.status || 'stopped',
|
status: processInfo?.status || 'stopped',
|
||||||
};
|
};
|
||||||
@@ -159,10 +223,13 @@ export class TspmDaemon {
|
|||||||
'delete',
|
'delete',
|
||||||
async (request: RequestForMethod<'delete'>) => {
|
async (request: RequestForMethod<'delete'>) => {
|
||||||
try {
|
try {
|
||||||
await this.tspmInstance.delete(request.id);
|
const id = toProcessId(request.id);
|
||||||
|
// Ensure desired state reflects stopped before deletion
|
||||||
|
await this.tspmInstance.setDesiredState(id, 'stopped');
|
||||||
|
await this.tspmInstance.delete(id);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Process ${request.id} deleted successfully`,
|
message: `Process ${id} deleted successfully`,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`Failed to delete process: ${error.message}`);
|
throw new Error(`Failed to delete process: ${error.message}`);
|
||||||
@@ -171,6 +238,34 @@ export class TspmDaemon {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Query handlers
|
// Query handlers
|
||||||
|
this.ipcServer.onMessage(
|
||||||
|
'add',
|
||||||
|
async (request: RequestForMethod<'add'>) => {
|
||||||
|
try {
|
||||||
|
const id = await this.tspmInstance.add(request.config as any);
|
||||||
|
const config = this.tspmInstance.processConfigs.get(id)!;
|
||||||
|
return { id, config };
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to add process: ${error.message}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Note: 'remove' is only a CLI alias. Daemon exposes 'delete' only.
|
||||||
|
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'list',
|
'list',
|
||||||
async (request: RequestForMethod<'list'>) => {
|
async (request: RequestForMethod<'list'>) => {
|
||||||
@@ -182,16 +277,15 @@ export class TspmDaemon {
|
|||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'describe',
|
'describe',
|
||||||
async (request: RequestForMethod<'describe'>) => {
|
async (request: RequestForMethod<'describe'>) => {
|
||||||
const processInfo = await this.tspmInstance.describe(request.id);
|
const id = toProcessId(request.id);
|
||||||
const config = this.tspmInstance.processConfigs.get(request.id);
|
const result = await this.tspmInstance.describe(id);
|
||||||
|
if (!result) {
|
||||||
if (!processInfo || !config) {
|
throw new Error(`Process ${id} not found`);
|
||||||
throw new Error(`Process ${request.id} not found`);
|
|
||||||
}
|
}
|
||||||
|
// Return correctly shaped response
|
||||||
return {
|
return {
|
||||||
processInfo,
|
processInfo: result.info,
|
||||||
config,
|
config: result.config,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -199,18 +293,71 @@ export class TspmDaemon {
|
|||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'getLogs',
|
'getLogs',
|
||||||
async (request: RequestForMethod<'getLogs'>) => {
|
async (request: RequestForMethod<'getLogs'>) => {
|
||||||
const logs = await this.tspmInstance.getLogs(request.id);
|
const logs = await this.tspmInstance.getLogs(toProcessId(request.id));
|
||||||
return { logs };
|
return { logs };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Resolve target (id:n | name:foo | numeric string) to ProcessId
|
||||||
|
this.ipcServer.onMessage(
|
||||||
|
'resolveTarget',
|
||||||
|
async (request: RequestForMethod<'resolveTarget'>) => {
|
||||||
|
const raw = String(request.target || '').trim();
|
||||||
|
if (!raw) {
|
||||||
|
throw new Error('Empty target');
|
||||||
|
}
|
||||||
|
|
||||||
|
// id:<n>
|
||||||
|
if (/^id:\s*\d+$/i.test(raw)) {
|
||||||
|
const idNum = raw.split(':')[1].trim();
|
||||||
|
const id = toProcessId(idNum);
|
||||||
|
const config = this.tspmInstance.processConfigs.get(id);
|
||||||
|
if (!config) throw new Error(`Process ${id} not found`);
|
||||||
|
return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// name:<label>
|
||||||
|
if (/^name:/i.test(raw)) {
|
||||||
|
const name = raw.slice(raw.indexOf(':') + 1).trim();
|
||||||
|
if (!name) throw new Error('Missing name after name:');
|
||||||
|
const matches = Array.from(this.tspmInstance.processConfigs.values()).filter(
|
||||||
|
(c) => (c.name || '').trim() === name,
|
||||||
|
);
|
||||||
|
if (matches.length === 0) {
|
||||||
|
throw new Error(`No process found with name "${name}"`);
|
||||||
|
}
|
||||||
|
if (matches.length > 1) {
|
||||||
|
const ids = matches.map((c) => String(c.id)).join(', ');
|
||||||
|
throw new Error(
|
||||||
|
`Multiple processes found with name "${name}": ids [${ids}]. Please use id:<n>.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { id: matches[0].id, name } as ResponseForMethod<'resolveTarget'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// bare numeric id
|
||||||
|
if (/^\d+$/.test(raw)) {
|
||||||
|
const id = toProcessId(raw);
|
||||||
|
const config = this.tspmInstance.processConfigs.get(id);
|
||||||
|
if (!config) throw new Error(`Process ${id} not found`);
|
||||||
|
return { id, name: config.name } as ResponseForMethod<'resolveTarget'>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown format
|
||||||
|
throw new Error(
|
||||||
|
'Unsupported target format. Use numeric id (e.g. 1), id:<n> (e.g. id:1), or name:<label> (e.g. name:api).',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Batch operations handlers
|
// Batch operations handlers
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'startAll',
|
'startAll',
|
||||||
async (request: RequestForMethod<'startAll'>) => {
|
async (request: RequestForMethod<'startAll'>) => {
|
||||||
const started: string[] = [];
|
const started: ProcessId[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: ProcessId; error: string }> = [];
|
||||||
|
|
||||||
|
await this.tspmInstance.setDesiredStateForAll('online');
|
||||||
await this.tspmInstance.startAll();
|
await this.tspmInstance.startAll();
|
||||||
|
|
||||||
// Get status of all processes
|
// Get status of all processes
|
||||||
@@ -229,9 +376,10 @@ export class TspmDaemon {
|
|||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'stopAll',
|
'stopAll',
|
||||||
async (request: RequestForMethod<'stopAll'>) => {
|
async (request: RequestForMethod<'stopAll'>) => {
|
||||||
const stopped: string[] = [];
|
const stopped: ProcessId[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: ProcessId; error: string }> = [];
|
||||||
|
|
||||||
|
await this.tspmInstance.setDesiredStateForAll('stopped');
|
||||||
await this.tspmInstance.stopAll();
|
await this.tspmInstance.stopAll();
|
||||||
|
|
||||||
// Get status of all processes
|
// Get status of all processes
|
||||||
@@ -250,8 +398,8 @@ export class TspmDaemon {
|
|||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'restartAll',
|
'restartAll',
|
||||||
async (request: RequestForMethod<'restartAll'>) => {
|
async (request: RequestForMethod<'restartAll'>) => {
|
||||||
const restarted: string[] = [];
|
const restarted: ProcessId[] = [];
|
||||||
const failed: Array<{ id: string; error: string }> = [];
|
const failed: Array<{ id: ProcessId; error: string }> = [];
|
||||||
|
|
||||||
await this.tspmInstance.restartAll();
|
await this.tspmInstance.restartAll();
|
||||||
|
|
||||||
@@ -268,6 +416,15 @@ export class TspmDaemon {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reset handler: stops all and clears configs
|
||||||
|
this.ipcServer.onMessage(
|
||||||
|
'reset',
|
||||||
|
async (request: RequestForMethod<'reset'>) => {
|
||||||
|
const result = await this.tspmInstance.reset();
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// Daemon management handlers
|
// Daemon management handlers
|
||||||
this.ipcServer.onMessage(
|
this.ipcServer.onMessage(
|
||||||
'daemon:status',
|
'daemon:status',
|
||||||
@@ -280,6 +437,7 @@ export class TspmDaemon {
|
|||||||
processCount: this.tspmInstance.processes.size,
|
processCount: this.tspmInstance.processes.size,
|
||||||
memoryUsage: memUsage.heapUsed,
|
memoryUsage: memUsage.heapUsed,
|
||||||
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
|
cpuUsage: process.cpuUsage().user / 1000000, // Convert to seconds
|
||||||
|
version: this.version,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -472,3 +630,11 @@ export const startDaemon = async (): Promise<void> => {
|
|||||||
// Keep the process alive
|
// Keep the process alive
|
||||||
await new Promise(() => {});
|
await new Promise(() => {});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If this file is run directly (not imported), start the daemon
|
||||||
|
if (process.env.TSPM_DAEMON_MODE === 'true') {
|
||||||
|
startDaemon().catch((error) => {
|
||||||
|
console.error('Failed to start TSPM daemon:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@@ -10,11 +10,13 @@ import * as npmextra from '@push.rocks/npmextra';
|
|||||||
import * as projectinfo from '@push.rocks/projectinfo';
|
import * as projectinfo from '@push.rocks/projectinfo';
|
||||||
import * as smartcli from '@push.rocks/smartcli';
|
import * as smartcli from '@push.rocks/smartcli';
|
||||||
import * as smartdaemon from '@push.rocks/smartdaemon';
|
import * as smartdaemon from '@push.rocks/smartdaemon';
|
||||||
|
import * as smartfile from '@push.rocks/smartfile';
|
||||||
import * as smartipc from '@push.rocks/smartipc';
|
import * as smartipc from '@push.rocks/smartipc';
|
||||||
import * as smartpath from '@push.rocks/smartpath';
|
import * as smartpath from '@push.rocks/smartpath';
|
||||||
|
import * as smartinteract from '@push.rocks/smartinteract';
|
||||||
|
|
||||||
// Export with explicit module types
|
// Export with explicit module types
|
||||||
export { npmextra, projectinfo, smartcli, smartdaemon, smartipc, smartpath };
|
export { npmextra, projectinfo, smartcli, smartdaemon, smartfile, smartipc, smartpath, smartinteract };
|
||||||
|
|
||||||
// third-party scope
|
// third-party scope
|
||||||
import psTree from 'ps-tree';
|
import psTree from 'ps-tree';
|
||||||
|
56
ts/shared/protocol/id.ts
Normal file
56
ts/shared/protocol/id.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Branded type for process IDs to ensure type safety
|
||||||
|
*/
|
||||||
|
export type ProcessId = number & { readonly __brand: 'ProcessId' };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input type that accepts various ID formats for backward compatibility
|
||||||
|
*/
|
||||||
|
export type ProcessIdInput = ProcessId | number | string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes various ID input formats to a ProcessId
|
||||||
|
* @param input - The ID in various formats (string, number, or ProcessId)
|
||||||
|
* @returns A normalized ProcessId
|
||||||
|
* @throws Error if the input is not a valid process ID
|
||||||
|
*/
|
||||||
|
export function toProcessId(input: ProcessIdInput): ProcessId {
|
||||||
|
let num: number;
|
||||||
|
|
||||||
|
if (typeof input === 'string') {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
if (!/^\d+$/.test(trimmed)) {
|
||||||
|
throw new Error(`Invalid process ID: "${input}" is not a numeric string`);
|
||||||
|
}
|
||||||
|
num = parseInt(trimmed, 10);
|
||||||
|
} else if (typeof input === 'number') {
|
||||||
|
num = input;
|
||||||
|
} else {
|
||||||
|
// Already a ProcessId
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Number.isInteger(num) || num < 1) {
|
||||||
|
throw new Error(`Invalid process ID: ${input} must be a positive integer`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return num as ProcessId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to check if a value is a ProcessId
|
||||||
|
*/
|
||||||
|
export function isProcessId(value: unknown): value is ProcessId {
|
||||||
|
return typeof value === 'number' && Number.isInteger(value) && value >= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the next sequential ID given existing IDs
|
||||||
|
*/
|
||||||
|
export function getNextProcessId(existingIds: Iterable<ProcessId>): ProcessId {
|
||||||
|
let maxId = 0;
|
||||||
|
for (const id of existingIds) {
|
||||||
|
maxId = Math.max(maxId, id);
|
||||||
|
}
|
||||||
|
return (maxId + 1) as ProcessId;
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ProcessId } from './id.js';
|
||||||
|
|
||||||
// Process-related interfaces (used in IPC communication)
|
// Process-related interfaces (used in IPC communication)
|
||||||
export interface IMonitorConfig {
|
export interface IMonitorConfig {
|
||||||
name?: string; // Optional name to identify the instance
|
name?: string; // Optional name to identify the instance
|
||||||
@@ -11,14 +13,14 @@ export interface IMonitorConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IProcessConfig extends IMonitorConfig {
|
export interface IProcessConfig extends IMonitorConfig {
|
||||||
id: string; // Unique identifier for the process
|
id: ProcessId; // Unique identifier for the process
|
||||||
autorestart: boolean; // Whether to restart the process automatically on crash
|
autorestart: boolean; // Whether to restart the process automatically on crash
|
||||||
watch?: boolean; // Whether to watch for file changes and restart
|
watch?: boolean; // Whether to watch for file changes and restart
|
||||||
watchPaths?: string[]; // Paths to watch for changes
|
watchPaths?: string[]; // Paths to watch for changes
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IProcessInfo {
|
export interface IProcessInfo {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
status: 'online' | 'stopped' | 'errored';
|
status: 'online' | 'stopped' | 'errored';
|
||||||
memory: number;
|
memory: number;
|
||||||
@@ -61,14 +63,25 @@ export interface StartRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StartResponse {
|
export interface StartResponse {
|
||||||
processId: string;
|
processId: ProcessId;
|
||||||
|
pid?: number;
|
||||||
|
status: 'online' | 'stopped' | 'errored';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start by id (server resolves config)
|
||||||
|
export interface StartByIdRequest {
|
||||||
|
id: ProcessId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StartByIdResponse {
|
||||||
|
processId: ProcessId;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
status: 'online' | 'stopped' | 'errored';
|
status: 'online' | 'stopped' | 'errored';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stop command
|
// Stop command
|
||||||
export interface StopRequest {
|
export interface StopRequest {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StopResponse {
|
export interface StopResponse {
|
||||||
@@ -78,18 +91,18 @@ export interface StopResponse {
|
|||||||
|
|
||||||
// Restart command
|
// Restart command
|
||||||
export interface RestartRequest {
|
export interface RestartRequest {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RestartResponse {
|
export interface RestartResponse {
|
||||||
processId: string;
|
processId: ProcessId;
|
||||||
pid?: number;
|
pid?: number;
|
||||||
status: 'online' | 'stopped' | 'errored';
|
status: 'online' | 'stopped' | 'errored';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete command
|
// Delete command
|
||||||
export interface DeleteRequest {
|
export interface DeleteRequest {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteResponse {
|
export interface DeleteResponse {
|
||||||
@@ -108,7 +121,7 @@ export interface ListResponse {
|
|||||||
|
|
||||||
// Describe command
|
// Describe command
|
||||||
export interface DescribeRequest {
|
export interface DescribeRequest {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DescribeResponse {
|
export interface DescribeResponse {
|
||||||
@@ -118,7 +131,7 @@ export interface DescribeResponse {
|
|||||||
|
|
||||||
// Get logs command
|
// Get logs command
|
||||||
export interface GetLogsRequest {
|
export interface GetLogsRequest {
|
||||||
id: string;
|
id: ProcessId;
|
||||||
lines?: number;
|
lines?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,9 +145,9 @@ export interface StartAllRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StartAllResponse {
|
export interface StartAllResponse {
|
||||||
started: string[];
|
started: ProcessId[];
|
||||||
failed: Array<{
|
failed: Array<{
|
||||||
id: string;
|
id: ProcessId;
|
||||||
error: string;
|
error: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
@@ -145,9 +158,9 @@ export interface StopAllRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface StopAllResponse {
|
export interface StopAllResponse {
|
||||||
stopped: string[];
|
stopped: ProcessId[];
|
||||||
failed: Array<{
|
failed: Array<{
|
||||||
id: string;
|
id: ProcessId;
|
||||||
error: string;
|
error: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
@@ -158,9 +171,23 @@ export interface RestartAllRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RestartAllResponse {
|
export interface RestartAllResponse {
|
||||||
restarted: string[];
|
restarted: ProcessId[];
|
||||||
failed: Array<{
|
failed: Array<{
|
||||||
id: string;
|
id: ProcessId;
|
||||||
|
error: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset command (stop all and clear configs)
|
||||||
|
export interface ResetRequest {
|
||||||
|
// No parameters needed
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResetResponse {
|
||||||
|
stopped: ProcessId[];
|
||||||
|
removed: ProcessId[];
|
||||||
|
failed: Array<{
|
||||||
|
id: ProcessId;
|
||||||
error: string;
|
error: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
@@ -177,6 +204,7 @@ export interface DaemonStatusResponse {
|
|||||||
processCount: number;
|
processCount: number;
|
||||||
memoryUsage?: number;
|
memoryUsage?: number;
|
||||||
cpuUsage?: number;
|
cpuUsage?: number;
|
||||||
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Daemon shutdown command
|
// Daemon shutdown command
|
||||||
@@ -200,18 +228,56 @@ export interface HeartbeatResponse {
|
|||||||
status: 'healthy' | 'degraded';
|
status: 'healthy' | 'degraded';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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?: ProcessId };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AddResponse {
|
||||||
|
id: ProcessId;
|
||||||
|
config: IProcessConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove (delete config and stop if running)
|
||||||
|
|
||||||
|
// Update (modify existing config)
|
||||||
|
export interface UpdateRequest {
|
||||||
|
id: ProcessId;
|
||||||
|
updates: Partial<Omit<IProcessConfig, 'id'>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UpdateResponse {
|
||||||
|
id: ProcessId;
|
||||||
|
config: IProcessConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve a user-provided target (id:n or name:foo or numeric string) to a ProcessId
|
||||||
|
export interface ResolveTargetRequest {
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResolveTargetResponse {
|
||||||
|
id: ProcessId;
|
||||||
|
name?: string;
|
||||||
|
}
|
||||||
|
|
||||||
// Type mappings for methods
|
// Type mappings for methods
|
||||||
export type IpcMethodMap = {
|
export type IpcMethodMap = {
|
||||||
start: { request: StartRequest; response: StartResponse };
|
start: { request: StartRequest; response: StartResponse };
|
||||||
|
startById: { request: StartByIdRequest; response: StartByIdResponse };
|
||||||
stop: { request: StopRequest; response: StopResponse };
|
stop: { request: StopRequest; response: StopResponse };
|
||||||
restart: { request: RestartRequest; response: RestartResponse };
|
restart: { request: RestartRequest; response: RestartResponse };
|
||||||
delete: { request: DeleteRequest; response: DeleteResponse };
|
delete: { request: DeleteRequest; response: DeleteResponse };
|
||||||
|
add: { request: AddRequest; response: AddResponse };
|
||||||
|
update: { request: UpdateRequest; response: UpdateResponse };
|
||||||
list: { request: ListRequest; response: ListResponse };
|
list: { request: ListRequest; response: ListResponse };
|
||||||
describe: { request: DescribeRequest; response: DescribeResponse };
|
describe: { request: DescribeRequest; response: DescribeResponse };
|
||||||
getLogs: { request: GetLogsRequest; response: GetLogsResponse };
|
getLogs: { request: GetLogsRequest; response: GetLogsResponse };
|
||||||
startAll: { request: StartAllRequest; response: StartAllResponse };
|
startAll: { request: StartAllRequest; response: StartAllResponse };
|
||||||
stopAll: { request: StopAllRequest; response: StopAllResponse };
|
stopAll: { request: StopAllRequest; response: StopAllResponse };
|
||||||
restartAll: { request: RestartAllRequest; response: RestartAllResponse };
|
restartAll: { request: RestartAllRequest; response: RestartAllResponse };
|
||||||
|
reset: { request: ResetRequest; response: ResetResponse };
|
||||||
'daemon:status': {
|
'daemon:status': {
|
||||||
request: DaemonStatusRequest;
|
request: DaemonStatusRequest;
|
||||||
response: DaemonStatusResponse;
|
response: DaemonStatusResponse;
|
||||||
@@ -221,6 +287,7 @@ export type IpcMethodMap = {
|
|||||||
response: DaemonShutdownResponse;
|
response: DaemonShutdownResponse;
|
||||||
};
|
};
|
||||||
heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse };
|
heartbeat: { request: HeartbeatRequest; response: HeartbeatResponse };
|
||||||
|
resolveTarget: { request: ResolveTargetRequest; response: ResolveTargetResponse };
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper type to extract request type for a method
|
// Helper type to extract request type for a method
|
||||||
|
Reference in New Issue
Block a user