refactor(cli): centralize server resolution · argszero/opencode@cccd013 · GitHub
Skip to content

Commit cccd013

Browse files
committed
refactor(cli): centralize server resolution
1 parent be7a684 commit cccd013

13 files changed

Lines changed: 152 additions & 292 deletions

File tree

packages/cli/src/commands/commands.ts

Lines changed: 14 additions & 17 deletions

packages/cli/src/commands/handlers/default.ts

Lines changed: 6 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
1-
import { NodeFileSystem } from "@effect/platform-node"
21
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
32
import { Global } from "@opencode-ai/core/global"
43
import { run } from "@opencode-ai/tui"
54
import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
65
import { TuiConfig } from "@opencode-ai/tui/config"
76
import { Commands } from "../commands"
87
import { Runtime } from "../../framework/runtime"
9-
import { Effect, Option, Redacted } from "effect"
10-
import { Service } from "@opencode-ai/client/effect"
11-
import { Env } from "../../env"
12-
import { ServiceConfig } from "../../services/service-config"
13-
import { Standalone } from "../../services/standalone"
8+
import { Effect, Option } from "effect"
9+
import { Server } from "../../services/server"
1410
import { Updater } from "../../services/updater"
1511

1612
export default Runtime.handler(Commands, (input) =>
@@ -19,82 +15,15 @@ export default Runtime.handler(Commands, (input) =>
1915
if (requestedDirectory !== undefined) process.chdir(requestedDirectory)
2016
const updater = yield* Updater.Service
2117
yield* updater.check().pipe(Effect.forkScoped)
22-
const server = Option.getOrUndefined(input.server)
23-
if (server !== undefined && input.standalone)
24-
return yield* Effect.fail(new Error("--server and --standalone cannot be combined"))
25-
const endpoint = yield* Effect.gen(function* () {
26-
if (server !== undefined) {
27-
const password = yield* Env.password
28-
const explicit = {
29-
url: server,
30-
auth: password
31-
? { type: "basic" as const, username: "opencode", password: Redacted.value(password) }
32-
: undefined,
33-
} satisfies Service.Endpoint
34-
// Fail loudly before entering the TUI: an explicit server that is
35-
// unreachable or rejects auth should not present as reconnect churn.
36-
const response = yield* Effect.tryPromise(() =>
37-
fetch(new URL("/api/health", server), {
38-
headers: Service.headers(explicit),
39-
signal: AbortSignal.timeout(5_000),
40-
}),
41-
).pipe(Effect.mapError((cause) => new Error(`Could not reach server at ${server}`, { cause })))
42-
if (response.status === 401)
43-
return yield* Effect.fail(
44-
new Error(
45-
password
46-
? `Server at ${server} rejected the password`
47-
: `Server at ${server} requires a password; set OPENCODE_PASSWORD`,
48-
),
49-
)
50-
if (!response.ok)
51-
return yield* Effect.fail(new Error(`Server at ${server} responded with status ${response.status}`))
52-
return explicit
53-
}
54-
if (input.standalone) return yield* Standalone.start()
55-
const options = yield* ServiceConfig.options()
56-
const found = yield* Service.discover(options)
57-
return found ?? (yield* Service.start(options))
18+
const server = yield* Server.resolve({
19+
server: Option.getOrUndefined(input.server),
20+
standalone: input.standalone,
5821
})
59-
// The TUI re-runs discover whenever its event stream drops. For an explicit
60-
// --server or a standalone child the endpoint is fixed, so reconnects
61-
// retry the same address; for the managed service discovery re-reads the
62-
// registration and may start a replacement.
63-
const serviceOptions = server === undefined && !input.standalone ? yield* ServiceConfig.options() : undefined
64-
// Only startup enforces the CLI version. A reconnect must accept a server
65-
// replaced by another client or the two clients will restart it forever.
66-
const reconnectOptions = serviceOptions ? { ...serviceOptions, version: undefined } : undefined
67-
const discover = reconnectOptions
68-
? () =>
69-
Effect.runPromise(
70-
Effect.gen(function* () {
71-
const found = yield* Service.discover(reconnectOptions)
72-
return found ?? (yield* Service.start(reconnectOptions))
73-
}).pipe(Effect.provide(NodeFileSystem.layer)),
74-
)
75-
: undefined
76-
// Restart the managed service in place; start() resolves once the
77-
// replacement is healthy and the reconnect loop reattaches on its own.
78-
// Only meaningful in service mode: --server is not ours to restart and a
79-
// standalone child cannot be respawned.
80-
const reload = serviceOptions
81-
? () =>
82-
Effect.runPromise(
83-
Effect.gen(function* () {
84-
yield* Service.stop(serviceOptions)
85-
yield* Service.start(serviceOptions)
86-
}).pipe(Effect.provide(NodeFileSystem.layer)),
87-
)
88-
: undefined
8922
const config = TuiConfig.resolve({}, { terminalSuspend: false })
9023
let disposeSlots: (() => void) | undefined
9124
const runFork = Effect.runForkWith(yield* Effect.context())
9225
yield* run({
93-
server: {
94-
endpoint,
95-
discover,
96-
reload,
97-
},
26+
server,
9827
args: { continue: input.continue, sessionID: Option.getOrUndefined(input.session) },
9928
config,
10029
log: (level, message, tags) => {

packages/cli/src/commands/handlers/mini.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,21 @@
1-
import { Effect, Option, Redacted } from "effect"
1+
import { Effect, Option } from "effect"
22
import path from "node:path"
33
import { Commands } from "../commands"
4-
import { Env } from "../../env"
54
import { Runtime } from "../../framework/runtime"
5+
import { Server } from "../../services/server"
66

77
export default Runtime.handler(Commands.commands.mini, (input) =>
88
Effect.gen(function* () {
9-
const { runMini } = yield* Effect.promise(() => import("../../mini"))
9+
const { runMini, validateMiniTerminal } = yield* Effect.promise(() => import("../../mini"))
10+
yield* Effect.promise(async () => validateMiniTerminal())
1011
const project = Option.getOrUndefined(input.project)
11-
const server = Option.getOrUndefined(input.server)
12-
const password = yield* Env.password
12+
const serverURL = Option.getOrUndefined(input.server)
13+
const server = yield* Server.resolve({ server: serverURL, standalone: input.standalone })
1314
yield* Effect.promise(() =>
1415
runMini({
15-
attach: server,
16-
password: password ? Redacted.value(password) : undefined,
16+
server,
1717
directory:
18-
server !== undefined
19-
? project
20-
: project === undefined
21-
? process.cwd()
22-
: path.resolve(process.env.PWD ?? process.cwd(), project),
18+
project === undefined ? process.cwd() : path.resolve(process.env.PWD ?? process.cwd(), project),
2319
continue: input.continue,
2420
session: Option.getOrUndefined(input.session),
2521
fork: input.fork,

packages/cli/src/commands/handlers/run.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
1-
import { Effect, Option, Redacted } from "effect"
1+
import { Effect, Option } from "effect"
22
import { Commands } from "../commands"
3-
import { Env } from "../../env"
43
import { Runtime } from "../../framework/runtime"
4+
import { Server } from "../../services/server"
55

66
export default Runtime.handler(Commands.commands.run, (input) =>
77
Effect.gen(function* () {
88
const { runNonInteractive } = yield* Effect.promise(() => import("../../mini"))
9-
const password = yield* Env.password
109
const separator = process.argv.indexOf("--", 2)
10+
const server = yield* Server.resolve({
11+
server: Option.getOrUndefined(input.server),
12+
standalone: input.standalone,
13+
})
1114
yield* Effect.promise(() =>
1215
runNonInteractive({
16+
server,
1317
message: [...input.message, ...(separator === -1 ? [] : process.argv.slice(separator + 1))],
1418
continue: input.continue,
1519
session: Option.getOrUndefined(input.session),
@@ -19,9 +23,6 @@ export default Runtime.handler(Commands.commands.run, (input) =>
1923
format: input.format,
2024
file: [...input.file],
2125
title: Option.getOrUndefined(input.title),
22-
server: Option.getOrUndefined(input.server),
23-
password: password ? Redacted.value(password) : undefined,
24-
directory: Option.getOrUndefined(input.dir),
2526
variant: Option.getOrUndefined(input.variant),
2627
thinking: input.thinking,
2728
dangerouslySkipPermissions: input.auto || input.yolo || input.dangerouslySkipPermissions,

packages/cli/src/daemon.ts

Lines changed: 0 additions & 46 deletions
This file was deleted.

packages/cli/src/mini/index.ts

Lines changed: 1 addition & 1 deletion

0 commit comments

Comments
 (0)