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:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @file Tests for the `ccam stage` / `ccam lanes` CLI subcommands: lane
|
||||
* @file Tests for the `agenthub stage` / `agenthub lanes` CLI subcommands: lane
|
||||
* resolution from the current directory, the stage round-trip through the HTTP
|
||||
* API, and the non-zero exit when the cwd belongs to no lane.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
@@ -14,10 +14,10 @@ const http = require("http");
|
||||
const { spawn } = require("child_process");
|
||||
|
||||
const TEST_DB = path.join(os.tmpdir(), `dashboard-lanes-cli-${Date.now()}-${process.pid}.db`);
|
||||
const PIPELINE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-pipeline-fixture-"));
|
||||
const LANES_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-managed-lanes-cli-"));
|
||||
const SOURCE_REPO = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-worktree-source-"));
|
||||
const REMOTE_REPO = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-worktree-remote-"));
|
||||
const PIPELINE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-pipeline-fixture-"));
|
||||
const LANES_ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-managed-lanes-cli-"));
|
||||
const SOURCE_REPO = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-worktree-source-"));
|
||||
const REMOTE_REPO = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-worktree-remote-"));
|
||||
process.env.DASHBOARD_DB_PATH = TEST_DB;
|
||||
process.env.DASHBOARD_PIPELINES_DIR = PIPELINE_DIR;
|
||||
process.env.DASHBOARD_REMOTE_SYNC_MS = "0";
|
||||
@@ -39,8 +39,8 @@ fs.writeFileSync(
|
||||
|
||||
const { createApp, startServer } = require("../index");
|
||||
|
||||
const LANE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-cli-"));
|
||||
const CLI = path.join(__dirname, "..", "..", "bin", "ccam.js");
|
||||
const LANE_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-cli-"));
|
||||
const CLI = path.join(__dirname, "..", "..", "bin", "agenthub.js");
|
||||
|
||||
let server;
|
||||
let BASE;
|
||||
@@ -188,7 +188,7 @@ after(() => {
|
||||
}
|
||||
});
|
||||
|
||||
describe("ccam stage", () => {
|
||||
describe("agenthub stage", () => {
|
||||
it("reports a stage for the lane owning the current directory", async () => {
|
||||
const r = await cli(["stage", "review", "--evidence", "3 findings"], LANE_DIR);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -231,9 +231,9 @@ describe("ccam stage", () => {
|
||||
|
||||
// A cwd can back one `claude` lane AND one `codex` lane (idx_lanes_cwd_tool),
|
||||
// so cwd alone no longer identifies a lane. Picking the first of the tie means
|
||||
// a codex session's `ccam stage` silently drives the claude lane's card.
|
||||
describe("ccam stage in a directory shared by two lanes", () => {
|
||||
const DUAL_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-dual-lane-"));
|
||||
// a codex session's `agenthub stage` silently drives the claude lane's card.
|
||||
describe("agenthub stage in a directory shared by two lanes", () => {
|
||||
const DUAL_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-dual-lane-"));
|
||||
let claudeLane;
|
||||
let codexLane;
|
||||
// The suite itself runs under Claude Code, whose CLAUDECODE marker would
|
||||
@@ -251,10 +251,10 @@ describe("ccam stage in a directory shared by two lanes", () => {
|
||||
return (await get(`/api/lanes/${id}`)).body.lane.stage;
|
||||
}
|
||||
|
||||
it("CCAM_LANE_ID wins — a dashboard-started pane always knows its own lane", async () => {
|
||||
it("AGENTHUB_LANE_ID wins — a dashboard-started pane always knows its own lane", async () => {
|
||||
const r = await cli(["stage", "gate"], DUAL_DIR, {
|
||||
...NO_HARNESS,
|
||||
CCAM_LANE_ID: String(codexLane.id),
|
||||
AGENTHUB_LANE_ID: String(codexLane.id),
|
||||
});
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.equal(await stageOf(codexLane.id), "gate");
|
||||
@@ -287,7 +287,7 @@ describe("ccam stage in a directory shared by two lanes", () => {
|
||||
});
|
||||
|
||||
it("refuses when the requested tool owns no lane here", async () => {
|
||||
const solo = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-solo-lane-"));
|
||||
const solo = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-solo-lane-"));
|
||||
await post("/api/lanes", { cwd: solo, title: "solo claude" });
|
||||
const r = await cli(["stage", "plan", "--tool", "codex"], solo, NO_HARNESS);
|
||||
assert.notEqual(r.status, 0);
|
||||
@@ -302,20 +302,20 @@ describe("ccam stage in a directory shared by two lanes", () => {
|
||||
assert.notEqual(await stageOf(claudeLane.id), "review");
|
||||
});
|
||||
|
||||
it("positional numeric ID acts like --lane for commands that support it (e.g. ccam lanes pipeline)", async () => {
|
||||
// Commands like `ccam lanes pipeline` treat a leading numeric arg as the lane ID.
|
||||
// Other commands like `ccam stage` have their own positional argument and don't support it.
|
||||
it("positional numeric ID acts like --lane for commands that support it (e.g. agenthub lanes pipeline)", async () => {
|
||||
// Commands like `agenthub lanes pipeline` treat a leading numeric arg as the lane ID.
|
||||
// Other commands like `agenthub stage` have their own positional argument and don't support it.
|
||||
const r = await cli(["lanes", "pipeline", String(codexLane.id)], DUAL_DIR, NO_HARNESS);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.match(r.stdout, new RegExp(`lane #${codexLane.id}`));
|
||||
});
|
||||
|
||||
it("non-numeric CCAM_LANE_ID is ignored", async () => {
|
||||
// If CCAM_LANE_ID is garbage (not a number), it should be skipped and
|
||||
it("non-numeric AGENTHUB_LANE_ID is ignored", async () => {
|
||||
// If AGENTHUB_LANE_ID is garbage (not a number), it should be skipped and
|
||||
// fall through to --tool or env markers
|
||||
const r = await cli(["stage", "ship"], DUAL_DIR, {
|
||||
...NO_HARNESS,
|
||||
CCAM_LANE_ID: "not-a-number",
|
||||
AGENTHUB_LANE_ID: "not-a-number",
|
||||
CLAUDECODE: "1", // This env marker should pick claude lane
|
||||
});
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -323,27 +323,27 @@ describe("ccam stage in a directory shared by two lanes", () => {
|
||||
assert.notEqual(await stageOf(codexLane.id), "ship");
|
||||
});
|
||||
|
||||
it("a CCAM_LANE_ID naming no lane falls through instead of addressing it", async () => {
|
||||
it("a AGENTHUB_LANE_ID naming no lane falls through instead of addressing it", async () => {
|
||||
// A pane outlives the lane it was started for, and lane ids are reused
|
||||
// after a delete. Trusting the number blind would either 404 or, worse,
|
||||
// write this session's stage onto whatever lane inherited that id.
|
||||
const r = await cli(["stage", "gate"], DUAL_DIR, {
|
||||
...NO_HARNESS,
|
||||
CCAM_LANE_ID: "999999",
|
||||
AGENTHUB_LANE_ID: "999999",
|
||||
CLAUDECODE: "1",
|
||||
});
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.equal(await stageOf(claudeLane.id), "gate");
|
||||
});
|
||||
|
||||
it("a CCAM_LANE_ID for a lane outside this directory is not used", async () => {
|
||||
it("a AGENTHUB_LANE_ID for a lane outside this directory is not used", async () => {
|
||||
// The pane's lane is the truth only while the command runs inside it; an
|
||||
// agent that cd'd into a different repo must resolve by that directory.
|
||||
const elsewhere = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-elsewhere-"));
|
||||
const elsewhere = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-elsewhere-"));
|
||||
const other = (await post("/api/lanes", { cwd: elsewhere, title: "elsewhere" })).body.lane;
|
||||
const r = await cli(["stage", "review"], DUAL_DIR, {
|
||||
...NO_HARNESS,
|
||||
CCAM_LANE_ID: String(other.id),
|
||||
AGENTHUB_LANE_ID: String(other.id),
|
||||
CLAUDECODE: "1",
|
||||
});
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -352,9 +352,9 @@ describe("ccam stage in a directory shared by two lanes", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam lanes add", () => {
|
||||
describe("agenthub lanes add", () => {
|
||||
it("creates a lane and it appears in lanes list", async () => {
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-add-"));
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-add-"));
|
||||
try {
|
||||
const r = await cli(["lanes", "add", "--cwd", addDir, "--title", "CLI added"], addDir);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -369,7 +369,7 @@ describe("ccam lanes add", () => {
|
||||
});
|
||||
|
||||
it("defaults to 'default' pipeline when --pipeline is omitted", async () => {
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-no-pipeline-"));
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-no-pipeline-"));
|
||||
try {
|
||||
const r = await cli(["lanes", "add", "--cwd", addDir, "--title", "Default pipeline"], addDir);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
@@ -385,7 +385,7 @@ describe("ccam lanes add", () => {
|
||||
});
|
||||
|
||||
it("accepts --pipeline flag and creates lane with custom pipeline", async () => {
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-custom-pipeline-"));
|
||||
const addDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-custom-pipeline-"));
|
||||
try {
|
||||
const r = await cli(
|
||||
[
|
||||
@@ -458,7 +458,7 @@ describe("ccam lanes add", () => {
|
||||
});
|
||||
|
||||
it("reports the failure notes, not success, when provisioning fails", async () => {
|
||||
const emptyRepo = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-cli-unborn-repo-"));
|
||||
const emptyRepo = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-cli-unborn-repo-"));
|
||||
try {
|
||||
await git(["init", "-b", "main"], emptyRepo);
|
||||
const r = await cli(
|
||||
@@ -475,7 +475,7 @@ describe("ccam lanes add", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam lanes destructive lifecycle", () => {
|
||||
describe("agenthub lanes destructive lifecycle", () => {
|
||||
// Every test provisions its OWN lane. These tests used to share one mutable
|
||||
// `managedLane` across describe blocks, which meant they only ever exercised
|
||||
// whatever state the previous test happened to leave behind — the same
|
||||
@@ -557,7 +557,7 @@ describe("ccam lanes destructive lifecycle", () => {
|
||||
});
|
||||
|
||||
it("refuses to RESET an adopted lane but FORGETS it on remove, leaving the directory intact", async () => {
|
||||
const adoptedDir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-adopted-remove-"));
|
||||
const adoptedDir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-adopted-remove-"));
|
||||
const preserved = path.join(adoptedDir, "preserve.txt");
|
||||
fs.writeFileSync(preserved, "do not delete\n");
|
||||
try {
|
||||
@@ -602,9 +602,9 @@ describe("ccam lanes destructive lifecycle", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam lanes — inferred stage detection", () => {
|
||||
describe("agenthub lanes — inferred stage detection", () => {
|
||||
it("prints the inferred stage when detection leads the declared stage", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-detect-lead-"));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-detect-lead-"));
|
||||
try {
|
||||
const created = await post("/api/lanes", { cwd: dir, title: "Detect Lead" });
|
||||
const id = created.body.lane.id;
|
||||
@@ -626,7 +626,7 @@ describe("ccam lanes — inferred stage detection", () => {
|
||||
const r = await cli(["lanes"], dir);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
const line = r.stdout.split("\n").find((l) => l.startsWith(`#${id}`));
|
||||
assert.ok(line, "lane row not found in ccam lanes output");
|
||||
assert.ok(line, "lane row not found in agenthub lanes output");
|
||||
assert.match(line, /⇢ detected:tests/);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
@@ -634,7 +634,7 @@ describe("ccam lanes — inferred stage detection", () => {
|
||||
});
|
||||
|
||||
it("does not print an inferred stage when the declaration already leads", async () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-lane-detect-behind-"));
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-lane-detect-behind-"));
|
||||
try {
|
||||
const created = await post("/api/lanes", { cwd: dir, title: "Detect Behind" });
|
||||
const id = created.body.lane.id;
|
||||
@@ -658,7 +658,7 @@ describe("ccam lanes — inferred stage detection", () => {
|
||||
const r = await cli(["lanes"], dir);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
const line = r.stdout.split("\n").find((l) => l.startsWith(`#${id}`));
|
||||
assert.ok(line, "lane row not found in ccam lanes output");
|
||||
assert.ok(line, "lane row not found in agenthub lanes output");
|
||||
assert.doesNotMatch(line, /detected:/);
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
@@ -666,15 +666,15 @@ describe("ccam lanes — inferred stage detection", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam ship-full / ccam ship-quick", () => {
|
||||
describe("agenthub ship-full / agenthub ship-quick", () => {
|
||||
it("ship-quick prints the 8-stage pipeline's skill, ship-full the 16-stage one", async () => {
|
||||
const quick = await cli(["ship-quick", "add", "an", "export", "button", "--print"], LANE_DIR);
|
||||
assert.equal(quick.status, 0, quick.stderr);
|
||||
assert.match(quick.stdout, /claude "\/ccam:ship-feature add an export button"/);
|
||||
assert.match(quick.stdout, /claude "\/agenthub:ship-feature add an export button"/);
|
||||
|
||||
const full = await cli(["ship-full", "add", "an", "export", "button", "--print"], LANE_DIR);
|
||||
assert.equal(full.status, 0, full.stderr);
|
||||
assert.match(full.stdout, /claude "\/ccam:ship-feature-lane add an export button"/);
|
||||
assert.match(full.stdout, /claude "\/agenthub:ship-feature-lane add an export button"/);
|
||||
});
|
||||
|
||||
it("--print switches nothing: the lane keeps whatever template it was on", async () => {
|
||||
@@ -689,21 +689,21 @@ describe("ccam ship-full / ccam ship-quick", () => {
|
||||
it("exits non-zero with the command's own usage when no requirement is given", async () => {
|
||||
const r = await cli(["ship-full"], LANE_DIR);
|
||||
assert.notEqual(r.status, 0);
|
||||
assert.match(r.stderr, /usage: ccam ship-full/);
|
||||
assert.match(r.stderr, /usage: agenthub ship-full/);
|
||||
|
||||
const q = await cli(["ship-quick"], LANE_DIR);
|
||||
assert.notEqual(q.status, 0);
|
||||
assert.match(q.stderr, /usage: ccam ship-quick/);
|
||||
assert.match(q.stderr, /usage: agenthub ship-quick/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam lanes node", () => {
|
||||
describe("agenthub lanes node", () => {
|
||||
it("with no node id, lists every node and its command, and exits non-zero with usage", async () => {
|
||||
const r = await cli(["lanes", "node"], LANE_DIR);
|
||||
assert.notEqual(r.status, 0);
|
||||
assert.match(r.stdout, /\bintake\b/);
|
||||
assert.match(r.stdout, /\bdone\b/);
|
||||
assert.match(r.stderr, /usage: ccam lanes node/);
|
||||
assert.match(r.stderr, /usage: agenthub lanes node/);
|
||||
});
|
||||
|
||||
it("--print shows a slash-command node as a claude prompt, extra args appended", async () => {
|
||||
@@ -717,7 +717,7 @@ describe("ccam lanes node", () => {
|
||||
await cli(["lanes", "pipeline", "ship-feature"], LANE_DIR);
|
||||
const r = await cli(["lanes", "node", "gates", "--print"], LANE_DIR);
|
||||
assert.equal(r.status, 0, r.stderr);
|
||||
assert.match(r.stdout, /"ccam" "lanes" "hook" "ci-gate"/);
|
||||
assert.match(r.stdout, /"agenthub" "lanes" "hook" "ci-gate"/);
|
||||
});
|
||||
|
||||
it("every node of both shipped templates declares a runnable command", async () => {
|
||||
@@ -731,7 +731,7 @@ describe("ccam lanes node", () => {
|
||||
|
||||
it("matches a node by alias, so one slash command works on either template", async () => {
|
||||
// `default` calls the step `implement`, `ship-feature` calls it
|
||||
// `implementing` — /ccam:implement must not depend on which one is active.
|
||||
// `implementing` — /agenthub:implement must not depend on which one is active.
|
||||
await cli(["lanes", "pipeline", "ship-feature"], LANE_DIR);
|
||||
const ship = await cli(["lanes", "node", "implement", "--print"], LANE_DIR);
|
||||
assert.equal(ship.status, 0, ship.stderr);
|
||||
@@ -751,12 +751,12 @@ describe("ccam lanes node", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("ccam lanes ci status", () => {
|
||||
describe("agenthub lanes ci status", () => {
|
||||
let restorePath;
|
||||
let bin;
|
||||
|
||||
before(() => {
|
||||
bin = fs.mkdtempSync(path.join(os.tmpdir(), "ccam-ci-status-cli-bin-"));
|
||||
bin = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-ci-status-cli-bin-"));
|
||||
fs.writeFileSync(
|
||||
path.join(bin, "gh"),
|
||||
`#!/usr/bin/env bash
|
||||
|
||||
Reference in New Issue
Block a user