claudish 7.50.0 → 7.52.0

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.
package/bin/claudish.cjs CHANGED
@@ -3,8 +3,38 @@
3
3
  // Launcher script: checks for Bun runtime before starting claudish.
4
4
  // Claudish uses Bun-specific APIs (bun:ffi for TUI, Bun.spawn, etc.)
5
5
  // so it cannot run under Node.js directly.
6
+ //
7
+ // ── Why this is spawn() and not spawnSync() ──────────────────────────────────
8
+ //
9
+ // This process is a THIN WRAPPER around the real CLI, and it is the process
10
+ // every caller's signal actually lands on. It used to run the child with
11
+ // `spawnSync(bun, ..., { stdio: "inherit" })`, which made the whole tree
12
+ // unkillable in a way that looked like the child was ignoring signals:
13
+ //
14
+ // · `spawnSync` blocks the event loop, so this process cannot forward
15
+ // anything while the child runs.
16
+ // · `stdio: "inherit"` gives the Bun child THIS process's fd 0/1/2. When a
17
+ // caller pipes our stdout, the Bun child holds the write end of that pipe.
18
+ // · So SIGTERM killed only this launcher. The Bun child was ORPHANED, not
19
+ // killed — it kept running, kept billing, and kept the caller's pipe open,
20
+ // writing into it long after the caller had given up.
21
+ //
22
+ // Measured 2026-08-15 with a repro of this exact topology: SIGTERM to the
23
+ // launcher pid leaked 330 B of post-kill output, and so did SIGKILL — because
24
+ // the problem is signalling the wrong process, not a stubborn one. It cost a
25
+ // real `team` run: a model declared TIMEOUT with 0 B wrote a complete 40,699 B
26
+ // answer into the response file 375 s later, and was scored as empty.
27
+ //
28
+ // Now: async spawn, forward the signals a supervisor actually sends, and exit
29
+ // with the child's own status. A caller that terminates `claudish` terminates
30
+ // claudish.
31
+ //
32
+ // This does NOT cover being SIGKILLed ourselves — nothing running in this
33
+ // process can. Callers that need a hard guarantee should spawn claudish
34
+ // `detached` and signal the process GROUP, which reaches the Bun child (and its
35
+ // own descendants) directly; `team-orchestrator.ts` does exactly that.
6
36
 
7
- const { execFileSync, execSync } = require("node:child_process");
37
+ const { execFileSync, execSync, spawn } = require("node:child_process");
8
38
  const { resolve } = require("node:path");
9
39
 
10
40
  /**
@@ -75,18 +105,64 @@ Learn more: https://bun.sh`);
75
105
  process.exit(1);
76
106
  }
77
107
 
78
- // Exec into bun with the real entry point
79
108
  const entry = resolve(__dirname, "..", "dist", "index.js");
109
+
110
+ let child;
80
111
  try {
81
- const result = require("node:child_process").spawnSync(bun, [entry, ...process.argv.slice(2)], {
112
+ child = spawn(bun, [entry, ...process.argv.slice(2)], {
82
113
  stdio: "inherit",
83
114
  env: process.env,
84
115
  });
85
- process.exit(result.status ?? 1);
86
116
  } catch (err) {
87
117
  console.error("Failed to start claudish:", err.message);
88
118
  process.exit(1);
89
119
  }
120
+
121
+ // Forward the signals a supervisor, terminal, or parent process actually sends.
122
+ // SIGKILL is deliberately absent — it cannot be caught, which is the whole
123
+ // reason callers wanting a hard kill must target the process group instead.
124
+ //
125
+ // Handlers are installed only AFTER a successful spawn, so a signal arriving
126
+ // during startup keeps Node's default terminate-immediately behaviour rather
127
+ // than being swallowed by a forward to a child that does not exist yet.
128
+ const FORWARDED =
129
+ process.platform === "win32" ? ["SIGINT", "SIGTERM"] : ["SIGINT", "SIGTERM", "SIGHUP"];
130
+ for (const signal of FORWARDED) {
131
+ process.on(signal, () => {
132
+ // Best-effort: the child may have exited between the signal and this line.
133
+ try {
134
+ if (!child.killed) child.kill(signal);
135
+ } catch {}
136
+ // Do NOT exit here. Waiting for the child's own "exit" keeps our status
137
+ // honest and, more importantly, keeps this process alive as long as the
138
+ // child is — a caller watching OUR pid must not see us disappear while the
139
+ // real work is still running.
140
+ });
141
+ }
142
+
143
+ child.on("error", (err) => {
144
+ console.error("Failed to start claudish:", err.message);
145
+ process.exit(1);
146
+ });
147
+
148
+ child.on("exit", (code, signal) => {
149
+ if (signal) {
150
+ // Re-raise the signal on ourselves so our parent observes the same cause
151
+ // of death the child had, rather than a synthesised exit code. The handler
152
+ // is removed first, or we would forward it straight back to a dead child.
153
+ process.removeAllListeners(signal);
154
+ try {
155
+ process.kill(process.pid, signal);
156
+ return;
157
+ } catch {
158
+ // Re-raise failed (unsupported signal on this platform) — fall back to
159
+ // the shell convention for "died from signal N".
160
+ const NUMBERS = { SIGINT: 2, SIGQUIT: 3, SIGKILL: 9, SIGTERM: 15, SIGHUP: 1 };
161
+ process.exit(128 + (NUMBERS[signal] ?? 0));
162
+ }
163
+ }
164
+ process.exit(code ?? 1);
165
+ });
90
166
  }
91
167
 
92
168
  // Running it as a program launches. Requiring it exposes the helpers so the