17d56d0e01
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).
175 lines
5.7 KiB
JavaScript
175 lines
5.7 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* @file plugin-doctor.js
|
|
* @description Reports the health of a plugin-installed AgentHub: Node version,
|
|
* bootstrap state, runtime dependencies, server liveness, duplicate hook
|
|
* entries (the one failure that silently doubles every token and cost figure),
|
|
* the `agenthub` CLI launcher and its PATH, whether the committed MCP build still
|
|
* matches `mcp/src`, and whether the slash commands the pipeline templates name
|
|
* as node drivers are actually resolvable (the `superpowers` plugin, today).
|
|
* Read-only — it diagnoses, it never repairs.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
const fs = require("fs");
|
|
const os = require("os");
|
|
const path = require("path");
|
|
|
|
const boot = require("./plugin-bootstrap");
|
|
const { getSettingsPath, getDataDir } = require("../server/lib/claude-home");
|
|
const { mcpBuildStatus } = require("./check-mcp-build");
|
|
|
|
const PLUGIN_ROOT = path.resolve(__dirname, "..");
|
|
|
|
/** @returns {{level:"ok"|"warn"|"fail", label:string, detail:string}[]} */
|
|
function diagnose() {
|
|
const rt = boot.runtimeDir();
|
|
const out = [];
|
|
const add = (level, label, detail) => out.push({ level, label, detail });
|
|
|
|
add(
|
|
boot.nodeVersionOk() ? "ok" : "fail",
|
|
"Node",
|
|
boot.nodeVersionOk()
|
|
? `v${process.versions.node}`
|
|
: `v${process.versions.node} — AgentHub needs >= ${boot.MIN_NODE.join(".")} (node:sqlite)`
|
|
);
|
|
|
|
add("ok", "Plugin root", PLUGIN_ROOT);
|
|
add("ok", "Data dir", getDataDir());
|
|
|
|
const state = boot.readState(rt);
|
|
if (!state) {
|
|
add(
|
|
"warn",
|
|
"Bootstrap",
|
|
`no state recorded — start a new session, or check ${rt}/bootstrap.log`
|
|
);
|
|
} else {
|
|
const moved = state.pluginRoot !== PLUGIN_ROOT;
|
|
add(
|
|
moved ? "warn" : "ok",
|
|
"Bootstrap",
|
|
moved
|
|
? `recorded against a previous plugin version (${state.pluginRoot}) — run /agenthub-update`
|
|
: `last run ${state.updatedAt}`
|
|
);
|
|
}
|
|
|
|
const deps = fs.existsSync(path.join(rt, "node_modules"));
|
|
add(
|
|
deps ? "ok" : "fail",
|
|
"Runtime deps",
|
|
deps ? path.join(rt, "node_modules") : "missing — run /agenthub-update"
|
|
);
|
|
|
|
const live = boot.liveServers();
|
|
add(
|
|
live.length ? "ok" : "fail",
|
|
"Server",
|
|
live.length
|
|
? `listening on ${live.map((s) => `http://localhost:${s.port}`).join(", ")}`
|
|
: `not running — see ${path.join(rt, "server.log")}`
|
|
);
|
|
|
|
const dup = countLegacyHookEntries();
|
|
add(
|
|
dup ? "fail" : "ok",
|
|
"Hooks",
|
|
dup
|
|
? `${dup} entr${dup === 1 ? "y" : "ies"} in ${getSettingsPath()} duplicate the plugin's hooks — ` +
|
|
`every event is counted twice. Remove them (a new session does it automatically).`
|
|
: "provided by the plugin only"
|
|
);
|
|
|
|
add(...cliStatus());
|
|
add(...skillPluginStatus());
|
|
|
|
const mcp = mcpBuildStatus(PLUGIN_ROOT);
|
|
add(
|
|
mcp.ok ? "ok" : "fail",
|
|
"MCP build",
|
|
mcp.ok ? "matches mcp/src" : `${mcp.reason} — run npm run mcp:build`
|
|
);
|
|
|
|
const dist = process.env.DASHBOARD_CLIENT_DIST || path.join(rt, "client-dist");
|
|
add(
|
|
fs.existsSync(path.join(dist, "index.html")) ? "ok" : "warn",
|
|
"Dashboard UI",
|
|
fs.existsSync(path.join(dist, "index.html")) ? dist : "not built yet — run /agenthub-open"
|
|
);
|
|
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* WARN (never FAIL) when a plugin the pipelines depend on cannot be resolved:
|
|
* the dashboard, hooks, CLI and MCP all work without it — only the pipeline
|
|
* skills that drive it stop partway, so this must not fail a doctor run.
|
|
*
|
|
* The requirement derivation and the missing/satisfied rules live in
|
|
* `server/lib/skill-plugins.js`, shared with the bootstrap that installs them.
|
|
*/
|
|
function skillPluginStatus() {
|
|
const skillPlugins = require("../server/lib/skill-plugins");
|
|
const required = skillPlugins.requiredSkillPlugins();
|
|
if (!required.size) return ["ok", "Skill plugins", "no pipeline names an external plugin"];
|
|
|
|
let missing;
|
|
try {
|
|
missing = skillPlugins.missingSkillPlugins();
|
|
} catch {
|
|
return ["warn", "Skill plugins", "could not read the installed-plugin manifest"];
|
|
}
|
|
if (!missing.length) {
|
|
return ["ok", "Skill plugins", `${[...required.keys()].sort().join(", ")} available`];
|
|
}
|
|
return [
|
|
"warn",
|
|
"Skill plugins",
|
|
`${missing.map((m) => `${m.name} (needed for ${m.skills.join(", ")})`).join("; ")} — ` +
|
|
`the pipelines name these commands; install with ` +
|
|
`claude plugin install ${missing.map((m) => m.autoInstall?.spec || m.name).join(" ")}`,
|
|
];
|
|
}
|
|
|
|
function countLegacyHookEntries() {
|
|
try {
|
|
const settings = JSON.parse(fs.readFileSync(getSettingsPath(), "utf8"));
|
|
if (!settings.hooks) return 0;
|
|
const isLegacy = boot.isCheckoutHookEntry();
|
|
return Object.values(settings.hooks)
|
|
.filter(Array.isArray)
|
|
.reduce((n, entries) => n + entries.filter(isLegacy).length, 0);
|
|
} catch {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
function cliStatus() {
|
|
const dir = path.join(os.homedir(), ".local", "bin");
|
|
const file = path.join(dir, process.platform === "win32" ? "agenthub.cmd" : "agenthub");
|
|
if (!fs.existsSync(file)) return ["warn", "agenthub CLI", `no launcher at ${file}`];
|
|
const onPath = (process.env.PATH || "")
|
|
.split(path.delimiter)
|
|
.some((p) => path.resolve(p) === path.resolve(dir));
|
|
return onPath
|
|
? ["ok", "agenthub CLI", file]
|
|
: [
|
|
"warn",
|
|
"agenthub CLI",
|
|
`${file} exists but ${dir} is not on PATH — add: export PATH="${dir}:$PATH"`,
|
|
];
|
|
}
|
|
|
|
function report() {
|
|
const marks = { ok: "OK ", warn: "WARN", fail: "FAIL" };
|
|
const rows = diagnose();
|
|
for (const r of rows) console.log(`${marks[r.level]} ${r.label.padEnd(14)} ${r.detail}`);
|
|
return rows.some((r) => r.level === "fail") ? 1 : 0;
|
|
}
|
|
|
|
if (require.main === module) process.exitCode = report();
|
|
|
|
module.exports = { diagnose, report, countLegacyHookEntries };
|