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)
This commit is contained in:
chenbaowang 2026-04-22 16:01:12 +08:00
parent 3349182072
commit 2fffc2b377
4 changed files with 21 additions and 146 deletions

View file

@ -32,6 +32,7 @@ interface SkillDetail {
starCount: number;
downloadCount: number;
version: string;
ratingAvg?: number;
}
async function fetchSkillDetail(client: ApiClient, namespace: string, name: string): Promise<SkillDetail | null> {
@ -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 <n>", "Max results", "20")
.option("-s, --sort <sort>", "Sort by: hot, newest, downloads (default: interactive mode)")
.option("--hot", "Sort by popularity (shorthand for --sort hot)")
.option("-s, --sort <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<string, string> = { hot: "rating", newest: "newest", downloads: "downloads" };
const sortMap: Record<string, string> = {
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}`);

View file

@ -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 <slug>")
.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<string>();
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<VersionsResponse>(
`/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<SkillDetailResponse>(
`/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;
}
});
}

View file

@ -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);
}

View file

@ -59,6 +59,7 @@ export interface SkillsListResponse {
downloads?: number;
stars?: number;
};
ratingAvg?: number;
latestVersion?: {
version: string;
};