@bivy/bivy 0.17.2-staging.5 → 0.17.2-staging.6
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/dist/command-launch.js +51 -6
- package/dist/runtime/process.js +2 -2
- package/dist/server.js +15 -26
- package/dist/terminal.js +1 -1
- package/package.json +1 -1
- package/dist/pty-runner.py +0 -95
package/dist/command-launch.js
CHANGED
|
@@ -1,12 +1,57 @@
|
|
|
1
1
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
2
2
|
// Copyright (c) 2026 Petter André Sjulstad
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { loadPty } from "./terminal.js";
|
|
6
|
+
const signalNames = new Map(Object.entries(os.constants.signals).map(([name, num]) => [num, name]));
|
|
3
7
|
/**
|
|
4
8
|
* Keep terminal semantics opt-in for short-lived commands. Native interactive
|
|
5
|
-
* commands must set requiresTty
|
|
6
|
-
*
|
|
9
|
+
* commands must set requiresTty and run under node-pty (the same dependency the
|
|
10
|
+
* interactive terminals use); ordinary commands stay on direct pipes.
|
|
7
11
|
*/
|
|
8
|
-
export function
|
|
9
|
-
return requiresTty
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
export function launchCommand(spec, requiresTty, handlers) {
|
|
13
|
+
return requiresTty ? launchPty(spec, handlers) : launchPipes(spec, handlers);
|
|
14
|
+
}
|
|
15
|
+
function launchPipes({ command, args, cwd, env }, handlers) {
|
|
16
|
+
const child = spawn(command, args, { cwd, env });
|
|
17
|
+
child.stdout.on("data", (data) => handlers.onOutput("stdout", String(data)));
|
|
18
|
+
child.stderr.on("data", (data) => handlers.onOutput("stderr", String(data)));
|
|
19
|
+
child.on("error", handlers.onError);
|
|
20
|
+
child.on("exit", handlers.onExit);
|
|
21
|
+
return {
|
|
22
|
+
usesPty: false,
|
|
23
|
+
write: (text) => child.stdin.write(text),
|
|
24
|
+
interrupt: () => child.kill("SIGINT"),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function launchPty({ command, args, cwd, env }, handlers) {
|
|
28
|
+
let proc;
|
|
29
|
+
try {
|
|
30
|
+
proc = loadPty().spawn(command, args, { name: env.TERM ?? "xterm-256color", cols: 120, rows: 40, cwd, env });
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
// Match child_process: a launch failure is reported asynchronously, not thrown.
|
|
34
|
+
queueMicrotask(() => handlers.onError(error instanceof Error ? error : new Error(String(error))));
|
|
35
|
+
return { usesPty: true, write: () => { }, interrupt: () => { } };
|
|
36
|
+
}
|
|
37
|
+
// A PTY merges stdout and stderr into one stream.
|
|
38
|
+
proc.onData((text) => handlers.onOutput("stdout", text));
|
|
39
|
+
proc.onExit(({ exitCode, signal }) => handlers.onExit(signal ? null : exitCode, signal ? signalNames.get(signal) ?? null : null));
|
|
40
|
+
return {
|
|
41
|
+
usesPty: true,
|
|
42
|
+
write: (text) => proc.write(text),
|
|
43
|
+
interrupt: () => {
|
|
44
|
+
// node-pty makes the child a session (and process-group) leader, so a
|
|
45
|
+
// negative pid reaches anything it forked too.
|
|
46
|
+
try {
|
|
47
|
+
process.kill(-proc.pid, "SIGINT");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
try {
|
|
51
|
+
proc.kill("SIGINT");
|
|
52
|
+
}
|
|
53
|
+
catch { /* already exited */ }
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
};
|
|
12
57
|
}
|
package/dist/runtime/process.js
CHANGED
|
@@ -16,8 +16,8 @@ import { withExactCapabilitySurface } from "./types.js";
|
|
|
16
16
|
* worker processes) die with it instead of being orphaned. Relies on the child
|
|
17
17
|
* having been spawned `detached` (making it its own process-group leader, POSIX
|
|
18
18
|
* only — see the spawn() call in ProcessSession.prompt); `process.kill(-pid,
|
|
19
|
-
* signal)` then targets the whole group,
|
|
20
|
-
*
|
|
19
|
+
* signal)` then targets the whole group, as the PTY command launcher does
|
|
20
|
+
* (src/command-launch.ts). Falls back to killing just the direct child on Windows (no
|
|
21
21
|
* negative-pid group kill there) or if the group is already gone.
|
|
22
22
|
*/
|
|
23
23
|
function killProcessGroup(child, signal) {
|
package/dist/server.js
CHANGED
|
@@ -91,7 +91,7 @@ import { hostedCustodyNode } from "./hosted-custody.js";
|
|
|
91
91
|
import { PolicyEngine } from "./policy/policy-engine.js";
|
|
92
92
|
import { SessionAllowRules } from "./policy/session-allow.js";
|
|
93
93
|
import { TerminalManager } from "./terminal.js";
|
|
94
|
-
import {
|
|
94
|
+
import { launchCommand } from "./command-launch.js";
|
|
95
95
|
import { listMultiplexerSessions } from "./multiplexer.js";
|
|
96
96
|
import { createWorktree, removeWorktree, gitRepoRoot } from "./worktree.js";
|
|
97
97
|
import { HarnessManager } from "./harness/manager.js";
|
|
@@ -344,10 +344,6 @@ const worktreeCleanupSweepMs = Math.max(60 * 60 * 1000, Math.min(worktreeRetenti
|
|
|
344
344
|
// Native Pi commands run the operator-installed agent. Bivy does not substitute
|
|
345
345
|
// a private TUI; BIVY_PI_COMMAND is an explicit path override for managed nodes.
|
|
346
346
|
const piCommand = process.env.BIVY_PI_COMMAND?.trim() || "pi";
|
|
347
|
-
const ptyRunnerScript = process.env.BIVY_PTY_RUNNER ?? (fs.existsSync(path.join(assetRoot, "src", "pty-runner.py"))
|
|
348
|
-
? path.join(assetRoot, "src", "pty-runner.py")
|
|
349
|
-
: path.join(assetRoot, "dist", "pty-runner.py"));
|
|
350
|
-
const pythonCommand = process.env.PYTHON ?? "python3";
|
|
351
347
|
// Per-process secret gating the loopback bootstrap endpoint. Published to a
|
|
352
348
|
// 0600 file (and stdout) so only the launching user can read it.
|
|
353
349
|
const bootstrapSecret = randomBytes(32).toString("base64url");
|
|
@@ -5619,26 +5615,19 @@ function runNativeCommand(command) {
|
|
|
5619
5615
|
NO_COLOR: "1",
|
|
5620
5616
|
};
|
|
5621
5617
|
// Native commands (login/model/config/etc.) need a TTY for prompts and the
|
|
5622
|
-
// terminal UI
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
});
|
|
5634
|
-
child.on("error", (error) => {
|
|
5635
|
-
commandProcesses.delete(runId);
|
|
5636
|
-
broadcast({ type: "command.error", runId, command: command.name, error: String(error?.stack ?? error) });
|
|
5637
|
-
});
|
|
5638
|
-
child.on("exit", (code, signal) => {
|
|
5639
|
-
commandProcesses.delete(runId);
|
|
5640
|
-
broadcast({ type: "command.exited", runId, command: command.name, code, signal });
|
|
5618
|
+
// terminal UI, so they run under a PTY; everything else uses ordinary pipes.
|
|
5619
|
+
const handle = launchCommand({ command: command.spawn.command, args: command.spawn.args, cwd: repoRoot, env }, command.spawn.requiresTty, {
|
|
5620
|
+
onOutput: (stream, text) => broadcast({ type: "command.output", runId, command: command.name, stream, text: stripAnsi(text) }),
|
|
5621
|
+
onError: (error) => {
|
|
5622
|
+
commandProcesses.delete(runId);
|
|
5623
|
+
broadcast({ type: "command.error", runId, command: command.name, error: String(error?.stack ?? error) });
|
|
5624
|
+
},
|
|
5625
|
+
onExit: (code, signal) => {
|
|
5626
|
+
commandProcesses.delete(runId);
|
|
5627
|
+
broadcast({ type: "command.exited", runId, command: command.name, code, signal });
|
|
5628
|
+
},
|
|
5641
5629
|
});
|
|
5630
|
+
commandProcesses.set(runId, handle);
|
|
5642
5631
|
return { ok: true, runId };
|
|
5643
5632
|
}
|
|
5644
5633
|
const guardianInterceptorImpl = async ({ sessionId, toolName, input, signal }) => {
|
|
@@ -11087,14 +11076,14 @@ app.post("/api/commands/:runId/input", (req, res) => {
|
|
|
11087
11076
|
const child = commandProcesses.get(req.params.runId);
|
|
11088
11077
|
if (!child)
|
|
11089
11078
|
return res.status(404).json({ error: "No active command process" });
|
|
11090
|
-
child.
|
|
11079
|
+
child.write(`${String(req.body?.text ?? "")}\n`);
|
|
11091
11080
|
res.json({ ok: true });
|
|
11092
11081
|
});
|
|
11093
11082
|
app.post("/api/commands/:runId/terminate", (req, res) => {
|
|
11094
11083
|
const child = commandProcesses.get(req.params.runId);
|
|
11095
11084
|
if (!child)
|
|
11096
11085
|
return res.status(404).json({ error: "No active command process" });
|
|
11097
|
-
child.
|
|
11086
|
+
child.interrupt();
|
|
11098
11087
|
res.json({ ok: true });
|
|
11099
11088
|
});
|
|
11100
11089
|
// On-demand "update GitHub status" for one session: force `refreshPullRequests`
|
package/dist/terminal.js
CHANGED
|
@@ -6,7 +6,7 @@ import { createRequire } from "node:module";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { depCacheEnv } from "./harness/dep-cache.js";
|
|
8
8
|
let ptyModule;
|
|
9
|
-
function loadPty() {
|
|
9
|
+
export function loadPty() {
|
|
10
10
|
if (ptyModule)
|
|
11
11
|
return ptyModule;
|
|
12
12
|
if (ptyModule === null) {
|
package/package.json
CHANGED
package/dist/pty-runner.py
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Run a command under a pseudo-terminal while relaying stdio over pipes."""
|
|
3
|
-
|
|
4
|
-
import os
|
|
5
|
-
import pty
|
|
6
|
-
import select
|
|
7
|
-
import signal
|
|
8
|
-
import subprocess
|
|
9
|
-
import sys
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def main() -> int:
|
|
13
|
-
if len(sys.argv) < 2:
|
|
14
|
-
print("usage: pty-runner.py command [args...]", file=sys.stderr)
|
|
15
|
-
return 2
|
|
16
|
-
|
|
17
|
-
master_fd, slave_fd = pty.openpty()
|
|
18
|
-
env = os.environ.copy()
|
|
19
|
-
env.setdefault("TERM", "xterm-256color")
|
|
20
|
-
|
|
21
|
-
proc = subprocess.Popen(
|
|
22
|
-
sys.argv[1:],
|
|
23
|
-
stdin=slave_fd,
|
|
24
|
-
stdout=slave_fd,
|
|
25
|
-
stderr=slave_fd,
|
|
26
|
-
env=env,
|
|
27
|
-
close_fds=True,
|
|
28
|
-
start_new_session=True,
|
|
29
|
-
)
|
|
30
|
-
os.close(slave_fd)
|
|
31
|
-
|
|
32
|
-
def forward_signal(signum, _frame):
|
|
33
|
-
try:
|
|
34
|
-
os.killpg(proc.pid, signum)
|
|
35
|
-
except ProcessLookupError:
|
|
36
|
-
pass
|
|
37
|
-
|
|
38
|
-
signal.signal(signal.SIGINT, forward_signal)
|
|
39
|
-
signal.signal(signal.SIGTERM, forward_signal)
|
|
40
|
-
|
|
41
|
-
stdin_fd = sys.stdin.fileno()
|
|
42
|
-
stdout_fd = sys.stdout.fileno()
|
|
43
|
-
stdin_open = True
|
|
44
|
-
|
|
45
|
-
while True:
|
|
46
|
-
read_fds = [master_fd]
|
|
47
|
-
if stdin_open:
|
|
48
|
-
read_fds.append(stdin_fd)
|
|
49
|
-
|
|
50
|
-
readable, _, _ = select.select(read_fds, [], [], 0.1)
|
|
51
|
-
|
|
52
|
-
if master_fd in readable:
|
|
53
|
-
try:
|
|
54
|
-
data = os.read(master_fd, 8192)
|
|
55
|
-
except OSError:
|
|
56
|
-
data = b""
|
|
57
|
-
if not data:
|
|
58
|
-
break
|
|
59
|
-
os.write(stdout_fd, data)
|
|
60
|
-
|
|
61
|
-
if stdin_open and stdin_fd in readable:
|
|
62
|
-
data = os.read(stdin_fd, 8192)
|
|
63
|
-
if not data:
|
|
64
|
-
stdin_open = False
|
|
65
|
-
else:
|
|
66
|
-
try:
|
|
67
|
-
os.write(master_fd, data)
|
|
68
|
-
except OSError:
|
|
69
|
-
break
|
|
70
|
-
|
|
71
|
-
if proc.poll() is not None:
|
|
72
|
-
# Drain any final PTY output.
|
|
73
|
-
while True:
|
|
74
|
-
readable, _, _ = select.select([master_fd], [], [], 0)
|
|
75
|
-
if not readable:
|
|
76
|
-
break
|
|
77
|
-
try:
|
|
78
|
-
data = os.read(master_fd, 8192)
|
|
79
|
-
except OSError:
|
|
80
|
-
break
|
|
81
|
-
if not data:
|
|
82
|
-
break
|
|
83
|
-
os.write(stdout_fd, data)
|
|
84
|
-
break
|
|
85
|
-
|
|
86
|
-
try:
|
|
87
|
-
os.close(master_fd)
|
|
88
|
-
except OSError:
|
|
89
|
-
pass
|
|
90
|
-
|
|
91
|
-
return proc.wait()
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if __name__ == "__main__":
|
|
95
|
-
raise SystemExit(main())
|