diff --git a/packages/desktop/src/main/wsl/sidecar-shell.test.ts b/packages/desktop/src/main/wsl/sidecar-shell.test.ts new file mode 100644 index 0000000000..ffb3b58fab --- /dev/null +++ b/packages/desktop/src/main/wsl/sidecar-shell.test.ts @@ -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" }) +}) diff --git a/packages/desktop/src/main/wsl/sidecar-shell.ts b/packages/desktop/src/main/wsl/sidecar-shell.ts new file mode 100644 index 0000000000..e1eceb6e3a --- /dev/null +++ b/packages/desktop/src/main/wsl/sidecar-shell.ts @@ -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"), + } +} diff --git a/packages/desktop/src/main/wsl/sidecar.ts b/packages/desktop/src/main/wsl/sidecar.ts index 522e575a47..1e4b550474 100644 --- a/packages/desktop/src/main/wsl/sidecar.ts +++ b/packages/desktop/src/main/wsl/sidecar.ts @@ -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) => {