From 14042e2a56c8813791fe602e8c372499db517e3b Mon Sep 17 00:00:00 2001 From: Brendan Allan Date: Tue, 7 Apr 2026 11:37:57 +0800 Subject: [PATCH] electron: implement server env probe for in-process server (#21196) --- packages/app/src/components/terminal.tsx | 1 + .../desktop-electron/electron.vite.config.ts | 4 +-- packages/desktop-electron/src/main/index.ts | 2 +- packages/desktop-electron/src/main/server.ts | 21 ++++++++++++++- .../desktop-electron/src/main/shell-env.ts | 26 +++++++++---------- packages/opencode/src/server/server.ts | 3 +++ 6 files changed, 40 insertions(+), 17 deletions(-) diff --git a/packages/app/src/components/terminal.tsx b/packages/app/src/components/terminal.tsx index 0a5a7d2d3e..7815128820 100644 --- a/packages/app/src/components/terminal.tsx +++ b/packages/app/src/components/terminal.tsx @@ -518,6 +518,7 @@ export const Terminal = (props: TerminalProps) => { const next = new URL(url + `/pty/${id}/connect`) next.searchParams.set("directory", directory) next.searchParams.set("cursor", String(seek)) + next.searchParams.set("token", btoa(`${username}:${password}`)) next.protocol = next.protocol === "https:" ? "wss:" : "ws:" next.username = username next.password = password diff --git a/packages/desktop-electron/electron.vite.config.ts b/packages/desktop-electron/electron.vite.config.ts index 73e8c4eda3..c18343c93a 100644 --- a/packages/desktop-electron/electron.vite.config.ts +++ b/packages/desktop-electron/electron.vite.config.ts @@ -43,8 +43,8 @@ export default defineConfig({ enforce: "post", async closeBundle() { for (const l of await fs.readdir(OPENCODE_SERVER_DIST)) { - if (l.endsWith(".js")) continue - await fs.writeFile(`./out/main/${l}`, await fs.readFile(`${OPENCODE_SERVER_DIST}/${l}`)) + if (!l.endsWith(".wasm")) continue + await fs.writeFile(`./out/main/chunks/${l}`, await fs.readFile(`${OPENCODE_SERVER_DIST}/${l}`)) } }, }, diff --git a/packages/desktop-electron/src/main/index.ts b/packages/desktop-electron/src/main/index.ts index 2002f10690..89e7c61ac5 100644 --- a/packages/desktop-electron/src/main/index.ts +++ b/packages/desktop-electron/src/main/index.ts @@ -36,7 +36,7 @@ import { parseMarkdown } from "./markdown" import { createMenu } from "./menu" import { getDefaultServerUrl, getWslConfig, setDefaultServerUrl, setWslConfig, spawnLocalServer } from "./server" import { createLoadingWindow, createMainWindow, setBackgroundColor, setDockIcon } from "./windows" -import { Server } from "virtual:opencode-server" +import type { Server } from "virtual:opencode-server" const initEmitter = new EventEmitter() let initStep: InitStep = { phase: "server_waiting" } diff --git a/packages/desktop-electron/src/main/server.ts b/packages/desktop-electron/src/main/server.ts index e09d7c3e75..5a6050013a 100644 --- a/packages/desktop-electron/src/main/server.ts +++ b/packages/desktop-electron/src/main/server.ts @@ -1,5 +1,6 @@ -import { Server, Log } from "virtual:opencode-server" +import { app } from "electron" import { DEFAULT_SERVER_URL_KEY, WSL_ENABLED_KEY } from "./constants" +import { getUserShell, loadShellEnv } from "./shell-env" import { store } from "./store" export type WslConfig = { enabled: boolean } @@ -30,6 +31,8 @@ export function setWslConfig(config: WslConfig) { } export async function spawnLocalServer(hostname: string, port: number, password: string) { + prepareServerEnv(password) + const { Log, Server } = await import("virtual:opencode-server") await Log.init({ level: "WARN" }) const listener = await Server.listen({ port, @@ -54,6 +57,22 @@ export async function spawnLocalServer(hostname: string, port: number, password: return { listener, health: { wait } } } +function prepareServerEnv(password: string) { + const shell = process.platform === "win32" ? null : getUserShell() + const shellEnv = shell ? (loadShellEnv(shell) ?? {}) : {} + const env = { + ...process.env, + ...shellEnv, + OPENCODE_EXPERIMENTAL_ICON_DISCOVERY: "true", + OPENCODE_EXPERIMENTAL_FILEWATCHER: "true", + OPENCODE_CLIENT: "desktop", + OPENCODE_SERVER_USERNAME: "opencode", + OPENCODE_SERVER_PASSWORD: password, + XDG_STATE_HOME: app.getPath("userData"), + } + Object.assign(process.env, env) +} + export async function checkHealth(url: string, password?: string | null): Promise { let healthUrl: URL try { diff --git a/packages/desktop-electron/src/main/shell-env.ts b/packages/desktop-electron/src/main/shell-env.ts index 3000848212..8453a5730d 100644 --- a/packages/desktop-electron/src/main/shell-env.ts +++ b/packages/desktop-electron/src/main/shell-env.ts @@ -1,7 +1,7 @@ import { spawnSync } from "node:child_process" import { basename } from "node:path" -const SHELL_ENV_TIMEOUT = 5_000 +const TIMEOUT = 5_000 type Probe = { type: "Loaded"; value: Record } | { type: "Timeout" } | { type: "Unavailable" } @@ -20,28 +20,28 @@ export function parseShellEnv(out: Buffer) { return env } -function probeShellEnv(shell: string, mode: "-il" | "-l"): Probe { +function probe(shell: string, mode: "-il" | "-l"): Probe { const out = spawnSync(shell, [mode, "-c", "env -0"], { stdio: ["ignore", "pipe", "ignore"], - timeout: SHELL_ENV_TIMEOUT, + timeout: TIMEOUT, windowsHide: true, }) const err = out.error as NodeJS.ErrnoException | undefined if (err) { if (err.code === "ETIMEDOUT") return { type: "Timeout" } - console.log(`[cli] Shell env probe failed for ${shell} ${mode}: ${err.message}`) + console.log(`[server] Shell env probe failed for ${shell} ${mode}: ${err.message}`) return { type: "Unavailable" } } if (out.status !== 0) { - console.log(`[cli] Shell env probe exited with non-zero status for ${shell} ${mode}`) + console.log(`[server] Shell env probe exited with non-zero status for ${shell} ${mode}`) return { type: "Unavailable" } } const env = parseShellEnv(out.stdout) if (Object.keys(env).length === 0) { - console.log(`[cli] Shell env probe returned empty env for ${shell} ${mode}`) + console.log(`[server] Shell env probe returned empty env for ${shell} ${mode}`) return { type: "Unavailable" } } @@ -56,27 +56,27 @@ export function isNushell(shell: string) { export function loadShellEnv(shell: string) { if (isNushell(shell)) { - console.log(`[cli] Skipping shell env probe for nushell: ${shell}`) + console.log(`[server] Skipping shell env probe for nushell: ${shell}`) return null } - const interactive = probeShellEnv(shell, "-il") + const interactive = probe(shell, "-il") if (interactive.type === "Loaded") { - console.log(`[cli] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`) + console.log(`[server] Loaded shell environment with -il (${Object.keys(interactive.value).length} vars)`) return interactive.value } if (interactive.type === "Timeout") { - console.warn(`[cli] Interactive shell env probe timed out: ${shell}`) + console.warn(`[server] Interactive shell env probe timed out: ${shell}`) return null } - const login = probeShellEnv(shell, "-l") + const login = probe(shell, "-l") if (login.type === "Loaded") { - console.log(`[cli] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`) + console.log(`[server] Loaded shell environment with -l (${Object.keys(login.value).length} vars)`) return login.value } - console.warn(`[cli] Falling back to app environment: ${shell}`) + console.warn(`[server] Falling back to app environment: ${shell}`) return null } diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 8261f1738c..a37cc167df 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -54,6 +54,9 @@ export namespace Server { const password = Flag.OPENCODE_SERVER_PASSWORD if (!password) return next() const username = Flag.OPENCODE_SERVER_USERNAME ?? "opencode" + + if (c.req.query("token")) c.req.raw.headers.set("authorization", `Basic ${c.req.query("token")}`) + return basicAuth({ username, password })(c, next) }) .use(async (c, next) => {