2026-04-11 20:55:17 +00:00
import { generateSpecs } from "hono-openapi"
2025-05-31 18:41:00 +00:00
import { Hono } from "hono"
2026-04-11 20:55:17 +00:00
import { adapter } from "#hono"
2026-03-09 21:13:42 +00:00
import { lazy } from "@/util/lazy"
2026-04-27 18:33:33 +00:00
import * as Log from "@opencode-ai/core/util/log"
2026-04-25 17:29:52 +00:00
import { Flag } from "@opencode-ai/core/flag/flag"
2026-04-17 06:06:20 +00:00
import { WorkspaceID } from "@/control-plane/schema"
import { MDNS } from "./mdns"
2026-04-11 20:55:17 +00:00
import { AuthMiddleware , CompressionMiddleware , CorsMiddleware , ErrorMiddleware , LoggerMiddleware } from "./middleware"
2026-04-16 01:04:37 +00:00
import { FenceMiddleware } from "./fence"
2026-03-25 14:47:40 +00:00
import { initProjectors } from "./projectors"
2026-04-17 06:06:20 +00:00
import { InstanceRoutes } from "./routes/instance"
import { ControlPlaneRoutes } from "./routes/control"
import { UIRoutes } from "./routes/ui"
import { GlobalRoutes } from "./routes/global"
import { WorkspaceRouterMiddleware } from "./workspace"
2026-04-17 20:39:34 +00:00
import { InstanceMiddleware } from "./routes/instance/middleware"
import { WorkspaceRoutes } from "./routes/control/workspace"
2026-04-23 21:32:02 +00:00
import { ExperimentalHttpApiServer } from "./routes/instance/httpapi/server"
2026-04-29 13:34:50 +00:00
import * as ServerBackend from "./backend"
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-04-29 13:36:05 +00:00
const DefaultHono = lazy ( ( ) = >
withBackend ( { backend : "hono" , reason : "stable" } , createHono ( { } , { backend : "hono" , reason : "stable" } ) ) ,
)
2026-04-29 13:34:50 +00:00
const DefaultHttpApi = lazy ( ( ) = > createDefaultHttpApi ( ) )
function select() {
return ServerBackend . select ( )
}
export const backend = select
export const Default = ( ) = > {
const selected = select ( )
return selected . backend === "effect-httpapi" ? DefaultHttpApi ( ) : DefaultHono ( )
}
2026-04-16 01:04:37 +00:00
2026-04-16 23:51:01 +00:00
function create ( opts : { cors? : string [ ] } ) {
2026-04-29 13:34:50 +00:00
const selected = select ( )
return selected . backend === "effect-httpapi"
? withBackend ( selected , createHttpApi ( ) )
: withBackend ( selected , createHono ( opts , selected ) )
}
export function Legacy ( opts : { cors? : string [ ] } = { } ) {
return withBackend ( { backend : "hono" , reason : "explicit" } , createHono ( opts , { backend : "hono" , reason : "explicit" } ) )
}
function createDefaultHttpApi() {
return withBackend ( select ( ) , createHttpApi ( ) )
}
function withBackend < T extends { app : ServerApp ; runtime : unknown } > ( selection : ServerBackend.Selection , built : T ) {
log . info ( "server backend selected" , ServerBackend . attributes ( selection ) )
return built
2026-04-28 15:02:35 +00:00
}
function createHttpApi() {
const handler = ExperimentalHttpApiServer . webHandler ( ) . handler
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 ) )
} ,
}
return {
app ,
runtime : adapter.createFetch ( app ) ,
}
}
2026-04-29 13:36:05 +00:00
function createHono (
opts : { cors? : string [ ] } ,
selection : ServerBackend.Selection = ServerBackend . force ( select ( ) , "hono" ) ,
) {
2026-04-29 13:34:50 +00:00
const backendAttributes = ServerBackend . attributes ( selection )
2026-04-16 23:51:01 +00:00
const app = new Hono ( )
2026-04-17 06:06:20 +00:00
. onError ( ErrorMiddleware )
. use ( AuthMiddleware )
2026-04-29 13:34:50 +00:00
. use ( LoggerMiddleware ( backendAttributes ) )
2026-04-17 06:06:20 +00:00
. use ( CompressionMiddleware )
. use ( CorsMiddleware ( opts ) )
. route ( "/global" , GlobalRoutes ( ) )
2026-04-16 23:51:01 +00:00
const runtime = adapter . create ( app )
2026-04-16 01:04:37 +00:00
2026-04-16 23:51:01 +00:00
if ( Flag . OPENCODE_WORKSPACE_ID ) {
2026-04-06 17:24:55 +00:00
return {
2026-04-11 20:55:17 +00:00
app : app
2026-04-17 20:39:34 +00:00
. use ( InstanceMiddleware ( Flag . OPENCODE_WORKSPACE_ID ? WorkspaceID . make ( Flag . OPENCODE_WORKSPACE_ID ) : undefined ) )
2026-04-16 23:51:01 +00:00
. use ( FenceMiddleware )
2026-04-17 20:39:34 +00:00
. route ( "/" , InstanceRoutes ( runtime . upgradeWebSocket ) ) ,
2026-04-11 20:55:17 +00:00
runtime ,
2026-04-06 17:24:55 +00:00
}
2026-03-27 15:51:21 +00:00
}
2026-03-09 21:13:42 +00:00
2026-04-23 21:32:02 +00:00
const workspaceApp = new Hono ( )
const workspaceLegacyApp = new Hono ( )
. use ( InstanceMiddleware ( ) )
. route ( "/experimental/workspace" , WorkspaceRoutes ( ) )
. use ( WorkspaceRouterMiddleware ( runtime . upgradeWebSocket ) )
workspaceApp . route ( "/" , workspaceLegacyApp )
2026-04-16 23:51:01 +00:00
return {
app : app
. route ( "/" , ControlPlaneRoutes ( ) )
2026-04-23 21:32:02 +00:00
. route ( "/" , workspaceApp )
2026-04-16 23:51:01 +00:00
. route ( "/" , InstanceRoutes ( runtime . upgradeWebSocket ) )
. route ( "/" , UIRoutes ( ) ) ,
runtime ,
2025-05-18 18:28:08 +00:00
}
2026-04-16 23:51:01 +00:00
}
2025-05-18 18:13:04 +00:00
2026-04-16 23:51:01 +00:00
export async function openapi() {
// Build a fresh app with all routes registered directly so
// hono-openapi can see describeRoute metadata (`.route()` wraps
// handlers when the sub-app has a custom errorHandler, which
// strips the metadata symbol).
2026-04-28 15:02:35 +00:00
const { app } = createHono ( { } )
2026-04-16 23:51:01 +00:00
const result = await generateSpecs ( app , {
documentation : {
info : {
title : "opencode" ,
version : "1.0.0" ,
description : "opencode api" ,
} ,
openapi : "3.1.1" ,
} ,
} )
return result
}
2026-03-11 21:41:49 +00:00
2026-04-16 23:51:01 +00:00
export let url : URL
2025-12-26 20:24:44 +00:00
2026-04-16 23:51:01 +00:00
export async function listen ( opts : {
port : number
hostname : string
mdns? : boolean
mdnsDomain? : string
cors? : string [ ]
} ) : Promise < Listener > {
const built = create ( opts )
const server = await built . runtime . listen ( opts )
2026-04-06 17:24:55 +00:00
2026-04-16 23:51:01 +00:00
const next = new URL ( "http://localhost" )
next . hostname = opts . hostname
next . port = String ( server . port )
url = next
2025-12-26 20:24:44 +00:00
2026-04-16 23:51:01 +00:00
const mdns =
opts . mdns &&
server . port &&
opts . hostname !== "127.0.0.1" &&
opts . hostname !== "localhost" &&
opts . hostname !== "::1"
if ( mdns ) {
MDNS . publish ( server . port , opts . mdnsDomain )
} else if ( opts . mdns ) {
log . warn ( "mDNS enabled but hostname is loopback; skipping mDNS publish" )
}
let closing : Promise < void > | undefined
return {
hostname : opts.hostname ,
port : server.port ,
url : next ,
stop ( close? : boolean ) {
closing ? ? = ( async ( ) = > {
if ( mdns ) MDNS . unpublish ( )
await server . stop ( close )
} ) ( )
return closing
} ,
2025-05-18 01:31:42 +00:00
}
}
2026-04-16 23:51:01 +00:00
export * as Server from "./server"