cloudaxe-opencode/packages/opencode/src/server/server.ts

197 lines
5.8 KiB
TypeScript
Raw Normal View History

import { generateSpecs } from "hono-openapi"
2025-05-31 18:41:00 +00:00
import { Hono } from "hono"
import { adapter } from "#hono"
import { lazy } from "@/util/lazy"
import * as Log from "@opencode-ai/core/util/log"
import { Flag } from "@opencode-ai/core/flag/flag"
import { WorkspaceID } from "@/control-plane/schema"
import { MDNS } from "./mdns"
import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware"
import { FenceMiddleware } from "./fence"
import { initProjectors } from "./projectors"
import { InstanceRoutes } from "./routes/instance"
import { ControlPlaneRoutes } from "./routes/control"
import { UIRoutes } from "./routes/ui"
import { GlobalRoutes } from "./routes/global"
import { WorkspaceRouterMiddleware } from "./workspace"
import { InstanceMiddleware } from "./routes/instance/middleware"
import { WorkspaceRoutes } from "./routes/control/workspace"
import { ExperimentalHttpApiServer } from "./routes/instance/httpapi/server"
import * as ServerBackend from "./backend"
2025-06-09 18:01:11 +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
initProjectors()
const log = Log.create({ service: "server" })
export type Listener = {
hostname: string
port: number
url: URL
stop: (close?: boolean) => Promise<void>
}
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" })),
)
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()
}
function create(opts: { cors?: string[] }) {
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
}
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"),
) {
const backendAttributes = ServerBackend.attributes(selection)
const app = new Hono()
.onError(ErrorMiddleware)
.use(AuthMiddleware)
.use(LoggerMiddleware(backendAttributes))
.use(CompressionMiddleware)
.use(CorsMiddleware(opts))
.route("/global", GlobalRoutes())
const runtime = adapter.create(app)
if (Flag.OPENCODE_WORKSPACE_ID) {
return {
app: app
.use(InstanceMiddleware(Flag.OPENCODE_WORKSPACE_ID ? WorkspaceID.make(Flag.OPENCODE_WORKSPACE_ID) : undefined))
.use(FenceMiddleware)
.route("/", InstanceRoutes(runtime.upgradeWebSocket)),
runtime,
}
}
const workspaceApp = new Hono()
const workspaceLegacyApp = new Hono()
.use(InstanceMiddleware())
.route("/experimental/workspace", WorkspaceRoutes())
.use(WorkspaceRouterMiddleware(runtime.upgradeWebSocket))
workspaceApp.route("/", workspaceLegacyApp)
return {
app: app
.route("/", ControlPlaneRoutes())
.route("/", workspaceApp)
.route("/", InstanceRoutes(runtime.upgradeWebSocket))
.route("/", UIRoutes()),
runtime,
2025-05-18 18:28:08 +00:00
}
}
2025-05-18 18:13:04 +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).
const { app } = createHono({})
const result = await generateSpecs(app, {
documentation: {
info: {
title: "opencode",
version: "1.0.0",
description: "opencode api",
},
openapi: "3.1.1",
},
})
return result
}
export let url: URL
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)
const next = new URL("http://localhost")
next.hostname = opts.hostname
next.port = String(server.port)
url = next
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
}
}
export * as Server from "./server"