refactor(cli): scope server discovery to CLI

This commit is contained in:
Dax Raad 2026-05-15 20:28:53 -04:00
parent 13822b9424
commit 26f64f29fb
4 changed files with 32 additions and 34 deletions

View file

@ -1,6 +1,6 @@
import { Effect } from "effect"
import { Server } from "../../server/server"
import { ServerDiscovery } from "@/server/discovery"
import { ServerDiscovery } from "@/cli/server-discovery"
import { effectCmd } from "../effect-cmd"
import { withNetworkOptions, resolveNetworkOptions } from "../network"
import { Flag } from "@opencode-ai/core/flag/flag"
@ -17,26 +17,26 @@ export const ServeCommand = effectCmd({
// Server loads instances per-request via x-opencode-directory header — no
// need for an ambient project InstanceContext at startup.
instance: false,
handler: Effect.fn("Cli.serve")(function* (args) {
if (!Flag.OPENCODE_SERVER_PASSWORD) {
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
}
const opts = yield* resolveNetworkOptions(args)
const server = yield* Effect.promise(() => Server.listen(opts))
if (args.discoverable) {
yield* ServerDiscovery.Service.use((discovery) => discovery.write(server.url))
process.on("exit", ServerDiscovery.removeSync)
}
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
handler: (args) =>
Effect.gen(function* () {
if (!Flag.OPENCODE_SERVER_PASSWORD) {
console.log("Warning: OPENCODE_SERVER_PASSWORD is not set; server is unsecured.")
}
const opts = yield* resolveNetworkOptions(args)
const server = yield* Effect.promise(() => Server.listen(opts))
const discovery = args.discoverable ? yield* ServerDiscovery.Service : undefined
if (discovery) {
yield* discovery.write(server.url)
process.on("exit", ServerDiscovery.removeSync)
}
console.log(`opencode server listening on http://${server.hostname}:${server.port}`)
yield* Effect.never.pipe(
Effect.ensuring(
args.discoverable
? ServerDiscovery.Service.use((discovery) => discovery.remove()).pipe(
Effect.ensuring(Effect.sync(() => process.off("exit", ServerDiscovery.removeSync))),
)
: Effect.void,
),
)
}),
yield* Effect.never.pipe(
Effect.ensuring(
discovery
? discovery.remove().pipe(Effect.ensuring(Effect.sync(() => process.off("exit", ServerDiscovery.removeSync))))
: Effect.void,
),
)
}).pipe(Effect.provide(ServerDiscovery.defaultLayer)),
})

View file

@ -10,7 +10,7 @@ import { withTimeout } from "@/util/timeout"
import { withNetworkOptions, resolveNetworkOptionsNoConfig } from "@/cli/network"
import { Filesystem } from "@/util/filesystem"
import { ServerAuth } from "@/server/auth"
import { ServerDiscovery } from "@/server/discovery"
import { ServerDiscovery } from "@/cli/server-discovery"
import type { GlobalEvent } from "@opencode-ai/sdk/v2"
import type { EventSource } from "./context/sdk"
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"

View file

@ -1,12 +1,12 @@
export * as ServerDiscovery from "./discovery"
export * as ServerDiscovery from "./server-discovery"
import { ServerAuth } from "@/server/auth"
import { makeRuntime } from "@/effect/run-service"
import { ServerAuth } from "@/server/auth"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Global } from "@opencode-ai/core/global"
import { Context, Effect, Layer, Option, Schema } from "effect"
import path from "path"
import { readFileSync, unlinkSync } from "fs"
import path from "path"
export const file = path.join(Global.Path.state, "server.json")
@ -23,36 +23,36 @@ export interface Interface {
readonly find: () => Effect.Effect<string | undefined>
}
export class Service extends Context.Service<Service, Interface>()("@opencode/ServerDiscovery") {}
export class Service extends Context.Service<Service, Interface>()("@opencode/CliServerDiscovery") {}
export const layer = Layer.effect(
Service,
Effect.gen(function* () {
const fs = yield* AppFileSystem.Service
const read = Effect.fn("ServerDiscovery.read")(function* () {
const read = Effect.fn("CliServerDiscovery.read")(function* () {
const entry = yield* fs.readJson(file).pipe(Effect.catch(() => Effect.succeed(undefined)))
return Option.getOrUndefined(decodeEntry(entry))
})
const remove = Effect.fn("ServerDiscovery.remove")(function* () {
const remove = Effect.fn("CliServerDiscovery.remove")(function* () {
const entry = yield* read()
if (entry?.pid !== process.pid) return
yield* fs.remove(file).pipe(Effect.ignore)
})
const removeStale = Effect.fn("ServerDiscovery.removeStale")(function* (entry: Entry) {
const removeStale = Effect.fn("CliServerDiscovery.removeStale")(function* (entry: Entry) {
const current = yield* read()
if (current?.pid !== entry.pid || current.url !== entry.url) return
yield* fs.remove(file).pipe(Effect.ignore)
})
return Service.of({
write: Effect.fn("ServerDiscovery.write")(function* (url) {
write: Effect.fn("CliServerDiscovery.write")(function* (url) {
yield* fs.writeJson(file, { url: localURL(url).toString(), pid: process.pid }, 0o600).pipe(Effect.orDie)
}),
remove,
find: Effect.fn("ServerDiscovery.find")(function* () {
find: Effect.fn("CliServerDiscovery.find")(function* () {
const entry = yield* read()
if (!entry) return undefined
const url = yield* healthy(entry.url)

View file

@ -58,7 +58,6 @@ import { DataMigration } from "@/data-migration"
import { BackgroundJob } from "@/background/job"
import { EventV2Bridge } from "@/event-v2-bridge"
import { RuntimeFlags } from "@/effect/runtime-flags"
import { ServerDiscovery } from "@/server/discovery"
export const AppLayer = Layer.mergeAll(
Npm.defaultLayer,
@ -115,7 +114,6 @@ export const AppLayer = Layer.mergeAll(
SyncEvent.defaultLayer,
EventV2Bridge.defaultLayer,
DataMigration.defaultLayer,
ServerDiscovery.defaultLayer,
).pipe(Layer.provideMerge(InstanceLayer.layer), Layer.provideMerge(Observability.layer))
const rt = ManagedRuntime.make(AppLayer, { memoMap })