Files
millionco__react-doctor/packages/react-doctor/tests/launch-agent.test.ts
Skosh bd0f465e61 fix: launch Cursor agent on Windows via bundled node resolution (#966)
* fix: launch Cursor agent on Windows via bundled node resolution

Resolves Cursor's PowerShell-wrapped .cmd launcher by detecting and using
the bundled node.exe + index.js under %LOCALAPPDATA%\cursor-agent\versions.

Also adds a shell fallback for .cmd files that don't match the npm-style
or Cursor-specific patterns, ensuring other CLI agents still work.

Fixes #964

Co-authored-by: Skosh <skoshx@users.noreply.github.com>

* chore: add changeset for Windows Cursor agent launcher fix

Co-authored-by: Skosh <skoshx@users.noreply.github.com>

* chore: fix formatting

Co-authored-by: Skosh <skoshx@users.noreply.github.com>

* fix: fallback to older Cursor versions when latest is incomplete

Addresses Bugbot finding: when the latest version folder has an
incomplete installation (missing node.exe or index.js after a failed
update), the resolver now tries the next-latest version instead of
giving up. This prevents falling back to shell: true and breaking
multiline handoff prompts.

Co-authored-by: Skosh <skoshx@users.noreply.github.com>

* fix(launch-agent): drop contradictory shell:true Windows fallback

The shell fallback re-introduced the exact multi-line-prompt mangling the PR
exists to prevent: any `<binary>.cmd` on PATH preempted the clean print-prompt
path and handed the prompt to cmd.exe. Remove it — an unresolved Cursor install
now falls through to the existing print-the-prompt fallback (usable text) rather
than a mangled launch. `spawnAgent`'s now-dead `shell` param goes with it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Skosh <skoshx@users.noreply.github.com>
Co-authored-by: Rayhan Noufal Arayilakath <me@rayhanadev.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 10:57:19 -07:00

144 lines
5.3 KiB
TypeScript

import { tmpdir } from "node:os";
import * as path from "node:path";
import * as fs from "node:fs";
import { afterEach, beforeEach, describe, expect, it } from "vite-plus/test";
import {
resolveCursorBundledNode,
resolveWindowsCmdEntryScript,
} from "../src/cli/utils/launch-agent.js";
describe.skipIf(process.platform !== "win32")("Windows CLI agent launching", () => {
let fakeBinDirectory: string;
let fakeLocalAppData: string;
let originalPath: string | undefined;
let originalLocalAppData: string | undefined;
beforeEach(() => {
fakeBinDirectory = fs.mkdtempSync(path.join(tmpdir(), "react-doctor-launch-"));
fakeLocalAppData = fs.mkdtempSync(path.join(tmpdir(), "react-doctor-appdata-"));
originalPath = process.env.PATH;
originalLocalAppData = process.env.LOCALAPPDATA;
process.env.PATH = fakeBinDirectory;
process.env.LOCALAPPDATA = fakeLocalAppData;
});
afterEach(() => {
process.env.PATH = originalPath;
process.env.LOCALAPPDATA = originalLocalAppData;
fs.rmSync(fakeBinDirectory, { recursive: true, force: true });
fs.rmSync(fakeLocalAppData, { recursive: true, force: true });
});
it("resolves Cursor bundled node.exe + index.js when present", () => {
const versionsDir = path.join(fakeLocalAppData, "cursor-agent", "versions", "1.0.0");
fs.mkdirSync(versionsDir, { recursive: true });
fs.writeFileSync(path.join(versionsDir, "node.exe"), "fake node");
fs.writeFileSync(path.join(versionsDir, "index.js"), "fake entry");
const resolution = resolveCursorBundledNode();
expect(resolution).not.toBeNull();
expect(resolution?.command).toBe(path.join(versionsDir, "node.exe"));
expect(resolution?.args).toEqual([path.join(versionsDir, "index.js")]);
});
it("returns null when LOCALAPPDATA is not set", () => {
delete process.env.LOCALAPPDATA;
const resolution = resolveCursorBundledNode();
expect(resolution).toBeNull();
});
it("returns null when cursor-agent directory does not exist", () => {
const resolution = resolveCursorBundledNode();
expect(resolution).toBeNull();
});
it("picks the latest version when multiple versions exist", () => {
const versionsRoot = path.join(fakeLocalAppData, "cursor-agent", "versions");
fs.mkdirSync(versionsRoot, { recursive: true });
const version1 = path.join(versionsRoot, "1.0.0");
const version2 = path.join(versionsRoot, "2.0.0");
const version3 = path.join(versionsRoot, "1.10.0");
fs.mkdirSync(version1, { recursive: true });
fs.writeFileSync(path.join(version1, "node.exe"), "fake node");
fs.writeFileSync(path.join(version1, "index.js"), "fake entry");
fs.mkdirSync(version2, { recursive: true });
fs.writeFileSync(path.join(version2, "node.exe"), "fake node");
fs.writeFileSync(path.join(version2, "index.js"), "fake entry");
fs.mkdirSync(version3, { recursive: true });
fs.writeFileSync(path.join(version3, "node.exe"), "fake node");
fs.writeFileSync(path.join(version3, "index.js"), "fake entry");
const resolution = resolveCursorBundledNode();
expect(resolution).not.toBeNull();
expect(resolution?.command).toBe(path.join(version2, "node.exe"));
});
it("returns null when bundled files are incomplete", () => {
const versionsDir = path.join(fakeLocalAppData, "cursor-agent", "versions", "1.0.0");
fs.mkdirSync(versionsDir, { recursive: true });
fs.writeFileSync(path.join(versionsDir, "node.exe"), "fake node");
const resolution = resolveCursorBundledNode();
expect(resolution).toBeNull();
});
it("falls back to older version when latest is incomplete", () => {
const versionsRoot = path.join(fakeLocalAppData, "cursor-agent", "versions");
fs.mkdirSync(versionsRoot, { recursive: true });
const version1 = path.join(versionsRoot, "1.0.0");
const version2 = path.join(versionsRoot, "2.0.0");
fs.mkdirSync(version1, { recursive: true });
fs.writeFileSync(path.join(version1, "node.exe"), "fake node");
fs.writeFileSync(path.join(version1, "index.js"), "fake entry");
fs.mkdirSync(version2, { recursive: true });
fs.writeFileSync(path.join(version2, "node.exe"), "fake node");
const resolution = resolveCursorBundledNode();
expect(resolution).not.toBeNull();
expect(resolution?.command).toBe(path.join(version1, "node.exe"));
});
it("resolves npm-style .cmd wrappers pointing to .js files", () => {
const cmdFilePath = path.join(fakeBinDirectory, "test-cli.cmd");
const entryScriptPath = path.join(fakeBinDirectory, "test-cli.js");
fs.writeFileSync(cmdFilePath, '@echo off\r\nnode "%~dp0\\test-cli.js" %*\r\n');
fs.writeFileSync(entryScriptPath, "console.log('test');");
const resolved = resolveWindowsCmdEntryScript("test-cli");
expect(resolved).toBe(entryScriptPath);
});
it("returns null for PowerShell-based .cmd wrappers", () => {
const cmdFilePath = path.join(fakeBinDirectory, "cursor-agent.cmd");
fs.writeFileSync(
cmdFilePath,
'@echo off\r\npwsh.exe -NoLogo -ExecutionPolicy Bypass -File "%USERPROFILE%\\.local\\bin\\cursor-agent-profile-wrapper.ps1" %*\r\n',
);
const resolved = resolveWindowsCmdEntryScript("cursor-agent");
expect(resolved).toBeNull();
});
it("returns null when .cmd file does not exist", () => {
const resolved = resolveWindowsCmdEntryScript("nonexistent");
expect(resolved).toBeNull();
});
});