Skip to content

Commit 42e2dde

Browse files
committed
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.
1 parent a1b274e commit 42e2dde

2 files changed

Lines changed: 18 additions & 4 deletions

File tree

‎packages/cli/src/node/index.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
11
import "./plugin-runtime.promise"
22
import "./plugin-runtime.effect"
3-
import "../index"
3+
4+
process.stdout.on("error", (error) => {
5+
if ("code" in error && error.code === "EPIPE") return
6+
throw error
7+
})
8+
9+
await import("../index")

‎packages/cli/src/services/standalone.ts‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Service, type Endpoint } from "@opencode-ai/client/effect/service"
22
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
33
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
4-
import { Effect, Schema, Stream } from "effect"
4+
import { Deferred, Effect, Schema, Stream } from "effect"
55
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
66
import { randomBytes } from "node:crypto"
77
import { selfCommand } from "../util/process"
@@ -36,8 +36,16 @@ const makeEndpoint = Effect.fn("cli.standalone.endpoint")(
3636
const password = randomBytes(32).toString("base64url")
3737
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
3838
const proc = yield* spawner.spawn(command(password, options))
39-
const output = yield* proc.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.take(1), Stream.mkString)
40-
if (!output) return yield* Effect.fail(new Error("Standalone server exited before reporting readiness"))
39+
const readyLine = yield* Deferred.make<string, Error>()
40+
// Keep draining stdout after readiness so later server writes cannot hit EPIPE.
41+
yield* proc.stdout.pipe(
42+
Stream.decodeText(),
43+
Stream.splitLines,
44+
Stream.runForEach((line) => Deferred.succeed(readyLine, line)),
45+
Effect.ensuring(Deferred.fail(readyLine, new Error("Standalone server exited before reporting readiness"))),
46+
Effect.forkScoped,
47+
)
48+
const output = yield* Deferred.await(readyLine)
4149
const ready = yield* Effect.tryPromise(() => decodeReady(output))
4250
return {
4351
url: ready.url,

0 commit comments

Comments
 (0)