npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@elytracms/cli

v0.0.101

Published

The Elytra CLI — run a consuming repo's studio, frontend, and Convex backend under one command.

Readme

@elytracms/cli

The Elytra CLI — run an Elytra consuming repo's studio, delivery frontend, and Convex backend under one command.

Installed as a devDependency in projects scaffolded by create-elytra; its dev/build scripts call elytra.

Commands

elytra dev      Start the studio + frontend (and Convex in connected mode)
                together as one supervised process group. One Ctrl-C stops
                everything; if any process exits, the rest are torn down.

elytra build    Build the studio + frontend (and deploy Convex in connected
                mode), in order — failing as a whole if any step fails.
                Optional target (the per-app Vercel build commands):
                  elytra build studio     Convex deploy coupled with the studio
                                          build (functions + client in lockstep)
                  elytra build frontend   frontend build only

elytra init     First-run setup: provision Convex, generate the auth signing
                keys, verify readiness. Idempotent — safe to re-run.

elytra doctor   Diagnose the active deployment's readiness and print each fix.
                Includes the expected deployment env vars, reported set/missing
                by NAME only — values are never read into the report or printed.
                When the publish revalidate pair is set, also sends a signed
                no-op ping to the endpoint to prove the publish → live chain
                end to end. Also checks your cms/ route set for URL conflicts
                (see below), and reports the generated-previews artifacts
                (manifest present, image files present, no strays — warnings
                only; previews are optional). Read-only; exits non-zero if any
                check fails.

