Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228import { Service, type Endpoint } from "@opencode/client/effect/service"import { ClientError, OpenCode, type OpenCodeClient } from "@opencode/client/promise"import type { MiniFrontendInput } from "@opencode/tui/mini"import { setTimeout } from "node:timers/promises"import { readStdin } from "./util/io"import { createMiniHost, INTERACTIVE_INPUT_ERROR, usingInteractiveStdin } from "./mini-host"import { parseSessionTargetModel, resolveSessionTarget, type SessionTargetPreparation } from "./session-target"import { Env } from "./env"
export type MiniCommandInput = { server: { endpoint: Endpoint reconnect?: (signal: AbortSignal) => Promise<Endpoint> } continue?: boolean session?: string fork?: boolean model?: string agent?: string prompt?: string replay?: boolean replayLimit?: number demo?: boolean tuiConfig?: MiniFrontendInput["tuiConfig"] config?: MiniFrontendInput["config"] paths: { home: string; state: string; log: string }}
type Model = MiniFrontendInput["model"]
class MiniInputError extends Error {}
export async function runMini(input: MiniCommandInput) { try { validate(input) const result = await usingInteractiveStdin(async (terminal) => { const initialInput = mergeInput(process.stdin.isTTY ? undefined : await readStdin(), input.prompt) const frontendTask = import("@opencode/tui/mini") const directory = localDirectory() const connection = createMiniConnection(input.server) const sdk = connection.sdk const environment = input.server.reconnect ? Env.session() : undefined const requested = parseModel(input.model) const model = requested ? { providerID: requested.providerID, modelID: requested.id } : undefined const prepare = prepareTarget(input.agent) const resolveTarget = async (initial: OpenCodeClient, signal: AbortSignal) => { const resolved = await resolveMiniTarget({ sdk: initial, reconnect: connection.reconnect, signal, resolve: (client) => resolveSessionTarget({ client, location: { directory }, continue: input.continue, session: input.session, fork: input.fork, model: requested, agent: input.agent, environment, prepare, signal, }).catch((error) => { if (error instanceof Error && error.message === "Session not found") throw new MiniInputError(error.message) throw error }), }) const target = resolved.value return { sdk: resolved.sdk, sessionID: target.session.id, sessionTitle: target.session.title, location: target.location, model: target.model ? { providerID: target.model.providerID, modelID: target.model.id } : undefined, variant: target.model?.variant, agent: target.agent, resume: target.resume, } } const create = ( client: OpenCodeClient, next: { location: { directory: string } agent: string | undefined model: Model variant: string | undefined }, signal?: AbortSignal, ) => resolveSessionTarget({ client, location: { directory: next.location.directory }, agent: next.agent, environment, model: next.model ? { providerID: next.model.providerID, id: next.model.modelID, variant: next.variant } : undefined, prepare, signal, }).then((target) => ({ sessionID: target.session.id, sessionTitle: target.session.title, location: target.location, model: target.model ? { providerID: target.model.providerID, modelID: target.model.id } : undefined, variant: target.model?.variant, agent: target.agent, resume: false, })) const frontend = await frontendTask return frontend.runMiniFrontend({ host: createMiniHost({ terminal, directory, paths: input.paths }), sdk, directory, target: resolveTarget, reconnect: connection.reconnect, createSession: create, agent: input.agent, model, variant: requested?.variant, files: [], initialInput, replay: input.replay ?? true, replayLimit: input.replayLimit, demo: input.demo, tuiConfig: input.tuiConfig, config: input.config, }) }) if (result.exitCode !== 0) process.exit(result.exitCode) } catch (error) { if (error instanceof MiniInputError || (error instanceof Error && error.message === INTERACTIVE_INPUT_ERROR)) fail(error.message) throw error }}
/** @internal Exported for CLI boundary tests. */export function createMiniConnection(input: MiniCommandInput["server"]) { const make = (endpoint: Endpoint) => OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint), }) const reconnect = input.reconnect return { sdk: make(input.endpoint), reconnect: reconnect ? async (signal: AbortSignal) => { const endpoint = await reconnect(signal) return make(endpoint) } : undefined, }}
/** @internal Exported for reconnect lifecycle tests. */export async function resolveMiniTarget<A>(input: { sdk: OpenCodeClient reconnect?: (signal: AbortSignal) => Promise<OpenCodeClient> signal: AbortSignal resolve: (sdk: OpenCodeClient) => Promise<A>}) { let sdk = input.sdk while (true) { try { return { sdk, value: await input.resolve(sdk) } } catch (error) { if (!input.reconnect || !(error instanceof ClientError) || error.reason !== "Transport") throw error while (true) { try { sdk = await input.reconnect(input.signal) break } catch (resolveError) { if (input.signal.aborted) throw resolveError await setTimeout(250, undefined, { signal: input.signal }) } } } }}
export function validateMiniTerminal() { if (!process.stdout.isTTY) fail("opencode mini requires a TTY stdout")}
/** @internal Exported for testing. */export function mergeInput(piped: string | undefined, prompt: string | undefined) { if (!prompt) return piped || undefined if (!piped) return prompt return piped + "\n" + prompt}
function validate(input: MiniCommandInput) { validateMiniTerminal() if (input.replayLimit !== undefined && (!Number.isInteger(input.replayLimit) || input.replayLimit <= 0)) { fail("--replay-limit must be a positive integer") } if (input.fork && !input.continue && !input.session) fail("--fork requires --continue or --session")}
function localDirectory(): string { const root = process.env.PWD ?? process.cwd() try { process.chdir(root) return process.cwd() } catch { throw new MiniInputError(`Failed to change directory to ${root}`) }}
function parseModel(value?: string) { try { return parseSessionTargetModel(value) } catch { throw new MiniInputError("--model must use the format provider/model[#variant]") }}
function prepareTarget(requestedAgent?: string): SessionTargetPreparation { return async (input) => ({ model: input.model, agent: requestedAgent ?? input.agent })}
function fail(message: string): never { process.stderr.write(`\x1b[91m\x1b[1mError: \x1b[0m${message}\n`) process.exit(1)}