Files
nntrivi2001 17d56d0e01 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).
2026-09-07 12:09:15 +07:00

99 lines
3.8 KiB
JavaScript

/**
* @file A lane's own database: create it once at provisioning and boot, drop
* it on remove. AgentHub stays stack-agnostic here — `createdb` vs `mysqladmin
* create` vs `touch foo.db` genuinely differ, so the actual command lives in
* the profile's `db-create.sh`/`db-drop.sh` hooks (already in the A1
* allowlist); this module only decides WHEN to call them and guards WHICH
* name a drop may ever target.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const fs = require("node:fs");
const path = require("node:path");
const { assertManaged } = require("./worktree");
const { dbName, slotDirs } = require("./lane-slots");
const { runHook } = require("./lane-profile");
/**
* Where AgentHub records that a slot's database has already been created.
*
* A hook can't be trusted to know this on its own without being stack-aware
* (a plain `createdb` errors on a database that already exists; `touch`
* wouldn't), so AgentHub tracks it itself with one flag file per database name,
* beside the rest of a slot's runtime bookkeeping. This is also how `upLane`
* tells "freshly created" from "already existed" to decide whether to seed.
*/
function markerPath(slot, name) {
return path.join(slotDirs(slot).stateDir, `db-created-${name}`);
}
/**
* Ensure a lane's database exists, creating it via the profile's `db-create`
* hook the first time only.
*
* A no-op when the profile declares no `DB_PREFIX` — no name is derived, so
* no hook is ever called and nothing is created.
*
* @param {object} lane - Lane row; `slot` must already be allocated.
* @param {object} profile - Resolved profile.
* @param {{onLine?: Function, timeoutMs?: number}} [options]
* @returns {Promise<{name: string|null, created: boolean}>}
*/
async function ensureDatabase(lane, profile, options = {}) {
const name = dbName(profile, lane.slot);
if (!name) return { name: null, created: false };
const marker = markerPath(lane.slot, name);
if (fs.existsSync(marker)) return { name, created: false };
const result = await runHook(lane, profile, "db-create", [name], options);
if (result.code !== 0) {
throw Object.assign(new Error(`db-create hook exited ${result.code}`), {
code: "EDBCREATE",
logPath: result.logPath,
});
}
fs.mkdirSync(path.dirname(marker), { recursive: true });
fs.writeFileSync(marker, new Date().toISOString());
return { name, created: true };
}
/**
* Drop a database via the profile's `db-drop` hook.
*
* Two checks run before anything is spawned, and neither trusts the caller:
* `assertManaged` (an adopted lane's data is the user's own, never AgentHub's to
* destroy) and a name check against what THIS lane's slot actually derives —
* the lane's own database or its `_test` sibling, and nothing else. Only a
* name AgentHub itself computed can ever reach the hook.
*
* @param {object} lane - Lane row; `slot` must already be allocated.
* @param {object} profile - Resolved profile.
* @param {string} name - The database to drop; must be `dbName` or `${dbName}_test`.
* @param {{onLine?: Function, timeoutMs?: number}} [options]
* @returns {Promise<{name: string}>}
*/
async function dropDatabase(lane, profile, name, options = {}) {
assertManaged(lane);
const derived = dbName(profile, lane.slot);
const allowed = derived && (name === derived || name === `${derived}_test`);
if (!allowed) {
throw Object.assign(new Error(`refusing to drop undeclared database: ${name}`), {
code: "EBADDBNAME",
});
}
const result = await runHook(lane, profile, "db-drop", [name], options);
if (result.code !== 0) {
throw Object.assign(new Error(`db-drop hook exited ${result.code}`), {
code: "EDBDROP",
logPath: result.logPath,
});
}
fs.rmSync(markerPath(lane.slot, name), { force: true });
return { name };
}
module.exports = { ensureDatabase, dropDatabase, markerPath };