feat(plugins): make CCAM installable straight from a Claude Code plugin
Adds a root `ccam` plugin (`.claude-plugin/plugin.json`, `"source": "./"`) so
`/plugin marketplace add` + `/plugin install ccam@...` is enough on a machine
with nothing but Claude Code: no clone, no npm run setup, no manual npm start.
- scripts/plugin-bootstrap.js: SessionStart hook. Fast-path exit, Node >=22.5
gate (node:sqlite), mkdir lock with stale reclaim, deps installed into
~/.claude/agent-dashboard/runtime/ (never the plugin cache), legacy
checkout-hook cleanup (backed up), ~/.local/bin/ccam launcher, eager UI
build so client routes like /run work immediately, detached server spawn.
- scripts/plugin-open.js, scripts/plugin-doctor.js: /ccam-open, /ccam-doctor.
- server/index.js: DASHBOARD_CLIENT_DIST override (plugin cache is read-only).
- mcp/build/ is committed (plugin MCP servers start before any bootstrap could
build them) and kept honest by scripts/check-mcp-build.js (content hash,
not mtime), enforced by pre-commit when mcp/src changes.
- plugins/ccam-dashboard/.mcp.json moved under plugins/ccam/ with a working
${CLAUDE_PLUGIN_ROOT} path (the old relative path never resolved from a
marketplace-cached subdir).
- Docs: README, INSTALL, SETUP, ARCHITECTURE, CLAUDE.md, docs/PLUGINS.md,
docs/MCP.md, docs/CLI.md, docs/HOOKS.md.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* @file banner.ts
|
||||
* @description Console startup UI for the MCP server's non-stdio transports (HTTP and REPL):
|
||||
* the ASCII-art wordmark, a boxed server-info panel (version, transport, dashboard URL, port,
|
||||
* tool count, mutation/destructive policy state), a "ready" line, and a shutdown message. The
|
||||
* stdio transport never calls any of these, since stdout there is the MCP JSON-RPC channel.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
/* =============================================================================
|
||||
* MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed)
|
||||
* =============================================================================
|
||||
* **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode.
|
||||
*
|
||||
* ## Design constraints
|
||||
* - Local-first: no telemetry leaves the machine unless the user configures webhooks.
|
||||
* - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
|
||||
* philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
|
||||
* - Destructive flows stay behind explicit confirmation modals and server-side gates.
|
||||
* - Internationalization: user-visible strings belong in i18n JSON, not literals here.
|
||||
*
|
||||
* ## Remote data & SSH
|
||||
* Remote Data Sources let operators aggregate multiple machines. SSH entries describe
|
||||
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every
|
||||
* scoped GET via `?sources=`. Health checks and import history surface in Settings.
|
||||
*
|
||||
* ## Observability
|
||||
* Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four
|
||||
* provisioned boards (overview, sessions, tools, alerts). Native npm scripts and
|
||||
* Docker Compose profiles are documented in `monitoring/README.md`.
|
||||
*
|
||||
* ## Public surface
|
||||
* - `printBanner` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `printServerInfo` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `printReady` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `printShutdown` — exported API; see TSDoc on the symbol for behavior.
|
||||
*
|
||||
* ## Testing pointers
|
||||
* - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
|
||||
* - Server contract changes require `npm run test:server` and OpenAPI sync.
|
||||
* - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`.
|
||||
*
|
||||
* ## Related docs
|
||||
* - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline.
|
||||
* - `docs/API.md` — REST reference.
|
||||
* - `.claude/skills/file-headers/` — mandatory `@author` header policy.
|
||||
* ============================================================================= */
|
||||
/* -----------------------------------------------------------------------------
|
||||
* EXPORT CATALOG — quick index of symbols defined below (documentation only).
|
||||
* -----------------------------------------------------------------------------
|
||||
* **printBanner**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **printServerInfo**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **printReady**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **printShutdown**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* ----------------------------------------------------------------------------- */
|
||||
import * as c from "./colors.js";
|
||||
/** ASCII-art wordmark rendered by {@link printBanner} with a color gradient. */
|
||||
const BANNER = `
|
||||
$$\\ $$\\ $$$$$$\\ $$$$$$$\\ $$$$$$$$\\ $$\\
|
||||
$$$\\ $$$ |$$ __$$\\ $$ __$$\\ \\__$$ __| $$ |
|
||||
$$$$\\ $$$$ |$$ / \\__|$$ | $$ | $$ | $$$$$$\\ $$$$$$\\ $$ | $$$$$$$\\
|
||||
$$\\$$\\$$ $$ |$$ | $$$$$$$ | $$ |$$ __$$\\ $$ __$$\\ $$ |$$ _____|
|
||||
$$ \\$$$ $$ |$$ | $$ ____/ $$ |$$ / $$ |$$ / $$ |$$ |\\$$$$$$\\
|
||||
$$ |\\$ /$$ |$$ | $$\\ $$ | $$ |$$ | $$ |$$ | $$ |$$ | \\____$$\\
|
||||
$$ | \\_/ $$ |\\$$$$$$ |$$ | $$ |\\$$$$$$ |\\$$$$$$ |$$ |$$$$$$$ |
|
||||
\\__| \\__| \\______/ \\__| \\__| \\______/ \\______/ \\__|\\_______/ `;
|
||||
/** Prints {@link BANNER} one line per gradient color (cyan to magenta).
|
||||
* Called at HTTP/REPL startup only. */
|
||||
export function printBanner() {
|
||||
const gradient = [c.brightCyan, c.cyan, c.brightBlue, c.blue, c.brightMagenta, c.magenta];
|
||||
const lines = BANNER.split("\n").filter((l) => l.length > 0);
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const colorFn = gradient[Math.min(i, gradient.length - 1)];
|
||||
process.stdout.write(colorFn(lines[i]) + "\n");
|
||||
}
|
||||
process.stdout.write("\n");
|
||||
}
|
||||
/** Prints a boxed config summary beneath the banner, shared by HTTP (`port`
|
||||
* set) and REPL (`port` omitted). Mutations/Destructive rows mirror the
|
||||
* `policy/tool-guards.ts` flags, warning-colored when enabled. Ends with a
|
||||
* reminder that the dashboard must already be running at the printed URL. */
|
||||
export function printServerInfo(info) {
|
||||
const divider = c.dim(c.cyan("─".repeat(62)));
|
||||
const line = (label, value) => ` ${c.dim(c.cyan("│"))} ${c.label(label.padEnd(18))} ${value}`;
|
||||
process.stdout.write(divider + "\n");
|
||||
process.stdout.write(` ${c.dim(c.cyan("│"))} ${c.bold(c.brightWhite("Agent Dashboard MCP Server"))}\n`);
|
||||
process.stdout.write(divider + "\n");
|
||||
process.stdout.write(line("Version", c.brightCyan(info.version)) + "\n");
|
||||
process.stdout.write(line("Transport", c.accent(info.transport.toUpperCase())) + "\n");
|
||||
process.stdout.write(line("Dashboard API", c.green(info.dashboard)) + "\n");
|
||||
if (info.port !== undefined) {
|
||||
process.stdout.write(line("HTTP Port", c.brightYellow(String(info.port))) + "\n");
|
||||
}
|
||||
process.stdout.write(line("Tools Registered", c.brightWhite(String(info.tools))) + "\n");
|
||||
process.stdout.write(line("Mutations", info.mutations ? c.warn("ENABLED") : c.success("disabled")) + "\n");
|
||||
process.stdout.write(line("Destructive", info.destructive ? c.error("ENABLED") : c.success("disabled")) + "\n");
|
||||
process.stdout.write(divider + "\n");
|
||||
process.stdout.write(` ${c.dim(c.cyan("│"))} ${c.warn("⚠")} ${c.dim("Dashboard must be running at the URL above.")}\n`);
|
||||
process.stdout.write(` ${c.dim(c.cyan("│"))} ${c.dim(" Start it first:")} ${c.brightWhite("npm run dev")} ${c.dim("or")} ${c.brightWhite("npm start")}\n`);
|
||||
process.stdout.write(divider + "\n\n");
|
||||
}
|
||||
/** Prints "Server ready" once the HTTP server has bound to its port; not
|
||||
* used by the REPL transport. */
|
||||
export function printReady(transport) {
|
||||
const icon = "✔";
|
||||
process.stdout.write(` ${c.success(icon)} ${c.bold(c.brightWhite("Server ready"))} ${c.muted(`(${transport})`)}\n\n`);
|
||||
}
|
||||
/** Prints "Shutting down...". Called from HTTP/REPL shutdown paths and
|
||||
* `index.ts`'s SIGINT/SIGTERM handler; never from stdio. */
|
||||
export function printShutdown() {
|
||||
process.stdout.write(`\n ${c.warn("⏻")} ${c.bold(c.brightWhite("Shutting down..."))}\n`);
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/**
|
||||
* @file colors.ts
|
||||
* @description Provides utility functions for applying ANSI color codes to text in the terminal. This module defines a set of functions for styling text with various colors and modifiers such as bold, italic, underline, and strikethrough. It also includes support for 256-color mode and a function to strip ANSI codes from text. The color functions are designed to be composable, allowing for easy combination of styles. The module checks for color support in the terminal environment and gracefully degrades if colors are not supported.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
/* =============================================================================
|
||||
* MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed)
|
||||
* =============================================================================
|
||||
* **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode.
|
||||
*
|
||||
* ## Design constraints
|
||||
* - Local-first: no telemetry leaves the machine unless the user configures webhooks.
|
||||
* - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
|
||||
* philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
|
||||
* - Destructive flows stay behind explicit confirmation modals and server-side gates.
|
||||
* - Internationalization: user-visible strings belong in i18n JSON, not literals here.
|
||||
*
|
||||
* ## Remote data & SSH
|
||||
* Remote Data Sources let operators aggregate multiple machines. SSH entries describe
|
||||
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every
|
||||
* scoped GET via `?sources=`. Health checks and import history surface in Settings.
|
||||
*
|
||||
* ## Observability
|
||||
* Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four
|
||||
* provisioned boards (overview, sessions, tools, alerts). Native npm scripts and
|
||||
* Docker Compose profiles are documented in `monitoring/README.md`.
|
||||
*
|
||||
* ## Public surface
|
||||
* - `bold` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `dim` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `italic` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `underline` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `strikethrough` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `black` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `red` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `green` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `yellow` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `blue` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `magenta` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `cyan` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `white` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `gray` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `brightRed` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `brightGreen` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `brightYellow` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `brightBlue` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `brightMagenta` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `brightCyan` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `brightWhite` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `bgRed` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `bgGreen` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `bgYellow` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `bgBlue` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `bgMagenta` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `bgCyan` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `bgWhite` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `bgGray` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `fg256` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `bg256` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `reset` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `stripAnsi` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `success` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `error` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `warn` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `info` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `muted` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `highlight` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `label` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - … plus 1 additional exports
|
||||
*
|
||||
* ## Testing pointers
|
||||
* - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
|
||||
* - Server contract changes require `npm run test:server` and OpenAPI sync.
|
||||
* - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`.
|
||||
*
|
||||
* ## Related docs
|
||||
* - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline.
|
||||
* - `docs/API.md` — REST reference.
|
||||
* - `.claude/skills/file-headers/` — mandatory `@author` header policy.
|
||||
* ============================================================================= */
|
||||
/* -----------------------------------------------------------------------------
|
||||
* EXPORT CATALOG — quick index of symbols defined below (documentation only).
|
||||
* -----------------------------------------------------------------------------
|
||||
* **bold**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **dim**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **italic**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **underline**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **strikethrough**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **black**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **red**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **green**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **yellow**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **blue**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **magenta**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **cyan**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **white**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **gray**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **brightRed**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **brightGreen**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **brightYellow**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **brightBlue**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **brightMagenta**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **brightCyan**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **brightWhite**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **bgRed**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **bgGreen**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **bgYellow**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **bgBlue**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **bgMagenta**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **bgCyan**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **bgWhite**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **bgGray**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **fg256**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **bg256**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **reset**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **stripAnsi**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **success**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **error**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **warn**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **info**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **muted**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **highlight**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **label**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **accent**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* ----------------------------------------------------------------------------- */
|
||||
/** Whether ANSI colors should be emitted: `NO_COLOR` always disables;
|
||||
* `FORCE_COLOR=0` disables, any other `FORCE_COLOR` enables regardless of
|
||||
* TTY; otherwise enabled only on an interactive stdout TTY. Computed once
|
||||
* at module load. */
|
||||
const isColorSupported = process.env.FORCE_COLOR !== "0" &&
|
||||
process.env.NO_COLOR === undefined &&
|
||||
(process.env.FORCE_COLOR !== undefined || (process.stdout.isTTY ?? false));
|
||||
/** Builds a styling function wrapping text in ANSI open/close codes, or an
|
||||
* identity function when colors are unsupported — every color/modifier
|
||||
* below is built with this, so disabling color no-ops all of them at once. */
|
||||
function wrap(open, close) {
|
||||
if (!isColorSupported)
|
||||
return (text) => text;
|
||||
return (text) => `\x1b[${open}m${text}\x1b[${close}m`;
|
||||
}
|
||||
// Modifiers
|
||||
export const bold = wrap("1", "22");
|
||||
export const dim = wrap("2", "22");
|
||||
export const italic = wrap("3", "23");
|
||||
export const underline = wrap("4", "24");
|
||||
export const strikethrough = wrap("9", "29");
|
||||
// Foreground colors
|
||||
export const black = wrap("30", "39");
|
||||
export const red = wrap("31", "39");
|
||||
export const green = wrap("32", "39");
|
||||
export const yellow = wrap("33", "39");
|
||||
export const blue = wrap("34", "39");
|
||||
export const magenta = wrap("35", "39");
|
||||
export const cyan = wrap("36", "39");
|
||||
export const white = wrap("37", "39");
|
||||
export const gray = wrap("90", "39");
|
||||
// Bright foreground colors
|
||||
export const brightRed = wrap("91", "39");
|
||||
export const brightGreen = wrap("92", "39");
|
||||
export const brightYellow = wrap("93", "39");
|
||||
export const brightBlue = wrap("94", "39");
|
||||
export const brightMagenta = wrap("95", "39");
|
||||
export const brightCyan = wrap("96", "39");
|
||||
export const brightWhite = wrap("97", "39");
|
||||
// Background colors
|
||||
export const bgRed = wrap("41", "49");
|
||||
export const bgGreen = wrap("42", "49");
|
||||
export const bgYellow = wrap("43", "49");
|
||||
export const bgBlue = wrap("44", "49");
|
||||
export const bgMagenta = wrap("45", "49");
|
||||
export const bgCyan = wrap("46", "49");
|
||||
export const bgWhite = wrap("47", "49");
|
||||
export const bgGray = wrap("100", "49");
|
||||
// 256-color support
|
||||
/** Foreground-color function for an xterm 256-color index; not currently
|
||||
* used by any composable style below. */
|
||||
export function fg256(code) {
|
||||
if (!isColorSupported)
|
||||
return (text) => text;
|
||||
return (text) => `\x1b[38;5;${code}m${text}\x1b[39m`;
|
||||
}
|
||||
/** Background-color function for an xterm 256-color index. */
|
||||
export function bg256(code) {
|
||||
if (!isColorSupported)
|
||||
return (text) => text;
|
||||
return (text) => `\x1b[48;5;${code}m${text}\x1b[49m`;
|
||||
}
|
||||
// Utility
|
||||
/** Raw ANSI "reset all styles" sequence, or `""` when colors are disabled. */
|
||||
export const reset = isColorSupported ? "\x1b[0m" : "";
|
||||
/** Strips ANSI SGR sequences from `text`. Used throughout `ui/formatter.ts`
|
||||
* to measure/pad colored strings by visible length, not byte length. */
|
||||
export function stripAnsi(text) {
|
||||
return text.replace(/\x1b\[[0-9;]*m/g, "");
|
||||
}
|
||||
// Composable styles
|
||||
/** Semantic style aliases used throughout `ui/banner.ts`, `ui/formatter.ts`,
|
||||
* and `transports/repl.ts` so call sites express intent, not a specific color. */
|
||||
export const success = (t) => bold(green(t));
|
||||
export const error = (t) => bold(red(t));
|
||||
export const warn = (t) => bold(yellow(t));
|
||||
export const info = (t) => bold(cyan(t));
|
||||
export const muted = (t) => dim(gray(t));
|
||||
export const highlight = (t) => bold(brightMagenta(t));
|
||||
export const label = (t) => bold(brightWhite(t));
|
||||
export const accent = (t) => bold(brightCyan(t));
|
||||
@@ -0,0 +1,279 @@
|
||||
/**
|
||||
* @file formatter.ts
|
||||
* @description A collection of utility functions for formatting console output in the MCP application. This includes functions for creating boxed sections, tables, status badges, formatted tool results, and key-value lists. The formatting is designed to be visually appealing and informative when printed to the terminal, using colors and styles to enhance readability. These utilities are used across various tools and components in the MCP application to maintain a consistent look and feel in the console output.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
/* =============================================================================
|
||||
* MODULE_GUIDE — extended in-file reference (comments only; safe to read, never executed)
|
||||
* =============================================================================
|
||||
* **Purpose:** Dashboard module consumed by the React client, MCP tools, or desktop shell depending on deployment mode.
|
||||
*
|
||||
* ## Design constraints
|
||||
* - Local-first: no telemetry leaves the machine unless the user configures webhooks.
|
||||
* - Fail-safe hooks path on the server must never block Claude Code; UI mirrors that
|
||||
* philosophy by degrading gracefully (empty states, stale badges, reconnect loops).
|
||||
* - Destructive flows stay behind explicit confirmation modals and server-side gates.
|
||||
* - Internationalization: user-visible strings belong in i18n JSON, not literals here.
|
||||
*
|
||||
* ## Remote data & SSH
|
||||
* Remote Data Sources let operators aggregate multiple machines. SSH entries describe
|
||||
* how to reach a peer dashboard; the global data scope (`dataScope.ts`) narrows every
|
||||
* scoped GET via `?sources=`. Health checks and import history surface in Settings.
|
||||
*
|
||||
* ## Observability
|
||||
* Prometheus scrapes `GET /api/metrics` (see `monitoring/`). Grafana ships four
|
||||
* provisioned boards (overview, sessions, tools, alerts). Native npm scripts and
|
||||
* Docker Compose profiles are documented in `monitoring/README.md`.
|
||||
*
|
||||
* ## Public surface
|
||||
* - `box` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `divider` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `Column` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `table` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `badge` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `formatToolResult` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `formatToolError` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `keyValue` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `sectionHeader` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `SPINNER_FRAMES` — exported API; see TSDoc on the symbol for behavior.
|
||||
* - `progressBar` — exported API; see TSDoc on the symbol for behavior.
|
||||
*
|
||||
* ## Testing pointers
|
||||
* - Prefer colocated `__tests__` with Vitest + Testing Library for UI.
|
||||
* - Server contract changes require `npm run test:server` and OpenAPI sync.
|
||||
* - MCP edits: `npm run mcp:typecheck` and `npm run mcp:build`.
|
||||
*
|
||||
* ## Related docs
|
||||
* - `ARCHITECTURE.md` — hooks → API → SQLite → WebSocket → UI pipeline.
|
||||
* - `docs/API.md` — REST reference.
|
||||
* - `.claude/skills/file-headers/` — mandatory `@author` header policy.
|
||||
* ============================================================================= */
|
||||
/* -----------------------------------------------------------------------------
|
||||
* EXPORT CATALOG — quick index of symbols defined below (documentation only).
|
||||
* -----------------------------------------------------------------------------
|
||||
* **box**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **divider**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **Column**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **table**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **badge**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **formatToolResult**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **formatToolError**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **keyValue**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **sectionHeader**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **SPINNER_FRAMES**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* **progressBar**
|
||||
* Part of this module's public contract. Downstream imports should treat
|
||||
* the signature and return type as stable unless release notes say otherwise.
|
||||
* When behavior changes, update the `@file` overview and relevant tests.
|
||||
*
|
||||
* ----------------------------------------------------------------------------- */
|
||||
import * as c from "./colors.js";
|
||||
// ── Box drawing ───────────────────────────────────────────────
|
||||
const BOX_TL = "╭";
|
||||
const BOX_TR = "╮";
|
||||
const BOX_BL = "╰";
|
||||
const BOX_BR = "╯";
|
||||
const BOX_H = "─";
|
||||
const BOX_V = "│";
|
||||
/** Unused "tee" joints; not referenced by {@link box}. */
|
||||
const BOX_ML = "├";
|
||||
const BOX_MR = "┤";
|
||||
/** Right-pads `text` to `width` visible columns via {@link stripAnsi}. */
|
||||
function pad(text, width) {
|
||||
const visLen = c.stripAnsi(text).length;
|
||||
return text + " ".repeat(Math.max(0, width - visLen));
|
||||
}
|
||||
/** Renders `content` in a rounded-corner box with `title` in the top
|
||||
* border. Not currently called; kept as a general-purpose primitive. */
|
||||
export function box(title, content, width = 60) {
|
||||
const inner = width - 4;
|
||||
const titleLine = ` ${title} `;
|
||||
const topPad = inner - c.stripAnsi(titleLine).length;
|
||||
const lines = [];
|
||||
lines.push(c.dim(c.cyan(BOX_TL + BOX_H)) +
|
||||
c.bold(c.brightCyan(titleLine)) +
|
||||
c.dim(c.cyan(BOX_H.repeat(Math.max(0, topPad)) + BOX_TR)));
|
||||
for (const row of content.split("\n")) {
|
||||
lines.push(c.dim(c.cyan(BOX_V)) + " " + pad(row, inner) + " " + c.dim(c.cyan(BOX_V)));
|
||||
}
|
||||
lines.push(c.dim(c.cyan(BOX_BL + BOX_H.repeat(width - 2) + BOX_BR)));
|
||||
return lines.join("\n");
|
||||
}
|
||||
/** Plain horizontal rule; `repl.ts` imports this without calling it. */
|
||||
export function divider(width = 60) {
|
||||
return c.dim(c.cyan(BOX_H.repeat(width)));
|
||||
}
|
||||
/** Pads/aligns `text` to `width` visible columns per `align`. */
|
||||
function alignText(text, width, align = "left") {
|
||||
const len = c.stripAnsi(text).length;
|
||||
const diff = Math.max(0, width - len);
|
||||
if (align === "right")
|
||||
return " ".repeat(diff) + text;
|
||||
if (align === "center") {
|
||||
const left = Math.floor(diff / 2);
|
||||
return " ".repeat(left) + text + " ".repeat(diff - left);
|
||||
}
|
||||
return text + " ".repeat(diff);
|
||||
}
|
||||
/** Renders `rows` as an ASCII table; used by `repl.ts`'s `printToolList`. */
|
||||
export function table(columns, rows) {
|
||||
const colWidths = columns.map((col) => {
|
||||
if (col.width)
|
||||
return col.width;
|
||||
const headerLen = col.label.length;
|
||||
const maxDataLen = rows.reduce((max, row) => {
|
||||
const val = String(row[col.key] ?? "");
|
||||
return Math.max(max, val.length);
|
||||
}, 0);
|
||||
return Math.max(headerLen, maxDataLen) + 2;
|
||||
});
|
||||
const lines = [];
|
||||
// Header
|
||||
const headerParts = columns.map((col, i) => c.bold(c.brightWhite(alignText(col.label, colWidths[i], col.align))));
|
||||
lines.push(" " + headerParts.join(c.dim(c.cyan(" │ "))));
|
||||
// Separator
|
||||
const sep = colWidths.map((w) => BOX_H.repeat(w));
|
||||
lines.push(" " + c.dim(c.cyan(sep.join("─┼─"))));
|
||||
// Rows
|
||||
for (const row of rows) {
|
||||
const parts = columns.map((col, i) => {
|
||||
const raw = String(row[col.key] ?? "");
|
||||
const styled = col.color ? col.color(raw) : raw;
|
||||
return alignText(styled, colWidths[i], col.align);
|
||||
});
|
||||
lines.push(" " + parts.join(c.dim(c.cyan(" │ "))));
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
// ── Status badges ─────────────────────────────────────────────
|
||||
/** Color mapping for {@link badge}. */
|
||||
const STATUS_COLORS = {
|
||||
active: c.success,
|
||||
completed: c.info,
|
||||
error: c.error,
|
||||
abandoned: c.warn,
|
||||
idle: c.muted,
|
||||
connected: c.info,
|
||||
working: (t) => c.bold(c.brightYellow(t)),
|
||||
ok: c.success,
|
||||
healthy: c.success,
|
||||
unhealthy: c.error,
|
||||
enabled: c.warn,
|
||||
disabled: c.success,
|
||||
};
|
||||
/** Renders `[STATUS]` colored via {@link STATUS_COLORS} (falls back to
|
||||
* muted). Used by `repl.ts`'s `printConfig`. */
|
||||
export function badge(status) {
|
||||
const colorFn = STATUS_COLORS[status.toLowerCase()] ?? c.muted;
|
||||
return colorFn(`[${status.toUpperCase()}]`);
|
||||
}
|
||||
// ── Tool result formatting ────────────────────────────────────
|
||||
/** Renders a successful REPL tool invocation: a header plus the result,
|
||||
* JSON-highlighted via {@link syntaxHighlight}; results over 30 lines are
|
||||
* truncated to 25 (display-only, doesn't affect the actual return value). */
|
||||
export function formatToolResult(name, data, durationMs) {
|
||||
const lines = [];
|
||||
const header = `${c.success("✔")} ${c.bold(c.brightWhite(name))} ${c.muted(`(${durationMs}ms)`)}`;
|
||||
lines.push(header);
|
||||
if (data === null || data === undefined) {
|
||||
lines.push(c.muted(" (no data)"));
|
||||
return lines.join("\n");
|
||||
}
|
||||
const json = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
||||
const jsonLines = json.split("\n");
|
||||
if (jsonLines.length <= 30) {
|
||||
lines.push(syntaxHighlight(json));
|
||||
}
|
||||
else {
|
||||
lines.push(syntaxHighlight(jsonLines.slice(0, 25).join("\n")));
|
||||
lines.push(c.muted(` ... +${jsonLines.length - 25} more lines`));
|
||||
}
|
||||
return lines.join("\n");
|
||||
}
|
||||
/** Renders a failed REPL tool invocation; given only a plain message
|
||||
* string, unlike {@link errorResult}'s structured `ApiError` handling. */
|
||||
export function formatToolError(name, error, durationMs) {
|
||||
return (`${c.error("✘")} ${c.bold(c.brightWhite(name))} ${c.muted(`(${durationMs}ms)`)}\n` +
|
||||
` ${c.red(error)}`);
|
||||
}
|
||||
// ── JSON syntax highlighting ──────────────────────────────────
|
||||
/** Regex-based JSON token coloring; a display heuristic, not a real
|
||||
* tokenizer — safe since input is always `JSON.stringify` output. */
|
||||
function syntaxHighlight(json) {
|
||||
return json.replace(/("(?:\\.|[^"\\])*")\s*(:)?|(\b(?:true|false|null)\b)|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)/g, (_match, str, colon, bool, num) => {
|
||||
if (str) {
|
||||
if (colon)
|
||||
return c.cyan(str) + c.dim(":");
|
||||
return c.green(str);
|
||||
}
|
||||
if (bool)
|
||||
return c.brightMagenta(bool);
|
||||
if (num)
|
||||
return c.brightYellow(num);
|
||||
return _match;
|
||||
});
|
||||
}
|
||||
// ── Key-value list ────────────────────────────────────────────
|
||||
/** Renders an aligned label/value list. Not currently called — `repl.ts`'s
|
||||
* `printConfig` builds an equivalent layout inline. */
|
||||
export function keyValue(pairs, labelWidth = 20) {
|
||||
return pairs.map(([k, v]) => ` ${c.label(k.padEnd(labelWidth))} ${v}`).join("\n");
|
||||
}
|
||||
// ── Section header ────────────────────────────────────────────
|
||||
/** Renders a `◆ Title` heading used throughout `repl.ts`. */
|
||||
export function sectionHeader(title) {
|
||||
return `\n ${c.bold(c.brightCyan("◆"))} ${c.bold(c.brightWhite(title))}\n`;
|
||||
}
|
||||
// ── Spinner frames (for async operations) ─────────────────────
|
||||
/** Braille spinner animation frames; no current caller drives one. */
|
||||
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||
/** Renders a block progress bar with a percentage label, clamped to
|
||||
* `[0, 1]`. No current caller reports incremental progress. */
|
||||
export function progressBar(current, total, width = 30) {
|
||||
const pct = Math.min(1, Math.max(0, current / total));
|
||||
const filled = Math.round(pct * width);
|
||||
const empty = width - filled;
|
||||
const bar = c.brightCyan("█".repeat(filled)) + c.dim("░".repeat(empty));
|
||||
const label = c.muted(`${Math.round(pct * 100)}%`);
|
||||
return ` ${bar} ${label}`;
|
||||
}
|
||||
Reference in New Issue
Block a user