export * as Formatter from "./formatter.js" import { Context, Effect, Layer } from "effect" import { ChildProcess } from "effect/unstable/process" import path from "path" import { makeLocationNode } from "@opencode-ai/util/effect/app-node" import { AppProcess } from "@opencode-ai/util/process" import { Location } from "./location.js" import type { Info } from "./formatter/builtins.js" import { State } from "./state.js" type Data = { formatters: Info[] } export type Editor = { set: (formatter: Info) => void remove: (name: string) => void } export interface Interface extends State.Transformable { readonly file: (filepath: string) => Effect.Effect } export class Service extends Context.Service()("@opencode/v2/Formatter") {} const layer = Layer.effect( Service, Effect.gen(function* () { const location = yield* Location.Service const processes = yield* AppProcess.Service const commands = new WeakMap() const state = State.create({ name: "formatter", initial: () => ({ formatters: [] }), editor: (editor) => ({ set: (formatter) => { const index = editor.formatters.findIndex((item) => item.name === formatter.name) if (index === -1) editor.formatters.push(formatter) else editor.formatters[index] = formatter }, remove: (name) => { editor.formatters = editor.formatters.filter((formatter) => formatter.name !== name) }, }), }) const command = Effect.fnUntraced(function* (formatter: Info) { const cached = commands.get(formatter) if (cached !== undefined) return cached const result = yield* formatter.enabled if (result !== false) commands.set(formatter, result) return result }) const file = Effect.fn("Formatter.file")(function* (filepath: string) { const extension = path.extname(filepath) const matching = state.get().formatters.filter((formatter) => formatter.extensions.includes(extension)) for (const formatter of matching) { const enabled = yield* command(formatter) if (enabled === false) continue const cmd = enabled.map((argument) => argument.replace("$FILE", filepath)) yield* Effect.logInfo("formatting file", { file: filepath, command: cmd }) const result = yield* processes .run( ChildProcess.make(cmd[0], cmd.slice(1), { cwd: location.directory, env: formatter.environment, extendEnv: true, stdin: "ignore", stdout: "ignore", stderr: "ignore", }), ) .pipe( Effect.catch((error) => Effect.logError("failed to format file", { file: filepath, command: cmd, error: error.message, }).pipe(Effect.as(undefined)), ), ) if (!result) continue if (result.exitCode === 0) return true yield* Effect.logError("formatter exited unsuccessfully", { file: filepath, command: cmd, exitCode: result.exitCode, }) } return false }) return Service.of({ transform: state.transform, reload: state.reload, file }) }), ) export const node = makeLocationNode({ service: Service, layer, deps: [Location.node, AppProcess.node], })