From cf0dfa9de0027003189d3b19ba563f42c0598217 Mon Sep 17 00:00:00 2001 From: 4ndreello <4ndreello@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:58:53 -0300 Subject: [PATCH] fix(opencode): Detect managed v2 installation Look for OpenCode's managed v2 binary when the daemon PATH predates the installation. Reuse the resolved path for driver spawns so detection and execution use the same binary. Co-Authored-By: Codex --- src/drivers/opencode/driver.ts | 30 ++++++++++++++++++--- tests/opencode-driver.test.ts | 49 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) create mode 100644 tests/opencode-driver.test.ts diff --git a/src/drivers/opencode/driver.ts b/src/drivers/opencode/driver.ts index 328a19b..9cc7191 100644 --- a/src/drivers/opencode/driver.ts +++ b/src/drivers/opencode/driver.ts @@ -1,3 +1,6 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { detectBinary, runCommandWithTimeout } from "../helpers.js"; import type { AgentInstallation, StartOptions } from "../../core/driver.js"; import type { AgentCapabilities } from "../../core/capabilities.js"; @@ -5,8 +8,18 @@ import type { ListModelsOptions, ModelInfo, ProviderModels } from "../../core/mo import { createRuntimeHooks, SessionDriver } from "../session-driver.js"; import { parseOpencodeLine } from "./parser.js"; +function opencodeV2BinaryPath(homeDir: string = os.homedir()): string { + return path.join(homeDir, ".opencode", "bin", "opencode"); +} + +function resolveOpencodeCommand(homeDir: string = os.homedir()): string { + const localBin = opencodeV2BinaryPath(homeDir); + return fs.existsSync(localBin) ? localBin : "opencode"; +} + export class OpencodeDriver extends SessionDriver { readonly id = "opencode" as const; + private detectedPath?: string; protected readonly hooks = createRuntimeHooks({ parse: parseOpencodeLine, @@ -16,6 +29,10 @@ export class OpencodeDriver extends SessionDriver { protected readonly resumeError = "No native session id for Opencode resume"; + protected override getCommand(): string { + return this.detectedPath ?? resolveOpencodeCommand(); + } + protected buildArgs(options: StartOptions): string[] { const args: string[] = ["run", "--format", "json"]; if (options.model) args.push("--model", options.model); @@ -40,8 +57,16 @@ export class OpencodeDriver extends SessionDriver { } async detect(): Promise { - const res = await detectBinary("opencode"); - if (!res.installed) return { installed: false, error: "opencode binary not found" }; + let res = await detectBinary("opencode"); + if (!res.installed) { + const localBin = opencodeV2BinaryPath(); + if (fs.existsSync(localBin)) res = await detectBinary(localBin); + } + if (!res.installed || !res.path) { + this.detectedPath = undefined; + return { installed: false, error: "opencode binary not found" }; + } + this.detectedPath = res.path; return { installed: true, path: res.path, version: res.version, details: "opencode run --format json" }; } @@ -92,4 +117,3 @@ export class OpencodeDriver extends SessionDriver { } } } - diff --git a/tests/opencode-driver.test.ts b/tests/opencode-driver.test.ts new file mode 100644 index 0000000..6b2bea9 --- /dev/null +++ b/tests/opencode-driver.test.ts @@ -0,0 +1,49 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../src/drivers/helpers.js", async (importOriginal) => { + const mod = await importOriginal(); + return { ...mod, detectBinary: vi.fn() }; +}); + +import { OpencodeDriver } from "../src/drivers/opencode/driver.js"; +import { detectBinary } from "../src/drivers/helpers.js"; + +const mockedDetect = vi.mocked(detectBinary); + +afterEach(() => { + vi.clearAllMocks(); + vi.unstubAllEnvs(); +}); + +describe("OpencodeDriver", () => { + it("detects and launches the managed OpenCode v2 binary when PATH is stale", async () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "codedeck-opencode-")); + const binary = path.join(home, ".opencode", "bin", "opencode"); + fs.mkdirSync(path.dirname(binary), { recursive: true }); + fs.writeFileSync(binary, ""); + vi.stubEnv("HOME", home); + + mockedDetect + .mockResolvedValueOnce({ installed: false }) + .mockResolvedValueOnce({ installed: true, path: binary, version: "opencode v2.0.12" }); + + try { + const driver = new OpencodeDriver(); + + expect((driver as any).getCommand()).toBe(binary); + await expect(driver.detect()).resolves.toMatchObject({ + installed: true, + path: binary, + version: "opencode v2.0.12", + }); + expect(mockedDetect).toHaveBeenNthCalledWith(1, "opencode"); + expect(mockedDetect).toHaveBeenNthCalledWith(2, binary); + expect((driver as any).getCommand()).toBe(binary); + } finally { + fs.rmSync(home, { recursive: true, force: true }); + } + }); +});