import { cmd } from "@/cli/cmd/cmd" import { Rpc } from "@/util/rpc" import { type rpc } from "../tui/worker" import path from "path" import { fileURLToPath } from "url" import { UI } from "@/cli/ui" import { errorMessage } from "@opencode-ai/tui/util/error" import { withTimeout } from "@/util/timeout" import { withNetworkOptions, resolveNetworkOptionsNoConfig, hasArg } from "@/cli/network" import { Filesystem } from "@/util/filesystem" import { OpenCode } from "@opencode-ai/client/promise" import { createOpencodeClient } from "@opencode-ai/sdk/v2" import { writeHeapSnapshot } from "v8" import { ServerAuth } from "@/server/auth" import { validateSession } from "../tui/validate-session" import { win32InstallCtrlCGuard } from "@opencode-ai/tui/terminal-win32" declare global { const OPENCODE_WORKER_PATH: string } async function target() { if (typeof OPENCODE_WORKER_PATH !== "undefined") return OPENCODE_WORKER_PATH const dist = new URL("./cli/tui/worker.js", import.meta.url) if (await Filesystem.exists(fileURLToPath(dist))) return dist return new URL("../tui/worker.ts", import.meta.url) } async function input(value?: string) { const piped = process.stdin.isTTY ? undefined : await Bun.stdin.text() if (!value) return piped if (!piped) return value return piped + "\n" + value } export function resolveThreadDirectory(project?: string, envPWD = process.env.PWD, cwd = process.cwd()) { const root = Filesystem.resolve(envPWD ?? cwd) if (project) return Filesystem.resolve(path.isAbsolute(project) ? project : path.join(root, project)) return Filesystem.resolve(cwd) } 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", }) .option("auto", { type: "boolean", describe: "auto-approve permissions that are not explicitly denied (dangerous!)", default: false, }) .option("yolo", { type: "boolean", hidden: true, default: false, }) .option("dangerously-skip-permissions", { type: "boolean", hidden: true, default: false, }), handler: async (args) => { const unguard = win32InstallCtrlCGuard() try { const { TuiConfig } = await import("@/config/tui") if (args.fork && !args.continue && !args.session) { UI.error("--fork requires --continue or --session") process.exitCode = 1 return } // Resolve relative --project paths from PWD, then use the real cwd after // chdir so the thread and worker share the same directory key. const next = resolveThreadDirectory(args.project) const file = await target() try { process.chdir(next) } catch { UI.error("Failed to change directory to " + next) return } const cwd = Filesystem.resolve(process.cwd()) const worker = new Worker(file, { env: Object.fromEntries( Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined), ), }) const client = Rpc.client(worker) const reload = () => { client.call("reload", undefined).catch(() => {}) } process.on("SIGUSR2", reload) let stopped = false const stop = async () => { if (stopped) return stopped = true process.off("SIGUSR2", reload) await withTimeout(client.call("shutdown", undefined), 5000).catch(() => {}) worker.terminate() } const prompt = await input(args.prompt) const config = await TuiConfig.get() const network = resolveNetworkOptionsNoConfig(args) const external = hasArg("--port") || hasArg("--hostname") || network.mdns === true const headers = external ? ServerAuth.headers() : undefined const url = (await client.call("server", network)).url try { await validateSession({ url, sessionID: args.session, directory: cwd, headers, }) } catch (error) { UI.error(errorMessage(error)) process.exitCode = 1 return } setTimeout(() => { client.call("checkUpgrade", { directory: cwd }).catch(() => {}) }, 1000).unref?.() try { const { Effect } = await import("effect") const { run } = await import("../tui/layer") const { createLegacyTuiPluginHost } = await import("@/plugin/tui/runtime") await Effect.runPromise( run({ // @ts-expect-error V1 does not consume the V2-only server input. client: createOpencodeClient({ baseUrl: url, headers, directory: cwd }), api: OpenCode.make({ baseUrl: url, headers }), async onSnapshot() { const tui = writeHeapSnapshot("tui.heapsnapshot") const server = await client.call("snapshot", undefined) return [tui, server] }, config, pluginHost: createLegacyTuiPluginHost(), args: { continue: args.continue, sessionID: args.session, agent: args.agent, model: args.model, prompt, fork: args.fork, auto: args.auto || args.yolo || args["dangerously-skip-permissions"], }, }), ) } finally { await stop() } } finally { try { unguard?.() } catch {} } process.exit(0) }, }) // scratch