fix(cc-config): read .claude.json from the same root the CLI writes it to
getClaudeJsonPath() was the only one of cc-discovery's 20 read paths not derived from getClaudeHome(), so a CLAUDE_HOME install read one file while the CLI wrote another. The default case was fixed by dropping the CLAUDE_CONFIG_DIR pin; this closes the mirrored half. Also replaces the Remove button's display-string comparison with a removable flag from the reader: rewording the source label silently resurrected a button that always fails.
This commit is contained in:
@@ -2651,6 +2651,11 @@ export interface CcMcpServer {
|
|||||||
name: string;
|
name: string;
|
||||||
/** Which config file this entry came from (e.g. a `.mcp.json` path). */
|
/** Which config file this entry came from (e.g. a `.mcp.json` path). */
|
||||||
source: string;
|
source: string;
|
||||||
|
/**
|
||||||
|
* False when no `claude mcp` scope can address this entry, so the dashboard
|
||||||
|
* cannot remove it. Absent means removable — only the reader sets it.
|
||||||
|
*/
|
||||||
|
removable?: boolean;
|
||||||
/** Transport: local subprocess ("stdio"), remote HTTP, or undetermined. */
|
/** Transport: local subprocess ("stdio"), remote HTTP, or undetermined. */
|
||||||
kind: "stdio" | "http" | "unknown";
|
kind: "stdio" | "http" | "unknown";
|
||||||
/** Launch command, for `kind === "stdio"`. */
|
/** Launch command, for `kind === "stdio"`. */
|
||||||
|
|||||||
@@ -2022,10 +2022,11 @@ function McpCard({
|
|||||||
}) {
|
}) {
|
||||||
const [confirmRemove, setConfirmRemove] = useState(false);
|
const [confirmRemove, setConfirmRemove] = useState(false);
|
||||||
const busyId = `${scope}:${server.name}`;
|
const busyId = `${scope}:${server.name}`;
|
||||||
// Entries from ~/.claude/settings.json have no CLI scope that addresses
|
// The reader decides this: entries from ~/.claude/settings.json have no CLI
|
||||||
// them (`claude mcp remove` only ever touches ~/.claude.json / .mcp.json)
|
// scope that addresses them, so a Remove button would look live and never
|
||||||
// — a Remove button here would look live and never work.
|
// work. Reading it off a flag rather than matching `source` keeps a reworded
|
||||||
const removable = server.source !== "~/.claude/settings.json";
|
// display string from silently bringing that button back.
|
||||||
|
const removable = server.removable !== false;
|
||||||
return (
|
return (
|
||||||
<div className="rounded-lg border border-border bg-surface-2 px-4 py-3">
|
<div className="rounded-lg border border-border bg-surface-2 px-4 py-3">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
|||||||
@@ -224,6 +224,29 @@ describe("cc-plugin-cli CLAUDE_CONFIG_DIR pinning", () => {
|
|||||||
else process.env.CLAUDE_HOME = originalHome;
|
else process.env.CLAUDE_HOME = originalHome;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The two env tests above pin one half each. This pins the pair: the file
|
||||||
|
// the dashboard READS must be the file the CLI WRITES, under both settings.
|
||||||
|
// `.claude.json` was the only read path in cc-discovery not derived from
|
||||||
|
// getClaudeHome(), which is exactly how the two halves drifted apart.
|
||||||
|
it("reads the same .claude.json the CLI writes, with CLAUDE_HOME set or unset", () => {
|
||||||
|
const cc = require("../lib/cc-discovery");
|
||||||
|
const originalHome = process.env.CLAUDE_HOME;
|
||||||
|
|
||||||
|
// CLI's write target = <CLAUDE_CONFIG_DIR>/.claude.json, and
|
||||||
|
// CLAUDE_CONFIG_DIR is either CLAUDE_HOME or the CLI's own default,
|
||||||
|
// which is the same directory getClaudeHome() returns.
|
||||||
|
try {
|
||||||
|
delete process.env.CLAUDE_HOME;
|
||||||
|
assert.equal(cc.getClaudeJsonPath(), path.join(os.homedir(), ".claude", ".claude.json"));
|
||||||
|
|
||||||
|
process.env.CLAUDE_HOME = "/tmp/some-other-claude-home";
|
||||||
|
assert.equal(cc.getClaudeJsonPath(), "/tmp/some-other-claude-home/.claude.json");
|
||||||
|
} finally {
|
||||||
|
if (originalHome === undefined) delete process.env.CLAUDE_HOME;
|
||||||
|
else process.env.CLAUDE_HOME = originalHome;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("cc-plugin-cli run outcomes", () => {
|
describe("cc-plugin-cli run outcomes", () => {
|
||||||
|
|||||||
+15
-10
@@ -29,16 +29,13 @@ function getProjectClaudeDir(cwd) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getClaudeJsonPath() {
|
function getClaudeJsonPath() {
|
||||||
// ~/.claude.json sits beside ~/.claude/, NOT inside it. Resolve from $HOME
|
// One root for everything: this file used to resolve from $HOME while the
|
||||||
// so a CLAUDE_HOME override doesn't accidentally relocate it.
|
// other 19 read paths derived from getClaudeHome(), and cc-plugin-cli.js's
|
||||||
//
|
// runClaude() pinned CLAUDE_CONFIG_DIR to getClaudeHome() — which is where
|
||||||
// Paired with cc-plugin-cli.js's runClaude(): with CLAUDE_HOME unset (the
|
// the CLI puts .claude.json. The dashboard read one file and made the CLI
|
||||||
// default), it deliberately leaves CLAUDE_CONFIG_DIR unset too, so the CLI
|
// write another, so MCP add/remove was inert. Keep this and runClaude's
|
||||||
// writes here (its own default). With CLAUDE_HOME set, this read stays
|
// env in step; they are the same decision seen from two sides.
|
||||||
// pinned to $HOME/.claude.json while the CLI writes
|
return path.join(getClaudeHome(), ".claude.json");
|
||||||
// <CLAUDE_HOME>/.claude.json — a known, unfixed split for that case; see
|
|
||||||
// runClaude's comment.
|
|
||||||
return path.join(os.homedir(), ".claude.json");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -516,6 +513,11 @@ function readMcpServers(opts = {}) {
|
|||||||
out.user.push({
|
out.user.push({
|
||||||
name,
|
name,
|
||||||
source: "~/.claude/settings.json",
|
source: "~/.claude/settings.json",
|
||||||
|
// No `claude mcp` scope addresses settings.json, so the dashboard
|
||||||
|
// cannot remove these. A flag, not a string comparison on `source`:
|
||||||
|
// the UI used to match that display text, so rewording it silently
|
||||||
|
// brought back a Remove button that always fails.
|
||||||
|
removable: false,
|
||||||
...summarizeMcpDef(def),
|
...summarizeMcpDef(def),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -911,6 +913,9 @@ function readOverview(opts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
// Exported so a test can pin it against the file the CLI writes — the two
|
||||||
|
// drifted apart once and the mismatch was invisible from either side alone.
|
||||||
|
getClaudeJsonPath,
|
||||||
// surface readers
|
// surface readers
|
||||||
readSkills,
|
readSkills,
|
||||||
readAgents,
|
readAgents,
|
||||||
|
|||||||
Reference in New Issue
Block a user