fix(cli): prevent Node stdout EPIPE failures

Keep draining standalone server stdout after reading its readiness message so
later server output does not write into a closed pipe.

Install the Node CLI broken-pipe handler before loading the main entrypoint.
Ignore only EPIPE while preserving all unrelated stdout errors. This prevents
Windows Node builds from failing when the TUI prints its exit epilogue after
renderer teardown.
This commit is contained in:
Simon Klee 2026-07-17 14:46:46 +02:00
parent a1b274e6f8
commit 42e2dde739
No known key found for this signature in database
GPG key ID: B91696044D47BEA3
2 changed files with 18 additions and 4 deletions

View file

@ -1,3 +1,9 @@
import "./plugin-runtime.promise"
import "./plugin-runtime.effect"
import "../index"
process.stdout.on("error", (error) => {
if ("code" in error && error.code === "EPIPE") return
throw error
})
await import("../index")

View file

@ -1,7 +1,7 @@
import { Service, type Endpoint } from "@opencode-ai/client/effect/service"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { Effect, Schema, Stream } from "effect"
import { Deferred, Effect, Schema, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { randomBytes } from "node:crypto"
import { selfCommand } from "../util/process"
@ -36,8 +36,16 @@ const makeEndpoint = Effect.fn("cli.standalone.endpoint")(
const password = randomBytes(32).toString("base64url")
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const proc = yield* spawner.spawn(command(password, options))
const output = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString)
if (!output) return yield* Effect.fail(new Error("Standalone server exited before reporting readiness"))
const readyLine = yield* Deferred.make<string, Error>()
// Keep draining stdout after readiness so later server writes cannot hit EPIPE.
yield* proc.stdout.pipe(
Stream.decodeText(),
Stream.splitLines,
Stream.runForEach((line) => Deferred.succeed(readyLine, line)),
Effect.ensuring(Deferred.fail(readyLine, new Error("Standalone server exited before reporting readiness"))),
Effect.forkScoped,
)
const output = yield* Deferred.await(readyLine)
const ready = yield* Effect.tryPromise(() => decodeReady(output))
return {
url: ready.url,