SPB Git

Repository Public MIT

manifest.ts 6.3 KB 136 lines typescript

1import { z } from "zod";2import semver from "semver";3import { ARTIFACT_TYPES, CAPABILITIES, CLIENTS, EXAMPLE_CLIENTS, EXAMPLE_LIMITS, FULL_NAME_RE, LIMITS, PERMISSIONS, SCHEMA_VERSION, type ArtifactType, type Capability } from "./constants";45const httpsUrl = z6  .string()7  .max(500)8  .refine((u) => {9    try {10      const p = new URL(u);11      return p.protocol === "https:" || p.protocol === "http:";12    } catch {13      return false;14    }15  }, "must be an http(s) URL");1617export const versionSchema = z18  .string()19  .max(64)20  .refine((v) => semver.valid(v) === v && !v.includes("+"), "must be a strict semantic version without build metadata, e.g. 1.2.0 or 1.2.0-beta.1");2122const compatibilityEntry = z.union([23  z.enum(CLIENTS),24  z.object({25    client: z.enum(CLIENTS),26    minVersion: z.string().max(32).optional(),27    platform: z.enum(["any", "macos", "linux", "windows"]).optional(),28    installStrategy: z.enum(["copy-directory", "mcp-config", "manual"]).optional(),29  }).strict(),30]);3132const dependencySchema = z.object({33  name: z.string().regex(FULL_NAME_RE, "must look like @namespace/slug"),34  version: z.string().min(1).max(64).refine((r) => semver.validRange(r) !== null, "must be a valid semver range"),35  optional: z.boolean().optional(),36}).strict();3738const envVar = z.object({39  name: z.string().regex(/^[A-Z_][A-Z0-9_]{0,63}$/),40  description: z.string().max(300).optional(),41  required: z.boolean().optional(),42  secret: z.boolean().optional(),43}).strict();4445export const mcpBlockSchema = z.object({46  transport: z.enum(["stdio", "streamable-http"]),47  /** stdio: command the USER runs locally; never executed by the registry. */48  command: z.string().max(200).optional(),49  args: z.array(z.string().max(500)).max(32).optional(),50  env: z.array(envVar).max(32).optional(),51  /** streamable-http: endpoint operated by the publisher, not by this registry. */52  url: httpsUrl.optional(),53  tools: z.array(z.object({ name: z.string().max(128), description: z.string().max(1000).optional() }).strict()).max(200).optional(),54}).strict().superRefine((m, ctx) => {55  if (m.transport === "stdio" && !m.command) ctx.addIssue({ code: "custom", path: ["command"], message: "stdio servers must declare the command users run locally" });56  if (m.transport === "streamable-http" && !m.url) ctx.addIssue({ code: "custom", path: ["url"], message: "streamable-http servers must declare their endpoint url" });57});5859/** A screenshot of the package in use. The image lives in the archive; the validator checks the file itself. */60export const exampleSchema = z.object({61  title: z.string().trim().min(1).max(120),62  client: z.enum(EXAMPLE_CLIENTS),63  image: z.string().min(1).max(LIMITS.maxPathLength),64  alt: z.string().trim().min(1).max(300),65  prompt: z.string().max(2000).optional(),66  caption: z.string().max(500).optional(),67  capturedAt: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "must be a date like 2026-11-10").refine((d) => !Number.isNaN(Date.parse(`${d}T00:00:00Z`)) && new Date(`${d}T00:00:00Z`).toISOString().startsWith(d), "must be a real calendar date").optional(),68}).strict();6970export const manifestSchema = z.object({71  schemaVersion: z.literal(SCHEMA_VERSION),72  name: z.string().regex(FULL_NAME_RE, "must look like @namespace/slug (lowercase letters, digits, single hyphens)"),73  version: versionSchema,74  type: z.enum(ARTIFACT_TYPES),75  description: z.string().trim().min(10).max(LIMITS.maxSummaryLength),76  license: z.string().trim().min(1).max(100),77  repository: httpsUrl.optional(),78  homepage: httpsUrl.optional(),79  issues: httpsUrl.optional(),80  entrypoint: z.string().max(LIMITS.maxPathLength).optional(),81  keywords: z.array(z.string().regex(/^[a-z0-9][a-z0-9-]{0,31}$/, "lowercase letters, digits and hyphens")).max(12).optional(),82  compatibility: z.array(compatibilityEntry).max(16).optional(),83  permissions: z.array(z.enum(PERMISSIONS)).max(PERMISSIONS.length).optional(),84  dependencies: z.array(dependencySchema).max(64).optional(),85  capabilities: z.array(z.enum(CAPABILITIES)).max(CAPABILITIES.length).optional(),86  mcp: mcpBlockSchema.optional(),87  examples: z.array(exampleSchema).max(EXAMPLE_LIMITS.maxExamples).optional(),88  sourceCommit: z.string().regex(/^[0-9a-f]{7,64}$/).optional(),89}).strict();9091export type Manifest = z.infer<typeof manifestSchema>;92export type ManifestExample = z.infer<typeof exampleSchema>;93export type McpBlock = z.infer<typeof mcpBlockSchema>;9495export function parseName(name: string): { namespace: string; slug: string } | null {96  const m = FULL_NAME_RE.exec(name);97  return m ? { namespace: m[1]!, slug: m[2]! } : null;98}99100export function normalizeCompatibility(m: Manifest) {101  return (m.compatibility ?? []).map((c) =>102    typeof c === "string"103      ? { client: c, minVersion: null as string | null, platform: "any", installStrategy: defaultStrategy(m.type) }104      : { client: c.client, minVersion: c.minVersion ?? null, platform: c.platform ?? "any", installStrategy: c.installStrategy ?? defaultStrategy(m.type) },105  );106}107108function defaultStrategy(type: ArtifactType): "copy-directory" | "mcp-config" | "manual" {109  if (type === "skill") return "copy-directory";110  if (type === "mcp-server") return "mcp-config";111  return "manual";112}113114/** Capabilities are derived from facts when the publisher does not state them. */115export function deriveCapabilities(m: Manifest, hasExecutableFiles: boolean): Capability[] {116  if (m.capabilities?.length) return [...new Set(m.capabilities)];117  const caps = new Set<Capability>(["downloadable"]);118  if (m.type === "skill" || m.type === "bundle" || m.type === "template") caps.add("installable");119  if (m.type === "mcp-server") {120    if (m.mcp?.transport === "streamable-http") caps.add("remote-endpoint");121    else caps.add("installable");122  }123  if (m.repository) caps.add("source-linked");124  if (hasExecutableFiles) caps.add("executable");125  return [...caps];126}127128const SPDX_COMMON = new Set([129  "MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "MPL-2.0", "GPL-2.0-only", "GPL-2.0-or-later", "GPL-3.0-only",130  "GPL-3.0-or-later", "LGPL-2.1-only", "LGPL-3.0-only", "AGPL-3.0-only", "AGPL-3.0-or-later", "Unlicense", "CC0-1.0",131  "CC-BY-4.0", "CC-BY-SA-4.0", "CC-BY-NC-4.0", "0BSD", "Zlib", "BSL-1.0", "EUPL-1.2",132]);133export function isKnownLicense(license: string): boolean {134  return license.split(/\s+(?:OR|AND)\s+/).every((p) => SPDX_COMMON.has(p.replace(/[()]/g, "")));135}136
Files main
Menu

Theme