effect(util): make Process.run/text/lines return Effect

Migrate the run/text/lines helpers in util/process.ts to Effect-returning
functions that yield ChildProcessSpawner and use ChildProcess.make
internally. The legacy Promise-based behaviour stays available under
runPromise/textPromise/linesPromise for non-Effect callers; these are
thin wrappers over the original spawn() path so AbortSignal and timeout
semantics are preserved.

server.ts now runs Process.run/Process.text through a private
ManagedRuntime backed by CrossSpawnSpawner.defaultLayer and the shared
memoMap, since LSP.spawn callbacks execute inside Effect.promise blocks.

Other Process.run/text/lines call sites are renamed to the *Promise
variants and otherwise left untouched for follow-up migrations.
This commit is contained in:
Kit Langton 2026-05-12 16:31:38 -04:00
commit 82b13ca184
14 changed files with 173 additions and 68 deletions

View file

@ -29,14 +29,14 @@ export const PrCommand = effectCmd({
UI.println(`Fetching and checking out PR #${prNumber}...`) UI.println(`Fetching and checking out PR #${prNumber}...`)
const checkout = yield* Effect.promise(() => const checkout = yield* Effect.promise(() =>
Process.run(["gh", "pr", "checkout", `${prNumber}`, "--branch", localBranchName, "--force"], { nothrow: true }), Process.runPromise(["gh", "pr", "checkout", `${prNumber}`, "--branch", localBranchName, "--force"], { nothrow: true }),
) )
if (checkout.code !== 0) { if (checkout.code !== 0) {
return yield* fail(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`) return yield* fail(`Failed to checkout PR #${prNumber}. Make sure you have gh CLI installed and authenticated.`)
} }
const prInfoResult = yield* Effect.promise(() => const prInfoResult = yield* Effect.promise(() =>
Process.text( Process.textPromise(
[ [
"gh", "gh",
"pr", "pr",
@ -80,7 +80,7 @@ export const PrCommand = effectCmd({
UI.println(`Importing session...`) UI.println(`Importing session...`)
const importResult = yield* Effect.promise(() => const importResult = yield* Effect.promise(() =>
Process.text(["opencode", "import", sessionUrl], { nothrow: true }), Process.textPromise(["opencode", "import", sessionUrl], { nothrow: true }),
) )
if (importResult.code === 0) { if (importResult.code === 0) {
const sessionIdMatch = importResult.text.trim().match(/Imported session: ([a-zA-Z0-9_-]+)/) const sessionIdMatch = importResult.text.trim().match(/Imported session: ([a-zA-Z0-9_-]+)/)

View file

@ -50,7 +50,7 @@ export async function read(): Promise<Content | undefined> {
if (os === "darwin") { if (os === "darwin") {
const tmpfile = path.join(tmpdir(), "opencode-clipboard.png") const tmpfile = path.join(tmpdir(), "opencode-clipboard.png")
try { try {
await Process.run( await Process.runPromise(
[ [
"osascript", "osascript",
"-e", "-e",
@ -79,7 +79,7 @@ export async function read(): Promise<Content | undefined> {
if (os === "win32" || release().includes("WSL")) { if (os === "win32" || release().includes("WSL")) {
const script = const script =
"Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }" "Add-Type -AssemblyName System.Windows.Forms; $img = [System.Windows.Forms.Clipboard]::GetImage(); if ($img) { $ms = New-Object System.IO.MemoryStream; $img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png); [System.Convert]::ToBase64String($ms.ToArray()) }"
const base64 = await Process.text(["powershell.exe", "-NonInteractive", "-NoProfile", "-command", script], { const base64 = await Process.textPromise(["powershell.exe", "-NonInteractive", "-NoProfile", "-command", script], {
nothrow: true, nothrow: true,
}) })
if (base64.text) { if (base64.text) {
@ -91,11 +91,11 @@ export async function read(): Promise<Content | undefined> {
} }
if (os === "linux") { if (os === "linux") {
const wayland = await Process.run(["wl-paste", "-t", "image/png"], { nothrow: true }) const wayland = await Process.runPromise(["wl-paste", "-t", "image/png"], { nothrow: true })
if (wayland.stdout.byteLength > 0) { if (wayland.stdout.byteLength > 0) {
return { data: Buffer.from(wayland.stdout).toString("base64"), mime: "image/png" } return { data: Buffer.from(wayland.stdout).toString("base64"), mime: "image/png" }
} }
const x11 = await Process.run(["xclip", "-selection", "clipboard", "-t", "image/png", "-o"], { const x11 = await Process.runPromise(["xclip", "-selection", "clipboard", "-t", "image/png", "-o"], {
nothrow: true, nothrow: true,
}) })
if (x11.stdout.byteLength > 0) { if (x11.stdout.byteLength > 0) {
@ -118,7 +118,7 @@ const getCopyMethod = lazy(async () => {
console.log("clipboard: using osascript") console.log("clipboard: using osascript")
return async (text: string) => { return async (text: string) => {
const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"') const escaped = text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')
await Process.run(["osascript", "-e", `set the clipboard to "${escaped}"`], { nothrow: true }) await Process.runPromise(["osascript", "-e", `set the clipboard to "${escaped}"`], { nothrow: true })
} }
} }

View file

@ -192,7 +192,7 @@ async function executeUninstall(method: Installation.Method, targets: RemovalTar
const cmd = cmds[method] const cmd = cmds[method]
if (cmd) { if (cmd) {
spinner.start(`Running ${cmd.join(" ")}...`) spinner.start(`Running ${cmd.join(" ")}...`)
const result = await Process.run(method === "choco" ? ["choco", "uninstall", "opencode", "-y", "-r"] : cmd, { const result = await Process.runPromise(method === "choco" ? ["choco", "uninstall", "opencode", "-y", "-r"] : cmd, {
nothrow: true, nothrow: true,
}) })
if (result.code !== 0) { if (result.code !== 0) {

View file

@ -56,7 +56,7 @@ export async function readManagedPreferences() {
for (const plist of paths) { for (const plist of paths) {
if (!existsSync(plist)) continue if (!existsSync(plist)) continue
log.info("reading macOS managed preferences", { path: plist }) log.info("reading macOS managed preferences", { path: plist })
const result = await Process.run(["plutil", "-convert", "json", "-o", "-", plist], { nothrow: true }) const result = await Process.runPromise(["plutil", "-convert", "json", "-o", "-", plist], { nothrow: true })
if (result.code !== 0) { if (result.code !== 0) {
log.warn("failed to convert managed preferences plist", { path: plist }) log.warn("failed to convert managed preferences plist", { path: plist })
continue continue

View file

@ -221,7 +221,7 @@ export const rlang: Info = {
const air = which("air") const air = which("air")
if (air == null) return false if (air == null) return false
const output = await Process.text([air, "--help"], { nothrow: true }) const output = await Process.textPromise([air, "--help"], { nothrow: true })
// Check for "Air: An R language server and formatter" // Check for "Air: An R language server and formatter"
const firstLine = output.text.split("\n")[0] const firstLine = output.text.split("\n")[0]
@ -239,7 +239,7 @@ export const uvformat: Info = {
if (await ruff.enabled(context)) return false if (await ruff.enabled(context)) return false
const uv = which("uv") const uv = which("uv")
if (uv == null) return false if (uv == null) return false
const output = await Process.run([uv, "format", "--help"], { nothrow: true }) const output = await Process.runPromise([uv, "format", "--help"], { nothrow: true })
if (output.code === 0) return [uv, "format", "--", "$FILE"] if (output.code === 0) return [uv, "format", "--", "$FILE"]
return false return false
}, },

View file

@ -47,7 +47,7 @@ export async function install(ide: (typeof SUPPORTED_IDES)[number]["name"]) {
const cmd = SUPPORTED_IDES.find((i) => i.name === ide)?.cmd const cmd = SUPPORTED_IDES.find((i) => i.name === ide)?.cmd
if (!cmd) throw new Error(`Unknown IDE: ${ide}`) if (!cmd) throw new Error(`Unknown IDE: ${ide}`)
const p = await Process.run([cmd, "--install-extension", "sst-dev.opencode"], { const p = await Process.runPromise([cmd, "--install-extension", "sst-dev.opencode"], {
nothrow: true, nothrow: true,
}) })
const stdout = p.stdout.toString() const stdout = p.stdout.toString()

View file

@ -5,6 +5,9 @@ import { Global } from "@opencode-ai/core/global"
import * as Log from "@opencode-ai/core/util/log" import * as Log from "@opencode-ai/core/util/log"
import { text } from "node:stream/consumers" import { text } from "node:stream/consumers"
import fs from "fs/promises" import fs from "fs/promises"
import { Effect, ManagedRuntime } from "effect"
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
import { memoMap } from "@opencode-ai/core/effect/memo-map"
import { Filesystem } from "@/util/filesystem" import { Filesystem } from "@/util/filesystem"
import type { InstanceContext } from "../project/instance" import type { InstanceContext } from "../project/instance"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
@ -21,8 +24,17 @@ const pathExists = async (p: string) =>
.stat(p) .stat(p)
.then(() => true) .then(() => true)
.catch(() => false) .catch(() => false)
const run = (cmd: string[], opts: Process.RunOptions = {}) => Process.run(cmd, { ...opts, nothrow: true })
const output = (cmd: string[], opts: Process.RunOptions = {}) => Process.text(cmd, { ...opts, nothrow: true }) // Private runtime so the Effect-returning `Process.run` / `Process.text` can be
// invoked from this file's promise-based spawn callbacks. The `LSP` service
// layer in `lsp.ts` calls these spawn functions inside `Effect.promise(async
// () => ...)`, so a re-entry point is needed. Sharing `memoMap` keeps a single
// `ChildProcessSpawner` instance across the process.
const processRuntime = ManagedRuntime.make(CrossSpawnSpawner.defaultLayer, { memoMap })
const run = (cmd: string[], opts: Process.RunOptions = {}) =>
processRuntime.runPromise(Process.run(cmd, { ...opts, nothrow: true }))
const output = (cmd: string[], opts: Process.RunOptions = {}) =>
processRuntime.runPromise(Process.text(cmd, { ...opts, nothrow: true }))
export interface Handle { export interface Handle {
process: ChildProcessWithoutNullStreams process: ChildProcessWithoutNullStreams
@ -188,8 +200,12 @@ export const ESLint: Info = {
await fs.rename(extractedPath, finalPath) await fs.rename(extractedPath, finalPath)
const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm" const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm"
await Process.run([npmCmd, "install"], { cwd: finalPath }) await processRuntime.runPromise(
await Process.run([npmCmd, "run", "compile"], { cwd: finalPath }) Effect.gen(function* () {
yield* Process.run([npmCmd, "install"], { cwd: finalPath })
yield* Process.run([npmCmd, "run", "compile"], { cwd: finalPath })
}),
)
log.info("installed VS Code ESLint server", { serverPath }) log.info("installed VS Code ESLint server", { serverPath })
} }
@ -570,9 +586,13 @@ export const ElixirLS: Info = {
const cwd = path.join(Global.Path.bin, "elixir-ls-master") const cwd = path.join(Global.Path.bin, "elixir-ls-master")
const env = { MIX_ENV: "prod", ...process.env } const env = { MIX_ENV: "prod", ...process.env }
await Process.run(["mix", "deps.get"], { cwd, env }) await processRuntime.runPromise(
await Process.run(["mix", "compile"], { cwd, env }) Effect.gen(function* () {
await Process.run(["mix", "elixir_ls.release2", "-o", "release"], { cwd, env }) yield* Process.run(["mix", "deps.get"], { cwd, env })
yield* Process.run(["mix", "compile"], { cwd, env })
yield* Process.run(["mix", "elixir_ls.release2", "-o", "release"], { cwd, env })
}),
)
log.info(`installed elixir-ls`, { log.info(`installed elixir-ls`, {
path: elixirLsPath, path: elixirLsPath,

View file

@ -1908,7 +1908,7 @@ NOTE: At any point in time through this workflow you should feel free to ask the
const sh = Shell.preferred(cfg.shell) const sh = Shell.preferred(cfg.shell)
const results = yield* Effect.promise(() => const results = yield* Effect.promise(() =>
Promise.all( Promise.all(
shellMatches.map(async ([, cmd]) => (await Process.text([cmd], { shell: sh, nothrow: true })).text), shellMatches.map(async ([, cmd]) => (await Process.textPromise([cmd], { shell: sh, nothrow: true })).text),
), ),
) )
let index = 0 let index = 0

View file

@ -7,11 +7,11 @@ export async function extractZip(zipPath: string, destDir: string) {
const winDestDir = path.resolve(destDir) const winDestDir = path.resolve(destDir)
// $global:ProgressPreference suppresses PowerShell's blue progress bar popup // $global:ProgressPreference suppresses PowerShell's blue progress bar popup
const cmd = `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -Path '${winZipPath}' -DestinationPath '${winDestDir}' -Force` const cmd = `$global:ProgressPreference = 'SilentlyContinue'; Expand-Archive -Path '${winZipPath}' -DestinationPath '${winDestDir}' -Force`
await Process.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd]) await Process.runPromise(["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd])
return return
} }
await Process.run(["unzip", "-o", "-q", zipPath, "-d", destDir]) await Process.runPromise(["unzip", "-o", "-q", zipPath, "-d", destDir])
} }
export * as Archive from "./archive" export * as Archive from "./archive"

View file

@ -1,6 +1,8 @@
import { type ChildProcess } from "child_process" import { type ChildProcess as NodeChildProcess } from "child_process"
import launch from "cross-spawn" import launch from "cross-spawn"
import { buffer } from "node:stream/consumers" import { buffer } from "node:stream/consumers"
import { Effect, Stream } from "effect"
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
import { errorMessage } from "./error" import { errorMessage } from "./error"
export type Stdio = "inherit" | "pipe" | "ignore" export type Stdio = "inherit" | "pipe" | "ignore"
@ -53,7 +55,7 @@ export class RunFailedError extends Error {
} }
} }
export type Child = ChildProcess & { exited: Promise<number> } export type Child = NodeChildProcess & { exited: Promise<number> }
export function spawn(cmd: string[], opts: Options = {}): Child { export function spawn(cmd: string[], opts: Options = {}): Child {
if (cmd.length === 0) throw new Error("Command is required") if (cmd.length === 0) throw new Error("Command is required")
@ -110,8 +112,104 @@ export function spawn(cmd: string[], opts: Options = {}): Child {
return child return child
} }
export async function run(cmd: string[], opts: RunOptions = {}): Promise<Result> { // Duplicated in `packages/sdk/js/src/process.ts` because the SDK cannot import
const proc = spawn(cmd, { // `opencode` without creating a cycle. Keep both copies in sync.
export async function stop(proc: NodeChildProcess) {
if (proc.exitCode !== null || proc.signalCode !== null) return
if (process.platform !== "win32" || !proc.pid) {
proc.kill()
return
}
const out = await runPromise(["taskkill", "/pid", String(proc.pid), "/T", "/F"], {
nothrow: true,
})
if (out.code === 0) return
proc.kill()
}
const mergeEnv = (env: NodeJS.ProcessEnv | null | undefined): { env: Record<string, string>; extendEnv: boolean } => {
if (env === null) return { env: {}, extendEnv: false }
if (env === undefined) return { env: {}, extendEnv: true }
const out: Record<string, string> = {}
for (const [k, v] of Object.entries(env)) {
if (v !== undefined) out[k] = v
}
return { env: out, extendEnv: true }
}
export const run = Effect.fn("Process.run")(function* (cmd: string[], opts: RunOptions = {}) {
if (cmd.length === 0) return yield* Effect.die(new Error("Command is required"))
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
const { env, extendEnv } = mergeEnv(opts.env)
const result = yield* Effect.scoped(
Effect.gen(function* () {
const proc = ChildProcess.make(cmd[0], cmd.slice(1), {
cwd: opts.cwd,
env,
extendEnv,
shell: opts.shell,
stdin: opts.stdin ?? "ignore",
stdout: "pipe",
stderr: "pipe",
})
const handle = yield* spawner.spawn(proc)
const [stdoutBytes, stderrBytes, exitCode] = yield* Effect.all(
[Stream.mkUint8Array(handle.stdout), Stream.mkUint8Array(handle.stderr), handle.exitCode],
{ concurrency: 3 },
)
return {
code: exitCode as number,
stdout: Buffer.from(stdoutBytes),
stderr: Buffer.from(stderrBytes),
} satisfies Result
}),
).pipe(
Effect.catch((err) =>
opts.nothrow
? Effect.succeed({
code: 1,
stdout: Buffer.alloc(0),
stderr: Buffer.from(errorMessage(err)),
} satisfies Result)
: Effect.die(err),
),
)
if (result.code === 0 || opts.nothrow) return result
return yield* Effect.die(new RunFailedError(cmd, result.code, result.stdout, result.stderr))
})
export const text = Effect.fn("Process.text")(function* (cmd: string[], opts: RunOptions = {}) {
const out = yield* run(cmd, opts)
return {
...out,
text: out.stdout.toString(),
} satisfies TextResult
})
export const lines = Effect.fn("Process.lines")(function* (cmd: string[], opts: RunOptions = {}) {
const out = yield* text(cmd, opts)
return out.text.split(/\r?\n/).filter(Boolean)
})
// ---------------------------------------------------------------------------
// Promise-returning facades for legacy non-Effect callers.
//
// The new `run` / `text` / `lines` exports above return Effects. These
// wrappers preserve the original Promise-based shape (failing with
// `RunFailedError` on non-zero exit, etc.) and the legacy AbortSignal /
// timeout semantics by using `spawn(...)` directly.
//
// New code should yield the Effect versions. These wrappers exist only to
// avoid touching the remaining non-Effect call sites in this PR.
// ---------------------------------------------------------------------------
export function runPromise(cmd: string[], opts: RunOptions = {}): Promise<Result> {
const spawnOpts = {
cwd: opts.cwd, cwd: opts.cwd,
env: opts.env, env: opts.env,
stdin: opts.stdin, stdin: opts.stdin,
@ -119,13 +217,16 @@ export async function run(cmd: string[], opts: RunOptions = {}): Promise<Result>
abort: opts.abort, abort: opts.abort,
kill: opts.kill, kill: opts.kill,
timeout: opts.timeout, timeout: opts.timeout,
stdout: "pipe", stdout: "pipe" as const,
stderr: "pipe", stderr: "pipe" as const,
}) }
if (!proc.stdout || !proc.stderr) throw new Error("Process output not available") // Preserve the legacy abort/timeout semantics by using `spawn(...)` directly
// rather than the Effect path (which lacks AbortSignal hooks today).
const proc = spawn(cmd, spawnOpts)
if (!proc.stdout || !proc.stderr) return Promise.reject(new Error("Process output not available"))
const out = await Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)]) return Promise.all([proc.exited, buffer(proc.stdout), buffer(proc.stderr)])
.then(([code, stdout, stderr]) => ({ .then(([code, stdout, stderr]) => ({
code, code,
stdout, stdout,
@ -137,40 +238,24 @@ export async function run(cmd: string[], opts: RunOptions = {}): Promise<Result>
code: 1, code: 1,
stdout: Buffer.alloc(0), stdout: Buffer.alloc(0),
stderr: Buffer.from(errorMessage(err)), stderr: Buffer.from(errorMessage(err)),
} } satisfies Result
})
.then((out) => {
if (out.code === 0 || opts.nothrow) return out
throw new RunFailedError(cmd, out.code, out.stdout, out.stderr)
}) })
if (out.code === 0 || opts.nothrow) return out
throw new RunFailedError(cmd, out.code, out.stdout, out.stderr)
} }
// Duplicated in `packages/sdk/js/src/process.ts` because the SDK cannot import export async function textPromise(cmd: string[], opts: RunOptions = {}): Promise<TextResult> {
// `opencode` without creating a cycle. Keep both copies in sync. const out = await runPromise(cmd, opts)
export async function stop(proc: ChildProcess) {
if (proc.exitCode !== null || proc.signalCode !== null) return
if (process.platform !== "win32" || !proc.pid) {
proc.kill()
return
}
const out = await run(["taskkill", "/pid", String(proc.pid), "/T", "/F"], {
nothrow: true,
})
if (out.code === 0) return
proc.kill()
}
export async function text(cmd: string[], opts: RunOptions = {}): Promise<TextResult> {
const out = await run(cmd, opts)
return { return {
...out, ...out,
text: out.stdout.toString(), text: out.stdout.toString(),
} }
} }
export async function lines(cmd: string[], opts: RunOptions = {}): Promise<string[]> { export async function linesPromise(cmd: string[], opts: RunOptions = {}): Promise<string[]> {
return (await text(cmd, opts)).text.split(/\r?\n/).filter(Boolean) return (await textPromise(cmd, opts)).text.split(/\r?\n/).filter(Boolean)
} }
export * as Process from "./process" export * as Process from "./process"

View file

@ -17,7 +17,7 @@ type Msg = {
} }
function run(msg: Msg) { function run(msg: Msg) {
return Process.run([process.execPath, worker, JSON.stringify(msg)], { return Process.runPromise([process.execPath, worker, JSON.stringify(msg)], {
cwd: root, cwd: root,
nothrow: true, nothrow: true,
}) })

View file

@ -12,7 +12,7 @@ const root = path.join(import.meta.dir, "../..")
const worker = path.join(import.meta.dir, "../fixture/plugin-meta-worker.ts") const worker = path.join(import.meta.dir, "../fixture/plugin-meta-worker.ts")
function run(input: { file: string; spec: string; target: string; id: string }) { function run(input: { file: string; spec: string; target: string; id: string }) {
return Process.run([process.execPath, worker, JSON.stringify(input)], { return Process.runPromise([process.execPath, worker, JSON.stringify(input)], {
cwd: root, cwd: root,
nothrow: true, nothrow: true,
}) })

View file

@ -224,7 +224,7 @@ describe("Truncate", () => {
) )
test("loads truncate effect in a fresh process", async () => { test("loads truncate effect in a fresh process", async () => {
const out = await Process.run([process.execPath, "run", path.join(ROOT, "src", "tool", "truncate.ts")], { const out = await Process.runPromise([process.execPath, "run", path.join(ROOT, "src", "tool", "truncate.ts")], {
cwd: ROOT, cwd: ROOT,
}) })

View file

@ -10,19 +10,19 @@ function node(script: string) {
describe("util.process", () => { describe("util.process", () => {
test("captures stdout and stderr", async () => { test("captures stdout and stderr", async () => {
const out = await Process.run(node('process.stdout.write("out");process.stderr.write("err")')) const out = await Process.runPromise(node('process.stdout.write("out");process.stderr.write("err")'))
expect(out.code).toBe(0) expect(out.code).toBe(0)
expect(out.stdout.toString()).toBe("out") expect(out.stdout.toString()).toBe("out")
expect(out.stderr.toString()).toBe("err") expect(out.stderr.toString()).toBe("err")
}) })
test("returns code when nothrow is enabled", async () => { test("returns code when nothrow is enabled", async () => {
const out = await Process.run(node("process.exit(7)"), { nothrow: true }) const out = await Process.runPromise(node("process.exit(7)"), { nothrow: true })
expect(out.code).toBe(7) expect(out.code).toBe(7)
}) })
test("throws RunFailedError on non-zero exit", async () => { test("throws RunFailedError on non-zero exit", async () => {
const err = await Process.run(node('process.stderr.write("bad");process.exit(3)')).catch((error) => error) const err = await Process.runPromise(node('process.stderr.write("bad");process.exit(3)')).catch((error) => error)
expect(err).toBeInstanceOf(Process.RunFailedError) expect(err).toBeInstanceOf(Process.RunFailedError)
if (!(err instanceof Process.RunFailedError)) throw err if (!(err instanceof Process.RunFailedError)) throw err
expect(err.code).toBe(3) expect(err.code).toBe(3)
@ -34,7 +34,7 @@ describe("util.process", () => {
const started = Date.now() const started = Date.now()
setTimeout(() => abort.abort(), 25) setTimeout(() => abort.abort(), 25)
const out = await Process.run(node("setInterval(() => {}, 1000)"), { const out = await Process.runPromise(node("setInterval(() => {}, 1000)"), {
abort: abort.signal, abort: abort.signal,
nothrow: true, nothrow: true,
}) })
@ -50,7 +50,7 @@ describe("util.process", () => {
const started = Date.now() const started = Date.now()
setTimeout(() => abort.abort(), 25) setTimeout(() => abort.abort(), 25)
const out = await Process.run(node('process.on("SIGTERM", () => {}); setInterval(() => {}, 1000)'), { const out = await Process.runPromise(node('process.on("SIGTERM", () => {}); setInterval(() => {}, 1000)'), {
abort: abort.signal, abort: abort.signal,
nothrow: true, nothrow: true,
timeout: 25, timeout: 25,
@ -62,14 +62,14 @@ describe("util.process", () => {
test("uses cwd when spawning commands", async () => { test("uses cwd when spawning commands", async () => {
await using tmp = await tmpdir() await using tmp = await tmpdir()
const out = await Process.run(node("process.stdout.write(process.cwd())"), { const out = await Process.runPromise(node("process.stdout.write(process.cwd())"), {
cwd: tmp.path, cwd: tmp.path,
}) })
expect(out.stdout.toString()).toBe(tmp.path) expect(out.stdout.toString()).toBe(tmp.path)
}) })
test("merges environment overrides", async () => { test("merges environment overrides", async () => {
const out = await Process.run(node('process.stdout.write(process.env.OPENCODE_TEST ?? "")'), { const out = await Process.runPromise(node('process.stdout.write(process.env.OPENCODE_TEST ?? "")'), {
env: { env: {
OPENCODE_TEST: "set", OPENCODE_TEST: "set",
}, },
@ -80,7 +80,7 @@ describe("util.process", () => {
test("uses shell in run on Windows", async () => { test("uses shell in run on Windows", async () => {
if (process.platform !== "win32") return if (process.platform !== "win32") return
const out = await Process.run(["set", "OPENCODE_TEST_SHELL"], { const out = await Process.runPromise(["set", "OPENCODE_TEST_SHELL"], {
shell: true, shell: true,
env: { env: {
OPENCODE_TEST_SHELL: "ok", OPENCODE_TEST_SHELL: "ok",