mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-15 23:31:10 +00:00
feat(cli): 添加 --namespace 选项和智能 namespace 解析
- 新建 skill-resolver.ts 公共函数,提供 resolveSkillNamespace 和 parseSkillNamespace - 为 resolve, download, star, rating, rate, report, delete, archive, hide, unhide 添加 --namespace 选项 - inspect 使用 resolveSkillNamespace 替代手动解析 - install 优化 --from 为主选项,-a, --add 为别名 - download 优化帮助界面,添加 Examples 和更清晰的描述
This commit is contained in:
parent
58ee1a5220
commit
273e14a26f
11 changed files with 185 additions and 112 deletions
|
|
@ -3,16 +3,16 @@ 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 { parseSkillName } from "../core/skill-name.js";
|
||||
|
||||
export function registerArchive(program: Command) {
|
||||
program
|
||||
.command("archive")
|
||||
.description("Archive a skill you own")
|
||||
.argument("<skill>", "Skill name or namespace/skill-name")
|
||||
.option("-y, --yes", "Skip confirmation")
|
||||
.action(async (slug: string, opts: { yes?: boolean }) => {
|
||||
const { namespace, slug: skillSlug } = parseSkillName(slug);
|
||||
.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 });
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@ 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 { parseSkillName } from "../core/skill-name.js";
|
||||
|
||||
export function registerDelete(program: Command) {
|
||||
program
|
||||
.command("delete")
|
||||
.description("Delete a skill you own")
|
||||
.argument("<skill>", "Skill name or namespace/skill-name")
|
||||
.option("-y, --yes", "Skip confirmation")
|
||||
.action(async (slug: string, opts: { yes?: boolean }) => {
|
||||
const { namespace, slug: skillSlug } = parseSkillName(slug);
|
||||
.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 });
|
||||
|
|
|
|||
|
|
@ -7,19 +7,58 @@ import { ApiRoutes } from "../schema/routes.js";
|
|||
import { loadConfig, loadConfigFromProgram } from "../core/config.js";
|
||||
import { readToken } from "../core/auth-token.js";
|
||||
import { success, error } from "../utils/logger.js";
|
||||
import { parseSkillName } from "../core/skill-name.js";
|
||||
|
||||
import ora from "ora";
|
||||
|
||||
function buildDownloadHelp(cmd: Command): string {
|
||||
const lines: string[] = [];
|
||||
const BOLD = "\x1b[1m";
|
||||
const RESET = "\x1b[0m";
|
||||
const CYAN = "\x1b[36m";
|
||||
const DIM = "\x1b[38;5;102m";
|
||||
|
||||
lines.push(`${BOLD}Usage:${RESET} skillhub download [options] <skill>`);
|
||||
lines.push("");
|
||||
lines.push("Download a skill package as a .zip file");
|
||||
lines.push("");
|
||||
|
||||
lines.push(`${BOLD}Arguments:${RESET}`);
|
||||
lines.push(` ${CYAN}skill${RESET} Skill name or namespace/skill-name`);
|
||||
lines.push("");
|
||||
|
||||
lines.push(`${BOLD}Options:${RESET}`);
|
||||
lines.push(` ${CYAN}-v, --skill-version <ver>${RESET} Specific version to download`);
|
||||
lines.push(` ${CYAN}--tag <tag>${RESET} Tag to download (default: "latest")`);
|
||||
lines.push(` ${CYAN}--output <dir>${RESET} Output directory (default: current directory)`);
|
||||
lines.push(` ${CYAN}--namespace <ns>${RESET} Override namespace (default: parsed from skill or 'global')`);
|
||||
lines.push(` ${CYAN}-h, --help${RESET} Display help for command`);
|
||||
lines.push("");
|
||||
|
||||
lines.push(`${BOLD}Examples:${RESET}`);
|
||||
lines.push(`${DIM} skillhub download docker-build-push${RESET}`);
|
||||
lines.push(`${DIM} skillhub download vision2group/docker-build-push${RESET}`);
|
||||
lines.push(`${DIM} skillhub download docker-build-push --output ./skills${RESET}`);
|
||||
lines.push(`${DIM} skillhub download docker-build-push --skill-version 1.0.0${RESET}`);
|
||||
lines.push(`${DIM} skillhub download docker-build-push --tag v1.0.0${RESET}`);
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function registerDownload(program: Command) {
|
||||
program
|
||||
const downloadCmd = program
|
||||
.command("download")
|
||||
.description("Download a skill package to local directory")
|
||||
.description("Download a skill package as a .zip file")
|
||||
.argument("<skill>", "Skill name or namespace/skill-name")
|
||||
.option("-v, --skill-version <ver>", "Specific version")
|
||||
.option("--tag <tag>", "Tag to download", "latest")
|
||||
.option("--output <dir>", "Output directory")
|
||||
.action(async (slug: string, opts: Record<string, string>) => {
|
||||
const { namespace, slug: skillSlug } = parseSkillName(slug);
|
||||
.option("--namespace <ns>", "Override namespace (default: parsed from skill or 'global')");
|
||||
|
||||
downloadCmd.helpInformation = () => buildDownloadHelp(downloadCmd);
|
||||
|
||||
downloadCmd.action(async (slug: string, opts: Record<string, string>) => {
|
||||
const { parseSkillNamespace } = await import("../core/skill-resolver.js");
|
||||
const { namespace, slug: skillSlug } = parseSkillNamespace(slug, opts.namespace);
|
||||
const config = loadConfigFromProgram(program);
|
||||
const token = await readToken();
|
||||
const client = new ApiClient({ baseUrl: config.registry, token: token || undefined });
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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 { parseSkillName } from "../core/skill-name.js";
|
||||
|
||||
|
||||
export function registerHide(program: Command) {
|
||||
const hideCmd = program
|
||||
|
|
@ -11,8 +11,10 @@ export function registerHide(program: Command) {
|
|||
.description("Hide a skill (admin only)")
|
||||
.argument("<skill>", "Skill name or namespace/skill-name")
|
||||
.option("-y, --yes", "Skip confirmation")
|
||||
.action(async (slug: string, opts: { yes?: boolean }) => {
|
||||
const { namespace, slug: skillSlug } = parseSkillName(slug);
|
||||
.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 });
|
||||
|
|
@ -52,8 +54,10 @@ export function registerHide(program: Command) {
|
|||
.description("Unhide a skill (admin only)")
|
||||
.argument("<skill>", "Skill name or namespace/skill-name")
|
||||
.option("-y, --yes", "Skip confirmation")
|
||||
.action(async (slug: string, opts: { yes?: boolean }) => {
|
||||
const { namespace, slug: skillSlug } = parseSkillName(slug);
|
||||
.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 });
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { ApiClient } from "../core/api-client.js";
|
|||
import { ApiRoutes } from "../schema/routes.js";
|
||||
import { loadConfigFromProgram } from "../core/config.js";
|
||||
import { readToken } from "../core/auth-token.js";
|
||||
import { parseSkillName } from "../core/skill-name.js";
|
||||
|
||||
import { info, dim, error } from "../utils/logger.js";
|
||||
import { searchSkills } from "../core/interactive-search.js";
|
||||
import * as p from "@clack/prompts";
|
||||
|
|
@ -149,8 +149,6 @@ export function registerInspect(program: Command) {
|
|||
const client = new ApiClient({ baseUrl: config.registry, token: token || undefined });
|
||||
|
||||
const isJson = program.opts().json;
|
||||
const { namespace: defaultNs, slug: parsedSlug } = parseSkillName(slug, "");
|
||||
const targetNamespace = opts.namespace || defaultNs;
|
||||
|
||||
async function fetchVersionsAndTags(ns: string, skillSlug: string) {
|
||||
try {
|
||||
|
|
@ -169,16 +167,16 @@ export function registerInspect(program: Command) {
|
|||
const detail = await client.get<SkillDetailResponse>(
|
||||
`${ApiRoutes.skillDetail.replace("{namespace}", ns).replace("{slug}", skillSlug)}`
|
||||
);
|
||||
|
||||
|
||||
const { versions, tags } = await fetchVersionsAndTags(ns, skillSlug);
|
||||
|
||||
|
||||
if (version && versions) {
|
||||
const selectedVersion = versions.find(v => v.version === version);
|
||||
if (selectedVersion) {
|
||||
detail.publishedVersion = { version: selectedVersion.version };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isJson) {
|
||||
const output = opts.details ? { ...detail, versions, tags } : detail;
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
|
|
@ -200,22 +198,22 @@ export function registerInspect(program: Command) {
|
|||
|
||||
async function inspectWithVersionSelection(ns: string, skillSlug: string) {
|
||||
const { versions, tags } = await fetchVersionsAndTags(ns, skillSlug);
|
||||
|
||||
|
||||
if (!versions || versions.length === 0) {
|
||||
await displaySkillDetail(ns, skillSlug);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (opts.details) {
|
||||
await displaySkillDetail(ns, skillSlug);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (versions.length === 1) {
|
||||
await displaySkillDetail(ns, skillSlug);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const versionTagsMap = new Map<number, string[]>();
|
||||
if (tags) {
|
||||
for (const tag of tags) {
|
||||
|
|
@ -225,7 +223,7 @@ export function registerInspect(program: Command) {
|
|||
versionTagsMap.get(tag.versionId)!.push(tag.tagName);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const selected = await p.select({
|
||||
message: "Select version to inspect",
|
||||
options: versions.map((v) => ({
|
||||
|
|
@ -234,82 +232,22 @@ export function registerInspect(program: Command) {
|
|||
hint: versionTagsMap.get(v.id)?.join(", ") || "",
|
||||
})),
|
||||
});
|
||||
|
||||
|
||||
if (p.isCancel(selected)) {
|
||||
console.log("Cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
await displaySkillDetail(ns, skillSlug, selected as string);
|
||||
}
|
||||
|
||||
if (targetNamespace) {
|
||||
const { resolveSkillNamespace } = await import("../core/skill-resolver.js");
|
||||
const { namespace: targetNamespace, slug: parsedSlug } = await resolveSkillNamespace(client, slug, opts.namespace);
|
||||
|
||||
if (opts.skillVersion) {
|
||||
await displaySkillDetail(targetNamespace, parsedSlug, opts.skillVersion);
|
||||
} else {
|
||||
await inspectWithVersionSelection(targetNamespace, parsedSlug);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const spinner = ora(`Searching for ${parsedSlug}`).start();
|
||||
|
||||
try {
|
||||
const results = await searchSkills(client, parsedSlug, 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) {
|
||||
spinner.fail(`Skill not found: ${parsedSlug}`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (uniqueResults.length === 1) {
|
||||
spinner.stop();
|
||||
const ns = uniqueResults[0].namespace;
|
||||
const name = uniqueResults[0].name;
|
||||
if (opts.skillVersion) {
|
||||
await displaySkillDetail(ns, name, opts.skillVersion);
|
||||
} else {
|
||||
await inspectWithVersionSelection(ns, name);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
spinner.succeed(`Found ${uniqueResults.length} matches for ${parsedSlug}`);
|
||||
|
||||
const selected = await p.select({
|
||||
message: "Select skill to inspect",
|
||||
options: uniqueResults.map((r) => ({
|
||||
value: `${r.namespace}/${r.name}`,
|
||||
label: `${r.namespace}/${r.name}`,
|
||||
hint: r.summary ? r.summary.slice(0, 50) : undefined,
|
||||
})),
|
||||
});
|
||||
|
||||
if (p.isCancel(selected)) {
|
||||
console.log("Cancelled.");
|
||||
return;
|
||||
}
|
||||
|
||||
const [selectedNs, selectedName] = (selected as string).split("/", 2);
|
||||
if (opts.skillVersion) {
|
||||
await displaySkillDetail(selectedNs, selectedName, opts.skillVersion);
|
||||
} else {
|
||||
await inspectWithVersionSelection(selectedNs, selectedName);
|
||||
}
|
||||
} catch (e: any) {
|
||||
spinner.fail(e.message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -210,8 +210,8 @@ function buildInstallHelp(cmd: Command): string {
|
|||
lines.push("");
|
||||
|
||||
lines.push(chalk.bold("Source Options:"));
|
||||
lines.push(` ${chalk.cyan("-a, --add <source>")} Install from GitHub or local path`);
|
||||
lines.push(` ${chalk.cyan("--from <source>")} Alias for --add`);
|
||||
lines.push(` ${chalk.cyan("--from <source>")} Install from GitHub or local path`);
|
||||
lines.push(` ${chalk.cyan("-a, --add <source>")} Alias for --from`);
|
||||
lines.push("");
|
||||
|
||||
lines.push(chalk.bold("Target Options:"));
|
||||
|
|
@ -250,8 +250,8 @@ export function registerInstall(program: Command) {
|
|||
.alias("i")
|
||||
.description("Install skills from registry, git repositories, or local paths")
|
||||
.argument("<skill>", "Skill name or namespace/skill-name from registry")
|
||||
.option("-a, --add <source>", "Install from GitHub or local path")
|
||||
.option("--from <source>", "Alias for --add")
|
||||
.option("--from <source>", "Install from GitHub or local path")
|
||||
.option("-a, --add <source>", "Alias for --from")
|
||||
.option("--agent <agents...>", "Target specific agents")
|
||||
.option("-g, --global", "Install to global scope")
|
||||
.option("-y, --yes", "Skip all prompts")
|
||||
|
|
|
|||
|
|
@ -3,16 +3,16 @@ import { ApiClient } from "../core/api-client.js";
|
|||
import { loadConfig, loadConfigFromProgram } from "../core/config.js";
|
||||
import { requireToken } from "../core/auth-token.js";
|
||||
import { success, error, info, dim } from "../utils/logger.js";
|
||||
import { parseSkillName } from "../core/skill-name.js";
|
||||
|
||||
export function registerRating(program: Command) {
|
||||
program
|
||||
.command("rating")
|
||||
.description("View your rating for a skill")
|
||||
.argument("<skill>", "Skill name or namespace/skill-name")
|
||||
.action(async (slug: string) => {
|
||||
.option("--namespace <ns>", "Override namespace (default: parsed from skill or 'global')")
|
||||
.action(async (slug: string, opts: { namespace?: string }) => {
|
||||
try {
|
||||
const { namespace, slug: skillSlug } = parseSkillName(slug);
|
||||
const { parseSkillNamespace } = await import("../core/skill-resolver.js");
|
||||
const { namespace, slug: skillSlug } = parseSkillNamespace(slug, opts.namespace);
|
||||
const token = await requireToken();
|
||||
const config = loadConfigFromProgram(program);
|
||||
const client = new ApiClient({ baseUrl: config.registry, token });
|
||||
|
|
@ -44,7 +44,8 @@ export function registerRate(program: Command) {
|
|||
.description("Rate a skill (1-5)")
|
||||
.argument("<skill>", "Skill name or namespace/skill-name")
|
||||
.argument("<score>", "Rating score (1-5)")
|
||||
.action(async (slug: string, scoreStr: string) => {
|
||||
.option("--namespace <ns>", "Override namespace (default: parsed from skill or 'global')")
|
||||
.action(async (slug: string, scoreStr: string, opts: { namespace?: string }) => {
|
||||
const score = parseInt(scoreStr, 10);
|
||||
if (isNaN(score) || score < 1 || score > 5) {
|
||||
error("Score must be between 1 and 5");
|
||||
|
|
@ -52,7 +53,8 @@ export function registerRate(program: Command) {
|
|||
}
|
||||
|
||||
try {
|
||||
const { namespace, slug: skillSlug } = parseSkillName(slug);
|
||||
const { parseSkillNamespace } = await import("../core/skill-resolver.js");
|
||||
const { namespace, slug: skillSlug } = parseSkillNamespace(slug, opts.namespace);
|
||||
const token = await requireToken();
|
||||
const config = loadConfigFromProgram(program);
|
||||
const client = new ApiClient({ baseUrl: config.registry, token });
|
||||
|
|
|
|||
|
|
@ -4,17 +4,17 @@ 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 { parseSkillName } from "../core/skill-name.js";
|
||||
|
||||
export function registerReport(program: Command) {
|
||||
program
|
||||
.command("report")
|
||||
.description("Report a skill for review")
|
||||
.argument("<skill>", "Skill name or namespace/skill-name")
|
||||
.option("--reason <reason>", "Report reason")
|
||||
.action(async (slug: string, opts: { reason?: string }) => {
|
||||
.option("--namespace <ns>", "Override namespace (default: parsed from skill or 'global')")
|
||||
.action(async (slug: string, opts: { reason?: string; namespace?: string }) => {
|
||||
try {
|
||||
const { namespace, slug: skillSlug } = parseSkillName(slug);
|
||||
const { parseSkillNamespace } = await import("../core/skill-resolver.js");
|
||||
const { namespace, slug: skillSlug } = parseSkillNamespace(slug, opts.namespace);
|
||||
const token = await requireToken();
|
||||
const config = loadConfigFromProgram(program);
|
||||
const client = new ApiClient({ baseUrl: config.registry, token });
|
||||
|
|
|
|||
|
|
@ -49,9 +49,11 @@ export function registerResolve(program: Command) {
|
|||
.option("-v, --skill-version <ver>", "Specific version")
|
||||
.option("--tag <tag>", "Tag to resolve (default: latest, ignored if --skill-version)")
|
||||
.option("--hash <hash>", "Content hash")
|
||||
.option("--namespace <ns>", "Override namespace (default: parsed from skill or 'global')")
|
||||
.action(async (slug: string, opts: Record<string, string>) => {
|
||||
try {
|
||||
const { namespace, slug: skillSlug } = parseSkillName(slug);
|
||||
const { parseSkillNamespace } = await import("../core/skill-resolver.js");
|
||||
const { namespace, slug: skillSlug } = parseSkillNamespace(slug, opts.namespace);
|
||||
const config = loadConfigFromProgram(program);
|
||||
const token = await readToken();
|
||||
const client = new ApiClient({ baseUrl: config.registry, token: token || undefined });
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { ApiRoutes } from "../schema/routes.js";
|
|||
import { requireToken } from "../core/auth-token.js";
|
||||
import { loadConfig, loadConfigFromProgram } from "../core/config.js";
|
||||
import { success, error } from "../utils/logger.js";
|
||||
import { parseSkillName } from "../core/skill-name.js";
|
||||
|
||||
|
||||
export function registerStar(program: Command) {
|
||||
program
|
||||
|
|
@ -12,9 +12,11 @@ export function registerStar(program: Command) {
|
|||
.description("Star a skill")
|
||||
.argument("<skill>", "Skill name or namespace/skill-name")
|
||||
.option("--unstar", "Remove star")
|
||||
.action(async (slug: string, opts: { unstar: boolean }) => {
|
||||
.option("--namespace <ns>", "Override namespace (default: parsed from skill or 'global')")
|
||||
.action(async (slug: string, opts: { unstar: boolean; namespace?: string }) => {
|
||||
try {
|
||||
const { namespace, slug: skillSlug } = parseSkillName(slug);
|
||||
const { parseSkillNamespace } = await import("../core/skill-resolver.js");
|
||||
const { namespace, slug: skillSlug } = parseSkillNamespace(slug, opts.namespace);
|
||||
const token = await requireToken();
|
||||
const config = loadConfigFromProgram(program);
|
||||
const client = new ApiClient({ baseUrl: config.registry, token });
|
||||
|
|
|
|||
86
skillhub-cli/src/core/skill-resolver.ts
Normal file
86
skillhub-cli/src/core/skill-resolver.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import { ApiClient } from "./api-client.js";
|
||||
import { parseSkillName } from "./skill-name.js";
|
||||
import { searchSkills, runInteractiveSearch } from "./interactive-search.js";
|
||||
|
||||
export interface ResolvedSkill {
|
||||
namespace: string;
|
||||
slug: string;
|
||||
userSpecified: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 skill 的 namespace,支持智能搜索
|
||||
*
|
||||
* @param client - API 客户端
|
||||
* @param slug - 输入的 skill 名称(可能包含 namespace)
|
||||
* @param explicitNamespace - 通过 --namespace 选项显式指定的 namespace
|
||||
* @returns 解析后的 namespace 和 slug
|
||||
*/
|
||||
export async function resolveSkillNamespace(
|
||||
client: ApiClient,
|
||||
slug: string,
|
||||
explicitNamespace?: string
|
||||
): Promise<ResolvedSkill> {
|
||||
const { namespace: parsedNs, slug: actualSlug } = parseSkillName(slug);
|
||||
|
||||
if (explicitNamespace) {
|
||||
return { namespace: explicitNamespace, slug: actualSlug, userSpecified: true };
|
||||
}
|
||||
|
||||
if (parsedNs !== "global") {
|
||||
return { namespace: parsedNs, slug: actualSlug, userSpecified: true };
|
||||
}
|
||||
|
||||
const results = await searchSkills(client, actualSlug, 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) {
|
||||
throw new Error(`Skill not found: ${actualSlug}`);
|
||||
}
|
||||
|
||||
if (uniqueResults.length === 1) {
|
||||
return {
|
||||
namespace: uniqueResults[0].namespace,
|
||||
slug: uniqueResults[0].name,
|
||||
userSpecified: false,
|
||||
};
|
||||
}
|
||||
|
||||
const selected = await runInteractiveSearch(client, actualSlug);
|
||||
if (!selected) {
|
||||
throw new Error("Cancelled");
|
||||
}
|
||||
|
||||
const [ns, name] = selected.split("/", 2);
|
||||
return { namespace: ns, slug: name, userSpecified: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单解析 skill namespace,不触发智能搜索
|
||||
* 用于不需要搜索的命令(delete, star 等)
|
||||
*/
|
||||
export function parseSkillNamespace(
|
||||
slug: string,
|
||||
explicitNamespace?: string
|
||||
): ResolvedSkill {
|
||||
const { namespace: parsedNs, slug: actualSlug } = parseSkillName(slug);
|
||||
|
||||
if (explicitNamespace) {
|
||||
return { namespace: explicitNamespace, slug: actualSlug, userSpecified: true };
|
||||
}
|
||||
|
||||
return {
|
||||
namespace: parsedNs,
|
||||
slug: actualSlug,
|
||||
userSpecified: parsedNs !== "global",
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue