From 2fffc2b3775c2cbc06fe9ae7dc423298fc9a92b5 Mon Sep 17 00:00:00 2001 From: chenbaowang <49091147+Rsweater@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:01:12 +0800 Subject: [PATCH] fix(cli): remove unsupported rating sort, add stars sort, clean up versions command - Remove --rating option and rating sort (API doesn't support it) - Add --stars shorthand for star-based sorting - Fix interactive-search.ts to use stars sort instead of rating - Add ratingAvg field to SkillDetail and display in explore output - Delete deprecated versions.ts command (functionality merged into inspect) --- skillhub-cli/src/commands/explore.ts | 21 ++- skillhub-cli/src/commands/versions.ts | 135 -------------------- skillhub-cli/src/core/interactive-search.ts | 10 +- skillhub-cli/src/schema/routes.ts | 1 + 4 files changed, 21 insertions(+), 146 deletions(-) delete mode 100644 skillhub-cli/src/commands/versions.ts diff --git a/skillhub-cli/src/commands/explore.ts b/skillhub-cli/src/commands/explore.ts index b9e44d84..676a520e 100644 --- a/skillhub-cli/src/commands/explore.ts +++ b/skillhub-cli/src/commands/explore.ts @@ -32,6 +32,7 @@ interface SkillDetail { starCount: number; downloadCount: number; version: string; + ratingAvg?: number; } async function fetchSkillDetail(client: ApiClient, namespace: string, name: string): Promise { @@ -220,15 +221,21 @@ export function registerExplore(program: Command) { .description("Browse or search skills from the registry") .argument("[query]", "Search query for finding skills") .option("-n, --limit ", "Max results", "20") - .option("-s, --sort ", "Sort by: hot, newest, downloads (default: interactive mode)") - .option("--hot", "Sort by popularity (shorthand for --sort hot)") + .option("-s, --sort ", "Sort by: hot, newest, downloads, stars (default: interactive mode)") + .option("--hot", "Sort by comprehensive popularity (downloads + stars)") .option("--newest", "Sort by newest first (shorthand for --sort newest)") .option("--downloads", "Sort by download count (shorthand for --sort downloads)") - .action(async (query: string | undefined, opts: { limit: string; sort?: string; hot?: boolean; newest?: boolean; downloads?: boolean }) => { + .option("--stars", "Sort by star count (shorthand for --sort stars)") + .action(async (query: string | undefined, opts: { limit: string; sort?: string; hot?: boolean; newest?: boolean; downloads?: boolean; stars?: boolean }) => { const config = loadConfigFromProgram(program); const token = await readToken(); const client = new ApiClient({ baseUrl: config.registry, token: token || undefined }); - const sortMap: Record = { hot: "rating", newest: "newest", downloads: "downloads" }; + const sortMap: Record = { + hot: "hot", + newest: "newest", + downloads: "downloads", + stars: "stars" + }; // Resolve sort priority: explicit --sort > shorthand flags > default let effectiveSort = opts.sort; @@ -236,12 +243,13 @@ export function registerExplore(program: Command) { if (opts.hot) effectiveSort = "hot"; else if (opts.newest) effectiveSort = "newest"; else if (opts.downloads) effectiveSort = "downloads"; + else if (opts.stars) effectiveSort = "stars"; } const apiSort = sortMap[effectiveSort || "newest"] || "newest"; try { // Enter interactive mode only if no query AND no sort option (explicit or shorthand) - const hasSortOption = opts.sort || opts.hot || opts.newest || opts.downloads; + const hasSortOption = opts.sort || opts.hot || opts.newest || opts.downloads || opts.stars; if (!query && !hasSortOption) { const selected = await runInteractiveSearch(client, "", apiSort); if (!selected) { @@ -277,8 +285,9 @@ export function registerExplore(program: Command) { const nsBadge = skill.namespace !== "global" ? ` ${YELLOW}[${skill.namespace}]${RESET}` : ""; const stars = detail?.starCount ? ` ${YELLOW}⭐ ${detail.starCount}${RESET}` : ""; const downloads = detail?.downloadCount ? ` ${CYAN}↓ ${formatInstalls(detail.downloadCount)}${RESET}` : ""; + const rating = detail?.ratingAvg ? ` ${GREEN}★ ${detail.ratingAvg.toFixed(1)}${RESET}` : ""; - console.log(`${TEXT}${skill.name}${RESET}${nsBadge}${stars}${downloads}`); + console.log(`${TEXT}${skill.name}${RESET}${nsBadge}${stars}${downloads}${rating}`); console.log(`${DIM}└ skillhub install ${skill.namespace}/${skill.name}${RESET}`); if (skill.summary) { console.log(`${DIM} ${skill.summary.slice(0, 60)}${RESET}`); diff --git a/skillhub-cli/src/commands/versions.ts b/skillhub-cli/src/commands/versions.ts deleted file mode 100644 index 3c74df73..00000000 --- a/skillhub-cli/src/commands/versions.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { Command } from "commander"; -import { ApiClient } from "../core/api-client.js"; -import { readToken } from "../core/auth-token.js"; -import { loadConfig, loadConfigFromProgram } from "../core/config.js"; -import { error, info, dim, success } from "../utils/logger.js"; -import { parseSkillName } from "../core/skill-name.js"; -import { searchSkills, runInteractiveSearch } from "../core/interactive-search.js"; - -export interface SkillVersionItem { - id: number; - version: string; - status: string; - changelog: string | null; - fileCount: number; - totalSize: number; - publishedAt: string; - downloadAvailable: boolean; -} - -export interface VersionsResponse { - items: SkillVersionItem[]; - total: number; - page: number; - size: number; -} - -function formatBytes(bytes: number): string { - if (bytes < 1024) return `${bytes} B`; - if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; - return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; -} - -interface SkillDetailResponse { - id: number; - namespace: string; - slug: string; - displayName: string; - ownerDisplayName: string; - summary: string; - visibility: string; - status: string; - starCount: number; - downloadCount: number; - labels: Array<{ slug: string; name: string }>; - publishedVersion?: { version: string }; -} - -export function registerVersions(program: Command) { - program - .command("versions ") - .description("List skill versions") - .option("--detail", "Show additional skill metadata (stars, downloads, summary)") - .action(async (slug: string, opts: { detail?: boolean }) => { - try { - const { namespace, slug: skillSlug } = parseSkillName(slug); - const config = loadConfigFromProgram(program); - const token = await readToken(); - const client = new ApiClient({ baseUrl: config.registry, token: token || undefined }); - - let targetNamespace = namespace; - let targetSlug = skillSlug; - - if (namespace === "global") { - const results = await searchSkills(client, skillSlug, 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) { - error(`Skill not found: ${skillSlug}`); - process.exitCode = 1; - } - - if (uniqueResults.length === 1) { - targetNamespace = uniqueResults[0].namespace; - targetSlug = uniqueResults[0].name; - } else { - const selected = await runInteractiveSearch(client, skillSlug); - if (!selected) { - info("Cancelled."); - return; - } - const [ns, name] = selected.split("/", 2); - targetNamespace = ns; - targetSlug = name; - } - } - - const resp = await client.get( - `/api/v1/skills/${targetNamespace}/${targetSlug}/versions` - ); - const versions = resp.items || []; - if (versions.length === 0) { - console.log("No versions found."); - return; - } - - if (opts.detail) { - try { - const detail = await client.get( - `/api/v1/skills/${targetNamespace}/${targetSlug}` - ); - console.log(""); - info(`${detail.displayName} (${detail.slug})`); - dim(`Namespace: ${detail.namespace}`); - dim(`Author: ${detail.ownerDisplayName}`); - dim(`Stars: ${detail.starCount} Downloads: ${detail.downloadCount}`); - if (detail.summary) console.log(`\n${detail.summary}`); - if (detail.labels && detail.labels.length > 0) { - dim(`Labels: ${detail.labels.map((l) => l.name || l.slug).join(", ")}`); - } - console.log(""); - } catch {} - } - - if (targetNamespace !== "global" && !opts.detail) { - success(`${targetNamespace}/${targetSlug}`); - } - for (const v of versions) { - info(`v${v.version}`); - dim(` ${v.status} · ${v.fileCount} files · ${formatBytes(v.totalSize)} · ${v.publishedAt}`); - } - } catch (e: any) { - error(`Failed: ${e.message}`); - process.exitCode = 1; - } - }); -} diff --git a/skillhub-cli/src/core/interactive-search.ts b/skillhub-cli/src/core/interactive-search.ts index ed86a755..0ebf7535 100644 --- a/skillhub-cli/src/core/interactive-search.ts +++ b/skillhub-cli/src/core/interactive-search.ts @@ -83,14 +83,14 @@ export async function searchSkills( summary: s.summary, installs: s.stats?.downloads || 0, stars: s.stats?.stars || 0, - rating: 0, + rating: s.ratingAvg || 0, updatedAt: s.updatedAt || 0, }; }); if (sort === "downloads") { return skills.sort((a, b) => b.installs - a.installs); - } else if (sort === "rating") { - return skills.sort((a, b) => b.rating - a.rating || b.stars - a.stars); + } else if (sort === "stars") { + return skills.sort((a, b) => b.stars - a.stars); } else { return skills.sort((a, b) => b.updatedAt - a.updatedAt); } @@ -125,8 +125,8 @@ export async function searchSkills( if (sort === "downloads") { return skills.sort((a, b) => b.installs - a.installs); - } else if (sort === "rating") { - return skills.sort((a, b) => b.rating - a.rating || b.stars - a.stars); + } else if (sort === "stars") { + return skills.sort((a, b) => b.stars - a.stars); } else { return skills.sort((a, b) => b.updatedAt - a.updatedAt); } diff --git a/skillhub-cli/src/schema/routes.ts b/skillhub-cli/src/schema/routes.ts index d0411f71..83829ecb 100644 --- a/skillhub-cli/src/schema/routes.ts +++ b/skillhub-cli/src/schema/routes.ts @@ -59,6 +59,7 @@ export interface SkillsListResponse { downloads?: number; stars?: number; }; + ratingAvg?: number; latestVersion?: { version: string; };