From fa7df1976d0c82eddabd21513d95cd22594c4039 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 21 Jan 2026 04:47:35 +0000 Subject: [PATCH] feat: theme hooks/skills/plugins output --- src/cli/hooks-cli.ts | 242 +++++++++++++++++++++------------------- src/cli/plugins-cli.ts | 124 ++++++++++++-------- src/cli/security-cli.ts | 3 +- src/cli/skills-cli.ts | 188 +++++++++++++++---------------- 4 files changed, 304 insertions(+), 253 deletions(-) diff --git a/src/cli/hooks-cli.ts b/src/cli/hooks-cli.ts index b7fa2f37b..e2cd504c3 100644 --- a/src/cli/hooks-cli.ts +++ b/src/cli/hooks-cli.ts @@ -1,7 +1,6 @@ import fs from "node:fs"; import fsp from "node:fs/promises"; import path from "node:path"; -import chalk from "chalk"; import type { Command } from "commander"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import type { ClawdbotConfig } from "../config/config.js"; @@ -23,6 +22,7 @@ import { recordHookInstall } from "../hooks/installs.js"; import { buildPluginStatusReport } from "../plugins/status.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; +import { renderTable } from "../terminal/table.js"; import { theme } from "../terminal/theme.js"; import { formatCliCommand } from "./command-format.js"; import { resolveUserPath } from "../utils.js"; @@ -66,50 +66,40 @@ function buildHooksReport(config: ClawdbotConfig): HookStatusReport { return buildWorkspaceHookStatus(workspaceDir, { config, entries }); } -/** - * Format a single hook for display in the list - */ -function formatHookLine(hook: HookStatusEntry, verbose = false): string { +function formatHookStatus(hook: HookStatusEntry): string { + if (hook.eligible) return theme.success("✓ ready"); + if (hook.disabled) return theme.warn("⏸ disabled"); + return theme.error("✗ missing"); +} + +function formatHookName(hook: HookStatusEntry): string { const emoji = hook.emoji ?? "🔗"; - const status = hook.eligible - ? chalk.green("✓") - : hook.disabled - ? chalk.yellow("disabled") - : chalk.red("missing reqs"); + return `${emoji} ${theme.command(hook.name)}`; +} - const name = hook.eligible ? chalk.white(hook.name) : chalk.gray(hook.name); +function formatHookSource(hook: HookStatusEntry): string { + if (!hook.managedByPlugin) return hook.source; + return `plugin:${hook.pluginId ?? "unknown"}`; +} - const desc = chalk.gray( - hook.description.length > 50 ? `${hook.description.slice(0, 47)}...` : hook.description, - ); - const sourceLabel = hook.managedByPlugin - ? chalk.magenta(`plugin:${hook.pluginId ?? "unknown"}`) - : ""; - - if (verbose) { - const missing: string[] = []; - if (hook.missing.bins.length > 0) { - missing.push(`bins: ${hook.missing.bins.join(", ")}`); - } - if (hook.missing.anyBins.length > 0) { - missing.push(`anyBins: ${hook.missing.anyBins.join(", ")}`); - } - if (hook.missing.env.length > 0) { - missing.push(`env: ${hook.missing.env.join(", ")}`); - } - if (hook.missing.config.length > 0) { - missing.push(`config: ${hook.missing.config.join(", ")}`); - } - if (hook.missing.os.length > 0) { - missing.push(`os: ${hook.missing.os.join(", ")}`); - } - const missingStr = missing.length > 0 ? chalk.red(` [${missing.join("; ")}]`) : ""; - const sourceSuffix = sourceLabel ? ` ${sourceLabel}` : ""; - return `${emoji} ${name} ${status}${missingStr}\n ${desc}${sourceSuffix}`; +function formatHookMissingSummary(hook: HookStatusEntry): string { + const missing: string[] = []; + if (hook.missing.bins.length > 0) { + missing.push(`bins: ${hook.missing.bins.join(", ")}`); } - - const sourceSuffix = sourceLabel ? ` ${sourceLabel}` : ""; - return `${emoji} ${name} ${status} - ${desc}${sourceSuffix}`; + if (hook.missing.anyBins.length > 0) { + missing.push(`anyBins: ${hook.missing.anyBins.join(", ")}`); + } + if (hook.missing.env.length > 0) { + missing.push(`env: ${hook.missing.env.join(", ")}`); + } + if (hook.missing.config.length > 0) { + missing.push(`config: ${hook.missing.config.join(", ")}`); + } + if (hook.missing.os.length > 0) { + missing.push(`os: ${hook.missing.os.join(", ")}`); + } + return missing.join("; "); } async function readInstalledPackageVersion(dir: string): Promise { @@ -157,27 +147,39 @@ export function formatHooksList(report: HookStatusReport, opts: HooksListOptions } const eligible = hooks.filter((h) => h.eligible); - const notEligible = hooks.filter((h) => !h.eligible); + const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1); + const rows = hooks.map((hook) => { + const missing = formatHookMissingSummary(hook); + return { + Status: formatHookStatus(hook), + Hook: formatHookName(hook), + Description: theme.muted(hook.description), + Source: formatHookSource(hook), + Missing: missing ? theme.warn(missing) : "", + }; + }); + + const columns = [ + { key: "Status", header: "Status", minWidth: 10 }, + { key: "Hook", header: "Hook", minWidth: 18, flex: true }, + { key: "Description", header: "Description", minWidth: 24, flex: true }, + { key: "Source", header: "Source", minWidth: 12, flex: true }, + ]; + if (opts.verbose) { + columns.push({ key: "Missing", header: "Missing", minWidth: 18, flex: true }); + } const lines: string[] = []; - lines.push(chalk.bold.cyan("Hooks") + chalk.gray(` (${eligible.length}/${hooks.length} ready)`)); - lines.push(""); - - if (eligible.length > 0) { - lines.push(chalk.bold.green("Ready:")); - for (const hook of eligible) { - lines.push(` ${formatHookLine(hook, opts.verbose)}`); - } - } - - if (notEligible.length > 0 && !opts.eligible) { - if (eligible.length > 0) lines.push(""); - lines.push(chalk.bold.yellow("Not ready:")); - for (const hook of notEligible) { - lines.push(` ${formatHookLine(hook, opts.verbose)}`); - } - } - + lines.push( + `${theme.heading("Hooks")} ${theme.muted(`(${eligible.length}/${hooks.length} ready)`)}`, + ); + lines.push( + renderTable({ + width: tableWidth, + columns, + rows, + }).trimEnd(), + ); return lines.join("\n"); } @@ -205,33 +207,33 @@ export function formatHookInfo( const lines: string[] = []; const emoji = hook.emoji ?? "🔗"; const status = hook.eligible - ? chalk.green("✓ Ready") + ? theme.success("✓ Ready") : hook.disabled - ? chalk.yellow("⏸ Disabled") - : chalk.red("✗ Missing requirements"); + ? theme.warn("⏸ Disabled") + : theme.error("✗ Missing requirements"); - lines.push(`${emoji} ${chalk.bold.cyan(hook.name)} ${status}`); + lines.push(`${emoji} ${theme.heading(hook.name)} ${status}`); lines.push(""); - lines.push(chalk.white(hook.description)); + lines.push(hook.description); lines.push(""); // Details - lines.push(chalk.bold("Details:")); + lines.push(theme.heading("Details:")); if (hook.managedByPlugin) { - lines.push(` Source: ${hook.source} (${hook.pluginId ?? "unknown"})`); + lines.push(`${theme.muted(" Source:")} ${hook.source} (${hook.pluginId ?? "unknown"})`); } else { - lines.push(` Source: ${hook.source}`); + lines.push(`${theme.muted(" Source:")} ${hook.source}`); } - lines.push(` Path: ${chalk.gray(hook.filePath)}`); - lines.push(` Handler: ${chalk.gray(hook.handlerPath)}`); + lines.push(`${theme.muted(" Path:")} ${hook.filePath}`); + lines.push(`${theme.muted(" Handler:")} ${hook.handlerPath}`); if (hook.homepage) { - lines.push(` Homepage: ${chalk.blue(hook.homepage)}`); + lines.push(`${theme.muted(" Homepage:")} ${hook.homepage}`); } if (hook.events.length > 0) { - lines.push(` Events: ${hook.events.join(", ")}`); + lines.push(`${theme.muted(" Events:")} ${hook.events.join(", ")}`); } if (hook.managedByPlugin) { - lines.push(` Managed by plugin; enable/disable via hooks CLI not available.`); + lines.push(theme.muted(" Managed by plugin; enable/disable via hooks CLI not available.")); } // Requirements @@ -244,40 +246,40 @@ export function formatHookInfo( if (hasRequirements) { lines.push(""); - lines.push(chalk.bold("Requirements:")); + lines.push(theme.heading("Requirements:")); if (hook.requirements.bins.length > 0) { const binsStatus = hook.requirements.bins.map((bin) => { const missing = hook.missing.bins.includes(bin); - return missing ? chalk.red(`✗ ${bin}`) : chalk.green(`✓ ${bin}`); + return missing ? theme.error(`✗ ${bin}`) : theme.success(`✓ ${bin}`); }); - lines.push(` Binaries: ${binsStatus.join(", ")}`); + lines.push(`${theme.muted(" Binaries:")} ${binsStatus.join(", ")}`); } if (hook.requirements.anyBins.length > 0) { const anyBinsStatus = hook.missing.anyBins.length > 0 - ? chalk.red(`✗ (any of: ${hook.requirements.anyBins.join(", ")})`) - : chalk.green(`✓ (any of: ${hook.requirements.anyBins.join(", ")})`); - lines.push(` Any binary: ${anyBinsStatus}`); + ? theme.error(`✗ (any of: ${hook.requirements.anyBins.join(", ")})`) + : theme.success(`✓ (any of: ${hook.requirements.anyBins.join(", ")})`); + lines.push(`${theme.muted(" Any binary:")} ${anyBinsStatus}`); } if (hook.requirements.env.length > 0) { const envStatus = hook.requirements.env.map((env) => { const missing = hook.missing.env.includes(env); - return missing ? chalk.red(`✗ ${env}`) : chalk.green(`✓ ${env}`); + return missing ? theme.error(`✗ ${env}`) : theme.success(`✓ ${env}`); }); - lines.push(` Environment: ${envStatus.join(", ")}`); + lines.push(`${theme.muted(" Environment:")} ${envStatus.join(", ")}`); } if (hook.requirements.config.length > 0) { const configStatus = hook.configChecks.map((check) => { - return check.satisfied ? chalk.green(`✓ ${check.path}`) : chalk.red(`✗ ${check.path}`); + return check.satisfied ? theme.success(`✓ ${check.path}`) : theme.error(`✗ ${check.path}`); }); - lines.push(` Config: ${configStatus.join(", ")}`); + lines.push(`${theme.muted(" Config:")} ${configStatus.join(", ")}`); } if (hook.requirements.os.length > 0) { const osStatus = hook.missing.os.length > 0 - ? chalk.red(`✗ (${hook.requirements.os.join(", ")})`) - : chalk.green(`✓ (${hook.requirements.os.join(", ")})`); - lines.push(` OS: ${osStatus}`); + ? theme.error(`✗ (${hook.requirements.os.join(", ")})`) + : theme.success(`✓ (${hook.requirements.os.join(", ")})`); + lines.push(`${theme.muted(" OS:")} ${osStatus}`); } } @@ -313,15 +315,15 @@ export function formatHooksCheck(report: HookStatusReport, opts: HooksCheckOptio const notEligible = report.hooks.filter((h) => !h.eligible); const lines: string[] = []; - lines.push(chalk.bold.cyan("Hooks Status")); + lines.push(theme.heading("Hooks Status")); lines.push(""); - lines.push(`Total hooks: ${report.hooks.length}`); - lines.push(chalk.green(`Ready: ${eligible.length}`)); - lines.push(chalk.yellow(`Not ready: ${notEligible.length}`)); + lines.push(`${theme.muted("Total hooks:")} ${report.hooks.length}`); + lines.push(`${theme.success("Ready:")} ${eligible.length}`); + lines.push(`${theme.warn("Not ready:")} ${notEligible.length}`); if (notEligible.length > 0) { lines.push(""); - lines.push(chalk.bold.yellow("Hooks not ready:")); + lines.push(theme.heading("Hooks not ready:")); for (const hook of notEligible) { const reasons = []; if (hook.disabled) reasons.push("disabled"); @@ -374,7 +376,9 @@ export async function enableHook(hookName: string): Promise { }; await writeConfigFile(nextConfig); - console.log(`${chalk.green("✓")} Enabled hook: ${hook.emoji ?? "🔗"} ${hookName}`); + defaultRuntime.log( + `${theme.success("✓")} Enabled hook: ${hook.emoji ?? "🔗"} ${theme.command(hookName)}`, + ); } export async function disableHook(hookName: string): Promise { @@ -408,7 +412,9 @@ export async function disableHook(hookName: string): Promise { }; await writeConfigFile(nextConfig); - console.log(`${chalk.yellow("⏸")} Disabled hook: ${hook.emoji ?? "🔗"} ${hookName}`); + defaultRuntime.log( + `${theme.warn("⏸")} Disabled hook: ${hook.emoji ?? "🔗"} ${theme.command(hookName)}`, + ); } export function registerHooksCli(program: Command): void { @@ -431,9 +437,11 @@ export function registerHooksCli(program: Command): void { try { const config = loadConfig(); const report = buildHooksReport(config); - console.log(formatHooksList(report, opts)); + defaultRuntime.log(formatHooksList(report, opts)); } catch (err) { - console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err)); + defaultRuntime.error( + `${theme.error("Error:")} ${err instanceof Error ? err.message : String(err)}`, + ); process.exit(1); } }); @@ -446,9 +454,11 @@ export function registerHooksCli(program: Command): void { try { const config = loadConfig(); const report = buildHooksReport(config); - console.log(formatHookInfo(report, name, opts)); + defaultRuntime.log(formatHookInfo(report, name, opts)); } catch (err) { - console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err)); + defaultRuntime.error( + `${theme.error("Error:")} ${err instanceof Error ? err.message : String(err)}`, + ); process.exit(1); } }); @@ -461,9 +471,11 @@ export function registerHooksCli(program: Command): void { try { const config = loadConfig(); const report = buildHooksReport(config); - console.log(formatHooksCheck(report, opts)); + defaultRuntime.log(formatHooksCheck(report, opts)); } catch (err) { - console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err)); + defaultRuntime.error( + `${theme.error("Error:")} ${err instanceof Error ? err.message : String(err)}`, + ); process.exit(1); } }); @@ -475,7 +487,9 @@ export function registerHooksCli(program: Command): void { try { await enableHook(name); } catch (err) { - console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err)); + defaultRuntime.error( + `${theme.error("Error:")} ${err instanceof Error ? err.message : String(err)}`, + ); process.exit(1); } }); @@ -487,7 +501,9 @@ export function registerHooksCli(program: Command): void { try { await disableHook(name); } catch (err) { - console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err)); + defaultRuntime.error( + `${theme.error("Error:")} ${err instanceof Error ? err.message : String(err)}`, + ); process.exit(1); } }); @@ -570,7 +586,7 @@ export function registerHooksCli(program: Command): void { path: resolved, logger: { info: (msg) => defaultRuntime.log(msg), - warn: (msg) => defaultRuntime.log(chalk.yellow(msg)), + warn: (msg) => defaultRuntime.log(theme.warn(msg)), }, }); if (!result.ok) { @@ -650,7 +666,7 @@ export function registerHooksCli(program: Command): void { spec: raw, logger: { info: (msg) => defaultRuntime.log(msg), - warn: (msg) => defaultRuntime.log(chalk.yellow(msg)), + warn: (msg) => defaultRuntime.log(theme.warn(msg)), }, }); if (!result.ok) { @@ -726,15 +742,15 @@ export function registerHooksCli(program: Command): void { for (const hookId of targets) { const record = installs[hookId]; if (!record) { - defaultRuntime.log(chalk.yellow(`No install record for "${hookId}".`)); + defaultRuntime.log(theme.warn(`No install record for "${hookId}".`)); continue; } if (record.source !== "npm") { - defaultRuntime.log(chalk.yellow(`Skipping "${hookId}" (source: ${record.source}).`)); + defaultRuntime.log(theme.warn(`Skipping "${hookId}" (source: ${record.source}).`)); continue; } if (!record.spec) { - defaultRuntime.log(chalk.yellow(`Skipping "${hookId}" (missing npm spec).`)); + defaultRuntime.log(theme.warn(`Skipping "${hookId}" (missing npm spec).`)); continue; } @@ -749,11 +765,11 @@ export function registerHooksCli(program: Command): void { expectedHookPackId: hookId, logger: { info: (msg) => defaultRuntime.log(msg), - warn: (msg) => defaultRuntime.log(chalk.yellow(msg)), + warn: (msg) => defaultRuntime.log(theme.warn(msg)), }, }); if (!probe.ok) { - defaultRuntime.log(chalk.red(`Failed to check ${hookId}: ${probe.error}`)); + defaultRuntime.log(theme.error(`Failed to check ${hookId}: ${probe.error}`)); continue; } @@ -773,11 +789,11 @@ export function registerHooksCli(program: Command): void { expectedHookPackId: hookId, logger: { info: (msg) => defaultRuntime.log(msg), - warn: (msg) => defaultRuntime.log(chalk.yellow(msg)), + warn: (msg) => defaultRuntime.log(theme.warn(msg)), }, }); if (!result.ok) { - defaultRuntime.log(chalk.red(`Failed to update ${hookId}: ${result.error}`)); + defaultRuntime.log(theme.error(`Failed to update ${hookId}: ${result.error}`)); continue; } @@ -811,9 +827,11 @@ export function registerHooksCli(program: Command): void { try { const config = loadConfig(); const report = buildHooksReport(config); - console.log(formatHooksList(report, {})); + defaultRuntime.log(formatHooksList(report, {})); } catch (err) { - console.error(chalk.red("Error:"), err instanceof Error ? err.message : String(err)); + defaultRuntime.error( + `${theme.error("Error:")} ${err instanceof Error ? err.message : String(err)}`, + ); process.exit(1); } }); diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index d9874af2e..80f61a6f1 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -1,6 +1,5 @@ import fs from "node:fs"; import path from "node:path"; -import chalk from "chalk"; import type { Command } from "commander"; import { loadConfig, writeConfigFile } from "../config/config.js"; @@ -14,6 +13,7 @@ import { buildPluginStatusReport } from "../plugins/status.js"; import { updateNpmInstalledPlugins } from "../plugins/update.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; +import { renderTable } from "../terminal/table.js"; import { theme } from "../terminal/theme.js"; import { resolveUserPath } from "../utils.js"; @@ -35,19 +35,19 @@ export type PluginUpdateOptions = { function formatPluginLine(plugin: PluginRecord, verbose = false): string { const status = plugin.status === "loaded" - ? chalk.green("✓") + ? theme.success("loaded") : plugin.status === "disabled" - ? chalk.yellow("disabled") - : chalk.red("error"); - const name = plugin.name ? chalk.white(plugin.name) : chalk.white(plugin.id); - const idSuffix = plugin.name !== plugin.id ? chalk.gray(` (${plugin.id})`) : ""; + ? theme.warn("disabled") + : theme.error("error"); + const name = theme.command(plugin.name || plugin.id); + const idSuffix = plugin.name && plugin.name !== plugin.id ? theme.muted(` (${plugin.id})`) : ""; const desc = plugin.description - ? chalk.gray( + ? theme.muted( plugin.description.length > 60 ? `${plugin.description.slice(0, 57)}...` : plugin.description, ) - : chalk.gray("(no description)"); + : theme.muted("(no description)"); if (!verbose) { return `${name}${idSuffix} ${status} - ${desc}`; @@ -55,14 +55,14 @@ function formatPluginLine(plugin: PluginRecord, verbose = false): string { const parts = [ `${name}${idSuffix} ${status}`, - ` source: ${chalk.gray(plugin.source)}`, + ` source: ${theme.muted(plugin.source)}`, ` origin: ${plugin.origin}`, ]; if (plugin.version) parts.push(` version: ${plugin.version}`); if (plugin.providerIds.length > 0) { parts.push(` providers: ${plugin.providerIds.join(", ")}`); } - if (plugin.error) parts.push(chalk.red(` error: ${plugin.error}`)); + if (plugin.error) parts.push(theme.error(` error: ${plugin.error}`)); return parts.join("\n"); } @@ -87,7 +87,7 @@ function applySlotSelectionForPlugin( function logSlotWarnings(warnings: string[]) { if (warnings.length === 0) return; for (const warning of warnings) { - defaultRuntime.log(chalk.yellow(warning)); + defaultRuntime.log(theme.warn(warning)); } } @@ -124,19 +124,51 @@ export function registerPluginsCli(program: Command) { } if (list.length === 0) { - defaultRuntime.log("No plugins found."); + defaultRuntime.log(theme.muted("No plugins found.")); + return; + } + + const loaded = list.filter((p) => p.status === "loaded").length; + defaultRuntime.log( + `${theme.heading("Plugins")} ${theme.muted(`(${loaded}/${list.length} loaded)`)}`, + ); + + if (!opts.verbose) { + const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1); + const rows = list.map((plugin) => ({ + Name: plugin.name || plugin.id, + ID: plugin.name && plugin.name !== plugin.id ? plugin.id : "", + Status: + plugin.status === "loaded" + ? theme.success("loaded") + : plugin.status === "disabled" + ? theme.warn("disabled") + : theme.error("error"), + Source: plugin.source, + Version: plugin.version ?? "", + Description: plugin.description ?? "", + })); + defaultRuntime.log( + renderTable({ + width: tableWidth, + columns: [ + { key: "Name", header: "Name", minWidth: 14, flex: true }, + { key: "ID", header: "ID", minWidth: 10, flex: true }, + { key: "Status", header: "Status", minWidth: 10 }, + { key: "Source", header: "Source", minWidth: 10 }, + { key: "Version", header: "Version", minWidth: 8 }, + { key: "Description", header: "Description", minWidth: 18, flex: true }, + ], + rows, + }).trimEnd(), + ); return; } const lines: string[] = []; - const loaded = list.filter((p) => p.status === "loaded").length; - lines.push( - `${chalk.bold.cyan("Plugins")} ${chalk.gray(`(${loaded}/${list.length} loaded)`)}`, - ); - lines.push(""); for (const plugin of list) { - lines.push(formatPluginLine(plugin, opts.verbose)); - if (opts.verbose) lines.push(""); + lines.push(formatPluginLine(plugin, true)); + lines.push(""); } defaultRuntime.log(lines.join("\n").trim()); }); @@ -162,43 +194,45 @@ export function registerPluginsCli(program: Command) { } const lines: string[] = []; - lines.push(chalk.bold.cyan(plugin.name || plugin.id)); + lines.push(theme.heading(plugin.name || plugin.id)); if (plugin.name && plugin.name !== plugin.id) { - lines.push(chalk.gray(`id: ${plugin.id}`)); + lines.push(theme.muted(`id: ${plugin.id}`)); } if (plugin.description) lines.push(plugin.description); lines.push(""); - lines.push(`Status: ${plugin.status}`); - lines.push(`Source: ${plugin.source}`); - lines.push(`Origin: ${plugin.origin}`); - if (plugin.version) lines.push(`Version: ${plugin.version}`); + lines.push(`${theme.muted("Status:")} ${plugin.status}`); + lines.push(`${theme.muted("Source:")} ${plugin.source}`); + lines.push(`${theme.muted("Origin:")} ${plugin.origin}`); + if (plugin.version) lines.push(`${theme.muted("Version:")} ${plugin.version}`); if (plugin.toolNames.length > 0) { - lines.push(`Tools: ${plugin.toolNames.join(", ")}`); + lines.push(`${theme.muted("Tools:")} ${plugin.toolNames.join(", ")}`); } if (plugin.hookNames.length > 0) { - lines.push(`Hooks: ${plugin.hookNames.join(", ")}`); + lines.push(`${theme.muted("Hooks:")} ${plugin.hookNames.join(", ")}`); } if (plugin.gatewayMethods.length > 0) { - lines.push(`Gateway methods: ${plugin.gatewayMethods.join(", ")}`); + lines.push(`${theme.muted("Gateway methods:")} ${plugin.gatewayMethods.join(", ")}`); } if (plugin.providerIds.length > 0) { - lines.push(`Providers: ${plugin.providerIds.join(", ")}`); + lines.push(`${theme.muted("Providers:")} ${plugin.providerIds.join(", ")}`); } if (plugin.cliCommands.length > 0) { - lines.push(`CLI commands: ${plugin.cliCommands.join(", ")}`); + lines.push(`${theme.muted("CLI commands:")} ${plugin.cliCommands.join(", ")}`); } if (plugin.services.length > 0) { - lines.push(`Services: ${plugin.services.join(", ")}`); + lines.push(`${theme.muted("Services:")} ${plugin.services.join(", ")}`); } - if (plugin.error) lines.push(chalk.red(`Error: ${plugin.error}`)); + if (plugin.error) lines.push(`${theme.error("Error:")} ${plugin.error}`); if (install) { lines.push(""); - lines.push(`Install: ${install.source}`); - if (install.spec) lines.push(`Spec: ${install.spec}`); - if (install.sourcePath) lines.push(`Source path: ${install.sourcePath}`); - if (install.installPath) lines.push(`Install path: ${install.installPath}`); - if (install.version) lines.push(`Recorded version: ${install.version}`); - if (install.installedAt) lines.push(`Installed at: ${install.installedAt}`); + lines.push(`${theme.muted("Install:")} ${install.source}`); + if (install.spec) lines.push(`${theme.muted("Spec:")} ${install.spec}`); + if (install.sourcePath) lines.push(`${theme.muted("Source path:")} ${install.sourcePath}`); + if (install.installPath) + lines.push(`${theme.muted("Install path:")} ${install.installPath}`); + if (install.version) lines.push(`${theme.muted("Recorded version:")} ${install.version}`); + if (install.installedAt) + lines.push(`${theme.muted("Installed at:")} ${install.installedAt}`); } defaultRuntime.log(lines.join("\n")); }); @@ -308,7 +342,7 @@ export function registerPluginsCli(program: Command) { path: resolved, logger: { info: (msg) => defaultRuntime.log(msg), - warn: (msg) => defaultRuntime.log(chalk.yellow(msg)), + warn: (msg) => defaultRuntime.log(theme.warn(msg)), }, }); if (!result.ok) { @@ -372,7 +406,7 @@ export function registerPluginsCli(program: Command) { spec: raw, logger: { info: (msg) => defaultRuntime.log(msg), - warn: (msg) => defaultRuntime.log(chalk.yellow(msg)), + warn: (msg) => defaultRuntime.log(theme.warn(msg)), }, }); if (!result.ok) { @@ -430,17 +464,17 @@ export function registerPluginsCli(program: Command) { dryRun: opts.dryRun, logger: { info: (msg) => defaultRuntime.log(msg), - warn: (msg) => defaultRuntime.log(chalk.yellow(msg)), + warn: (msg) => defaultRuntime.log(theme.warn(msg)), }, }); for (const outcome of result.outcomes) { if (outcome.status === "error") { - defaultRuntime.log(chalk.red(outcome.message)); + defaultRuntime.log(theme.error(outcome.message)); continue; } if (outcome.status === "skipped") { - defaultRuntime.log(chalk.yellow(outcome.message)); + defaultRuntime.log(theme.warn(outcome.message)); continue; } defaultRuntime.log(outcome.message); @@ -467,14 +501,14 @@ export function registerPluginsCli(program: Command) { const lines: string[] = []; if (errors.length > 0) { - lines.push(chalk.bold.red("Plugin errors:")); + lines.push(theme.error("Plugin errors:")); for (const entry of errors) { lines.push(`- ${entry.id}: ${entry.error ?? "failed to load"} (${entry.source})`); } } if (diags.length > 0) { if (lines.length > 0) lines.push(""); - lines.push(chalk.bold.yellow("Diagnostics:")); + lines.push(theme.warn("Diagnostics:")); for (const diag of diags) { const target = diag.pluginId ? `${diag.pluginId}: ` : ""; lines.push(`- ${target}${diag.message}`); diff --git a/src/cli/security-cli.ts b/src/cli/security-cli.ts index 31311022d..726b5ba03 100644 --- a/src/cli/security-cli.ts +++ b/src/cli/security-cli.ts @@ -1,4 +1,3 @@ -import chalk from "chalk"; import type { Command } from "commander"; import { loadConfig } from "../config/config.js"; @@ -121,7 +120,7 @@ export function registerSecurityCli(program: Command) { lines.push(""); lines.push(heading(label)); for (const f of list) { - lines.push(`${chalk.gray(f.checkId)} ${f.title}`); + lines.push(`${theme.muted(f.checkId)} ${f.title}`); lines.push(` ${f.detail}`); if (f.remediation?.trim()) lines.push(` ${muted(`Fix: ${f.remediation.trim()}`)}`); } diff --git a/src/cli/skills-cli.ts b/src/cli/skills-cli.ts index 42aa1680a..21c934e17 100644 --- a/src/cli/skills-cli.ts +++ b/src/cli/skills-cli.ts @@ -1,4 +1,3 @@ -import chalk from "chalk"; import type { Command } from "commander"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { @@ -9,6 +8,7 @@ import { import { loadConfig } from "../config/config.js"; import { defaultRuntime } from "../runtime.js"; import { formatDocsLink } from "../terminal/links.js"; +import { renderTable } from "../terminal/table.js"; import { theme } from "../terminal/theme.js"; import { formatCliCommand } from "./command-format.js"; @@ -31,47 +31,36 @@ function appendClawdHubHint(output: string, json?: boolean): string { return `${output}\n\nTip: use \`npx clawdhub\` to search, install, and sync skills.`; } -/** - * Format a single skill for display in the list - */ -function formatSkillLine(skill: SkillStatusEntry, verbose = false): string { +function formatSkillStatus(skill: SkillStatusEntry): string { + if (skill.eligible) return theme.success("✓ ready"); + if (skill.disabled) return theme.warn("⏸ disabled"); + if (skill.blockedByAllowlist) return theme.warn("🚫 blocked"); + return theme.error("✗ missing"); +} + +function formatSkillName(skill: SkillStatusEntry): string { const emoji = skill.emoji ?? "📦"; - const status = skill.eligible - ? chalk.green("✓") - : skill.disabled - ? chalk.yellow("disabled") - : skill.blockedByAllowlist - ? chalk.yellow("blocked") - : chalk.red("missing reqs"); + return `${emoji} ${theme.command(skill.name)}`; +} - const name = skill.eligible ? chalk.white(skill.name) : chalk.gray(skill.name); - - const desc = chalk.gray( - skill.description.length > 50 ? `${skill.description.slice(0, 47)}...` : skill.description, - ); - - if (verbose) { - const missing: string[] = []; - if (skill.missing.bins.length > 0) { - missing.push(`bins: ${skill.missing.bins.join(", ")}`); - } - if (skill.missing.anyBins.length > 0) { - missing.push(`anyBins: ${skill.missing.anyBins.join(", ")}`); - } - if (skill.missing.env.length > 0) { - missing.push(`env: ${skill.missing.env.join(", ")}`); - } - if (skill.missing.config.length > 0) { - missing.push(`config: ${skill.missing.config.join(", ")}`); - } - if (skill.missing.os.length > 0) { - missing.push(`os: ${skill.missing.os.join(", ")}`); - } - const missingStr = missing.length > 0 ? chalk.red(` [${missing.join("; ")}]`) : ""; - return `${emoji} ${name} ${status}${missingStr}\n ${desc}`; +function formatSkillMissingSummary(skill: SkillStatusEntry): string { + const missing: string[] = []; + if (skill.missing.bins.length > 0) { + missing.push(`bins: ${skill.missing.bins.join(", ")}`); } - - return `${emoji} ${name} ${status} - ${desc}`; + if (skill.missing.anyBins.length > 0) { + missing.push(`anyBins: ${skill.missing.anyBins.join(", ")}`); + } + if (skill.missing.env.length > 0) { + missing.push(`env: ${skill.missing.env.join(", ")}`); + } + if (skill.missing.config.length > 0) { + missing.push(`config: ${skill.missing.config.join(", ")}`); + } + if (skill.missing.os.length > 0) { + missing.push(`os: ${skill.missing.os.join(", ")}`); + } + return missing.join("; "); } /** @@ -108,28 +97,39 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti } const eligible = skills.filter((s) => s.eligible); - const notEligible = skills.filter((s) => !s.eligible); + const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1); + const rows = skills.map((skill) => { + const missing = formatSkillMissingSummary(skill); + return { + Status: formatSkillStatus(skill), + Skill: formatSkillName(skill), + Description: theme.muted(skill.description), + Source: skill.source ?? "", + Missing: missing ? theme.warn(missing) : "", + }; + }); + + const columns = [ + { key: "Status", header: "Status", minWidth: 10 }, + { key: "Skill", header: "Skill", minWidth: 18, flex: true }, + { key: "Description", header: "Description", minWidth: 24, flex: true }, + { key: "Source", header: "Source", minWidth: 10 }, + ]; + if (opts.verbose) { + columns.push({ key: "Missing", header: "Missing", minWidth: 18, flex: true }); + } const lines: string[] = []; lines.push( - chalk.bold.cyan("Skills") + chalk.gray(` (${eligible.length}/${skills.length} ready)`), + `${theme.heading("Skills")} ${theme.muted(`(${eligible.length}/${skills.length} ready)`)}`, + ); + lines.push( + renderTable({ + width: tableWidth, + columns, + rows, + }).trimEnd(), ); - lines.push(""); - - if (eligible.length > 0) { - lines.push(chalk.bold.green("Ready:")); - for (const skill of eligible) { - lines.push(` ${formatSkillLine(skill, opts.verbose)}`); - } - } - - if (notEligible.length > 0 && !opts.eligible) { - if (eligible.length > 0) lines.push(""); - lines.push(chalk.bold.yellow("Not ready:")); - for (const skill of notEligible) { - lines.push(` ${formatSkillLine(skill, opts.verbose)}`); - } - } return appendClawdHubHint(lines.join("\n"), opts.json); } @@ -161,27 +161,27 @@ export function formatSkillInfo( const lines: string[] = []; const emoji = skill.emoji ?? "📦"; const status = skill.eligible - ? chalk.green("✓ Ready") + ? theme.success("✓ Ready") : skill.disabled - ? chalk.yellow("⏸ Disabled") + ? theme.warn("⏸ Disabled") : skill.blockedByAllowlist - ? chalk.yellow("🚫 Blocked by allowlist") - : chalk.red("✗ Missing requirements"); + ? theme.warn("🚫 Blocked by allowlist") + : theme.error("✗ Missing requirements"); - lines.push(`${emoji} ${chalk.bold.cyan(skill.name)} ${status}`); + lines.push(`${emoji} ${theme.heading(skill.name)} ${status}`); lines.push(""); - lines.push(chalk.white(skill.description)); + lines.push(skill.description); lines.push(""); // Details - lines.push(chalk.bold("Details:")); - lines.push(` Source: ${skill.source}`); - lines.push(` Path: ${chalk.gray(skill.filePath)}`); + lines.push(theme.heading("Details:")); + lines.push(`${theme.muted(" Source:")} ${skill.source}`); + lines.push(`${theme.muted(" Path:")} ${skill.filePath}`); if (skill.homepage) { - lines.push(` Homepage: ${chalk.blue(skill.homepage)}`); + lines.push(`${theme.muted(" Homepage:")} ${skill.homepage}`); } if (skill.primaryEnv) { - lines.push(` Primary env: ${skill.primaryEnv}`); + lines.push(`${theme.muted(" Primary env:")} ${skill.primaryEnv}`); } // Requirements @@ -194,51 +194,51 @@ export function formatSkillInfo( if (hasRequirements) { lines.push(""); - lines.push(chalk.bold("Requirements:")); + lines.push(theme.heading("Requirements:")); if (skill.requirements.bins.length > 0) { const binsStatus = skill.requirements.bins.map((bin) => { const missing = skill.missing.bins.includes(bin); - return missing ? chalk.red(`✗ ${bin}`) : chalk.green(`✓ ${bin}`); + return missing ? theme.error(`✗ ${bin}`) : theme.success(`✓ ${bin}`); }); - lines.push(` Binaries: ${binsStatus.join(", ")}`); + lines.push(`${theme.muted(" Binaries:")} ${binsStatus.join(", ")}`); } if (skill.requirements.anyBins.length > 0) { const anyBinsMissing = skill.missing.anyBins.length > 0; const anyBinsStatus = skill.requirements.anyBins.map((bin) => { const missing = anyBinsMissing; - return missing ? chalk.red(`✗ ${bin}`) : chalk.green(`✓ ${bin}`); + return missing ? theme.error(`✗ ${bin}`) : theme.success(`✓ ${bin}`); }); - lines.push(` Any binaries: ${anyBinsStatus.join(", ")}`); + lines.push(`${theme.muted(" Any binaries:")} ${anyBinsStatus.join(", ")}`); } if (skill.requirements.env.length > 0) { const envStatus = skill.requirements.env.map((env) => { const missing = skill.missing.env.includes(env); - return missing ? chalk.red(`✗ ${env}`) : chalk.green(`✓ ${env}`); + return missing ? theme.error(`✗ ${env}`) : theme.success(`✓ ${env}`); }); - lines.push(` Environment: ${envStatus.join(", ")}`); + lines.push(`${theme.muted(" Environment:")} ${envStatus.join(", ")}`); } if (skill.requirements.config.length > 0) { const configStatus = skill.requirements.config.map((cfg) => { const missing = skill.missing.config.includes(cfg); - return missing ? chalk.red(`✗ ${cfg}`) : chalk.green(`✓ ${cfg}`); + return missing ? theme.error(`✗ ${cfg}`) : theme.success(`✓ ${cfg}`); }); - lines.push(` Config: ${configStatus.join(", ")}`); + lines.push(`${theme.muted(" Config:")} ${configStatus.join(", ")}`); } if (skill.requirements.os.length > 0) { const osStatus = skill.requirements.os.map((osName) => { const missing = skill.missing.os.includes(osName); - return missing ? chalk.red(`✗ ${osName}`) : chalk.green(`✓ ${osName}`); + return missing ? theme.error(`✗ ${osName}`) : theme.success(`✓ ${osName}`); }); - lines.push(` OS: ${osStatus.join(", ")}`); + lines.push(`${theme.muted(" OS:")} ${osStatus.join(", ")}`); } } // Install options if (skill.install.length > 0 && !skill.eligible) { lines.push(""); - lines.push(chalk.bold("Install options:")); + lines.push(theme.heading("Install options:")); for (const inst of skill.install) { - lines.push(` ${chalk.yellow("→")} ${inst.label}`); + lines.push(` ${theme.warn("→")} ${inst.label}`); } } @@ -281,17 +281,17 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp } const lines: string[] = []; - lines.push(chalk.bold.cyan("Skills Status Check")); + lines.push(theme.heading("Skills Status Check")); lines.push(""); - lines.push(`Total: ${report.skills.length}`); - lines.push(`${chalk.green("✓")} Eligible: ${eligible.length}`); - lines.push(`${chalk.yellow("⏸")} Disabled: ${disabled.length}`); - lines.push(`${chalk.yellow("🚫")} Blocked by allowlist: ${blocked.length}`); - lines.push(`${chalk.red("✗")} Missing requirements: ${missingReqs.length}`); + lines.push(`${theme.muted("Total:")} ${report.skills.length}`); + lines.push(`${theme.success("✓")} ${theme.muted("Eligible:")} ${eligible.length}`); + lines.push(`${theme.warn("⏸")} ${theme.muted("Disabled:")} ${disabled.length}`); + lines.push(`${theme.warn("🚫")} ${theme.muted("Blocked by allowlist:")} ${blocked.length}`); + lines.push(`${theme.error("✗")} ${theme.muted("Missing requirements:")} ${missingReqs.length}`); if (eligible.length > 0) { lines.push(""); - lines.push(chalk.bold.green("Ready to use:")); + lines.push(theme.heading("Ready to use:")); for (const skill of eligible) { const emoji = skill.emoji ?? "📦"; lines.push(` ${emoji} ${skill.name}`); @@ -300,7 +300,7 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp if (missingReqs.length > 0) { lines.push(""); - lines.push(chalk.bold.red("Missing requirements:")); + lines.push(theme.heading("Missing requirements:")); for (const skill of missingReqs) { const emoji = skill.emoji ?? "📦"; const missing: string[] = []; @@ -319,7 +319,7 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp if (skill.missing.os.length > 0) { missing.push(`os: ${skill.missing.os.join(", ")}`); } - lines.push(` ${emoji} ${skill.name} ${chalk.gray(`(${missing.join("; ")})`)}`); + lines.push(` ${emoji} ${skill.name} ${theme.muted(`(${missing.join("; ")})`)}`); } } @@ -350,7 +350,7 @@ export function registerSkillsCli(program: Command) { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); - console.log(formatSkillsList(report, opts)); + defaultRuntime.log(formatSkillsList(report, opts)); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); @@ -367,7 +367,7 @@ export function registerSkillsCli(program: Command) { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); - console.log(formatSkillInfo(report, name, opts)); + defaultRuntime.log(formatSkillInfo(report, name, opts)); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); @@ -383,7 +383,7 @@ export function registerSkillsCli(program: Command) { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); - console.log(formatSkillsCheck(report, opts)); + defaultRuntime.log(formatSkillsCheck(report, opts)); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1); @@ -396,7 +396,7 @@ export function registerSkillsCli(program: Command) { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); - console.log(formatSkillsList(report, {})); + defaultRuntime.log(formatSkillsList(report, {})); } catch (err) { defaultRuntime.error(String(err)); defaultRuntime.exit(1);