From aa9fd5f7714d0b5ecde22011ccf5ec5a700064d5 Mon Sep 17 00:00:00 2001 From: chenbaowang <49091147+Rsweater@users.noreply.github.com> Date: Fri, 24 Apr 2026 10:16:16 +0800 Subject: [PATCH] feat(cli): add command aliases and unhide command --- skillhub-cli/src/cli.ts | 6 +- skillhub-cli/src/commands/config.ts | 2 +- skillhub-cli/src/commands/delete.ts | 3 +- skillhub-cli/src/commands/explore.ts | 1 + skillhub-cli/src/commands/hide.ts | 144 ++++++++------------- skillhub-cli/src/commands/inspect.ts | 1 + skillhub-cli/src/commands/list.ts | 1 + skillhub-cli/src/commands/me.ts | 5 +- skillhub-cli/src/commands/namespaces.ts | 2 +- skillhub-cli/src/commands/notifications.ts | 1 + skillhub-cli/src/commands/publish.ts | 4 +- skillhub-cli/src/commands/reviews.ts | 1 + skillhub-cli/src/commands/uninstall.ts | 1 + skillhub-cli/src/commands/update.ts | 1 + skillhub-cli/src/commands/whoami.ts | 2 +- skillhub-cli/src/core/api-client.ts | 30 +++++ 16 files changed, 108 insertions(+), 97 deletions(-) diff --git a/skillhub-cli/src/cli.ts b/skillhub-cli/src/cli.ts index 3fa89075..6ff6a7ba 100644 --- a/skillhub-cli/src/cli.ts +++ b/skillhub-cli/src/cli.ts @@ -144,7 +144,8 @@ export async function createCli(): Promise { .description("CLI for SkillHub — publish, search, and manage agent skills") .version(version) .option("--registry ", "Registry API base URL") - .option("--json", "Output results as JSON"); + .option("--json", "Output results as JSON") + .option("--debug", "Show debug information for API requests"); const customHelp = buildTopLevelHelp(version); const originalHelpInformation = program.helpInformation.bind(program); @@ -179,7 +180,7 @@ export async function createCli(): Promise { { registerInspect }, { registerExplore }, { registerTransfer }, - { registerHide }, + { registerHide, registerUnhide }, { registerConfig }, ] = await Promise.all([ import("./commands/login.js"), @@ -238,6 +239,7 @@ export async function createCli(): Promise { registerExplore(program); registerTransfer(program); registerHide(program); + registerUnhide(program); registerConfig(program); return program; diff --git a/skillhub-cli/src/commands/config.ts b/skillhub-cli/src/commands/config.ts index ef028731..ad75af81 100644 --- a/skillhub-cli/src/commands/config.ts +++ b/skillhub-cli/src/commands/config.ts @@ -2,7 +2,7 @@ import { Command } from "commander"; import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs"; import { homedir } from "node:os"; import { join } from "node:path"; -import { success, error, info } from "../utils/logger.js"; +import { success, error, info, dim } from "../utils/logger.js"; import chalk from "chalk"; const CONFIG_DIR = join(homedir(), ".skillhub"); diff --git a/skillhub-cli/src/commands/delete.ts b/skillhub-cli/src/commands/delete.ts index afe16cbd..42b6fb1e 100644 --- a/skillhub-cli/src/commands/delete.ts +++ b/skillhub-cli/src/commands/delete.ts @@ -6,6 +6,7 @@ import { success, error } from "../utils/logger.js"; export function registerDelete(program: Command) { program .command("delete") + .aliases(["del", "unpublish"]) .description("Delete a skill you own") .argument("", "Skill name or namespace/skill-name") .option("-y, --yes", "Skip confirmation") @@ -29,7 +30,7 @@ export function registerDelete(program: Command) { try { const token = await requireToken(); const config = loadConfigFromProgram(program); - const client = new ApiClient({ baseUrl: config.registry, token }); + const client = new ApiClient({ baseUrl: config.registry, token, debug: program.opts().debug }); await client.delete(`/api/v1/skills/${namespace}/${skillSlug}`); success(`Deleted ${skillSlug} from ${namespace}`); } catch (e: any) { diff --git a/skillhub-cli/src/commands/explore.ts b/skillhub-cli/src/commands/explore.ts index 898fd27c..46776ccc 100644 --- a/skillhub-cli/src/commands/explore.ts +++ b/skillhub-cli/src/commands/explore.ts @@ -255,6 +255,7 @@ function buildExploreHelp(cmd: Command): string { export function registerExplore(program: Command) { const exploreCmd = program .command("explore") + .aliases(["find", "find-skills", "search"]) .description("Browse or search skills from the registry") .argument("[query]", "Search query for finding skills") .option("-n, --limit ", "Max results", "20") diff --git a/skillhub-cli/src/commands/hide.ts b/skillhub-cli/src/commands/hide.ts index 4f7deaf8..5908d6f2 100644 --- a/skillhub-cli/src/commands/hide.ts +++ b/skillhub-cli/src/commands/hide.ts @@ -2,109 +2,79 @@ import { Command } from "commander"; import { ApiClient } from "../core/api-client.js"; import { requireToken } from "../core/auth-token.js"; import { loadConfig, loadConfigFromProgram } from "../core/config.js"; -import { success, error } from "../utils/logger.js"; +import { success, error, dim } from "../utils/logger.js"; +async function hideSkill( + program: Command, + slug: string, + opts: { yes?: boolean; namespace?: string }, + action: "hide" | "unhide" +) { + const { parseSkillNamespace } = await import("../core/skill-resolver.js"); + const { namespace, slug: skillSlug } = parseSkillNamespace(slug, opts.namespace); + if (!opts.yes) { + const { createInterface } = await import("node:readline"); + const rl = createInterface({ input: process.stdin, output: process.stdout }); + const actionText = action === "hide" ? "Hide" : "Unhide"; + const answer = await new Promise((r) => + rl.question(`${actionText} ${skillSlug} from ${namespace}? [y/N] `, r) + ); + rl.close(); + if (answer.toLowerCase() !== "y") { + console.log("Cancelled."); + return; + } + } + + try { + const token = await requireToken(); + const config = loadConfigFromProgram(program); + const client = new ApiClient({ baseUrl: config.registry, token }); + + const detail = await client.get<{ id: number }>( + `/api/v1/skills/${namespace}/${skillSlug}` + ); + + await client.post(`/api/v1/admin/skills/${detail.id}/${action}`, { + body: JSON.stringify({}), + headers: { "Content-Type": "application/json" }, + }); + + success(`${action === "hide" ? "Hidden" : "Unhidden"} ${skillSlug}`); + } catch (e: any) { + const status = e.status || e.statusCode; + if (status === 404) { + error(`Skill not found: ${namespace}/${skillSlug}`); + if (!slug.includes("/")) { + dim("Tip: Use namespace/skill-name format, e.g., vision2group/docker-build-push"); + } + } else { + error(`Failed: ${e.message}`); + } + process.exitCode = 1; + } +} export function registerHide(program: Command) { - const hideCmd = program + program .command("hide") .description("Hide a skill (admin only)") .argument("", "Skill name or namespace/skill-name") .option("-y, --yes", "Skip confirmation") .option("--namespace ", "Override namespace (default: parsed from skill or 'global')") .action(async (slug: string, opts: { yes?: boolean; namespace?: string }) => { - const { parseSkillNamespace } = await import("../core/skill-resolver.js"); - const { namespace, slug: skillSlug } = parseSkillNamespace(slug, opts.namespace); - if (!opts.yes) { - const { createInterface } = await import("node:readline"); - const rl = createInterface({ input: process.stdin, output: process.stdout }); - const answer = await new Promise((r) => - rl.question(`Hide ${skillSlug} from ${namespace}? [y/N] `, r) - ); - rl.close(); - if (answer.toLowerCase() !== "y") { - console.log("Cancelled."); - return; - } - } - - try { - const token = await requireToken(); - const config = loadConfigFromProgram(program); - const client = new ApiClient({ baseUrl: config.registry, token }); - - const detail = await client.get<{ id: number }>( - `/api/v1/skills/${namespace}/${skillSlug}` - ); - - await client.post(`/api/v1/admin/skills/${detail.id}/hide`, { - body: JSON.stringify({}), - headers: { "Content-Type": "application/json" }, - }); - - success(`Hidden ${skillSlug}`); - } catch (e: any) { - const status = e.status || e.statusCode; - if (status === 404) { - error(`Skill not found: ${namespace}/${skillSlug}`); - if (!slug.includes("/")) { - dim("Tip: Use namespace/skill-name format, e.g., vision2group/docker-build-push"); - } - } else { - error(`Failed: ${e.message}`); - } - process.exitCode = 1; - } + await hideSkill(program, slug, opts, "hide"); }); +} - hideCmd +export function registerUnhide(program: Command) { + program .command("unhide") .description("Unhide a skill (admin only)") .argument("", "Skill name or namespace/skill-name") .option("-y, --yes", "Skip confirmation") .option("--namespace ", "Override namespace (default: parsed from skill or 'global')") .action(async (slug: string, opts: { yes?: boolean; namespace?: string }) => { - const { parseSkillNamespace } = await import("../core/skill-resolver.js"); - const { namespace, slug: skillSlug } = parseSkillNamespace(slug, opts.namespace); - if (!opts.yes) { - const { createInterface } = await import("node:readline"); - const rl = createInterface({ input: process.stdin, output: process.stdout }); - const answer = await new Promise((r) => - rl.question(`Unhide ${skillSlug} from ${namespace}? [y/N] `, r) - ); - rl.close(); - if (answer.toLowerCase() !== "y") { - console.log("Cancelled."); - return; - } - } - - try { - const token = await requireToken(); - const config = loadConfigFromProgram(program); - const client = new ApiClient({ baseUrl: config.registry, token }); - - const detail = await client.get<{ id: number }>( - `/api/v1/skills/${namespace}/${skillSlug}` - ); - - await client.post(`/api/v1/admin/skills/${detail.id}/unhide`, { - body: JSON.stringify({}), - headers: { "Content-Type": "application/json" }, - }); - - success(`Unhidden ${skillSlug}`); - } catch (e: any) { - const status = e.status || e.statusCode; - if (status === 404) { - error(`Skill not found: ${namespace}/${skillSlug}`); - if (!slug.includes("/")) { - dim("Tip: Use namespace/skill-name format, e.g., vision2group/docker-build-push"); - } - } else { - error(`Failed: ${e.message}`); - } - process.exitCode = 1; - } + await hideSkill(program, slug, opts, "unhide"); }); } diff --git a/skillhub-cli/src/commands/inspect.ts b/skillhub-cli/src/commands/inspect.ts index 19968d46..74d36725 100644 --- a/skillhub-cli/src/commands/inspect.ts +++ b/skillhub-cli/src/commands/inspect.ts @@ -138,6 +138,7 @@ function printInspectHeader(detail: SkillDetailResponse, versions?: SkillVersion export function registerInspect(program: Command) { program .command("inspect") + .aliases(["info", "view"]) .description("View skill metadata without installing") .argument("", "Skill name or namespace/skill-name") .option("--namespace ", "Search in specific namespace (searches all if not specified)") diff --git a/skillhub-cli/src/commands/list.ts b/skillhub-cli/src/commands/list.ts index 6205dc0f..c9cf1fc5 100644 --- a/skillhub-cli/src/commands/list.ts +++ b/skillhub-cli/src/commands/list.ts @@ -18,6 +18,7 @@ interface ListOptions { export function registerList(program: Command) { program .command("list") + .alias("ls") .description("List installed skills") .option("-g, --global", "List global skills only") .option("-p, --project", "List project skills only") diff --git a/skillhub-cli/src/commands/me.ts b/skillhub-cli/src/commands/me.ts index a0b3a911..32d9391a 100644 --- a/skillhub-cli/src/commands/me.ts +++ b/skillhub-cli/src/commands/me.ts @@ -28,12 +28,13 @@ export function registerMe(program: Command) { me .command("skills") + .alias("ls") .description("List your published skills") .action(async () => { try { const token = await requireToken(); const config = loadConfigFromProgram(program); - const client = new ApiClient({ baseUrl: config.registry, token }); + const client = new ApiClient({ baseUrl: config.registry, token, debug: program.opts().debug }); const resp = await client.get("/api/v1/me/skills"); const skills = resp.items || []; const isJson = program.opts().json; @@ -63,7 +64,7 @@ export function registerMe(program: Command) { try { const token = await requireToken(); const config = loadConfigFromProgram(program); - const client = new ApiClient({ baseUrl: config.registry, token }); + const client = new ApiClient({ baseUrl: config.registry, token, debug: program.opts().debug }); const resp = await client.get("/api/v1/me/stars"); const skills = resp.items || []; const isJson = program.opts().json; diff --git a/skillhub-cli/src/commands/namespaces.ts b/skillhub-cli/src/commands/namespaces.ts index 6607070b..db045de9 100644 --- a/skillhub-cli/src/commands/namespaces.ts +++ b/skillhub-cli/src/commands/namespaces.ts @@ -13,7 +13,7 @@ export function registerNamespaces(program: Command) { try { const token = await requireToken(); const config = loadConfigFromProgram(program); - const client = new ApiClient({ baseUrl: config.registry, token }); + const client = new ApiClient({ baseUrl: config.registry, token, debug: program.opts().debug }); const namespaces = await client.get(ApiRoutes.meNamespaces); const isJson = program.opts().json; if (isJson) { diff --git a/skillhub-cli/src/commands/notifications.ts b/skillhub-cli/src/commands/notifications.ts index f2929c0a..0e83a6bd 100644 --- a/skillhub-cli/src/commands/notifications.ts +++ b/skillhub-cli/src/commands/notifications.ts @@ -15,6 +15,7 @@ export interface Notification { export function registerNotifications(program: Command) { const cmd = program .command("notifications") + .alias("notif") .description("Manage notifications"); cmd diff --git a/skillhub-cli/src/commands/publish.ts b/skillhub-cli/src/commands/publish.ts index 26e02aec..47828795 100644 --- a/skillhub-cli/src/commands/publish.ts +++ b/skillhub-cli/src/commands/publish.ts @@ -16,7 +16,7 @@ export function registerPublish(program: Command) { .description("Publish a skill to SkillHub registry") .option("--namespace ", "Target namespace (default: global)") .option("--slug ", "Skill slug") - .option("-v, --skill-version ", "Version (semver)") + .option("--skill-version ", "Version (semver)") .option("--name ", "Display name") .option("--changelog ", "Changelog text") .option("--tag ", "Comma-separated tags (e.g. beta,stable)", "latest") @@ -29,7 +29,7 @@ export function registerPublish(program: Command) { } const slug = opts.slug || basename(folder); - let version = opts["skill-version"] || opts.ver; + let version = opts["skill-version"] || opts.v; if (!version) { const now = new Date(); const yyyymmdd = now.getFullYear() * 10000 + (now.getMonth() + 1) * 100 + now.getDate(); diff --git a/skillhub-cli/src/commands/reviews.ts b/skillhub-cli/src/commands/reviews.ts index ca6ee1c0..745c6113 100644 --- a/skillhub-cli/src/commands/reviews.ts +++ b/skillhub-cli/src/commands/reviews.ts @@ -19,6 +19,7 @@ export function registerReviews(program: Command) { reviews .command("my") + .alias("submissions") .description("List your review submissions") .action(async () => { try { diff --git a/skillhub-cli/src/commands/uninstall.ts b/skillhub-cli/src/commands/uninstall.ts index a3e78883..9bc583b0 100644 --- a/skillhub-cli/src/commands/uninstall.ts +++ b/skillhub-cli/src/commands/uninstall.ts @@ -111,6 +111,7 @@ function findAgentsWithSkill(skillName: string, scope: "global" | "local", agent export function registerUninstall(program: Command) { program .command("uninstall [skill]") + .alias("un") .description("Uninstall a skill or all skills from local agent") .option("-g, --global", "Uninstall from global scope") .option("-a, --agent ", "Uninstall from specific agents") diff --git a/skillhub-cli/src/commands/update.ts b/skillhub-cli/src/commands/update.ts index 9f4c1c43..c37d4787 100644 --- a/skillhub-cli/src/commands/update.ts +++ b/skillhub-cli/src/commands/update.ts @@ -29,6 +29,7 @@ interface UpdateInfo { export function registerUpdate(program: Command) { program .command("update [skill]") + .alias("up") .description("Update installed skills from their source") .option("-a, --all", "Update all installed skills") .option("-g, --global", "Update global scope skills") diff --git a/skillhub-cli/src/commands/whoami.ts b/skillhub-cli/src/commands/whoami.ts index 43cf73d1..cfcb50e5 100644 --- a/skillhub-cli/src/commands/whoami.ts +++ b/skillhub-cli/src/commands/whoami.ts @@ -13,7 +13,7 @@ export function registerWhoami(program: Command) { try { const token = await requireToken(); const config = loadConfigFromProgram(program); - const client = new ApiClient({ baseUrl: config.registry, token }); + const client = new ApiClient({ baseUrl: config.registry, token, debug: program.opts().debug }); const resp = await client.get(ApiRoutes.whoami); const isJson = program.opts().json; if (isJson) { diff --git a/skillhub-cli/src/core/api-client.ts b/skillhub-cli/src/core/api-client.ts index cd5850a8..32bd2cef 100644 --- a/skillhub-cli/src/core/api-client.ts +++ b/skillhub-cli/src/core/api-client.ts @@ -3,6 +3,7 @@ import { request, FormData as UndiciFormData } from "undici"; export interface ApiClientOptions { baseUrl: string; token?: string; + debug?: boolean; } interface NativeApiResponse { @@ -37,13 +38,29 @@ export class ApiClient { return data as T; } + private logDebug(method: string, url: string, statusCode?: number, body?: unknown) { + if (!this.options.debug) return; + const token = this.options.token; + const tokenPreview = token ? `${token.substring(0, 20)}...` : "none"; + console.error(`[DEBUG] ${method} ${url}`); + console.error(`[DEBUG] Token: ${tokenPreview}`); + if (statusCode !== undefined) { + console.error(`[DEBUG] Status: ${statusCode}`); + } + if (body !== undefined) { + console.error(`[DEBUG] Body:`, JSON.stringify(body, null, 2)); + } + } + async get(path: string): Promise { const url = new URL(path, this.options.baseUrl); + this.logDebug("GET", url.toString()); const { statusCode, body } = await request(url.toString(), { method: "GET", headers: this.headers(), }); const data = await body.json(); + this.logDebug("GET", url.toString(), statusCode, data); if (statusCode >= 400) { throw new ApiError(statusCode, data); } @@ -130,6 +147,11 @@ function extractHumanMessage(body: unknown): string | null { if (typeof b.msg === "string" && b.msg.length > 0) return b.msg; if (typeof b.message === "string" && b.message.length > 0) return b.message; if (typeof b.error === "string" && b.error.length > 0) return b.error; + if (typeof b.detail === "string" && b.detail.length > 0) return b.detail; + if (typeof b.reason === "string" && b.reason.length > 0) return b.reason; + if (typeof b.description === "string" && b.description.length > 0) return b.description; + + if (typeof b.data === "string" && b.data.length > 0) return b.data; return null; } @@ -154,6 +176,14 @@ export class ApiError extends Error { detail += " - Or use: skillhub --registry "; } + // Enhanced error messages for 403 Forbidden + if (statusCode === 403) { + detail += "\n\nšŸ’” Access denied. This could mean:\n"; + detail += " - Your account doesn't have permission to access this resource\n"; + detail += " - Contact your administrator if you believe this is an error\n"; + detail += " - Run 'skillhub whoami' to verify your account"; + } + super(detail); } }