Commits merged: - chore(test-isolation): broaden runtime ignore list for live fusion app paths - feat(FN-3573): complete Step 2 — add plugin setup API client helpers - feat(FN-3573): complete Step 1 — backfill plugin setup routes Files changed: plugins/fusion-plugin-agent-browser/README.md | 26 ++++++ plugins/fusion-plugin-agent-browser/manifest.json | 15 ++++ plugins/fusion-plugin-agent-browser/package.json | 25 ++++++ .../src/__tests__/index.test.ts | 66 +++++++++++++++ .../src/__tests__/prompts.test.ts | 21 +++++ .../src/__tests__/setup.test.ts | 39 +++++++++ .../src/__tests__/skills.test.ts | 9 ++ .../src/__tests__/tools.test.ts | 55 ++++++++++++ .../src/__tests__/types.test.ts | 16 ++++ .../src/__tests__/workflow-steps.test.ts | 10 +++ plugins/fusion-plugin-agent-browser/src/index.ts | 67 +++++++++++++++ plugins/fusion-plugin-agent-browser/src/probe.ts | 98 ++++++++++++++++++++++ plugins/fusion-plugin-agent-browser/src/prompts.ts | 23 +++++ plugins/fusion-plugin-agent-browser/src/setup.ts | 31 +++++++ plugins/fusion-plugin-agent-browser/src/skills.ts | 12 +++ plugins/fusion-plugin-agent-browser/src/tools.ts | 34 ++++++++ plugins/fusion-plugin-agent-browser/src/types.ts | 56 +++++++++++++ .../src/workflow-steps.ts | 15 ++++ plugins/fusion-plugin-agent-browser/tsconfig.json | 9 ++ .../fusion-plugin-agent-browser/vitest.config.ts | 22 +++++ pnpm-lock.yaml | 16 ++++ pnpm-workspace.yaml | 1 + 22 files changed, 666 insertions(+) Fusion-Task-Id: FN-3573
99 lines
3.0 KiB
TypeScript
99 lines
3.0 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
|
|
export interface AgentBrowserProbeResult {
|
|
available: boolean;
|
|
binaryPath?: string;
|
|
version?: string;
|
|
reason?: string;
|
|
notFound?: boolean;
|
|
}
|
|
|
|
export async function probeAgentBrowserBinary(opts?: { binaryPath?: string; timeoutMs?: number }): Promise<AgentBrowserProbeResult> {
|
|
const binary = opts?.binaryPath?.trim() || "agent-browser";
|
|
const timeoutMs = opts?.timeoutMs ?? 2000;
|
|
const resolvedPath = await tryResolveBinaryPath(binary);
|
|
|
|
return new Promise((resolve) => {
|
|
let settled = false;
|
|
const child = spawn(resolvedPath ?? binary, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
|
|
let stdout = "";
|
|
let stderr = "";
|
|
|
|
const timer = setTimeout(() => {
|
|
if (settled) return;
|
|
settled = true;
|
|
child.kill("SIGKILL");
|
|
resolve({ available: false, binaryPath: resolvedPath, reason: `Probe timed out after ${timeoutMs}ms` });
|
|
}, timeoutMs);
|
|
|
|
child.stdout?.on("data", (c: Buffer) => (stdout += c.toString("utf-8")));
|
|
child.stderr?.on("data", (c: Buffer) => (stderr += c.toString("utf-8")));
|
|
child.on("error", (err: NodeJS.ErrnoException) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
resolve({
|
|
available: false,
|
|
binaryPath: resolvedPath,
|
|
reason: err.code === "ENOENT" ? "`agent-browser` not found on PATH" : err.message,
|
|
notFound: err.code === "ENOENT",
|
|
});
|
|
});
|
|
child.on("close", (code) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
if (code === 0) {
|
|
resolve({ available: true, binaryPath: resolvedPath, version: stdout.trim() || undefined });
|
|
} else {
|
|
resolve({
|
|
available: false,
|
|
binaryPath: resolvedPath,
|
|
reason: stderr.trim() || `agent-browser --version exited with code ${String(code)}`,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async function tryResolveBinaryPath(binary: string): Promise<string | undefined> {
|
|
return new Promise((resolvePromise) => {
|
|
const which = process.platform === "win32" ? "where" : "which";
|
|
const child = spawn(which, [binary], { stdio: ["ignore", "pipe", "ignore"] });
|
|
let out = "";
|
|
let settled = false;
|
|
|
|
const timer = setTimeout(() => {
|
|
if (settled) return;
|
|
settled = true;
|
|
try {
|
|
child.kill("SIGKILL");
|
|
} catch {
|
|
// ignored
|
|
}
|
|
resolvePromise(undefined);
|
|
}, 2000);
|
|
|
|
child.stdout?.on("data", (chunk: Buffer) => {
|
|
out += chunk.toString("utf-8");
|
|
});
|
|
child.on("error", () => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
resolvePromise(undefined);
|
|
});
|
|
child.on("close", (code: number | null) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
if (code === 0) {
|
|
const first = out.trim().split(/\r?\n/)[0];
|
|
resolvePromise(first?.length ? first : undefined);
|
|
} else {
|
|
resolvePromise(undefined);
|
|
}
|
|
});
|
|
});
|
|
}
|