Files
fusion/plugins/fusion-plugin-cursor-runtime/src/probe.ts
gsxdsm abb1917999 FN-7419: add Cursor CLI binary path override
Add a global Cursor CLI binary path override that is validated before enabling or probing Cursor routing.

- Add global settings schema/types and API support for storing a trimmed Cursor CLI binary path override.
- Surface a Settings Authentication control to save, clear, and test the Cursor CLI binary path.
- Probe configured Cursor binaries before PATH fallbacks and expose diagnostics for status/routes.
- Cover settings, route, dashboard, and cursor runtime behavior with focused tests and documentation.

Files changed:
 .changeset/fn-7419-cursor-cli-binary-path.md       |   7 ++
 docs/cursor-cli-contract.md                        |  33 ++++--
 docs/settings-reference.md                         |   2 +
 .../core/src/__tests__/cursor-cli-settings.test.ts |  34 ++++++
 packages/core/src/settings-schema.ts               |   6 +
 packages/core/src/types.ts                         |   8 ++
 packages/dashboard/app/api/legacy.ts               |  17 ++-
 .../app/components/CursorCliProviderCard.css       |  37 +++++-
 .../app/components/CursorCliProviderCard.tsx       |  78 ++++++++++++-
 .../__tests__/ModelOnboardingModal.test.tsx        |   4 +
 .../__tests__/SettingsModal.general.test.tsx       |   2 +
 .../__tests__/SettingsModal.models-auth.test.tsx   |  75 ++++++++++++
 .../SettingsModal.remote-notifications.test.tsx    |   2 +
 .../SettingsModal.scheduling-merge.test.tsx        |   2 +
 .../__tests__/SettingsModal.test-harness.tsx       |   3 +
 .../dashboard/src/__tests__/routes-auth.test.ts    | 130 ++++++++++++++++++++-
 .../dashboard/src/routes/register-auth-routes.ts   |  69 +++++++++--
 .../src/__tests__/probe.test.ts                    |  61 +++++++++-
 .../src/__tests__/process-manager.test.ts          |  10 ++
 .../src/__tests__/provider.test.ts                 |  57 +++++++++
 plugins/fusion-plugin-cursor-runtime/src/probe.ts  |  39 +++++--
 .../fusion-plugin-cursor-runtime/src/provider.ts   |  15 ++-
 plugins/fusion-plugin-cursor-runtime/src/types.ts  |   3 +
 23 files changed, 655 insertions(+), 39 deletions(-)

Fusion-Task-Id: FN-7419
Fusion-Task-Lineage: 2c192a37-a1db-41a9-99f5-a480ed311d9c
Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
2026-07-02 11:38:33 -07:00

88 lines
3.5 KiB
TypeScript

import { runCursorCommand } from "./cli-spawn.js";
import type { CursorBinaryStatus } from "./types.js";
const CANDIDATES = ["cursor-agent", "cursor"] as const;
const MAX_FAILURE_DETAIL_LENGTH = 180;
function buildCandidates(binaryPath?: string): { candidates: string[]; configuredBinaryPath?: string } {
/*
FNXC:CursorCli 2026-07-02-00:00:
Manual operator paths must be tried before PATH candidates without deleting the fallback order. Deduping keeps `cursor-agent`/`cursor` overrides from probing the same shim twice while still preserving auto-detection.
*/
const configuredBinaryPath = binaryPath?.trim() || undefined;
const ordered = configuredBinaryPath ? [configuredBinaryPath, ...CANDIDATES] : [...CANDIDATES];
return { candidates: Array.from(new Set(ordered)), configuredBinaryPath };
}
function summarizeFailure(binary: string, stdout: string, stderr: string): string | undefined {
const detail = `${stderr || stdout}`.replace(/\s+/g, " ").trim();
if (!detail) return undefined;
const truncated = detail.length > MAX_FAILURE_DETAIL_LENGTH ? `${detail.slice(0, MAX_FAILURE_DETAIL_LENGTH - 1)}…` : detail;
return `${binary}: ${truncated}`;
}
export async function probeCursorBinary(options?: { timeoutMs?: number; binaryPath?: string }): Promise<CursorBinaryStatus> {
const startedAt = Date.now();
const timeoutMs = options?.timeoutMs ?? 3000;
const { candidates, configuredBinaryPath } = buildCandidates(options?.binaryPath);
const failureDetails: string[] = [];
for (const binary of candidates) {
const version = await runCursorCommand(binary, ["--version"], timeoutMs);
const failureDetail = summarizeFailure(binary, version.stdout, version.stderr);
if (failureDetail) failureDetails.push(failureDetail);
const common = {
binaryName: binary,
binaryPath: binary,
configuredBinaryPath,
usingConfiguredBinaryPath: configuredBinaryPath === binary,
diagnostics: failureDetails.length > 0 ? [...failureDetails] : undefined,
probeDurationMs: Date.now() - startedAt,
};
if (version.code === 0) {
// NOTE: Cursor CLI currently lacks a stable auth-status contract we can
// invoke without side effects. Treating successful --version as ready is
// a best-effort heuristic; keychain/auth errors are handled by fallback
// probes below when surfaced in stderr/stdout.
return {
available: true,
authenticated: true,
...common,
version: version.stdout.trim() || undefined,
};
}
const combined = `${version.stdout}\n${version.stderr}`.toLowerCase();
if (combined.includes("keychain is locked")) {
return {
available: true,
authenticated: false,
...common,
reason: "macOS login keychain is locked",
};
}
if (combined.includes("no cursor ide installation found")) {
return {
available: true,
authenticated: false,
...common,
reason: "Cursor IDE installation not found",
};
}
}
const baseReason = configuredBinaryPath
? `Configured Cursor CLI binary '${configuredBinaryPath}' failed; PATH fallback cursor-agent/cursor also failed`
: "cursor-agent/cursor not found on PATH";
return {
available: false,
authenticated: false,
configuredBinaryPath,
usingConfiguredBinaryPath: false,
diagnostics: failureDetails.length > 0 ? failureDetails : undefined,
reason: failureDetails.length > 0 ? `${baseReason} (${failureDetails.join("; ")})` : baseReason,
probeDurationMs: Date.now() - startedAt,
};
}