From f83f9cc62f0c753661f7da0cacd1b0107990b682 Mon Sep 17 00:00:00 2001 From: danieliyahu1 Date: Fri, 10 Jul 2026 11:20:40 +0300 Subject: [PATCH] fix(process): resolve opencode.exe directly from PATH when no .cmd shim exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When opencode is installed via non-npm methods (install script, scoop, choco, manual download), only opencode.exe is on PATH — no .cmd shim exists. The existing code only searched for opencode.cmd, so it fell back to cmd.exe /c opencode, which still shows a visible console window. Fix: add a first pass that checks each PATH entry for opencode.exe directly before falling through to the .cmd resolution logic. Adds a test that isolates PATH to only the temp dir with opencode.exe (no .cmd shim) and asserts the resolved exe path is returned. --- src/opencode/process.ts | 13 +++++++++++-- tests/opencode/process.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/opencode/process.ts b/src/opencode/process.ts index 40a41d2e..f8967ca4 100644 --- a/src/opencode/process.ts +++ b/src/opencode/process.ts @@ -46,11 +46,20 @@ export function resolveLocalOpencodeTarget(apiUrl: string): LocalOpencodeTarget } function resolveWindowsOpencodeExe(): string { - // npm on Windows usually puts opencode.cmd on PATH (not opencode.exe). - // We locate the shim and derive the real exe path from its directory. const pathEnv = process.env.PATH ?? ""; const pathEntries = pathEnv.split(path.delimiter).filter(Boolean); + // First pass: look for opencode.exe directly on PATH. + // Covers non-npm installations (install script, scoop, choco, manual download, etc.). + for (const entry of pathEntries) { + const candidateExe = path.join(entry, "opencode.exe"); + if (existsSync(candidateExe)) { + return candidateExe; + } + } + + // Second pass: look for opencode.cmd (npm global install). + // Derive the real exe path from the shim location. for (const entry of pathEntries) { const opencodeCmd = path.join(entry, "opencode.cmd"); if (!existsSync(opencodeCmd)) { diff --git a/tests/opencode/process.test.ts b/tests/opencode/process.test.ts index 0d3ef09c..920cf012 100644 --- a/tests/opencode/process.test.ts +++ b/tests/opencode/process.test.ts @@ -76,6 +76,35 @@ describe("opencode/process", () => { } }); + it("resolves opencode.exe directly from PATH when no .cmd shim exists", () => { + if (process.platform !== "win32") { + return; + } + + const originalPath = process.env.PATH; + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-telegram-bot-")); + const binDir = path.join(tempRoot, "bin"); + const exePath = path.join(binDir, "opencode.exe"); + + try { + fs.mkdirSync(binDir, { recursive: true }); + fs.writeFileSync(exePath, "", "utf8"); + + // Isolate PATH to only the temp dir — no npm .cmd shim on PATH + process.env.PATH = binDir; + + const command = createOpencodeServeSpawnCommand({ host: "localhost", port: 4987 }); + expect(command).toEqual({ + command: exePath, + args: ["serve", "--port", "4987"], + windowsHide: true, + }); + } finally { + process.env.PATH = originalPath; + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + it("uses resolved opencode.exe on Windows when opencode.cmd is on PATH and exe exists", () => { if (process.platform !== "win32") { return;