mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-13 23:11:06 +00:00
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:
parent
3349182072
commit
2fffc2b377
4 changed files with 21 additions and 146 deletions
|
|
@ -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}`);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ export interface SkillsListResponse {
|
|||
downloads?: number;
|
||||
stars?: number;
|
||||
};
|
||||
ratingAvg?: number;
|
||||
latestVersion?: {
|
||||
version: string;
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue