Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions packages/opencode/src/cli/stdout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export type FlushableWriteStream = {
destroyed?: boolean
writableEnded?: boolean
write(chunk: string, callback: () => void): boolean
}

export function flushWriteStream(stream: FlushableWriteStream) {
if (stream.destroyed || stream.writableEnded) return Promise.resolve()
return new Promise<void>((resolve) => {
stream.write("", () => resolve())
})
}
2 changes: 2 additions & 0 deletions packages/opencode/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { Heap } from "./cli/heap"
import { drizzle } from "drizzle-orm/bun-sqlite"
import { ensureProcessMetadata } from "@opencode-ai/core/util/opencode-process"
import { isRecord } from "@/util/record"
import { flushWriteStream } from "@/cli/stdout"

const processMetadata = ensureProcessMetadata("main")

Expand Down Expand Up @@ -247,5 +248,6 @@ try {
// Most notably, some docker-container-based MCP servers don't handle such signals unless
// run using `docker run --init`.
// Explicitly exit to avoid any hanging subprocesses.
await Promise.all([flushWriteStream(process.stdout), flushWriteStream(process.stderr)])
process.exit()
}
40 changes: 40 additions & 0 deletions packages/opencode/test/cli/stdout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { describe, expect, test } from "bun:test"
import { flushWriteStream } from "../../src/cli/stdout"

describe("flushWriteStream", () => {
test("waits for the stream write callback", async () => {
let flush: (() => void) | undefined
let resolved = false

const pending = flushWriteStream({
write(_chunk, callback) {
flush = () => callback()
return false
},
}).then(() => {
resolved = true
})

await Promise.resolve()
expect(resolved).toBe(false)

flush?.()
await pending

expect(resolved).toBe(true)
})

test("skips destroyed streams", async () => {
let wrote = false

await flushWriteStream({
destroyed: true,
write() {
wrote = true
return true
},
})

expect(wrote).toBe(false)
})
})
Loading