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

@headroom-cms/cli

v0.3.1

Published

Command-line interface for managing Headroom CMS sites

Readme

@headroom-cms/cli

Command-line interface for managing Headroom CMS sites.

Install

The CLI is published to npm and can be invoked with npx without installing:

npx @headroom-cms/cli --help

Or install globally:

npm install -g @headroom-cms/cli
headroom --help

Authenticate

headroom login accepts either a deployed admin UI URL or a Lambda function / API URL. With an admin URL, the CLI auto-discovers the API URL by fetching /.well-known/headroom.json published by the admin site.

# Recommended: paste the admin URL
headroom login https://headroom-admin.example.com

# Or the API URL directly (legacy form)
headroom login https://xxx.lambda-url.us-east-1.on.aws

# Set the active site at the same time
headroom login https://headroom-admin.example.com mysite.com

A bare hostname is auto-prefixed with https:// (or http:// for localhost / 127.0.0.1 / *.local).

Bootstrap your project

headroom bootstrap adds Headroom to an existing Astro or Next.js project. Run it from the project root:

cd ./my-astro-site
npx @headroom-cms/cli bootstrap

What it does:

  1. Detects the framework (next / astro / generic) and package manager (npm / pnpm / yarn / bun).
  2. Resolves the API + media URLs from your active login.
  3. Prompts you to pick (or create) the site to bootstrap against.
  4. Creates an API key on the site (label format bootstrap-{framework}-{YYYYMMDD}-{shortid}).
  5. Writes HEADROOM_API_URL, HEADROOM_MEDIA_URL, HEADROOM_SITE, HEADROOM_API_KEY to .env (.env.local for Next.js).
  6. Adds the env file to .gitignore if missing.
  7. Installs @headroom-cms/api with your detected package manager.
  8. Drops HEADROOM_AGENTS.md at the project root — an instruction sheet for AI agents working in the repo.

Idempotency

Re-running bootstrap is safe:

  • The existing HEADROOM_API_KEY is reused when its hash is still recognized by the server. No new key is created.
  • HEADROOM_AGENTS.md is never overwritten — delete it first if you want it regenerated.

To explicitly rotate the API key:

npx @headroom-cms/cli bootstrap --rotate-key --yes

Non-interactive mode (CI)

# Use a specific site without prompts; create it if missing
npx @headroom-cms/cli bootstrap --site mysite.com --create-site --yes

Other useful flags:

  • --framework next|astro|generic — override detection
  • --env-file <path> — write to a custom env file
  • --skip-install — leave package.json alone
  • --skip-agents-md — don't write HEADROOM_AGENTS.md

Admin commands

Super-admin maintenance commands live under headroom admin. These talk to endpoints gated by the installation's super-admin allow-list — most users will never need them.

restore-from-file — Restore a backup from a different installation

Use case: you downloaded an archive from installation A and want to recreate that site on installation B (a staging clone, a dev environment, a one-off test bed).

The target host MUST NOT already exist on this installation. To refresh an existing site, delete it first or use the in-UI restore flow. Against a fresh installation with no super-admin yet, the first caller of this command becomes the new super-admin (bootstrap fallback — matches headroom sites create).

headroom admin restore-from-file ./prod-site-2026-04-01T120000Z.tar.gz \
  --target-host staging-clone.local

You'll be prompted for confirmation showing the source host + record counts

  • what's being copied. The caller becomes the sole admin of the new site.

Defaults:

  • --include-site-users (audience PII; default ON — appropriate for dev/test clones. Pass --no-site-users to skip.)
  • --include-api-keys (default OFF — keys from source installation are stripped. Pass --include-api-keys to preserve.)
  • --include-audit (default OFF.)
  • --yes skips the confirmation prompt.

After a successful restore, the CLI prints a headroom admin backups delete command for the uploaded archive — it sits in the destination's S3 bucket alongside native backups. Run that command once you no longer need it.

Resolved reads (--resolved / --public)

