import { clearActiveProgressLine } from "./terminal/progress-line.js"; import { restoreTerminalState } from "./terminal/restore.js"; export type RuntimeEnv = { log: (...args: unknown[]) => void; error: (...args: unknown[]) => void; exit: (code: number) => void; }; export type OutputRuntimeEnv = RuntimeEnv & { writeStdout: (value: string) => void; writeJson: (value: unknown, space?: number) => void; }; function shouldEmitRuntimeLog(env: NodeJS.ProcessEnv = process.env): boolean { if (env.VITEST !== "true") { return true; } if (env.OPENCLAW_TEST_RUNTIME_LOG === "1") { return true; } const maybeMockedLog = console.log as unknown as { mock?: unknown }; return typeof maybeMockedLog.mock === "object"; } function shouldEmitRuntimeStdout(env: NodeJS.ProcessEnv = process.env): boolean { if (env.VITEST !== "true") { return true; } if (env.OPENCLAW_TEST_RUNTIME_LOG === "1") { return true; } const stdout = process.stdout as NodeJS.WriteStream & { write: { mock?: unknown; }; }; return typeof stdout.write.mock === "object"; } function isPipeClosedError(err: unknown): boolean { const code = (err as { code?: string })?.code; return code === "EPIPE" || code === "EIO"; } function hasRuntimeOutputWriter( runtime: RuntimeEnv | OutputRuntimeEnv, ): runtime is OutputRuntimeEnv { return typeof (runtime as Partial).writeStdout === "function"; } function writeStdout(value: string): void { if (!shouldEmitRuntimeStdout()) { return; } clearActiveProgressLine(); const line = value.endsWith("\n") ? value : `${value}\n`; try { process.stdout.write(line); } catch (err) { if (isPipeClosedError(err)) { return; } throw err; } } function createRuntimeIo(): Pick { return { log: (...args: Parameters) => { if (!shouldEmitRuntimeLog()) { return; } clearActiveProgressLine(); console.log(...args); }, error: (...args: Parameters) => { clearActiveProgressLine(); console.error(...args); }, writeStdout, writeJson: (value: unknown, space = 2) => { writeStdout(JSON.stringify(value, null, space > 0 ? space : undefined)); }, }; } export const defaultRuntime: OutputRuntimeEnv = { ...createRuntimeIo(), exit: (code) => { restoreTerminalState("runtime exit", { resumeStdinIfPaused: false }); process.exit(code); throw new Error("unreachable"); // satisfies tests when mocked }, }; export function createNonExitingRuntime(): OutputRuntimeEnv { return { ...createRuntimeIo(), exit: (code: number) => { throw new Error(`exit ${code}`); }, }; } export function writeRuntimeStdout(runtime: RuntimeEnv | OutputRuntimeEnv, value: string): void { if (hasRuntimeOutputWriter(runtime)) { runtime.writeStdout(value); return; } runtime.log(value); } export function writeRuntimeJson( runtime: RuntimeEnv | OutputRuntimeEnv, value: unknown, space = 2, ): void { if (hasRuntimeOutputWriter(runtime)) { runtime.writeJson(value, space); return; } runtime.log(JSON.stringify(value, null, space > 0 ? space : undefined)); }