Skip to content

Commit 47cd52b

Browse files
committed
feat(server): support OPENCODE_WEB_URL for local frontend serving
1 parent e9a17e4 commit 47cd52b

2 files changed

Lines changed: 41 additions & 2 deletions

File tree

packages/opencode/src/flag/flag.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export namespace Flag {
6969
export const OPENCODE_EXPERIMENTAL_MARKDOWN = !falsy("OPENCODE_EXPERIMENTAL_MARKDOWN")
7070
export const OPENCODE_MODELS_URL = process.env["OPENCODE_MODELS_URL"]
7171
export const OPENCODE_MODELS_PATH = process.env["OPENCODE_MODELS_PATH"]
72+
export const OPENCODE_WEB_URL = process.env["OPENCODE_WEB_URL"]
7273
export const OPENCODE_DISABLE_CHANNEL_DB = truthy("OPENCODE_DISABLE_CHANNEL_DB")
7374
export const OPENCODE_SKIP_MIGRATIONS = truthy("OPENCODE_SKIP_MIGRATIONS")
7475
export const OPENCODE_STRICT_CONFIG_DEPS = truthy("OPENCODE_STRICT_CONFIG_DEPS")

packages/opencode/src/server/server.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,12 +557,50 @@ export namespace Server {
557557
)
558558
.all("/*", async (c) => {
559559
const path = c.req.path
560+
const appDir = Flag.OPENCODE_WEB_URL
560561

561-
const response = await proxy(`https://app.opencode.ai${path}`, {
562+
// Serve from local dist directory if OPENCODE_WEB_URL is a file path
563+
if (appDir && !appDir.startsWith("http")) {
564+
const fs = await import("fs")
565+
const nodePath = await import("path")
566+
const mimeTypes: Record<string, string> = {
567+
".html": "text/html",
568+
".js": "application/javascript",
569+
".mjs": "application/javascript",
570+
".css": "text/css",
571+
".json": "application/json",
572+
".svg": "image/svg+xml",
573+
".png": "image/png",
574+
".woff": "font/woff",
575+
".woff2": "font/woff2",
576+
".wasm": "application/wasm",
577+
".onnx": "application/octet-stream",
578+
}
579+
const getMime = (p: string) => mimeTypes[nodePath.default.extname(p)] || "application/octet-stream"
580+
const filePath = nodePath.default.join(appDir, path === "/" ? "index.html" : path)
581+
if (fs.existsSync(filePath) && fs.statSync(filePath).isFile()) {
582+
return new Response(Bun.file(filePath), { headers: { "Content-Type": getMime(filePath) } })
583+
}
584+
// SPA fallback — only for paths that look like client-side routes
585+
const apiPrefixes = ["/global", "/project", "/pty", "/config", "/experimental", "/session", "/permission", "/question", "/provider", "/mcp", "/tui", "/voice", "/tts"]
586+
const isApiPath = apiPrefixes.some((prefix) => path.startsWith(prefix))
587+
if (!isApiPath) {
588+
const indexPath = nodePath.default.join(appDir, "index.html")
589+
if (fs.existsSync(indexPath)) {
590+
return new Response(Bun.file(indexPath), { headers: { "Content-Type": "text/html" } })
591+
}
592+
}
593+
return c.notFound()
594+
}
595+
596+
const appHost = appDir || "https://app.opencode.ai"
597+
const appHostname = new URL(appHost).hostname
598+
599+
const response = await proxy(`${appHost}${path}`, {
562600
...c.req,
563601
headers: {
564602
...c.req.raw.headers,
565-
host: "app.opencode.ai",
603+
host: appHostname,
566604
},
567605
})
568606
response.headers.set(

0 commit comments

Comments
 (0)