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

@ -11,9 +11,13 @@ import { join } from "node:path"
// is all a client needs to connect. The daemon's own configuration (port,
// persisted password) is CLI-owned and never read here.
export type Transport = {
export type Endpoint = {
readonly url: string
readonly headers?: RequestInit["headers"]
readonly auth?: {
readonly type: "basic"
readonly username: string
readonly password: string
}
}
export type Options = {
@ -31,7 +35,7 @@ export type Options = {
// Read-only lookup: registration file plus health check and version gate.
// Never spawns; escalation to start() is the caller's policy.
export const discover = Effect.fn("service.discover")(function* (options: Options = {}) {
return (yield* discoverLocal(options))?.transport
return (yield* discoverLocal(options))?.endpoint
})
const discoverLocal = Effect.fnUntraced(function* (options: Options) {
@ -72,7 +76,7 @@ export const start = Effect.fn("service.start")(function* (options: Options = {}
child.kill("SIGTERM")
}),
),
Effect.map((found) => found.transport),
Effect.map((found) => found.endpoint),
Effect.tapError(() => Effect.try({ try: () => child.kill("SIGTERM"), catch: () => undefined }).pipe(Effect.ignore)),
Effect.mapError(() => new Error("Failed to start server")),
)
@ -90,8 +94,9 @@ function fallback() {
return join(state, "opencode", "service.json")
}
function auth(password: string): RequestInit["headers"] {
return { authorization: "Basic " + btoa("opencode:" + password) }
export function headers(endpoint: Endpoint): RequestInit["headers"] {
if (endpoint.auth === undefined) return undefined
return { authorization: "Basic " + btoa(endpoint.auth.username + ":" + endpoint.auth.password) }
}
export const Info = Schema.Struct({
@ -120,14 +125,20 @@ const read = Effect.fnUntraced(function* (file?: string) {
type LocalService = {
readonly info: Info
readonly transport: Transport
readonly endpoint: Endpoint
}
const probe = Effect.fnUntraced(function* (info: Info, version?: string, allowLegacy = false) {
const headers = info.password === undefined ? undefined : auth(info.password)
const endpoint = {
url: info.url,
auth:
info.password === undefined
? undefined
: { type: "basic" as const, username: "opencode", password: info.password },
} satisfies Endpoint
const response = yield* Effect.tryPromise(() =>
fetch(new URL("/api/health", info.url), {
headers,
headers: headers(endpoint),
signal: AbortSignal.timeout(2_000),
}),
).pipe(Effect.option, Effect.map(Option.getOrUndefined))
@ -138,7 +149,7 @@ const probe = Effect.fnUntraced(function* (info: Info, version?: string, allowLe
if (health.value.pid !== info.pid) return undefined
if (info.version !== undefined && health.value.version !== info.version) return undefined
if (version !== undefined && health.value.version !== version) return undefined
return { info, transport: { url: info.url, headers } } satisfies LocalService
return { info, endpoint } satisfies LocalService
}
if (
!allowLegacy ||
@ -146,7 +157,7 @@ const probe = Effect.fnUntraced(function* (info: Info, version?: string, allowLe
(typeof body === "object" && body !== null && ("version" in body || "pid" in body))
)
return undefined
return { info, transport: { url: info.url, headers } } satisfies LocalService
return { info, endpoint } satisfies LocalService
})
// Health-checked lookup without the version gate: lifecycle operations must be