Compare commits

...

1 Commits

Author SHA1 Message Date
exe.dev user 11b14dff4d fix(desktop): load WSL shell environment 2026-07-22 04:14:47 +00:00
3 changed files with 62 additions and 16 deletions
@@ -0,0 +1,29 @@
import { expect, test } from "bun:test"
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { wslSidecarShell } from "./sidecar-shell"
test("loads interactive WSL environment variables for the desktop server", async () => {
const home = await mkdtemp(join(tmpdir(), "opencode-wsl-env-"))
const opencode = join(home, "opencode")
await writeFile(join(home, ".profile"), 'source "$HOME/.bashrc"\n')
await writeFile(join(home, ".bashrc"), "export COMPANY_BASEURL=https://company.example\n")
await writeFile(opencode, '#!/usr/bin/env bash\nprintf "%s" "$COMPANY_BASEURL"\n')
await chmod(opencode, 0o755)
const shell = wslSidecarShell(opencode, 4096, "opencode", "secret", false)
const proc = Bun.spawn(shell.args, {
cwd: home,
env: { ...process.env, HOME: home },
stdin: new Blob([shell.script]),
stdout: "pipe",
stderr: "pipe",
})
const output = await new Response(proc.stdout).text()
await new Response(proc.stderr).text()
const code = await proc.exited
await rm(home, { recursive: true, force: true })
expect({ code, output }).toEqual({ code: 0, output: "https://company.example" })
})
@@ -0,0 +1,28 @@
import { shellEscape } from "./runtime"
export function wslSidecarShell(opencode: string, port: number, username: string, password: string, packaged: boolean) {
return {
// Explicit WSL commands skip login startup files. Load the same user environment
// as a terminal launch, then replace the interactive shell before reading stdin.
args: [
"bash",
"--noprofile",
"--norc",
"-ic",
'for file in "$HOME/.bash_profile" "$HOME/.bash_login" "$HOME/.profile"; do [[ ! -r "$file" ]] || { source "$file"; break; }; done; exec bash -se',
],
script: [
"set -euo pipefail",
'cd "$HOME" || cd /',
'PATH=$(awk -v RS=: -v ORS=: \'$0 !~ /^\\/mnt\\//\' <<<"$PATH" | sed "s/:$//")',
"export PATH",
"export WSLENV=",
"export OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER=true",
"export OPENCODE_CLIENT=desktop",
`export OPENCODE_SERVER_USERNAME=${shellEscape(username)}`,
`export OPENCODE_SERVER_PASSWORD=${shellEscape(password)}`,
'export XDG_STATE_HOME="$HOME/.local/state"',
`exec ${shellEscape(opencode)} --print-logs --log-level ${packaged ? "WARN" : "INFO"} serve --hostname 0.0.0.0 --port ${port}`,
].join("\n"),
}
}
+5 -16
View File
@@ -3,7 +3,8 @@ import { randomUUID } from "node:crypto"
import { createServer } from "node:net"
import { app } from "electron"
import { checkHealth } from "../server"
import { type WslCommandLine, resolveWslOpencode, shellEscape, wslArgs } from "./runtime"
import { type WslCommandLine, resolveWslOpencode, wslArgs } from "./runtime"
import { wslSidecarShell } from "./sidecar-shell"
import { pollWslHealth } from "./startup"
export type WslSidecar = {
@@ -23,24 +24,12 @@ export async function spawnWslSidecar(
const port = await allocatePort()
const password = randomUUID()
const username = "opencode"
const script = [
"set -euo pipefail",
'cd "$HOME" || cd /',
'PATH=$(awk -v RS=: -v ORS=: \'$0 !~ /^\\/mnt\\//\' <<<"$PATH" | sed "s/:$//")',
"export PATH",
"export WSLENV=",
"export OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER=true",
"export OPENCODE_CLIENT=desktop",
`export OPENCODE_SERVER_USERNAME=${shellEscape(username)}`,
`export OPENCODE_SERVER_PASSWORD=${shellEscape(password)}`,
'export XDG_STATE_HOME="$HOME/.local/state"',
`exec ${shellEscape(opencode)} --print-logs --log-level ${app.isPackaged ? "WARN" : "INFO"} serve --hostname 0.0.0.0 --port ${port}`,
].join("\n")
const child = spawn("wsl", wslArgs(["bash", "-se"], distro), {
const shell = wslSidecarShell(opencode, port, username, password, app.isPackaged)
const child = spawn("wsl", wslArgs(shell.args, distro), {
stdio: ["pipe", "pipe", "pipe"],
windowsHide: true,
})
child.stdin.end(script)
child.stdin.end(shell.script)
const recentOutput: string[] = []
const emit = (line: WslCommandLine) => {