mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-15 23:31:10 +00:00
feat(cli): enhance inspect/install commands and reorganize help
- Add --details option to inspect for version history and tags - Normalize version input (strip 'v' prefix) in install command - Skip version selection when --skill-version or --tag is specified - Reorganize help: merge Discover & Info sections - Move SkillVersionItem/VersionsResponse types to schema/routes.ts - Remove versions command registration from CLI (file retained for now) - Fix error handling to return after setting exit code - Update tests to reflect command changes
This commit is contained in:
parent
d4ea485662
commit
acea441625
7 changed files with 242 additions and 44 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "motovis-skillhub",
|
||||
"version": "1.2.0",
|
||||
"version": "1.2.3",
|
||||
"type": "module",
|
||||
"description": "SkillHub CLI - 企业级 Agent Skill 管理工具,支持命名空间",
|
||||
"bin": {
|
||||
|
|
|
|||
|
|
@ -56,13 +56,19 @@ function buildTopLevelHelp(version: string): string {
|
|||
]));
|
||||
sections.push("");
|
||||
|
||||
sections.push(formatSection("Discover", [
|
||||
sections.push(formatSection("Discover & Info", [
|
||||
{ cmd: "explore", desc: "Browse or search skills from the registry", alias: "find, find-skills, search" },
|
||||
{ cmd: "inspect <slug>", desc: "View skill metadata and versions", alias: "info, view" },
|
||||
{ cmd: "resolve <slug>", desc: "Resolve the latest version of a skill" },
|
||||
{ cmd: "rating <slug>", desc: "View your rating for a skill" },
|
||||
{ cmd: "rate <slug> <score>", desc: "Rate a skill (1-5)" },
|
||||
{ cmd: "star <slug>", desc: "Star a skill" },
|
||||
{ cmd: "report <slug>", desc: "Report a skill for review" },
|
||||
]));
|
||||
sections.push("");
|
||||
|
||||
sections.push(formatSection("Install & Manage", [
|
||||
{ cmd: "install <source>", desc: "Install from registry, git, or local path", alias: "i" },
|
||||
{ cmd: "install <skill-name>", desc: "Install from registry, git, or local path", alias: "i" },
|
||||
{ cmd: "download <slug>", desc: "Download a skill package to local directory" },
|
||||
{ cmd: "update [slug]", desc: "Update installed skills from their source", alias: "up" },
|
||||
{ cmd: "uninstall [name]", desc: "Uninstall a skill from local agent", alias: "un" },
|
||||
|
|
@ -77,17 +83,6 @@ function buildTopLevelHelp(version: string): string {
|
|||
{ cmd: "sync [path]", desc: "Scan and publish all skills from a directory" },
|
||||
{ cmd: "delete <slug>", desc: "Delete a skill you own", alias: "del, unpublish" },
|
||||
{ cmd: "archive <slug>", desc: "Archive a skill you own" },
|
||||
{ cmd: "versions <slug>", desc: "List skill versions" },
|
||||
]));
|
||||
sections.push("");
|
||||
|
||||
sections.push(formatSection("Info & Review", [
|
||||
{ cmd: "inspect <slug>", desc: "View skill metadata without installing", alias: "info, view" },
|
||||
{ cmd: "resolve <slug>", desc: "Resolve the latest version of a skill" },
|
||||
{ cmd: "rating <slug>", desc: "View your rating for a skill" },
|
||||
{ cmd: "rate <slug> <score>", desc: "Rate a skill (1-5)" },
|
||||
{ cmd: "star <slug>", desc: "Star a skill" },
|
||||
{ cmd: "report <slug>", desc: "Report a skill for review" },
|
||||
]));
|
||||
sections.push("");
|
||||
|
||||
|
|
@ -171,7 +166,6 @@ export async function createCli(): Promise<Command> {
|
|||
{ registerReviews },
|
||||
{ registerNotifications },
|
||||
{ registerDelete },
|
||||
{ registerVersions },
|
||||
{ registerReport },
|
||||
{ registerResolve },
|
||||
{ registerRating, registerRate },
|
||||
|
|
@ -200,7 +194,6 @@ export async function createCli(): Promise<Command> {
|
|||
import("./commands/reviews.js"),
|
||||
import("./commands/notifications.js"),
|
||||
import("./commands/delete.js"),
|
||||
import("./commands/versions.js"),
|
||||
import("./commands/report.js"),
|
||||
import("./commands/resolve.js"),
|
||||
import("./commands/rating.js"),
|
||||
|
|
@ -230,7 +223,6 @@ export async function createCli(): Promise<Command> {
|
|||
registerReviews(program);
|
||||
registerNotifications(program);
|
||||
registerDelete(program);
|
||||
registerVersions(program);
|
||||
registerReport(program);
|
||||
registerResolve(program);
|
||||
registerRating(program);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Command } from "commander";
|
||||
import { ApiClient } from "../core/api-client.js";
|
||||
import { ApiRoutes } from "../schema/routes.js";
|
||||
import { loadConfig, loadConfigFromProgram } from "../core/config.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";
|
||||
|
|
@ -28,7 +28,38 @@ interface NamespaceInfo {
|
|||
status: string;
|
||||
}
|
||||
|
||||
function printSkillDetail(detail: SkillDetailResponse) {
|
||||
interface SkillVersionItem {
|
||||
id: number;
|
||||
version: string;
|
||||
status: string;
|
||||
changelog: string | null;
|
||||
fileCount: number;
|
||||
totalSize: number;
|
||||
publishedAt: string;
|
||||
downloadAvailable: boolean;
|
||||
}
|
||||
|
||||
interface VersionsResponse {
|
||||
items: SkillVersionItem[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
interface SkillTag {
|
||||
id: number;
|
||||
tagName: string;
|
||||
versionId: number;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
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`;
|
||||
}
|
||||
|
||||
function printSkillDetail(detail: SkillDetailResponse, versions?: SkillVersionItem[], tags?: SkillTag[]) {
|
||||
console.log("");
|
||||
info(`${detail.displayName} (${detail.slug})`);
|
||||
dim(`Namespace: ${detail.namespace}`);
|
||||
|
|
@ -40,10 +71,29 @@ function printSkillDetail(detail: SkillDetailResponse) {
|
|||
if (detail.labels && detail.labels.length > 0) {
|
||||
dim(`Labels: ${detail.labels.map((l) => l.name || l.slug).join(", ")}`);
|
||||
}
|
||||
|
||||
if (versions && versions.length > 0) {
|
||||
console.log("");
|
||||
info("Versions:");
|
||||
const versionTagsMap = new Map<number, string[]>();
|
||||
if (tags) {
|
||||
for (const tag of tags) {
|
||||
if (!versionTagsMap.has(tag.versionId)) {
|
||||
versionTagsMap.set(tag.versionId, []);
|
||||
}
|
||||
versionTagsMap.get(tag.versionId)!.push(tag.tagName);
|
||||
}
|
||||
}
|
||||
for (const v of versions) {
|
||||
const tagStr = versionTagsMap.get(v.id)?.join(", ") || "";
|
||||
dim(` v${v.version} ${v.status} · ${v.fileCount} files · ${formatBytes(v.totalSize)} · ${v.publishedAt}${tagStr ? " · tags: " + tagStr : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
}
|
||||
|
||||
function printInspectHeader(detail: SkillDetailResponse) {
|
||||
function printInspectHeader(detail: SkillDetailResponse, versions?: SkillVersionItem[], tags?: SkillTag[]) {
|
||||
console.log("");
|
||||
info(`=== ${detail.displayName} ===`);
|
||||
dim(`Namespace: ${detail.namespace}`);
|
||||
|
|
@ -60,6 +110,25 @@ function printInspectHeader(detail: SkillDetailResponse) {
|
|||
console.log("");
|
||||
dim(`Labels: ${detail.labels.map((l) => l.name || l.slug).join(", ")}`);
|
||||
}
|
||||
|
||||
if (versions && versions.length > 0) {
|
||||
console.log("");
|
||||
info("Versions:");
|
||||
const versionTagsMap = new Map<number, string[]>();
|
||||
if (tags) {
|
||||
for (const tag of tags) {
|
||||
if (!versionTagsMap.has(tag.versionId)) {
|
||||
versionTagsMap.set(tag.versionId, []);
|
||||
}
|
||||
versionTagsMap.get(tag.versionId)!.push(tag.tagName);
|
||||
}
|
||||
}
|
||||
for (const v of versions) {
|
||||
const tagStr = versionTagsMap.get(v.id)?.join(", ") || "";
|
||||
dim(` v${v.version} ${v.status} · ${v.fileCount} files · ${formatBytes(v.totalSize)} · ${v.publishedAt}${tagStr ? " · tags: " + tagStr : ""}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
}
|
||||
|
||||
|
|
@ -69,7 +138,8 @@ export function registerInspect(program: Command) {
|
|||
.aliases(["info", "view"])
|
||||
.description("View skill metadata without installing")
|
||||
.option("--namespace <ns>", "Search in specific namespace (searches all if not specified)")
|
||||
.action(async (slug: string, opts: { namespace?: string }) => {
|
||||
.option("--details", "Show all versions with tags")
|
||||
.action(async (slug: string, opts: { namespace?: string; details?: boolean }) => {
|
||||
const config = loadConfigFromProgram(program);
|
||||
const token = await readToken();
|
||||
const client = new ApiClient({ baseUrl: config.registry, token: token || undefined });
|
||||
|
|
@ -78,14 +148,29 @@ export function registerInspect(program: Command) {
|
|||
const { namespace: defaultNs, slug: parsedSlug } = parseSkillName(slug, "");
|
||||
const targetNamespace = opts.namespace || defaultNs;
|
||||
|
||||
async function fetchVersionsAndTags(ns: string, skillSlug: string) {
|
||||
if (!opts.details) return { versions: undefined, tags: undefined };
|
||||
try {
|
||||
const [versionsResp, tagsResp] = await Promise.all([
|
||||
client.get<VersionsResponse>(`/api/v1/skills/${ns}/${skillSlug}/versions`),
|
||||
client.get<SkillTag[]>(`/api/v1/skills/${ns}/${skillSlug}/tags`).catch(() => [] as SkillTag[]),
|
||||
]);
|
||||
return { versions: versionsResp.items || [], tags: tagsResp || [] };
|
||||
} catch {
|
||||
return { versions: undefined, tags: undefined };
|
||||
}
|
||||
}
|
||||
|
||||
if (targetNamespace) {
|
||||
const detail = await client.get<SkillDetailResponse>(
|
||||
`${ApiRoutes.skillDetail.replace("{namespace}", targetNamespace).replace("{slug}", parsedSlug)}`
|
||||
);
|
||||
const { versions, tags } = await fetchVersionsAndTags(targetNamespace, parsedSlug);
|
||||
if (isJson) {
|
||||
console.log(JSON.stringify(detail, null, 2));
|
||||
const output = opts.versions ? { ...detail, versions, tags } : detail;
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
} else {
|
||||
printSkillDetail(detail);
|
||||
printSkillDetail(detail, versions, tags);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
|
@ -95,6 +180,7 @@ export function registerInspect(program: Command) {
|
|||
if (!namespaces || namespaces.length === 0) {
|
||||
error("No namespaces found. You may need to log in.");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const searchPromises = namespaces.map(async (ns) => {
|
||||
|
|
@ -117,19 +203,30 @@ export function registerInspect(program: Command) {
|
|||
dim(`Tried namespaces: ${namespaces.map((n) => n.slug).join(", ")}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isJson) {
|
||||
if (matches.length === 1) {
|
||||
console.log(JSON.stringify(matches[0], null, 2));
|
||||
const { versions, tags } = await fetchVersionsAndTags(matches[0].namespace, matches[0].slug);
|
||||
const output = opts.details ? { ...matches[0], versions, tags } : matches[0];
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
} else {
|
||||
console.log(JSON.stringify(matches, null, 2));
|
||||
const outputs = await Promise.all(
|
||||
matches.map(async (m) => {
|
||||
const { versions, tags } = await fetchVersionsAndTags(m.namespace, m.slug);
|
||||
return opts.details ? { ...m, versions, tags } : m;
|
||||
})
|
||||
);
|
||||
console.log(JSON.stringify(outputs, null, 2));
|
||||
}
|
||||
} else if (matches.length === 1) {
|
||||
printSkillDetail(matches[0]);
|
||||
const { versions, tags } = await fetchVersionsAndTags(matches[0].namespace, matches[0].slug);
|
||||
printSkillDetail(matches[0], versions, tags);
|
||||
} else {
|
||||
for (const detail of matches) {
|
||||
printInspectHeader(detail);
|
||||
const { versions, tags } = await fetchVersionsAndTags(detail.namespace, detail.slug);
|
||||
printInspectHeader(detail, versions, tags);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,11 +12,12 @@ import { getAllAgents, detectInstalledAgents, isUniversalForScope, getAgentTarge
|
|||
import { parseSource, getCloneUrl } from "../core/source-parser.js";
|
||||
import { addToLock } from "../core/skill-lock.js";
|
||||
import { success, error, info, dim } from "../utils/logger.js";
|
||||
import chalk from "chalk";
|
||||
import unzipper from "unzipper";
|
||||
import { multiSelect, sectionMultiSelect } from "../utils/prompts.js";
|
||||
import { searchMultiselect, cancelSymbol } from "../utils/search-multiselect.js";
|
||||
import { runInteractiveSearch, searchSkills } from "../core/interactive-search.js";
|
||||
import type { SkillVersionItem } from "./versions.js";
|
||||
import type { SkillVersionItem } from "../schema/routes.js";
|
||||
|
||||
interface SkillTag {
|
||||
id: number;
|
||||
|
|
@ -196,9 +197,56 @@ function buildAgentSummary(targetAgents: AgentInfo[], mode: "symlink" | "copy",
|
|||
return lines;
|
||||
}
|
||||
|
||||
function buildInstallHelp(cmd: Command): string {
|
||||
const lines: string[] = [];
|
||||
|
||||
lines.push(`${chalk.bold("Usage:")} skillhub install|i [options] ${chalk.cyan("<skill-name>")}`);
|
||||
lines.push("");
|
||||
lines.push("Install skills from registry, git repositories, or local paths");
|
||||
lines.push("");
|
||||
|
||||
lines.push(chalk.bold("Arguments:"));
|
||||
lines.push(` ${chalk.cyan("skill-name")} Skill name or namespace/skill-name from registry`);
|
||||
lines.push("");
|
||||
|
||||
lines.push(chalk.bold("Source Options:"));
|
||||
lines.push(` ${chalk.cyan("-a, --add <source>")} Install from GitHub or local path (alias for --from)`);
|
||||
lines.push(` ${chalk.cyan("--from <source>")} Install from GitHub or local path (alias for -a)`);
|
||||
lines.push("");
|
||||
|
||||
lines.push(chalk.bold("Target Options:"));
|
||||
lines.push(` ${chalk.cyan("--agent <agents...>")} Target specific agents`);
|
||||
lines.push(` ${chalk.cyan("-g, --global")} Install to global scope`);
|
||||
lines.push("");
|
||||
|
||||
lines.push(chalk.bold("Version Options:"));
|
||||
lines.push(` ${chalk.cyan("-v, --skill-version <ver>")} Install specific version (non-interactive)`);
|
||||
lines.push(` ${chalk.cyan("--tag <tag>")} Install specific tag (non-interactive, resolves to version)`);
|
||||
lines.push("");
|
||||
|
||||
lines.push(chalk.bold("Mode Options:"));
|
||||
lines.push(` ${chalk.cyan("--copy")} Copy instead of symlink`);
|
||||
lines.push(` ${chalk.cyan("--list")} List available skills without installing`);
|
||||
lines.push("");
|
||||
|
||||
lines.push(chalk.bold("Other Options:"));
|
||||
lines.push(` ${chalk.cyan("-y, --yes")} Skip all prompts`);
|
||||
lines.push(` ${chalk.cyan("-h, --help")} Display help for command`);
|
||||
lines.push("");
|
||||
|
||||
lines.push(chalk.bold("Examples:"));
|
||||
lines.push(chalk.dim(" skillhub install vision2group/fork-workflow Install a skill from registry"));
|
||||
lines.push(chalk.dim(" skillhub install my-skill --from ./local/path Install from local directory"));
|
||||
lines.push(chalk.dim(" skillhub install my-skill --from github.com/user/repo Install from GitHub"));
|
||||
lines.push(chalk.dim(" skillhub install my-skill -g --yes Install globally, skip prompts"));
|
||||
lines.push(chalk.dim(" skillhub install my-skill --tag v1.0.0 Install specific tag"));
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
export function registerInstall(program: Command) {
|
||||
program
|
||||
.command("install <source>")
|
||||
const installCmd = program
|
||||
.command("install <skill-name>")
|
||||
.alias("i")
|
||||
.description("Install skills from registry, git repositories, or local paths")
|
||||
.option("-a, --add <source>", "Install from GitHub or local path (alias for --from)")
|
||||
|
|
@ -210,7 +258,14 @@ export function registerInstall(program: Command) {
|
|||
.option("--list", "List available skills without installing")
|
||||
.option("-v, --skill-version <ver>", "Install specific version (non-interactive)")
|
||||
.option("--tag <tag>", "Install specific tag (non-interactive, resolves to version)")
|
||||
.action(async (source: string, opts: Record<string, string | string[] | boolean>) => {
|
||||
.configureHelp({ showGlobalOptions: true });
|
||||
|
||||
const originalHelp = installCmd.helpInformation.bind(installCmd);
|
||||
installCmd.helpInformation = () => {
|
||||
return buildInstallHelp(installCmd);
|
||||
};
|
||||
|
||||
installCmd.action(async (source: string, opts: Record<string, string | string[] | boolean>) => {
|
||||
const fromSource = (opts.from || opts.add) as string | undefined;
|
||||
|
||||
let effectiveSource: SourceType;
|
||||
|
|
@ -316,10 +371,18 @@ async function installFromRegistry(
|
|||
|
||||
// Present version selection
|
||||
let selectedVersion: string = "latest";
|
||||
if (opts.yes && opts.skillVersion) {
|
||||
selectedVersion = String(opts.skillVersion);
|
||||
} else if (opts.yes && opts.tag) {
|
||||
// Non-interactive: resolve tag to version
|
||||
if (opts.skillVersion) {
|
||||
selectedVersion = String(opts.skillVersion).replace(/^v/, "");
|
||||
const versionExists = versions.some((v) => v.version === selectedVersion);
|
||||
if (!versionExists) {
|
||||
spinner.fail(`Version not found: ${opts.skillVersion}`);
|
||||
if (versions.length > 0) {
|
||||
info(`Available versions: ${versions.map((v) => v.version).join(", ")}`);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
} else if (opts.tag) {
|
||||
for (const [vid, tags] of versionTagsMap) {
|
||||
if (tags.includes(opts.tag as string)) {
|
||||
const v = versions.find((ver) => ver.id === vid);
|
||||
|
|
@ -383,6 +446,7 @@ async function installFromRegistry(
|
|||
spinner.fail(`Skill not found: ${ns}/${actualSlug}`);
|
||||
await rm(tmpDir, { recursive: true, force: true });
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const fileStream = createWriteStream(zipPath);
|
||||
|
|
|
|||
|
|
@ -30,11 +30,27 @@ function formatBytes(bytes: number): string {
|
|||
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")
|
||||
.action(async (slug: string) => {
|
||||
.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);
|
||||
|
|
@ -86,7 +102,25 @@ export function registerVersions(program: Command) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (targetNamespace !== "global") {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -65,3 +65,21 @@ export interface SkillsListResponse {
|
|||
}>;
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { registerWhoami } from "../src/commands/whoami.js";
|
|||
import { registerLogin } from "../src/commands/login.js";
|
||||
import { registerPublish } from "../src/commands/publish.js";
|
||||
import { registerMe } from "../src/commands/me.js";
|
||||
import { registerVersions } from "../src/commands/versions.js";
|
||||
import { registerNotifications } from "../src/commands/notifications.js";
|
||||
import { registerReviews } from "../src/commands/reviews.js";
|
||||
import { registerNamespaces } from "../src/commands/namespaces.js";
|
||||
|
|
@ -70,12 +69,6 @@ describe("Command registrations", () => {
|
|||
expect(subNames).toContain("stars");
|
||||
});
|
||||
|
||||
it("registers versions command", () => {
|
||||
const program = new Command();
|
||||
registerVersions(program);
|
||||
expect(getCommandNames(program)).toContain("versions");
|
||||
});
|
||||
|
||||
it("registers notifications command with subcommands", () => {
|
||||
const program = new Command();
|
||||
registerNotifications(program);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue