From c86d99f2270c769d626aa45218084f77bf940d93 Mon Sep 17 00:00:00 2001 From: chenbaowang <49091147+Rsweater@users.noreply.github.com> Date: Tue, 14 Apr 2026 14:12:37 +0800 Subject: [PATCH] feat(explore): add --sort parameter for hot/newest/downloads ordering - Use /api/v1/skills (compat API) when query is empty to get full stats - Map sort options: hot=rating, newest=newest, downloads=downloads - Remove vim-style k/j navigation to fix 'k' key input issue --- skillhub-cli/package.json | 2 +- skillhub-cli/src/commands/explore.ts | 24 ++++--- skillhub-cli/src/core/interactive-search.ts | 71 ++++++++++++++++++--- skillhub-cli/src/schema/routes.ts | 23 ++++++- 4 files changed, 98 insertions(+), 22 deletions(-) diff --git a/skillhub-cli/package.json b/skillhub-cli/package.json index 2d46e762..8d7e55d2 100644 --- a/skillhub-cli/package.json +++ b/skillhub-cli/package.json @@ -1,5 +1,5 @@ { - "name": "@motovis/skillhub", + "name": "@iflytek/skillhub", "version": "1.0.0", "type": "module", "description": "SkillHub CLI - 企业级 Agent Skill 管理工具,支持命名空间", diff --git a/skillhub-cli/src/commands/explore.ts b/skillhub-cli/src/commands/explore.ts index 9944e720..dba7576c 100644 --- a/skillhub-cli/src/commands/explore.ts +++ b/skillhub-cli/src/commands/explore.ts @@ -47,7 +47,8 @@ async function fetchSkillDetail(client: ApiClient, namespace: string, name: stri async function runInteractiveSearch( client: ApiClient, - initialQuery: string = "" + initialQuery: string = "", + sort: string = "newest" ): Promise { const MAX_VISIBLE = 8; let query = initialQuery; @@ -136,7 +137,7 @@ async function runInteractiveSearch( debounceTimer = setTimeout(async () => { try { - results = await searchSkills(client, q); + results = await searchSkills(client, q, 10, sort); selectedIndex = 0; } catch { results = []; @@ -179,13 +180,13 @@ async function runInteractiveSearch( return; } - if (key.name === "up" || key.name === "k") { + if (key.name === "up") { selectedIndex = Math.max(0, selectedIndex - 1); render(); return; } - if (key.name === "down" || key.name === "j") { + if (key.name === "down") { selectedIndex = Math.min(Math.max(0, results.length - 1), selectedIndex + 1); render(); return; @@ -219,14 +220,17 @@ 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") - .action(async (query: string | undefined, opts: { limit: string }) => { + .option("-s, --sort ", "Sort by: hot, newest, downloads (default: interactive mode)") + .action(async (query: string | undefined, opts: { limit: string; sort?: string }) => { const config = loadConfig(); const token = await readToken(); const client = new ApiClient({ baseUrl: config.registry, token: token || undefined }); + const sortMap: Record = { hot: "rating", newest: "newest", downloads: "downloads" }; + const apiSort = sortMap[opts.sort || "newest"] || "newest"; try { - if (!query) { - const selected = await runInteractiveSearch(client, ""); + if (!query && !opts.sort) { + const selected = await runInteractiveSearch(client, "", apiSort); if (!selected) { console.log("\nCancelled."); return; @@ -236,14 +240,14 @@ export function registerExplore(program: Command) { return; } - const results = await searchSkills(client, query, parseInt(opts.limit, 10)); + const results = await searchSkills(client, query || "", parseInt(opts.limit, 10), apiSort); if (results.length === 0) { - console.log(`${DIM}No skills found for "${query}"${RESET}`); + console.log(`${DIM}No skills found${RESET}`); return; } - const maxResults = Math.min(results.length, 6); + const maxResults = Math.min(results.length, parseInt(opts.limit, 10)); const detailPromises = results.slice(0, maxResults).map((s) => fetchSkillDetail(client, s.namespace, s.name) diff --git a/skillhub-cli/src/core/interactive-search.ts b/skillhub-cli/src/core/interactive-search.ts index c36d929e..ed86a755 100644 --- a/skillhub-cli/src/core/interactive-search.ts +++ b/skillhub-cli/src/core/interactive-search.ts @@ -1,5 +1,5 @@ import { ApiClient } from "./api-client.js"; -import { ApiRoutes, SearchResponse } from "../schema/routes.js"; +import { ApiRoutes, SearchResponse, SkillsListResponse } from "../schema/routes.js"; import * as readline from "readline"; import { dim, info } from "../utils/logger.js"; @@ -59,17 +59,56 @@ async function fetchSkillDetail(client: ApiClient, namespace: string, name: stri export async function searchSkills( client: ApiClient, query: string, - limit: number = 10 + limit: number = 10, + sort?: string ): Promise { + if (!query) { + const params = new URLSearchParams({ limit: limit.toString() }); + if (sort && sort !== "newest") { + params.set("sort", sort); + } + const result = await client.get( + `${ApiRoutes.skills}?${params.toString()}` + ); + if (!result.items || result.items.length === 0) { + return []; + } + const skills = result.items.map((s) => { + const { namespace, name } = parseNamespace(s.slug); + return { + name, + slug: s.slug, + namespace, + version: s.latestVersion?.version || "", + summary: s.summary, + installs: s.stats?.downloads || 0, + stars: s.stats?.stars || 0, + rating: 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 { + return skills.sort((a, b) => b.updatedAt - a.updatedAt); + } + } + + const params = new URLSearchParams({ q: query, limit: limit.toString() }); + if (sort) { + params.set("sort", sort); + } const result = await client.get( - `${ApiRoutes.search}?q=${encodeURIComponent(query)}&limit=${limit}` + `${ApiRoutes.search}?${params.toString()}` ); if (!result.results || result.results.length === 0) { return []; } - return result.results.map((s) => { + const skills = result.results.map((s) => { const { namespace, name } = parseNamespace(s.slug); return { name, @@ -77,14 +116,26 @@ export async function searchSkills( namespace, version: s.version, summary: s.summary, - installs: (s as any).installCount || 0, + installs: s.downloadCount || 0, + stars: s.starCount || 0, + rating: s.ratingAvg || 0, + updatedAt: s.updatedAt ? new Date(s.updatedAt).getTime() : 0, }; - }).sort((a, b) => (b.installs || 0) - (a.installs || 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 { + return skills.sort((a, b) => b.updatedAt - a.updatedAt); + } } export async function runInteractiveSearch( client: ApiClient, - initialQuery: string = "" + initialQuery: string = "", + sort: string = "newest" ): Promise { const MAX_VISIBLE = 8; let query = initialQuery; @@ -172,7 +223,7 @@ export async function runInteractiveSearch( debounceTimer = setTimeout(async () => { try { - results = await searchSkills(client, q); + results = await searchSkills(client, q, 10, sort); selectedIndex = 0; } catch { results = []; @@ -215,13 +266,13 @@ export async function runInteractiveSearch( return; } - if (key.name === "up" || key.name === "k") { + if (key.name === "up") { selectedIndex = Math.max(0, selectedIndex - 1); render(); return; } - if (key.name === "down" || key.name === "j") { + if (key.name === "down") { selectedIndex = Math.min(Math.max(0, results.length - 1), selectedIndex + 1); render(); return; diff --git a/skillhub-cli/src/schema/routes.ts b/skillhub-cli/src/schema/routes.ts index 1bbdeaec..662242f7 100644 --- a/skillhub-cli/src/schema/routes.ts +++ b/skillhub-cli/src/schema/routes.ts @@ -41,6 +41,27 @@ export interface SearchResponse { displayName: string; summary: string; version: string; - namespace?: string; // Namespace where the skill is published + namespace?: string; + downloadCount?: number; + starCount?: number; + ratingAvg?: number; + updatedAt?: string; }>; } + +export interface SkillsListResponse { + items: Array<{ + slug: string; + displayName: string; + summary: string; + updatedAt: number; + stats: { + downloads?: number; + stars?: number; + }; + latestVersion?: { + version: string; + }; + }>; + nextCursor: string | null; +}