diff --git a/src/cli/skills-cli.format.ts b/src/cli/skills-cli.format.ts new file mode 100644 index 0000000000..97f77a3530 --- /dev/null +++ b/src/cli/skills-cli.format.ts @@ -0,0 +1,316 @@ +import type { SkillStatusEntry, SkillStatusReport } from "../agents/skills-status.js"; +import { renderTable } from "../terminal/table.js"; +import { theme } from "../terminal/theme.js"; +import { shortenHomePath } from "../utils.js"; +import { formatCliCommand } from "./command-format.js"; + +export type SkillsListOptions = { + json?: boolean; + eligible?: boolean; + verbose?: boolean; +}; + +export type SkillInfoOptions = { + json?: boolean; +}; + +export type SkillsCheckOptions = { + json?: boolean; +}; + +function appendClawHubHint(output: string, json?: boolean): string { + if (json) { + return output; + } + return `${output}\n\nTip: use \`npx clawhub\` to search, install, and sync skills.`; +} + +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 ?? "📦"; + return `${emoji} ${theme.command(skill.name)}`; +} + +function formatSkillMissingSummary(skill: SkillStatusEntry): string { + 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(", ")}`); + } + return missing.join("; "); +} + +export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOptions): string { + const skills = opts.eligible ? report.skills.filter((s) => s.eligible) : report.skills; + + if (opts.json) { + const jsonReport = { + workspaceDir: report.workspaceDir, + managedSkillsDir: report.managedSkillsDir, + skills: skills.map((s) => ({ + name: s.name, + description: s.description, + emoji: s.emoji, + eligible: s.eligible, + disabled: s.disabled, + blockedByAllowlist: s.blockedByAllowlist, + source: s.source, + bundled: s.bundled, + primaryEnv: s.primaryEnv, + homepage: s.homepage, + missing: s.missing, + })), + }; + return JSON.stringify(jsonReport, null, 2); + } + + if (skills.length === 0) { + const message = opts.eligible + ? `No eligible skills found. Run \`${formatCliCommand("openclaw skills list")}\` to see all skills.` + : "No skills found."; + return appendClawHubHint(message, opts.json); + } + + const eligible = 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( + `${theme.heading("Skills")} ${theme.muted(`(${eligible.length}/${skills.length} ready)`)}`, + ); + lines.push( + renderTable({ + width: tableWidth, + columns, + rows, + }).trimEnd(), + ); + + return appendClawHubHint(lines.join("\n"), opts.json); +} + +export function formatSkillInfo( + report: SkillStatusReport, + skillName: string, + opts: SkillInfoOptions, +): string { + const skill = report.skills.find((s) => s.name === skillName || s.skillKey === skillName); + + if (!skill) { + if (opts.json) { + return JSON.stringify({ error: "not found", skill: skillName }, null, 2); + } + return appendClawHubHint( + `Skill "${skillName}" not found. Run \`${formatCliCommand("openclaw skills list")}\` to see available skills.`, + opts.json, + ); + } + + if (opts.json) { + return JSON.stringify(skill, null, 2); + } + + const lines: string[] = []; + 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(""); + + lines.push(theme.heading("Details:")); + lines.push(`${theme.muted(" Source:")} ${skill.source}`); + lines.push(`${theme.muted(" Path:")} ${shortenHomePath(skill.filePath)}`); + if (skill.homepage) { + lines.push(`${theme.muted(" Homepage:")} ${skill.homepage}`); + } + if (skill.primaryEnv) { + lines.push(`${theme.muted(" Primary env:")} ${skill.primaryEnv}`); + } + + 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("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(", ")}`); + } + } + + if (skill.install.length > 0 && !skill.eligible) { + lines.push(""); + 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); +} + +export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOptions): string { + const eligible = report.skills.filter((s) => s.eligible); + const disabled = report.skills.filter((s) => s.disabled); + const blocked = report.skills.filter((s) => s.blockedByAllowlist && !s.disabled); + const missingReqs = report.skills.filter( + (s) => !s.eligible && !s.disabled && !s.blockedByAllowlist, + ); + + if (opts.json) { + return JSON.stringify( + { + summary: { + total: report.skills.length, + eligible: eligible.length, + disabled: disabled.length, + blocked: blocked.length, + missingRequirements: missingReqs.length, + }, + eligible: eligible.map((s) => s.name), + disabled: disabled.map((s) => s.name), + blocked: blocked.map((s) => s.name), + missingRequirements: missingReqs.map((s) => ({ + name: s.name, + missing: s.missing, + install: s.install, + })), + }, + null, + 2, + ); + } + + 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}`); + + if (eligible.length > 0) { + lines.push(""); + lines.push(theme.heading("Ready to use:")); + for (const skill of eligible) { + const emoji = skill.emoji ?? "📦"; + lines.push(` ${emoji} ${skill.name}`); + } + } + + if (missingReqs.length > 0) { + lines.push(""); + lines.push(theme.heading("Missing requirements:")); + for (const skill of missingReqs) { + const emoji = skill.emoji ?? "📦"; + 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(", ")}`); + } + lines.push(` ${emoji} ${skill.name} ${theme.muted(`(${missing.join("; ")})`)}`); + } + } + + return appendClawHubHint(lines.join("\n"), opts.json); +} diff --git a/src/cli/skills-cli.test.ts b/src/cli/skills-cli.test.ts index c105d0f4d4..afc9336786 100644 --- a/src/cli/skills-cli.test.ts +++ b/src/cli/skills-cli.test.ts @@ -1,14 +1,16 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { afterAll, beforeAll, describe, expect, it } from "vitest"; -import { - buildWorkspaceSkillStatus, - type SkillStatusEntry, - type SkillStatusReport, -} from "../agents/skills-status.js"; -import { formatSkillInfo, formatSkillsCheck, formatSkillsList } from "./skills-cli.js"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import type { SkillStatusEntry, SkillStatusReport } from "../agents/skills-status.js"; +import type { SkillEntry } from "../agents/skills.js"; +import { formatSkillInfo, formatSkillsCheck, formatSkillsList } from "./skills-cli.format.js"; + +// Unit tests: don't pay the runtime cost of loading/parsing the real skills loader. +vi.mock("@mariozechner/pi-coding-agent", () => ({ + loadSkillsFromDir: () => ({ skills: [] }), + formatSkillsForPrompt: () => "", +})); function createMockSkill(overrides: Partial = {}): SkillStatusEntry { return { @@ -227,25 +229,29 @@ describe("skills-cli", () => { } }); - function resolveBundledSkillsDir(): string | undefined { - const moduleDir = path.dirname(fileURLToPath(import.meta.url)); - const root = path.resolve(moduleDir, "..", ".."); - const candidate = path.join(root, "skills"); - if (fs.existsSync(candidate)) { - return candidate; - } - return undefined; - } - - it("loads bundled skills and formats them", () => { - const bundledDir = resolveBundledSkillsDir(); - if (!bundledDir) { - // Skip if skills dir not found (e.g., in CI without skills) - return; - } + const createEntries = (): SkillEntry[] => { + const baseDir = path.join(tempWorkspaceDir, "peekaboo"); + return [ + { + skill: { + name: "peekaboo", + description: "Capture UI screenshots", + source: "openclaw-bundled", + filePath: path.join(baseDir, "SKILL.md"), + baseDir, + } as SkillEntry["skill"], + frontmatter: {}, + metadata: { emoji: "📸" }, + }, + ]; + }; + it("loads bundled skills and formats them", async () => { + const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js"); + const entries = createEntries(); const report = buildWorkspaceSkillStatus(tempWorkspaceDir, { managedSkillsDir: "/nonexistent", + entries, }); // Should have loaded some skills @@ -264,21 +270,18 @@ describe("skills-cli", () => { expect(parsed.skills).toBeInstanceOf(Array); }); - it("formats info for a real bundled skill (peekaboo)", () => { - const bundledDir = resolveBundledSkillsDir(); - if (!bundledDir) { - return; - } - + it("formats info for a real bundled skill (peekaboo)", async () => { + const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js"); + const entries = createEntries(); const report = buildWorkspaceSkillStatus(tempWorkspaceDir, { managedSkillsDir: "/nonexistent", + entries, }); // peekaboo is a bundled skill that should always exist const peekaboo = report.skills.find((s) => s.name === "peekaboo"); if (!peekaboo) { - // Skip if peekaboo not found - return; + throw new Error("peekaboo fixture skill missing"); } const output = formatSkillInfo(report, "peekaboo", {}); diff --git a/src/cli/skills-cli.ts b/src/cli/skills-cli.ts index 4941f38a28..6ed962564d 100644 --- a/src/cli/skills-cli.ts +++ b/src/cli/skills-cli.ts @@ -1,340 +1,17 @@ import type { Command } from "commander"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; -import { - buildWorkspaceSkillStatus, - type SkillStatusEntry, - type SkillStatusReport, -} from "../agents/skills-status.js"; 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 { shortenHomePath } from "../utils.js"; -import { formatCliCommand } from "./command-format.js"; +import { formatSkillInfo, formatSkillsCheck, formatSkillsList } from "./skills-cli.format.js"; -export type SkillsListOptions = { - json?: boolean; - eligible?: boolean; - verbose?: boolean; -}; - -export type SkillInfoOptions = { - json?: boolean; -}; - -export type SkillsCheckOptions = { - json?: boolean; -}; - -function appendClawHubHint(output: string, json?: boolean): string { - if (json) { - return output; - } - return `${output}\n\nTip: use \`npx clawhub\` to search, install, and sync skills.`; -} - -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 ?? "📦"; - return `${emoji} ${theme.command(skill.name)}`; -} - -function formatSkillMissingSummary(skill: SkillStatusEntry): string { - 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(", ")}`); - } - return missing.join("; "); -} - -/** - * Format the skills list output - */ -export function formatSkillsList(report: SkillStatusReport, opts: SkillsListOptions): string { - const skills = opts.eligible ? report.skills.filter((s) => s.eligible) : report.skills; - - if (opts.json) { - const jsonReport = { - workspaceDir: report.workspaceDir, - managedSkillsDir: report.managedSkillsDir, - skills: skills.map((s) => ({ - name: s.name, - description: s.description, - emoji: s.emoji, - eligible: s.eligible, - disabled: s.disabled, - blockedByAllowlist: s.blockedByAllowlist, - source: s.source, - bundled: s.bundled, - primaryEnv: s.primaryEnv, - homepage: s.homepage, - missing: s.missing, - })), - }; - return JSON.stringify(jsonReport, null, 2); - } - - if (skills.length === 0) { - const message = opts.eligible - ? `No eligible skills found. Run \`${formatCliCommand("openclaw skills list")}\` to see all skills.` - : "No skills found."; - return appendClawHubHint(message, opts.json); - } - - const eligible = 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( - `${theme.heading("Skills")} ${theme.muted(`(${eligible.length}/${skills.length} ready)`)}`, - ); - lines.push( - renderTable({ - width: tableWidth, - columns, - rows, - }).trimEnd(), - ); - - return appendClawHubHint(lines.join("\n"), opts.json); -} - -/** - * Format detailed info for a single skill - */ -export function formatSkillInfo( - report: SkillStatusReport, - skillName: string, - opts: SkillInfoOptions, -): string { - const skill = report.skills.find((s) => s.name === skillName || s.skillKey === skillName); - - if (!skill) { - if (opts.json) { - return JSON.stringify({ error: "not found", skill: skillName }, null, 2); - } - return appendClawHubHint( - `Skill "${skillName}" not found. Run \`${formatCliCommand("openclaw skills list")}\` to see available skills.`, - opts.json, - ); - } - - if (opts.json) { - return JSON.stringify(skill, null, 2); - } - - const lines: string[] = []; - 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 - lines.push(theme.heading("Details:")); - lines.push(`${theme.muted(" Source:")} ${skill.source}`); - lines.push(`${theme.muted(" Path:")} ${shortenHomePath(skill.filePath)}`); - if (skill.homepage) { - lines.push(`${theme.muted(" Homepage:")} ${skill.homepage}`); - } - if (skill.primaryEnv) { - lines.push(`${theme.muted(" Primary env:")} ${skill.primaryEnv}`); - } - - // Requirements - 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("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(", ")}`); - } - } - - // Install options - if (skill.install.length > 0 && !skill.eligible) { - lines.push(""); - 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); -} - -/** - * Format a check/summary of all skills status - */ -export function formatSkillsCheck(report: SkillStatusReport, opts: SkillsCheckOptions): string { - const eligible = report.skills.filter((s) => s.eligible); - const disabled = report.skills.filter((s) => s.disabled); - const blocked = report.skills.filter((s) => s.blockedByAllowlist && !s.disabled); - const missingReqs = report.skills.filter( - (s) => !s.eligible && !s.disabled && !s.blockedByAllowlist, - ); - - if (opts.json) { - return JSON.stringify( - { - summary: { - total: report.skills.length, - eligible: eligible.length, - disabled: disabled.length, - blocked: blocked.length, - missingRequirements: missingReqs.length, - }, - eligible: eligible.map((s) => s.name), - disabled: disabled.map((s) => s.name), - blocked: blocked.map((s) => s.name), - missingRequirements: missingReqs.map((s) => ({ - name: s.name, - missing: s.missing, - install: s.install, - })), - }, - null, - 2, - ); - } - - 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}`); - - if (eligible.length > 0) { - lines.push(""); - lines.push(theme.heading("Ready to use:")); - for (const skill of eligible) { - const emoji = skill.emoji ?? "📦"; - lines.push(` ${emoji} ${skill.name}`); - } - } - - if (missingReqs.length > 0) { - lines.push(""); - lines.push(theme.heading("Missing requirements:")); - for (const skill of missingReqs) { - const emoji = skill.emoji ?? "📦"; - 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(", ")}`); - } - lines.push(` ${emoji} ${skill.name} ${theme.muted(`(${missing.join("; ")})`)}`); - } - } - - return appendClawHubHint(lines.join("\n"), opts.json); -} +export type { + SkillInfoOptions, + SkillsCheckOptions, + SkillsListOptions, +} from "./skills-cli.format.js"; +export { formatSkillInfo, formatSkillsCheck, formatSkillsList } from "./skills-cli.format.js"; /** * Register the skills CLI commands @@ -359,6 +36,7 @@ export function registerSkillsCli(program: Command) { try { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); + const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js"); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); defaultRuntime.log(formatSkillsList(report, opts)); } catch (err) { @@ -376,6 +54,7 @@ export function registerSkillsCli(program: Command) { try { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); + const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js"); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); defaultRuntime.log(formatSkillInfo(report, name, opts)); } catch (err) { @@ -392,6 +71,7 @@ export function registerSkillsCli(program: Command) { try { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); + const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js"); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); defaultRuntime.log(formatSkillsCheck(report, opts)); } catch (err) { @@ -405,6 +85,7 @@ export function registerSkillsCli(program: Command) { try { const config = loadConfig(); const workspaceDir = resolveAgentWorkspaceDir(config, resolveDefaultAgentId(config)); + const { buildWorkspaceSkillStatus } = await import("../agents/skills-status.js"); const report = buildWorkspaceSkillStatus(workspaceDir, { config }); defaultRuntime.log(formatSkillsList(report, {})); } catch (err) {