feat(M3): Direct Execution
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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")
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user