feat(cli): embed web ui (#41525)

Co-authored-by: Dax Raad <d@ironbay.co>
This commit is contained in:
Brendan Allan
2026-08-12 06:43:07 +08:00
committed by GitHub
parent f1399dcb65
commit 20df2fdba6
19 changed files with 338 additions and 33 deletions
+41
View File
@@ -0,0 +1,41 @@
import { $ } from "bun"
import { readdir } from "node:fs/promises"
import path from "node:path"
import { brotliCompressSync, constants } from "node:zlib"
export async function buildAppArchive(channel: string) {
const root = path.resolve(import.meta.dirname, "../../app")
await $`bun run build`.cwd(root).env({ ...process.env, OPENCODE_CHANNEL: channel })
const assets = Object.fromEntries(
await Promise.all(
(await files(path.join(root, "dist")))
.filter((key) => !key.endsWith(".map"))
.map(async (key) => {
const source = path.join(root, "dist", key)
const body = Buffer.from(await Bun.file(source).arrayBuffer())
const encoding = isText(key) ? "utf8" : "base64"
return [key, { encoding, content: body.toString(encoding) }] as const
}),
),
)
return brotliCompressSync(JSON.stringify(assets), {
params: { [constants.BROTLI_PARAM_QUALITY]: 11 },
}).toString("base64")
}
function isText(key: string) {
return key === "_headers" || /\.(?:css|html|js|json|svg|txt|webmanifest|xml)$/.test(key)
}
async function files(root: string, current = root): Promise<string[]> {
return (
await Promise.all(
(await readdir(current, { withFileTypes: true })).map((entry) => {
const target = path.join(current, entry.name)
return entry.isDirectory() ? files(root, target) : [path.relative(root, target).replaceAll(path.sep, "/")]
}),
)
)
.flat()
.toSorted()
}
+10 -1
View File
@@ -12,6 +12,7 @@ import { modelsData } from "./generate"
import { collectNodeAssets, copyNodeAssets, hashNodeAssets, seaAssetMap } from "./node-assets"
import { mainConfig } from "../vite.node.config"
import { nodeExecArgv, nodeTarget, type NodeTarget } from "../src/node/target"
import { buildAppArchive } from "./app-assets"
const NODE_VERSION = "26.4.0"
const dir = path.resolve(import.meta.dirname, "..")
@@ -55,13 +56,21 @@ const builder =
!bundleOnly || targets.some((target) => target.platform === process.platform && target.arch === process.arch)
? await resolveHostNode()
: undefined
const appArchive = await buildAppArchive(Script.channel)
for (const target of targets) {
console.log(`building cli-node-${targetName(target)}`)
const assets = await collectNodeAssets(target)
await rm("dist-node", { recursive: true, force: true })
const assetHash = await hashNodeAssets(assets)
const input = { version: Script.version, channel: Script.channel, models: modelsData, assetHash, target }
const input = {
version: Script.version,
channel: Script.channel,
models: modelsData,
assetHash,
target,
appArchive,
}
await copyNodeAssets(assets)
await build(mainConfig(input))
+16 -1
View File
@@ -8,6 +8,7 @@ import { createSolidTransformPlugin } from "@opentui/solid/bun-plugin"
import type { BunPlugin } from "bun"
import pkg from "../package.json"
import { modelsData } from "./generate"
import { buildAppArchive } from "./app-assets"
const dir = path.resolve(import.meta.dirname, "..")
const binary = "opencode2"
@@ -54,6 +55,20 @@ const targets = singleFlag
: allTargets
if (!skipInstall) await $`bun install --os="*" --cpu="*" @opentui/core@${pkg.dependencies["@opentui/core"]}`
const appArchive = await buildAppArchive(Script.channel)
const appAssetsPlugin: BunPlugin = {
name: "opencode-app-assets",
setup(build) {
build.onResolve({ filter: /^virtual:opencode-app-assets$/ }, () => ({
path: "opencode-app-assets",
namespace: "opencode",
}))
build.onLoad({ filter: /^opencode-app-assets$/, namespace: "opencode" }, () => ({
loader: "js",
contents: `export default ${JSON.stringify(appArchive)}`,
}))
},
}
for (const item of targets) {
const parcelWatcherPackage = `@parcel/watcher-${item.os}-${item.arch}${item.os === "linux" ? `-${item.abi ?? "glibc"}` : ""}`
@@ -80,7 +95,7 @@ for (const item of targets) {
const result = await Bun.build({
entrypoints: ["./src/index.ts"],
tsconfig: "./tsconfig.json",
plugins: [solidPlugin, parcelWatcherPlugin],
plugins: [appAssetsPlugin, solidPlugin, parcelWatcherPlugin],
external: ["node-gyp"],
format: "esm",
minify: true,