feat: theme hooks/skills/plugins output

This commit is contained in:
Peter Steinberger
2026-01-21 04:47:35 +00:00
parent 2cd62f94a5
commit fa7df1976d
4 changed files with 304 additions and 253 deletions

View File

@@ -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);