From 7cb17860c6791a7151a61aebe4e6038beb04661a Mon Sep 17 00:00:00 2001 From: Yurii Chukhlib Date: Mon, 26 Jan 2026 11:50:04 +0100 Subject: [PATCH] fix(infra): use 'where' instead of 'which' on Windows for binary detection Fixes #2172 On Windows, the `which` command doesn't exist; Windows uses `where.exe` instead. This caused skill detection to fail for skills that require CLI binaries (e.g., blogwatcher, yt-dlp, ffmpeg). This change follows the existing pattern used elsewhere in the codebase: - src/daemon/program-args.ts:140 - src/commands/onboard-helpers.ts:304 Test coverage added for both Unix and Windows platforms. --- src/infra/binaries.test.ts | 96 +++++++++++++++++++++++++++++--------- src/infra/binaries.ts | 4 +- 2 files changed, 77 insertions(+), 23 deletions(-) diff --git a/src/infra/binaries.test.ts b/src/infra/binaries.test.ts index 50b48d372..896fa26be 100644 --- a/src/infra/binaries.test.ts +++ b/src/infra/binaries.test.ts @@ -1,34 +1,86 @@ -import { describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import type { runExec } from "../process/exec.js"; import type { RuntimeEnv } from "../runtime.js"; import { ensureBinary } from "./binaries.js"; +const originalPlatform = process.platform; + describe("ensureBinary", () => { - it("passes through when binary exists", async () => { - const exec: typeof runExec = vi.fn().mockResolvedValue({ - stdout: "", - stderr: "", + afterEach(() => { + // Restore original platform after each test + Object.defineProperty(process, "platform", { + value: originalPlatform, }); - const runtime: RuntimeEnv = { - log: vi.fn(), - error: vi.fn(), - exit: vi.fn(), - }; - await ensureBinary("node", exec, runtime); - expect(exec).toHaveBeenCalledWith("which", ["node"]); }); - it("logs and exits when missing", async () => { - const exec: typeof runExec = vi.fn().mockRejectedValue(new Error("missing")); - const error = vi.fn(); - const exit = vi.fn(() => { - throw new Error("exit"); + describe("on Unix", () => { + beforeEach(() => { + Object.defineProperty(process, "platform", { + value: "linux", + }); + }); + + it("passes through when binary exists", async () => { + const exec: typeof runExec = vi.fn().mockResolvedValue({ + stdout: "", + stderr: "", + }); + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + await ensureBinary("node", exec, runtime); + expect(exec).toHaveBeenCalledWith("which", ["node"]); + }); + + it("logs and exits when missing", async () => { + const exec: typeof runExec = vi.fn().mockRejectedValue(new Error("missing")); + const error = vi.fn(); + const exit = vi.fn(() => { + throw new Error("exit"); + }); + await expect(ensureBinary("ghost", exec, { log: vi.fn(), error, exit })).rejects.toThrow( + "exit", + ); + expect(error).toHaveBeenCalledWith("Missing required binary: ghost. Please install it."); + expect(exit).toHaveBeenCalledWith(1); + }); + }); + + describe("on Windows", () => { + beforeEach(() => { + Object.defineProperty(process, "platform", { + value: "win32", + }); + }); + + it("passes through when binary exists", async () => { + const exec: typeof runExec = vi.fn().mockResolvedValue({ + stdout: "", + stderr: "", + }); + const runtime: RuntimeEnv = { + log: vi.fn(), + error: vi.fn(), + exit: vi.fn(), + }; + await ensureBinary("node", exec, runtime); + expect(exec).toHaveBeenCalledWith("where", ["node"]); + }); + + it("logs and exits when missing", async () => { + const exec: typeof runExec = vi.fn().mockRejectedValue(new Error("missing")); + const error = vi.fn(); + const exit = vi.fn(() => { + throw new Error("exit"); + }); + await expect(ensureBinary("ghost", exec, { log: vi.fn(), error, exit })).rejects.toThrow( + "exit", + ); + expect(error).toHaveBeenCalledWith("Missing required binary: ghost. Please install it."); + expect(exit).toHaveBeenCalledWith(1); }); - await expect(ensureBinary("ghost", exec, { log: vi.fn(), error, exit })).rejects.toThrow( - "exit", - ); - expect(error).toHaveBeenCalledWith("Missing required binary: ghost. Please install it."); - expect(exit).toHaveBeenCalledWith(1); }); }); diff --git a/src/infra/binaries.ts b/src/infra/binaries.ts index 492cf5a9c..0ee7195a4 100644 --- a/src/infra/binaries.ts +++ b/src/infra/binaries.ts @@ -7,7 +7,9 @@ export async function ensureBinary( runtime: RuntimeEnv = defaultRuntime, ): Promise { // Abort early if a required CLI tool is missing. - await exec("which", [name]).catch(() => { + // Windows uses 'where', Unix uses 'which' + const command = process.platform === "win32" ? "where" : "which"; + await exec(command, [name]).catch(() => { runtime.error(`Missing required binary: ${name}. Please install it.`); runtime.exit(1); });