feat: package Pi extensions as Bun workspaces
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
/** Herdr surface backend. Subagents run in unfocused tabs in the current workspace. */
|
||||
import { execFile, execFileSync } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const commandAvailability = new Map<string, boolean>();
|
||||
const tabByPane = new Map<string, string>();
|
||||
|
||||
interface HerdrTabCreated {
|
||||
result?: {
|
||||
root_pane?: { pane_id?: unknown };
|
||||
tab?: { tab_id?: unknown };
|
||||
};
|
||||
}
|
||||
|
||||
interface HerdrPaneCreated {
|
||||
result?: {
|
||||
pane?: { pane_id?: unknown };
|
||||
};
|
||||
}
|
||||
|
||||
function hasCommand(command: string): boolean {
|
||||
const cached = commandAvailability.get(command);
|
||||
if (cached !== undefined) return cached;
|
||||
|
||||
let available = false;
|
||||
try {
|
||||
execFileSync("sh", ["-c", `command -v ${command}`], { stdio: "ignore" });
|
||||
available = true;
|
||||
} catch {
|
||||
available = false;
|
||||
}
|
||||
|
||||
commandAvailability.set(command, available);
|
||||
return available;
|
||||
}
|
||||
|
||||
export function isAvailable(): boolean {
|
||||
return (
|
||||
process.env.HERDR_ENV === "1" &&
|
||||
!!process.env.HERDR_WORKSPACE_ID &&
|
||||
!!process.env.HERDR_PANE_ID &&
|
||||
hasCommand("herdr")
|
||||
);
|
||||
}
|
||||
|
||||
export function setupHint(): string {
|
||||
return "Start pi inside a Herdr-managed pane with the `herdr` CLI on PATH.";
|
||||
}
|
||||
|
||||
function requireHerdr(): void {
|
||||
if (!isAvailable()) {
|
||||
throw new Error(`Herdr is required for this surface. ${setupHint()}`);
|
||||
}
|
||||
}
|
||||
|
||||
function parseJson<T>(command: string, output: string): T {
|
||||
try {
|
||||
return JSON.parse(output) as T;
|
||||
} catch {
|
||||
throw new Error(`Unexpected Herdr ${command} output: ${output.trim() || "(empty)"}`);
|
||||
}
|
||||
}
|
||||
|
||||
function requireId(value: unknown, command: string, field: string): string {
|
||||
if (typeof value !== "string" || value.length === 0) {
|
||||
throw new Error(`Herdr ${command} did not return ${field}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseCreatedTab(output: string): { paneId: string; tabId: string } {
|
||||
const response = parseJson<HerdrTabCreated>("tab create", output);
|
||||
return {
|
||||
paneId: requireId(response.result?.root_pane?.pane_id, "tab create", "result.root_pane.pane_id"),
|
||||
tabId: requireId(response.result?.tab?.tab_id, "tab create", "result.tab.tab_id"),
|
||||
};
|
||||
}
|
||||
|
||||
export function parseCreatedPane(output: string): string {
|
||||
const response = parseJson<HerdrPaneCreated>("pane split", output);
|
||||
return requireId(response.result?.pane?.pane_id, "pane split", "result.pane.pane_id");
|
||||
}
|
||||
|
||||
export function createSurface(name: string): string {
|
||||
requireHerdr();
|
||||
const workspace = process.env.HERDR_WORKSPACE_ID!;
|
||||
const output = execFileSync(
|
||||
"herdr",
|
||||
[
|
||||
"tab",
|
||||
"create",
|
||||
"--workspace",
|
||||
workspace,
|
||||
"--cwd",
|
||||
process.cwd(),
|
||||
"--label",
|
||||
name,
|
||||
"--no-focus",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
const { paneId, tabId } = parseCreatedTab(output);
|
||||
tabByPane.set(paneId, tabId);
|
||||
return paneId;
|
||||
}
|
||||
|
||||
export function createSurfaceSplit(
|
||||
name: string,
|
||||
direction: "left" | "right" | "up" | "down",
|
||||
fromSurface?: string,
|
||||
): string {
|
||||
requireHerdr();
|
||||
if (direction === "left" || direction === "up") {
|
||||
throw new Error(`Herdr pane splits only support right or down, not ${direction}`);
|
||||
}
|
||||
|
||||
const source = fromSurface ?? process.env.HERDR_PANE_ID!;
|
||||
const output = execFileSync(
|
||||
"herdr",
|
||||
[
|
||||
"pane",
|
||||
"split",
|
||||
"--pane",
|
||||
source,
|
||||
"--direction",
|
||||
direction,
|
||||
"--cwd",
|
||||
process.cwd(),
|
||||
"--no-focus",
|
||||
],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
const paneId = parseCreatedPane(output);
|
||||
|
||||
try {
|
||||
execFileSync("herdr", ["pane", "rename", paneId, name], { encoding: "utf8" });
|
||||
} catch {
|
||||
// A cosmetic label must not break a successful split.
|
||||
}
|
||||
|
||||
return paneId;
|
||||
}
|
||||
|
||||
export function sendCommand(surface: string, command: string): void {
|
||||
requireHerdr();
|
||||
execFileSync("herdr", ["pane", "run", surface, command], { encoding: "utf8" });
|
||||
}
|
||||
|
||||
export function readScreen(surface: string, lines = 50): string {
|
||||
requireHerdr();
|
||||
return execFileSync(
|
||||
"herdr",
|
||||
["pane", "read", surface, "--source", "recent-unwrapped", "--lines", String(Math.max(1, lines))],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
}
|
||||
|
||||
export async function readScreenAsync(surface: string, lines = 50): Promise<string> {
|
||||
requireHerdr();
|
||||
const { stdout } = await execFileAsync(
|
||||
"herdr",
|
||||
["pane", "read", surface, "--source", "recent-unwrapped", "--lines", String(Math.max(1, lines))],
|
||||
{ encoding: "utf8" },
|
||||
);
|
||||
return stdout;
|
||||
}
|
||||
|
||||
export function closeSurface(surface: string): void {
|
||||
requireHerdr();
|
||||
const tabId = tabByPane.get(surface);
|
||||
if (tabId) {
|
||||
execFileSync("herdr", ["tab", "close", tabId], { encoding: "utf8" });
|
||||
tabByPane.delete(surface);
|
||||
return;
|
||||
}
|
||||
execFileSync("herdr", ["pane", "close", surface], { encoding: "utf8" });
|
||||
}
|
||||
Reference in New Issue
Block a user