diff --git a/.changeset/registry-env-requires.md b/.changeset/registry-env-requires.md new file mode 100644 index 000000000..b8988076e --- /dev/null +++ b/.changeset/registry-env-requires.md @@ -0,0 +1,8 @@ +--- +"@emdash-cms/registry-client": minor +"@emdash-cms/plugin-cli": minor +"@emdash-cms/admin": minor +"emdash": minor +--- + +Registry plugins can now declare environment requirements. A plugin's manifest may set a release-level `requires` block (e.g. `{ "env:emdash": ">=1.0.0", "env:astro": ">=4.16" }`), which is published into the release record. When browsing a registry plugin, the admin compares those constraints against the running EmDash and Astro versions: if the host doesn't satisfy them, it shows a compatibility warning and disables the Install button. The server enforces the same check on install and update, refusing an incompatible release with `ENV_INCOMPATIBLE` so the gate can't be bypassed. diff --git a/packages/admin/src/components/RegistryPluginDetail.tsx b/packages/admin/src/components/RegistryPluginDetail.tsx index 028359cef..a8bdb7894 100644 --- a/packages/admin/src/components/RegistryPluginDetail.tsx +++ b/packages/admin/src/components/RegistryPluginDetail.tsx @@ -14,17 +14,20 @@ */ import { Badge, Button, LinkButton, Select } from "@cloudflare/kumo"; +import { checkEnvCompatibility } from "@emdash-cms/registry-client/env"; import { useLingui } from "@lingui/react/macro"; import { ShieldCheck, Warning } from "@phosphor-icons/react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Link } from "@tanstack/react-router"; import * as React from "react"; +import { fetchManifest } from "../lib/api/client.js"; import { artifactProxyUrl, canonicalCapabilitiesForDriftCheck, extractMediaArtifacts, getRegistryPackage, + hostEnvFromManifest, installRegistryPlugin, listRegistryReleases, releasePassesPolicy, @@ -61,6 +64,16 @@ export function RegistryPluginDetail({ pluginId, config }: RegistryPluginDetailP }, }); + // Host environment versions (`env:emdash`, `env:astro`) — used to evaluate + // the selected release's `requires` constraints before offering install. + // Derived from the admin manifest the shell already fetches under the same + // query key, so this view adds no extra round-trip. + const { data: manifest } = useQuery({ + queryKey: ["manifest"], + queryFn: fetchManifest, + }); + const hostEnv = React.useMemo(() => hostEnvFromManifest(manifest), [manifest]); + // Parse `/` out of the route param. The publisher // segment is either a handle (`example.dev`) or a DID // (`did:plc:abc...`). Slugs are `[A-Za-z][A-Za-z0-9_-]*` (no `/`), @@ -242,6 +255,20 @@ export function RegistryPluginDetail({ pluginId, config }: RegistryPluginDetailP const policyOk = release && pkg ? releasePassesPolicy(release, { did: pkg.did, slug }, config.policy) : true; + + // Environment compatibility: compare the selected release's `requires` + // constraints against the running host. `requires` is the lexicon's open + // `unknown` value; `checkEnvCompatibility` guards its shape. Mirrors the + // server-side install gate so the admin can't offer an install the server + // would reject. While the manifest is still loading `hostEnv` is empty, so + // every constraint is skipped (fail-open until the data arrives; the server + // gate is the authority either way). + const envMismatches = React.useMemo(() => { + if (!release) return []; + return checkEnvCompatibility(release.release?.requires, hostEnv); + }, [release, hostEnv]); + const envOk = envMismatches.length === 0; + // Handle resolution affects display only -- installs are addressed // by DID, so an unverified or missing handle doesn't block install. // A handle that *claims* a value but doesn't verify (`status: @@ -417,7 +444,7 @@ export function RegistryPluginDetail({ pluginId, config }: RegistryPluginDetailP ) : (