2026-04-27 18:33:33 +00:00
import * as Log from "@opencode-ai/core/util/log"
2026-05-04 17:06:29 +00:00
import { ConfigProvider , Context , Effect , Exit , Layer , Scope } from "effect"
2026-05-03 18:23:29 +00:00
import { HttpRouter , HttpServer } from "effect/unstable/http"
2026-05-03 13:06:23 +00:00
import { OpenApi } from "effect/unstable/httpapi"
2026-05-03 18:23:29 +00:00
import * as HttpApiServer from "#httpapi-server"
2026-04-17 06:06:20 +00:00
import { MDNS } from "./mdns"
2026-03-25 14:47:40 +00:00
import { initProjectors } from "./projectors"
2026-04-23 21:32:02 +00:00
import { ExperimentalHttpApiServer } from "./routes/instance/httpapi/server"
2026-05-03 18:23:29 +00:00
import { disposeMiddleware } from "./routes/instance/httpapi/lifecycle"
import { WebSocketTracker } from "./routes/instance/httpapi/websocket-tracker"
2026-05-03 13:06:23 +00:00
import { PublicApi } from "./routes/instance/httpapi/public"
2026-05-01 01:26:32 +00:00
import type { CorsOptions } from "./cors"
2025-06-09 18:01:11 +00:00
2025-11-20 14:42:24 +00:00
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis . AI_SDK_LOG_WARNINGS = false
2026-03-25 14:47:40 +00:00
initProjectors ( )
2026-04-16 23:51:01 +00:00
const log = Log . create ( { service : "server" } )
2026-04-11 20:55:17 +00:00
2026-04-16 23:51:01 +00:00
export type Listener = {
hostname : string
port : number
url : URL
stop : ( close? : boolean ) = > Promise < void >
}
2025-12-30 03:05:08 +00:00
2026-04-28 15:02:35 +00:00
type ServerApp = {
fetch ( request : Request ) : Response | Promise < Response >
request ( input : string | URL | Request , init? : RequestInit ) : Response | Promise < Response >
}
2026-05-01 01:26:32 +00:00
type ListenOptions = CorsOptions & {
port : number
hostname : string
mdns? : boolean
mdnsDomain? : string
}
2026-05-09 13:10:42 +00:00
const defaultHttpApi = ( ( ) = > {
const handler = ExperimentalHttpApiServer . webHandler ( ) . handler
2026-04-28 15:02:35 +00:00
const app : ServerApp = {
fetch : ( request : Request ) = > handler ( request , ExperimentalHttpApiServer . context ) ,
request ( input , init ) {
return app . fetch ( input instanceof Request ? input : new Request ( new URL ( input , "http://localhost" ) , init ) )
} ,
}
2026-05-09 13:10:42 +00:00
return { app }
} ) ( )
2026-03-09 21:13:42 +00:00
2026-05-09 13:10:42 +00:00
export const Default = ( ) = > defaultHttpApi
2026-04-23 21:32:02 +00:00
2026-04-16 23:51:01 +00:00
export async function openapi() {
2026-05-03 13:06:23 +00:00
return OpenApi . fromApi ( PublicApi )
}
2026-04-16 23:51:01 +00:00
export let url : URL
2025-12-26 20:24:44 +00:00
2026-05-01 01:26:32 +00:00
export async function listen ( opts : ListenOptions ) : Promise < Listener > {
2026-05-09 13:10:42 +00:00
log . info ( "server backend" , { "opencode.server.runtime" : HttpApiServer . name } )
2026-05-03 18:23:29 +00:00
const buildLayer = ( port : number ) = >
HttpRouter . serve ( ExperimentalHttpApiServer . createRoutes ( opts ) , {
middleware : disposeMiddleware ,
disableLogger : true ,
disableListenLog : true ,
} ) . pipe (
Layer . provideMerge ( WebSocketTracker . layer ) ,
Layer . provideMerge ( HttpApiServer . layer ( { port , hostname : opts.hostname } ) ) ,
2026-05-04 17:06:29 +00:00
// Install a fresh `ConfigProvider` per listener so `Config.string(...)`
// reads reflect the current `process.env`. Effect's default
// `ConfigProvider` snapshots `process.env` on first read and caches the
// result on a module-singleton Reference; without overriding it here,
// every later `Server.listen()` keeps observing that initial snapshot.
Layer . provide ( ConfigProvider . layer ( ConfigProvider . fromEnv ( ) ) ) ,
2026-05-03 18:23:29 +00:00
)
const start = async ( port : number ) = > {
const scope = Scope . makeUnsafe ( )
try {
const layer = buildLayer ( port ) as Layer . Layer <
HttpServer . HttpServer | WebSocketTracker . Service | HttpApiServer . Service ,
unknown ,
never
>
const ctx = await Effect . runPromise ( Layer . buildWithMemoMap ( layer , Layer . makeMemoMapUnsafe ( ) , scope ) )
return { scope , ctx }
} catch ( err ) {
await Effect . runPromise ( Scope . close ( scope , Exit . void ) ) . catch ( ( ) = > undefined )
throw err
}
}
// Match the legacy adapter port-resolution behavior: explicit `0` prefers
// 4096 first, then any free port.
let resolved : Awaited < ReturnType < typeof start > > | undefined
if ( opts . port === 0 ) {
resolved = await start ( 4096 ) . catch ( ( ) = > undefined )
if ( ! resolved ) resolved = await start ( 0 )
} else {
resolved = await start ( opts . port )
}
if ( ! resolved ) throw new Error ( ` Failed to start server on port ${ opts . port } ` )
const server = Context . get ( resolved . ctx , HttpServer . HttpServer )
if ( server . address . _tag !== "TcpAddress" ) {
await Effect . runPromise ( Scope . close ( resolved . scope , Exit . void ) )
throw new Error ( ` Unexpected HttpServer address tag: ${ server . address . _tag } ` )
}
const port = server . address . port
const innerUrl = new URL ( "http://localhost" )
innerUrl . hostname = opts . hostname
innerUrl . port = String ( port )
2026-05-09 13:10:42 +00:00
url = innerUrl
const mdns =
opts . mdns && port && opts . hostname !== "127.0.0.1" && opts . hostname !== "localhost" && opts . hostname !== "::1"
if ( mdns ) {
MDNS . publish ( port , opts . mdnsDomain )
} else if ( opts . mdns ) {
log . warn ( "mDNS enabled but hostname is loopback; skipping mDNS publish" )
}
2026-05-03 18:23:29 +00:00
let forceStopPromise : Promise < void > | undefined
let stopPromise : Promise < void > | undefined
2026-05-09 13:10:42 +00:00
let mdnsUnpublished = false
const unpublish = ( ) = > {
if ( ! mdns || mdnsUnpublished ) return
mdnsUnpublished = true
MDNS . unpublish ( )
}
2026-05-03 18:23:29 +00:00
const forceStop = ( ) = > {
forceStopPromise ? ? = Effect . runPromiseExit (
Effect . gen ( function * ( ) {
yield * Context . get ( resolved ! . ctx , HttpApiServer . Service ) . closeAll
yield * Context . get ( resolved ! . ctx , WebSocketTracker . Service ) . closeAll
} ) ,
) . then ( ( ) = > undefined )
return forceStopPromise
}
return {
hostname : opts.hostname ,
port ,
url : innerUrl ,
stop : ( close? : boolean ) = > {
2026-05-09 13:10:42 +00:00
unpublish ( )
2026-05-03 18:23:29 +00:00
const requested = close ? forceStop ( ) : Promise . resolve ( )
2026-05-03 18:24:37 +00:00
stopPromise ? ? = requested
. then ( ( ) = > Effect . runPromiseExit ( Scope . close ( resolved ! . scope , Exit . void ) ) )
. then ( ( ) = > undefined )
2026-05-03 18:23:29 +00:00
return requested . then ( ( ) = > stopPromise ! )
2026-04-16 23:51:01 +00:00
} ,
2025-05-18 01:31:42 +00:00
}
}
2026-04-16 23:51:01 +00:00
export * as Server from "./server"