SPB Git

Repository Public MIT

index.ts 46.8 KB 846 lines typescript

1import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";2import { dirname, isAbsolute, join, resolve } from "node:path";3import {4  ARTIFACT_TYPES, CLIENTS, MANIFEST_FILE, TOKEN_RE, ArchiveError, parseName, readArchive, sha256Hex, skillDestination, validatePackage,5  type ArtifactType, type Client, type Finding, type InstallInstructions, type ValidationResult,6} from "@skills/registry-core";7import { collectDirectory, extractTo, packFiles } from "@skills/registry-core/node";8import { DEFAULT_REGISTRY, RegistryClient, RegistryError, type Me, type TokenInfo } from "@skills/sdk";9import { resolveCredential, writableStore, type Resolved } from "./credentials";10import { defaultIO, type IO } from "./io";1112export type { IO } from "./io";13export const CLI_VERSION = "0.1.0";14export const LOCK_FILE = ".skills-registry.json";1516const EXIT = { ok: 0, error: 1, validation: 2, auth: 3 } as const;1718class CliError extends Error {19  constructor(message: string, public exit: number = EXIT.error) {20    super(message);21  }22}2324const BOOL_FLAGS = new Set(["json", "help", "version", "yes", "force", "project", "dry-run", "token-stdin", "insecure-file", "store"]);25const ALIASES: Record<string, string> = { o: "output", h: "help", y: "yes", V: "version" };2627interface Args {28  pos: string[];29  flags: Record<string, string | true>;30}31function parseArgs(argv: string[]): Args {32  const pos: string[] = [];33  const flags: Args["flags"] = {};34  for (let i = 0; i < argv.length; i++) {35    const a = argv[i]!;36    if (a === "--") {37      pos.push(...argv.slice(i + 1));38      break;39    }40    let key: string | null = null;41    let inline: string | undefined;42    if (a.startsWith("--")) {43      const eq = a.indexOf("=");44      key = eq === -1 ? a.slice(2) : a.slice(2, eq);45      if (eq !== -1) inline = a.slice(eq + 1);46    } else if (/^-[A-Za-z]$/.test(a)) key = ALIASES[a[1]!] ?? a[1]!;47    if (key === null) {48      pos.push(a);49      continue;50    }51    if (BOOL_FLAGS.has(key)) flags[key] = true;52    else if (inline !== undefined) flags[key] = inline;53    else {54      const v = argv[++i];55      if (v === undefined) throw new CliError(`option --${key} needs a value`);56      flags[key] = v;57    }58  }59  return { pos, flags };60}6162const HELP = `skills ${CLI_VERSION} — client for the Skills open agent capability registry6364Usage: skills <command> [options]6566Account67  login [--token-stdin] [--insecure-file]     Store an API token (hidden prompt or stdin); verified with the registry68  logout                                      Remove the stored token69  whoami                                      Show the authenticated identity70  auth status                                 Credential source, scopes, namespaces, expiry71  auth token create --name N --scopes a,b --namespace ns[,ns] [--expires-in 90d|12h] [--ip CIDR[,CIDR]] [--store]72  auth token list            (alias: token list)73  auth token revoke <id|prefix> [--yes]74  auth token rotate [<id|prefix>] [--yes]     Default: the current credential (new token stored, never printed)7576Authoring and publishing77  init [dir] --type <type> --name @ns/slug [--description TEXT]78  validate <dir>                              Static checks; exit 2 on blocking findings79  pack <dir> [-o file]                        Deterministic .tar.gz with SHA-25680  publish <dir> [--visibility public|unlisted|private] [--dry-run] [--yes] [--notes TEXT | --notes-file F]81  deprecate @ns/slug --reason TEXT [--yes]82  revoke @ns/slug@version --reason TEXT [--yes]8384Discovery and installation85  search "query" [--type t] [--client c]86  inspect @ns/slug [--version v] [--file path]87  versions @ns/slug88  download @ns/slug[@v] [--version v] [-o file] [--extract dir]89  install @ns/slug[@v] --client <client> [--project] [--dir path] [--yes] [--force]90  update @ns/slug --client <client> [--project] [--dir path] [--yes]9192Global options93  --registry URL   Registry base URL (env SKILLS_REGISTRY; default ${DEFAULT_REGISTRY})94  --json           Machine-readable output95  --help, --version9697Clients: ${CLIENTS.join(", ")}98Types:   ${ARTIFACT_TYPES.join(", ")}99Credentials: SKILLS_TOKEN env (process-scoped) → OS credential store → opt-in file (--insecure-file).100Exit codes: 0 ok · 1 error · 2 validation failed · 3 authentication or authorization error101Installing only copies files. The CLI never executes anything from a package.102`;103104interface Ctx {105  io: IO;106  args: Args;107  registry: string;108  account: string;109  json: boolean;110  out: (s?: string) => void;111  err: (s?: string) => void;112}113114const flag = (c: Ctx, k: string): string | undefined => (typeof c.args.flags[k] === "string" ? (c.args.flags[k] as string) : undefined);115const has = (c: Ctx, k: string): boolean => c.args.flags[k] === true || typeof c.args.flags[k] === "string";116117function credential(c: Ctx): Resolved | null {118  return resolveCredential(c.io, c.account);119}120function client(c: Ctx, token?: string | null): RegistryClient {121  return new RegistryClient({ baseUrl: c.registry, token: token ?? undefined, fetch: c.io.fetch, userAgent: `skills-cli/${CLI_VERSION}` });122}123function authed(c: Ctx): { api: RegistryClient; cred: Resolved } {124  const cred = credential(c);125  if (!cred) throw new CliError("Not logged in. Run `skills login` (or set SKILLS_TOKEN for a single process).", EXIT.auth);126  return { api: client(c, cred.token), cred };127}128/** Read commands work anonymously, and with the stored token when there is one (to see private artifacts). */129function reader(c: Ctx): RegistryClient {130  return client(c, credential(c)?.token);131}132133async function confirm(c: Ctx, question: string): Promise<void> {134  if (has(c, "yes")) return;135  if (!c.io.isTTY) throw new CliError(`Confirmation required: ${question} Re-run with --yes to confirm non-interactively.`);136  const a = (await c.io.prompt(`${question} [y/N] `)).trim().toLowerCase();137  if (a !== "y" && a !== "yes") throw new CliError("Cancelled.");138}139140function parseRef(ref: string | undefined, versionFlag?: string): { name: string; namespace: string; slug: string; version?: string } {141  if (!ref) throw new CliError("Missing artifact name (expected @namespace/slug).");142  const at = ref.lastIndexOf("@");143  const namePart = at > 0 ? ref.slice(0, at) : ref;144  const version = at > 0 ? ref.slice(at + 1) : versionFlag;145  const id = parseName(namePart.startsWith("@") ? namePart : `@${namePart}`);146  if (!id) throw new CliError(`"${ref}" is not a valid artifact reference (expected @namespace/slug or @namespace/slug@version).`);147  return { name: `@${id.namespace}/${id.slug}`, ...id, version: version || undefined };148}149150function parseDurationHours(s: string): number {151  const m = /^(\d+(?:\.\d+)?)(m|h|d)$/.exec(s);152  if (!m) throw new CliError(`Invalid duration "${s}" (use e.g. 30m, 12h, 90d).`);153  return Number(m[1]) * (m[2] === "m" ? 1 / 60 : m[2] === "h" ? 1 : 24);154}155156const fmtBytes = (n: number) => (n < 1024 ? `${n} B` : n < 1024 * 1024 ? `${(n / 1024).toFixed(1)} kB` : `${(n / 1024 / 1024).toFixed(2)} MB`);157const fmtDate = (d: string | null | undefined) => (d ? new Date(d).toISOString().slice(0, 10) : "—");158159function printFindings(c: Ctx, findings: Finding[]) {160  const blocking = findings.filter((f) => f.blocking);161  const advisory = findings.filter((f) => !f.blocking);162  const line = (f: Finding) => `    [${f.severity}] ${f.code}${f.path ? ` — ${f.path}${f.line ? `:${f.line}` : ""}` : ""}: ${f.message}`;163  if (blocking.length) {164    c.out(`  Blocking findings (${blocking.length}):`);165    blocking.forEach((f) => c.out(line(f)));166  }167  if (advisory.length) {168    c.out(`  Advisory findings (${advisory.length}):`);169    advisory.forEach((f) => c.out(line(f)));170  }171  if (!findings.length) c.out("  No findings from static checks (advisory; not a guarantee of safety).");172}173174function loadPackage(c: Ctx, dirArg: string | undefined) {175  if (!dirArg) throw new CliError("Missing package directory.");176  const dir = resolve(c.io.cwd, dirArg);177  let collected;178  try {179    collected = collectDirectory(dir);180  } catch (e) {181    if (e instanceof ArchiveError) throw new CliError(`${e.message} [${e.code}]`, EXIT.validation);182    throw e;183  }184  return { dir, ...collected, result: validatePackage(collected.files) };185}186187function printValidation(c: Ctx, r: ValidationResult, skipped: { path: string; reason: string }[]) {188  c.out(`  Files (${r.files.length}, ${fmtBytes(r.totalBytes)} unpacked):`);189  for (const f of r.files) c.out(`    ${f.mode & 0o111 ? "x" : " "} ${fmtBytes(f.size).padStart(9)}  ${f.path}`);190  if (skipped.length) {191    c.out(`  Not included (${skipped.length}):`);192    for (const s of skipped) c.out(`      ${s.path} — ${s.reason}`);193  }194  const examples = r.manifest?.examples ?? [];195  if (examples.length) {196    c.out(`  Examples (${examples.length}, screenshots shown on the artifact page):`);197    for (const e of examples) c.out(`      [${e.client}] ${e.title} — ${e.image}`);198  }199  printFindings(c, r.findings);200}201202function describeApiError(e: RegistryError): { lines: string[]; exit: number } {203  const lines = [`error: ${e.message} [${e.code}]${e.requestId ? ` (request id ${e.requestId})` : ""}`];204  let exit: number = EXIT.error;205  if (e.status === 401) {206    exit = EXIT.auth;207    if (e.code === "token_expired") lines.push("hint: this token has expired. Create a new one (`skills auth token create` from a valid credential, the web dashboard, or the server console) and run `skills login`.");208    else if (e.code === "token_revoked") lines.push("hint: this token was revoked. Run `skills login` with a new token.");209    else lines.push("hint: run `skills login` to store a valid token, or set SKILLS_TOKEN for this process.");210  } else if (e.status === 403) {211    exit = EXIT.auth;212    const d = e.details as { required?: string; granted?: string[] } | undefined;213    if (e.code === "insufficient_scope") lines.push(`hint: this token lacks the "${d?.required ?? "required"}" scope${d?.granted ? ` (granted: ${d.granted.join(", ")})` : ""}. Create a token that includes it.`);214    else if (e.code === "namespace_forbidden") lines.push("hint: this token is not allowed to write to that namespace. Check `skills auth status` for the namespaces it covers.");215    else if (e.code === "reauth_required") lines.push("hint: this action needs a recent interactive sign-in; use the web dashboard.");216  } else if (e.status === 422 && (e.code === "release_rejected" || e.code === "validation_failed")) exit = EXIT.validation;217  else if (e.status === 429) lines.push("hint: rate limited; wait and retry.");218  return { lines, exit };219}220221/* ───────────────────────── account ───────────────────────── */222223function printIdentity(c: Ctx, me: Me, source?: string) {224  c.out(`User:       ${me.user.displayName} <${me.user.email}> (${me.user.platformRole})`);225  const k = me.credential;226  if (k.kind === "token") {227    c.out(`Token:      ${k.name} (prefix ${k.prefix})${k.elevated ? " — ELEVATED" : ""}`);228    c.out(`Scopes:     ${(k.scopes ?? []).join(", ")}`);229    c.out(`Namespaces: ${(k.namespaces ?? []).join(", ") || "—"}`);230    const days = k.expiresAt ? Math.floor((new Date(k.expiresAt).getTime() - Date.now()) / 86400_000) : null;231    c.out(`Expires:    ${fmtDate(k.expiresAt)}${days !== null ? ` (${days} day${days === 1 ? "" : "s"} left)` : ""}`);232    if (days !== null && days < 14) c.err("warning: this token expires in under 14 days. Rotate it with `skills auth token rotate`.");233  }234  if (source) c.out(`Stored in:  ${source}`);235  c.out(`Registry:   ${c.registry}`);236}237238async function cmdLogin(c: Ctx) {239  const raw = has(c, "token-stdin") || !c.io.isTTY ? await c.io.stdin() : await c.io.prompt("API token (input hidden): ", { hidden: true });240  const token = raw.trim();241  if (!TOKEN_RE.test(token)) throw new CliError("That does not look like a Skills registry token (expected skr_…). Nothing was stored.", EXIT.auth);242  const me = await client(c, token).me();243  const store = writableStore(c.io, has(c, "insecure-file"));244  store.set(c.account, token);245  if (c.json) return c.out(JSON.stringify({ ok: true, user: me.user, credential: me.credential, store: store.kind }, null, 2));246  c.out("Logged in. The token was verified and stored; it will not be displayed.");247  printIdentity(c, me, store.describe);248  if (store.kind === "file") c.err("warning: the token is stored in a plaintext file (mode 600). Prefer an OS credential manager.");249}250251async function cmdLogout(c: Ctx) {252  let removed = false;253  for (const insecure of [false, true]) {254    try {255      removed = writableStore(c.io, insecure).delete(c.account) || removed;256    } catch { /* no OS store on this platform */ }257  }258  c.out(removed ? `Removed the stored token for ${c.account}. The token itself stays valid until it is revoked or expires.` : "No stored token.");259  if (c.io.env.SKILLS_TOKEN) c.err("note: SKILLS_TOKEN is still set in this environment.");260}261262async function cmdWhoami(c: Ctx, status = false) {263  const { api, cred } = authed(c);264  const me = await api.me();265  if (c.json) return c.out(JSON.stringify({ ...me, source: cred.source, registry: c.registry }, null, 2));266  if (status) c.out(`Credential: ${cred.describe}`);267  printIdentity(c, me);268}269270function tokenTable(c: Ctx, items: TokenInfo[]) {271  for (const t of items) {272    c.out(`${t.prefix}  ${t.status.padEnd(7)}  ${t.name}${t.elevated ? " [elevated]" : ""}`);273    c.out(`            id ${t.id} · expires ${fmtDate(t.expiresAt)} · last used ${fmtDate(t.lastUsedAt)}`);274    c.out(`            scopes: ${t.scopes.join(", ")} · namespaces: ${t.namespaces.join(", ") || "—"}`);275  }276  if (!items.length) c.out("No tokens.");277}278279async function resolveTokenId(api: RegistryClient, idOrPrefix: string): Promise<{ id: string; label: string }> {280  if (/^[0-9a-f-]{36}$/.test(idOrPrefix)) return { id: idOrPrefix, label: idOrPrefix };281  const { items } = await api.tokens();282  const hit = items.filter((t) => t.prefix === idOrPrefix || t.id.startsWith(idOrPrefix));283  if (hit.length !== 1) throw new CliError(hit.length ? `"${idOrPrefix}" matches several tokens; use the full id.` : `No token with id or prefix "${idOrPrefix}".`);284  return { id: hit[0]!.id, label: `${hit[0]!.name} (${hit[0]!.prefix})` };285}286287async function cmdToken(c: Ctx, sub: string | undefined, rest: string[]) {288  const { api, cred } = authed(c);289  if (sub === "list") {290    const { items } = await api.tokens();291    return c.json ? c.out(JSON.stringify(items, null, 2)) : tokenTable(c, items);292  }293  if (sub === "create") {294    const name = flag(c, "name");295    const scopes = flag(c, "scopes");296    if (!name || !scopes) throw new CliError("Usage: skills auth token create --name N --scopes a,b --namespace ns[,ns] [--expires-in 90d] [--ip CIDR] [--store]");297    const created = await api.createToken({298      name, scopes: scopes.split(",").map((s) => s.trim()).filter(Boolean), namespaces: flag(c, "namespace")?.split(",").map((s) => s.trim()).filter(Boolean) ?? [],299      expiresInHours: flag(c, "expires-in") ? parseDurationHours(flag(c, "expires-in")!) : undefined, ipAllowlist: flag(c, "ip")?.split(",").map((s) => s.trim()).filter(Boolean),300    });301    const { token, ...meta } = created;302    c.err(JSON.stringify(meta, null, 2));303    if (has(c, "store")) {304      const store = writableStore(c.io, has(c, "insecure-file"));305      store.set(c.account, token);306      c.err(`Token stored as the active credential in ${store.describe}. It was not displayed.`);307    } else {308      c.err("Token (shown once — store it in a credential manager now; do not paste it into prompts, files or shell history):");309      c.out(token);310    }311    return;312  }313  if (sub === "revoke") {314    if (!rest[0]) throw new CliError("Usage: skills auth token revoke <id|prefix> [--yes]");315    const t = await resolveTokenId(api, rest[0]);316    await confirm(c, `Revoke token ${t.label}? It stops working immediately.`);317    const r = await api.revokeToken(t.id);318    return c.json ? c.out(JSON.stringify(r, null, 2)) : c.out(`Revoked ${r.name} (${r.prefix}).`);319  }320  if (sub === "rotate") {321    const me = await api.me();322    const current = me.credential.id;323    const target = rest[0] ? await resolveTokenId(api, rest[0]) : current ? { id: current, label: `${me.credential.name} (${me.credential.prefix})` } : null;324    if (!target) throw new CliError("The current credential is not an API token; pass a token id or prefix.");325    await confirm(c, `Rotate token ${target.label}? The old secret stops working immediately.`);326    const { token, ...meta } = await api.rotateToken(target.id);327    if (target.id === current && cred.store) {328      cred.store.set(c.account, token);329      if (c.json) return c.out(JSON.stringify({ ...meta, stored: cred.store.kind }, null, 2));330      c.out(`Rotated ${meta.name}: new prefix ${meta.prefix}, expires ${fmtDate(meta.expiresAt)}.`);331      c.out(`The new token replaced the old one in ${cred.store.describe}. It was not displayed.`);332      return;333    }334    c.err(JSON.stringify(meta, null, 2));335    c.err(target.id === current ? "The rotated credential came from SKILLS_TOKEN, so it cannot be stored for you. New token (shown once):" : "New token (shown once — store it in a credential manager now):");336    c.out(token);337    return;338  }339  throw new CliError("Usage: skills auth token create|list|revoke|rotate");340}341342/* ───────────────────────── authoring ───────────────────────── */343344function cmdInit(c: Ctx) {345  const type = (flag(c, "type") ?? "skill") as ArtifactType;346  if (!(ARTIFACT_TYPES as readonly string[]).includes(type)) throw new CliError(`Unknown type "${type}". Valid types: ${ARTIFACT_TYPES.join(", ")}.`);347  const id = parseName(flag(c, "name") ?? "");348  if (!id) throw new CliError("--name @namespace/slug is required (lowercase letters, digits, single hyphens).");349  const dir = resolve(c.io.cwd, c.args.pos[1] ?? id.slug);350  const description = flag(c, "description") ?? `TODO: describe what this ${type} does and when to use it.`;351  const manifest: Record<string, unknown> = { schemaVersion: "1.0", name: `@${id.namespace}/${id.slug}`, version: "0.1.0", type, description, license: "MIT", keywords: [], permissions: [], dependencies: [] };352  if (type === "skill") Object.assign(manifest, { entrypoint: "SKILL.md", compatibility: ["claude-code", "codex", "cursor", "generic-agent-skills"] });353  if (type === "mcp-server") Object.assign(manifest, { compatibility: ["claude-code", "codex", "cursor", "generic-mcp"], mcp: { transport: "stdio", command: "node", args: ["server.mjs"], env: [], tools: [] } });354  const files: Record<string, string> = {355    [MANIFEST_FILE]: JSON.stringify(manifest, null, 2) + "\n",356    "README.md": `# ${id.slug}\n\n${description}\n\n## Usage\n\nExplain how to use this ${type}, what it needs and what it must never do.\n`,357    "CHANGELOG.md": "# Changelog\n\n## 0.1.0\n\n- Initial version.\n",358    LICENSE: "Replace this file with the full text of your licence. The manifest declares MIT.\n",359  };360  if (type === "skill") files["SKILL.md"] = `---\nname: ${id.slug}\ndescription: ${description.replace(/\n/g, " ").slice(0, 1024)}\nlicense: MIT\n---\n\n# ${id.slug}\n\nWrite the instructions the agent should follow here.\n`;361  const clash = Object.keys(files).filter((f) => existsSync(join(dir, f)));362  if (clash.length) throw new CliError(`Refusing to overwrite existing files in ${dir}: ${clash.join(", ")}.`);363  mkdirSync(dir, { recursive: true });364  for (const [f, body] of Object.entries(files)) writeFileSync(join(dir, f), body, { flag: "wx" });365  if (c.json) return c.out(JSON.stringify({ dir, files: Object.keys(files) }, null, 2));366  c.out(`Created ${type} scaffold for @${id.namespace}/${id.slug} in ${dir}:`);367  Object.keys(files).forEach((f) => c.out(`  ${f}`));368  c.out(`Next: edit the files, then \`skills validate ${c.args.pos[1] ?? id.slug}\`.`);369}370371function cmdValidate(c: Ctx): number {372  const p = loadPackage(c, c.args.pos[1]);373  if (c.json) c.out(JSON.stringify({ ok: p.result.ok, name: p.result.manifest?.name ?? null, version: p.result.manifest?.version ?? null, findings: p.result.findings, files: p.result.files, skipped: p.skipped, capabilities: p.result.capabilities }, null, 2));374  else {375    c.out(`${p.result.manifest ? `${p.result.manifest.name}@${p.result.manifest.version} (${p.result.manifest.type})` : p.dir}`);376    printValidation(c, p.result, p.skipped);377    c.out(p.result.ok ? "Valid: no blocking findings." : "Invalid: fix the blocking findings above.");378  }379  return p.result.ok ? EXIT.ok : EXIT.validation;380}381382function cmdPack(c: Ctx): number {383  const p = loadPackage(c, c.args.pos[1]);384  if (!p.result.ok || !p.result.manifest) {385    if (!c.json) printValidation(c, p.result, p.skipped);386    c.err("error: the package has blocking findings; nothing was packed.");387    return EXIT.validation;388  }389  const { archive, sha256 } = packFiles(p.files);390  const id = parseName(p.result.manifest.name)!;391  const outFile = resolve(c.io.cwd, flag(c, "output") ?? `${id.slug}-${p.result.manifest.version}.tar.gz`);392  writeFileSync(outFile, archive);393  if (c.json) c.out(JSON.stringify({ file: outFile, sizeBytes: archive.length, sha256, files: p.result.files.length }, null, 2));394  else {395    c.out(`Packed ${p.result.manifest.name}@${p.result.manifest.version}`);396    c.out(`  file:   ${outFile}`);397    c.out(`  size:   ${fmtBytes(archive.length)} (${archive.length} bytes, ${p.result.files.length} files)`);398    c.out(`  sha256: ${sha256}`);399  }400  return EXIT.ok;401}402403async function cmdPublish(c: Ctx): Promise<number> {404  const dryRun = has(c, "dry-run");405  const p = loadPackage(c, c.args.pos[1]);406  const m = p.result.manifest;407  if (!p.result.ok || !m) {408    if (c.json) c.out(JSON.stringify({ ok: false, findings: p.result.findings }, null, 2));409    else {410      printValidation(c, p.result, p.skipped);411      c.err("error: validation failed; nothing was uploaded.");412    }413    return EXIT.validation;414  }415  const id = parseName(m.name)!;416  const { archive, sha256 } = packFiles(p.files);417  const cred = credential(c);418  if (!cred && !dryRun) throw new CliError("Not logged in. Run `skills login` (or set SKILLS_TOKEN for a single process).", EXIT.auth);419  const api = client(c, cred?.token);420421  let existing: Awaited<ReturnType<RegistryClient["artifact"]>> | null = null;422  let versions: string[] = [];423  try {424    existing = await api.artifact(m.name);425    versions = (await api.versions(m.name)).items.map((v) => v.version);426  } catch (e) {427    if (!(e instanceof RegistryError) || e.status !== 404) throw e;428  }429  const requested = flag(c, "visibility");430  if (requested && !["public", "unlisted", "private"].includes(requested)) throw new CliError("--visibility must be public, unlisted or private.");431  const visibility = requested ?? existing?.visibility ?? "private";432  const firstRelease = versions.length === 0;433  const versionExists = versions.includes(m.version);434  const visibilityChange = !!existing && existing.visibility !== visibility;435  const notesFile = flag(c, "notes-file");436  const releaseNotes = flag(c, "notes") ?? (notesFile ? readFileSync(resolve(c.io.cwd, notesFile), "utf8") : undefined);437438  const report = {439    target: `${m.name}@${m.version}`, type: m.type, registry: c.registry, visibility, artifactExists: !!existing, firstRelease, versionExists,440    visibilityChange: visibilityChange ? { from: existing!.visibility, to: visibility } : null, permissions: m.permissions ?? [], dependencies: m.dependencies ?? [],441    archive: { sizeBytes: archive.length, sha256 }, files: p.result.files.map((f) => ({ path: f.path, sizeBytes: f.size, executable: (f.mode & 0o111) !== 0 })), skipped: p.skipped,442    findings: p.result.findings, authenticated: !!cred,443  };444  if (!c.json) {445    c.out(`${dryRun ? "Dry run — nothing will be uploaded" : "Publish plan"}`);446    c.out(`  Target:      ${report.target} (${m.type})`);447    c.out(`  Registry:    ${c.registry}`);448    c.out(`  Visibility:  ${visibility}${visibilityChange ? ` (CHANGES from ${existing!.visibility})` : ""}`);449    c.out(`  Artifact:    ${existing ? "exists" : "does not exist yet — it will be created"}${firstRelease ? " · this is its first release" : ""}`);450    c.out(`  Permissions: ${report.permissions.join(", ") || "none declared"}`);451    c.out(`  Archive:     ${fmtBytes(archive.length)} (${archive.length} bytes) sha256 ${sha256}`);452    printValidation(c, p.result, p.skipped);453    if (versionExists) c.out(`  Version ${m.version} is already published. Releases are immutable: bump the version.`);454  }455  if (dryRun) {456    if (c.json) c.out(JSON.stringify({ dryRun: true, ok: !versionExists, ...report }, null, 2));457    else c.out(versionExists ? "Dry run complete: this publish would be refused." : "Dry run complete: no changes were made on the registry.");458    return versionExists ? EXIT.error : EXIT.ok;459  }460  if (versionExists) throw new CliError(`${m.name}@${m.version} already exists. Releases are immutable; publish a new version.`);461462  if (!existing) await confirm(c, `Create the new ${visibility.toUpperCase()} artifact ${m.name} and publish version ${m.version}?`);463  else if (visibilityChange) await confirm(c, `Change the visibility of ${m.name} from ${existing.visibility} to ${visibility.toUpperCase()} and publish ${m.version}?`);464  else if (firstRelease && visibility !== "private") await confirm(c, `Publish the first release of the ${visibility.toUpperCase()} artifact ${m.name}?`);465466  let artifactId: string;467  if (!existing) {468    const a = await api.createArtifact({ namespace: id.namespace, slug: id.slug, type: m.type, summary: m.description, visibility, confirmPublic: visibility === "public", license: m.license, repository: m.repository, homepage: m.homepage, issues: m.issues, keywords: m.keywords });469    artifactId = a.id;470    c.err(`Created ${m.name} (${visibility}).`);471  } else {472    artifactId = existing.id;473    if (visibilityChange) {474      await api.updateArtifact(artifactId, { visibility, confirmPublic: true });475      c.err(`Visibility changed to ${visibility}.`);476    }477  }478  const up = await api.beginUpload(artifactId, { version: m.version, sha256, sizeBytes: archive.length, releaseNotes, sourceCommit: m.sourceCommit });479  await api.sendUpload(up.uploadId, new Uint8Array(archive));480  c.err("Archive uploaded; waiting for server-side validation and scanning…");481  let fin;482  try {483    fin = await api.finalizeUpload(up.uploadId, sha256);484  } catch (e) {485    if (e instanceof RegistryError && e.code === "release_rejected") {486      const findings = ((e.details as { findings?: Finding[] })?.findings ?? []) as Finding[];487      if (c.json) c.out(JSON.stringify({ ok: false, error: e.code, findings, requestId: e.requestId }, null, 2));488      else {489        c.out("The registry rejected this release. Nothing was published.");490        printFindings(c, findings);491      }492      return EXIT.validation;493    }494    throw e;495  }496497  // Verify what the registry now serves before reporting success.498  const verification: { metadata: boolean; download: boolean | null; note?: string } = { metadata: false, download: null };499  const served = await api.version(m.name, m.version);500  verification.metadata = served.sha256 === sha256 && served.version === m.version;501  if (!verification.metadata) throw new CliError(`Verification failed: the registry reports sha256 ${served.sha256} for ${report.target}, expected ${sha256}.`);502  try {503    const dl = await api.download(m.name, m.version);504    verification.download = sha256Hex(Buffer.from(dl.bytes)) === sha256;505    if (!verification.download) throw new CliError(`Verification failed: the downloaded archive of ${report.target} does not hash to ${sha256}.`);506  } catch (e) {507    if (e instanceof CliError) throw e;508    verification.note = `download could not be verified with this credential (${(e as Error).message})`;509  }510  const url = `${c.registry}/@${id.namespace}/${id.slug}`;511  const state = fin.release.state;512  if (c.json) c.out(JSON.stringify({ ok: true, name: m.name, version: m.version, state, securityStatus: fin.release.securityStatus, sha256, url, versionUrl: `${url}/versions/${m.version}`, visibility, verification, findings: fin.findings }, null, 2));513  else {514    c.out(state === "published" ? `Published ${report.target}` : `Uploaded ${report.target} — QUARANTINED for security review (not publicly downloadable until a moderator approves it)`);515    c.out(`  URL:        ${url}`);516    c.out(`  Version:    ${m.version} (${visibility}, security status: ${fin.release.securityStatus})`);517    c.out(`  SHA-256:    ${sha256}`);518    c.out(`  Verified:   metadata ✓${verification.download ? " · download re-hashed ✓" : ` · ${verification.note ?? "download not verified"}`}`);519    c.out(`  Install:    skills install ${report.target} --client claude-code`);520  }521  return EXIT.ok;522}523524async function cmdDeprecate(c: Ctx) {525  const ref = parseRef(c.args.pos[1]);526  const reason = flag(c, "reason");527  if (!reason) throw new CliError("--reason is required so users know what to use instead.");528  const { api } = authed(c);529  const a = await api.artifact(ref.name);530  await confirm(c, `Deprecate ${ref.name}? It stays downloadable but is marked deprecated.`);531  const r = await api.setState(a.id, "deprecate", { reason, confirm: true });532  c.json ? c.out(JSON.stringify(r, null, 2)) : c.out(`Deprecated ${r.name}: ${reason}`);533}534535async function cmdRevoke(c: Ctx) {536  const ref = parseRef(c.args.pos[1], flag(c, "version"));537  const reason = flag(c, "reason");538  if (!ref.version) throw new CliError("Usage: skills revoke @ns/slug@version --reason TEXT [--yes]");539  if (!reason) throw new CliError("--reason is required; it is shown publicly.");540  const { api } = authed(c);541  const rel = (await api.versions(ref.name)).items.find((v) => v.version === ref.version);542  if (!rel) throw new CliError(`${ref.name}@${ref.version} does not exist.`);543  await confirm(c, `Revoke ${ref.name}@${ref.version}? Downloads are blocked permanently; the record remains.`);544  const r = await api.revokeRelease(rel.id, reason, true);545  c.json ? c.out(JSON.stringify(r, null, 2)) : c.out(`Revoked ${ref.name}@${r.version}: ${reason}`);546}547548/* ───────────────────────── discovery ───────────────────────── */549550async function cmdSearch(c: Ctx) {551  const q = c.args.pos.slice(1).join(" ").trim();552  if (!q) throw new CliError('Usage: skills search "query" [--type t] [--client c]');553  const r = await reader(c).search(q, { type: flag(c, "type"), compatibility: flag(c, "client") });554  if (c.json) return c.out(JSON.stringify(r, null, 2));555  if (!r.items.length) return c.out(`No results for "${q}".`);556  for (const a of r.items) {557    c.out(`${a.name}${a.latestVersion ? `@${a.latestVersion}` : ""}  [${a.type}]${a.state === "deprecated" ? " (deprecated)" : ""}${a.publisher.identityVerified ? " · verified publisher" : ""}`);558    c.out(`    ${a.summary}`);559  }560  c.out(`${r.total} result${r.total === 1 ? "" : "s"}.`);561}562563async function cmdVersions(c: Ctx) {564  const ref = parseRef(c.args.pos[1]);565  const { items } = await reader(c).versions(ref.name);566  if (c.json) return c.out(JSON.stringify(items, null, 2));567  if (!items.length) return c.out(`${ref.name} has no releases.`);568  for (const v of items) c.out(`${v.version.padEnd(16)} ${v.state.padEnd(11)} ${fmtDate(v.publishedAt)}  ${fmtBytes(v.sizeBytes).padStart(9)}  sha256 ${v.sha256}${v.revocationReason ? `  — revoked: ${v.revocationReason}` : ""}`);569}570571async function cmdInspect(c: Ctx) {572  const ref = parseRef(c.args.pos[1], flag(c, "version"));573  const api = reader(c);574  const a = await api.artifact(ref.name);575  if (!a.latestVersion && !ref.version) {576    if (c.json) return c.out(JSON.stringify({ artifact: a, release: null }, null, 2));577    c.out(`${a.name} [${a.type}] — ${a.summary}`);578    return c.out("No releases yet.");579  }580  const r = await api.version(ref.name, ref.version ?? "latest");581  const file = flag(c, "file");582  if (file) {583    const f = await api.file(ref.name, r.version, file);584    return c.json ? c.out(JSON.stringify(f, null, 2)) : c.io.stdout(f.content.endsWith("\n") ? f.content : f.content + "\n");585  }586  if (c.json) return c.out(JSON.stringify({ artifact: a, release: r }, null, 2));587  c.out(`${a.name}@${r.version} [${a.type}] — ${a.summary}`);588  c.out(`  State:        ${a.state}${a.deprecationReason ? ` (${a.deprecationReason})` : ""} · release ${r.state}${r.revocationReason ? ` (${r.revocationReason})` : ""}`);589  c.out(`  Publisher:    @${a.namespace}${a.publisher.displayName ? ` (${a.publisher.displayName})` : ""} — identity ${a.publisher.identityVerified ? "verified" : "not verified"}; source ${a.sourceVerified ? "verified" : "not verified"}`);590  c.out(`  License:      ${a.license ?? "not declared"}`);591  c.out(`  Source:       ${a.repository ?? "not declared"}`);592  c.out(`  Published:    ${fmtDate(r.publishedAt)}${r.publishedBy ? ` by ${r.publishedBy}` : ""}`);593  c.out(`  Archive:      ${fmtBytes(r.sizeBytes)} · sha256 ${r.sha256}`);594  c.out(`  Capabilities: ${a.capabilities.join(", ") || "—"}`);595  c.out(`  Permissions:  ${(r.manifest.permissions ?? []).join(", ") || "none declared"}`);596  c.out(`  Compatible:   ${r.compatibility.map((x) => `${x.client}${x.verified ? " (verified)" : " (publisher claim)"}`).join(", ") || "not declared"}`);597  if (r.dependencies.length) c.out(`  Dependencies: ${r.dependencies.map((d) => `${d.name}@${d.version}${d.optional ? " (optional)" : ""}`).join(", ")}`);598  if (r.examples?.length) c.out(`  Examples:     ${r.examples.length} screenshot${r.examples.length === 1 ? "" : "s"} supplied by the publisher (${[...new Set(r.examples.map((e) => e.client))].join(", ")})`);599  c.out(`  Security:     ${r.securityStatus} (static checks are advisory, not a guarantee of safety)`);600  printFindings(c, r.scan?.findings ?? []);601  c.out(`  Files (${r.files.length}):`);602  for (const f of r.files) c.out(`    ${f.executable ? "x" : " "} ${fmtBytes(f.sizeBytes).padStart(9)}  ${f.path}`);603}604605/** Download and verify. Returns bytes only when the SHA-256 matches the release metadata. */606async function fetchVerified(api: RegistryClient, name: string, version: string, expectedSha: string): Promise<Buffer> {607  const dl = await api.download(name, version);608  const bytes = Buffer.from(dl.bytes);609  const got = sha256Hex(bytes);610  if (got !== expectedSha) throw new CliError(`Checksum mismatch for ${name}@${version}: expected ${expectedSha}, got ${got}. The download was discarded.`);611  return bytes;612}613614async function cmdDownload(c: Ctx) {615  const ref = parseRef(c.args.pos[1], flag(c, "version"));616  const api = reader(c);617  const r = await api.version(ref.name, ref.version ?? "latest");618  const bytes = await fetchVerified(api, ref.name, r.version, r.sha256);619  const extract = flag(c, "extract");620  if (extract) {621    const dest = resolve(c.io.cwd, extract);622    if (existsSync(dest) && readdirSync(dest).length > 0) throw new CliError(`Refusing to extract into ${dest}: the directory is not empty.`);623    const files = readArchive(bytes);624    const v = validatePackage(files, { expectedName: ref.name, expectedVersion: r.version });625    if (!v.ok) throw new CliError(`Refusing to extract ${ref.name}@${r.version}: the archive fails safety checks (${v.findings.filter((f) => f.blocking).map((f) => f.code).join(", ")}).`);626    mkdirSync(dest, { recursive: true });627    const written = extractTo(files, dest);628    if (c.json) return c.out(JSON.stringify({ name: ref.name, version: r.version, sha256: r.sha256, verified: true, extractedTo: dest, files: written }, null, 2));629    c.out(`Downloaded ${ref.name}@${r.version} — sha256 verified (${r.sha256})`);630    return c.out(`Extracted ${written.length} files into ${dest}. Nothing was executed.`);631  }632  const outFile = resolve(c.io.cwd, flag(c, "output") ?? `${ref.slug}-${r.version}.tar.gz`);633  writeFileSync(outFile, bytes);634  if (c.json) return c.out(JSON.stringify({ name: ref.name, version: r.version, sha256: r.sha256, verified: true, file: outFile, sizeBytes: bytes.length }, null, 2));635  c.out(`Downloaded ${ref.name}@${r.version} → ${outFile}`);636  c.out(`  sha256 verified: ${r.sha256}`);637}638639function printInstructions(c: Ctx, list: InstallInstructions[]) {640  for (const i of list) {641    c.out(`${i.clientLabel}${i.supported ? "" : ` — not supported: ${i.reason}`}`);642    i.steps.forEach((s, n) => {643      c.out(`  ${n + 1}. ${s.title}`);644      if (s.command) c.out(`       ${s.command}`);645      if (s.note) c.out(`       ${s.note}`);646    });647    for (const cfg of i.config) {648      c.out(`  Configuration for ${cfg.file}:`);649      cfg.content.trimEnd().split("\n").forEach((l) => c.out(`       ${l}`));650    }651    for (const e of i.requiredEnv) c.out(`  Environment: ${e.name}${e.required ? " (required)" : ""}${e.secret ? " (secret — set locally only)" : ""}${e.description ? ` — ${e.description}` : ""}`);652    i.warnings.forEach((w) => c.out(`  ! ${w}`));653  }654}655656function expandPath(c: Ctx, p: string): string {657  if (p === "~") return c.io.home;658  if (p.startsWith("~/")) return join(c.io.home, p.slice(2));659  return isAbsolute(p) ? p : resolve(c.io.cwd, p);660}661function destinationFor(c: Ctx, cl: Client, slug: string): string {662  const custom = flag(c, "dir");663  if (custom) return join(expandPath(c, custom), slug);664  const d = skillDestination(cl, slug, has(c, "project") ? "project" : "user");665  if (!d) throw new CliError(`Client "${cl}" does not install skill directories. Use one of: claude-code, codex, cursor, generic-agent-skills.`);666  return expandPath(c, d);667}668function requireClient(c: Ctx): Client {669  const cl = flag(c, "client");670  if (!cl || !(CLIENTS as readonly string[]).includes(cl)) throw new CliError(`--client is required. One of: ${CLIENTS.join(", ")}.`);671  return cl as Client;672}673674function backupExisting(c: Ctx, dest: string, slug: string): string {675  const backup = join(c.io.home, ".skills-registry", "backups", `${slug}-${new Date().toISOString().replace(/[^0-9]/g, "").slice(0, 17)}`);676  mkdirSync(dirname(backup), { recursive: true, mode: 0o700 });677  try {678    renameSync(dest, backup);679  } catch {680    cpSync(dest, backup, { recursive: true });681    rmSync(dest, { recursive: true, force: true });682  }683  return backup;684}685686async function installSkill(c: Ctx, opts: { name: string; slug: string; version?: string; client: Client; replace: boolean; skipConfirm?: boolean }): Promise<number> {687  const api = reader(c);688  const a = await api.artifact(opts.name);689  if (a.type === "mcp-server") {690    const r = await api.install(opts.name, opts.version ?? "latest", opts.client);691    if (c.json) c.out(JSON.stringify({ ...r, installed: false, note: "MCP servers are configured in your client; nothing was written or executed." }, null, 2));692    else {693      c.out(`${opts.name}@${r.version} is an MCP server listing. Nothing was downloaded, written or executed.`);694      c.out("Configure it in your client yourself:");695      printInstructions(c, r.instructions);696    }697    return EXIT.ok;698  }699  if (a.type !== "skill") {700    c.err(`${opts.name} is a ${a.type}; there is no client installer for that type.`);701    c.err(`Use: skills download ${opts.name}${opts.version ? `@${opts.version}` : ""} --extract ./${opts.slug}`);702    return EXIT.error;703  }704  const r = await api.version(opts.name, opts.version ?? "latest");705  if (r.state !== "published") throw new CliError(`${opts.name}@${r.version} is ${r.state}${r.revocationReason ? ` (${r.revocationReason})` : ""} and cannot be installed.`);706  const dest = destinationFor(c, opts.client, opts.slug);707  const exists = existsSync(dest);708  const advisory = (r.scan?.findings ?? []).filter((f) => f.severity !== "info");709  if (!c.json) {710    c.out(`Install ${opts.name}@${r.version} [skill] for ${opts.client}`);711    c.out(`  Publisher:   @${a.namespace} — identity ${a.publisher.identityVerified ? "verified" : "NOT verified"}; source ${a.sourceVerified ? "verified" : "not verified"}`);712    c.out(`  Permissions: ${(r.manifest.permissions ?? []).join(", ") || "none declared"}`);713    c.out(`  Security:    ${r.securityStatus} (advisory static checks)`);714    advisory.forEach((f) => c.out(`    [${f.severity}] ${f.code}${f.path ? ` — ${f.path}` : ""}: ${f.message}`));715    if (a.state === "deprecated") c.out(`  DEPRECATED:  ${a.deprecationReason ?? "no reason given"}`);716    c.out(`  Files:       ${r.fileCount} (${fmtBytes(r.unpackedBytes)}), ${r.files.filter((f) => f.executable).length} executable`);717    c.out(`  Destination: ${dest}${exists ? " (EXISTS)" : ""}`);718    c.out(`  Checksum:    sha256 ${r.sha256}`);719  }720  if (exists && !opts.replace) throw new CliError(`${dest} already exists. Use \`skills update\` to upgrade it, or --force to replace it (the old copy is backed up).`);721  if (!opts.skipConfirm) await confirm(c, `Copy these files into ${dest}? (Nothing will be executed.)`);722723  const bytes = await fetchVerified(api, opts.name, r.version, r.sha256);724  const files = readArchive(bytes);725  const check = validatePackage(files, { expectedName: opts.name, expectedVersion: r.version });726  if (!check.ok) throw new CliError(`Refusing to install ${opts.name}@${r.version}: the archive fails safety checks (${check.findings.filter((f) => f.blocking).map((f) => f.code).join(", ")}).`);727  let backup: string | null = null;728  if (exists) backup = backupExisting(c, dest, opts.slug);729  mkdirSync(dest, { recursive: true });730  let written: string[];731  try {732    written = extractTo(files, dest);733    writeFileSync(join(dest, LOCK_FILE), JSON.stringify({ name: opts.name, version: r.version, sha256: r.sha256, registry: c.registry, installedAt: new Date().toISOString(), client: opts.client }, null, 2) + "\n");734  } catch (e) {735    rmSync(dest, { recursive: true, force: true });736    if (backup) renameSync(backup, dest);737    throw e;738  }739  const rollback = backup ? `rm -rf '${dest}' && mv '${backup}' '${dest}'` : `rm -rf '${dest}'`;740  if (c.json) c.out(JSON.stringify({ installed: true, name: opts.name, version: r.version, sha256: r.sha256, destination: dest, files: written, backup, rollback, executed: false }, null, 2));741  else {742    c.out(`Installed ${opts.name}@${r.version} → ${dest} (${written.length} files, checksum verified, nothing executed).`);743    if (backup) c.out(`  Previous copy backed up to ${backup}`);744    c.out(`  Roll back with: ${rollback}`);745    c.out("  Read SKILL.md and any scripts before letting an agent use this skill.");746  }747  return EXIT.ok;748}749750async function cmdInstall(c: Ctx): Promise<number> {751  const ref = parseRef(c.args.pos[1], flag(c, "version"));752  return installSkill(c, { name: ref.name, slug: ref.slug, version: ref.version, client: requireClient(c), replace: has(c, "force") });753}754755async function cmdUpdate(c: Ctx): Promise<number> {756  const ref = parseRef(c.args.pos[1]);757  const cl = requireClient(c);758  const dest = destinationFor(c, cl, ref.slug);759  const lockPath = join(dest, LOCK_FILE);760  if (!existsSync(lockPath)) throw new CliError(`${ref.name} is not installed at ${dest} (no ${LOCK_FILE}). Use \`skills install\`.`);761  const lock = JSON.parse(readFileSync(lockPath, "utf8")) as { name: string; version: string; sha256: string };762  if (lock.name !== ref.name) throw new CliError(`${dest} contains ${lock.name}, not ${ref.name}.`);763  const api = reader(c);764  const a = await api.artifact(ref.name);765  const target = ref.version ?? a.latestVersion;766  if (!target) throw new CliError(`${ref.name} has no installable release.`);767  if (target === lock.version) {768    c.json ? c.out(JSON.stringify({ updated: false, version: lock.version }, null, 2)) : c.out(`${ref.name} is up to date (${lock.version}).`);769    return EXIT.ok;770  }771  const next = await api.version(ref.name, target);772  let before: string[] = [];773  try {774    before = (await api.version(ref.name, lock.version)).manifest.permissions ?? [];775  } catch { /* the installed version may have been revoked or removed from view */ }776  const after = next.manifest.permissions ?? [];777  const grew = after.filter((p) => !before.includes(p));778  if (!c.json) {779    c.out(`Update ${ref.name}: ${lock.version} → ${target}`);780    c.out(`  Permissions: ${before.join(", ") || "none"} → ${after.join(", ") || "none"}${grew.length ? `  (NEW: ${grew.join(", ")})` : "  (no new permissions)"}`);781  }782  await confirm(c, grew.length ? `This update declares NEW permissions (${grew.join(", ")}). Replace the installed copy?` : `Replace ${dest} with version ${target}? The current copy is backed up.`);783  return installSkill(c, { name: ref.name, slug: ref.slug, version: target, client: cl, replace: true, skipConfirm: true });784}785786/* ───────────────────────── entry point ───────────────────────── */787788export async function run(argv: string[], ioOverride: Partial<IO> = {}): Promise<number> {789  const io: IO = { ...defaultIO(), ...ioOverride };790  const out = (s = "") => io.stdout(s + "\n");791  const err = (s = "") => io.stderr(s + "\n");792  try {793    const args = parseArgs(argv);794    if (args.flags.version) return out(CLI_VERSION), EXIT.ok;795    const cmd = args.pos[0];796    if (!cmd || args.flags.help || cmd === "help") return io.stdout(HELP), cmd || args.flags.help ? EXIT.ok : EXIT.error;797    const registryRaw = (typeof args.flags.registry === "string" ? args.flags.registry : io.env.SKILLS_REGISTRY) ?? DEFAULT_REGISTRY;798    let url: URL;799    try {800      url = new URL(registryRaw);801    } catch {802      throw new CliError(`Invalid registry URL "${registryRaw}".`);803    }804    const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "[::1]";805    if (url.protocol !== "https:" && !(url.protocol === "http:" && local)) throw new CliError("The registry URL must use https (http is accepted only for localhost).");806    const c: Ctx = { io, args, registry: registryRaw.replace(/\/+$/, ""), account: url.host, json: args.flags.json === true, out, err };807    const sub = args.pos[1];808    switch (cmd) {809      case "login": await cmdLogin(c); break;810      case "logout": await cmdLogout(c); break;811      case "whoami": await cmdWhoami(c); break;812      case "auth":813        if (sub === "status") await cmdWhoami(c, true);814        else if (sub === "token") await cmdToken(c, args.pos[2], args.pos.slice(3));815        else throw new CliError("Usage: skills auth status | skills auth token create|list|revoke|rotate");816        break;817      case "token": await cmdToken(c, sub, args.pos.slice(2)); break;818      case "init": cmdInit(c); break;819      case "validate": return cmdValidate(c);820      case "pack": return cmdPack(c);821      case "publish": return await cmdPublish(c);822      case "deprecate": await cmdDeprecate(c); break;823      case "revoke": await cmdRevoke(c); break;824      case "search": await cmdSearch(c); break;825      case "versions": await cmdVersions(c); break;826      case "inspect": await cmdInspect(c); break;827      case "download": await cmdDownload(c); break;828      case "install": return await cmdInstall(c);829      case "update": return await cmdUpdate(c);830      default: throw new CliError(`Unknown command "${cmd}". Run \`skills --help\`.`);831    }832    return EXIT.ok;833  } catch (e) {834    if (e instanceof RegistryError) {835      const d = describeApiError(e);836      d.lines.forEach((l) => err(l));837      return d.exit;838    }839    if (e instanceof CliError) return err(`error: ${e.message}`), e.exit;840    if (e instanceof ArchiveError) return err(`error: ${e.message} [${e.code}]`), EXIT.validation;841    const msg = (e as Error)?.message ?? String(e);842    err(`error: ${/fetch failed|ECONNREFUSED|ENOTFOUND/.test(msg) ? `cannot reach the registry (${msg})` : msg}`);843    return EXIT.error;844  }845}846
Files main
Menu

Theme