headroom content get <id> returns the admin projection: blocks live behind a publishedBlockId reference, so fields.body looks empty. The output carries a _hint reminding you how to see the runtime view.

Pass --resolved (alias --public) to read the content through the public endpoint instead — the same shape the SDK sees at runtime, with resolved blocks inlined at fields.body:

headroom content get <id> --resolved

--resolved exercises the API-key-gated public read endpoint, so it needs a per-site API key. The CLI resolves it from the HEADROOM_API_KEY environment variable, or from HEADROOM_API_KEY= in your consumer project's .env (the file headroom bootstrap writes). Without one, the command exits non-zero with a NO_API_KEY error telling you to run headroom bootstrap or set HEADROOM_API_KEY.

--resolved is the narrow precursor to the cdn command tree, which exercises the full public CDN API (not just content-by-id) with the same API-key auth. Reach for headroom cdn content get <id> when you want the public projection as a first-class command.

CDN commands

headroom cdn … drives the public CDN API (/v1/{site}/..., gated by the X-Headroom-Key header) — the same surface the published @headroom-cms/api SDK uses at runtime. This is independent of the admin (Cognito) login: the CLI works with either auth mode configured, or both.

  • Admin commands (sites, content, collections, …) need an admin login (headroom login, Cognito JWT).
  • cdn commands need only a CDN API key — no admin login required. They are the right tool for read-only consumers, CI smoke tests, and exercising the exact path production sites depend on.

API-key precedence

The CDN API key is resolved with strict precedence (highest first):

| Rung | Source | Set by | |------|--------|--------| | 1 | --api-key <key> | per-command flag | | 2 | HEADROOM_API_KEY | environment variable | | 3 | .headroom/config.json apiKeys[<site>] | headroom cdn login | | 4 | <consumer project>/.env HEADROOM_API_KEY=… | headroom bootstrap |

whoami reports which rung resolved (under cdn.apiKeySource) and doctor probes the key against the live CDN — neither ever prints the key itself. CDN auth is optional, so doctor skips (does not fail) the CDN check when no key resolves.

The other CDN coordinates follow the same flag > env > config precedence:

apiUrl    : --api-url   > HEADROOM_API_URL   > config.apiUrl
site      : --site      > HEADROOM_SITE      > config.activeSite
mediaUrl  : --media-url > HEADROOM_MEDIA_URL > config.mediaUrl

mediaUrl is required even for media-free reads because the SDK constructor enforces it.

Persist a key (optional)

For interactive use you can stash a per-site key in .headroom/config.json so you don't re-export it every shell. Scripts that already set HEADROOM_* env vars don't need this.

headroom cdn login --site mysite.com            # prompts for the key
headroom cdn login --site mysite.com --api-key sk_…
headroom cdn logout --site mysite.com           # removes the stored key

Commands

# Read published content
headroom cdn content list      --collection <name> [--select <s>] [--limit <n>]
                               [--cursor <c>] [--sort <s>] [--before <ts>]
                               [--after <ts>] [--related-to <id>] [--rel-field <name>]
headroom cdn content get       <contentId>
headroom cdn content by-slug   <collection> <slug>
headroom cdn content singleton <collection>
headroom cdn content batch     <id...> [--select <s>]

# Read schemas / metadata
headroom cdn collections list
headroom cdn collections get   <name>
headroom cdn block-types list
headroom cdn version                       # site content version (raw)
headroom cdn openapi                       # the site's openapi.json

# Submission-collection content (write surface)
headroom cdn submissions list   --collection <name> [--status approved|pending]
                                [--select <s>] [--limit <n>] [--cursor <c>] [--sort <s>]
headroom cdn submissions get    <contentId> [--select <s>]
headroom cdn submissions batch  <id...> [--select <s>]
headroom cdn submissions create --collection <name> --data <json> [--session <token>]
headroom cdn submissions update <contentId>       --data <json> [--session <token>]

# Site-user OTP auth (email → session token)
headroom cdn auth request-otp <email>
headroom cdn auth verify-otp  <email> <code>     # prints { token, expiresAt, user }
headroom cdn auth session     --session <token>

