import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import { ChildProcessSpawner } from "effect/unstable/process"; import { VcsOutputDecodeError, type VcsError, VcsProcessExitError, VcsProcessSpawnError, VcsProcessTimeoutError, } from "@t3tools/contracts"; import { ProcessRunner, layer as ProcessRunnerLive } from "../processRunner.ts"; import * as Match from "effect/Match"; export interface VcsProcessInput { readonly operation: string; readonly command: string; readonly args: ReadonlyArray; readonly cwd: string; readonly spawnCwd?: string; readonly stdin?: string; readonly env?: NodeJS.ProcessEnv; readonly allowNonZeroExit?: boolean; readonly timeoutMs?: number; readonly maxOutputBytes?: number; readonly appendTruncationMarker?: boolean; } export interface VcsProcessOutput { readonly exitCode: ChildProcessSpawner.ExitCode; readonly stdout: string; readonly stderr: string; readonly stdoutTruncated: boolean; readonly stderrTruncated: boolean; } export interface VcsProcessShape { readonly run: (input: VcsProcessInput) => Effect.Effect; } export class VcsProcess extends Context.Service()( "t3/vcs/VcsProcess", ) {} const DEFAULT_TIMEOUT_MS = 30_000; const DEFAULT_MAX_OUTPUT_BYTES = 1_000_000; const OUTPUT_TRUNCATED_MARKER = "\n\n[truncated]"; function commandLabel(command: string, args: ReadonlyArray): string { return [command, ...args].join(" "); } export const make = Effect.fn("makeVcsProcess")(function* () { const processRunner = yield* ProcessRunner; const run = Effect.fn("VcsProcess.run")(function* (input: VcsProcessInput) { const label = commandLabel(input.command, input.args); const baseError = { operation: input.operation, command: label, cwd: input.cwd, }; const result = yield* processRunner .run({ command: input.command, args: input.args, cwd: input.cwd, ...(input.spawnCwd !== undefined ? { spawnCwd: input.spawnCwd } : {}), ...(input.stdin !== undefined ? { stdin: input.stdin } : {}), ...(input.env !== undefined ? { env: input.env } : {}), timeout: input.timeoutMs ?? DEFAULT_TIMEOUT_MS, maxOutputBytes: input.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES, outputMode: "truncate", truncatedMarker: input.appendTruncationMarker ? OUTPUT_TRUNCATED_MARKER : "", timeoutBehavior: "error", }) .pipe( Effect.mapError( Match.valueTags({ ProcessSpawnError: (error) => VcsProcessSpawnError.fromProcessSpawnError(baseError, error), ProcessOutputLimitError: (error) => VcsOutputDecodeError.fromProcessOutputLimitError(baseError, error), ProcessTimeoutError: (error) => VcsProcessTimeoutError.fromProcessTimeoutError(baseError, error), ProcessStdinError: (error) => VcsOutputDecodeError.fromProcessStdinError(baseError, error), ProcessReadError: (error) => VcsOutputDecodeError.fromProcessReadError(baseError, error), }), ), ); if (result.code === null) { return yield* VcsOutputDecodeError.missingExitCode(baseError); } if (!input.allowNonZeroExit && result.code !== 0) { return yield* new VcsProcessExitError({ operation: input.operation, command: label, cwd: input.cwd, exitCode: result.code, detail: result.stderr.trim() || `${label} exited with code ${result.code}.`, }); } return { exitCode: result.code, stdout: result.stdout, stderr: result.stderr, stdoutTruncated: result.stdoutTruncated, stderrTruncated: result.stderrTruncated, } satisfies VcsProcessOutput; }); return VcsProcess.of({ run }); }); export const layer = Layer.effect(VcsProcess, make()).pipe(Layer.provide(ProcessRunnerLive));