1- import { NodeFileSystem } from "@effect/platform-node"
21import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
32import { Global } from "@opencode-ai/core/global"
43import { run } from "@opencode-ai/tui"
54import { loadBuiltinPlugins } from "@opencode-ai/tui/builtins"
65import { TuiConfig } from "@opencode-ai/tui/config"
76import { Commands } from "../commands"
87import { 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"
1410import { Updater } from "../../services/updater"
1511
1612export 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 ) => {
0 commit comments