From c73345e3cf3e1849eaf724451fedefdb5013579c Mon Sep 17 00:00:00 2001 From: Serhii Vecherenko Date: Sat, 25 Jul 2026 15:27:27 -0700 Subject: [PATCH] fix(supervisor): resolve executable Scoop shims on Windows --- pnpm-lock.yaml | 4 +- .../agents/base.windows-path.test.ts | 38 +++++++++++++++++++ src/supervisor/agents/base/processRuntime.ts | 18 ++++++++- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 727a19fdb..119a4287a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -86,7 +86,7 @@ importers: version: 3.2.2(@react-spectrum/provider@3.11.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.2) '@heroui/styles': specifier: ^3.2.2 - version: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.3.2) + version: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.2) '@huggingface/transformers': specifier: ^4.2.0 version: 4.2.0(@types/node@25.9.5) @@ -414,7 +414,7 @@ importers: version: 3.2.2(@react-spectrum/provider@3.11.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(tailwindcss@4.3.2) '@heroui/styles': specifier: 3.2.2 - version: 3.2.2(tailwind-merge@3.6.0)(tailwindcss@4.3.2) + version: 3.2.2(tailwind-merge@3.4.0)(tailwindcss@4.3.2) '@vercel/analytics': specifier: ^2.0.1 version: 2.0.1(next@16.2.11(@babel/core@7.29.7)(@opentelemetry/api@1.9.1)(@types/node@25.9.5)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.7(react@19.2.7))(react@19.2.7))(react@19.2.7) diff --git a/src/supervisor/agents/base.windows-path.test.ts b/src/supervisor/agents/base.windows-path.test.ts index ebebff232..fe0a3de7b 100644 --- a/src/supervisor/agents/base.windows-path.test.ts +++ b/src/supervisor/agents/base.windows-path.test.ts @@ -153,6 +153,44 @@ describe.skipIf(process.platform !== "win32")("Windows executable path fallback" expect(resolveExecutablePath("claude")).toBe(exePath); }); + it("resolves Scoop .exe shims to their executable target", () => { + const root = mkdtempSync(join(tmpdir(), "poracode-scoop-shim-")); + tempDirs.push(root); + const shimDir = join(root, "scoop", "shims"); + const target = join(root, "scoop", "apps", "opencode", "current", "opencode.exe"); + mkdirSync(shimDir, { recursive: true }); + mkdirSync(join(target, ".."), { recursive: true }); + writeFileSync(join(shimDir, "opencode.exe"), ""); + writeFileSync(join(shimDir, "opencode.shim"), `path = "${target}"\n`); + writeFileSync(target, ""); + spawnSyncMock.mockReturnValueOnce({ + error: undefined, + status: 0, + stdout: `${join(shimDir, "opencode.exe")}\r\n`, + stderr: "", + }); + + expect(resolveExecutablePath("opencode")).toBe(target); + }); + + it("keeps Scoop shims that supply fixed arguments", () => { + const root = mkdtempSync(join(tmpdir(), "poracode-scoop-args-shim-")); + tempDirs.push(root); + const shim = join(root, "tool.exe"); + const target = join(root, "target.exe"); + writeFileSync(shim, ""); + writeFileSync(join(root, "tool.shim"), `path = "${target}"\nargs = "--fixed"\n`); + writeFileSync(target, ""); + spawnSyncMock.mockReturnValueOnce({ + error: undefined, + status: 0, + stdout: `${shim}\r\n`, + stderr: "", + }); + + expect(resolveExecutablePath("tool")).toBe(shim); + }); + it("keeps the .cmd path for npm node-shim wrappers (e.g. command-code.cmd → node index.mjs)", () => { // Regression: a previous version of resolveWindowsCmdExeTarget greedily // matched `"%dp0%\node.exe"` in npm's standard Node-script shim and diff --git a/src/supervisor/agents/base/processRuntime.ts b/src/supervisor/agents/base/processRuntime.ts index b9654d50a..4523844b0 100644 --- a/src/supervisor/agents/base/processRuntime.ts +++ b/src/supervisor/agents/base/processRuntime.ts @@ -284,7 +284,23 @@ function parseWindowsExecutablePath(stdout: string): string | undefined { .filter((line) => line.length > 0); const resolved = lines.findLast((line) => /\.(?:bat|cmd|com|exe|ps1)$/i.test(line)) ?? lines.at(-1); - return resolveWindowsCmdExeTarget(resolved) ?? resolved; + return ( + resolveWindowsScoopShimTarget(resolved) ?? resolveWindowsCmdExeTarget(resolved) ?? resolved + ); +} + +function resolveWindowsScoopShimTarget(path: string | undefined): string | undefined { + if (!path || !/\.exe$/i.test(path)) return undefined; + const shimPath = path.replace(/\.exe$/i, ".shim"); + if (!existsSync(shimPath)) return undefined; + try { + const body = readFileSync(shimPath, "utf8"); + if (/^\s*args\s*=/im.test(body)) return undefined; + const target = /^\s*path\s*=\s*"([^"]+)"\s*$/im.exec(body)?.[1]; + return target && /\.(?:com|exe)$/i.test(target) && existsSync(target) ? target : undefined; + } catch { + return undefined; + } } /**