# `biome-runner` _(no submodules)_ ## Purpose `@suaveplan/linter/biome-runner` is the seam between the linter and the Biome CLI. Rather than calling `execSync` from library code (which makes tests impossible and blocks the event loop), the runner shells out through an injectable `CommandRunner` whose default implementation wraps `node:child_process.spawn`. Tests pass real fake runners that return canned `CommandResult` objects; production callers get the real subprocess. Every run is wrapped in duration measurement so the OTel histogram captures the cost of every biome invocation across the project. ## Features - Three modes — `"check"` (read-only), `"check-write"` (lint+fix, `--write --unsafe`), `"format-write"` (formatter only with `--write`). - Returns a `BiomeReport` with `mode`, `exitCode`, `stdout`, `stderr`, `success`, and `durationMs` so downstream callers can render summaries without re-parsing. - `"check"` mode throws `LinterError(LINTER.BIOME_FAILED)` on a non-zero exit (the expected contract for CI gates); `"check-write"` and `"format-write"` return the report unconditionally so write-mode runs can report success even when biome surfaced diagnostics that it auto-fixed. - `probeBiomeAvailable({ runner })` — non-fatal `bunx biome --version` ping returning `boolean`. Used by the orchestrator to gate the biome lane. - Zod-validated options — invalid `mode` values raise `LinterError(LINTER.INVALID_OPTIONS)`. - Injectable `ClockLike` for deterministic duration measurement in tests. ## Usage: Basic ```ts import { nodeCommandRunner, runBiome, } from "@suaveplan/linter/biome-runner"; const report = await runBiome({ cwd: ".", mode: "check", runner: nodeCommandRunner, }); console.log(report.success, report.durationMs); ``` ## Usage: Advanced Drive the runner from a test with a recording fake runner: ```ts import { runBiome } from "@suaveplan/linter/biome-runner"; import type { CommandInput, CommandRunner } from "@suaveplan/linter"; const calls: CommandInput[] = []; const runner: CommandRunner = { async run(input) { calls.push(input); return { exitCode: 0, stdout: "ok", stderr: "" }; }, }; const clock = { now: () => 1000 }; const report = await runBiome({ cwd: ".", mode: "check-write", runner, clock, }); // calls[0].args === ['biome', 'check', '--write', '--unsafe', '--max-diagnostics', '500', '.'] // report.success === true // report.durationMs === 0 ``` ## API Reference ### `runBiome(options): Promise` - `options.cwd: string` — required, forwarded to the runner. - `options.mode: "check" | "check-write" | "format-write"` — required. - `options.runner: CommandRunner` — required. - `options.clock?: ClockLike` — defaults to `Date.now`. Throws `LinterError` with code `INVALID_OPTIONS`, `RUNNER_FAILED`, or `BIOME_FAILED`. ### `probeBiomeAvailable(options): Promise` - `options.runner: CommandRunner` — required. - `options.cwd?: string` — defaults to `"."`. Resolves to `true` only on `exitCode === 0`; runner rejections resolve to `false`. ### `nodeCommandRunner: CommandRunner` Default runner wrapping `child_process.spawn`. Inherits the current `process.env` and overlays any `env` field provided in the `CommandInput`. ## Implementation Notes - The runner shells through `bunx biome` (not `npx`) so the catalogued `@biomejs/biome` version is the one that runs. Bunx resolves the binary from the current workspace install, matching what `bun run lint` would invoke in every other package. - The default runner buffers stdout / stderr in memory and resolves on `close`, NOT `exit` — `close` fires after both streams are drained, so the report includes trailing output that `exit` would miss. - `RunBiomeOptions` is validated via Zod at the boundary so consumers passing hand-typed strings get an `INVALID_OPTIONS` error rather than a runtime crash deep inside the runner. - `recordBiomeRun(mode, outcome)` and `recordRunDuration("biome", ms)` fire on every successful runner completion (success or failure); the only path that skips them is a runner rejection, which is the `RUNNER_FAILED` branch. - `probeBiomeAvailable` deliberately swallows rejections — a missing binary should not bubble an unhandled rejection through the orchestrator. Callers who want strict behaviour can raise `BIOME_UNAVAILABLE` themselves after a `false` probe.