| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- import { cmd } from "@/cli/cmd/cmd"
- import { tui } from "./app"
- import { Rpc } from "@/util/rpc"
- import { type rpc } from "./worker"
- import path from "path"
- import { fileURLToPath } from "url"
- import { UI } from "@/cli/ui"
- import { Log } from "@/util/log"
- import { withTimeout } from "@/util/timeout"
- import { withNetworkOptions, resolveNetworkOptions } from "@/cli/network"
- import { Filesystem } from "@/util/filesystem"
- import type { Event } from "@opencode-ai/sdk/v2"
- import type { EventSource } from "./context/sdk"
- import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
- import { TuiConfig } from "@/config/tui"
- import { Instance } from "@/project/instance"
- declare global {
- const OPENCODE_WORKER_PATH: string
- }
- type RpcClient = ReturnType<typeof Rpc.client<typeof rpc>>
- function createWorkerFetch(client: RpcClient): typeof fetch {
- const fn = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
- const request = new Request(input, init)
- const body = request.body ? await request.text() : undefined
- const result = await client.call("fetch", {
- url: request.url,
- method: request.method,
- headers: Object.fromEntries(request.headers.entries()),
- body,
- })
- return new Response(result.body, {
- status: result.status,
- headers: result.headers,
- })
- }
- return fn as typeof fetch
- }
- function createEventSource(client: RpcClient): EventSource {
- return {
- on: (handler) => client.on<Event>("event", handler),
- }
- }
- async function target() {
- if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH
- const dist = new URL("./cli/cmd/tui/worker.js", import.meta.url)
- if (await Filesystem.exists(fileURLToPath(dist))) return dist
- return new URL("./worker.ts", import.meta.url)
- }
- async function input(value?: string) {
- const piped = process.stdin.isTTY
- ? undefined
- : await new Promise<string>((resolve) => {
- const chunks: Buffer[] = []
- process.stdin.on("data", (chunk) => chunks.push(chunk))
- process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")))
- })
- if (!value) return piped
- if (!piped) return value
- return piped + "\n" + value
- }
- export const TuiThreadCommand = cmd({
- command: "$0 [project]",
- describe: "start opencode tui",
- builder: (yargs) =>
- withNetworkOptions(yargs)
- .positional("project", {
- type: "string",
- describe: "path to start opencode in",
- })
- .option("model", {
- type: "string",
- alias: ["m"],
- describe: "model to use in the format of provider/model",
- })
- .option("continue", {
- alias: ["c"],
- describe: "continue the last session",
- type: "boolean",
- })
- .option("session", {
- alias: ["s"],
- type: "string",
- describe: "session id to continue",
- })
- .option("fork", {
- type: "boolean",
- describe: "fork the session when continuing (use with --continue or --session)",
- })
- .option("prompt", {
- type: "string",
- describe: "prompt to use",
- })
- .option("agent", {
- type: "string",
- describe: "agent to use",
- }),
- handler: async (args) => {
- // Keep ENABLE_PROCESSED_INPUT cleared even if other code flips it.
- // (Important when running under `bun run` wrappers on Windows.)
- const unguard = win32InstallCtrlCGuard()
- try {
- // Must be the very first thing — disables CTRL_C_EVENT before any Worker
- // spawn or async work so the OS cannot kill the process group.
- win32DisableProcessedInput()
- if (args.fork && !args.continue && !args.session) {
- UI.error("--fork requires --continue or --session")
- process.exitCode = 1
- return
- }
- // Resolve relative paths against PWD to preserve behavior when using --cwd flag
- const root = process.env.PWD ?? process.cwd()
- const cwd = args.project ? path.resolve(root, args.project) : process.cwd()
- const file = await target()
- try {
- process.chdir(cwd)
- } catch {
- UI.error("Failed to change directory to " + cwd)
- return
- }
- const worker = new Worker(file, {
- env: Object.fromEntries(
- Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
- ),
- })
- worker.onerror = (e) => {
- Log.Default.error(e)
- }
- const client = Rpc.client<typeof rpc>(worker)
- const error = (e: unknown) => {
- Log.Default.error(e)
- }
- const reload = () => {
- client.call("reload", undefined).catch((err) => {
- Log.Default.warn("worker reload failed", {
- error: err instanceof Error ? err.message : String(err),
- })
- })
- }
- process.on("uncaughtException", error)
- process.on("unhandledRejection", error)
- process.on("SIGUSR2", reload)
- let stopped = false
- const stop = async () => {
- if (stopped) return
- stopped = true
- process.off("uncaughtException", error)
- process.off("unhandledRejection", error)
- process.off("SIGUSR2", reload)
- await withTimeout(client.call("shutdown", undefined), 5000).catch((error) => {
- Log.Default.warn("worker shutdown failed", {
- error: error instanceof Error ? error.message : String(error),
- })
- })
- worker.terminate()
- }
- const prompt = await input(args.prompt)
- const config = await Instance.provide({
- directory: cwd,
- fn: () => TuiConfig.get(),
- })
- const network = await resolveNetworkOptions(args)
- const external =
- process.argv.includes("--port") ||
- process.argv.includes("--hostname") ||
- process.argv.includes("--mdns") ||
- network.mdns ||
- network.port !== 0 ||
- network.hostname !== "127.0.0.1"
- const transport = external
- ? {
- url: (await client.call("server", network)).url,
- fetch: undefined,
- events: undefined,
- }
- : {
- url: "http://opencode.internal",
- fetch: createWorkerFetch(client),
- events: createEventSource(client),
- }
- setTimeout(() => {
- client.call("checkUpgrade", { directory: cwd }).catch(() => {})
- }, 1000).unref?.()
- try {
- await tui({
- url: transport.url,
- config,
- directory: cwd,
- fetch: transport.fetch,
- events: transport.events,
- args: {
- continue: args.continue,
- sessionID: args.session,
- agent: args.agent,
- model: args.model,
- prompt,
- fork: args.fork,
- },
- onExit: stop,
- })
- } finally {
- await stop()
- }
- } finally {
- unguard?.()
- }
- process.exit(0)
- },
- })
|