Skip to content
Open
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
7 changes: 4 additions & 3 deletions src/config/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import os from "node:os"
import path from "node:path"
import { resolveNpmCliCommand } from "../utils/resolve-cli-command.js"
import {
claudeCodeHttpCommand,
claudeCodeStdioCommand,
Expand Down Expand Up @@ -137,7 +138,7 @@ const CLIENTS: Record<string, ClientDefinition> = {
label: "Claude Code",
install: {
method: "command",
command: "claude",
command: resolveNpmCliCommand("claude"),
templates: {
stdio: claudeCodeStdioCommand,
http: claudeCodeHttpCommand,
Expand Down Expand Up @@ -183,7 +184,7 @@ const CLIENTS: Record<string, ClientDefinition> = {
label: "Gemini CLI",
install: {
method: "command",
command: "gemini",
command: resolveNpmCliCommand("gemini"),
templates: {
stdio: geminiCliStdioCommand,
http: geminiCliHttpCommand,
Expand All @@ -198,7 +199,7 @@ const CLIENTS: Record<string, ClientDefinition> = {
label: "Codex",
install: {
method: "command",
command: "codex",
command: resolveNpmCliCommand("codex"),
templates: {
stdio: codexStdioCommand,
http: codexHttpCommand,
Expand Down
55 changes: 55 additions & 0 deletions src/lib/__tests__/client-config-io/command-install.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { beforeEach, describe, expect, test, vi } from "vitest"
import type { ClientDefinition } from "../../../config/clients"
import { runConfigCommand } from "../../client-config-io"

const { execFileSync } = vi.hoisted(() => ({
execFileSync: vi.fn(),
}))

vi.mock("node:child_process", () => ({
execFileSync,
}))

vi.mock("../../lib/logger", () => ({
verbose: vi.fn(),
}))

describe("runConfigCommand", () => {
const clientConfig: ClientDefinition = {
label: "Claude Code",
install: {
method: "command",
command: "claude",
templates: {
stdio: (_name, command, args) => ["mcp", "add", command, ...args],
},
},
transports: { stdio: {} },
}

beforeEach(() => {
vi.clearAllMocks()
})

test("throws a client-aware error when the CLI binary is missing", () => {
const error = Object.assign(new Error("spawn claude ENOENT"), {
code: "ENOENT",
})
execFileSync.mockImplementation(() => {
throw error
})

expect(() =>
runConfigCommand(
{
mcpServers: {
gmail: { command: "npx", args: ["-y", "gmail-mcp"] },
},
},
clientConfig,
),
).toThrow(
"Could not run Claude Code: 'claude' was not found on PATH. Install the Claude Code CLI and ensure it is available in your shell.",
)
})
})
2 changes: 1 addition & 1 deletion src/lib/client-config-io.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,7 @@ export function runConfigCommand(

if (error && (error as NodeJS.ErrnoException).code === "ENOENT") {
throw new Error(
`Command '${command}' not found. Make sure ${command} is installed and on your PATH`,
`Could not run ${clientConfig.label}: '${command}' was not found on PATH. Install the ${clientConfig.label} CLI and ensure it is available in your shell.`,
)
}

Expand Down
18 changes: 18 additions & 0 deletions src/utils/__tests__/resolve-cli-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { afterEach, describe, expect, test, vi } from "vitest"
import { resolveNpmCliCommand } from "../resolve-cli-command"

describe("resolveNpmCliCommand", () => {
afterEach(() => {
vi.restoreAllMocks()
})

test("appends .cmd on Windows", () => {
vi.spyOn(process, "platform", "get").mockReturnValue("win32")
expect(resolveNpmCliCommand("claude")).toBe("claude.cmd")
})

test("keeps bare command names on non-Windows platforms", () => {
vi.spyOn(process, "platform", "get").mockReturnValue("linux")
expect(resolveNpmCliCommand("claude")).toBe("claude")
})
})
7 changes: 7 additions & 0 deletions src/utils/resolve-cli-command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* npm global CLIs on Windows are exposed as `.cmd` shims.
* execFileSync does not apply PATHEXT, so the shim suffix is required.
*/
export function resolveNpmCliCommand(command: string): string {
return process.platform === "win32" ? `${command}.cmd` : command
}