fix(setup): recognize shared CLI launcher aliases (#4803)

This commit is contained in:
Shivam Dwivedi authored and GitHub committed 2026-10-17 23:22:43 +08:00
1 parent d38f099692
commit c1fe0d653d
4 files changed
+135 -25

No files matched your search

+16 -2
View File
@@ -1,3 +1,4 @@
import { realpathSync } from "node:fs";
import { isAbsolute, resolve } from "node:path";
import { valid } from "semver";
@@ -115,6 +116,19 @@ const isOwnedPackageReference = (packageReference: string): boolean => {
/** Check that one parsed command points to REA's MCP entry point. */
export type ClientRegistrationCommandIdentity = "owned" | "foreign" | "unknown";
/** POSIX bin links can name the same dispatcher without changing stored commands. */
const sameEntrypointPath = (candidate: string, current: string): boolean => {
if (!isAbsolute(candidate) || !isAbsolute(current)) return false;
if (resolve(candidate) === current) return true;
// Windows package bins are command shims, not links to the dispatcher.
if (process.platform === "win32") return false;
try {
return realpathSync(candidate) === realpathSync(current);
} catch {
return false;
}
};
/** Classify only what the command and current executable identity establish. */
export const classifyClientRegistrationCommand = (
command: readonly string[],
@@ -164,7 +178,7 @@ export const isOwnedClientRegistrationCommand = (
isAbsolute(command[0] ?? "") &&
isAbsolute(command[1] ?? "") &&
resolve(command[0] ?? "") === resolve(process.execPath) &&
resolve(command[1] ?? "") === currentCommandPath
sameEntrypointPath(command[1] ?? "", currentCommandPath)
)
return true;
@@ -175,7 +189,7 @@ export const isOwnedClientRegistrationCommand = (
(isAbsolute(executable) &&
currentCommandPath !== undefined &&
currentCommandPath.length > 0 &&
resolve(executable) === currentCommandPath)
sameEntrypointPath(executable, currentCommandPath))
)
return true;
}
+6 -22
View File
@@ -7,7 +7,7 @@ import {
type ClientConfigurationDocument,
} from "./ClientConfigurationDocument.js";
import { access } from "node:fs/promises";
import { isAbsolute, resolve } from "node:path";
import { isAbsolute } from "node:path";
import { z } from "zod";
import { valid } from "semver";
@@ -403,27 +403,11 @@ const registrationAligned = ({
})
)
return "aligned";
if (
currentCommandPath !== undefined &&
currentCommandPath.length > 0 &&
command.length === 3 &&
command[2] === "mcp" &&
isAbsolute(command[0] ?? "") &&
isAbsolute(command[1] ?? "") &&
resolve(command[0] ?? "") === resolve(process.execPath) &&
resolve(command[1] ?? "") === currentCommandPath
)
return "aligned";
if (
currentCommandPath !== undefined &&
currentCommandPath.length > 0 &&
command.length === 2 &&
command[1] === "mcp" &&
isAbsolute(command[0] ?? "") &&
resolve(command[0] ?? "") === currentCommandPath
)
return "aligned";
return "stale";
// Other owned package references remain stale until pinned to this version.
if (clientRegistrationPackageReference(command) !== undefined) return "stale";
// The identity owner already established an exact direct launcher match.
// A bare command still depends on the client's PATH, so cannot prove alignment.
return isAbsolute(command[0] ?? "") ? "aligned" : "stale";
};
const parseRegistration = (
@@ -0,0 +1,64 @@
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest.skipIf(process.platform === "win32").for([
["rea", "rea-agents"],
["rea-agents", "rea"],
] as const)(
"recognizes setup through %s when doctor and uninstall use %s",
async ([registered, alternate], { processes }) => {
const root = await createTestTempDirectory("rea-launcher-registration-");
const home = join(root, "home");
const config = join(home, ".cursor/mcp.json");
await mkdir(join(home, ".cursor"), { recursive: true });
const unrelated = { command: "other-server", args: [] };
await writeFile(config, JSON.stringify({ mcpServers: { unrelated } }));
for (const name of [registered, alternate])
await symlink(resolve("scripts/rea.mjs"), join(root, name));
const run = (alias: string, arguments_: readonly string[]) =>
processes.run(process.execPath, [join(root, alias), ...arguments_], {
cwd: home,
env: {
HOME: home,
USERPROFILE: home,
XDG_CONFIG_HOME: join(home, ".config"),
PATH: process.env.PATH ?? "",
},
timeoutMs: 30_000,
});
const setup = await run(registered, [
"setup",
"--client",
"cursor",
"--skill=false",
"--yes",
"--json",
]);
expect(setup.exitCode, setup.stderr).toBe(0);
for (const alias of [registered, alternate]) {
const doctor = await run(alias, [
"doctor",
"--client",
"cursor",
"--skill=false",
"--json",
]);
expect(doctor.exitCode, doctor.stdout).toBe(0);
expect(JSON.parse(doctor.stdout)).toMatchObject({
scope_checks: expect.arrayContaining([
expect.objectContaining({ name: "registration:cursor", ok: true }),
]),
});
}
const uninstall = await run(alternate, ["uninstall", "--json"]);
expect(uninstall.exitCode, uninstall.stderr).toBe(0);
expect(JSON.parse(await readFile(config, "utf8"))).toEqual({
mcpServers: { unrelated },
});
},
);
@@ -1,4 +1,4 @@
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { mkdir, readFile, symlink, writeFile } from "node:fs/promises";
import { join, relative, resolve } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
@@ -706,6 +706,54 @@ describe("platform-aware client config paths", () => {
});
describe("client registration command ownership", () => {
it.skipIf(process.platform === "win32").each([
["rea", "rea-agents"],
["rea-agents", "rea"],
])(
"recognizes %s through its %s launcher alias",
async (registered, current) => {
const root = await createTestTempDirectory("rea-launcher-alias-");
const script = join(root, "entry.mjs");
await writeFile(script, "// owned dispatcher\n");
await symlink(script, join(root, registered));
await symlink(script, join(root, current));
for (const command of [
[join(root, registered), "mcp"],
[process.execPath, join(root, registered), "mcp"],
]) {
expect(
classifyClientRegistrationCommand(command, join(root, current)),
).toBe("owned");
expect(
isOwnedClientRegistrationCommand(command, join(root, current)),
).toBe(true);
}
},
);
it.skipIf(process.platform === "win32")(
"does not own a same-named launcher for another script or a dangling link",
async () => {
const root = await createTestTempDirectory("rea-launcher-foreign-");
const owned = join(root, "entry.mjs");
const foreign = join(root, "other.mjs");
await writeFile(owned, "// owned\n");
await writeFile(foreign, "// foreign\n");
await symlink(owned, join(root, "rea"));
await symlink(foreign, join(root, "rea-agents"));
await symlink(join(root, "missing.mjs"), join(root, "missing"));
for (const candidate of ["rea-agents", "missing"]) {
const command = [join(root, candidate), "mcp"];
expect(
classifyClientRegistrationCommand(command, join(root, "rea")),
).toBe("foreign");
expect(
isOwnedClientRegistrationCommand(command, join(root, "rea")),
).toBe(false);
}
},
);
it("recognizes prior exact REA releases while rejecting foreign package commands", () => {
expect(isOwnedClientRegistrationCommand(["rea", "mcp"], undefined)).toBe(
true,