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:
morlutoandPolly Labs committed 2026-10-16 07:25:21 +08:00
1 parent 862a0b2631
commit 79e83cbd67
3 files changed
+128 -4

No files matched your search

+2
View File
@@ -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
+4 -4
View File
@@ -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,
+122
View File
@@ -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]}`,