Repository navigation
fix(oauth): verify Cursor installation on Linux before auto-import #4770
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,8 +2,65 @@ import { NextResponse } from "next/server"; | |
| import { access, constants, readFile } from "fs/promises"; | ||
| import { homedir } from "os"; | ||
| import { join } from "path"; | ||
| import { execFile } from "child_process"; | ||
| import { promisify } from "util"; | ||
| import { isAuthRequired, isAuthenticated } from "@/shared/utils/apiAuth"; | ||
|
|
||
| const execFileAsync = promisify(execFile); | ||
|
|
||
| /** | ||
| * Probe dependencies for {@link verifyLinuxCursorInstalled}. Injectable so the | ||
| * guard can be unit-tested without spawning a real `which` process or touching | ||
| * the filesystem — mirrors the `__setExecFileImpl` pattern in | ||
| * `src/lib/cli-helper/tool-detector.ts`. | ||
| */ | ||
| export interface CursorInstallProbe { | ||
| /** Runs `which <binary>`; rejects when the binary is not on PATH. */ | ||
| execFile?: ( | ||
| file: string, | ||
| args: string[], | ||
| options: { timeout: number } | ||
| ) => Promise<{ stdout: string; stderr: string }>; | ||
| /** Resolves when the path is readable; rejects otherwise (e.g. `fs.access`). */ | ||
| access?: (path: string, mode: number) => Promise<void>; | ||
| /** Override the home directory used to locate the `.desktop` fallback. */ | ||
| home?: string; | ||
| } | ||
|
|
||
| /** | ||
| * On Linux, verify that the Cursor IDE is actually installed before trusting | ||
| * leftover config files (state.vscdb). A removed Cursor install can leave its | ||
| * `~/.config/Cursor/...` directory behind, which would otherwise trigger a | ||
| * false-positive auto-import and create a phantom Cursor provider connection. | ||
| * | ||
| * The check prefers `which cursor` and falls back to a readable | ||
| * `~/.local/share/applications/cursor.desktop` entry (the desktop launcher a | ||
| * package install drops even when the CLI shim is not on PATH). | ||
| * | ||
| * Port of decolua/9router#313 — only the linux probe is added; macOS/Windows | ||
| * keep their existing behavior (no install probe). | ||
| */ | ||
| export async function verifyLinuxCursorInstalled( | ||
| probe: CursorInstallProbe = {} | ||
| ): Promise<boolean> { | ||
| const exec = probe.execFile ?? execFileAsync; | ||
| const canAccess = probe.access ?? access; | ||
| const home = probe.home ?? homedir(); | ||
|
|
||
| try { | ||
| await exec("which", ["cursor"], { timeout: 5000 }); | ||
| return true; | ||
| } catch { | ||
| try { | ||
| const desktopFile = join(home, ".local/share/applications/cursor.desktop"); | ||
| await canAccess(desktopFile, constants.R_OK); | ||
| return true; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Known key names Cursor IDE has used over time to persist the auth token | ||
| * and machine id in the local `state.vscdb`. Order matters — the first | ||
|
|
@@ -184,6 +241,17 @@ async function tryIdeAuth(): Promise<{ | |
| }; | ||
| } | ||
| } else { | ||
| // On Linux, verify Cursor is actually installed before trusting leftover | ||
| // config files — a removed install can leave ~/.config/Cursor behind and | ||
| // would otherwise create a phantom Cursor connection (port: 9router#313). | ||
| if (platform === "linux" && !(await verifyLinuxCursorInstalled())) { | ||
| return { | ||
| found: false, | ||
| error: | ||
| "Cursor config files found but Cursor IDE does not appear to be " + | ||
| "installed. Skipping auto-import.", | ||
| }; | ||
| } | ||
| dbPath = candidates[0]; | ||
|
Comment on lines
+244
to
255
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Checking dbPath = candidates[0];
// On Linux, verify Cursor is actually installed before trusting leftover
// config files — a removed install can leave ~/.config/Cursor behind and
// would otherwise create a phantom Cursor connection (port: 9router#313).
if (platform === "linux") {
try {
await access(dbPath, constants.R_OK);
if (!(await verifyLinuxCursorInstalled())) {
return {
found: false,
error:
"Cursor config files found but Cursor IDE does not appear to be " +
"installed. Skipping auto-import.",
};
}
} catch {
return {
found: false,
error: "Cursor IDE database not found",
};
}
} |
||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On Linux, desktop entries can be installed system-wide (e.g., via package managers or system-wide integrations) under
/usr/share/applications/or/usr/local/share/applications/instead of the user's home directory. Ifwhich cursoris not available, checking only the user-specific path~/.local/share/applications/cursor.desktopcan lead to false negatives. We should expand the search to include standard system-wide XDG desktop directories.