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>
88 lines
3.5 KiB
TypeScript
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,
|
|
};
|
|
}
|