fix(setup): register a stable launcher when run through bunx (#1731)

Setup run through bunx or bun x registered the bin inside bun's temporary
package cache. Detect bun's package-runner launch from npm_command=exec
with a bun user agent and register the pinned bunx launcher, wrapped in
cmd.exe on Windows like the npx launcher. Doctor, update maintenance and
uninstall recognize it as REA-owned, and setup's printed commands name
the runner that launched it.

Read the runner variables case-insensitively on Windows, where the CLI
environment snapshot upper-cases variable names and setup run through
npx also fell back to the cache path.

Co-authored-by: morluto <[email protected]>
This commit is contained in:
xeropresence
2026-10-11 19:17:15 +08:00
committed by GitHub
co-authored by morluto
parent 3a17882cc4
commit d412def722
14 changed files with 317 additions and 98 deletions
+2 -1
View File
@@ -130,7 +130,8 @@ npx rea-agents@latest setup
```
Review the setup changes and restart your agent. For one-off CLI commands,
use `npx rea-agents@latest` followed by the command.
use `npx rea-agents@latest` followed by the command. With Bun, use
`bunx rea-agents@latest` in place of `npx rea-agents@latest`.
## How REA works
+13 -5
View File
@@ -36,6 +36,13 @@ Setup continues to pin persistent MCP registrations to the exact version that
performed setup. Running current setup later migrates unversioned or older
managed registrations through the normal reviewed setup transaction.
With Bun, `bunx rea-agents setup` (or `bunx rea-agents@latest setup`) is the
equivalent entry point. Setup run through `npx` registers
`npx -y rea-agents@<version> mcp`; setup run through `bunx` or `bun x` registers
`bunx rea-agents@<version> mcp`. On Windows, setup wraps either command in
`cmd.exe /d /c` so clients can start the runner's command shim. Neither
registration points into the runner's temporary package cache.
REA supports Node.js 22.x (>=22.19), 24.x (>=24.11), and 26+. Node.js 23, 25, and prereleases are unsupported. It uses the npm already paired with that runtime and never upgrades Node.js, npm, or Homebrew.
Running `npm install rea-agents` without `--global` installs the executable only
@@ -669,7 +676,7 @@ rea providers --json
`rea setup` can configure supported Windows agents and install the bundled
REA skill after approval. Direct registrations use Node to launch REA's entry
script; package-runner registrations use the pinned `npx` command. Hopper
script; package-runner registrations use the pinned `npx` or `bunx` command. Hopper
installation remains unavailable on Windows. Setup never installs Ghidra,
Java, or Python. It preserves valid detected Ghidra/JDK settings in agent
registrations. See [Windows Ghidra P0](windows-ghidra-p0.md) for provider diagnostics.
@@ -836,7 +843,7 @@ Ghidra/JDK installation; neither is inferred from startup alone.
## Diagnose, update, and remove
`rea doctor --json` is strictly read-only. `rea update` updates only the npm or Bun global installation that owns the running CLI. Source checkouts and package-runner copies must be updated through the mechanism that owns them; a fresh package-runner invocation can use `npx rea-agents@latest`. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
`rea doctor --json` is strictly read-only. `rea update` updates only the npm or Bun global installation that owns the running CLI. Source checkouts and package-runner copies must be updated through the mechanism that owns them; a fresh package-runner invocation can use `npx rea-agents@latest` or `bunx rea-agents@latest`. `rea uninstall` removes only REA-owned agent registrations and skill files; `--purge-data` additionally removes REA cache and state paths.
```bash
rea update
@@ -881,6 +888,7 @@ For a client that requires manual configuration, use:
<!-- x-release-please-end -->
Persistent registrations should use one exact package version. `rea setup`
writes the same package-runner shape and pins it to the exact version that
performed setup. Run current setup to refresh an older registration, then
restart the client.
writes the same package-runner shape, or `bunx rea-agents@<version> mcp` when
setup runs through `bunx`, and pins it to the exact version that performed
setup. Run current setup to refresh an older registration, then restart the
client.
+91 -28
View File
@@ -3,20 +3,71 @@ import { valid } from "semver";
import { PRODUCT_IDENTITY } from "../identity.js";
/** Resolve a pinned npx launcher that stdio clients can spawn on the host. */
export const npxRegistrationCommand = (
/** Package runners whose pinned launcher setup can register. */
export type PackageRunner = "npx" | "bunx";
/** Read one variable; Windows variable names are case-insensitive. */
const environmentValue = (
environment: Readonly<NodeJS.ProcessEnv>,
name: string,
platform: NodeJS.Platform,
): string | undefined => {
if (platform !== "win32") return environment[name];
const key = Object.keys(environment).find(
(candidate) => candidate.toLowerCase() === name,
);
return key === undefined ? undefined : environment[key];
};
/** Identify the package runner that launched this process, if any. */
export const invokingPackageRunner = (
environment: Readonly<NodeJS.ProcessEnv>,
platform: NodeJS.Platform = process.platform,
): PackageRunner | undefined => {
if (environmentValue(environment, "npm_command", platform) !== "exec")
return undefined;
// `bunx` and `bun x` also report npm_command=exec, with a bun user agent.
return environmentValue(
environment,
"npm_config_user_agent",
platform,
)?.startsWith("bun/") === true
? "bunx"
: "npx";
};
/** Copy an environment without the variable that marks a package-runner launch. */
export const withoutPackageRunnerIdentity = (
environment: Readonly<NodeJS.ProcessEnv>,
platform: NodeJS.Platform = process.platform,
): NodeJS.ProcessEnv =>
Object.fromEntries(
Object.entries(environment).filter(([key]) =>
platform === "win32"
? key.toLowerCase() !== "npm_command"
: key !== "npm_command",
),
);
/** Resolve a pinned package-runner launcher that stdio clients can spawn on the host. */
export const packageRunnerRegistrationCommand = (
runner: PackageRunner,
platform: NodeJS.Platform = process.platform,
): readonly string[] => {
const command = [
"npx",
"-y",
PRODUCT_IDENTITY.registrationPackageSpecifier,
"mcp",
];
const command =
runner === "npx"
? ["npx", "-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"]
: ["bunx", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"];
// Both runners may be .cmd shims on Windows, which clients cannot spawn directly.
return platform === "win32" ? ["cmd.exe", "/d", "/c", ...command] : command;
};
const unwrapNpxCommand = (command: readonly string[]): readonly string[] => {
/** Resolve a pinned npx launcher that stdio clients can spawn on the host. */
export const npxRegistrationCommand = (
platform: NodeJS.Platform = process.platform,
): readonly string[] => packageRunnerRegistrationCommand("npx", platform);
const unwrapCmdCommand = (command: readonly string[]): readonly string[] => {
const executable = command[0]?.toLowerCase();
if (executable !== "cmd" && executable !== "cmd.exe") return command;
if (command[1]?.toLowerCase() === "/c") return command.slice(2);
@@ -25,6 +76,35 @@ const unwrapNpxCommand = (command: readonly string[]): readonly string[] => {
return [];
};
const packageRunnerReference = (
command: readonly string[],
): string | undefined => {
if (
command.length === 4 &&
command[0] === "npx" &&
command[1] === "-y" &&
command[3] === "mcp"
)
return command[2];
if (command.length === 3 && command[0] === "bunx" && command[2] === "mcp")
return command[1];
return undefined;
};
const isOwnedPackageReference = (packageReference: string): boolean => {
if (
packageReference === PRODUCT_IDENTITY.packageName ||
packageReference === PRODUCT_IDENTITY.packageSpecifier ||
packageReference === PRODUCT_IDENTITY.registrationPackageSpecifier
)
return true;
const versionPrefix = `${PRODUCT_IDENTITY.packageName}@`;
return (
packageReference.startsWith(versionPrefix) &&
valid(packageReference.slice(versionPrefix.length)) !== null
);
};
/** Check that one parsed command points to REA's MCP entry point. */
export const isOwnedClientRegistrationCommand = (
command: readonly string[],
@@ -49,25 +129,8 @@ export const isOwnedClientRegistrationCommand = (
return true;
}
const npxCommand = unwrapNpxCommand(command);
if (
npxCommand.length !== 4 ||
npxCommand[0] !== "npx" ||
npxCommand[1] !== "-y" ||
npxCommand[3] !== "mcp"
)
return false;
const packageReference = npxCommand[2] ?? "";
if (
packageReference === PRODUCT_IDENTITY.packageName ||
packageReference === PRODUCT_IDENTITY.packageSpecifier ||
packageReference === PRODUCT_IDENTITY.registrationPackageSpecifier
)
return true;
const versionPrefix = `${PRODUCT_IDENTITY.packageName}@`;
const packageReference = packageRunnerReference(unwrapCmdCommand(command));
return (
packageReference.startsWith(versionPrefix) &&
valid(packageReference.slice(versionPrefix.length)) !== null
packageReference !== undefined && isOwnedPackageReference(packageReference)
);
};
+8 -4
View File
@@ -17,7 +17,7 @@ import { PRODUCT_IDENTITY } from "../identity.js";
import { MCP_STARTUP_POLICY } from "../mcpStartupPolicy.js";
import {
isOwnedClientRegistrationCommand,
npxRegistrationCommand,
packageRunnerRegistrationCommand,
} from "./ClientRegistrationIdentity.js";
import {
clientEvidencePaths,
@@ -232,10 +232,14 @@ const registrationAligned = (
resolve(command[1] ?? "") === currentCommandPath
)
return true;
const npxCommand = npxRegistrationCommand(platform);
if (
command.length === npxCommand.length &&
command.every((argument, index) => argument === npxCommand[index])
(["npx", "bunx"] as const).some((runner) => {
const runnerCommand = packageRunnerRegistrationCommand(runner, platform);
return (
command.length === runnerCommand.length &&
command.every((argument, index) => argument === runnerCommand[index])
);
})
)
return true;
return (
+2 -2
View File
@@ -253,9 +253,9 @@ describe("setup workflow action selection", () => {
expect(result.status).toBe("ready");
expect(host.configuredCommands).toEqual([
setupRegistrationCommand("win32", false),
setupRegistrationCommand("win32", undefined),
]);
expect(setupRegistrationCommand("win32", false)).toEqual([
expect(setupRegistrationCommand("win32", undefined)).toEqual([
process.execPath,
resolve(process.argv[1] ?? "rea"),
"mcp",
+1 -1
View File
@@ -52,7 +52,7 @@ export class FakeSetupHost implements SetupHost {
constructor(platform: NodeJS.Platform = "darwin") {
this.platform = platform;
this.registrationCommand = setupRegistrationCommand(platform, false);
this.registrationCommand = setupRegistrationCommand(platform, undefined);
}
macosVersion = (): Promise<string | undefined> =>
+9 -5
View File
@@ -5,7 +5,11 @@ import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import { npxRegistrationCommand } from "./ClientRegistrationIdentity.js";
import {
invokingPackageRunner,
packageRunnerRegistrationCommand,
type PackageRunner,
} from "./ClientRegistrationIdentity.js";
import {
SUPPORTED_NODE_VERSION_PROSE,
supportsNodeVersion,
@@ -48,10 +52,10 @@ import type { DoctorScope } from "./Doctor.js";
/** Resolve the executable and arguments used in a managed MCP registration. */
export const setupRegistrationCommand = (
platform: NodeJS.Platform,
useNpmRunner: boolean,
packageRunner: PackageRunner | undefined,
): readonly string[] =>
useNpmRunner
? npxRegistrationCommand(platform)
packageRunner !== undefined
? packageRunnerRegistrationCommand(packageRunner, platform)
: platform === "win32"
? [
process.execPath,
@@ -113,7 +117,7 @@ export const systemSetupHost = (
skillDestinations(homeDirectory, clientIds, environment, platform),
registrationCommand: setupRegistrationCommand(
platform,
environment.npm_command === "exec",
invokingPackageRunner(environment, platform),
),
nodeVersion: process.versions.node,
macosVersion: () => doctorHost.macosVersion(),
+3 -3
View File
@@ -18,6 +18,7 @@ import {
type UpdateOutput,
} from "./Update.js";
import { planIntegrationMaintenance } from "./UpdateMaintenance.js";
import { withoutPackageRunnerIdentity } from "./ClientRegistrationIdentity.js";
/** npm filesystem queries used to identify the installation being updated. */
export interface NpmInstallationHost {
@@ -45,9 +46,8 @@ export const runUpdateCommand = (
resolveResult(err("No executable supplied."));
return;
}
const environment = { ...selectedEnvironment };
// A fresh installed entry point must not inherit npx's invocation identity.
delete environment.npm_command;
// A fresh installed entry point must not inherit a package runner's invocation identity.
const environment = withoutPackageRunnerIdentity(selectedEnvironment);
const child = spawn(executable, args, {
stdio: ["ignore", "pipe", "pipe"],
env: environment,
+5 -2
View File
@@ -16,6 +16,7 @@ import type {
SetupResult,
} from "../application/SetupTypes.js";
import { SUPPORTED_CLIENT_DEFINITIONS } from "../application/SupportedClients.js";
import { invokingPackageRunner } from "../application/ClientRegistrationIdentity.js";
const clientDisplayNames: ReadonlyMap<string, string> = new Map(
SUPPORTED_CLIENT_DEFINITIONS.map(({ name, displayName }) => [
@@ -224,8 +225,10 @@ const renderKeyboardHelp = (accessible: boolean): void => {
);
};
const cliInvocation = (environment: Readonly<NodeJS.ProcessEnv>): string =>
environment.npm_command === "exec" ? "npx rea-agents" : "rea";
const cliInvocation = (environment: Readonly<NodeJS.ProcessEnv>): string => {
const runner = invokingPackageRunner(environment);
return runner === undefined ? "rea" : `${runner} rea-agents`;
};
const renderReadyCapabilities = (
result: SetupResult,
+30
View File
@@ -12,6 +12,12 @@ import { systemUninstallHost } from "./application/Uninstall.js";
import { runUpdateCommand } from "./application/UpdateRuntime.js";
import { createSystemDoctorHost } from "./doctorRuntime.js";
import { PRODUCT_IDENTITY } from "./identity.js";
import {
invokingPackageRunner,
packageRunnerRegistrationCommand,
withoutPackageRunnerIdentity,
} from "./application/ClientRegistrationIdentity.js";
import { snapshotEnvironment } from "./process/snapshotEnvironment.js";
afterEach(() => vi.unstubAllEnvs());
@@ -98,3 +104,27 @@ it("passes only selected environment to updater subprocesses and drops npm invoc
);
expect(result).toEqual({ ok: true, value: '{"marker":"selected"}' });
});
it.each([
{ userAgent: "npm/10.9.0 node/v24.11.0 win32 x64", runner: "npx" },
{ userAgent: "bun/1.4.2 npm/? node/v26.3.0 win32 x64", runner: "bunx" },
] as const)(
"registers the $runner launcher from a Windows CLI environment snapshot",
({ userAgent, runner }) => {
// The CLI snapshot canonicalizes Windows variable names to upper case.
const environment = snapshotEnvironment(
{ npm_command: "exec", npm_config_user_agent: userAgent },
"win32",
);
expect(invokingPackageRunner(environment, "win32")).toBe(runner);
expect(
systemSetupHost(
createDoctorHostFixture({ platform: "win32" }),
environment,
).registrationCommand,
).toEqual(packageRunnerRegistrationCommand(runner, "win32"));
expect(withoutPackageRunnerIdentity(environment, "win32")).toEqual({
NPM_CONFIG_USER_AGENT: userAgent,
});
},
);
+18 -4
View File
@@ -216,11 +216,20 @@ describe("interactive setup journey", () => {
describe("interactive setup completion", () => {
it.each([
{ npmCommand: "", launcher: "rea" },
{ npmCommand: "exec", launcher: "npx rea-agents" },
{ npmCommand: "", userAgent: "", launcher: "rea" },
{
npmCommand: "exec",
userAgent: "npm/10.9.0 node/v24.11.0 linux x64",
launcher: "npx rea-agents",
},
{
npmCommand: "exec",
userAgent: "bun/1.4.2 npm/? node/v26.3.0 linux x64",
launcher: "bunx rea-agents",
},
])(
"prints an available next command for $launcher invocation",
async ({ npmCommand, launcher }) => {
async ({ npmCommand, userAgent, launcher }) => {
const { stderr } = await execute(
process.execPath,
[
@@ -246,7 +255,12 @@ describe("interactive setup completion", () => {
],
{
cwd: process.cwd(),
env: { ...process.env, npm_command: npmCommand, NO_COLOR: "1" },
env: {
...process.env,
npm_command: npmCommand,
npm_config_user_agent: userAgent,
NO_COLOR: "1",
},
},
);
@@ -0,0 +1,70 @@
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { z } from "zod";
import { packageRunnerRegistrationCommand } from "../../../src/application/ClientRegistrationIdentity.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const ompConfigurationSchema = z.object({
mcpServers: z.object({
rea: z.object({ command: z.string(), args: z.array(z.string()) }),
}),
});
// Package runners set these variables for the bin they launch from a temporary cache.
for (const { runner, userAgent } of [
{ runner: "npx", userAgent: "npm/10.9.0 node/v24.11.0 linux x64" },
{ runner: "bunx", userAgent: "bun/1.4.2 npm/? node/v26.3.0 linux x64" },
] as const) {
cliTest(
`${runner} setup registers the pinned ${runner} launcher instead of its cache`,
async ({ cli }) => {
const home = await createTestTempDirectory("rea-package-runner-");
const run = (arguments_: readonly string[]) =>
cli.run({
arguments: arguments_,
cwd: home,
environment: {
HOME: home,
USERPROFILE: home,
npm_command: "exec",
npm_config_user_agent: userAgent,
},
timeoutMs: 20_000,
});
const setup = await run([
"setup",
"--client",
"omp",
"--no-skill",
"--yes",
"--json",
]);
expect(setup.exitCode).toBe(0);
expect(setup.json).toMatchObject({ status: "ready" });
const configuration = ompConfigurationSchema.parse(
JSON.parse(
await readFile(join(home, ".omp", "agent", "mcp.json"), "utf8"),
),
);
const { command, args } = configuration.mcpServers.rea;
expect([command, ...args]).toEqual(
packageRunnerRegistrationCommand(runner, process.platform),
);
const doctor = await run(["doctor", "--client", "omp", "--json"]);
expect(doctor.json).toMatchObject({
identity: { registrations: [{ client: "omp", state: "aligned" }] },
});
const repeat = await run([
"setup",
"--client",
"omp",
"--no-skill",
"--dry-run",
"--json",
]);
expect(repeat.json).toMatchObject({ plannedActions: [] });
},
);
}
@@ -486,6 +486,16 @@ describe("platform-aware client config paths", () => {
expect(
isOwnedClientRegistrationCommand(["npx", "-y", "another-package", "mcp"]),
).toBe(false);
expect(
isOwnedClientRegistrationCommand([
"bunx",
`${PRODUCT_IDENTITY.packageName}@3.2.0`,
"mcp",
]),
).toBe(true);
expect(
isOwnedClientRegistrationCommand(["bunx", "another-package", "mcp"]),
).toBe(false);
expect(
isOwnedClientRegistrationCommand(
[process.execPath, "/packaged/rea.mjs", "mcp"],
@@ -56,7 +56,7 @@ it.each([
expect.objectContaining({ client: "claude_code", state: "stale" }),
);
const client = { name: "claude_code", configPath, format: "json" } as const;
const command = setupRegistrationCommand("win32", true);
const command = setupRegistrationCommand("win32", "npx");
expect(
await configureClientConfiguration(client, {}, command),
).toMatchObject({ status: "configured" });
@@ -83,35 +83,38 @@ it.each([
},
);
it("keeps the Windows launcher aligned through Codex TOML setup and removal", async () => {
const home = await createTestTempDirectory("rea-windows-codex-");
await mkdir(join(home, ".codex"));
const configPath = join(home, ".codex", "config.toml");
const original =
'\uFEFF# Keep this explanation.\nnotify = ["a", "b"]\nliteral = \'C:\\demo\\path\'\ndisabled_mcp_servers = ["rea"]\n\n[mcp_servers.other]\ncommand = "another-server"\n';
await writeFile(configPath, original);
const client = { name: "codex", configPath, format: "toml" } as const;
const command = setupRegistrationCommand("win32", true);
expect(await configureClientConfiguration(client, {}, command)).toMatchObject(
{ status: "configured" },
);
expect(
await readClientRegistrationStatuses(home, undefined, {
platform: "win32",
environment: {},
}),
).toContainEqual(
expect.objectContaining({ client: "codex", state: "aligned" }),
);
expect(await configureClientConfiguration(client, {}, command)).toEqual({
status: "unchanged",
});
expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original);
expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({
status: "removed",
});
expect(await readFile(configPath, "utf8")).toBe(original);
});
it.each(["npx", "bunx"] as const)(
"keeps the Windows %s launcher aligned through Codex TOML setup and removal",
async (runner) => {
const home = await createTestTempDirectory("rea-windows-codex-");
await mkdir(join(home, ".codex"));
const configPath = join(home, ".codex", "config.toml");
const original =
'\uFEFF# Keep this explanation.\nnotify = ["a", "b"]\nliteral = \'C:\\demo\\path\'\ndisabled_mcp_servers = ["rea"]\n\n[mcp_servers.other]\ncommand = "another-server"\n';
await writeFile(configPath, original);
const client = { name: "codex", configPath, format: "toml" } as const;
const command = setupRegistrationCommand("win32", runner);
expect(
await configureClientConfiguration(client, {}, command),
).toMatchObject({ status: "configured" });
expect(
await readClientRegistrationStatuses(home, undefined, {
platform: "win32",
environment: {},
}),
).toContainEqual(
expect.objectContaining({ client: "codex", state: "aligned" }),
);
expect(await configureClientConfiguration(client, {}, command)).toEqual({
status: "unchanged",
});
expect(await readFile(`${configPath}.rea.backup`, "utf8")).toBe(original);
expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({
status: "removed",
});
expect(await readFile(configPath, "utf8")).toBe(original);
},
);
it("retains rea-named custom shell registrations without writing a backup", async () => {
const home = await createTestTempDirectory("rea-custom-shell-registration-");
@@ -124,6 +127,9 @@ it("retains rea-named custom shell registrations without writing a backup", asyn
["cmd.exe", "/c", "npx", "-y", "another-package", "mcp"],
["cmd.exe", "/c", "npx", "-y", "[email protected]", "mcp", "&", "other"],
["cmd.exe", "/c", "npx", "-y", "[email protected] & other", "mcp"],
["cmd.exe", "/c", "bunx", "another-package", "mcp"],
["cmd.exe", "/c", "bunx", "-y", "[email protected]", "mcp"],
["cmd.exe", "/c", "bunx", "[email protected]", "mcp", "&", "other"],
]) {
const original = JSON.stringify({
mcpServers: { rea: { command: command[0], args: command.slice(1) } },
@@ -139,9 +145,12 @@ it("retains rea-named custom shell registrations without writing a backup", asyn
}
});
it.skipIf(process.platform !== "win32")(
"starts the written Windows registration through a native npx.cmd shim",
async () => {
it.skipIf(process.platform !== "win32").each([
["npx", ["-y", PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"]],
["bunx", [PRODUCT_IDENTITY.registrationPackageSpecifier, "mcp"]],
] as const)(
"starts the written Windows registration through a native %s.cmd shim",
async (runner, shimArguments) => {
const root = await createTestTempDirectory("rea-windows-mcp-");
const home = join(root, "home with spaces");
await mkdir(home);
@@ -151,26 +160,29 @@ it.skipIf(process.platform !== "win32")(
configPath,
format: "json",
} as const;
const command = setupRegistrationCommand("win32", runner);
// The default writer and npm-run setup must produce the same native launcher.
expect(await configureClientConfiguration(setupClient)).toMatchObject({
status: "configured",
});
expect(
await (runner === "npx"
? configureClientConfiguration(setupClient)
: configureClientConfiguration(setupClient, {}, command)),
).toMatchObject({ status: "configured" });
const decoded: unknown = JSON.parse(await readFile(configPath, "utf8"));
const registration = configurationSchema.parse(decoded).mcpServers.rea;
expect([registration.command, ...registration.args]).toEqual(
setupRegistrationCommand("win32", true),
);
expect([registration.command, ...registration.args]).toEqual(command);
const entry = fileURLToPath(
new URL("../../../scripts/rea.mjs", import.meta.url),
);
// Exercise the actual batch-shim boundary without registry access or package installation.
await writeFile(
join(home, "npx.cmd"),
join(home, `${runner}.cmd`),
[
"@echo off",
'if not "%~1"=="-y" exit /b 11',
`if not "%~2"=="${PRODUCT_IDENTITY.registrationPackageSpecifier}" exit /b 12`,
'if not "%~3"=="mcp" exit /b 13',
...shimArguments.map(
(argument, index) =>
`if not "%~${String(index + 1)}"=="${argument}" exit /b ${String(11 + index)}`,
),
`if not "%~${String(shimArguments.length + 1)}"=="" exit /b 19`,
`"${process.execPath}" "${entry}" mcp`,
"",
].join("\r\n"),