@gonrocca/zero-pi 0.1.13 → 0.1.14
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/extensions/win-tree-kill.ts +114 -0
- package/package.json +3 -1
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// zero-pi — Windows process-tree kill for aborts.
|
|
2
|
+
//
|
|
3
|
+
// On Windows, `ChildProcess.kill()` terminates only the target process, not
|
|
4
|
+
// its descendants. A provider like pi-claude-cli spawns `claude` through a
|
|
5
|
+
// `cmd.exe` batch wrapper (`claude` resolves to `claude.cmd`), so when pi
|
|
6
|
+
// aborts a turn the wrapper is killed but the real `claude` process is
|
|
7
|
+
// orphaned and keeps streaming — pressing Esc appears to do nothing.
|
|
8
|
+
//
|
|
9
|
+
// This extension patches `child_process.spawn` once, at load, so every
|
|
10
|
+
// subprocess spawned afterwards gets a `kill()` that terminates the whole
|
|
11
|
+
// process tree via `taskkill /T /F`. It is a no-op on non-Windows platforms.
|
|
12
|
+
//
|
|
13
|
+
// Patching the shared `child_process` module reaches code in other packages
|
|
14
|
+
// (pi-claude-cli, and the `cross-spawn` it depends on, both call into the same
|
|
15
|
+
// builtin) without modifying them — so the fix survives `pi update`.
|
|
16
|
+
|
|
17
|
+
import { createRequire } from "node:module";
|
|
18
|
+
|
|
19
|
+
/** Build the Windows command that kills a process and its whole tree. */
|
|
20
|
+
export function treeKillCommand(pid: number): string {
|
|
21
|
+
return `taskkill /pid ${pid} /t /f`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The slice of a child process this extension touches. */
|
|
25
|
+
export interface KillableChild {
|
|
26
|
+
pid?: number;
|
|
27
|
+
kill(signal?: NodeJS.Signals | number): boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Marker so a `kill` is wrapped at most once. */
|
|
31
|
+
const WRAPPED = Symbol.for("zero-pi.win-tree-kill.wrapped");
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Replace `child.kill` with one that terminates the whole process tree by
|
|
35
|
+
* running `exec(treeKillCommand(pid))`. Falls back to the original `kill` when
|
|
36
|
+
* there is no pid or the tree-kill throws. Idempotent — wrapping an already
|
|
37
|
+
* wrapped child is a no-op. Exported for tests.
|
|
38
|
+
*/
|
|
39
|
+
export function wrapKill(
|
|
40
|
+
child: KillableChild,
|
|
41
|
+
exec: (command: string) => void,
|
|
42
|
+
): KillableChild {
|
|
43
|
+
const original = child.kill;
|
|
44
|
+
if (typeof original !== "function") return child;
|
|
45
|
+
|
|
46
|
+
const tagged = original as typeof original & { [WRAPPED]?: boolean };
|
|
47
|
+
if (tagged[WRAPPED]) return child;
|
|
48
|
+
|
|
49
|
+
const wrapped = function (signal?: NodeJS.Signals | number): boolean {
|
|
50
|
+
const pid = child.pid;
|
|
51
|
+
if (typeof pid === "number") {
|
|
52
|
+
try {
|
|
53
|
+
exec(treeKillCommand(pid));
|
|
54
|
+
return true;
|
|
55
|
+
} catch {
|
|
56
|
+
// Process already gone, or taskkill unavailable — fall through.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return original.call(child, signal);
|
|
60
|
+
} as KillableChild["kill"] & { [WRAPPED]?: boolean };
|
|
61
|
+
wrapped[WRAPPED] = true;
|
|
62
|
+
|
|
63
|
+
child.kill = wrapped;
|
|
64
|
+
return child;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Whether the running platform needs the tree-kill patch. */
|
|
68
|
+
export function shouldPatch(platform: string): boolean {
|
|
69
|
+
return platform === "win32";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Module-level guard so the global patch is installed at most once. */
|
|
73
|
+
let patched = false;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The pi extension entry point. Patches `child_process.spawn` so every later
|
|
77
|
+
* subprocess tree-kills on `kill()`. Defensive: a failure here must never
|
|
78
|
+
* break a pi session, so it is swallowed.
|
|
79
|
+
*/
|
|
80
|
+
export default function register(): void {
|
|
81
|
+
if (patched || !shouldPatch(process.platform)) return;
|
|
82
|
+
try {
|
|
83
|
+
const require = createRequire(import.meta.url);
|
|
84
|
+
const cp = require("node:child_process") as {
|
|
85
|
+
spawn: (...args: unknown[]) => KillableChild;
|
|
86
|
+
execSync: (command: string, options?: unknown) => unknown;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const originalSpawn = cp.spawn as typeof cp.spawn & { [WRAPPED]?: boolean };
|
|
90
|
+
if (originalSpawn[WRAPPED]) {
|
|
91
|
+
patched = true;
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const exec = (command: string): void => {
|
|
96
|
+
cp.execSync(command, { stdio: "ignore" });
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const patchedSpawn = function (this: unknown, ...args: unknown[]): KillableChild {
|
|
100
|
+
const child = originalSpawn.apply(this, args);
|
|
101
|
+
try {
|
|
102
|
+
return wrapKill(child, exec);
|
|
103
|
+
} catch {
|
|
104
|
+
return child;
|
|
105
|
+
}
|
|
106
|
+
} as typeof cp.spawn & { [WRAPPED]?: boolean };
|
|
107
|
+
patchedSpawn[WRAPPED] = true;
|
|
108
|
+
|
|
109
|
+
cp.spawn = patchedSpawn;
|
|
110
|
+
patched = true;
|
|
111
|
+
} catch {
|
|
112
|
+
// Hardening must never break a session.
|
|
113
|
+
}
|
|
114
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gonrocca/zero-pi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "zero-pi — an installable layer for pi (pi.dev): the zero spec-driven development workflow, skill auto-learning, and an animated ZERO startup banner. Adds capability to pi without modifying pi.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"extensions": [
|
|
26
26
|
"./extensions/startup-banner.ts",
|
|
27
27
|
"./extensions/working-phrases.ts",
|
|
28
|
+
"./extensions/win-tree-kill.ts",
|
|
28
29
|
"./extensions/conversation-resume.ts",
|
|
29
30
|
"./extensions/zero-models.ts",
|
|
30
31
|
"./extensions/autotune-extension.ts",
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"themes",
|
|
39
40
|
"extensions/startup-banner.ts",
|
|
40
41
|
"extensions/working-phrases.ts",
|
|
42
|
+
"extensions/win-tree-kill.ts",
|
|
41
43
|
"extensions/conversation-resume.ts",
|
|
42
44
|
"extensions/zero-models.ts",
|
|
43
45
|
"extensions/autotune.ts",
|