fix(cc-config): stop pinning CLAUDE_CONFIG_DIR when CLAUDE_HOME is unset
runClaude() unconditionally set CLAUDE_CONFIG_DIR to getClaudeHome() (~/.claude), so the CLI wrote ~/.claude/.claude.json — a file cc-discovery.getClaudeJsonPath() never reads (it reads ~/.claude.json, beside ~/.claude/, by design). Every default-install MCP add/remove and plugin/marketplace mutation was inert: add toasted success and vanished on refetch, remove 422'd for every server the panel lists. With CLAUDE_HOME unset (the default), leave CLAUDE_CONFIG_DIR unset too — the CLI's own default config dir already lines up with what cc-discovery reads, so alignment comes from doing nothing. Only pin it when the operator explicitly set CLAUDE_HOME, which is the one case the pin is meant for. Left a residual, documented at both sites: with CLAUDE_HOME set, reads still come from $HOME/.claude.json while the CLI writes <CLAUDE_HOME>/.claude.json — a split predating this task, affecting a configuration nobody here runs. New tests pin the env contract directly (no real CLI needed): runClaude must not set CLAUDE_CONFIG_DIR when CLAUDE_HOME is absent, and must set it to CLAUDE_HOME's value when present.
This commit is contained in:
@@ -190,6 +190,42 @@ describe("cc-plugin-cli argv construction", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("cc-plugin-cli CLAUDE_CONFIG_DIR pinning", () => {
|
||||
// N1: pinning CLAUDE_CONFIG_DIR unconditionally to getClaudeHome() made the
|
||||
// CLI write ~/.claude/.claude.json while cc-discovery reads ~/.claude.json
|
||||
// — a different file, so every default-install add/remove was inert. The
|
||||
// fix is to leave CLAUDE_CONFIG_DIR unset unless the operator explicitly
|
||||
// set CLAUDE_HOME; this is the env contract that would have caught it.
|
||||
const PRINT_ENV = "printf '%s\\n' \"CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR-<unset>}\"";
|
||||
|
||||
it("leaves CLAUDE_CONFIG_DIR unset when CLAUDE_HOME is not set", async () => {
|
||||
const originalHome = process.env.CLAUDE_HOME;
|
||||
delete process.env.CLAUDE_HOME;
|
||||
const restore = stubClaude(PRINT_ENV);
|
||||
try {
|
||||
const result = await cli.marketplaceUpdate({ name: "m" });
|
||||
assert.equal(result.stdout.trim(), "CLAUDE_CONFIG_DIR=<unset>");
|
||||
} finally {
|
||||
restore();
|
||||
if (originalHome !== undefined) process.env.CLAUDE_HOME = originalHome;
|
||||
}
|
||||
});
|
||||
|
||||
it("pins CLAUDE_CONFIG_DIR to CLAUDE_HOME when the operator set it", async () => {
|
||||
const originalHome = process.env.CLAUDE_HOME;
|
||||
process.env.CLAUDE_HOME = "/tmp/some-other-claude-home";
|
||||
const restore = stubClaude(PRINT_ENV);
|
||||
try {
|
||||
const result = await cli.marketplaceUpdate({ name: "m" });
|
||||
assert.equal(result.stdout.trim(), "CLAUDE_CONFIG_DIR=/tmp/some-other-claude-home");
|
||||
} finally {
|
||||
restore();
|
||||
if (originalHome === undefined) delete process.env.CLAUDE_HOME;
|
||||
else process.env.CLAUDE_HOME = originalHome;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("cc-plugin-cli run outcomes", () => {
|
||||
it("reports a non-zero exit as ok:false instead of throwing", async () => {
|
||||
const restore = stubClaude('echo "✘ Failed to install plugin \\"nope\\"" >&2\nexit 1');
|
||||
|
||||
@@ -31,6 +31,13 @@ function getProjectClaudeDir(cwd) {
|
||||
function getClaudeJsonPath() {
|
||||
// ~/.claude.json sits beside ~/.claude/, NOT inside it. Resolve from $HOME
|
||||
// so a CLAUDE_HOME override doesn't accidentally relocate it.
|
||||
//
|
||||
// Paired with cc-plugin-cli.js's runClaude(): with CLAUDE_HOME unset (the
|
||||
// default), it deliberately leaves CLAUDE_CONFIG_DIR unset too, so the CLI
|
||||
// writes here (its own default). With CLAUDE_HOME set, this read stays
|
||||
// pinned to $HOME/.claude.json while the CLI writes
|
||||
// <CLAUDE_HOME>/.claude.json — a known, unfixed split for that case; see
|
||||
// runClaude's comment.
|
||||
return path.join(os.homedir(), ".claude.json");
|
||||
}
|
||||
|
||||
|
||||
@@ -131,15 +131,27 @@ function serialize(fn) {
|
||||
function runClaude(args, opts = {}) {
|
||||
const timeout = opts.timeoutMs || DEFAULT_TIMEOUT_MS;
|
||||
return new Promise((resolve, reject) => {
|
||||
// Only pin CLAUDE_CONFIG_DIR when the operator explicitly set CLAUDE_HOME
|
||||
// (a non-default install). Left unset, the CLI's own default config dir
|
||||
// is `~/.claude` and it writes `.claude.json` NEXT TO it (`~/.claude.json`)
|
||||
// — the exact file cc-discovery.getClaudeJsonPath() reads (see that
|
||||
// function's own comment). Pinning CLAUDE_CONFIG_DIR unconditionally to
|
||||
// getClaudeHome() made the CLI write `~/.claude/.claude.json` instead —
|
||||
// a file nothing reads — so every default-install add/remove was inert.
|
||||
// When CLAUDE_HOME IS set, this still pins it, but note the residual:
|
||||
// cc-discovery's read stays pinned to $HOME/.claude.json regardless (see
|
||||
// getClaudeJsonPath's comment) while the CLI then writes
|
||||
// <CLAUDE_HOME>/.claude.json — a pre-existing split this task does not
|
||||
// attempt to fix, since no install in this repo runs with CLAUDE_HOME set.
|
||||
const env = { ...process.env };
|
||||
if (process.env.CLAUDE_HOME) env.CLAUDE_CONFIG_DIR = getClaudeHome();
|
||||
const child = execFile(
|
||||
"claude",
|
||||
args,
|
||||
{
|
||||
timeout,
|
||||
maxBuffer: MAX_OUTPUT_BYTES,
|
||||
// Pin the CLI to the same config dir cc-discovery reads, so a write
|
||||
// made here is a write the dashboard can see.
|
||||
env: { ...process.env, CLAUDE_CONFIG_DIR: getClaudeHome() },
|
||||
env,
|
||||
},
|
||||
(err, stdout, stderr) => {
|
||||
const out = String(stdout || "");
|
||||
|
||||
Reference in New Issue
Block a user