Skip to content
Merged
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
19 changes: 16 additions & 3 deletions src/cli/model-discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,23 @@ export function parseCursorModelsOutput(output: string): DiscoveredModel[] {
return models;
}

export function discoverModelsFromCursorAgent(): DiscoveredModel[] {
const raw = execFileSync(resolveCursorAgentBinary(), ["models"], {
export type DiscoverDeps = {
platform?: NodeJS.Platform;
execFileSync?: typeof execFileSync;
resolveBinary?: () => string;
};

export function discoverModelsFromCursorAgent(deps: DiscoverDeps = {}): DiscoveredModel[] {
const platform = deps.platform ?? process.platform;
const exec = deps.execFileSync ?? execFileSync;
const resolveBinary = deps.resolveBinary ?? resolveCursorAgentBinary;

// On Windows cursor-agent is a .cmd shim, which requires shell mode when
// spawned from Node (execFileSync of a bare .cmd fails with EINVAL).
const raw = exec(resolveBinary(), ["models"], {
encoding: "utf8",
...(process.platform !== "win32" && { killSignal: "SIGTERM" as const }),
shell: platform === "win32",
...(platform !== "win32" && { killSignal: "SIGTERM" as const }),
stdio: ["ignore", "pipe", "pipe"],
timeout: MODEL_DISCOVERY_TIMEOUT_MS,
});
Expand Down
6 changes: 5 additions & 1 deletion src/cli/opencode-cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,10 @@ export function checkBun(): CheckResult {

export function checkCursorAgent(): CheckResult {
try {
const output = execFileSync(resolveCursorAgentBinary(), ["--version"], { encoding: "utf8" }).trim();
const output = execFileSync(resolveCursorAgentBinary(), ["--version"], {
encoding: "utf8",
shell: process.platform === "win32",
}).trim();
const version = output.split("\n")[0] || "installed";
return { name: "cursor-agent", passed: true, message: version };
} catch {
Expand All @@ -134,6 +137,7 @@ export function checkCursorAgentLogin(): CheckResult {
// Try running a command that requires auth
execFileSync(resolveCursorAgentBinary(), ["models"], {
encoding: "utf8",
shell: process.platform === "win32",
stdio: ["ignore", "pipe", "pipe"],
timeout: 3000,
});
Expand Down
44 changes: 43 additions & 1 deletion tests/unit/cli/model-discovery.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { describe, expect, it } from "bun:test";
import { parseCursorModelsOutput } from "../../../src/cli/model-discovery.js";
import {
discoverModelsFromCursorAgent,
parseCursorModelsOutput,
} from "../../../src/cli/model-discovery.js";

describe("cli/model-discovery", () => {
it("parses model ids and names from cursor-agent output", () => {
Expand Down Expand Up @@ -28,3 +31,42 @@ auto - Auto
expect(models).toEqual([{ id: "auto", name: "Auto" }]);
});
});

describe("cli/model-discovery discoverModelsFromCursorAgent", () => {
it("runs cursor-agent with shell mode on Windows", () => {
const calls: Array<{ cmd: string; args: string[]; opts: any }> = [];
const exec = (cmd: string, args: string[], opts: any) => {
calls.push({ cmd, args, opts });
return "auto - Auto\n";
};

const models = discoverModelsFromCursorAgent({
platform: "win32",
execFileSync: exec as any,
resolveBinary: () => "C:\\cursor-agent\\cursor-agent.cmd",
});

expect(models).toEqual([{ id: "auto", name: "Auto" }]);
expect(calls).toHaveLength(1);
expect(calls[0].cmd).toBe("C:\\cursor-agent\\cursor-agent.cmd");
expect(calls[0].args).toEqual(["models"]);
expect(calls[0].opts.shell).toBe(true);
});

it("does not use shell mode on non-Windows platforms", () => {
const calls: Array<{ cmd: string; args: string[]; opts: any }> = [];
const exec = (cmd: string, args: string[], opts: any) => {
calls.push({ cmd, args, opts });
return "auto - Auto\n";
};

discoverModelsFromCursorAgent({
platform: "linux",
execFileSync: exec as any,
resolveBinary: () => "/usr/local/bin/cursor-agent",
});

expect(calls[0].opts.shell).toBe(false);
expect(calls[0].opts.killSignal).toBe("SIGTERM");
});
});
Loading