@cruxy/cli 0.29.2 → 0.29.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,11 @@
1
1
  /**
2
- * Shared no-orphan machinery for long-lived child processes (C.12 LSP servers,
3
- * C.27 MCP servers). A child is spawned `detached` so it leads its own process
4
- * group; every kill here uses a **negative-PID** `SIGKILL` so the whole tree —
5
- * the child AND any grandchildren it forked (gopls's `go`, an MCP server's
6
- * helper) dies together.
2
+ * The process-EXIT backstop for long-lived child trees (C.12 LSP servers, C.27
3
+ * MCP servers). The platform-aware spawn/kill primitives themselves now live in
4
+ * {@link ./process-tree.js} {@link killTree} is re-exported here unchanged so
5
+ * existing LSP/MCP importers keep their import path, and so this backstop and
6
+ * those transports reap trees the SAME way on every platform (negative-PID
7
+ * `SIGKILL` on POSIX, `taskkill /T /F` on win32 — no more orphaned grandchildren
8
+ * on Windows).
7
9
  *
8
10
  * A per-session graceful shutdown covers the normal path, but a hard exit
9
11
  * (Ctrl-C, an uncaught throw) would otherwise orphan these trees. So every live
@@ -15,26 +17,10 @@
15
17
  * and MCP (newline-delimited JSON) share the SAME backstop, so `killTrackedTrees`
16
18
  * on exit reaps both and there is a single source of truth for "no orphans".
17
19
  */
18
- /**
19
- * Kill a process's entire group (POSIX negative-PID `SIGKILL`), falling back to
20
- * a direct kill when there is no group (or on win32). Swallows errors — the
21
- * process may already be gone.
22
- */
23
- export function killTree(pid) {
24
- if (pid === undefined)
25
- return;
26
- try {
27
- process.kill(-pid, "SIGKILL");
28
- }
29
- catch {
30
- try {
31
- process.kill(pid, "SIGKILL");
32
- }
33
- catch {
34
- /* already exited */
35
- }
36
- }
37
- }
20
+ import { killTree } from "./process-tree.js";
21
+ // Re-exported so LSP/MCP transports (and their tests) keep importing `killTree`
22
+ // from here; the implementation is the shared, platform-aware one.
23
+ export { killTree };
38
24
  const livePids = new Set();
39
25
  let handlersInstalled = false;