elytra previews Render every canvas-eligible component's fixtures against your
                theme and write committed insert-menu preview artifacts:
                640×400 WebP images in studio/public/elytra-previews/ plus an
                importable studio/elytra-previews.json manifest (per-entry
                input hashes — re-running with unchanged inputs rewrites
                nothing; removed components' images are pruned). Wire it with
                `generatedPreviewsFromManifest` from
                `@elytracms/studio/customization` (two lines — see the docs).
                Rendering needs Playwright once (optional, on demand), at
                the workspace root:
                  pnpm add -Dw playwright && pnpm exec playwright install chromium
                Options:
                  --check     verify the input hashes without rendering (no
                              Playwright needed); exits non-zero when previews
                              are missing/stale/orphaned. Wire into CI/builds.
                  --port <n>  harness dev-server port (default: first free 5440+)

elytra help     Show help
elytra version  Show version

Run from anywhere inside an Elytra project (a directory with elytra.config.ts).

Every subcommand accepts --help/-h and prints its own usage — inertly: no project inspection, no process spawned, no network call, no write. elytra dev --help never starts a server; elytra doctor --help never runs the network diagnostic; elytra build --help never validates a target.

Project layout

The studio app, the frontend app and the theme are found at studio/, frontend/ and components/theme.css — the scaffold layout — unless the repo-root package.json (beside elytra.config.ts) declares otherwise:

{ "elytra": { "studio": "./apps/editor", "frontend": "./apps/site", "theme": "./styles/site.css" } }

Every key is optional. An invalid section or a declared path that does not exist is reported (elytra doctor fails project-layout; the other commands refuse to start) — never silently replaced by the default. Full rules: docs/guide/06-cli-reference.md.

Mode-aware

The CLI reads the studio app's env files — the ones Vite loads for it (.env, .env.local, .env.<mode>, .env.<mode>.local) — through the SAME backend-mode parser the studio runs, so the two never disagree:

  • Playground (VITE_ELYTRA_MODE=playground) — runs the studio + frontend only; no Convex.
  • Connected (VITE_CONVEX_URL set) — also runs convex dev (or deploys on build) once a deployment is provisioned (CONVEX_DEPLOYMENT present).
  • Any other VITE_ELYTRA_MODE value is the studio's boot error, and the CLI names it the same way.

It orchestrates and owns the process lifecycle; it does not reimplement Vite, Next, or Convex — each step shells out to the underlying tool and inherits its watch/HMR/build behaviour.

Dev ports

The studio defaults to :5180, the frontend to :3000. One lever moves both together — set ELYTRA_DEV_PORT_OFFSET in studio/.env (e.g. 100 → studio :5280, frontend :3100) to run a second Elytra project alongside this one without a clash. elytra dev reads it from studio/.env and forwards it to both child processes; the studio's own vite.config.ts also reads it directly (so vite dev run standalone honors it too). Unset (the default) leaves both ports exactly as they are today.

Deploying (Vercel)

Deploys are git-push-driven — there is no elytra deploy. A consuming repo deploys as two Vercel projects on the same git repo, plus the Convex deployment that the studio build pushes functions to:

| Vercel project | Root Directory | Build command | | -------------- | -------------- | --------------------------------- | | studio | studio | pnpm exec elytra build studio | | frontend | frontend | pnpm exec elytra build frontend |

elytra build studio wraps Convex's recommended Vercel integration: it runs convex deploy --cmd 'pnpm run build', which pushes your Convex functions and then runs the studio build with the deployment URL injected into its env (VITE_CONVEX_URL). The built studio always matches the functions it was deployed with, and there is no hand-set URL to drift out of sync with the deploy key's target. elytra build frontend runs the Next.js build only — the frontend reads its Convex URL at runtime, not at build time.

Which env lives on which plane

Env lives on three planes. Keep them apart — most misconfiguration is a value set on the wrong plane:

  1. Local .env files (studio/.env, frontend/.env) — local development only. Gitignored; they never reach Vercel or the Convex deployment.
  2. Vercel project env — build- and runtime-env per Vercel project:
    • Studio project: CONVEX_DEPLOY_KEY (a production deploy key generated in the Convex dashboard — it selects and authorizes the deployment). Do not hand-set VITE_CONVEX_URL; the build injects it (above). Optional: the AI provider key (ANTHROPIC_API_KEY, or the Vercel AI Gateway) for the studio's AI route — a host/server env, never VITE_-prefixed.
    • Frontend project: ELYTRA_CONVEX_URL, ELYTRA_PROJECT_ID, REVALIDATE_SECRET, DRAFT_PREVIEW_TOKEN — see the scaffold's CONNECT.md for what each pairs with.
  3. Convex deployment env — set with npx convex env set (from studio/); lives on the deployment itself, visible in the Convex dashboard but in no file: the auth signing keys (JWT_PRIVATE_KEY, JWKS — set via npx @convex-dev/auth), SITE_URL, the instant-publish pair CANVAS_REVALIDATE_ENDPOINT / CANVAS_REVALIDATE_SECRET, and DRAFT_PREVIEW_TOKEN (the secret and the token must match their frontend-plane counterparts). elytra doctor diagnoses this plane's readiness.

Because plane 3 lives in no file, it is the easiest to overlook — elytra doctor therefore enumerates the expected vars there (auth keys, SITE_URL, the instant-publish pair, the draft-preview token, plus the AI provider key on the studio-host plane when the AI route is present) and reports each one set or missing, by name only: the doctor parses names out of npx convex env list and discards every value on the spot — values are never stored, printed, or logged. A missing required var (the auth keys) fails the run with its exact fix line; a missing recommended var (SITE_URL, the AI key, the draft-preview token) is a named warning that doesn't change the exit code. The studio's boot-time "Deployment setup" screen lists the same expected vars, so the CLI and the studio tell one story.

The instant-publish pair — CANVAS_REVALIDATE_ENDPOINT and CANVAS_REVALIDATE_SECRET — gets special treatment, because the publish path silently no-ops while either is unset: the write-chokepoint trigger schedules the revalidate webhook, and the action delivers it, only while both are set. Publishes then never update the live site until a redeploy, with no error anywhere. Both missing is a warning carrying exactly that consequence (a project without a delivery frontend may legitimately leave them unset); one of the two set fails the run, because a half-configured pair has no legitimate reading (documented together in the scaffold's CONNECT.md, set in one shell block, so a partial state is far more likely an interrupted setup than a deliberate choice). When the pair IS set, the doctor goes one step further and proves the chain end to end: it POSTs a signed no-op ping (the probe event — the one event that derives no cache tags) to the endpoint. A 200 {"revalidated":[]} passes (endpoint reachable, the frontend's REVALIDATE_SECRET matches, and the frontend speaks this engine's event wire); a 401 fails as a secret mismatch between Convex and the frontend host; a 400 fails as a version skew (the frontend runs a different Elytra version); a network error only warns (unreachable from your machine is not the same as broken — Convex may still reach it). The pair's values are read in-process for the ping only and never printed. (There is no third, scope var any more: the webhook carries the event, and the frontend derives its cache tags with its own ELYTRA_PROJECT_ID.)

The draft-preview token — DRAFT_PREVIEW_TOKEN — gates the deployment's draftSnapshot query and must equal the frontend host's own DRAFT_PREVIEW_TOKEN (the same secret that gates the frontend's preview-enable route); the doctor can only see the Convex-deployment side; it has no pairWith escalation because its real partner lives on a plane this doctor cannot read. Missing it is a warning: draft preview is closed everywhere (401 on both sides) until it's set — a project that never uses draft preview may legitimately leave it unset.

Route conflicts (elytra doctor)

Routes are code, so a route conflict is a build-time bug, and elytra doctor reports it: two routes that would match the exact same URLs fail the run with the offending ids and patterns named. Nothing about this needs a deployment — the check runs in playground mode too.

When the studio's deps are installed, the doctor executes elytra.config.ts through the studio's own Vite and judges the routes it resolved — exactly what the studio and delivery serve. Without them it falls back to reading your project's cms/index.ts natively (the collections + routes the scaffold's elytra.config.ts passes through), and the route-conflicts row says which of the two it judged. That tree is plain, React-free TypeScript by convention, so the CLI imports it natively — no bundler, no Vite, no dev server. Two consequences of the fallback worth knowing:

  • It needs a Node that runs TypeScript natively (≥22.18). On an older Node the check reports "not checked here" and names the version — never a false pass.
  • If the route set cannot be read for any reason (no cms/index.ts, a module that throws, an import the CLI cannot resolve such as a tsconfig path alias, TypeScript syntax Node cannot strip, or a routes export of the wrong shape), the check reports why, as "not checked here". It never crashes the doctor and never claims your routes are fine.

Conflicting hierarchy mounts

A hierarchy mount (/:path*, /docs/:path*) is not a router route: it is pulled out of the router and resolved per request against a collection's composed page tree. So the route-conflict check above cannot see mounts at all, and a second row — mount-conflicts — judges them by what they declare instead:

  • Two collections mounted at the same path fail the run. They claim an identical URL space, and the only thing separating them is declaration order.
  • A mount nested inside another's URL space is a warning. It is a real shadow, but only where the outer collection's tree actually composes a path into the inner mount's prefix — that is content, which a CLI cannot see, so naming the precedence is honest and failing your build on it would not be.

The precedence being named is worth stating plainly, because nothing else does: mounts are tried in declaration order, not by specificity, and the first whose collection resolves a document for that URL wins. A root mount declared first therefore outranks a deeper one — the opposite of how ordinary routes behave, where more static segments always win. Declare the more specific mount first.

Declarations that serve nothing

A third row — unreachable-routes — asks a different question from the two above: not "do two declarations fight?" but "can this declaration ever serve anything at all?" Three shapes trip it, all of which used to 404 silently with no issue raised anywhere:

  • An all-dynamic route over a mount's URL space — a warning. /:slug matches every one-segment URL, so the /:path* mount is consulted there only where the route resolves no document. Nothing is unconditionally unreachable (the resolver falls through — see Precedence below), but precedence at that depth is then decided by your content: a page whose composed path collides with a post slug is silently shadowed, and neither side is told. /blog/:slug is fine and always was: a static segment claims only the namespace you carved out. It is the all-dynamic pattern that takes a whole depth.
  • A mount on a collection that cannot back it — no parent self-relation, so there are no nested paths to compose, or the collection is not declared at all. The resolver skips such a mount silently; every URL under it 404s. This one fails the run: no content and no ordering can make it work.
  • A catch-all with a dynamic prefix (/:lang/:path*) — also a failure. It is dead in both directions: every router drops it as a catch-all, and no mount is built from it because a mount prefix must be static. For per-locale mounts, declare one mount per locale instead: { pattern: '/de/:path*', locale: 'de' }.

Precedence: a match is not a result

A route always wins over a mount for the URLs it can actually serve. When a route matches a URL but resolves no document there — no post with that slug, or none published in this perspective — the resolver falls through to the hierarchy mounts instead of returning 404. So declaring /blog/:slug does not take the /blog/* namespace away from your page tree: a page at blog/kid is served, and it stops being served the moment a post with slug kid exists.

The consequence worth knowing: which collection serves such a URL depends on what exists, not on the routes alone, and it is evaluated per perspective — a draft-only post wins in preview while the published page still serves live.

Redirect loops are not checked here. Since EC-303 redirects are editorial data in the backend, not code — a CLI has no editorial data. The studio checks them (and route conflicts, and the route-side half of unreachable-routes) live, at the section-rail foot, because it holds both the config routes and the redirects rail.