@cruxy/cli 0.29.0 → 0.29.3

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,12 +17,8 @@
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 declare function killTree(pid: number | undefined): void;
20
+ import { killTree } from "./process-tree.js";
21
+ export { killTree };
24
22
  /**
25
23
  * Force-kill the process group of every tracked-but-not-yet-shut-down child,
26
24
  * then forget them. This is exactly what the `exit`/`SIGINT`/`SIGTERM`/`SIGHUP`
@@ -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,81 @@
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 — three
19
+ * POSIX-only `killTree` variants (run_command, the test runner, the LSP/MCP
20
+ * backstop), each of which silently orphaned grandchildren on Windows. There is
21
+ * now one implementation; a change to the kill discipline changes every path.
22
+ */
23
+ const isWindows = process.platform === "win32";
24
+ /**
25
+ * Spawn a child as the head of a killable process tree, applying the
26
+ * platform-correct grouping options on top of the caller's own (`shell`, `cwd`,
27
+ * `stdio`, `env`, …). Any `detached`/`windowsHide` the caller passes is
28
+ * overridden — grouping is this module's responsibility, not the call site's.
29
+ */
30
+ export function spawnTree(command, args = [], options = {}) {
31
+ const grouped = isWindows
32
+ ? { ...options, detached: false, windowsHide: true }
33
+ : { ...options, detached: true };
34
+ return spawn(command, args, grouped);
35
+ }
36
+ /**
37
+ * Kill a child's ENTIRE process tree — the child and every descendant it
38
+ * spawned. POSIX: negative-PID `SIGKILL` targets the process group created by
39
+ * {@link spawnTree}'s `detached`. win32: `taskkill /PID <pid> /T /F` walks the
40
+ * OS tree (`/T`) and force-terminates it (`/F`). Fire-and-forget and
41
+ * error-swallowing on both paths — the tree may already be gone.
42
+ */
43
+ export function killTree(pid) {
44
+ if (pid === undefined)
45
+ return;
46
+ if (isWindows) {
47
+ killTreeWindows(pid);
48
+ return;
49
+ }
50
+ try {
51
+ // Negative PID = "the whole group led by `pid`", not just `pid`.
52
+ process.kill(-pid, "SIGKILL");
53
+ }
54
+ catch {
55
+ // Already exited, or no group — nothing to kill.
56
+ }
57
+ }
58
+ /**
59
+ * Reap `pid`'s tree via `taskkill`. The killer is itself a child process, so we
60
+ * reap IT too: `stdio: "ignore"` gives it nothing to block on, the `error`
61
+ * handler swallows a missing-`taskkill`/EPIPE rejection (an unhandled `error`
62
+ * event would otherwise crash the process), and `unref` keeps this short-lived
63
+ * helper from holding the event loop open. taskkill exits on its own; we neither
64
+ * wait for it nor let it leak.
65
+ */
66
+ function killTreeWindows(pid) {
67
+ try {
68
+ const killer = spawn("taskkill", ["/PID", String(pid), "/T", "/F"], {
69
+ stdio: "ignore",
70
+ windowsHide: true,
71
+ });
72
+ killer.on("error", () => {
73
+ // taskkill unavailable (should not happen on win32) — nothing more to do.
74
+ });
75
+ killer.unref();
76
+ }
77
+ catch {
78
+ // Even the spawn attempt failed — the tree, if any, outlives us. Nothing
79
+ // more we can do from here.
80
+ }
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cruxy/cli",
3
- "version": "0.29.0",
3
+ "version": "0.29.3",
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",
@@ -37,7 +36,10 @@
37
36
  "undici": "^6.21.0",
38
37
  "zod": "^3.23.8",
39
38
  "zod-to-json-schema": "^3.23.5",
40
- "@cruxy/sdk": "0.2.0"
39
+ "@cruxy/sdk": "0.2.1"
40
+ },
41
+ "optionalDependencies": {
42
+ "better-sqlite3": "^12.11.1"
41
43
  },
42
44
  "devDependencies": {
43
45
  "@types/better-sqlite3": "^7.6.13",
@@ -1,34 +0,0 @@
1
- import type { Usage } from "@cruxy/sdk";
2
- import type { LoopBudget } from "../agent/loop.js";
3
- import type { BudgetLimits } from "./types.js";
4
- /**
5
- * The subagent budget (C.14): iteration + token + optional wall-clock caps,
6
- * checked by the agent loop before every model turn (see `LoopBudget`). A
7
- * tripped cap stops the run with a human-readable reason — the subagent
8
- * returns a partial result, it never runs unbounded.
9
- */
10
- /**
11
- * Resolve the effective limits for one spawn: start from the configured
12
- * ceilings and let overrides only *narrow* them. A request above a ceiling is
13
- * clamped down, not honored — "budget overrides within limits" by construction.
14
- */
15
- export declare function resolveBudget(defaults: BudgetLimits, overrides?: Partial<BudgetLimits>): BudgetLimits;
16
- /**
17
- * A live budget for one subagent run. The wall clock starts at construction
18
- * (spawn time); the clock source is injectable so tests never sleep.
19
- */
20
- export declare class Budget implements LoopBudget {
21
- private readonly limits;
22
- private readonly now;
23
- private readonly startedAt;
24
- constructor(limits: BudgetLimits, now?: () => number);
25
- /**
26
- * The reason to stop before the next model turn, or `null` to continue.
27
- * Checked at iteration boundaries — the in-flight turn always completes, so
28
- * overshoot is bounded by one turn.
29
- */
30
- exceeded(state: {
31
- iterations: number;
32
- usage: Usage;
33
- }): string | null;
34
- }