@awebai/oats 0.22.1 → 0.22.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.
package/lib/herdr.mjs ADDED
@@ -0,0 +1,95 @@
1
+ /** Herdr 0.8.x host-local session backend. SSH belongs to the CLI router. */
2
+ import { execFileSync, spawn } from "node:child_process";
3
+ import { existsSync, mkdirSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { dirname, join, resolve } from "node:path";
6
+
7
+ export const HERDR_PROTOCOL = 20;
8
+ const quote = (s) => `'${String(s).replaceAll("'", "'\\''")}'`;
9
+ const pause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
10
+
11
+ export function herdrSocket(session = "oats") {
12
+ if (!/^[a-zA-Z0-9_-]+$/.test(session)) throw new Error("invalid Herdr session name");
13
+ return join(process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), "herdr", "sessions", session, "herdr.sock");
14
+ }
15
+
16
+ export function herdrCommand(target, args, { timeout = 10000, exec = execFileSync } = {}) {
17
+ const env = { ...process.env, HERDR_SOCKET_PATH: target.socket };
18
+ delete env.HERDR_SESSION;
19
+ const output = exec(target.binary || "herdr", args, { env, encoding: "utf8", timeout, maxBuffer: 8 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
20
+ // Some successful terminal mutations have no output; reads below require
21
+ // their documented result shape independently.
22
+ if (!output.trim()) return {};
23
+ const response = JSON.parse(output);
24
+ if (response.error) throw Object.assign(new Error(response.error.message), { code: response.error.code });
25
+ if (!response.result) throw new Error("Herdr returned no result");
26
+ return response.result;
27
+ }
28
+
29
+ export function herdrSnapshot(target, io) {
30
+ const snapshot = herdrCommand(target, ["api", "snapshot"], io).snapshot;
31
+ if (snapshot?.protocol !== HERDR_PROTOCOL || !Array.isArray(snapshot.panes)) {
32
+ throw new Error(`unsupported Herdr protocol ${snapshot?.protocol}; OATS requires ${HERDR_PROTOCOL}`);
33
+ }
34
+ return snapshot;
35
+ }
36
+
37
+ export function ensureHerdr({ binary = "herdr", socket, session = "oats" } = {}) {
38
+ const target = { backend: "herdr", binary, socket: resolve(socket || herdrSocket(session)), protocol: HERDR_PROTOCOL };
39
+ try { herdrSnapshot(target); return target; }
40
+ catch (e) {
41
+ // An explicit socket belongs to an operator-managed server. Never start a
42
+ // different server when it cannot be inspected or is incompatible.
43
+ if (socket || existsSync(target.socket)) throw e;
44
+ }
45
+ mkdirSync(dirname(target.socket), { recursive: true });
46
+ const env = { ...process.env };
47
+ delete env.HERDR_SESSION; delete env.HERDR_SOCKET_PATH;
48
+ const child = spawn(binary, ["--session", session, "server"], { detached: true, stdio: "ignore", env });
49
+ child.on("error", () => {});
50
+ child.unref();
51
+ let error;
52
+ for (let i = 0; i < 30; i++) {
53
+ pause(100);
54
+ try { herdrSnapshot(target); return target; } catch (e) { error = e; }
55
+ }
56
+ throw new Error(`Herdr did not start: ${error?.message || "no socket"}`);
57
+ }
58
+
59
+ export function allocateHerdr(target, { home, instance }, io) {
60
+ const result = herdrCommand(target, ["workspace", "create", "--cwd", home, "--label", instance, "--no-focus"], io);
61
+ const pane = result.root_pane;
62
+ if (!pane?.pane_id || !pane?.terminal_id || !pane?.workspace_id) throw new Error("Herdr allocation returned no session identity");
63
+ return { ...target, workspaceId: pane.workspace_id, paneId: pane.pane_id, terminalId: pane.terminal_id };
64
+ }
65
+
66
+ export function inspectHerdr(target, io) {
67
+ const snapshot = herdrSnapshot(target, io);
68
+ const pane = snapshot.panes.find((p) => p.pane_id === target.paneId && p.terminal_id === target.terminalId);
69
+ const agent = pane && snapshot.agents?.find((a) => a.terminal_id === target.terminalId);
70
+ return { present: !!pane, pane, agent, status: agent?.agent_status || "unknown" };
71
+ }
72
+
73
+ export function launchHerdr(target, command, io) {
74
+ if (!inspectHerdr(target, io).present) throw new Error("Herdr launch pane disappeared");
75
+ // This is the allocated shell's one launch command. Exec avoids a fallback
76
+ // shell that could accidentally consume a subsequent agent notification.
77
+ herdrCommand(target, ["pane", "run", target.paneId, `exec /bin/sh -c ${quote(command)}`], io);
78
+ }
79
+
80
+ export function stopHerdr(target, io) {
81
+ if (!inspectHerdr(target, io).present) return;
82
+ try { herdrCommand(target, ["pane", "close", target.paneId], io); } catch { /* inspect the effect even if the pane exited concurrently */ }
83
+ if (inspectHerdr(target, io).present) throw new Error(`Herdr pane ${target.paneId} is still present`);
84
+ }
85
+
86
+ export function inputHerdr(target, text, io) {
87
+ if (!inspectHerdr(target, io).present) throw new Error("Herdr session stopped or was replaced");
88
+ herdrCommand(target, ["pane", "run", target.paneId, text], io);
89
+ }
90
+
91
+ export function validHerdrTarget(value) {
92
+ return value?.backend === "herdr" && value.protocol === HERDR_PROTOCOL
93
+ && [value.socket, value.binary, value.workspaceId, value.paneId, value.terminalId].every((v) => typeof v === "string" && v.length > 0)
94
+ && value.socket === resolve(value.socket);
95
+ }