Skip to content
Open
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
2 changes: 1 addition & 1 deletion src/server/ExecutableResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [];
Expand Down
57 changes: 50 additions & 7 deletions src/server/ServerManager.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<boolean> {
Expand Down Expand Up @@ -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"],
};
}
Expand Down Expand Up @@ -224,11 +235,13 @@ export class ServerManager extends EventEmitter {

private async checkServerHealth(): Promise<boolean> {
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;
}
Expand Down Expand Up @@ -256,4 +269,34 @@ export class ServerManager extends EventEmitter {
private sleep(ms: number): Promise<void> {
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(":");
}
}
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const DEFAULT_SETTINGS: OpenCodeSettings = {
autoStart: false,
opencodePath: "opencode",
projectDirectory: "",
startupTimeout: 15000,
startupTimeout: 120000,
defaultViewLocation: "sidebar",
injectWorkspaceContext: false,
maxNotesInContext: 20,
Expand Down
Loading