Repository Public MIT
1import Link from "next/link";2import { notFound } from "next/navigation";3import { AlertTriangle, Download, ExternalLink, Flag } from "lucide-react";4import { EXAMPLE_CLIENTS, EXAMPLE_CLIENT_LABELS, TYPE_LABELS, type Finding } from "@skills/registry-core";5import { bytes, date, dateTime, plural } from "@/lib/format";6import { t } from "@/lib/i18n";7import { publicCtx } from "@/lib/session";8import { artifactHistory } from "@/server/community";9import { ApiError } from "@/server/errors";10import { artifactDto, getArtifactRow, getRelease, instructionsFor, listArtifacts, listReleases, releaseDetail, releaseDto } from "@/server/registry";11import { ArtifactCard } from "./artifact-card";12import { SecurityBadge, StateBadge, TrustBadges, TypeBadge } from "./badges";13import { Checksum, CodeBlock } from "./code";14import { ExamplesGallery, type ExampleItem } from "./examples-gallery";15import { FileExplorer } from "./file-explorer";16import { InstallTabs } from "./install-tabs";17import { Markdown } from "./markdown";1819export async function loadArtifact(namespace: string, slug: string, version?: string) {20 const ctx = await publicCtx();21 try {22 const a = await getArtifactRow(ctx, namespace, slug);23 const rel = a.latest_version || version ? await getRelease(ctx, a, version) : null;24 return { ctx, a, rel };25 } catch (e) {26 if (e instanceof ApiError && (e.status === 404 || e.status === 410)) notFound();27 throw e;28 }29}3031function Section({ id, title, children, aside }: { id: string; title: string; children: React.ReactNode; aside?: React.ReactNode }) {32 return (33 <section id={id} aria-labelledby={`${id}-h`} className="scroll-mt-24 border-t border-line pt-8">34 <div className="mb-4 flex flex-wrap items-baseline justify-between gap-2">35 <h2 id={`${id}-h`} className="text-lg font-semibold tracking-tight">36 {title}37 </h2>38 {aside}39 </div>40 {children}41 </section>42 );43}4445function Meta({ label, children }: { label: string; children: React.ReactNode }) {46 return (47 <div className="flex items-start justify-between gap-4 border-b border-line py-2.5 last:border-0">48 <dt className="shrink-0 text-sm text-muted">{label}</dt>49 <dd className="min-w-0 break-words text-right text-sm text-text">{children}</dd>50 </div>51 );52}53const missing = <span className="text-faint">not provided</span>;5455const SEV: Record<string, string> = { critical: "text-danger", high: "text-danger", medium: "text-warn", low: "text-muted", info: "text-faint" };5657export async function ArtifactView({ namespace, slug, version }: { namespace: string; slug: string; version?: string }) {58 const { ctx, a, rel } = await loadArtifact(namespace, slug, version);59 const dto = artifactDto(a);60 const name = `@${a.namespace}/${a.slug}`;61 const [detail, releases, history, related] = await Promise.all([62 rel ? releaseDetail(ctx, a, rel) : null,63 listReleases(ctx, a),64 artifactHistory(ctx, a),65 listArtifacts(ctx, { type: a.type, perPage: 4 }),66 ]);67 const instructions = rel && rel.state === "published" ? instructionsFor(a, rel) : [];68 const isLatest = !!rel && rel.version === a.latest_version;69 const findings: Finding[] = detail?.scan?.findings ?? [];70 const advisories = findings.filter((f) => f.severity !== "info");71 const perms = detail?.manifest.permissions ?? [];72 const relatedItems = related.items.filter((r) => r.id !== a.id).slice(0, 3);73 const mcp = detail?.manifest.mcp;74 const tools = mcp?.tools ?? [];75 const envVars = mcp?.env ?? [];76 // Screenshots are shown only for an installable release; the URLs are same-origin paths into the archive.77 const examples = rel && rel.state === "published" ? detail?.examples ?? [] : [];78 const exampleGroups = EXAMPLE_CLIENTS.map((c) => ({79 client: c as string,80 label: EXAMPLE_CLIENT_LABELS[c],81 items: examples.filter((e) => e.client === c).map((e): ExampleItem => ({82 title: e.title, client: e.client, clientLabel: EXAMPLE_CLIENT_LABELS[c], src: e.url.replace(/^https?:\/\/[^/]+/, ""), alt: e.alt,83 prompt: e.prompt, caption: e.caption, capturedAt: e.capturedAt, width: e.width, height: e.height,84 })),85 })).filter((g) => g.items.length > 0);86 const toc: [string, string][] = [["install", "Install"], ...(examples.length ? [["examples", `Examples (${examples.length})`] as [string, string]] : []), ["readme", "Documentation"], ...(tools.length ? [["tools", `Tools (${tools.length})`] as [string, string]] : []), ["files", "Files"], ["security", "Security"], ["versions", "Versions"], ["history", "History"]];87 const facts: [string, React.ReactNode][] = rel88 ? [89 ["Version", <span key="v" className="font-mono">{rel.version}</span>],90 ["Published", date(rel.published_at)],91 ["Licence", a.license ?? "not provided"],92 ["Archive", bytes(Number(rel.size_bytes))],93 ...(detail ? [["Files", String(detail.fileCount)] as [string, React.ReactNode]] : []),94 ...(mcp ? [["Transport", <span key="t" className="font-mono">{mcp.transport}</span>] as [string, React.ReactNode]] : []),95 ...(tools.length ? [["Tools", String(tools.length)] as [string, React.ReactNode]] : []),96 ...(examples.length ? [["Examples", String(examples.length)] as [string, React.ReactNode]] : []),97 ]98 : [];99100 return (101 <div className="container-page pb-10 pt-10">102 <nav aria-label="Breadcrumb" className="mb-5 font-mono text-[12px] text-faint">103 <Link href="/explore" className="hover:text-muted">explore</Link> / <Link href={`/explore?type=${a.type}`} className="hover:text-muted">{TYPE_LABELS[a.type].plural.toLowerCase()}</Link> /{" "}104 <Link href={`/@${a.namespace}`} className="hover:text-muted">@{a.namespace}</Link>105 </nav>106107 <header className="mb-8">108 <div className="mb-3 flex flex-wrap items-center gap-1.5">109 <TypeBadge type={a.type} />110 <StateBadge state={a.state} reason={a.deprecation_reason} />111 {rel && <StateBadge state={rel.state} reason={rel.revocation_reason} />}112 <TrustBadges identityVerified={dto.publisher.identityVerified} sourceVerified={a.source_verified} />113 {rel && <SecurityBadge status={rel.security_status} />}114 </div>115 <h1 className="break-words font-mono text-2xl font-medium tracking-tight sm:text-3xl">116 <Link href={`/@${a.namespace}`} className="text-muted underline decoration-line-strong decoration-1 underline-offset-4 hover:text-text">@{a.namespace}</Link>117 <span className="text-faint">/</span>118 {a.slug}119 {rel && <span className="ml-3 align-middle font-mono text-base text-faint">v{rel.version}</span>}120 </h1>121 <p className="mt-3 max-w-3xl text-base text-muted">{a.summary}</p>122 {a.capabilities.length > 0 && (123 <ul className="mt-4 flex flex-wrap gap-1.5" aria-label="What this entry is">124 {a.capabilities.map((c) => (125 <li key={c} className="chip">{c}</li>126 ))}127 </ul>128 )}129 {facts.length > 0 && (130 <dl className="mt-6 grid grid-cols-2 overflow-hidden rounded-xl border border-line bg-graphite sm:grid-cols-4 lg:flex" data-testid="quick-facts">131 {facts.map(([k, v]) => (132 <div key={k} className="min-w-0 border-b border-r border-line px-4 py-3 last:border-r-0 lg:flex-1 lg:border-b-0">133 <dt className="eyebrow">{k}</dt>134 <dd className="mt-1 truncate text-sm font-medium text-text">{v}</dd>135 </div>136 ))}137 </dl>138 )}139 </header>140141 {a.state === "deprecated" && (142 <div role="note" className="callout mb-6 border-warn/40 bg-warn/5">143 <AlertTriangle size={16} aria-hidden className="mt-0.5 shrink-0 text-warn" />144 <p><strong className="font-semibold">Deprecated.</strong> {a.deprecation_reason ?? "The maintainer no longer recommends this artifact."}</p>145 </div>146 )}147 {a.state === "archived" && (148 <div role="note" className="mb-6 rounded-lg border border-line-strong p-4 text-sm text-muted">Archived: this artifact is read-only and receives no new releases.</div>149 )}150 {rel?.state === "revoked" && (151 <div role="alert" className="callout mb-6 border-danger/40 bg-danger/5">152 <AlertTriangle size={16} aria-hidden className="mt-0.5 shrink-0 text-danger" />153 <p><strong className="font-semibold">Version {rel.version} was revoked on {date(rel.revoked_at)}.</strong> {rel.revocation_reason} Downloads are blocked; the record is kept for auditability.</p>154 </div>155 )}156 {rel && !isLatest && a.latest_version && rel.state !== "revoked" && (157 <div role="note" className="mb-6 rounded-lg border border-line-strong p-4 text-sm text-muted">158 You are viewing v{rel.version}. The latest release is <Link className="link" href={`/@${a.namespace}/${a.slug}`}>v{a.latest_version}</Link>.159 </div>160 )}161162 <div className="grid gap-10 lg:grid-cols-[minmax(0,1fr)_21rem]">163 <div className="min-w-0 space-y-10">164 <nav aria-label="On this page" className="flex gap-1 overflow-x-auto border-b border-line pb-px">165 {toc.map(([id, label]) => (166 <a key={id} href={`#${id}`} className="whitespace-nowrap rounded-t-md px-3 py-2 text-sm text-muted hover:text-text">{label}</a>167 ))}168 </nav>169170 <section id="install" aria-labelledby="install-h" className="scroll-mt-24">171 <h2 id="install-h" className="mb-4 text-lg font-semibold tracking-tight">Install</h2>172 {!rel ? (173 <p className="text-sm text-muted">Nothing to install: this artifact has no published release.</p>174 ) : rel.state !== "published" ? (175 <p className="text-sm text-muted">This version cannot be installed because it is {rel.state}.</p>176 ) : (177 <>178 <InstallTabs instructions={instructions} />179 <p className="mt-4 text-xs text-faint">180 Installing only copies or configures. Nothing is executed for you. The <Link className="link" href="/docs/cli">Skills CLI</Link> verifies the checksum before writing files.181 </p>182 </>183 )}184 </section>185186187 {exampleGroups.length > 0 && (188 <Section id="examples" title="Examples" aside={<span className="font-mono text-xs text-faint">{plural(examples.length, "screenshot")} · from the release archive</span>}>189 <ExamplesGallery groups={exampleGroups} />190 <p className="mt-4 text-xs text-faint">Screenshots supplied by the publisher. The registry serves them from the release archive and does not verify what they show.</p>191 </Section>192 )}193194 <section id="readme" aria-labelledby="readme-h" className="scroll-mt-24 border-t border-line pt-8">195 <h2 id="readme-h" className="mb-4 text-lg font-semibold tracking-tight">Documentation</h2>196 {detail?.readme ? (197 <Markdown source={detail.readme} />198 ) : (199 <p className="text-sm text-muted">{rel ? "This release ships no README." : "No release has been published yet, so there is no documentation to show."}</p>200 )}201 </section>202203 {(tools.length > 0 || envVars.length > 0) && (204 <Section id="tools" title="Tools" aside={<span className="font-mono text-xs text-faint">{plural(tools.length, "tool")} · declared by the publisher</span>}>205 {tools.length > 0 && (206 <div className="max-h-[34rem] overflow-auto rounded-xl border border-line" tabIndex={0} role="group" aria-label="Tool list">207 <table className="data-table" data-testid="tools-table">208 <thead className="sticky top-0 bg-graphite"><tr><th scope="col">Tool</th><th scope="col">What it does</th></tr></thead>209 <tbody>210 {tools.map((tl) => (211 <tr key={tl.name}>212 <td className="whitespace-nowrap font-mono text-[12.5px] text-text">{tl.name}</td>213 <td className="text-muted">{tl.description ?? "—"}</td>214 </tr>215 ))}216 </tbody>217 </table>218 </div>219 )}220 {envVars.length > 0 && (221 <div className="mt-5">222 <h3 className="mb-2 text-sm font-medium">Environment variables</h3>223 <div className="overflow-x-auto rounded-xl border border-line">224 <table className="data-table">225 <thead><tr><th scope="col">Variable</th><th scope="col">Required</th><th scope="col">Purpose</th></tr></thead>226 <tbody>227 {envVars.map((e) => (228 <tr key={e.name}>229 <td className="whitespace-nowrap font-mono text-[12.5px] text-text">{e.name}{e.secret && <span className="ml-2 rounded border border-warn/40 px-1 text-[10px] uppercase text-warn">secret</span>}</td>230 <td className="text-muted">{e.required ? "yes" : "optional"}</td>231 <td className="text-muted">{e.description ?? "—"}</td>232 </tr>233 ))}234 </tbody>235 </table>236 </div>237 </div>238 )}239 </Section>240 )}241242 <Section id="files" title="Files" aside={detail ? <span className="font-mono text-xs text-faint">{plural(detail.fileCount, "file")} · {bytes(detail.unpackedBytes)} unpacked</span> : null}>243 {detail ? <FileExplorer namespace={a.namespace} slug={a.slug} version={detail.version} files={detail.files} /> : <p className="text-sm text-muted">No files yet.</p>}244 </Section>245246 <Section id="security" title="Security & permissions">247 <div className="space-y-5 text-sm">248 <div>249 <h3 className="mb-2 font-medium">Declared permissions</h3>250 {perms.length ? (251 <ul className="flex flex-wrap gap-1.5">{perms.map((p) => <li key={p} className="chip text-text">{p}</li>)}</ul>252 ) : (253 <p className="text-muted">{rel ? "The publisher declared no permissions. This is a claim, not a guarantee." : "—"}</p>254 )}255 </div>256 <div>257 <h3 className="mb-2 font-medium">Static checks</h3>258 {!detail?.scan ? (259 <p className="text-muted">No scan result is recorded.</p>260 ) : advisories.length === 0 ? (261 <p className="text-muted">No advisory findings from {detail.scan.scannerVersion} on {date(detail.scan.completedAt)}. {findings.length > 0 && `${plural(findings.length, "informational note")}.`}</p>262 ) : (263 <ul className="divide-y divide-line rounded-lg border border-line">264 {advisories.map((f, i) => (265 <li key={i} className="flex flex-wrap items-baseline gap-x-3 gap-y-1 px-3 py-2">266 <span className={`w-16 shrink-0 font-mono text-[11px] uppercase ${SEV[f.severity]}`}>{f.severity}</span>267 <span className="min-w-0 flex-1 text-muted">{f.message}</span>268 {f.path && <code className="font-mono text-[11px] text-faint">{f.path}{f.line ? `:${f.line}` : ""}</code>}269 </li>270 ))}271 </ul>272 )}273 <p className="mt-3 text-xs text-faint">{t.trust.advisory} Compatibility entries are publisher claims unless marked verified.</p>274 </div>275 {detail && detail.dependencies.length > 0 && (276 <div>277 <h3 className="mb-2 font-medium">Dependencies</h3>278 <ul className="space-y-1">279 {detail.dependencies.map((d) => (280 <li key={d.name} className="font-mono text-[13px]">281 <Link className="link" href={`/${d.name}`}>{d.name}</Link> <span className="text-faint">{d.version}{d.optional ? " · optional" : ""}</span>282 </li>283 ))}284 </ul>285 </div>286 )}287 {detail && detail.compatibility.length > 0 && (288 <div>289 <h3 className="mb-2 font-medium">Compatibility</h3>290 <ul className="flex flex-wrap gap-1.5">291 {detail.compatibility.map((c) => (292 <li key={c.client + c.platform} className="chip">293 {c.client}{c.minVersion ? ` ≥ ${c.minVersion}` : ""}{c.platform !== "any" ? ` · ${c.platform}` : ""} · {c.verified ? "verified" : "claimed"}294 </li>295 ))}296 </ul>297 </div>298 )}299 </div>300 </Section>301302 <Section id="versions" title="Versions" aside={<span className="font-mono text-xs text-faint">{plural(releases.length, "release")} · immutable</span>}>303 {releases.length === 0 ? (304 <p className="text-sm text-muted">No release has been published.</p>305 ) : (306 <div className="overflow-x-auto rounded-xl border border-line">307 <table className="data-table">308 <thead>309 <tr>310 <th scope="col">Version</th>311 <th scope="col">Published</th>312 <th scope="col">Size</th>313 <th scope="col">SHA-256</th>314 <th scope="col">State</th>315 </tr>316 </thead>317 <tbody>318 {releases.map((r) => (319 <tr key={r.id}>320 <td className="font-mono">321 <Link className="link" href={`/@${a.namespace}/${a.slug}/versions/${r.version}`}>{r.version}</Link>322 {r.version === a.latest_version && <span className="ml-2 text-[10px] uppercase text-ok">latest</span>}323 </td>324 <td className="whitespace-nowrap text-muted">{date(r.published_at)}</td>325 <td className="whitespace-nowrap text-muted">{bytes(Number(r.size_bytes))}</td>326 <td className="font-mono text-[11px] text-faint" title={r.sha256}>{r.sha256.slice(0, 12)}…</td>327 <td className="text-muted">{r.state}</td>328 </tr>329 ))}330 </tbody>331 </table>332 </div>333 )}334 {detail?.releaseNotes && (335 <div className="mt-5">336 <h3 className="mb-2 text-sm font-medium">Release notes for v{detail.version}</h3>337 <Markdown source={detail.releaseNotes} />338 </div>339 )}340 </Section>341342 <Section id="history" title="Publication history">343 {history.length === 0 ? (344 <p className="text-sm text-muted">No recorded events.</p>345 ) : (346 <ol className="space-y-2 text-sm">347 {history.map((h, i) => (348 <li key={i} className="flex flex-wrap gap-x-3 text-muted">349 <time className="w-40 shrink-0 font-mono text-[12px] text-faint" dateTime={new Date(h.at).toISOString()}>{dateTime(h.at)}</time>350 <span className="font-mono text-[12px] text-text">{h.action}</span>351 <span className="min-w-0 break-all">{h.target}</span>352 {h.actor && <span className="text-faint">by {h.actor}</span>}353 </li>354 ))}355 </ol>356 )}357 </Section>358 </div>359360 <aside aria-label="Release details" className="space-y-5 lg:sticky lg:top-20 lg:self-start">361 {rel && rel.state === "published" && (362 <div className="card p-5">363 <a className="btn btn-primary w-full" href={releaseDto(a, rel).downloadUrl.replace(/^https?:\/\/[^/]+/, "")} download>364 <Download size={15} aria-hidden /> Download v{rel.version}365 </a>366 <p className="mb-1.5 mt-4 text-xs text-muted">SHA-256 of the archive ({bytes(Number(rel.size_bytes))})</p>367 <Checksum sha256={rel.sha256} />368 <div className="mt-3">369 <CodeBlock prompt label="Verify after downloading" code={`echo "${rel.sha256} ${a.slug}-${rel.version}.tar.gz" | shasum -a 256 -c -`} />370 </div>371 </div>372 )}373 <dl className="card px-5 py-2">374 <Meta label="Type">{TYPE_LABELS[a.type].singular}</Meta>375 <Meta label="Version">{rel ? rel.version : missing}</Meta>376 <Meta label="Published">{rel ? date(rel.published_at) : missing}</Meta>377 <Meta label="Licence">{a.license ?? missing}</Meta>378 <Meta label="Maintainer"><Link className="link" href={`/@${a.namespace}`}>{dto.publisher.displayName ?? `@${a.namespace}`}</Link></Meta>379 <Meta label="Source">380 {a.repository_url ? <a className="link inline-flex items-center gap-1" href={a.repository_url} rel="nofollow noopener noreferrer">repository <ExternalLink size={11} aria-hidden /></a> : missing}381 </Meta>382 <Meta label="Source commit">{rel?.source_commit ? <code className="font-mono text-[12px]">{rel.source_commit.slice(0, 12)}</code> : missing}</Meta>383 <Meta label="Homepage">{a.homepage_url ? <a className="link" href={a.homepage_url} rel="nofollow noopener noreferrer">link</a> : missing}</Meta>384 <Meta label="Issues">{a.issues_url ? <a className="link" href={a.issues_url} rel="nofollow noopener noreferrer">tracker</a> : missing}</Meta>385 <Meta label="Downloads (30 d, est.)"><Link href="/docs/security#download-counts" className="link">{dto.downloads30d.toLocaleString("en-CA")}</Link></Meta>386 </dl>387 {a.keywords.length > 0 && (388 <ul className="flex flex-wrap gap-1.5" aria-label="Keywords">389 {a.keywords.map((k) => (390 <li key={k}><Link href={`/explore?keyword=${k}`} className="chip hover:border-line-strong hover:text-text">#{k}</Link></li>391 ))}392 </ul>393 )}394 <div className="card p-5 text-sm">395 <p className="eyebrow mb-2">API</p>396 <code className="block break-all font-mono text-[12px] text-muted">GET /api/v1/artifacts/{a.namespace}/{a.slug}</code>397 <p className="mt-3 flex flex-wrap gap-x-4 gap-y-1">398 <a className="link" href={`/api/v1/artifacts/${a.namespace}/${a.slug}`}>metadata JSON</a>399 {rel && <a className="link" href={`/api/v1/artifacts/${a.namespace}/${a.slug}/versions/${rel.version}`}>release JSON</a>}400 </p>401 </div>402 <Link href={`/report?artifact=${encodeURIComponent(name)}${rel ? `&version=${rel.version}` : ""}`} className="inline-flex items-center gap-2 text-sm text-muted hover:text-text">403 <Flag size={13} aria-hidden /> Report a problem with this artifact404 </Link>405 </aside>406 </div>407408 {relatedItems.length > 0 && (409 <section aria-labelledby="related-h" className="mt-16 border-t border-line pt-8">410 <h2 id="related-h" className="mb-4 text-lg font-semibold tracking-tight">More {TYPE_LABELS[a.type].plural.toLowerCase()}</h2>411 <ul className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">{relatedItems.map((r) => <li key={r.id}><ArtifactCard a={r} /></li>)}</ul>412 </section>413 )}414 </div>415 );416}417