From 58900c0c3360b1e6dde6ac1a265cdbd43bb551d0 Mon Sep 17 00:00:00 2001 From: joyawang <13715852+JoyaWang@users.noreply.github.com> Date: Mon, 23 Feb 2026 13:48:26 +0800 Subject: [PATCH] fix: resolve PATH and health check issues for macOS GUI environment macOS GUI apps (Obsidian/Electron) inherit a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin) that doesn't include Homebrew, nvm, bun, etc. Since the opencode script uses `#!/usr/bin/env node`, spawning it from Obsidian fails with exit code 127 (node not found). Changes: - Build enhanced PATH for child processes by reusing the search directories already defined in ExecutableResolver - Fix health check URL to use /global/health (without base64 project path prefix which hits the SPA catch-all and returns HTML) - Validate health response JSON (check data.healthy === true) - Replace btoa() with Buffer.from().toString("base64") to support non-Latin1 characters in project paths (fixes #28) - Increase default startupTimeout from 15s to 120s since first boot may need 30-60s for DB migration and plugin installation Closes #28, closes #29 Co-Authored-By: Claude Opus 4.6 --- src/server/ExecutableResolver.ts | 2 +- src/server/ServerManager.ts | 57 ++++++++++++++++++++++++++++---- src/types.ts | 2 +- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/src/server/ExecutableResolver.ts b/src/server/ExecutableResolver.ts index b0c13ab..2124cde 100644 --- a/src/server/ExecutableResolver.ts +++ b/src/server/ExecutableResolver.ts @@ -66,7 +66,7 @@ export class ExecutableResolver { /** * Get platform-specific directories to search for executables */ - private static getSearchDirectories(): string[] { + static getSearchDirectories(): string[] { const currentPlatform = platform(); const homeDir = homedir(); const searchDirs: string[] = []; diff --git a/src/server/ServerManager.ts b/src/server/ServerManager.ts index f4aa368..67b1fee 100644 --- a/src/server/ServerManager.ts +++ b/src/server/ServerManager.ts @@ -1,4 +1,6 @@ import { ChildProcess, SpawnOptions } from "child_process"; +import { existsSync } from "fs"; +import { dirname } from "path"; import { EventEmitter } from "events"; import { OpenCodeSettings } from "../types"; import { ServerState } from "./types"; @@ -43,9 +45,13 @@ export class ServerManager extends EventEmitter { return this.lastError; } + getBaseUrl(): string { + return `http://${this.settings.hostname}:${this.settings.port}`; + } + getUrl(): string { - const encodedPath = btoa(this.projectDirectory); - return `http://${this.settings.hostname}:${this.settings.port}/${encodedPath}`; + const encodedPath = Buffer.from(this.projectDirectory).toString("base64"); + return `${this.getBaseUrl()}/${encodedPath}`; } async start(): Promise { @@ -77,16 +83,21 @@ export class ServerManager extends EventEmitter { } else { // Path mode: resolve executable and verify executablePath = ExecutableResolver.resolve(this.settings.opencodePath); - + // Pre-flight check: verify executable exists (only for path mode) const commandError = await this.processImpl.verifyCommand(executablePath); if (commandError) { return this.setError(commandError); } - + + // Build enhanced PATH: macOS/Linux GUI apps (Electron) inherit a minimal + // PATH that doesn't include Homebrew, nvm, bun, etc. The opencode script + // uses #!/usr/bin/env node, so node must be discoverable via PATH. + const enhancedPath = this.buildEnhancedPath(executablePath); + spawnOptions = { cwd: this.projectDirectory, - env: { ...process.env, NODE_USE_SYSTEM_CA: "1" }, + env: { ...process.env, NODE_USE_SYSTEM_CA: "1", PATH: enhancedPath }, stdio: ["ignore", "pipe", "pipe"], }; } @@ -224,11 +235,13 @@ export class ServerManager extends EventEmitter { private async checkServerHealth(): Promise { try { - const response = await fetch(`${this.getUrl()}/global/health`, { + const response = await fetch(`${this.getBaseUrl()}/global/health`, { method: "GET", signal: AbortSignal.timeout(2000), }); - return response.ok; + if (!response.ok) return false; + const data = await response.json(); + return data?.healthy === true; } catch { return false; } @@ -256,4 +269,34 @@ export class ServerManager extends EventEmitter { private sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } + + /** + * Build an enhanced PATH for child processes. + * macOS/Linux GUI apps (like Obsidian via Electron) inherit a minimal PATH + * (e.g. /usr/bin:/bin:/usr/sbin:/sbin) that doesn't include Homebrew, nvm, + * bun, etc. Since the opencode script uses `#!/usr/bin/env node`, the + * `node` binary must be discoverable via PATH. + */ + private buildEnhancedPath(resolvedExecutable: string): string { + const currentPath = process.env.PATH || ""; + const extraDirs: string[] = []; + + // Add the directory containing the resolved executable itself + try { + const binDir = dirname(resolvedExecutable); + if (binDir && !currentPath.includes(binDir)) { + extraDirs.push(binDir); + } + } catch { /* ignore */ } + + // Add well-known directories from ExecutableResolver + for (const dir of ExecutableResolver.getSearchDirectories()) { + if (!currentPath.includes(dir) && existsSync(dir)) { + extraDirs.push(dir); + } + } + + if (extraDirs.length === 0) return currentPath; + return [...extraDirs, currentPath].join(":"); + } } diff --git a/src/types.ts b/src/types.ts index 1f95282..b9110a5 100644 --- a/src/types.ts +++ b/src/types.ts @@ -21,7 +21,7 @@ export const DEFAULT_SETTINGS: OpenCodeSettings = { autoStart: false, opencodePath: "opencode", projectDirectory: "", - startupTimeout: 15000, + startupTimeout: 120000, defaultViewLocation: "sidebar", injectWorkspaceContext: false, maxNotesInContext: 20,