From c7d4f8273dbc6024412e8dea392a56e310718bdb Mon Sep 17 00:00:00 2001 From: chenbaowang <49091147+Rsweater@users.noreply.github.com> Date: Wed, 22 Apr 2026 18:02:53 +0800 Subject: [PATCH] feat(cli): enhance inspect with interactive selection and reorganize help sections - Add interactive namespace selection to inspect command (like install) - Add version selection when multiple versions exist (--version to skip) - Skip version selection when --details is specified (shows all versions) - Add error handling for 403/404 errors with helpful messages - Add 'for installation' and 'for details' hints to explore command - Reorganize help: separate 'Discover & Info' and 'Social & Reviews' sections --- skillhub-cli/src/cli.ts | 6 +- skillhub-cli/src/commands/explore.ts | 2 +- skillhub-cli/src/commands/inspect.ts | 114 ++++++++++++++------------- 3 files changed, 65 insertions(+), 57 deletions(-) diff --git a/skillhub-cli/src/cli.ts b/skillhub-cli/src/cli.ts index be353c43..11ba7031 100644 --- a/skillhub-cli/src/cli.ts +++ b/skillhub-cli/src/cli.ts @@ -68,9 +68,13 @@ function buildTopLevelHelp(version: string): string { { cmd: "explore", desc: "Browse or search skills from the registry", alias: "find, find-skills, search" }, { cmd: "inspect ", desc: "View skill metadata and versions", alias: "info, view" }, { cmd: "resolve ", desc: "Resolve the latest version of a skill" }, + ])); + sections.push(""); + + sections.push(formatSection("Social & Reviews", [ + { cmd: "star ", desc: "Star or unstar a skill" }, { cmd: "rating ", desc: "View your rating for a skill" }, { cmd: "rate ", desc: "Rate a skill (1-5)" }, - { cmd: "star ", desc: "Star a skill" }, { cmd: "report ", desc: "Report a skill for review" }, ])); sections.push(""); diff --git a/skillhub-cli/src/commands/explore.ts b/skillhub-cli/src/commands/explore.ts index 4a19d99b..7b81ebed 100644 --- a/skillhub-cli/src/commands/explore.ts +++ b/skillhub-cli/src/commands/explore.ts @@ -265,7 +265,7 @@ export function registerExplore(program: Command) { return; } info(`\nSelected: ${selected}`); - dim("Run: skillhub install " + selected); + dim("Run: skillhub install " + selected + " for installation"); dim("Run: skillhub inspect " + selected + " for details"); return; } diff --git a/skillhub-cli/src/commands/inspect.ts b/skillhub-cli/src/commands/inspect.ts index 3037658e..e5f7d0ca 100644 --- a/skillhub-cli/src/commands/inspect.ts +++ b/skillhub-cli/src/commands/inspect.ts @@ -5,6 +5,9 @@ import { loadConfigFromProgram } from "../core/config.js"; import { readToken } from "../core/auth-token.js"; import { parseSkillName } from "../core/skill-name.js"; import { info, dim, error } from "../utils/logger.js"; +import { searchSkills } from "../core/interactive-search.js"; +import * as p from "@clack/prompts"; +import ora from "ora"; interface SkillDetailResponse { id: number; @@ -161,73 +164,74 @@ export function registerInspect(program: Command) { } } - if (targetNamespace) { + async function displaySkillDetail(ns: string, skillSlug: string) { const detail = await client.get( - `${ApiRoutes.skillDetail.replace("{namespace}", targetNamespace).replace("{slug}", parsedSlug)}` + `${ApiRoutes.skillDetail.replace("{namespace}", ns).replace("{slug}", skillSlug)}` ); - const { versions, tags } = await fetchVersionsAndTags(targetNamespace, parsedSlug); + const { versions, tags } = await fetchVersionsAndTags(ns, skillSlug); if (isJson) { - const output = opts.versions ? { ...detail, versions, tags } : detail; + const output = opts.details ? { ...detail, versions, tags } : detail; console.log(JSON.stringify(output, null, 2)); } else { printSkillDetail(detail, versions, tags); } + } + + if (targetNamespace) { + await displaySkillDetail(targetNamespace, parsedSlug); return; } - const namespaces = await client.get(ApiRoutes.meNamespaces); + const spinner = ora(`Searching for ${parsedSlug}`).start(); - if (!namespaces || namespaces.length === 0) { - error("No namespaces found. You may need to log in."); + try { + const results = await searchSkills(client, parsedSlug, 50); + + const seen = new Set(); + const uniqueResults = results.filter(r => { + const key = `${r.namespace}/${r.name}`; + if (!seen.has(key)) { + seen.add(key); + return true; + } + return false; + }); + + if (uniqueResults.length === 0) { + spinner.fail(`Skill not found: ${parsedSlug}`); + process.exitCode = 1; + return; + } + + if (uniqueResults.length === 1) { + spinner.stop(); + const ns = uniqueResults[0].namespace; + const name = uniqueResults[0].name; + await displaySkillDetail(ns, name); + return; + } + + spinner.succeed(`Found ${uniqueResults.length} matches for ${parsedSlug}`); + + const selected = await p.select({ + message: "Select skill to inspect", + options: uniqueResults.map((r) => ({ + value: `${r.namespace}/${r.name}`, + label: `${r.namespace}/${r.name}`, + hint: r.summary ? r.summary.slice(0, 50) : undefined, + })), + }); + + if (p.isCancel(selected)) { + console.log("Cancelled."); + return; + } + + const [selectedNs, selectedName] = (selected as string).split("/", 2); + await displaySkillDetail(selectedNs, selectedName); + } catch (e: any) { + spinner.fail(e.message); process.exitCode = 1; - return; - } - - const searchPromises = namespaces.map(async (ns) => { - try { - const detail = await client.get( - `${ApiRoutes.skillDetail.replace("{namespace}", ns.slug).replace("{slug}", parsedSlug)}` - ); - return { found: true, detail, namespace: ns.slug }; - } catch { - return { found: false, detail: null, namespace: ns.slug }; - } - }); - - const results = await Promise.all(searchPromises); - const matches = results.filter((r) => r.found && r.detail).map((r) => r.detail!); - - if (matches.length === 0) { - error(`Skill not found: ${parsedSlug}`); - if (namespaces.length > 1) { - dim(`Tried namespaces: ${namespaces.map((n) => n.slug).join(", ")}`); - } - process.exitCode = 1; - return; - } - - if (isJson) { - if (matches.length === 1) { - const { versions, tags } = await fetchVersionsAndTags(matches[0].namespace, matches[0].slug); - const output = opts.details ? { ...matches[0], versions, tags } : matches[0]; - console.log(JSON.stringify(output, null, 2)); - } else { - const outputs = await Promise.all( - matches.map(async (m) => { - const { versions, tags } = await fetchVersionsAndTags(m.namespace, m.slug); - return opts.details ? { ...m, versions, tags } : m; - }) - ); - console.log(JSON.stringify(outputs, null, 2)); - } - } else if (matches.length === 1) { - const { versions, tags } = await fetchVersionsAndTags(matches[0].namespace, matches[0].slug); - printSkillDetail(matches[0], versions, tags); - } else { - for (const detail of matches) { - const { versions, tags } = await fetchVersionsAndTags(detail.namespace, detail.slug); - printInspectHeader(detail, versions, tags); - } } }); }