# Optional persistent key store
headroom cdn login  [--site <host>] [--api-key <key>]
headroom cdn logout [--site <host>]

All cdn read commands print JSON by default and honor --table / --csv / -q, so jq over the output sees the SDK shape verbatim.

Singletons

A singleton collection holds exactly one content row. Resolve it in one command:

headroom content singleton get <collection>
headroom content singleton get <collection> --resolved   # inline blocks

Resolution order:

  1. If the collection isn't a singleton, the command errors (COLLECTION_NOT_SINGLETON).
  2. If the collection carries a singletonContentId, that row is fetched directly.
  3. Otherwise the command lists the collection's content and returns the published row (falling back to the most recently edited row). If more than one row exists — orphans left by re-running collections create for a singleton — it prints a warning to stderr and still returns the canonical row.

The canonical accessor for the singleton's id remains collections get <collection>, whose output includes singletonContentId. (That field is deliberately stripped from config export / config.json, which is schema-only — it's server-managed, not schema-as-code.)

Cleaning up orphan rows

Re-running collections create for a singleton mints a fresh row, leaving the old one behind as an unpublished orphan. To remove them in one pass — keeping only the published row:

headroom content singleton cleanup <collection> --force

Only unpublished rows are deleted; the published singleton row is protected (the API returns 409 if you try to delete it directly). After a cleanup run, content list --collection <collection> returns exactly one row.

See also the Field Types reference for the collection field vocabulary and the params.body wire shape used by content create / content draft.

Schema-as-code workflow

headroom config export pulls the live collections + block types into a schema-only config.json; config diff <file> shows how a file differs from live; config import <file> applies a file's changes back to the site. The canonical edit-then-regenerate loop is:

1. Edit config.json
2. headroom config import        # apply locally-authored changes
3. headroom config diff          # confirm "no changes"
4. headroom config export        # re-pull from live (or your wrapper
                                 # script, e.g. pnpm headroom:types)

Order matters. config export re-pulls the live schema, so running it (or any wrapper such as pnpm headroom:types, which runs config export first) before importing your local edits would overwrite them with the old live schema — the new field never generates and you don't know why.

To prevent that silent revert, config export guards a dirty local file: when --output <file> points at a non-empty config.json that differs from the current live schema, the command aborts with a DIRTY_LOCAL_CONFIG error telling you to config import first. Pass --overwrite-local to skip the guard (back-compat for CI that drives codegen from live). A missing or empty target file is the normal first-time bootstrap path and is never flagged.

Generated bindings (and config export output) sort collections and fields alphabetically, so config.json insertion order is immaterial — see the Field Types reference (bottom note) for details. (singletonContentId is server-managed and deliberately stripped from config export / config.json.)

Generate typed schemas (codegen)

headroom codegen writes a TypeScript file of Zod schemas for the site's collections plus a discriminated Block union — one member per built-in block type and per custom block type the site defines, keyed on type. Point your content-loading code at the emitted file to get compile-time narrowing of block.props (including props.data.* for custom blocks).

# Writes ./headroom.generated.ts for the active site
headroom codegen

# Explicit site + output path + plain `zod` import (default is "astro/zod")
headroom codegen --site mysite.com --out src/headroom.generated.ts --zod-import zod

| Flag | Default | Effect | |------|---------|--------| | --site <host> | active site | Site to generate for. Resolves --site then config.activeSite; does not read HEADROOM_SITE (this is an admin-scoped command — see below). | | --out <path> | headroom.generated.ts | Output file (relative to cwd). | | --zod-import <src> | astro/zod | Import source for z — use zod for a plain Zod dependency. |