40
26
  /**
@@ -0,0 +1,16 @@
1
+ import { type ChildProcess, type SpawnOptions } from "node:child_process";
2
+ /**
3
+ * Spawn a child as the head of a killable process tree, applying the
4
+ * platform-correct grouping options on top of the caller's own (`shell`, `cwd`,
5
+ * `stdio`, `env`, …). Any `detached`/`windowsHide` the caller passes is
6
+ * overridden — grouping is this module's responsibility, not the call site's.
7
+ */
8
+ export declare function spawnTree(command: string, args?: readonly string[], options?: SpawnOptions): ChildProcess;
9
+ /**
10
+ * Kill a child's ENTIRE process tree — the child and every descendant it
11
+ * spawned. POSIX: negative-PID `SIGKILL` targets the process group created by
12
+ * {@link spawnTree}'s `detached`. win32: `taskkill /PID <pid> /T /F` walks the
13
+ * OS tree (`/T`) and force-terminates it (`/F`). Fire-and-forget and
14
+ * error-swallowing on both paths — the tree may already be gone.
15
+ */
16
+ export declare function killTree(pid: number | undefined): void;
@@ -0,0 +1,84 @@
1
+ import { spawn, } from "node:child_process";
2
+ /**
3
+ * The single platform-aware primitive for spawning a killable process tree and
4
+ * reaping it whole. Both halves — {@link spawnTree} and {@link killTree} — MUST
5
+ * come from here as a pair, because how a child is spawned decides how its tree
6
+ * can be killed, and the two differ by platform:
7
+ *
8
+ * - POSIX: spawn `detached` so the child leads its own process group, then kill
9
+ * the whole group with a negative-PID `SIGKILL`. One signal reaps the shell
10
+ * AND everything it forked (a `shell:true` grandchild, gopls's `go`, …).
11
+ * - win32: there is no process-group signalling to lean on. `detached` there
12
+ * means "new console / new group" — the wrong semantics, and a flashing
13
+ * window. So we DON'T detach (just `windowsHide`), and kill by walking the
14
+ * real OS parent-PID tree with `taskkill /T /F`, which a negative-PID signal
15
+ * could never do on Windows (it would kill only the direct child and orphan
16
+ * the grandchildren).
17
+ *
18
+ * This module exists because that pairing used to be copy-pasted — four
19
+ * POSIX-only `killTree` variants (run_command, the test runner, the LSP/MCP
20
+ * backstop, and the docker client), each of which silently orphaned
21
+ * grandchildren on Windows. There is now one implementation; a change to the
22
+ * kill discipline changes every path. Spawn sites likewise route through
23
+ * `spawnTree` (run_command, run_tests, LSP, MCP, docker) so none re-introduces
24
+ * the win32 `detached` console-flash the grouping logic exists to avoid.
25
+ */
26
+ const isWindows = process.platform === "win32";
27
+ /**
28
+ * Spawn a child as the head of a killable process tree, applying the
29
+ * platform-correct grouping options on top of the caller's own (`shell`, `cwd`,
30
+ * `stdio`, `env`, …). Any `detached`/`windowsHide` the caller passes is
31
+ * overridden — grouping is this module's responsibility, not the call site's.
32
+ */
33
+ export function spawnTree(command, args = [], options = {}) {
34
+ const grouped = isWindows
35
+ ? { ...options, detached: false, windowsHide: true }
36
+ : { ...options, detached: true };
37
+ return spawn(command, args, grouped);
38
+ }
39
+ /**
40
+ * Kill a child's ENTIRE process tree — the child and every descendant it
41
+ * spawned. POSIX: negative-PID `SIGKILL` targets the process group created by
42
+ * {@link spawnTree}'s `detached`. win32: `taskkill /PID <pid> /T /F` walks the
43
+ * OS tree (`/T`) and force-terminates it (`/F`). Fire-and-forget and
44
+ * error-swallowing on both paths — the tree may already be gone.
45
+ */
46
+ export function killTree(pid) {
47
+ if (pid === undefined)
48
+ return;
49
+ if (isWindows) {
50
+ killTreeWindows(pid);
51
+ return;
52
+ }
53
+ try {
54
+ // Negative PID = "the whole group led by `pid`", not just `pid`.
55
+ process.kill(-pid, "SIGKILL");
56
+ }
57
+ catch {
58
+ // Already exited, or no group — nothing to kill.
59
+ }
60
+ }
61
+ /**
62
+ * Reap `pid`'s tree via `taskkill`. The killer is itself a child process, so we
63
+ * reap IT too: `stdio: "ignore"` gives it nothing to block on, the `error`
64
+ * handler swallows a missing-`taskkill`/EPIPE rejection (an unhandled `error`
65
+ * event would otherwise crash the process), and `unref` keeps this short-lived
66
+ * helper from holding the event loop open. taskkill exits on its own; we neither
67
+ * wait for it nor let it leak.
68
+ */
69
+ function killTreeWindows(pid) {
70
+ try {
71
+ const killer = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
72
+ stdio: "ignore",
73
+ windowsHide: true,
74
+ });
75
+ killer.on("error", () => {
76
+ // taskkill unavailable (should not happen on win32) — nothing more to do.
77
+ });
78
+ killer.unref();
79
+ }
80
+ catch {
81
+ // Even the spawn attempt failed — the tree, if any, outlives us. Nothing
82
+ // more we can do from here.
83
+ }
84
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.29.2",
3
+ "version": "0.29.4",
4
4
  "description": "an agentic coding CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,6 @@
29
29
  "directory": "packages/cli"
30
30
  },
31
31
  "dependencies": {
32
- "better-sqlite3": "^12.11.1",
33
32
  "commander": "^12.1.0",
34
33
  "fastembed": "^2.1.0",
35
34
  "picocolors": "^1.1.1",
@@ -39,6 +38,9 @@
39
38
  "zod-to-json-schema": "^3.23.5",
40
39
  "@cruxy/sdk": "0.2.1"
41
40
  },
41
+ "optionalDependencies": {
42
+ "better-sqlite3": "^12.11.1"
43
+ },
42
44
  "devDependencies": {
43
45
  "@types/better-sqlite3": "^7.6.13",
44
46
  "@types/node": "^22.10.0",