diff --git a/src/ProcessManager.ts b/src/ProcessManager.ts index f660ba1..f6fdf2f 100644 --- a/src/ProcessManager.ts +++ b/src/ProcessManager.ts @@ -1,8 +1,41 @@ -import { spawn, ChildProcess } from "child_process"; +import { spawn, ChildProcess, execFileSync } from "child_process"; import { OpenCodeSettings } from "./types"; export type ProcessState = "stopped" | "starting" | "running" | "error"; +export function parseShellEnvOutput(output: Buffer): Record { + const env: Record = {}; + const vars = output.toString("utf8").split("\0"); + + for (const v of vars) { + if (!v) continue; + const eq = v.indexOf("="); + if (eq <= 0) continue; + const key = v.slice(0, eq); + const value = v.slice(eq + 1); + env[key] = value; + } + + return env; +} + +function getUserShellEnv(): Record { + try { + const shellCandidate = (process.env.SHELL || "/bin/sh").trim(); + const shell = shellCandidate.length > 0 ? shellCandidate : "/bin/sh"; + const result = execFileSync(shell, ["-l", "-c", "env -0"], { + encoding: "buffer", + timeout: 5000, + maxBuffer: 2 * 1024 * 1024, + stdio: ["ignore", "pipe", "pipe"], + }); + + return parseShellEnvOutput(result); + } catch { + return {}; + } +} + export class ProcessManager { private process: ChildProcess | null = null; private state: ProcessState = "stopped"; @@ -70,26 +103,45 @@ export class ProcessManager { projectDirectory: this.projectDirectory, }); - this.process = spawn( - this.settings.opencodePath, - [ - "serve", - "--port", - this.settings.port.toString(), - "--hostname", - this.settings.hostname, - "--cors", - "app://obsidian.md", - ], - { + const isWindows = process.platform === "win32"; + + if (isWindows) { + // Windows: use shell mode for better compatibility + this.process = spawn( + this.settings.opencodePath, + [ + "serve", + "--port", + this.settings.port.toString(), + "--hostname", + this.settings.hostname, + "--cors", + "app://obsidian.md", + ], + { + cwd: this.projectDirectory, + env: { ...process.env, NODE_USE_SYSTEM_CA: "1" }, + stdio: ["ignore", "pipe", "pipe"], + shell: true, + windowsHide: true, + detached: false, + } + ); + } else { + const userEnv = getUserShellEnv(); + const command = `${this.settings.opencodePath} serve --port ${this.settings.port} --hostname ${this.settings.hostname} --cors app://obsidian.md`; + + this.process = spawn("/bin/bash", ["-i", "-l", "-c", command], { cwd: this.projectDirectory, - env: { ...process.env, NODE_USE_SYSTEM_CA: "1" }, + env: { + ...process.env, + ...userEnv, + NODE_USE_SYSTEM_CA: "1", + }, stdio: ["ignore", "pipe", "pipe"], - shell: true, - windowsHide: true, - detached: (process.platform !== "win32"), - } - ); + detached: true, + }); + } console.log("[OpenCode] Process spawned with PID:", this.process.pid); diff --git a/tests/ProcessManager.test.ts b/tests/ProcessManager.test.ts index 31fe0ec..42bc621 100644 --- a/tests/ProcessManager.test.ts +++ b/tests/ProcessManager.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect, beforeAll, afterEach } from "bun:test"; -import { ProcessManager, ProcessState } from "../src/ProcessManager"; +import { ProcessManager, ProcessState, parseShellEnvOutput } from "../src/ProcessManager"; import { OpenCodeSettings } from "../src/types"; // Test configuration @@ -309,7 +309,10 @@ describe("ProcessManager", () => { expect(success).toBe(false); expect(currentManager.getState()).toBe("error"); - expect(currentManager.getLastError()).toContain("not found"); + const lastError = currentManager.getLastError() ?? ""; + const errorMatches = + lastError.includes("not found") || lastError.includes("exit code 127"); + expect(errorMatches).toBe(true); }); test("handles double stop gracefully", async () => { @@ -334,4 +337,26 @@ describe("ProcessManager", () => { expect(currentManager.getState()).toBe("stopped"); }); }); + + describe("parseShellEnvOutput", () => { + test("parses null-delimited entries", () => { + const output = Buffer.from("A=1\0B=two=2\0C=\0\0"); + const result = parseShellEnvOutput(output); + + expect(result).toEqual({ + A: "1", + B: "two=2", + C: "", + }); + }); + + test("skips empty and invalid entries", () => { + const output = Buffer.from("=bad\0NOEQ\0OK=1\0"); + const result = parseShellEnvOutput(output); + + expect(result).toEqual({ + OK: "1", + }); + }); + }); });