mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-15 23:31:10 +00:00
feat(cli): add command aliases and unhide command
This commit is contained in:
parent
803540b157
commit
aa9fd5f771
16 changed files with 108 additions and 97 deletions
|
|
@ -144,7 +144,8 @@ export async function createCli(): Promise<Command> {
|
|||
.description("CLI for SkillHub — publish, search, and manage agent skills")
|
||||
.version(version)
|
||||
.option("--registry <url>", "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<Command> {
|
|||
{ registerInspect },
|
||||
{ registerExplore },
|
||||
{ registerTransfer },
|
||||
{ registerHide },
|
||||
{ registerHide, registerUnhide },
|
||||
{ registerConfig },
|
||||
] = await Promise.all([
|
||||
import("./commands/login.js"),
|
||||
|
|
@ -238,6 +239,7 @@ export async function createCli(): Promise<Command> {
|
|||
registerExplore(program);
|
||||
registerTransfer(program);
|
||||
registerHide(program);
|
||||
registerUnhide(program);
|
||||
registerConfig(program);
|
||||
|
||||
return program;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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>", "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) {
|
||||
|
|
|
|||
|
|
@ -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 <n>", "Max results", "20")
|
||||
|
|
|
|||
|
|
@ -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<string>((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>", "Skill name or namespace/skill-name")
|
||||
.option("-y, --yes", "Skip confirmation")
|
||||
.option("--namespace <ns>", "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<string>((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>", "Skill name or namespace/skill-name")
|
||||
.option("-y, --yes", "Skip confirmation")
|
||||
.option("--namespace <ns>", "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<string>((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");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>", "Skill name or namespace/skill-name")
|
||||
.option("--namespace <ns>", "Search in specific namespace (searches all if not specified)")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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<MeSkillsResponse>("/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<MeSkillsResponse>("/api/v1/me/stars");
|
||||
const skills = resp.items || [];
|
||||
const isJson = program.opts().json;
|
||||
|
|
|
|||
|
|
@ -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<NamespaceResponse[]>(ApiRoutes.meNamespaces);
|
||||
const isJson = program.opts().json;
|
||||
if (isJson) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface Notification {
|
|||
export function registerNotifications(program: Command) {
|
||||
const cmd = program
|
||||
.command("notifications")
|
||||
.alias("notif")
|
||||
.description("Manage notifications");
|
||||
|
||||
cmd
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export function registerPublish(program: Command) {
|
|||
.description("Publish a skill to SkillHub registry")
|
||||
.option("--namespace <ns>", "Target namespace (default: global)")
|
||||
.option("--slug <slug>", "Skill slug")
|
||||
.option("-v, --skill-version <ver>", "Version (semver)")
|
||||
.option("--skill-version <ver>", "Version (semver)")
|
||||
.option("--name <name>", "Display name")
|
||||
.option("--changelog <text>", "Changelog text")
|
||||
.option("--tag <tags>", "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();
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export function registerReviews(program: Command) {
|
|||
|
||||
reviews
|
||||
.command("my")
|
||||
.alias("submissions")
|
||||
.description("List your review submissions")
|
||||
.action(async () => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -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 <agents...>", "Uninstall from specific agents")
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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<WhoamiResponse>(ApiRoutes.whoami);
|
||||
const isJson = program.opts().json;
|
||||
if (isJson) {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { request, FormData as UndiciFormData } from "undici";
|
|||
export interface ApiClientOptions {
|
||||
baseUrl: string;
|
||||
token?: string;
|
||||
debug?: boolean;
|
||||
}
|
||||
|
||||
interface NativeApiResponse<T> {
|
||||
|
|
@ -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<T>(path: string): Promise<T> {
|
||||
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 <url> <command>";
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue