refactor!: rebrand CCAM to AgentHub

Clean break, no compatibility alias. Renames the product name, the CLI
binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` ->
`AGENTHUB_`), the Prometheus metric namespace (`ccam_*` ->
`agenthub_*`), the tmux session prefix (`ccam-lane-<id>` ->
`agenthub-lane-<id>`), the per-repo runtime profile directory
(`.ccam/profile` -> `.agenthub/profile`), the lanes state root
(`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled
plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash
commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` ->
`/agenthub:*`).

Because there is no alias, an existing install has to be updated by
hand: rename its `.ccam/profile` directory and any `CCAM_*` variables,
and reinstall the plugin under its new name.

Two other name families are deliberately left alone here, since they are
not just strings: the `Claude-Code-Monitor` repository/marketplace name,
and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard`
runtime directory, which holds the live database and needs a migration
rather than a rename.

Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across
tracked files, `openapi.yaml` regenerated (95 paths, unchanged count).
This commit is contained in:
2026-09-07 12:09:15 +07:00
parent ae9b3dbcd5
commit 17d56d0e01
345 changed files with 2705 additions and 2727 deletions
+21 -21
View File
@@ -3,7 +3,7 @@
* makes a plugin install self-sufficient. Covers the fast path, the Node
* version gate, stale-lock reclaim, legacy hook removal (the duplicate-hook
* double-counting bug) and the CLI launcher's refusal to clobber a foreign
* `ccam`. Every path is exercised against injected temp directories — nothing
* `agenthub`. Every path is exercised against injected temp directories — nothing
* here touches the real $HOME.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
@@ -14,7 +14,7 @@ const fs = require("fs");
const os = require("os");
const path = require("path");
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-bootstrap-"));
const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-bootstrap-"));
process.env.CLAUDE_HOME = TMP_HOME;
delete process.env.DASHBOARD_DATA_DIR;
@@ -135,17 +135,17 @@ describe("skill-plugin auto-install gate", () => {
// Both cases here must return WITHOUT reaching server/lib/skill-plugins.js —
// that module shells out to `claude plugin install`, and no test may.
beforeEach(resetRuntime);
after(() => delete process.env.CCAM_AUTO_INSTALL_PLUGINS);
after(() => delete process.env.AGENTHUB_AUTO_INSTALL_PLUGINS);
for (const flag of ["0", "false", "off"]) {
it(`is off when CCAM_AUTO_INSTALL_PLUGINS=${flag}`, () => {
process.env.CCAM_AUTO_INSTALL_PLUGINS = flag;
it(`is off when AGENTHUB_AUTO_INSTALL_PLUGINS=${flag}`, () => {
process.env.AGENTHUB_AUTO_INSTALL_PLUGINS = flag;
assert.equal(boot.autoInstallSkillPlugins(RT), "off");
});
}
it("runs once: a settled state is returned verbatim, not re-attempted", () => {
delete process.env.CCAM_AUTO_INSTALL_PLUGINS;
delete process.env.AGENTHUB_AUTO_INSTALL_PLUGINS;
boot.writeState({ skillPluginsSettled: "installed" }, RT);
// Verbatim matters: writeState records whatever comes back, so a marker
@@ -153,8 +153,8 @@ describe("skill-plugin auto-install gate", () => {
assert.equal(boot.autoInstallSkillPlugins(RT), "installed");
});
it("is off under the flag even when forced, so /ccam-update honours the opt-out", () => {
process.env.CCAM_AUTO_INSTALL_PLUGINS = "0";
it("is off under the flag even when forced, so /agenthub-update honours the opt-out", () => {
process.env.AGENTHUB_AUTO_INSTALL_PLUGINS = "0";
boot.writeState({ skillPluginsSettled: "failed" }, RT);
assert.equal(boot.autoInstallSkillPlugins(RT, { force: true }), "off");
});
@@ -200,7 +200,7 @@ describe("legacy hook cleanup", () => {
beforeEach(() => {
fs.rmSync(settings, { force: true });
fs.rmSync(`${settings}.ccam-bak`, { force: true });
fs.rmSync(`${settings}.agenthub-bak`, { force: true });
});
it("removes checkout-installed hook entries and backs the file up first", () => {
@@ -234,7 +234,7 @@ describe("legacy hook cleanup", () => {
assert.equal(after.hooks.PreToolUse.length, 1, "unrelated hooks must survive");
assert.match(JSON.stringify(after.hooks.PreToolUse), /other\/tool\.js/);
assert.equal(after.hooks.SessionStart, undefined, "an emptied list is dropped");
assert.ok(fs.existsSync(`${settings}.ccam-bak`), "must back up before writing");
assert.ok(fs.existsSync(`${settings}.agenthub-bak`), "must back up before writing");
});
it("never removes the plugin's own cache-resolved hook entries", () => {
@@ -249,7 +249,7 @@ describe("legacy hook cleanup", () => {
"plugins",
"cache",
"claude-code-agent-monitor-plugins",
"ccam",
"agenthub",
"abc123",
"scripts",
"hook-handler.js"
@@ -280,10 +280,10 @@ describe("legacy hook cleanup", () => {
assert.match(JSON.stringify(after.hooks.PreToolUse), /plugins.*cache/);
});
it("leaves a file with no CCAM hooks untouched and writes no backup", () => {
it("leaves a file with no AgentHub hooks untouched and writes no backup", () => {
fs.writeFileSync(settings, JSON.stringify({ hooks: { Stop: [{ hooks: [] }] } }));
assert.equal(boot.stripLegacyHooks(settings), 0);
assert.equal(fs.existsSync(`${settings}.ccam-bak`), false);
assert.equal(fs.existsSync(`${settings}.agenthub-bak`), false);
});
it("is a no-op when no settings file exists", () => {
@@ -291,17 +291,17 @@ describe("legacy hook cleanup", () => {
});
});
describe("ccam CLI launcher", () => {
describe("agenthub CLI launcher", () => {
const bin = path.join(TMP_HOME, "bin");
const target = path.join(bin, process.platform === "win32" ? "ccam.cmd" : "ccam");
const target = path.join(bin, process.platform === "win32" ? "agenthub.cmd" : "agenthub");
beforeEach(() => {
fs.rmSync(bin, { recursive: true, force: true });
});
it("writes a launcher that points at the plugin's bin/ccam.js", () => {
it("writes a launcher that points at the plugin's bin/agenthub.js", () => {
assert.equal(boot.linkCli(bin), "written");
assert.match(fs.readFileSync(target, "utf8"), /bin[/\\]ccam\.js/);
assert.match(fs.readFileSync(target, "utf8"), /bin[/\\]agenthub\.js/);
});
it("is idempotent", () => {
@@ -309,9 +309,9 @@ describe("ccam CLI launcher", () => {
assert.equal(boot.linkCli(bin), "exists");
});
it("never clobbers a ccam it did not write (a linked checkout keeps winning)", () => {
it("never clobbers a agenthub it did not write (a linked checkout keeps winning)", () => {
fs.mkdirSync(bin, { recursive: true });
fs.writeFileSync(target, '#!/bin/sh\nexec node /my/checkout/bin/ccam.js "$@"\n');
fs.writeFileSync(target, '#!/bin/sh\nexec node /my/checkout/bin/agenthub.js "$@"\n');
assert.equal(boot.linkCli(bin), "foreign");
assert.match(fs.readFileSync(target, "utf8"), /my\/checkout/);
});
@@ -319,10 +319,10 @@ describe("ccam CLI launcher", () => {
describe("plugin-open / plugin-bootstrap require cycle", () => {
// bootstrap() lazily requires plugin-open for buildClient, and plugin-open
// requires bootstrap at load. When plugin-open is the entry point (/ccam-open)
// requires bootstrap at load. When plugin-open is the entry point (/agenthub-open)
// that lazy require resolves to whatever sits on its module.exports at that
// instant — so the assignment must come before the entry block that calls
// bootstrap(), or /ccam-open dies with "buildClient is not a function".
// bootstrap(), or /agenthub-open dies with "buildClient is not a function".
const src = fs.readFileSync(
path.join(__dirname, "..", "..", "scripts", "plugin-open.js"),
"utf8"