mirror of
https://github.com/morluto/rea.git
synced 2026-10-22 05:42:31 -05:00
Claude Code shared the generic `json` client format, which the shared configuration reader admits through JSONC parsing with comments and trailing commas enabled. Native Claude Code reads `.claude.json` with a strict JSON parser (after a UTF-8 BOM), so setup reported `ready`, wrote a registration into a file the client rejects as corrupt, and doctor reported it `aligned`. Give Claude Code its own `claude_code` configuration format and parse it at the owning boundary with strict JSON.parse after removing a leading BOM. Setup, doctor, and uninstall all consume that parser, so comment or trailing-comma documents are now refused as malformed before any backup or write, while plain and BOM-prefixed JSON still configure. Clients that natively accept JSONC keep the `json` format and its behavior. Fixes #4940 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
4.0 KiB
TypeScript
129 lines
4.0 KiB
TypeScript
import { readFile, writeFile } from "node:fs/promises";
|
||
import { join } from "node:path";
|
||
import { expect } from "vitest";
|
||
|
||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||
import { cliTest, type TestCli } from "../../support/cli/cliFixture.js";
|
||
|
||
const peer = '"peer": { "command": "/usr/bin/true", "args": [] }';
|
||
|
||
const runIn = (cli: TestCli, home: string) => (arguments_: readonly string[]) =>
|
||
cli.run({
|
||
arguments: arguments_,
|
||
cwd: home,
|
||
environment: {
|
||
HOME: home,
|
||
USERPROFILE: home,
|
||
CLAUDE_CONFIG_DIR: home,
|
||
XDG_CONFIG_HOME: join(home, ".config"),
|
||
},
|
||
});
|
||
|
||
const setupArguments = [
|
||
"setup",
|
||
"--client",
|
||
"claude_code",
|
||
"--skill=false",
|
||
"--json",
|
||
] as const;
|
||
const doctorArguments = [
|
||
"doctor",
|
||
"--client",
|
||
"claude_code",
|
||
"--skill=false",
|
||
"--json",
|
||
] as const;
|
||
|
||
// Native Claude Code rejects these documents with its strict JSON reader.
|
||
for (const [syntax, original] of [
|
||
[
|
||
"a comment",
|
||
`{\n // preserve this unrelated setting\n "mcpServers": {\n ${peer}\n }\n}\n`,
|
||
],
|
||
["a trailing comma", `{\n "mcpServers": {\n ${peer},\n }\n}\n`],
|
||
] as const)
|
||
cliTest(
|
||
`Claude Code setup, doctor, and uninstall refuse JSON with ${syntax}`,
|
||
async ({ cli }) => {
|
||
const home = await createTestTempDirectory("rea-claude-strict-json-");
|
||
const config = join(home, ".claude.json");
|
||
await writeFile(config, original);
|
||
const run = runIn(cli, home);
|
||
|
||
for (const mode of ["--dry-run", "--yes"]) {
|
||
const setup = await run([...setupArguments, mode]);
|
||
expect(setup.exitCode, setup.stdout).toBe(1);
|
||
expect(setup.json).toMatchObject({
|
||
status: "needs_human",
|
||
plannedActions: [],
|
||
appliedActions: [],
|
||
remediation: expect.stringContaining("malformed"),
|
||
});
|
||
}
|
||
const doctor = await run(doctorArguments);
|
||
expect(doctor.exitCode).toBe(1);
|
||
expect(doctor.json).toMatchObject({
|
||
healthy: false,
|
||
identity: {
|
||
registrations: [
|
||
expect.objectContaining({
|
||
client: "claude_code",
|
||
state: "invalid",
|
||
}),
|
||
],
|
||
},
|
||
});
|
||
const removed = await run(["uninstall", "--json"]);
|
||
expect(removed.exitCode).toBe(1);
|
||
expect(removed.json).toMatchObject({
|
||
status: "failed",
|
||
items: expect.arrayContaining([
|
||
expect.objectContaining({ name: "claude_code", status: "failed" }),
|
||
]),
|
||
});
|
||
expect(await readFile(config, "utf8")).toBe(original);
|
||
await expect(readFile(`${config}.rea.backup`)).rejects.toMatchObject({
|
||
code: "ENOENT",
|
||
});
|
||
},
|
||
);
|
||
|
||
for (const prefix of ["", ""])
|
||
cliTest(
|
||
`Claude Code setup configures strict JSON with prefix ${JSON.stringify(prefix)}`,
|
||
async ({ cli }) => {
|
||
const home = await createTestTempDirectory("rea-claude-strict-json-");
|
||
const config = join(home, ".claude.json");
|
||
const original = `${prefix}{\n "mcpServers": {\n ${peer}\n }\n}\n`;
|
||
await writeFile(config, original);
|
||
const run = runIn(cli, home);
|
||
|
||
const setup = await run([...setupArguments, "--yes"]);
|
||
expect(setup.exitCode, setup.stdout).toBe(0);
|
||
expect(setup.json).toMatchObject({
|
||
status: "ready",
|
||
clients: { claude_code: { status: "configured" } },
|
||
});
|
||
expect(await readFile(`${config}.rea.backup`, "utf8")).toBe(original);
|
||
const configured = await readFile(config, "utf8");
|
||
expect(configured.startsWith(`${prefix}{`)).toBe(true);
|
||
expect(JSON.parse(configured.slice(prefix.length))).toMatchObject({
|
||
mcpServers: {
|
||
peer: { command: "/usr/bin/true", args: [] },
|
||
rea: { command: expect.any(String) },
|
||
},
|
||
});
|
||
const doctor = await run(doctorArguments);
|
||
expect(doctor.json).toMatchObject({
|
||
identity: {
|
||
registrations: [
|
||
expect.objectContaining({
|
||
client: "claude_code",
|
||
state: "aligned",
|
||
}),
|
||
],
|
||
},
|
||
});
|
||
},
|
||
);
|