Files
fusion/plugins/fusion-plugin-cli-printing-press/src/generation/runner.ts
Fusion e466df5a76 feat(FN-3765): add run panel and generation pipeline for CLI printing press
Adds a CLI command generation pipeline with a runner to execute generated commands, run/artifact API routes, and a TestRunnerPanel UI for monitoring execution — complete with tests covering the generator, runner, run routes, and the panel component.

Fusion-Task-Id: FN-3765
2026-05-10 17:30:44 -07:00

80 lines
2.7 KiB
TypeScript

import { exec } from "node:child_process";
import { promisify } from "node:util";
import { redact } from "./redact.js";
import type { GeneratedCliArtifact, RunResult } from "./types.js";
const execAsync = promisify(exec);
export interface RunGeneratedCliInput {
artifact: GeneratedCliArtifact;
endpointId: string;
params: Record<string, string | number | boolean>;
credentials?: Record<string, string>;
timeoutMs?: number;
cwd?: string;
}
function toFlagName(key: string): string {
return key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
}
function createArgs(endpointId: string, params: Record<string, string | number | boolean>): string[] {
const args: string[] = ["--endpoint", endpointId];
for (const [key, value] of Object.entries(params)) {
if (typeof value === "boolean") {
if (value) args.push(`--${toFlagName(key)}`);
continue;
}
args.push(`--${toFlagName(key)}`, String(value));
}
return args;
}
function quoteArg(arg: string): string {
return JSON.stringify(arg);
}
// Credentials are passed only via env vars: CLIPP_CRED_<UPPER_SNAKE_KEY>.
export async function runGeneratedCli({ artifact, endpointId, params, credentials, timeoutMs = 30_000, cwd }: RunGeneratedCliInput): Promise<RunResult> {
const args = createArgs(endpointId, params);
const argv = [artifact.binPath, ...args];
const command = ["node", ...argv].map(quoteArg).join(" ");
const credEnv: Record<string, string> = {};
for (const [key, value] of Object.entries(credentials ?? {})) {
credEnv[`CLIPP_CRED_${key.replace(/[^a-zA-Z0-9]/g, "_").toUpperCase()}`] = value;
}
const start = Date.now();
try {
const { stdout, stderr } = await execAsync(command, {
cwd,
timeout: timeoutMs,
maxBuffer: 10 * 1024 * 1024,
env: { ...process.env, ...credEnv },
});
const secrets = Object.values(credentials ?? {});
return {
stdout: redact(stdout, secrets),
stderr: redact(stderr, secrets),
exitCode: 0,
durationMs: Date.now() - start,
timedOut: false,
argv: argv.map((part) => redact(part, secrets)),
};
} catch (error) {
const err = error as { stdout?: string; stderr?: string; code?: number | null; killed?: boolean; signal?: string };
const timedOut = Boolean(err.killed && err.signal === "SIGTERM");
const secrets = Object.values(credentials ?? {});
return {
stdout: redact(err.stdout ?? "", secrets),
stderr: redact(err.stderr ?? (timedOut ? "Command timed out" : ""), secrets),
exitCode: timedOut ? null : (typeof err.code === "number" ? err.code : null),
durationMs: Date.now() - start,
timedOut,
argv: argv.map((part) => redact(part, secrets)),
};
}
}