Skip to content
Merged
21 changes: 7 additions & 14 deletions packages/opencode/src/bun/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,21 @@ import { Log } from "../util/log"
import path from "path"
import { Filesystem } from "../util/filesystem"
import { NamedError } from "@opencode-ai/util/error"
import { readableStreamToText } from "bun"
import { text } from "node:stream/consumers"
import { Lock } from "../util/lock"
import { PackageRegistry } from "./registry"
import { proxied } from "@/util/proxied"
import { Process } from "../util/process"

export namespace BunProc {
const log = Log.create({ service: "bun" })

export async function run(cmd: string[], options?: Bun.SpawnOptions.OptionsObject<any, any, any>) {
export async function run(cmd: string[], options?: Process.Options) {
log.info("running", {
cmd: [which(), ...cmd],
...options,
})
const result = Bun.spawn([which(), ...cmd], {
const result = Process.spawn([which(), ...cmd], {
...options,
stdout: "pipe",
stderr: "pipe",
Expand All @@ -28,23 +29,15 @@ export namespace BunProc {
},
})
const code = await result.exited
const stdout = result.stdout
? typeof result.stdout === "number"
? result.stdout
: await readableStreamToText(result.stdout)
: undefined
const stderr = result.stderr
? typeof result.stderr === "number"
? result.stderr
: await readableStreamToText(result.stderr)
: undefined
const stdout = result.stdout ? await text(result.stdout) : undefined
const stderr = result.stderr ? await text(result.stderr) : undefined
log.info("done", {
code,
stdout,
stderr,
})
if (code !== 0) {
throw new Error(`Command failed with exit code ${result.exitCode}`)
throw new Error(`Command failed with exit code ${code}`)
}
return result
}
Expand Down
10 changes: 6 additions & 4 deletions packages/opencode/src/bun/registry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { readableStreamToText, semver } from "bun"
import { semver } from "bun"
import { text } from "node:stream/consumers"
import { Log } from "../util/log"
import { Process } from "../util/process"

export namespace PackageRegistry {
const log = Log.create({ service: "bun" })
Expand All @@ -9,7 +11,7 @@ export namespace PackageRegistry {
}

export async function info(pkg: string, field: string, cwd?: string): Promise<string | null> {
const result = Bun.spawn([which(), "info", pkg, field], {
const result = Process.spawn([which(), "info", pkg, field], {
cwd,
stdout: "pipe",
stderr: "pipe",
Expand All @@ -20,8 +22,8 @@ export namespace PackageRegistry {
})

const code = await result.exited
const stdout = result.stdout ? await readableStreamToText(result.stdout) : ""
const stderr = result.stderr ? await readableStreamToText(result.stderr) : ""
const stdout = result.stdout ? await text(result.stdout) : ""
const stderr = result.stderr ? await text(result.stderr) : ""

if (code !== 0) {
log.warn("bun info failed", { pkg, field, code, stderr })
Expand Down
12 changes: 9 additions & 3 deletions packages/opencode/src/cli/cmd/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { Global } from "../../global"
import { Plugin } from "../../plugin"
import { Instance } from "../../project/instance"
import type { Hooks } from "@opencode-ai/plugin"
import { Process } from "../../util/process"
import { text } from "node:stream/consumers"

type PluginAuth = NonNullable<Hooks["auth"]>

Expand Down Expand Up @@ -263,8 +265,7 @@ export const AuthLoginCommand = cmd({
if (args.url) {
const wellknown = await fetch(`${args.url}/.well-known/opencode`).then((x) => x.json() as any)
prompts.log.info(`Running \`${wellknown.auth.command.join(" ")}\``)
const proc = Bun.spawn({
cmd: wellknown.auth.command,
const proc = Process.spawn(wellknown.auth.command, {
stdout: "pipe",
})
const exit = await proc.exited
Expand All @@ -273,7 +274,12 @@ export const AuthLoginCommand = cmd({
prompts.outro("Done")
return
}
const token = await new Response(proc.stdout).text()
if (!proc.stdout) {
prompts.log.error("Failed")
prompts.outro("Done")
return
}
const token = await text(proc.stdout)
await Auth.set(args.url, {
type: "wellknown",
key: wellknown.auth.env,
Expand Down
9 changes: 7 additions & 2 deletions packages/opencode/src/cli/cmd/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { UI } from "../ui"
import { Locale } from "../../util/locale"
import { Flag } from "../../flag/flag"
import { Filesystem } from "../../util/filesystem"
import { Process } from "../../util/process"
import { EOL } from "os"
import path from "path"

Expand Down Expand Up @@ -102,13 +103,17 @@ export const SessionListCommand = cmd({
const shouldPaginate = process.stdout.isTTY && !args.maxCount && args.format === "table"

if (shouldPaginate) {
const proc = Bun.spawn({
cmd: pagerCmd(),
const proc = Process.spawn(pagerCmd(), {
stdin: "pipe",
stdout: "inherit",
stderr: "inherit",
})

if (!proc.stdin) {
console.log(output)
return
}

proc.stdin.write(output)
proc.stdin.end()
await proc.exited
Expand Down
13 changes: 9 additions & 4 deletions packages/opencode/src/cli/cmd/tui/util/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { lazy } from "../../../../util/lazy.js"
import { tmpdir } from "os"
import path from "path"
import { Filesystem } from "../../../../util/filesystem"
import { Process } from "../../../../util/process"

/**
* Writes text to clipboard via OSC 52 escape sequence.
Expand Down Expand Up @@ -87,7 +88,8 @@ export namespace Clipboard {
if (process.env["WAYLAND_DISPLAY"] && Bun.which("wl-copy")) {
console.log("clipboard: using wl-copy")
return async (text: string) => {
const proc = Bun.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
const proc = Process.spawn(["wl-copy"], { stdin: "pipe", stdout: "ignore", stderr: "ignore" })
if (!proc.stdin) return
proc.stdin.write(text)
proc.stdin.end()
await proc.exited.catch(() => {})
Expand All @@ -96,11 +98,12 @@ export namespace Clipboard {
if (Bun.which("xclip")) {
console.log("clipboard: using xclip")
return async (text: string) => {
const proc = Bun.spawn(["xclip", "-selection", "clipboard"], {
const proc = Process.spawn(["xclip", "-selection", "clipboard"], {
stdin: "pipe",
stdout: "ignore",
stderr: "ignore",
})
if (!proc.stdin) return
proc.stdin.write(text)
proc.stdin.end()
await proc.exited.catch(() => {})
Expand All @@ -109,11 +112,12 @@ export namespace Clipboard {
if (Bun.which("xsel")) {
console.log("clipboard: using xsel")
return async (text: string) => {
const proc = Bun.spawn(["xsel", "--clipboard", "--input"], {
const proc = Process.spawn(["xsel", "--clipboard", "--input"], {
stdin: "pipe",
stdout: "ignore",
stderr: "ignore",
})
if (!proc.stdin) return
proc.stdin.write(text)
proc.stdin.end()
await proc.exited.catch(() => {})
Expand All @@ -125,7 +129,7 @@ export namespace Clipboard {
console.log("clipboard: using powershell")
return async (text: string) => {
// Pipe via stdin to avoid PowerShell string interpolation ($env:FOO, $(), etc.)
const proc = Bun.spawn(
const proc = Process.spawn(
[
"powershell.exe",
"-NonInteractive",
Expand All @@ -140,6 +144,7 @@ export namespace Clipboard {
},
)

if (!proc.stdin) return
proc.stdin.write(text)
proc.stdin.end()
await proc.exited.catch(() => {})
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/src/cli/cmd/tui/util/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { tmpdir } from "node:os"
import { join } from "node:path"
import { CliRenderer } from "@opentui/core"
import { Filesystem } from "@/util/filesystem"
import { Process } from "@/util/process"

export namespace Editor {
export async function open(opts: { value: string; renderer: CliRenderer }): Promise<string | undefined> {
Expand All @@ -17,8 +18,7 @@ export namespace Editor {
opts.renderer.suspend()
opts.renderer.currentRenderBuffer.clear()
const parts = editor.split(" ")
const proc = Bun.spawn({
cmd: [...parts, filepath],
const proc = Process.spawn([...parts, filepath], {
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
Expand Down
57 changes: 27 additions & 30 deletions packages/opencode/src/file/ripgrep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import { NamedError } from "@opencode-ai/util/error"
import { lazy } from "../util/lazy"
import { $ } from "bun"
import { Filesystem } from "../util/filesystem"
import { Process } from "../util/process"
import { text } from "node:stream/consumers"

import { ZipReader, BlobReader, BlobWriter } from "@zip.js/zip.js"
import { Log } from "@/util/log"
Expand Down Expand Up @@ -153,17 +155,19 @@ export namespace Ripgrep {
if (platformKey.endsWith("-darwin")) args.push("--include=*/rg")
if (platformKey.endsWith("-linux")) args.push("--wildcards", "*/rg")

const proc = Bun.spawn(args, {
const proc = Process.spawn(args, {
cwd: Global.Path.bin,
stderr: "pipe",
stdout: "pipe",
})
await proc.exited
if (proc.exitCode !== 0)
const exit = await proc.exited
if (exit !== 0) {
const stderr = proc.stderr ? await text(proc.stderr) : ""
throw new ExtractionFailedError({
filepath,
stderr: await Bun.readableStreamToText(proc.stderr),
stderr,
})
}
}
if (config.extension === "zip") {
const zipFileReader = new ZipReader(new BlobReader(new Blob([arrayBuffer])))
Expand Down Expand Up @@ -227,8 +231,7 @@ export namespace Ripgrep {
}
}

// Bun.spawn should throw this, but it incorrectly reports that the executable does not exist.
// See https://github.com/oven-sh/bun/issues/24012
// Guard against invalid cwd to provide a consistent ENOENT error.
if (!(await fs.stat(input.cwd).catch(() => undefined))?.isDirectory()) {
throw Object.assign(new Error(`No such file or directory: '${input.cwd}'`), {
code: "ENOENT",
Expand All @@ -237,41 +240,35 @@ export namespace Ripgrep {
})
}

const proc = Bun.spawn(args, {
const proc = Process.spawn(args, {
cwd: input.cwd,
stdout: "pipe",
stderr: "ignore",
maxBuffer: 1024 * 1024 * 20,
signal: input.signal,
abort: input.signal,
})

const reader = proc.stdout.getReader()
const decoder = new TextDecoder()
let buffer = ""

try {
while (true) {
input.signal?.throwIfAborted()
if (!proc.stdout) {
throw new Error("Process output not available")
}

const { done, value } = await reader.read()
if (done) break
let buffer = ""
const stream = proc.stdout as AsyncIterable<Buffer | string>
for await (const chunk of stream) {
input.signal?.throwIfAborted()

buffer += decoder.decode(value, { stream: true })
// Handle both Unix (\n) and Windows (\r\n) line endings
const lines = buffer.split(/\r?\n/)
buffer = lines.pop() || ""
buffer += typeof chunk === "string" ? chunk : chunk.toString()
// Handle both Unix (\n) and Windows (\r\n) line endings
const lines = buffer.split(/\r?\n/)
buffer = lines.pop() || ""

for (const line of lines) {
if (line) yield line
}
for (const line of lines) {
if (line) yield line
}

if (buffer) yield buffer
} finally {
reader.releaseLock()
await proc.exited
}

if (buffer) yield buffer
await proc.exited

input.signal?.throwIfAborted()
}

Expand Down
10 changes: 6 additions & 4 deletions packages/opencode/src/format/formatter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { readableStreamToText } from "bun"
import { text } from "node:stream/consumers"
import { BunProc } from "../bun"
import { Instance } from "../project/instance"
import { Filesystem } from "../util/filesystem"
import { Process } from "../util/process"
import { Flag } from "@/flag/flag"

export interface Info {
Expand Down Expand Up @@ -213,12 +214,13 @@ export const rlang: Info = {
if (airPath == null) return false

try {
const proc = Bun.spawn(["air", "--help"], {
const proc = Process.spawn(["air", "--help"], {
stdout: "pipe",
stderr: "pipe",
})
await proc.exited
const output = await readableStreamToText(proc.stdout)
if (!proc.stdout) return false
const output = await text(proc.stdout)

// Check for "Air: An R language server and formatter"
const firstLine = output.split("\n")[0]
Expand All @@ -238,7 +240,7 @@ export const uvformat: Info = {
async enabled() {
if (await ruff.enabled()) return false
if (Bun.which("uv") !== null) {
const proc = Bun.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
const proc = Process.spawn(["uv", "format", "--help"], { stderr: "pipe", stdout: "pipe" })
const code = await proc.exited
return code === 0
}
Expand Down
17 changes: 10 additions & 7 deletions packages/opencode/src/format/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import * as Formatter from "./formatter"
import { Config } from "../config/config"
import { mergeDeep } from "remeda"
import { Instance } from "../project/instance"
import { Process } from "../util/process"

export namespace Format {
const log = Log.create({ service: "format" })
Expand Down Expand Up @@ -110,13 +111,15 @@ export namespace Format {
for (const item of await getFormatter(ext)) {
log.info("running", { command: item.command })
try {
const proc = Bun.spawn({
cmd: item.command.map((x) => x.replace("$FILE", file)),
cwd: Instance.directory,
env: { ...process.env, ...item.environment },
stdout: "ignore",
stderr: "ignore",
})
const proc = Process.spawn(
item.command.map((x) => x.replace("$FILE", file)),
{
cwd: Instance.directory,
env: { ...process.env, ...item.environment },
stdout: "ignore",
stderr: "ignore",
},
)
const exit = await proc.exited
if (exit !== 0)
log.error("failed", {
Expand Down
Loading
Loading