@workflow-manager/runner
Version:
CLI runner for in-memory and markdown workflow orchestration using ATEP-like envelopes
199 lines (198 loc) • 7.49 kB
JavaScript
import { spawnSync } from "node:child_process";
import path from "node:path";
import { readRunArchive } from "./runArchive.js";
function asRecord(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
}
function stringValue(value) {
return typeof value === "string" && value.trim() ? value : undefined;
}
function textValue(value) {
return typeof value === "string" ? value : value?.toString("utf-8") ?? "";
}
function shellQuote(value) {
return `'${value.replace(/'/g, `'"'"'`)}'`;
}
function followCommand(commandName, sessionFilePath, stepKey) {
const args = [commandName, "follow", "--session-file", path.resolve(sessionFilePath)];
if (stepKey)
args.push("--step", stepKey);
return args.map(shellQuote).join(" ");
}
function commandFailure(command, result) {
const detail = result.error?.message ?? (textValue(result.stderr).trim() || `exited with status ${result.status ?? "unknown"}`);
return `${command} failed: ${detail}`;
}
function herdrPaneId(output) {
try {
const parsed = asRecord(JSON.parse(output));
const result = asRecord(parsed?.result) ?? parsed;
const rootPane = asRecord(result?.root_pane);
return stringValue(rootPane?.pane_id) ?? null;
}
catch {
return null;
}
}
export function openFollowTab(options) {
const env = options.env ?? process.env;
const runCommand = options.runCommand ?? ((command, args, spawnOptions) => spawnSync(command, args, spawnOptions));
const command = followCommand(options.commandName, options.sessionFilePath, options.stepKey);
if (env.HERDR_ENV === "1") {
const workspaceId = stringValue(env.HERDR_WORKSPACE_ID);
if (!workspaceId) {
return "Cannot open a follow tab: HERDR_WORKSPACE_ID is not set.";
}
const create = runCommand("herdr", ["tab", "create", "--workspace", workspaceId, "--cwd", options.cwd, "--label", "wfm follow", "--no-focus"], { encoding: "utf-8" });
if (create.status !== 0) {
return commandFailure("herdr tab create", create);
}
const paneId = herdrPaneId(textValue(create.stdout));
if (!paneId) {
return "herdr tab create did not return a root pane id.";
}
const start = runCommand("herdr", ["pane", "run", paneId, command], { encoding: "utf-8" });
if (start.status !== 0) {
return commandFailure("herdr pane run", start);
}
return null;
}
if (env.TMUX) {
const result = runCommand("tmux", ["new-window", "-d", "-n", "wfm-follow", "-c", options.cwd, command], { encoding: "utf-8" });
return result.status === 0 ? null : commandFailure("tmux new-window", result);
}
return "Cannot open a follow tab: start wfm inside Herdr or tmux, or run wfm follow in another terminal.";
}
function eventFromUnknown(value) {
const record = asRecord(value);
if (!record)
return null;
const type = stringValue(record.type);
const occurredAt = stringValue(record.occurredAt);
const data = asRecord(record.data);
if (!type || !occurredAt || !data)
return null;
return {
type,
occurredAt,
stepKey: stringValue(record.stepKey),
data,
};
}
function writeEvent(event, output, stepKey) {
if (stepKey && event.stepKey !== stepKey)
return;
const text = stringValue(event.data.text);
if (event.type === "agent.stdout" && text) {
output.stdout(text);
return;
}
if (event.type === "agent.stderr" && text) {
output.stderr(text);
return;
}
const timestamp = event.occurredAt.replace("T", " ").replace(".000Z", "Z");
const step = event.stepKey ? ` ${event.stepKey}` : "";
const adapter = stringValue(event.data.adapter);
const command = stringValue(event.data.command);
const details = [adapter, command].filter(Boolean).join(" · ");
output.stderr(`[${timestamp}]${step} ${event.type}${details ? ` (${details})` : ""}\n`);
}
async function resolveRunId(connection) {
if (connection.runId)
return { runId: connection.runId };
const response = await fetch(`${connection.baseUrl}/session`, {
headers: { Authorization: `Bearer ${connection.token}` },
});
if (!response.ok) {
return { error: `Could not determine run id: ${await response.text()}` };
}
const session = asRecord(await response.json());
const run = asRecord(session?.run);
const runId = stringValue(run?.runId);
return runId ? { runId } : { error: "Could not determine run id. Pass --run-id explicitly." };
}
function sseData(frame) {
const lines = frame.replace(/\r/g, "").split("\n");
const data = lines
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice("data:".length).trimStart())
.join("\n");
if (!data)
return null;
try {
return JSON.parse(data);
}
catch {
return null;
}
}
export async function followRun(connection, options = {}) {
const output = options.output ?? {
stdout: (text) => process.stdout.write(text),
stderr: (text) => process.stderr.write(text),
};
let runId;
try {
const resolved = await resolveRunId(connection);
if (!resolved.runId) {
output.stderr(`${resolved.error ?? "Could not determine run id."}\n`);
return 1;
}
runId = resolved.runId;
const response = await fetch(`${connection.baseUrl}/runs/${encodeURIComponent(runId)}/events`, {
headers: { Authorization: `Bearer ${connection.token}` },
});
if (!response.ok || !response.body) {
output.stderr(`follow failed: ${await response.text()}\n`);
return 1;
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let pending = "";
while (true) {
const { done, value } = await reader.read();
pending += decoder.decode(value, { stream: !done });
let separator = pending.indexOf("\n\n");
while (separator >= 0) {
const frame = pending.slice(0, separator);
pending = pending.slice(separator + 2);
const event = eventFromUnknown(sseData(frame));
if (event)
writeEvent(event, output, options.stepKey);
separator = pending.indexOf("\n\n");
}
if (done)
break;
}
return 0;
}
catch (error) {
output.stderr(`follow failed: ${error.message}\n`);
return 1;
}
}
export function replayRunArchive(filePath, options = {}) {
const output = options.output ?? {
stdout: (text) => process.stdout.write(text),
stderr: (text) => process.stderr.write(text),
};
const records = readRunArchive(filePath);
if (typeof records === "string") {
output.stderr(`${records}\n`);
return 1;
}
for (const record of records) {
if (record.type !== "event")
continue;
const event = eventFromUnknown({
type: record.data.type,
occurredAt: record.occurredAt,
stepKey: record.data.stepKey,
data: record.data.payload,
});
if (event)
writeEvent(event, output, options.stepKey);
}
return 0;
}