refactor(cli): unify server endpoint handling

This commit is contained in:
Dax Raad 2026-07-08 21:51:37 -04:00
commit be7a684791
18 changed files with 212 additions and 223 deletions

View file

@ -1,9 +1,11 @@
import { NodeFileSystem } from "@effect/platform-node"
import { Service } from "@opencode-ai/client/effect"
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
import { Global } from "@opencode-ai/core/global"
import { Effect } from "effect"
import path from "node:path"
import { Daemon } from "../daemon"
import { ServiceConfig } from "../services/service-config"
import { waitForCatalogReady } from "./catalog.shared"
import { INTERACTIVE_INPUT_ERROR, resolveInteractiveStdin } from "./runtime.stdin"
import type { RunInput, RunTuiConfig } from "./types"
@ -27,28 +29,26 @@ export type MiniCommandInput = {
}
type Session = Awaited<ReturnType<OpenCodeClient["session"]["get"]>>
type Transport = { readonly url: string; readonly headers?: HeadersInit }
export async function runMini(input: MiniCommandInput) {
validate(input)
const initialInput = mergeInput(process.stdin.isTTY ? undefined : await Bun.stdin.text(), input.prompt)
const runtimeTask = import("./runtime")
const directory = input.attach ? input.directory : localDirectory(input.directory)
const transportTask = startTransport(input)
void transportTask.catch(() => {})
const endpointTask = startEndpoint(input)
void endpointTask.catch(() => {})
try {
if (input.attach) await transportTask
if (input.attach) await endpointTask
const sdk = OpenCode.make({
baseUrl: "http://opencode.pending",
fetch: deferredFetch(transportTask),
fetch: deferredFetch(endpointTask),
})
const attachedSession =
input.attach && input.session && !input.directory
? await sdk.session.get({ sessionID: input.session }).catch(() => fail("Session not found"))
: undefined
const resolvedDirectory =
directory ?? attachedSession?.location.directory ?? (await remoteDirectory(await transportTask, sdk))
directory ?? attachedSession?.location.directory ?? (await remoteDirectory(await endpointTask, sdk))
const model = parseModel(input.model)
let agentTask: Promise<string | undefined> | undefined
const resolveAgent = () => {
@ -120,11 +120,10 @@ function localDirectory(directory?: string): string {
}
}
function startTransport(input: MiniCommandInput): Promise<Transport> {
function startEndpoint(input: MiniCommandInput): Promise<Service.Endpoint> {
if (input.attach) {
return Effect.runPromise(
Daemon.transport({
mode: "attach",
Daemon.connect({
url: input.attach,
password: input.password ?? process.env.OPENCODE_SERVER_PASSWORD,
username: input.username ?? process.env.OPENCODE_SERVER_USERNAME,
@ -132,31 +131,36 @@ function startTransport(input: MiniCommandInput): Promise<Transport> {
)
}
return Effect.runPromise(
Daemon.transport({ mode: "shared", command: input.serverCommand }).pipe(
Effect.gen(function* () {
const options = yield* ServiceConfig.options()
return yield* Service.start(
input.serverCommand === undefined ? options : { ...options, command: input.serverCommand },
)
}).pipe(
Effect.provide(NodeFileSystem.layer),
Effect.provide(Global.layerWith({})),
),
)
}
function deferredFetch(transportTask: Promise<{ url: string; headers?: HeadersInit }>): typeof globalThis.fetch {
function deferredFetch(endpointTask: Promise<Service.Endpoint>): typeof globalThis.fetch {
const fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
const transport = await transportTask
const endpoint = await endpointTask
const request = new Request(input, init)
const source = new URL(request.url)
const headers = new Headers(request.headers)
for (const [key, value] of new Headers(transport.headers)) headers.set(key, value)
return globalThis.fetch(new Request(new URL(source.pathname + source.search, transport.url), request), { headers })
for (const [key, value] of new Headers(Service.headers(endpoint))) headers.set(key, value)
return globalThis.fetch(new Request(new URL(source.pathname + source.search, endpoint.url), request), { headers })
}
return fetch as typeof globalThis.fetch
}
async function remoteDirectory(
transport: { url: string; headers?: HeadersInit },
endpoint: Service.Endpoint,
sdk: OpenCodeClient,
): Promise<string> {
const location = await sdk.location.get()
if (!location.directory) throw new Error(`Failed to resolve remote directory from ${transport.url}`)
if (!location.directory) throw new Error(`Failed to resolve remote directory from ${endpoint.url}`)
return location.directory
}

View file

@ -1,4 +1,5 @@
import { NodeFileSystem } from "@effect/platform-node"
import { Service } from "@opencode-ai/client/effect"
import { OpenCode, type OpenCodeClient } from "@opencode-ai/client/promise"
import { Global } from "@opencode-ai/core/global"
import { FSUtil } from "@opencode-ai/core/fs-util"
@ -7,6 +8,7 @@ import { Effect } from "effect"
import { open } from "node:fs/promises"
import path from "node:path"
import { Daemon } from "../daemon"
import { ServiceConfig } from "../services/service-config"
import { Standalone } from "../services/standalone"
import { loadRunAgents, waitForCatalogReady } from "./catalog.shared"
import { runNonInteractivePrompt } from "./noninteractive"
@ -39,8 +41,6 @@ type FilePart = {
mime: string
}
type Transport = { readonly url: string; readonly headers?: HeadersInit }
type Prepared = {
directory?: string
message: string
@ -67,17 +67,17 @@ async function run(input: RunCommandInput) {
return Effect.runPromise(
Effect.scoped(
Effect.gen(function* () {
const transport = yield* Standalone.transport({ command: input.standaloneCommand })
yield* Effect.promise(() => execute(input, prepared, transport))
const endpoint = yield* Standalone.start({ command: input.standaloneCommand })
yield* Effect.promise(() => execute(input, prepared, endpoint))
}),
),
)
const transport = await startTransport(input)
return execute(input, prepared, transport)
const endpoint = await startEndpoint(input)
return execute(input, prepared, endpoint)
}
async function execute(input: RunCommandInput, prepared: Prepared, transport: Transport) {
const client = OpenCode.make({ baseUrl: transport.url, headers: transport.headers })
async function execute(input: RunCommandInput, prepared: Prepared, endpoint: Service.Endpoint) {
const client = OpenCode.make({ baseUrl: endpoint.url, headers: Service.headers(endpoint) })
const requestedDirectory = prepared.directory ?? (await client.location.get()).directory
if (!requestedDirectory) fail("Failed to resolve server directory")
const session = await selectSession(client, requestedDirectory, input)
@ -163,11 +163,10 @@ function localDirectory(directory: string | undefined, root: string) {
}
}
function startTransport(input: RunCommandInput) {
function startEndpoint(input: RunCommandInput) {
if (input.server) {
return Effect.runPromise(
Daemon.transport({
mode: "attach",
Daemon.connect({
url: input.server,
password: input.password,
username: input.username,
@ -175,7 +174,9 @@ function startTransport(input: RunCommandInput) {
)
}
return Effect.runPromise(
Daemon.transport({ mode: "shared" }).pipe(
Effect.gen(function* () {
return yield* Service.start(yield* ServiceConfig.options())
}).pipe(
Effect.provide(NodeFileSystem.layer),
Effect.provide(Global.layerWith({})),
),