From 8d1b2b5df1d0db58d63d4b84f8b19bff3674e33d Mon Sep 17 00:00:00 2001 From: Matt Kane Date: Sun, 31 May 2026 11:57:11 +0100 Subject: [PATCH 1/5] feat(registry): release env requires + admin compatibility gate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plugins published to the experimental registry can declare release-level environment constraints. A manifest's `requires` block (e.g. `{ "env:emdash": ">=1.0.0", "env:astro": ">=4.16" }`) is validated at publish time and written into the release record. - registry-client: new dependency-free `./env` module shared by the CLI, server, and admin — `parseRequires` (guards the lexicon-`unknown` value), `isValidVersionRange`, `satisfiesRange`, and `checkEnvCompatibility` over a focused semver-range grammar (comparators, caret, tilde, partial versions, wildcard, AND sets). - plugin-cli: `RequiresSchema` on the manifest, threaded release-level into `publishRelease` (never via the profile input); JSON Schema regenerated. - core: capture the host Astro version in `astro:config:setup` and surface it (with EmDash VERSION) on the admin manifest. New `assertEnvCompatible` gate refuses incompatible install AND update with `ENV_INCOMPATIBLE` (409), placed after yank-check, before the artifact fetch. - admin: RegistryPluginDetail reads host versions, renders a localized compatibility warning and disables Install when unsatisfied. Closes #1031. --- .changeset/registry-env-requires.md | 8 + .../src/components/RegistryPluginDetail.tsx | 61 +++- packages/admin/src/lib/api/client.ts | 2 + packages/admin/src/lib/api/registry.ts | 21 ++ packages/core/src/api/errors.ts | 2 + packages/core/src/api/handlers/index.ts | 2 + packages/core/src/api/handlers/registry.ts | 76 +++- packages/core/src/astro/integration/index.ts | 26 ++ .../core/src/astro/integration/runtime.ts | 8 + .../api/admin/plugins/registry/[id]/update.ts | 5 +- .../api/admin/plugins/registry/install.ts | 9 +- packages/core/src/astro/types.ts | 6 + packages/core/src/emdash-runtime.ts | 1 + .../unit/api/registry-env-compat.test.ts | 78 ++++ .../schemas/emdash-plugin.schema.json | 27 ++ packages/plugin-cli/src/commands/publish.ts | 1 + packages/plugin-cli/src/manifest/schema.ts | 38 ++ packages/plugin-cli/src/manifest/translate.ts | 8 + packages/plugin-cli/src/publish/api.ts | 11 + .../plugin-cli/tests/manifest-schema.test.ts | 54 +++ .../tests/manifest-translate.test.ts | 27 ++ packages/plugin-cli/tests/publish.test.ts | 22 ++ packages/registry-client/package.json | 4 + packages/registry-client/src/env/index.ts | 334 ++++++++++++++++++ packages/registry-client/src/index.ts | 9 + packages/registry-client/tests/env.test.ts | 160 +++++++++ packages/registry-client/tsdown.config.ts | 1 + 27 files changed, 996 insertions(+), 5 deletions(-) create mode 100644 .changeset/registry-env-requires.md create mode 100644 packages/core/tests/unit/api/registry-env-compat.test.ts create mode 100644 packages/registry-client/src/env/index.ts create mode 100644 packages/registry-client/tests/env.test.ts 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 f0cd9260a..41b75f2c1 100644 --- a/packages/admin/src/components/RegistryPluginDetail.tsx +++ b/packages/admin/src/components/RegistryPluginDetail.tsx @@ -14,6 +14,7 @@ */ 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"; @@ -22,6 +23,7 @@ import * as React from "react"; import { canonicalCapabilitiesForDriftCheck, + fetchHostEnv, getRegistryPackage, installRegistryPlugin, listRegistryReleases, @@ -59,6 +61,13 @@ 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. + const { data: hostEnv } = useQuery({ + queryKey: ["host-env"], + queryFn: fetchHostEnv, + }); + // 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 `/`), @@ -211,6 +220,19 @@ 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. Empty while the host env is still loading (fail-open until + // the data arrives; the server gate is the authority either way). + const envMismatches = React.useMemo(() => { + if (!release || !hostEnv) 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: @@ -367,7 +389,7 @@ export function RegistryPluginDetail({ pluginId, config }: RegistryPluginDetailP ) : (