mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(setup): preserve equivalent caller launcher paths
This commit is contained in:
1 parent
f19aebab76
commit
adafef2c9e
4 files changed
+94
-1
No files matched your search
@@ -265,6 +265,12 @@ the diagnostic identifies the setting to repair. See the
|
||||
[Codex configuration reference](https://developers.openai.com/codex/config-reference/)
|
||||
for the supported policy values.
|
||||
|
||||
When direct launcher paths prove the same executable or Node entrypoint,
|
||||
setup retains the existing caller-selected spelling. An otherwise aligned
|
||||
registration therefore needs no rewrite or recovery backup merely because
|
||||
REA was invoked through another symlink. Unresolved identity does not prove
|
||||
equivalence.
|
||||
|
||||
| Settings | Setup ownership |
|
||||
| ----------------------------------------- | --------------------------------------------------------------------- |
|
||||
| Launcher, transport and activation | Refreshes the REA stdio connection and enables it. |
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type ClientRegistrationDialect,
|
||||
} from "./ClientConfigurationDocument.js";
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
import { equivalentClientRegistrationCommands } from "./ClientRegistrationIdentity.js";
|
||||
|
||||
const tomlTimeoutSchema = z.union([
|
||||
z.number().positive(),
|
||||
@@ -181,8 +182,17 @@ export const refreshClientRegistration = (
|
||||
const existing = source.success ? source.data : {};
|
||||
const policy = validateClientPolicy(existing, client);
|
||||
if (!policy.ok) return policy;
|
||||
const prior = parseClientRegistration(existing, client);
|
||||
const priorCommand = prior.ok
|
||||
? [prior.value.command, ...prior.value.args]
|
||||
: undefined;
|
||||
const launcher =
|
||||
priorCommand !== undefined &&
|
||||
equivalentClientRegistrationCommands(priorCommand, command)
|
||||
? priorCommand
|
||||
: command;
|
||||
const registration = {
|
||||
...clientRegistrationEntry(dialect, command, environment),
|
||||
...clientRegistrationEntry(dialect, launcher, environment),
|
||||
...Object.fromEntries(
|
||||
Object.entries(existing).filter(
|
||||
([key]) => !managedConnectionKeys.has(key),
|
||||
|
||||
@@ -149,6 +149,40 @@ const directEntrypointIdentity = (
|
||||
: "unknown";
|
||||
};
|
||||
|
||||
/** Preserve caller spelling only when both direct launchers prove the same file. */
|
||||
export const equivalentClientRegistrationCommands = (
|
||||
previous: readonly string[],
|
||||
requested: readonly string[],
|
||||
): boolean => {
|
||||
if (
|
||||
previous.length === requested.length &&
|
||||
previous.every((value, index) => value === requested[index])
|
||||
)
|
||||
return true;
|
||||
const entrypoint = (command: readonly string[]): string | undefined => {
|
||||
if (
|
||||
command.length === 2 &&
|
||||
command[1] === "mcp" &&
|
||||
isAbsolute(command[0] ?? "")
|
||||
)
|
||||
return command[0];
|
||||
if (
|
||||
command.length === 3 &&
|
||||
command[2] === "mcp" &&
|
||||
isAbsolute(command[1] ?? "") &&
|
||||
entrypointIdentity(command[0] ?? "", process.execPath) === "owned"
|
||||
)
|
||||
return normalizeEntrypointPath(command[1]);
|
||||
return undefined;
|
||||
};
|
||||
const current = entrypoint(requested);
|
||||
return (
|
||||
current !== undefined &&
|
||||
entrypoint(previous) !== undefined &&
|
||||
classifyClientRegistrationCommand(previous, current) === "owned"
|
||||
);
|
||||
};
|
||||
|
||||
/** Classify the command once; uncertainty never grants positive ownership. */
|
||||
export const classifyClientRegistrationCommand = (
|
||||
command: readonly string[],
|
||||
|
||||
@@ -17,6 +17,7 @@ import { readConfigurationFile } from "../../../src/application/ClientConfigurat
|
||||
import {
|
||||
configureClientConfiguration,
|
||||
inspectClientConfiguration,
|
||||
clientConfigurationAligned,
|
||||
} from "../../../src/application/SetupClientConfiguration.js";
|
||||
import { resolveWindowsClaudeDesktopLocation } from "../../../src/application/ClaudeDesktopConfigLocation.js";
|
||||
import { supportedClients } from "../../../src/application/SupportedClients.js";
|
||||
@@ -26,6 +27,48 @@ import { PRODUCT_IDENTITY } from "../../../src/identity.js";
|
||||
|
||||
const pinnedPackage = PRODUCT_IDENTITY.registrationPackageSpecifier;
|
||||
|
||||
it.skipIf(process.platform === "win32")(
|
||||
"preserves an aligned caller launcher alias without writes or a backup",
|
||||
async () => {
|
||||
const home = await createTestTempDirectory(
|
||||
"rea-config-equivalent-launcher-",
|
||||
);
|
||||
const real = join(home, "rea-current");
|
||||
const alias = join(home, "rea-alias");
|
||||
await writeFile(real, "current launcher\n");
|
||||
await symlink(real, alias);
|
||||
await mkdir(join(home, ".cursor"));
|
||||
const configPath = join(home, ".cursor", "mcp.json");
|
||||
const original = JSON.stringify({
|
||||
mcpServers: {
|
||||
rea: { command: alias, args: ["mcp"], future_policy: { retain: true } },
|
||||
},
|
||||
});
|
||||
await writeFile(configPath, original);
|
||||
const client = { name: "cursor", format: "json", configPath } as const;
|
||||
expect(await clientConfigurationAligned(client, {}, [real, "mcp"])).toBe(
|
||||
true,
|
||||
);
|
||||
expect(await inspectClientConfiguration(client, {}, [real, "mcp"])).toEqual(
|
||||
{ status: "already_current" },
|
||||
);
|
||||
expect(
|
||||
await configureClientConfiguration(client, {}, [real, "mcp"]),
|
||||
).toEqual({ status: "unchanged" });
|
||||
expect(await readFile(configPath, "utf8")).toBe(original);
|
||||
await expect(readFile(`${configPath}.rea.backup`)).rejects.toThrow();
|
||||
const other = join(home, "another-launcher");
|
||||
await writeFile(other, "different launcher\n");
|
||||
expect(
|
||||
await configureClientConfiguration(client, {}, [other, "mcp"]),
|
||||
).toMatchObject({ status: "configured" });
|
||||
expect(JSON.parse(await readFile(configPath, "utf8"))).toMatchObject({
|
||||
mcpServers: { rea: { command: other, future_policy: { retain: true } } },
|
||||
});
|
||||
expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original);
|
||||
},
|
||||
);
|
||||
|
||||
describe("JSON client configuration transaction: writing configuration through safe paths", () => {
|
||||
it.skipIf(typeof process.getuid !== "function")(
|
||||
"rejects a symlink target not owned by the current user",
|
||||
|
||||
Reference in new issue
Block a user