Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 71 additions & 19 deletions src/ProcessManager.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> {
const env: Record<string, string> = {};
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<string, string> {
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";
Expand Down Expand Up @@ -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);

Expand Down
29 changes: 27 additions & 2 deletions tests/ProcessManager.test.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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",
});
});
});
});