SPB Git

Repository Public MIT

install.ts 9.5 KB 172 lines typescript

1import { CLIENT_LABELS, type ArtifactType, type Client } from "./constants";2import type { Manifest, McpBlock } from "./manifest";34/**5 * Installation adapters. Pure functions that DESCRIBE what to do; nothing here runs a package.6 * Paths and config formats were checked against each client's official documentation on 2026-11-097 * (see docs/STANDARDS.md). Download, configure and execute stay three separate user decisions.8 */910export type InstallScope = "user" | "project";1112export interface InstallStep {13  title: string;14  command?: string;15  note?: string;16}17export interface ConfigSnippet {18  file: string;19  language: "json" | "toml" | "bash";20  content: string;21}22export interface InstallInstructions {23  client: Client;24  clientLabel: string;25  supported: boolean;26  reason?: string;27  strategy: "copy-directory" | "mcp-config" | "manual";28  destination?: { user?: string; project?: string };29  cli?: string;30  steps: InstallStep[];31  config: ConfigSnippet[];32  warnings: string[];33  requiredEnv: { name: string; description?: string; required: boolean; secret: boolean }[];34}3536const SKILL_DIRS: Partial<Record<Client, { user: string; project: string }>> = {37  "claude-code": { user: "~/.claude/skills", project: ".claude/skills" },38  codex: { user: "~/.agents/skills", project: ".agents/skills" },39  cursor: { user: "~/.cursor/skills", project: ".cursor/skills" },40  "generic-agent-skills": { user: "~/.agents/skills", project: ".agents/skills" },41};4243export function skillDestination(client: Client, slug: string, scope: InstallScope): string | null {44  const d = SKILL_DIRS[client];45  return d ? `${d[scope]}/${slug}` : null;46}4748export interface InstallTarget {49  namespace: string;50  slug: string;51  version: string;52  type: ArtifactType;53  sha256: string;54  manifest: Pick<Manifest, "mcp" | "permissions">;55  baseUrl: string;56}5758export function downloadUrl(t: Pick<InstallTarget, "namespace" | "slug" | "version" | "baseUrl">): string {59  return `${t.baseUrl}/api/v1/artifacts/${t.namespace}/${t.slug}/versions/${t.version}/download`;60}6162function shq(s: string): string {63  return /^[A-Za-z0-9_@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`;64}65function tomlStr(s: string): string {66  return JSON.stringify(s);67}6869function manualSteps(t: InstallTarget, dest: string): InstallStep[] {70  const file = `${t.slug}-${t.version}.tar.gz`;71  return [72    { title: "Download the release archive", command: `curl -fsSLo ${file} ${downloadUrl(t)}` },73    { title: "Verify the SHA-256 checksum", command: `echo "${t.sha256}  ${file}" | shasum -a 256 -c -` },74    { title: "Inspect the contents before extracting", command: `tar -tzf ${file}` },75    { title: `Extract into ${dest}`, command: `mkdir -p ${dest} && tar -xzf ${file} -C ${dest}` },76  ];77}7879function mcpEnv(mcp: McpBlock) {80  return (mcp.env ?? []).map((e) => ({ name: e.name, description: e.description, required: e.required ?? true, secret: e.secret ?? false }));81}8283function mcpInstructions(t: InstallTarget, client: Client, mcp: McpBlock): Pick<InstallInstructions, "steps" | "config" | "warnings"> {84  const name = t.slug;85  const env = mcp.env ?? [];86  const steps: InstallStep[] = [];87  const config: ConfigSnippet[] = [];88  const warnings: string[] = [];89  if (mcp.transport === "stdio") {90    const command = mcp.command!;91    const args = mcp.args ?? [];92    warnings.push("This server runs locally on your machine with your user's privileges. Review the source before running it; the registry does not execute or host it.");93    if (client === "claude-code") {94      const envFlags = env.map((e) => `--env ${e.name}=<value>`).join(" ");95      steps.push({ title: "Register the local server", command: `claude mcp add --transport stdio ${envFlags ? envFlags + " " : ""}${shq(name)} -- ${[command, ...args].map(shq).join(" ")}` });96      config.push({ file: ".mcp.json (project scope)", language: "json", content: JSON.stringify({ mcpServers: { [name]: { command, args, ...(env.length ? { env: Object.fromEntries(env.map((e) => [e.name, `\${${e.name}}`])) } : {}) } } }, null, 2) });97    } else if (client === "codex") {98      let toml = `[mcp_servers.${/^[A-Za-z0-9_-]+$/.test(name) ? name : tomlStr(name)}]\ncommand = ${tomlStr(command)}\nargs = [${args.map(tomlStr).join(", ")}]\n`;99      if (env.length) toml += `env_vars = [${env.map((e) => tomlStr(e.name)).join(", ")}]\n`;100      config.push({ file: "~/.codex/config.toml", language: "toml", content: toml });101      steps.push({ title: "Add the server block to your Codex configuration", note: "env_vars forwards the named variables from your shell; set them locally. Codex asks before each tool call; when it runs with approval policy \"never\" it refuses MCP tool calls unless the server block also has default_tools_approval_mode = \"approve\" (observed with Codex CLI 0.167)." });102    } else if (client === "cursor") {103      config.push({ file: ".cursor/mcp.json (project) or ~/.cursor/mcp.json (global)", language: "json", content: JSON.stringify({ mcpServers: { [name]: { type: "stdio", command, args, ...(env.length ? { env: Object.fromEntries(env.map((e) => [e.name, `\${env:${e.name}}`])) } : {}) } } }, null, 2) });104      steps.push({ title: "Add the server to your Cursor MCP configuration" });105    } else {106      steps.push({ title: "Start the server over stdio from your MCP client", command: [command, ...args].map(shq).join(" ") });107    }108  } else {109    const url = mcp.url!;110    warnings.push("This is a remote endpoint operated by the publisher, not by this registry. Requests and any data you send go to that operator.");111    if (client === "claude-code") {112      steps.push({ title: "Register the remote server", command: `claude mcp add --transport http ${shq(name)} ${shq(url)}` });113      config.push({ file: ".mcp.json (project scope)", language: "json", content: JSON.stringify({ mcpServers: { [name]: { type: "http", url } } }, null, 2) });114    } else if (client === "codex") {115      config.push({ file: "~/.codex/config.toml", language: "toml", content: `[mcp_servers.${/^[A-Za-z0-9_-]+$/.test(name) ? name : tomlStr(name)}]\nurl = ${tomlStr(url)}\n` });116      steps.push({ title: "Add the server block to your Codex configuration" });117    } else if (client === "cursor") {118      config.push({ file: ".cursor/mcp.json (project) or ~/.cursor/mcp.json (global)", language: "json", content: JSON.stringify({ mcpServers: { [name]: { url } } }, null, 2) });119      steps.push({ title: "Add the server to your Cursor MCP configuration" });120    } else {121      steps.push({ title: "Connect your MCP client to the Streamable HTTP endpoint", note: url });122    }123  }124  if (env.length) steps.push({ title: "Set the required environment variables locally", note: `${env.map((e) => e.name).join(", ")} — configured on your machine only; never sent to the registry.` });125  return { steps, config, warnings };126}127128export function installInstructions(t: InstallTarget, client: Client): InstallInstructions {129  const ref = `@${t.namespace}/${t.slug}@${t.version}`;130  const base: InstallInstructions = { client, clientLabel: CLIENT_LABELS[client], supported: true, strategy: "manual", steps: [], config: [], warnings: [], requiredEnv: [] };131  const perms = t.manifest.permissions ?? [];132  if (perms.length) base.warnings.push(`Declared permissions: ${perms.join(", ")}.`);133134  if (t.type === "skill") {135    if (client === "generic-mcp") return { ...base, supported: false, reason: "Agent Skills are not MCP servers; pick a Skills-compatible client." };136    const dirs = SKILL_DIRS[client]!;137    const user = `${dirs.user}/${t.slug}`;138    const project = `${dirs.project}/${t.slug}`;139    return {140      ...base,141      strategy: "copy-directory",142      destination: { user, project },143      cli: `skills install ${ref} --client ${client}`,144      steps: [145        { title: "Install with the Skills CLI (verifies the checksum, previews the destination, never runs scripts)", command: `skills install ${ref} --client ${client}`, note: `Add --project to install into ${project} instead of ${user}.` },146        ...manualSteps(t, user).map((s, i) => (i === 0 ? { ...s, title: `Or manually: ${s.title.toLowerCase()}` } : s)),147      ],148      warnings: [...base.warnings, "Skills can contain scripts. Installing only copies files; read SKILL.md and any scripts before letting an agent use them."],149    };150  }151152  if (t.type === "mcp-server") {153    if (client === "generic-agent-skills") return { ...base, supported: false, reason: "MCP servers are configured in an MCP client, not installed as a skill directory." };154    const mcp = t.manifest.mcp;155    if (!mcp) return { ...base, supported: false, reason: "This release does not declare its MCP transport." };156    const r = mcpInstructions(t, client, mcp);157    const src = mcp.transport === "stdio" ? [{ title: "Optional: download the listing's source archive for review", command: `skills download ${ref}` }] : [];158    return { ...base, strategy: "mcp-config", steps: [...r.steps, ...src], config: r.config, warnings: [...base.warnings, ...r.warnings], requiredEnv: mcpEnv(mcp) };159  }160161  if (client === "generic-mcp") return { ...base, supported: false, reason: `A ${t.type} is not an MCP server.` };162  const dest = `./${t.slug}`;163  return {164    ...base,165    strategy: "manual",166    destination: { project: dest },167    cli: `skills download ${ref}`,168    steps: [{ title: "Download and verify with the Skills CLI", command: `skills download ${ref} --extract ${dest}` }, ...manualSteps(t, dest).map((s, i) => (i === 0 ? { ...s, title: `Or manually: ${s.title.toLowerCase()}` } : s))],169    warnings: [...base.warnings, `No client-specific installer exists for type "${t.type}"; follow the package README.`],170  };171}172
Files main
Menu

Theme