mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
fix(setup): resolve empty Claude profile paths consistently
Use the default home registration and personal skill roots for an empty profile override. Share marker and skill path resolution while preserving nonempty caller paths, backups and idempotence. Refs #4262, #4270 Co-authored-by: Polly Labs <pollychen.lab@gmail.com>
This commit is contained in:
1 parent
862a0b2631
commit
79e83cbd67
3 files changed
+128
-4
No files matched your search
@@ -179,6 +179,8 @@ as well, so it is broader than this Claude-only migration.
|
||||
history under `CLAUDE_CONFIG_DIR`. Setup follows the same
|
||||
variable: it writes the registration to `$CLAUDE_CONFIG_DIR/.claude.json` and
|
||||
the skill to `$CLAUDE_CONFIG_DIR/skills`.
|
||||
For REA setup, an empty `CLAUDE_CONFIG_DIR` is treated as unset: registration
|
||||
goes to `~/.claude.json` and the skill goes to `~/.claude/skills`.
|
||||
|
||||
```bash
|
||||
CLAUDE_CONFIG_DIR=~/.claude-rea npx rea-agents setup --client claude_code
|
||||
|
||||
@@ -159,10 +159,10 @@ const claudeDesktopLocation = (
|
||||
};
|
||||
|
||||
const claudeCodeConfigDirectory = ({ home, env }: ClientPathContext): string =>
|
||||
env.CLAUDE_CONFIG_DIR ?? home;
|
||||
env.CLAUDE_CONFIG_DIR || home;
|
||||
|
||||
const claudeCodeMarkerDirectory = ({ home, env }: ClientPathContext): string =>
|
||||
env.CLAUDE_CONFIG_DIR ?? join(home, ".claude");
|
||||
env.CLAUDE_CONFIG_DIR || join(home, ".claude");
|
||||
|
||||
const codexDirectory = ({ home, env }: ClientPathContext): string =>
|
||||
env.CODEX_HOME || join(home, ".codex");
|
||||
@@ -486,8 +486,8 @@ export const SUPPORTED_CLIENT_DEFINITIONS = [
|
||||
{
|
||||
name: "claude_code",
|
||||
displayName: "Claude Code",
|
||||
skillPath: ({ home, env }: ClientPathContext) =>
|
||||
join(env.CLAUDE_CONFIG_DIR ?? join(home, ".claude"), "skills"),
|
||||
skillPath: (context: ClientPathContext) =>
|
||||
join(claudeCodeMarkerDirectory(context), "skills"),
|
||||
configPath: (context: ClientPathContext) =>
|
||||
join(claudeCodeConfigDirectory(context), ".claude.json"),
|
||||
markerPath: claudeCodeMarkerDirectory,
|
||||
|
||||
@@ -6,6 +6,128 @@ import { PRODUCT_IDENTITY } from "../../../src/identity.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"Claude Code routes an empty config directory to its home registration",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-claude-empty-config-dir-");
|
||||
const home = join(root, "home");
|
||||
const cwd = join(root, "work");
|
||||
const config = join(home, ".claude.json");
|
||||
const backup = `${config}.rea.backup`;
|
||||
const skill = join(home, ".claude", "skills", PRODUCT_IDENTITY.skillName);
|
||||
const cwdSkill = join(cwd, "skills", PRODUCT_IDENTITY.skillName);
|
||||
const original = JSON.stringify({
|
||||
theme: "dark",
|
||||
mcpServers: { other: { command: "other" } },
|
||||
});
|
||||
await mkdir(home, { recursive: true });
|
||||
await mkdir(cwd, { recursive: true });
|
||||
await writeFile(config, original);
|
||||
const run = (arguments_: readonly string[]) =>
|
||||
cli.run({
|
||||
arguments: arguments_,
|
||||
cwd,
|
||||
environment: {
|
||||
HOME: home,
|
||||
USERPROFILE: home,
|
||||
CLAUDE_CONFIG_DIR: "",
|
||||
},
|
||||
timeoutMs: 20_000,
|
||||
});
|
||||
const setup = (flags: readonly string[] = []) =>
|
||||
run(["setup", "--client", "claude_code", ...flags, "--json"]);
|
||||
|
||||
const planned = await setup(["--dry-run"]);
|
||||
expect(planned.exitCode, planned.stderr).toBe(0);
|
||||
expect(planned.json).toMatchObject({
|
||||
status: "planned",
|
||||
plannedActions: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "configure_client:claude_code",
|
||||
target: config,
|
||||
backupPath: backup,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
id: "install_skill:claude_code",
|
||||
target: skill,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
expect(await readFile(config, "utf8")).toBe(original);
|
||||
await expect(access(join(cwd, ".claude.json"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(access(join(skill, "SKILL.md"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(access(join(cwdSkill, "SKILL.md"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(access(backup)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
|
||||
const applied = await setup(["--yes"]);
|
||||
expect(applied.exitCode, applied.stderr).toBe(0);
|
||||
expect(applied.json).toMatchObject({ status: "ready" });
|
||||
expect(await readFile(backup, "utf8")).toBe(original);
|
||||
expect(parse(await readFile(config, "utf8"))).toMatchObject({
|
||||
theme: "dark",
|
||||
mcpServers: {
|
||||
other: { command: "other" },
|
||||
rea: expect.any(Object),
|
||||
},
|
||||
});
|
||||
await expect(access(join(skill, "SKILL.md"))).resolves.toBeUndefined();
|
||||
await expect(access(join(cwd, ".claude.json"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(access(join(cwdSkill, "SKILL.md"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
|
||||
const repeat = await setup(["--yes"]);
|
||||
expect(repeat.exitCode, repeat.stderr).toBe(0);
|
||||
expect(repeat.json).toMatchObject({
|
||||
status: "ready",
|
||||
appliedActions: [],
|
||||
});
|
||||
expect(await readFile(backup, "utf8")).toBe(original);
|
||||
const doctor = await run([
|
||||
"doctor",
|
||||
"--client",
|
||||
"claude_code",
|
||||
"--skill",
|
||||
"--json",
|
||||
]);
|
||||
expect(doctor.exitCode, doctor.stderr).toBe(0);
|
||||
expect(doctor.json).toMatchObject({
|
||||
healthy: true,
|
||||
identity: {
|
||||
skill: { state: "aligned" },
|
||||
registrations: expect.arrayContaining([
|
||||
expect.objectContaining({ client: "claude_code", state: "aligned" }),
|
||||
]),
|
||||
},
|
||||
});
|
||||
|
||||
const uninstall = await run(["uninstall", "--json"]);
|
||||
expect(uninstall.exitCode, uninstall.stderr).toBe(0);
|
||||
expect(uninstall.json).toMatchObject({ status: "complete" });
|
||||
expect(parse(await readFile(config, "utf8"))).toEqual({
|
||||
theme: "dark",
|
||||
mcpServers: { other: { command: "other" } },
|
||||
});
|
||||
await expect(access(join(skill, "SKILL.md"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(access(join(cwd, ".claude.json"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
await expect(access(join(cwdSkill, "SKILL.md"))).rejects.toMatchObject({
|
||||
code: "ENOENT",
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
for (const policy of [{ excluded: ["rea"] }, { allowed: ["other"] }]) {
|
||||
cliTest(
|
||||
`Gemini refuses a registration blocked by ${Object.keys(policy)[0]}`,
|
||||
|
||||
Reference in new issue
Block a user