Revert "cli: format skills capability output"

This reverts commit acd5e8b41428f939d95a0000c4e7e62d0267690d.
This commit is contained in:
Vincent Koc
2026-02-27 09:04:02 -08:00
parent 25b0b62c1b
commit af26a80bba

View File

@@ -1,5 +1,4 @@
import type { SkillStatusEntry, SkillStatusReport } from "../agents/skills-status.js";
import type { SkillCapability } from "../agents/skills/types.js";
import { renderTable } from "../terminal/table.js";
import { theme } from "../terminal/theme.js";
import { shortenHomePath } from "../utils.js";
@@ -19,37 +18,6 @@ export type SkillsCheckOptions = {
json?: boolean;
};
const CAPABILITY_ICONS: Record<SkillCapability, string> = {
shell: "shell",
filesystem: "filesystem",
network: "network",
browser: "browser",
sessions: "sessions",
};
function formatCapabilityTags(capabilities: SkillCapability[]): string {
if (capabilities.length === 0) {
return "";
}
return capabilities.map((cap) => CAPABILITY_ICONS[cap] ?? cap).join(" ");
}
function formatScanBadge(scanResult?: { severity: string }): string {
if (!scanResult) {
return "";
}
switch (scanResult.severity) {
case "critical":
return theme.error("[blocked]");
case "warn":
return theme.warn("[warn]");
case "info":
return theme.muted("[notice]");
default:
return "";
}
}
function appendClawHubHint(output: string, json?: boolean): string {
if (json) {
return output;
@@ -58,19 +26,21 @@ function appendClawHubHint(output: string, json?: boolean): string {
}
function formatSkillStatus(skill: SkillStatusEntry): string {
if (skill.scanResult?.severity === "critical") {
return theme.error("x blocked");
}
if (skill.eligible) {
return theme.success("+ ready");
return theme.success(" ready");
}
if (skill.disabled) {
return theme.warn("- disabled");
return theme.warn(" disabled");
}
if (skill.blockedByAllowlist) {
return theme.warn("x blocked");
return theme.warn("🚫 blocked");
}
return theme.error("x missing");
return theme.error(" missing");
}
function formatSkillName(skill: SkillStatusEntry): string {
const emoji = skill.emoji ?? "📦";
return `${emoji} ${theme.command(skill.name)}`;
}
function formatSkillMissingSummary(skill: SkillStatusEntry): string {
@@ -112,8 +82,6 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
primaryEnv: s.primaryEnv,
homepage: s.homepage,
missing: s.missing,
capabilities: s.capabilities,
scanResult: s.scanResult,
})),
};
return JSON.stringify(jsonReport, null, 2);
@@ -127,26 +95,13 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
}
const eligible = skills.filter((s) => s.eligible);
const termWidth = process.stdout.columns ?? 120;
const tableWidth = Math.max(60, termWidth - 1);
const descLimit = opts.verbose ? 30 : 44;
const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
const rows = skills.map((skill) => {
const missing = formatSkillMissingSummary(skill);
const caps = formatCapabilityTags(skill.capabilities);
const scan = formatScanBadge(skill.scanResult);
// Plain text name (no emoji) to avoid double-width alignment issues
const name = theme.command(skill.name);
const skillLabel = caps ? `${name} ${theme.muted(caps)}` : name;
// Truncate description as plain text BEFORE applying ANSI
const rawDesc =
skill.description.length > descLimit
? skill.description.slice(0, descLimit - 1) + "..."
: skill.description;
return {
Status: formatSkillStatus(skill),
Skill: skillLabel,
Scan: scan,
Description: theme.muted(rawDesc),
Skill: formatSkillName(skill),
Description: theme.muted(skill.description),
Source: skill.source ?? "",
Missing: missing ? theme.warn(missing) : "",
};
@@ -154,13 +109,12 @@ export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOpti
const columns = [
{ key: "Status", header: "Status", minWidth: 10 },
{ key: "Skill", header: "Skill", minWidth: 16 },
{ key: "Description", header: "Description", minWidth: 20, maxWidth: descLimit + 4 },
{ 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: "Scan", header: "Scan", minWidth: 10 });
columns.push({ key: "Missing", header: "Missing", minWidth: 14 });
columns.push({ key: "Missing", header: "Missing", minWidth: 18, flex: true });
}
const lines: string[] = [];
@@ -199,165 +153,85 @@ export function formatSkillInfo(
return JSON.stringify(skill, null, 2);
}
const status =
skill.scanResult?.severity === "critical"
? theme.error("x Blocked (security)")
: skill.eligible
? theme.success("+ Ready")
: skill.disabled
? theme.warn("- Disabled")
: skill.blockedByAllowlist
? theme.warn("x Blocked by allowlist")
: theme.error("x Missing requirements");
const lines: string[] = [];
lines.push(`${theme.heading(skill.name)} ${status}`);
const emoji = skill.emoji ?? "📦";
const status = skill.eligible
? theme.success("✓ Ready")
: skill.disabled
? theme.warn("⏸ Disabled")
: skill.blockedByAllowlist
? theme.warn("🚫 Blocked by allowlist")
: theme.error("✗ Missing requirements");
lines.push(`${emoji} ${theme.heading(skill.name)} ${status}`);
lines.push("");
lines.push(skill.description);
lines.push("");
// Details table
const detailRows: Array<Record<string, string>> = [
{ Field: "Source", Value: skill.source },
{ Field: "Path", Value: shortenHomePath(skill.filePath) },
];
lines.push(theme.heading("Details:"));
lines.push(`${theme.muted(" Source:")} ${skill.source}`);
lines.push(`${theme.muted(" Path:")} ${shortenHomePath(skill.filePath)}`);
if (skill.homepage) {
detailRows.push({ Field: "Homepage", Value: skill.homepage });
lines.push(`${theme.muted(" Homepage:")} ${skill.homepage}`);
}
if (skill.primaryEnv) {
detailRows.push({ Field: "Primary env", Value: skill.primaryEnv });
lines.push(`${theme.muted(" Primary env:")} ${skill.primaryEnv}`);
}
lines.push("");
lines.push(
renderTable({
columns: [
{ key: "Field", header: "Detail", minWidth: 12 },
{ key: "Value", header: "Value", minWidth: 20 },
],
rows: detailRows,
}).trimEnd(),
);
// Capabilities table
if (skill.capabilities.length > 0) {
const capLabels: Record<SkillCapability, string> = {
shell: "Run shell commands",
filesystem: "Read and write files",
network: "Make outbound HTTP requests",
browser: "Control browser sessions",
sessions: "Spawn sub-sessions and agents",
};
const capRows = skill.capabilities.map((cap) => ({
Capability: CAPABILITY_ICONS[cap] ?? cap,
Name: cap,
Description: capLabels[cap] ?? cap,
}));
const hasRequirements =
skill.requirements.bins.length > 0 ||
skill.requirements.anyBins.length > 0 ||
skill.requirements.env.length > 0 ||
skill.requirements.config.length > 0 ||
skill.requirements.os.length > 0;
if (hasRequirements) {
lines.push("");
lines.push(theme.heading("Capabilities"));
lines.push(
renderTable({
columns: [
{ key: "Capability", header: "Icon", minWidth: 6 },
{ key: "Name", header: "Capability", minWidth: 12 },
{ key: "Description", header: "Description", minWidth: 20 },
],
rows: capRows,
}).trimEnd(),
);
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 ? theme.error(`${bin}`) : theme.success(`${bin}`);
});
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 ? theme.error(`${bin}`) : theme.success(`${bin}`);
});
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 ? theme.error(`${env}`) : theme.success(`${env}`);
});
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 ? theme.error(`${cfg}`) : theme.success(`${cfg}`);
});
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 ? theme.error(`${osName}`) : theme.success(`${osName}`);
});
lines.push(`${theme.muted(" OS:")} ${osStatus.join(", ")}`);
}
}
// Security table
if (skill.scanResult) {
const scanBadge = formatScanBadge(skill.scanResult);
const secRows: Array<Record<string, string>> = [
{ Field: "Scan", Value: scanBadge || theme.success("+ clean") },
];
lines.push("");
lines.push(theme.heading("Security"));
lines.push(
renderTable({
columns: [
{ key: "Field", header: "Check", minWidth: 10 },
{ key: "Value", header: "Result", minWidth: 14 },
],
rows: secRows,
}).trimEnd(),
);
}
// Requirements table
const reqRows: Array<Record<string, string>> = [];
for (const bin of skill.requirements.bins) {
const ok = !skill.missing.bins.includes(bin);
reqRows.push({
Type: "bin",
Name: bin,
Status: ok ? theme.success("+ ok") : theme.error("x missing"),
});
}
for (const bin of skill.requirements.anyBins) {
const ok = skill.missing.anyBins.length === 0;
reqRows.push({
Type: "anyBin",
Name: bin,
Status: ok ? theme.success("+ ok") : theme.error("x missing"),
});
}
for (const env of skill.requirements.env) {
const ok = !skill.missing.env.includes(env);
reqRows.push({
Type: "env",
Name: env,
Status: ok ? theme.success("+ ok") : theme.error("x missing"),
});
}
for (const cfg of skill.requirements.config) {
const ok = !skill.missing.config.includes(cfg);
reqRows.push({
Type: "config",
Name: cfg,
Status: ok ? theme.success("+ ok") : theme.error("x missing"),
});
}
for (const osName of skill.requirements.os) {
const ok = !skill.missing.os.includes(osName);
reqRows.push({
Type: "os",
Name: osName,
Status: ok ? theme.success("+ ok") : theme.error("x missing"),
});
}
if (reqRows.length > 0) {
lines.push("");
lines.push(theme.heading("Requirements"));
lines.push(
renderTable({
columns: [
{ key: "Type", header: "Type", minWidth: 8 },
{ key: "Name", header: "Name", minWidth: 14 },
{ key: "Status", header: "Status", minWidth: 10 },
],
rows: reqRows,
}).trimEnd(),
);
}
// Install options table
if (skill.install.length > 0 && !skill.eligible) {
const installRows = skill.install.map((inst) => ({
Kind: inst.kind,
Label: inst.label,
}));
lines.push("");
lines.push(theme.heading("Install options"));
lines.push(
renderTable({
columns: [
{ key: "Kind", header: "Kind", minWidth: 8 },
{ key: "Label", header: "Action", minWidth: 20 },
],
rows: installRows,
}).trimEnd(),
);
lines.push(theme.heading("Install options:"));
for (const inst of skill.install) {
lines.push(` ${theme.warn("→")} ${inst.label}`);
}
}
return appendClawHubHint(lines.join("\n"), opts.json);
@@ -397,113 +271,22 @@ export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOp
const lines: string[] = [];
lines.push(theme.heading("Skills Status Check"));
lines.push("");
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}`);
// Summary table
const summaryRows = [
{ Metric: "Total", Count: String(report.skills.length) },
{ Metric: theme.success("Eligible"), Count: String(eligible.length) },
{ Metric: theme.warn("Disabled"), Count: String(disabled.length) },
{ Metric: theme.warn("Blocked (allowlist)"), Count: String(blocked.length) },
{ Metric: theme.error("Missing requirements"), Count: String(missingReqs.length) },
];
lines.push(
renderTable({
columns: [
{ key: "Metric", header: "Status", minWidth: 20 },
{ key: "Count", header: "Count", minWidth: 6 },
],
rows: summaryRows,
}).trimEnd(),
);
// Capability summary for community skills
const communitySkills = report.skills.filter(
(s) => s.source === "openclaw-managed" && !s.bundled,
);
if (communitySkills.length > 0) {
const capCounts = new Map<SkillCapability, string[]>();
for (const skill of communitySkills) {
for (const cap of skill.capabilities) {
const list = capCounts.get(cap) ?? [];
list.push(skill.name);
capCounts.set(cap, list);
}
}
if (capCounts.size > 0) {
const capRows = [...capCounts.entries()].map(([cap, names]) => ({
Icon: CAPABILITY_ICONS[cap] ?? cap,
Capability: cap,
Count: String(names.length),
Skills: names.join(", "),
}));
lines.push("");
lines.push(theme.heading("Community skill capabilities"));
lines.push(
renderTable({
columns: [
{ key: "Icon", header: "Icon", minWidth: 5 },
{ key: "Capability", header: "Capability", minWidth: 12 },
{ key: "Count", header: "#", minWidth: 4 },
{ key: "Skills", header: "Skills", minWidth: 16 },
],
rows: capRows,
}).trimEnd(),
);
}
// Scan results summary
const scanClean = communitySkills.filter(
(s) => !s.scanResult || s.scanResult.severity === "clean",
).length;
const scanWarn = communitySkills.filter((s) => s.scanResult?.severity === "warn").length;
const scanBlocked = communitySkills.filter((s) => s.scanResult?.severity === "critical").length;
if (scanWarn > 0 || scanBlocked > 0) {
const scanRows = [
{ Result: theme.success("Clean"), Count: String(scanClean) },
...(scanWarn > 0 ? [{ Result: theme.warn("Warning"), Count: String(scanWarn) }] : []),
...(scanBlocked > 0
? [{ Result: theme.error("Blocked"), Count: String(scanBlocked) }]
: []),
];
lines.push("");
lines.push(theme.heading("Scan results"));
lines.push(
renderTable({
columns: [
{ key: "Result", header: "Result", minWidth: 10 },
{ key: "Count", header: "#", minWidth: 4 },
],
rows: scanRows,
}).trimEnd(),
);
}
}
// Ready skills table
if (eligible.length > 0) {
const readyRows = eligible.map((skill) => {
const caps = formatCapabilityTags(skill.capabilities);
return {
Skill: theme.command(skill.name),
Caps: caps ? theme.muted(caps) : "",
Source: skill.source,
};
});
lines.push("");
lines.push(theme.heading("Ready to use"));
lines.push(
renderTable({
columns: [
{ key: "Skill", header: "Skill", minWidth: 16 },
{ key: "Caps", header: "Caps", minWidth: 8 },
{ key: "Source", header: "Source", minWidth: 10 },
],
rows: readyRows,
}).trimEnd(),
);
lines.push(theme.heading("Ready to use:"));
for (const skill of eligible) {
const emoji = skill.emoji ?? "📦";
lines.push(` ${emoji} ${skill.name}`);
}
}
// Missing requirements
if (missingReqs.length > 0) {
lines.push("");
lines.push(theme.heading("Missing requirements:"));