Register Droid provider startup without waiting on local binary probes. - Register the Droid CLI provider synchronously with an empty model list, then refresh discovered models asynchronously. - Harden Droid runtime probes to convert spawn errors and timeouts into unavailable sentinel results. - Add coverage for non-blocking startup, failed probes, unavailable binaries, and process cleanup behavior. - Add a patch changeset for the published Fusion CLI package. Files changed: .changeset/fn-6878-droid-boot.md | 5 ++ packages/droid-cli/index.ts | 73 +++++++++++------- packages/droid-cli/src/__tests__/index.test.ts | 52 ++++++++++++- .../src/__tests__/process-manager.test.ts | 58 +++++++-------- .../src/__tests__/discover-models.test.ts | 8 ++ .../src/__tests__/probe.test.ts | 43 ++++++++++- .../src/__tests__/startup-probes.test.ts | 86 ++++++++++++++++++++++ plugins/fusion-plugin-droid-runtime/src/probe.ts | 32 +++++--- 8 files changed, 283 insertions(+), 74 deletions(-) Fusion-Task-Id: FN-6878 Fusion-Task-Lineage: 4e44a6e8-0eea-4867-a172-3bdc0c6368e1
80 lines
2.7 KiB
TypeScript
80 lines
2.7 KiB
TypeScript
import { spawn } from "node:child_process";
|
|
|
|
export interface DroidBinaryStatus {
|
|
available: boolean;
|
|
authenticated?: boolean;
|
|
binaryPath?: string;
|
|
version?: string;
|
|
reason?: string;
|
|
probeDurationMs: number;
|
|
}
|
|
|
|
export function resolveDroidBinaryPath(settings?: Record<string, unknown>): string {
|
|
if (typeof settings?.droidBinaryPath === "string" && settings.droidBinaryPath.trim().length > 0) {
|
|
return settings.droidBinaryPath.trim();
|
|
}
|
|
return "droid";
|
|
}
|
|
|
|
async function run(binary: string, args: string[], timeoutMs = 2000): Promise<{ code: number | null; stdout: string; stderr: string }> {
|
|
return new Promise((resolve) => {
|
|
let stdout = "";
|
|
let stderr = "";
|
|
let child: ReturnType<typeof spawn>;
|
|
try {
|
|
child = spawn(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
} catch {
|
|
resolve({ code: 127, stdout, stderr });
|
|
return;
|
|
}
|
|
|
|
/*
|
|
FNXC:CliRuntime 2026-06-21-12:00:
|
|
Droid binary probes run on dashboard and engine startup status paths, so they must never reject or wait forever. Convert synchronous spawn guards, ENOENT, and timeout hangs into sentinel exit codes so boot degrades provider availability instead of blocking on a broken local `droid` install.
|
|
*/
|
|
let settled = false;
|
|
const settle = (code: number | null) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
resolve({ code, stdout, stderr });
|
|
};
|
|
const timer = setTimeout(() => {
|
|
try { child.kill("SIGKILL"); } catch {
|
|
// ignore kill errors
|
|
}
|
|
settle(124);
|
|
}, 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", () => settle(127));
|
|
child.on("close", (code) => settle(code));
|
|
});
|
|
}
|
|
|
|
export async function probeDroidBinary(options?: { binaryPath?: string; settings?: Record<string, unknown>; timeoutMs?: number }): Promise<DroidBinaryStatus> {
|
|
const startedAt = Date.now();
|
|
const binaryPath = options?.binaryPath?.trim() || resolveDroidBinaryPath(options?.settings);
|
|
const timeoutMs = options?.timeoutMs ?? 2000;
|
|
|
|
const versionRun = await run(binaryPath, ["--version"], timeoutMs);
|
|
if (versionRun.code !== 0) {
|
|
return {
|
|
available: false,
|
|
binaryPath,
|
|
reason:
|
|
versionRun.code === 124
|
|
? `Probe timed out after ${timeoutMs}ms`
|
|
: `Binary not found or not executable: ${binaryPath}`,
|
|
probeDurationMs: Date.now() - startedAt,
|
|
};
|
|
}
|
|
|
|
return {
|
|
available: true,
|
|
binaryPath,
|
|
version: versionRun.stdout.trim() || undefined,
|
|
probeDurationMs: Date.now() - startedAt,
|
|
};
|
|
}
|