Auth. codegen resolves its site by the admin rule (--site > config.activeSite), but the per-site block union it reads (GET /v1/{site}/openapi.json) is API-key gated, so an API key must also be resolvable (--api-key > HEADROOM_API_KEY > the cdn login key store > the consumer project's .env, same precedence as the cdn commands). Run headroom login (for the site/URL config) and have a key available. Unlike the other cdn commands, codegen does not require a media URL — it reads only text endpoints, so an admin who never ran headroom bootstrap can still run it.

Narrowing custom-block props. The emitted union appends an open UnknownBlock escape-hatch member (so a stale cache or a newer block type degrades gracefully instead of failing to type-check). Because UnknownBlock.type is a bare string, a bare switch (block.type) guard narrows to Member | UnknownBlock — and UnknownBlock has no typed props.data. For deep custom-block prop access, type against the member interface directly (e.g. CallToActionBlock["props"]["data"]["text"]) or use the SDK's BlockComponentProps<Member> helper (@headroom-cms/api/react), which reads block.props.data.* fully typed.

Concurrent-editing safety

content draft and content publish are unconditional by default — an agent write during a live editing session silently clobbers (or is clobbered by) the editor's next autosave. Two opt-in guards make sessionless writes concurrency-aware. They are recommended for agents editing content a human might also edit; omit them for bulk seeding where you are the only writer.

| Flag | Commands | Effect | |------|----------|--------| | --expected-updated-at <ms> | content draft | Conditions the write on the draft's updatedAt (Unix ms — the value content get returns as draft.updatedAt). Mismatch → 409 STALE_WRITE. | | --respect-lock | content draft, content publish | Rejects the write if a live edit lock exists (a human has the content open in the admin UI) → 409 LOCK_HELD. |

The recommended agent flow is read-modify-write:

# 1. Read the current draft — note draft.updatedAt (the concurrency base stamp).
headroom content get <id>

# 2. Write with both guards.
headroom content draft <id> --data '…' \
  --expected-updated-at <draft.updatedAt> --respect-lock

# 3. On STALE_WRITE: re-run step 1, merge, retry with the new stamp.
#    On LOCK_HELD: a human is editing — back off and retry later.

Neither flag set = today's unconditional write (existing behavior; safe for seed scripts and migrations). Combining either guard with a sessionId/generation (the admin UI's own machinery) is rejected with a 400 — the guards are sessionless-only.

Conflict codes

On a 409 the CLI writes a machine-readable JSON envelope to stderr — code plus the structured recovery field, and a human hint line beside it:

  • code: "STALE_WRITE" — the draft changed since your --expected-updated-at base. The envelope carries currentUpdatedAt as a top-level field (the row's current stamp, 0 if no draft exists yet). Re-read with content get, merge, retry with the new stamp.
  • code: "LOCK_HELD" — a live edit lock exists. The envelope carries lockHolder as a top-level field ({ name?, email? }). Back off and retry later, or omit --respect-lock to override.

Configuration

The CLI keeps a per-repo config in .headroom/config.json (at the git root). Refresh tokens live in .headroom/tokens/*.json with mode 0o600. The bootstrap command also drops .headroom/.gitignore (*) so contents are never committed.

Tips

  • All commands output JSON by default (pipe to jq for filtering).
  • Use --table for human-readable output. Nested objects/arrays render as JSON strings (no more [object Object]).
  • Use --csv to emit CSV (list commands only; nested objects render as JSON strings). For single-object commands (e.g. headroom sites get), --csv falls through to JSON silently.
  • The CLI auto-paginates when --limit exceeds the per-endpoint cap (100 for admin endpoints). For example, headroom content list --limit 500 makes five paginated requests under the hood and returns all 500 (or fewer if exhausted). Set --quiet to suppress the "(more results available)" notice.
  • When editing content a human might also have open, guard your writes — see Concurrent-editing safety for the --expected-updated-at / --respect-lock flags and the STALE_WRITE / LOCK_HELD conflict codes.

See also

  • headroom doctor — diagnose API reachability, auth, and API-key validity
  • headroom whoami — show the active session
  • headroom collections list --table — discover the schemas
  • headroom content list --collection posts — read content
  • Field Types reference — every field type, its options.*, the generated TS/Zod shape, and when to use each (and when not to reach for json) when editing collection schemas