feat(M3): Direct Execution

This commit is contained in:
2026-05-18 18:11:45 -05:00
parent fe94629797
commit a2cfa7027e
2 changed files with 29 additions and 7 deletions
+13 -3
View File
@@ -1,4 +1,6 @@
import type { ChildProcess } from "node:child_process";
import { spawn as defaultSpawn } from "node:child_process"; import { spawn as defaultSpawn } from "node:child_process";
import type { PathLike } from "node:fs";
import { existsSync as defaultExistsSync } from "node:fs"; import { existsSync as defaultExistsSync } from "node:fs";
import type { ClientName, ExecResult } from "./types.js"; import type { ClientName, ExecResult } from "./types.js";
import { ClientNotFoundError, ExecError } from "./types.js"; import { ClientNotFoundError, ExecError } from "./types.js";
@@ -12,8 +14,8 @@ const CLIENT_ARGS: Record<ClientName, (prompt: string) => string[]> = {
export interface ExecuteOptions { export interface ExecuteOptions {
clientPath?: string; clientPath?: string;
timeoutMs?: number; timeoutMs?: number;
spawn?: typeof defaultSpawn; spawn?: (command: string, args: string[], options?: { shell?: boolean }) => ChildProcess;
existsSync?: typeof defaultExistsSync; existsSync?: (path: PathLike) => boolean;
} }
export async function executePrompt( export async function executePrompt(
@@ -38,7 +40,15 @@ export async function executePrompt(
throw new ClientNotFoundError(client); throw new ClientNotFoundError(client);
} }
const args = CLIENT_ARGS[client](prompt); const argBuilder = (CLIENT_ARGS as Record<string, (prompt: string) => string[]>)[client];
if (!argBuilder) {
throw new ExecError(`Unknown client: ${client}`, {
stdout: "",
stderr: "",
exitCode: -1,
});
}
const args = argBuilder(prompt);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let settled = false; let settled = false;
+16 -4
View File
@@ -4,6 +4,7 @@ import { EventEmitter } from "node:events";
import { Readable } from "node:stream"; import { Readable } from "node:stream";
import { executePrompt } from "../src/execute.js"; import { executePrompt } from "../src/execute.js";
import { ClientNotFoundError, ExecError } from "../src/types.js"; import { ClientNotFoundError, ExecError } from "../src/types.js";
import type { ClientName } from "../src/types.js";
interface MockScenario { interface MockScenario {
stdout?: string; stdout?: string;
@@ -14,7 +15,7 @@ interface MockScenario {
} }
function createMockChildProcess(scenario: MockScenario): any { function createMockChildProcess(scenario: MockScenario): any {
const child = new EventEmitter(); const child = new EventEmitter() as any;
child.stdout = Readable.from( child.stdout = Readable.from(
scenario.stdout !== undefined ? [scenario.stdout] : [] scenario.stdout !== undefined ? [scenario.stdout] : []
); );
@@ -61,8 +62,8 @@ function createMockChildProcess(scenario: MockScenario): any {
function mockSpawn( function mockSpawn(
scenarios: Map<string, MockScenario> scenarios: Map<string, MockScenario>
): (cmd: string, args: string[], opts: any) => any { ): any {
return (cmd: string, args: string[], _opts: any) => { return (cmd: string, args: string[], _opts: any): any => {
const key = [cmd, ...args].join(" "); const key = [cmd, ...args].join(" ");
const scenario = scenarios.get(key); const scenario = scenarios.get(key);
if (!scenario) { if (!scenario) {
@@ -74,7 +75,7 @@ function mockSpawn(
}; };
} }
function mockExistsSync(allowedPaths: Set<string>): (p: string) => boolean { function mockExistsSync(allowedPaths: Set<string>): any {
return (p: string) => allowedPaths.has(p); return (p: string) => allowedPaths.has(p);
} }
@@ -191,4 +192,15 @@ describe("executePrompt", () => {
err instanceof ExecError && err.message.includes("empty") err instanceof ExecError && err.message.includes("empty")
); );
}); });
it("throws ExecError for invalid client at runtime", async () => {
await assert.rejects(
executePrompt("bogus" as unknown as ClientName, "hello", {
spawn: mockSpawn(new Map()),
existsSync: () => true,
}),
(err: unknown) =>
err instanceof ExecError && err.message.includes("Unknown client")
);
});
}); });