summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDax Raad <[email protected]>2026-03-09 17:13:42 -0400
committerDax Raad <[email protected]>2026-03-09 17:13:52 -0400
commit89d6f60d254028834ba340958d373c0f3199f631 (patch)
tree8dfabc52fe4c5f31bb924f6dee90562c9373c7aa
parentee18c9976e00ebf0162fe1dcc47b209e0507b3e5 (diff)
downloadopencode-89d6f60d254028834ba340958d373c0f3199f631.tar.gz
opencode-89d6f60d254028834ba340958d373c0f3199f631.zip
refactor(server): extract createApp function for server initialization
- Replace Server.App() with Server.Default() for internal server access - Extract server app creation into Server.createApp(opts) for testability - Move CORS whitelist from module-level variable to function parameter - Update all tests to use Server.Default() instead of Server.App()
-rw-r--r--packages/opencode/src/cli/cmd/run.ts2
-rw-r--r--packages/opencode/src/cli/cmd/tui/worker.ts4
-rw-r--r--packages/opencode/src/plugin/index.ts7
-rw-r--r--packages/opencode/src/server/server.ts910
-rw-r--r--packages/opencode/test/server/project-init-git.test.ts4
-rw-r--r--packages/opencode/test/server/session-select.test.ts6
6 files changed, 461 insertions, 472 deletions
diff --git a/packages/opencode/src/cli/cmd/run.ts b/packages/opencode/src/cli/cmd/run.ts
index ab30e2714..f92d3305b 100644
--- a/packages/opencode/src/cli/cmd/run.ts
+++ b/packages/opencode/src/cli/cmd/run.ts
@@ -667,7 +667,7 @@ export const RunCommand = cmd({
await bootstrap(process.cwd(), async () => {
const fetchFn = (async (input: RequestInfo | URL, init?: RequestInit) => {
const request = new Request(input, init)
- return Server.App().fetch(request)
+ return Server.Default().fetch(request)
}) as typeof globalThis.fetch
const sdk = createOpencodeClient({ baseUrl: "http://opencode.internal", fetch: fetchFn })
await execute(sdk)
diff --git a/packages/opencode/src/cli/cmd/tui/worker.ts b/packages/opencode/src/cli/cmd/tui/worker.ts
index f8dcee78a..408350c52 100644
--- a/packages/opencode/src/cli/cmd/tui/worker.ts
+++ b/packages/opencode/src/cli/cmd/tui/worker.ts
@@ -54,7 +54,7 @@ const startEventStream = (input: { directory: string; workspaceID?: string }) =>
const request = new Request(input, init)
const auth = getAuthorizationHeader()
if (auth) request.headers.set("Authorization", auth)
- return Server.App().fetch(request)
+ return Server.Default().fetch(request)
}) as typeof globalThis.fetch
const sdk = createOpencodeClient({
@@ -110,7 +110,7 @@ export const rpc = {
headers,
body: input.body,
})
- const response = await Server.App().fetch(request)
+ const response = await Server.Default().fetch(request)
const body = await response.text()
return {
status: response.status,
diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts
index e65d21bfd..1c129f608 100644
--- a/packages/opencode/src/plugin/index.ts
+++ b/packages/opencode/src/plugin/index.ts
@@ -25,8 +25,7 @@ export namespace Plugin {
const client = createOpencodeClient({
baseUrl: "http://localhost:4096",
directory: Instance.directory,
- // @ts-ignore - fetch type incompatibility
- fetch: async (...args) => Server.App().fetch(...args),
+ fetch: async (...args) => Server.Default().fetch(...args),
})
const config = await Config.get()
const hooks: Hooks[] = []
@@ -35,7 +34,9 @@ export namespace Plugin {
project: Instance.project,
worktree: Instance.worktree,
directory: Instance.directory,
- serverUrl: Server.url(),
+ get serverUrl(): URL {
+ throw new Error("Server URL is no longer supported in plugins")
+ },
$: Bun.$,
}
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index e353198af..3d435c8c9 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -31,7 +31,6 @@ import { FileRoutes } from "./routes/file"
import { ConfigRoutes } from "./routes/config"
import { ExperimentalRoutes } from "./routes/experimental"
import { ProviderRoutes } from "./routes/provider"
-import { lazy } from "../util/lazy"
import { InstanceBootstrap } from "../project/bootstrap"
import { NotFoundError } from "../storage/db"
import type { ContentfulStatusCode } from "hono/utils/http-status"
@@ -43,6 +42,7 @@ import { QuestionRoutes } from "./routes/question"
import { PermissionRoutes } from "./routes/permission"
import { GlobalRoutes } from "./routes/global"
import { MDNS } from "./mdns"
+import { lazy } from "@/util/lazy"
// @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
@@ -50,538 +50,529 @@ globalThis.AI_SDK_LOG_WARNINGS = false
export namespace Server {
const log = Log.create({ service: "server" })
- let _url: URL | undefined
- let _corsWhitelist: string[] = []
+ export const Default = lazy(() => createApp({}))
- export function url(): URL {
- return _url ?? new URL("http://localhost:4096")
- }
-
- const app = new Hono()
- export const App: () => Hono = lazy(
- () =>
- // TODO: Break server.ts into smaller route files to fix type inference
- app
- .onError((err, c) => {
- log.error("failed", {
- error: err,
- })
- if (err instanceof NamedError) {
- let status: ContentfulStatusCode
- if (err instanceof NotFoundError) status = 404
- else if (err instanceof Provider.ModelNotFoundError) status = 400
- else if (err.name.startsWith("Worktree")) status = 400
- else status = 500
- return c.json(err.toObject(), { status })
- }
- if (err instanceof HTTPException) return err.getResponse()
- const message = err instanceof Error && err.stack ? err.stack : err.toString()
- return c.json(new NamedError.Unknown({ message }).toObject(), {
- status: 500,
- })
+ export const createApp = (opts: { cors?: string[] }): Hono => {
+ const app = new Hono()
+ return app
+ .onError((err, c) => {
+ log.error("failed", {
+ error: err,
})
- .use((c, next) => {
- // Allow CORS preflight requests to succeed without auth.
- // Browser clients sending Authorization headers will preflight with OPTIONS.
- if (c.req.method === "OPTIONS") return next()
- const password = Flag.OPENCODE_SERVER_PASSWORD
- if (!password) return next()
- const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
- return basicAuth({ username, password })(c, next)
+ if (err instanceof NamedError) {
+ let status: ContentfulStatusCode
+ if (err instanceof NotFoundError) status = 404
+ else if (err instanceof Provider.ModelNotFoundError) status = 400
+ else if (err.name.startsWith("Worktree")) status = 400
+ else status = 500
+ return c.json(err.toObject(), { status })
+ }
+ if (err instanceof HTTPException) return err.getResponse()
+ const message = err instanceof Error && err.stack ? err.stack : err.toString()
+ return c.json(new NamedError.Unknown({ message }).toObject(), {
+ status: 500,
})
- .use(async (c, next) => {
- const skipLogging = c.req.path === "/log"
- if (!skipLogging) {
- log.info("request", {
- method: c.req.method,
- path: c.req.path,
- })
- }
- const timer = log.time("request", {
+ })
+ .use((c, next) => {
+ // Allow CORS preflight requests to succeed without auth.
+ // Browser clients sending Authorization headers will preflight with OPTIONS.
+ if (c.req.method === "OPTIONS") return next()
+ const password = Flag.OPENCODE_SERVER_PASSWORD
+ if (!password) return next()
+ const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode"
+ return basicAuth({ username, password })(c, next)
+ })
+ .use(async (c, next) => {
+ const skipLogging = c.req.path === "/log"
+ if (!skipLogging) {
+ log.info("request", {
method: c.req.method,
path: c.req.path,
})
- await next()
- if (!skipLogging) {
- timer.stop()
- }
+ }
+ const timer = log.time("request", {
+ method: c.req.method,
+ path: c.req.path,
})
- .use(
- cors({
- origin(input) {
- if (!input) return
+ await next()
+ if (!skipLogging) {
+ timer.stop()
+ }
+ })
+ .use(
+ cors({
+ origin(input) {
+ if (!input) return
- if (input.startsWith("http://localhost:")) return input
- if (input.startsWith("http://127.0.0.1:")) return input
- if (
- input === "tauri://localhost" ||
- input === "http://tauri.localhost" ||
- input === "https://tauri.localhost"
- )
- return input
+ if (input.startsWith("http://localhost:")) return input
+ if (input.startsWith("http://127.0.0.1:")) return input
+ if (
+ input === "tauri://localhost" ||
+ input === "http://tauri.localhost" ||
+ input === "https://tauri.localhost"
+ )
+ return input
- // *.opencode.ai (https only, adjust if needed)
- if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
- return input
- }
- if (_corsWhitelist.includes(input)) {
- return input
- }
+ // *.opencode.ai (https only, adjust if needed)
+ if (/^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/.test(input)) {
+ return input
+ }
+ if (opts?.cors?.includes(input)) {
+ return input
+ }
- return
- },
- }),
- )
- .route("/global", GlobalRoutes())
- .put(
- "/auth/:providerID",
- describeRoute({
- summary: "Set auth credentials",
- description: "Set authentication credentials",
- operationId: "auth.set",
- responses: {
- 200: {
- description: "Successfully set authentication credentials",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
+ return
+ },
+ }),
+ )
+ .route("/global", GlobalRoutes())
+ .put(
+ "/auth/:providerID",
+ describeRoute({
+ summary: "Set auth credentials",
+ description: "Set authentication credentials",
+ operationId: "auth.set",
+ responses: {
+ 200: {
+ description: "Successfully set authentication credentials",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
},
},
- ...errors(400),
},
- }),
- validator(
- "param",
- z.object({
- providerID: z.string(),
- }),
- ),
- validator("json", Auth.Info),
- async (c) => {
- const providerID = c.req.valid("param").providerID
- const info = c.req.valid("json")
- await Auth.set(providerID, info)
- return c.json(true)
+ ...errors(400),
},
- )
- .delete(
- "/auth/:providerID",
- describeRoute({
- summary: "Remove auth credentials",
- description: "Remove authentication credentials",
- operationId: "auth.remove",
- responses: {
- 200: {
- description: "Successfully removed authentication credentials",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
+ }),
+ validator(
+ "param",
+ z.object({
+ providerID: z.string(),
+ }),
+ ),
+ validator("json", Auth.Info),
+ async (c) => {
+ const providerID = c.req.valid("param").providerID
+ const info = c.req.valid("json")
+ await Auth.set(providerID, info)
+ return c.json(true)
+ },
+ )
+ .delete(
+ "/auth/:providerID",
+ describeRoute({
+ summary: "Remove auth credentials",
+ description: "Remove authentication credentials",
+ operationId: "auth.remove",
+ responses: {
+ 200: {
+ description: "Successfully removed authentication credentials",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
},
},
- ...errors(400),
},
- }),
- validator(
- "param",
- z.object({
- providerID: z.string(),
- }),
- ),
- async (c) => {
- const providerID = c.req.valid("param").providerID
- await Auth.remove(providerID)
- return c.json(true)
+ ...errors(400),
},
+ }),
+ validator(
+ "param",
+ z.object({
+ providerID: z.string(),
+ }),
+ ),
+ async (c) => {
+ const providerID = c.req.valid("param").providerID
+ await Auth.remove(providerID)
+ return c.json(true)
+ },
+ )
+ .use(async (c, next) => {
+ if (c.req.path === "/log") return next()
+ const workspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
+ const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
+ const directory = Filesystem.resolve(
+ (() => {
+ try {
+ return decodeURIComponent(raw)
+ } catch {
+ return raw
+ }
+ })(),
)
- .use(async (c, next) => {
- if (c.req.path === "/log") return next()
- const workspaceID = c.req.query("workspace") || c.req.header("x-opencode-workspace")
- const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
- const directory = Filesystem.resolve(
- (() => {
- try {
- return decodeURIComponent(raw)
- } catch {
- return raw
- }
- })(),
- )
- return WorkspaceContext.provide({
- workspaceID,
- async fn() {
- return Instance.provide({
- directory,
- init: InstanceBootstrap,
- async fn() {
- return next()
- },
- })
- },
- })
- })
- .use(WorkspaceRouterMiddleware)
- .get(
- "/doc",
- openAPIRouteHandler(app, {
- documentation: {
- info: {
- title: "opencode",
- version: "0.0.3",
- description: "opencode api",
+ return WorkspaceContext.provide({
+ workspaceID,
+ async fn() {
+ return Instance.provide({
+ directory,
+ init: InstanceBootstrap,
+ async fn() {
+ return next()
},
- openapi: "3.1.1",
+ })
+ },
+ })
+ })
+ .use(WorkspaceRouterMiddleware)
+ .get(
+ "/doc",
+ openAPIRouteHandler(app, {
+ documentation: {
+ info: {
+ title: "opencode",
+ version: "0.0.3",
+ description: "opencode api",
},
+ openapi: "3.1.1",
+ },
+ }),
+ )
+ .use(
+ validator(
+ "query",
+ z.object({
+ directory: z.string().optional(),
+ workspace: z.string().optional(),
}),
- )
- .use(
- validator(
- "query",
- z.object({
- directory: z.string().optional(),
- workspace: z.string().optional(),
- }),
- ),
- )
- .route("/project", ProjectRoutes())
- .route("/pty", PtyRoutes())
- .route("/config", ConfigRoutes())
- .route("/experimental", ExperimentalRoutes())
- .route("/session", SessionRoutes())
- .route("/permission", PermissionRoutes())
- .route("/question", QuestionRoutes())
- .route("/provider", ProviderRoutes())
- .route("/", FileRoutes())
- .route("/mcp", McpRoutes())
- .route("/tui", TuiRoutes())
- .post(
- "/instance/dispose",
- describeRoute({
- summary: "Dispose instance",
- description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
- operationId: "instance.dispose",
- responses: {
- 200: {
- description: "Instance disposed",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
+ ),
+ )
+ .route("/project", ProjectRoutes())
+ .route("/pty", PtyRoutes())
+ .route("/config", ConfigRoutes())
+ .route("/experimental", ExperimentalRoutes())
+ .route("/session", SessionRoutes())
+ .route("/permission", PermissionRoutes())
+ .route("/question", QuestionRoutes())
+ .route("/provider", ProviderRoutes())
+ .route("/", FileRoutes())
+ .route("/mcp", McpRoutes())
+ .route("/tui", TuiRoutes())
+ .post(
+ "/instance/dispose",
+ describeRoute({
+ summary: "Dispose instance",
+ description: "Clean up and dispose the current OpenCode instance, releasing all resources.",
+ operationId: "instance.dispose",
+ responses: {
+ 200: {
+ description: "Instance disposed",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
},
},
},
- }),
- async (c) => {
- await Instance.dispose()
- return c.json(true)
},
- )
- .get(
- "/path",
- describeRoute({
- summary: "Get paths",
- description:
- "Retrieve the current working directory and related path information for the OpenCode instance.",
- operationId: "path.get",
- responses: {
- 200: {
- description: "Path",
- content: {
- "application/json": {
- schema: resolver(
- z
- .object({
- home: z.string(),
- state: z.string(),
- config: z.string(),
- worktree: z.string(),
- directory: z.string(),
- })
- .meta({
- ref: "Path",
- }),
- ),
- },
+ }),
+ async (c) => {
+ await Instance.dispose()
+ return c.json(true)
+ },
+ )
+ .get(
+ "/path",
+ describeRoute({
+ summary: "Get paths",
+ description: "Retrieve the current working directory and related path information for the OpenCode instance.",
+ operationId: "path.get",
+ responses: {
+ 200: {
+ description: "Path",
+ content: {
+ "application/json": {
+ schema: resolver(
+ z
+ .object({
+ home: z.string(),
+ state: z.string(),
+ config: z.string(),
+ worktree: z.string(),
+ directory: z.string(),
+ })
+ .meta({
+ ref: "Path",
+ }),
+ ),
},
},
},
- }),
- async (c) => {
- return c.json({
- home: Global.Path.home,
- state: Global.Path.state,
- config: Global.Path.config,
- worktree: Instance.worktree,
- directory: Instance.directory,
- })
},
- )
- .get(
- "/vcs",
- describeRoute({
- summary: "Get VCS info",
- description:
- "Retrieve version control system (VCS) information for the current project, such as git branch.",
- operationId: "vcs.get",
- responses: {
- 200: {
- description: "VCS info",
- content: {
- "application/json": {
- schema: resolver(Vcs.Info),
- },
+ }),
+ async (c) => {
+ return c.json({
+ home: Global.Path.home,
+ state: Global.Path.state,
+ config: Global.Path.config,
+ worktree: Instance.worktree,
+ directory: Instance.directory,
+ })
+ },
+ )
+ .get(
+ "/vcs",
+ describeRoute({
+ summary: "Get VCS info",
+ description: "Retrieve version control system (VCS) information for the current project, such as git branch.",
+ operationId: "vcs.get",
+ responses: {
+ 200: {
+ description: "VCS info",
+ content: {
+ "application/json": {
+ schema: resolver(Vcs.Info),
},
},
},
- }),
- async (c) => {
- const branch = await Vcs.branch()
- return c.json({
- branch,
- })
},
- )
- .get(
- "/command",
- describeRoute({
- summary: "List commands",
- description: "Get a list of all available commands in the OpenCode system.",
- operationId: "command.list",
- responses: {
- 200: {
- description: "List of commands",
- content: {
- "application/json": {
- schema: resolver(Command.Info.array()),
- },
+ }),
+ async (c) => {
+ const branch = await Vcs.branch()
+ return c.json({
+ branch,
+ })
+ },
+ )
+ .get(
+ "/command",
+ describeRoute({
+ summary: "List commands",
+ description: "Get a list of all available commands in the OpenCode system.",
+ operationId: "command.list",
+ responses: {
+ 200: {
+ description: "List of commands",
+ content: {
+ "application/json": {
+ schema: resolver(Command.Info.array()),
},
},
},
- }),
- async (c) => {
- const commands = await Command.list()
- return c.json(commands)
},
- )
- .post(
- "/log",
- describeRoute({
- summary: "Write log",
- description: "Write a log entry to the server logs with specified level and metadata.",
- operationId: "app.log",
- responses: {
- 200: {
- description: "Log entry written successfully",
- content: {
- "application/json": {
- schema: resolver(z.boolean()),
- },
+ }),
+ async (c) => {
+ const commands = await Command.list()
+ return c.json(commands)
+ },
+ )
+ .post(
+ "/log",
+ describeRoute({
+ summary: "Write log",
+ description: "Write a log entry to the server logs with specified level and metadata.",
+ operationId: "app.log",
+ responses: {
+ 200: {
+ description: "Log entry written successfully",
+ content: {
+ "application/json": {
+ schema: resolver(z.boolean()),
},
},
- ...errors(400),
},
+ ...errors(400),
+ },
+ }),
+ validator(
+ "json",
+ z.object({
+ service: z.string().meta({ description: "Service name for the log entry" }),
+ level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
+ message: z.string().meta({ description: "Log message" }),
+ extra: z
+ .record(z.string(), z.any())
+ .optional()
+ .meta({ description: "Additional metadata for the log entry" }),
}),
- validator(
- "json",
- z.object({
- service: z.string().meta({ description: "Service name for the log entry" }),
- level: z.enum(["debug", "info", "error", "warn"]).meta({ description: "Log level" }),
- message: z.string().meta({ description: "Log message" }),
- extra: z
- .record(z.string(), z.any())
- .optional()
- .meta({ description: "Additional metadata for the log entry" }),
- }),
- ),
- async (c) => {
- const { service, level, message, extra } = c.req.valid("json")
- const logger = Log.create({ service })
+ ),
+ async (c) => {
+ const { service, level, message, extra } = c.req.valid("json")
+ const logger = Log.create({ service })
- switch (level) {
- case "debug":
- logger.debug(message, extra)
- break
- case "info":
- logger.info(message, extra)
- break
- case "error":
- logger.error(message, extra)
- break
- case "warn":
- logger.warn(message, extra)
- break
- }
+ switch (level) {
+ case "debug":
+ logger.debug(message, extra)
+ break
+ case "info":
+ logger.info(message, extra)
+ break
+ case "error":
+ logger.error(message, extra)
+ break
+ case "warn":
+ logger.warn(message, extra)
+ break
+ }
- return c.json(true)
- },
- )
- .get(
- "/agent",
- describeRoute({
- summary: "List agents",
- description: "Get a list of all available AI agents in the OpenCode system.",
- operationId: "app.agents",
- responses: {
- 200: {
- description: "List of agents",
- content: {
- "application/json": {
- schema: resolver(Agent.Info.array()),
- },
+ return c.json(true)
+ },
+ )
+ .get(
+ "/agent",
+ describeRoute({
+ summary: "List agents",
+ description: "Get a list of all available AI agents in the OpenCode system.",
+ operationId: "app.agents",
+ responses: {
+ 200: {
+ description: "List of agents",
+ content: {
+ "application/json": {
+ schema: resolver(Agent.Info.array()),
},
},
},
- }),
- async (c) => {
- const modes = await Agent.list()
- return c.json(modes)
},
- )
- .get(
- "/skill",
- describeRoute({
- summary: "List skills",
- description: "Get a list of all available skills in the OpenCode system.",
- operationId: "app.skills",
- responses: {
- 200: {
- description: "List of skills",
- content: {
- "application/json": {
- schema: resolver(Skill.Info.array()),
- },
+ }),
+ async (c) => {
+ const modes = await Agent.list()
+ return c.json(modes)
+ },
+ )
+ .get(
+ "/skill",
+ describeRoute({
+ summary: "List skills",
+ description: "Get a list of all available skills in the OpenCode system.",
+ operationId: "app.skills",
+ responses: {
+ 200: {
+ description: "List of skills",
+ content: {
+ "application/json": {
+ schema: resolver(Skill.Info.array()),
},
},
},
- }),
- async (c) => {
- const skills = await Skill.all()
- return c.json(skills)
},
- )
- .get(
- "/lsp",
- describeRoute({
- summary: "Get LSP status",
- description: "Get LSP server status",
- operationId: "lsp.status",
- responses: {
- 200: {
- description: "LSP server status",
- content: {
- "application/json": {
- schema: resolver(LSP.Status.array()),
- },
+ }),
+ async (c) => {
+ const skills = await Skill.all()
+ return c.json(skills)
+ },
+ )
+ .get(
+ "/lsp",
+ describeRoute({
+ summary: "Get LSP status",
+ description: "Get LSP server status",
+ operationId: "lsp.status",
+ responses: {
+ 200: {
+ description: "LSP server status",
+ content: {
+ "application/json": {
+ schema: resolver(LSP.Status.array()),
},
},
},
- }),
- async (c) => {
- return c.json(await LSP.status())
},
- )
- .get(
- "/formatter",
- describeRoute({
- summary: "Get formatter status",
- description: "Get formatter status",
- operationId: "formatter.status",
- responses: {
- 200: {
- description: "Formatter status",
- content: {
- "application/json": {
- schema: resolver(Format.Status.array()),
- },
+ }),
+ async (c) => {
+ return c.json(await LSP.status())
+ },
+ )
+ .get(
+ "/formatter",
+ describeRoute({
+ summary: "Get formatter status",
+ description: "Get formatter status",
+ operationId: "formatter.status",
+ responses: {
+ 200: {
+ description: "Formatter status",
+ content: {
+ "application/json": {
+ schema: resolver(Format.Status.array()),
},
},
},
- }),
- async (c) => {
- return c.json(await Format.status())
},
- )
- .get(
- "/event",
- describeRoute({
- summary: "Subscribe to events",
- description: "Get events",
- operationId: "event.subscribe",
- responses: {
- 200: {
- description: "Event stream",
- content: {
- "text/event-stream": {
- schema: resolver(BusEvent.payloads()),
- },
+ }),
+ async (c) => {
+ return c.json(await Format.status())
+ },
+ )
+ .get(
+ "/event",
+ describeRoute({
+ summary: "Subscribe to events",
+ description: "Get events",
+ operationId: "event.subscribe",
+ responses: {
+ 200: {
+ description: "Event stream",
+ content: {
+ "text/event-stream": {
+ schema: resolver(BusEvent.payloads()),
},
},
},
- }),
- async (c) => {
- log.info("event connected")
- c.header("X-Accel-Buffering", "no")
- c.header("X-Content-Type-Options", "nosniff")
- return streamSSE(c, async (stream) => {
+ },
+ }),
+ async (c) => {
+ log.info("event connected")
+ c.header("X-Accel-Buffering", "no")
+ c.header("X-Content-Type-Options", "nosniff")
+ return streamSSE(c, async (stream) => {
+ stream.writeSSE({
+ data: JSON.stringify({
+ type: "server.connected",
+ properties: {},
+ }),
+ })
+ const unsub = Bus.subscribeAll(async (event) => {
+ await stream.writeSSE({
+ data: JSON.stringify(event),
+ })
+ if (event.type === Bus.InstanceDisposed.type) {
+ stream.close()
+ }
+ })
+
+ // Send heartbeat every 10s to prevent stalled proxy streams.
+ const heartbeat = setInterval(() => {
stream.writeSSE({
data: JSON.stringify({
- type: "server.connected",
+ type: "server.heartbeat",
properties: {},
}),
})
- const unsub = Bus.subscribeAll(async (event) => {
- await stream.writeSSE({
- data: JSON.stringify(event),
- })
- if (event.type === Bus.InstanceDisposed.type) {
- stream.close()
- }
- })
+ }, 10_000)
- // Send heartbeat every 10s to prevent stalled proxy streams.
- const heartbeat = setInterval(() => {
- stream.writeSSE({
- data: JSON.stringify({
- type: "server.heartbeat",
- properties: {},
- }),
- })
- }, 10_000)
-
- await new Promise<void>((resolve) => {
- stream.onAbort(() => {
- clearInterval(heartbeat)
- unsub()
- resolve()
- log.info("event disconnected")
- })
+ await new Promise<void>((resolve) => {
+ stream.onAbort(() => {
+ clearInterval(heartbeat)
+ unsub()
+ resolve()
+ log.info("event disconnected")
})
})
+ })
+ },
+ )
+ .all("/*", async (c) => {
+ const path = c.req.path
+
+ const response = await proxy(`https://app.opencode.ai${path}`, {
+ ...c.req,
+ headers: {
+ ...c.req.raw.headers,
+ host: "app.opencode.ai",
},
+ })
+ response.headers.set(
+ "Content-Security-Policy",
+ "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:",
)
- .all("/*", async (c) => {
- const path = c.req.path
-
- const response = await proxy(`https://app.opencode.ai${path}`, {
- ...c.req,
- headers: {
- ...c.req.raw.headers,
- host: "app.opencode.ai",
- },
- })
- response.headers.set(
- "Content-Security-Policy",
- "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; media-src 'self' data:; connect-src 'self' data:",
- )
- return response
- }) as unknown as Hono,
- )
+ return response
+ })
+ }
export async function openapi() {
// Cast to break excessive type recursion from long route chains
- const result = await generateSpecs(App() as Hono, {
+ const result = await generateSpecs(Default(), {
documentation: {
info: {
title: "opencode",
@@ -601,12 +592,11 @@ export namespace Server {
mdnsDomain?: string
cors?: string[]
}) {
- _corsWhitelist = opts.cors ?? []
-
+ const app = createApp(opts)
const args = {
hostname: opts.hostname,
idleTimeout: 0,
- fetch: App().fetch,
+ fetch: app.fetch,
websocket: websocket,
} as const
const tryServe = (port: number) => {
@@ -619,8 +609,6 @@ export namespace Server {
const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port)
if (!server) throw new Error(`Failed to start server on port ${opts.port}`)
- _url = server.url
-
const shouldPublishMDNS =
opts.mdns &&
server.port &&
diff --git a/packages/opencode/test/server/project-init-git.test.ts b/packages/opencode/test/server/project-init-git.test.ts
index 50cda0ab5..cc1ac0cbc 100644
--- a/packages/opencode/test/server/project-init-git.test.ts
+++ b/packages/opencode/test/server/project-init-git.test.ts
@@ -19,7 +19,7 @@ afterEach(async () => {
describe("project.initGit endpoint", () => {
test("initializes git and reloads immediately", async () => {
await using tmp = await tmpdir()
- const app = Server.App()
+ const app = Server.Default()
const seen: { directory?: string; payload: { type: string } }[] = []
const fn = (evt: { directory?: string; payload: { type: string } }) => {
seen.push(evt)
@@ -75,7 +75,7 @@ describe("project.initGit endpoint", () => {
test("does not reload when the project is already git", async () => {
await using tmp = await tmpdir({ git: true })
- const app = Server.App()
+ const app = Server.Default()
const seen: { directory?: string; payload: { type: string } }[] = []
const fn = (evt: { directory?: string; payload: { type: string } }) => {
seen.push(evt)
diff --git a/packages/opencode/test/server/session-select.test.ts b/packages/opencode/test/server/session-select.test.ts
index 479be4a17..a336f8133 100644
--- a/packages/opencode/test/server/session-select.test.ts
+++ b/packages/opencode/test/server/session-select.test.ts
@@ -17,7 +17,7 @@ describe("tui.selectSession endpoint", () => {
const session = await Session.create({})
// #when
- const app = Server.App()
+ const app = Server.Default()
const response = await app.request("/tui/select-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -42,7 +42,7 @@ describe("tui.selectSession endpoint", () => {
const nonExistentSessionID = "ses_nonexistent123"
// #when
- const app = Server.App()
+ const app = Server.Default()
const response = await app.request("/tui/select-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -63,7 +63,7 @@ describe("tui.selectSession endpoint", () => {
const invalidSessionID = "invalid_session_id"
// #when
- const app = Server.App()
+ const app = Server.Default()
const response = await app.request("/tui/select-session", {
method: "POST",
headers: { "Content-Type": "application/json" },