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
17 changes: 11 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,17 @@
"configuration": {
"title": "OpenCode",
"properties": {
"opencode.serverPort": {
"type": "number",
"default": 0,
"description": "Port for OpenCode server (0 = auto-assign)"
},
"opencode.autoStart": {
"opencode.serverPort": {
"type": "number",
"default": 0,
"description": "Port for OpenCode server (0 = auto-assign)"
},
"opencode.cliPath": {
"type": "string",
"default": "",
"description": "Optional absolute path to the OpenCode CLI executable when it is not visible to the VS Code extension host PATH."
},
"opencode.autoStart": {
"type": "boolean",
"default": true,
"description": "Automatically start OpenCode server on activation"
Expand Down
71 changes: 60 additions & 11 deletions src/services/OpencodeServerManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ import * as vscode from "vscode";
import * as cp from "child_process";
import * as net from "net";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import { createOpencodeClient, OpencodeClient } from "@opencode-ai/sdk/v2";
import { createLogger } from "../utils/Logger";
import { LoggingCategories } from "../utils/LoggingSchema";
Expand Down Expand Up @@ -308,23 +310,38 @@ export class OpencodeServerManager {
return this.opencodeBinaryPath;
}

let opencodeBinary = "opencode";
const configuredBinary = vscode.workspace
.getConfiguration("opencode")
.get<string>("cliPath", "")
.trim();
if (configuredBinary) {
this.opencodeBinaryPath = this.normalizeWindowsExecutablePath(configuredBinary);
log.info("Using configured opencode CLI path", {
path: this.opencodeBinaryPath,
});
return this.opencodeBinaryPath;
}

let opencodeBinary: string | undefined;
try {
const resolverCommand =
process.platform === "win32" ? "where opencode" : "which opencode";
const resolverResult = cp
.execSync(resolverCommand, {
const resolverResult = cp.execFileSync(
process.platform === "win32"
? "where"
: process.platform === "darwin"
? "/bin/zsh"
: "/bin/sh",
process.platform === "win32" ? ["opencode"] : ["-lc", "command -v opencode"],
{
encoding: "utf-8",
stdio: ["ignore", "pipe", "ignore"],
})
},
)
.trim()
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
if (resolverResult.length > 0) {
opencodeBinary = this.normalizeWindowsExecutablePath(
resolverResult[0],
);
opencodeBinary = this.normalizeWindowsExecutablePath(resolverResult[0]);
log.debug("Resolved opencode binary path", { path: opencodeBinary });
}
} catch (error) {
Expand All @@ -334,8 +351,40 @@ export class OpencodeServerManager {
);
}

this.opencodeBinaryPath = opencodeBinary;
return opencodeBinary;
if (!opencodeBinary) {
const home = os.homedir();
const candidatePaths = process.platform === "win32"
? [
path.join(home, ".opencode", "bin", "opencode.exe"),
path.join(home, ".bun", "bin", "opencode.exe"),
path.join(home, "AppData", "Roaming", "npm", "opencode.cmd"),
]
: [
path.join(home, ".opencode", "bin", "opencode"),
path.join(home, ".bun", "bin", "opencode"),
path.join(home, ".local", "bin", "opencode"),
"/opt/homebrew/bin/opencode",
"/usr/local/bin/opencode",
"/usr/bin/opencode",
];
const existingCandidate = candidatePaths.find((candidate) => {
try {
fs.accessSync(candidate, fs.constants.X_OK);
return true;
} catch {
return false;
}
});
if (existingCandidate) {
opencodeBinary = this.normalizeWindowsExecutablePath(existingCandidate);
log.info("Resolved opencode CLI from fallback install locations", {
path: opencodeBinary,
});
}
}

this.opencodeBinaryPath = opencodeBinary ?? "opencode";
return this.opencodeBinaryPath;
}

private probeOpencodeBinaryVersion(binaryPath: string): string | undefined {
Expand Down
18 changes: 18 additions & 0 deletions tests/services/opencode-version-compatibility.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,21 @@ test("server manager still probes the CLI after a failed SDK health request", ()
"a healthy compatibility endpoint must not bypass the CLI version fallback",
);
});

test("server manager resolves CLI paths for GUI-launched VS Code hosts", () => {
assert.match(
serverManagerSource,
/getConfiguration\("opencode"\)[\s\S]*get<string>\("cliPath"/,
"users must be able to configure an absolute CLI path",
);
assert.match(
serverManagerSource,
/command -v opencode/,
"CLI lookup should also load the shell command path used outside VS Code",
);
assert.match(
serverManagerSource,
/\.opencode[\s\S]*\.bun[\s\S]*\.local[\s\S]*homebrew/,
"CLI lookup should cover standard user and Homebrew install locations",
);
});
Loading