/** * Post-processing service for linting and formatting generated code. * * @since 4.0.0 */ import * as Context from "effect/Context" import * as Data from "effect/Data" import * as Effect from "effect/Effect" import * as Layer from "effect/Layer" import * as Stream from "effect/Stream" import * as ChildProcess from "effect/unstable/process/ChildProcess" import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawner" /** * Error during post-processing (lint or format). * * **Example** (Creating a post-process error) * * ```ts * import * as PostProcess from "@effect/ai-codegen/PostProcess" * * const error = new PostProcess.PostProcessError({ * step: "lint", * command: "pnpm exec oxlint --fix /path/to/file.ts", * filePath: "/path/to/file.ts", * exitCode: 1, * stdout: "", * stderr: "error: some lint error", * cause: new Error("Lint failed") * }) * ``` * * @category errors * @since 4.0.0 */ export class PostProcessError extends Data.TaggedError("PostProcessError")<{ readonly step: "lint" | "format" readonly command: string readonly filePath: string readonly exitCode?: number | undefined readonly stdout: string readonly stderr: string readonly cause: unknown }> { override get message(): string { const lines: Array = [ `${this.step} failed for ${this.filePath}`, `command: ${this.command}` ] if (this.exitCode !== undefined) { lines.push(`exit code: ${this.exitCode}`) } if (this.stderr.length > 0) { lines.push(`stderr:\n${this.stderr}`) } if (this.stdout.length > 0) { lines.push(`stdout:\n${this.stdout}`) } return lines.join("\n") } } /** * Service for post-processing generated code. * * @category models * @since 4.0.0 */ export interface PostProcessor { readonly lint: (filePath: string) => Effect.Effect readonly format: (filePath: string) => Effect.Effect } /** * Service tag for linting and formatting generated code. * * @category services * @since 4.0.0 */ export const PostProcessor: Context.Service = Context.Service( "@effect/ai-codegen/PostProcessor" ) /** * Layer providing the PostProcessor service. * * @category layers * @since 4.0.0 */ export const layer: Layer.Layer< PostProcessor, never, ChildProcessSpawner.ChildProcessSpawner > = Effect.gen(function*() { const spawner = yield* ChildProcessSpawner.ChildProcessSpawner const collectStream = (stream: Stream.Stream) => stream.pipe( Stream.decodeText(), Stream.mkString, Effect.orElseSucceed(() => "") ) const runCommand = Effect.fn("runCommand")(function*( command: string, args: ReadonlyArray, step: "lint" | "format", filePath: string ) { const fullCommand = [command, ...args].join(" ") const cmd = ChildProcess.make(command, args, { stdout: "pipe", stderr: "pipe" }) yield* Effect.scoped(Effect.gen(function*() { const handle = yield* spawner.spawn(cmd).pipe( Effect.mapError((cause) => new PostProcessError({ step, command: fullCommand, filePath, stdout: "", stderr: "", cause }) ) ) const [stdout, stderr] = yield* Effect.all([ collectStream(handle.stdout), collectStream(handle.stderr) ]) const exitCode = yield* handle.exitCode.pipe( Effect.mapError((cause) => new PostProcessError({ step, command: fullCommand, filePath, stdout, stderr, cause }) ) ) if (exitCode !== 0) { return yield* new PostProcessError({ step, command: fullCommand, filePath, exitCode, stdout, stderr, cause: new Error(`Command exited with code ${exitCode}`) }) } })) }) const lint = Effect.fn("lint")(function*(filePath: string) { yield* runCommand("pnpm", ["exec", "oxlint", "--silent", "--quiet", "--fix", filePath], "lint", filePath) }) const format = Effect.fn("format")(function*(filePath: string) { yield* runCommand("pnpm", ["exec", "dprint", "--log-level", "silent", "fmt", filePath], "format", filePath) }) return { lint, format } }).pipe(Layer.effect(PostProcessor))