diff --git a/skillhub-cli/src/commands/explore.ts b/skillhub-cli/src/commands/explore.ts index 676a520e..a1e1cee1 100644 --- a/skillhub-cli/src/commands/explore.ts +++ b/skillhub-cli/src/commands/explore.ts @@ -221,20 +221,23 @@ 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, stars (default: interactive mode)") + .option("-s, --sort ", "Sort by: hot, newest, downloads, stars, rating (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)") .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 }) => { + .option("--rating", "Sort by average rating (shorthand for --sort rating)") + .action(async (query: string | undefined, opts: { limit: string; sort?: string; hot?: boolean; newest?: boolean; downloads?: boolean; stars?: boolean; rating?: boolean }) => { const config = loadConfigFromProgram(program); const token = await readToken(); const client = new ApiClient({ baseUrl: config.registry, token: token || undefined }); - const sortMap: Record = { - hot: "hot", - newest: "newest", + + // Backend-supported sorts: newest, downloads, rating + // Client-side sorts: hot, stars (fetch with newest, then re-sort) + const backendSortMap: Record = { + newest: "newest", downloads: "downloads", - stars: "stars" + rating: "rating" }; // Resolve sort priority: explicit --sort > shorthand flags > default @@ -244,12 +247,17 @@ export function registerExplore(program: Command) { else if (opts.newest) effectiveSort = "newest"; else if (opts.downloads) effectiveSort = "downloads"; else if (opts.stars) effectiveSort = "stars"; + else if (opts.rating) effectiveSort = "rating"; } - const apiSort = sortMap[effectiveSort || "newest"] || "newest"; + + // Determine API sort and client-side re-sort + const isClientSort = effectiveSort === "hot" || effectiveSort === "stars"; + const apiSort = isClientSort ? "newest" : (backendSortMap[effectiveSort || "newest"] || "newest"); + const clientSort = isClientSort ? effectiveSort : undefined; 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 || opts.stars; + const hasSortOption = opts.sort || opts.hot || opts.newest || opts.downloads || opts.stars || opts.rating; if (!query && !hasSortOption) { const selected = await runInteractiveSearch(client, "", apiSort); if (!selected) { @@ -261,7 +269,7 @@ export function registerExplore(program: Command) { return; } - const results = await searchSkills(client, query || "", parseInt(opts.limit, 10), apiSort); + const results = await searchSkills(client, query || "", parseInt(opts.limit, 10), apiSort, clientSort); if (results.length === 0) { console.log(`${DIM}No skills found${RESET}`); diff --git a/skillhub-cli/src/core/interactive-search.ts b/skillhub-cli/src/core/interactive-search.ts index 0ebf7535..f0544a43 100644 --- a/skillhub-cli/src/core/interactive-search.ts +++ b/skillhub-cli/src/core/interactive-search.ts @@ -22,6 +22,9 @@ export interface SearchSkill { version?: string; summary?: string; installs?: number; + stars?: number; + rating?: number; + updatedAt?: number; } interface SkillDetail { @@ -56,16 +59,39 @@ async function fetchSkillDetail(client: ApiClient, namespace: string, name: stri } } +function applyClientSort(skills: SearchSkill[], sort: string): SearchSkill[] { + switch (sort) { + case "downloads": + return skills.sort((a, b) => (b.installs || 0) - (a.installs || 0)); + case "stars": + return skills.sort((a, b) => (b.stars || 0) - (a.stars || 0)); + case "rating": + return skills.sort((a, b) => (b.rating || 0) - (a.rating || 0)); + case "hot": + return skills.sort((a, b) => { + const hotA = (a.installs || 0) * 0.6 + (a.stars || 0) * 0.4; + const hotB = (b.installs || 0) * 0.6 + (b.stars || 0) * 0.4; + return hotB - hotA; + }); + case "newest": + default: + return skills.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0)); + } +} + export async function searchSkills( client: ApiClient, query: string, limit: number = 10, - sort?: string + apiSort: string = "newest", + clientSort?: string ): Promise { + const needsClientSort = clientSort && clientSort !== apiSort; + if (!query) { const params = new URLSearchParams({ limit: limit.toString() }); - if (sort && sort !== "newest") { - params.set("sort", sort); + if (apiSort && apiSort !== "newest") { + params.set("sort", apiSort); } const result = await client.get( `${ApiRoutes.skills}?${params.toString()}` @@ -87,18 +113,12 @@ export async function searchSkills( updatedAt: s.updatedAt || 0, }; }); - if (sort === "downloads") { - return skills.sort((a, b) => b.installs - a.installs); - } else if (sort === "stars") { - return skills.sort((a, b) => b.stars - a.stars); - } else { - return skills.sort((a, b) => b.updatedAt - a.updatedAt); - } + return needsClientSort ? applyClientSort(skills, clientSort) : applyClientSort(skills, apiSort); } const params = new URLSearchParams({ q: query, limit: limit.toString() }); - if (sort) { - params.set("sort", sort); + if (apiSort && apiSort !== "newest") { + params.set("sort", apiSort); } const result = await client.get( `${ApiRoutes.search}?${params.toString()}` @@ -123,13 +143,7 @@ export async function searchSkills( }; }); - if (sort === "downloads") { - return skills.sort((a, b) => b.installs - a.installs); - } else if (sort === "stars") { - return skills.sort((a, b) => b.stars - a.stars); - } else { - return skills.sort((a, b) => b.updatedAt - a.updatedAt); - } + return needsClientSort ? applyClientSort(skills, clientSort) : applyClientSort(skills, apiSort); } export async function runInteractiveSearch(