feat(cli): implement comprehensive sort strategy with backend and client-side sorting

- Backend sorts (direct API): newest, downloads, rating
- Client-side sorts (fetch then re-sort): hot, stars
- Add --rating option for rating-based sorting (backend supported)
- Add applyClientSort() function for client-side sorting logic
- Hot sort formula: downloads * 0.6 + stars * 0.4
- Update SearchSkill interface to include stars, rating, updatedAt fields
- Ensure all sort options work correctly with both list and search APIs
This commit is contained in:
chenbaowang 2026-04-22 16:35:06 +08:00
parent 2fffc2b377
commit 4a7ebb4efc
2 changed files with 50 additions and 28 deletions

View file

@ -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 <n>", "Max results", "20")
.option("-s, --sort <sort>", "Sort by: hot, newest, downloads, stars (default: interactive mode)")
.option("-s, --sort <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<string, string> = {
hot: "hot",
newest: "newest",
// Backend-supported sorts: newest, downloads, rating
// Client-side sorts: hot, stars (fetch with newest, then re-sort)
const backendSortMap: Record<string, string> = {
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}`);

View file

@ -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<SearchSkill[]> {
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<SkillsListResponse>(
`${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<SearchResponse>(
`${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(