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

@bettercms-ai/cli

v0.16.2

Published

Work on a BetterCMS project locally: sign in, clone the source, install dependencies.

Readme

@bettercms-ai/cli

Work on a BetterCMS project on your own machine.

npx @bettercms-ai/cli@latest init --project-id <id>

Copy the command (with your project's id already in it) from the Work on this project locally button in the project top bar. Requires Node 20 or newer.

Commands

| Command | What it does | |---|---| | init --project-id <id> [--agent <name>] [--repo <folder\|GitHub URL>] [--dir <path>\|.] [--framework <name>] [--no-launch] [--no-sections] [--push\|--no-push] [--authoring <components\|fields>] [--forms-email <address>] | Signs you in, fetches the source (or, on a headless project, uses your front end or starts one from a starter), installs dependencies, pre-fills .env, installs the agent skills, wraps each page's sections so they follow the CMS order (not on headless), opens your coding agent on the project. | | status [--project-id <id>] [--json] [--gate] | What is live, what is building, and whether the setup is complete. Defaults to the cached sign-in. | | pull-content [--dir <path>] | Headless: a converted site's first build step (bettercms pull-content && <build>). Writes the published content into the page snapshots its code imports, so a rebuild shows what was published. No key. | | derive [--project-id <id>] [--dir <path>] [--no-build] [--json] | Headless: builds and runs your front end here, renders each page as a visitor sees it, and sends the pages to BetterCMS, which derives their fields and today's copy. Your code is never changed. | | deploy --to <vercel\|netlify\|cloudflare> [--name <site>] [--dir <out>] [--no-hook] [--json] | Puts this repository on your own host through the host's own CLI and browser sign-in, sets the site's BetterCMS address there, and registers the host's deploy hook so every publish rebuilds it. | | env push [--project-id <id>] [--all] | Sets the project's variables from this repository's .env files. Prints names, never values. | | bridge [--project <id>] [--once] | Keeps this machine available to the dashboard as an executor. | | whoami | The cached sign-in — project, workspace, API, token expiry. Offline. | | logout | Forgets the cached sign-in. Idempotent. | | --version / --help | The installed version / this usage. |

| Flag | Default | What it is | |---|---|---| | --project-id <id> | init: required · status: the cached sign-in | The project to work on. | | --dir <path> | the project slug | Directory init creates. Must not already exist. | | --api <rung\|url> | prod | A rung by name — prod, dev, local — or a full URL. Also BETTERCMS_API_URL. | | --json | off | status only: print the API response verbatim. | | --agent | the one installed, else asked | init only: claude, codex, opencode, cursor or gemini (env BETTERCMS_AGENT); remembered in ~/.bettercms/cli-prefs.json. | | --framework | the project's, else asked | init only, headless with no front end yet: the starter (next, astro, react-ts). | | --no-launch | off | init only: print the next steps instead of opening the coding agent. | | --no-sections | off | init only: leave the page files alone (skip step 7). | | --repo | the connected repository | init only: use this folder or GitHub URL as the site's code. | | --push / --no-push | asked | init only: push BetterCMS's setup commits as it goes (--no-push: never). | | --authoring | asked | init only: how editors build pages, components or fields. | | --forms-email | asked | init only: where the site's forms send submissions ("" keeps the site's own handling). | | --gate | off | status only: print the setup gate (totals per check, the five worst pages, the next fix), and exit 3 while it is incomplete. |

Both --flag value and --flag=value work. Unknown flags are refused, not ignored.

Environments

--api takes a rung by name, so nobody has to remember a hostname:

| Name | API | |---|---| | prod (default) | https://api.bettercms.ai | | dev | https://dev-api.bettercms.ai | | local | http://localhost:3000 |

npx @bettercms-ai/cli@latest init --project-id <id> --api dev

A full https:// URL still works. A bare word that is not a rung is refused on the command line, listing the ones that are — it is never sent to DNS as a hostname.

The dev API is on bettercms.ai while the rest of the dev estate is on bettercms.dev (dashboard app.bettercms.dev, MCP mcp.bettercms.dev). bettercms.dev has a wildcard DNS record, so dev-api.bettercms.dev resolves and answers an nginx 404. Use --api dev and the question does not come up.

Each rung caches its own sign-in under its own origin in ~/.bettercms/cli-credentials.json, so switching never presents one rung's token to another — but the first command against a new rung asks you to approve it.

Sign-in is global, not per directory: init or status for another project replaces the cached grant, so a bare status afterwards reports that other project. status prints the project id on its first line for exactly this reason.

init

  1. Signs you in. An OAuth device code opens your browser at once, with the project pre-selected. Approve it there. If the browser cannot be opened, the link is printed instead. The grant is cached in ~/.bettercms/cli-credentials.json, so the next run is silent — unless the cached grant is for a different project, in which case you are asked again.
  2. Reads the project — its slug (the directory name) and workspace.
  3. Fetches the source. A connected GitHub repo is cloned with git clone — with no GitHub sign-in: BetterCMS lends the clone its own read access to that one repository (an hour, never written into the clone), and git never prompts. If the clone still cannot work, or there is no connected repo, the latest deployed build is downloaded and unpacked instead.
  4. Installs dependencies with the package manager the lockfile names — bun.lock/ bun.lockb → bun, pnpm-lock.yaml → pnpm, yarn.lock → yarn, otherwise npm. A project with no package.json skips this step. If the install fails, init warns with the command to rerun inside the directory and carries on; stopping would leave a folder that blocks the rerun.
  5. Pre-fills .env from .env.example when there is one and no .env yet: BETTERCMS_API_URL / PUBLIC_BCMS_API_URL / BCMS_API_URL are set to the API, and BETTERCMS_WORKSPACE / PUBLIC_BCMS_WORKSPACE / BCMS_WORKSPACE to your workspace slug. Every other line — your keys included — is left exactly as it was.
  6. Sets up your coding agents, like npm create sanity: runs npx -y @bettercms-ai/install@latest --global --mcp --api <rung>. In a terminal it asks "Configure BetterCMS MCP and agent skills for these editors?" (Claude Code, Codex CLI, Cursor, VS Code, Zed); without one it uses the editors it finds. Each chosen editor gets the BetterCMS skills (all 24: the compact bettercms, bettercms-migrate and bettercms-abm, plus the separate bettercms-* skills) and the hosted MCP in its user-level config. An API that is neither a named rung nor https gets the skills only. A failure only warns with the command.
  7. Wraps each page's sections so the site's own build renders them in the order the CMS page lists them, with each section's spacing and style, and draws a section added in the editor: npx -y @bettercms-ai/convert@latest --sections, with the project's page list from the API. It runs whether or not an agent is installed. First a dry run, whose plan is printed: each page, each section and the slot it gets, and each page it leaves alone with the reason. Then the same run for real, then a check that no class or style token differs from HEAD, then one local commit of exactly the files it wrote: chore(bettercms): render sections from BetterCMS. It never pushes, never amends, and leaves anything else you have staged alone. If a file it would change has uncommitted changes it does nothing and says which; if the style check or an SDK install fails it puts the files back. A directory that is not a git repository is wrapped and not committed. The wrap only adds <BcmsSections> and one slot tag around each section: no section's markup, props, classes or styles change, and a second run changes nothing. When the repository's @bettercms-ai SDK predates <BcmsSections>, its range is raised in package.json and the package manager installs it (that change is in the same commit). What it did is recorded with BetterCMS so the setup gate knows which routes were wrapped and which were refused. --no-sections skips the step.
  8. Opens Claude Code on the project, BetterCMS connected and a first prompt already sent — the same prompt as before, now typed for you. Needs claude on your PATH (npm install -g @anthropic-ai/claude-code); without it, or without a terminal, or with --no-launch, init prints the next steps and the command to run instead. Rerunning init in the same directory reuses the existing checkout and reopens Claude Code rather than failing with "already exists". Set BETTERCMS_AGENT_MODEL to name the model Claude Code uses; unset, it is your own default.
  9. Reads the setup gate and prints it when the agent exits, however it exited, on one screen whatever the size of the site: pages built, created and published; one totals line per counted check (text, images, links, sections, style, SVGs, numbers, attribute text), each editable/total; what is still open per check; the five worst routes; every recorded exception with its note; the one fix that closes the most items (Next); the first five items left, each with the tool call that fixes it; and the two commands below. A check nothing has measured yet (a project set up before that check existed) prints UNMEASURED in place of a share, and an Unmeasured line names them: neither a failure nor a pass, and one more setup run measures them. While the gate is incomplete init exits 3 and prints the command that reopens the agent on the same site. After --no-launch, or without a terminal, the gate is printed and init still exits 0: it was only asked to fetch the site. A gate that cannot be read (an older API, the network) is said in one line and is not a failure.
Setup     incomplete: 23 items left
Pages     18 built, 18 created, 16 published
Text      41210/52008 (79%) characters editable
Images    36/61 (59%) editable
Links     402/410 (98%) editable
Sections  52/61 (85%) stamped and movable
Style     UNMEASURED: setup has not counted this on the live build
SVGs      20/44 (45%) editable
Numbers   12/12 (100%) editable
Attrs     70/96 (72%) editable (alt, aria-label, title, placeholder)
Open      text 7, svg 6, style 4, image 3, published 2, seo 1
Unmeasured style: not a failure and not a pass. Setup measures them (editor_setup { force: true })
Routes    7 of 18 pass (page, text, images, links, sections, style; worst first)
  /services      draft      text 310/2400     images 0/4      links 12/12    sections 1/9    style ?      fails: published, text, image, sections
  /blog/post-1   published  text 900/2400     images 2/4      links 12/12    sections 3/3    style ?      svg 0/3  fails: text, image, svg
  …
  … and 13 more
Next      bind-elements closes 16 of 23: get_conversion_brief {"complete":true}

Left to do:
  1. 12% of the visible text on /services is a field's (310 of 2400 characters; the bar is 98%).
     fix (bind-elements): get_conversion_brief {"complete":true}
  2. 1 of 9 sections on /services are a stamped root the editor can find: the rest are not drawn from the page's blocks.
     fix (run-sections-conversion): componentize_sections {"codemod":true,"digest":"<digest from get_componentize_plan>"}
  …
  … and 18 more

Resume the agent:  npx @bettercms-ai/cli@latest init --project-id proj_abc123
Read this again:   npx @bettercms-ai/cli@latest status --gate --project-id proj_abc123

The read key

init does not mint an API key: a one-hour sign-in must not leave a key that never expires. Published content needs none (the SDK reads it by BCMS_PROJECT_ID). When the front end's config reads one, the coding agent creates it with the create_delivery_key tool, a named, read-only key listed under Project → Settings → API Keys, and writes it into .env.

A headless project

A headless project's site is built by its own host, so init needs no deployed source: it uses YOUR front end. Run it in that folder (or pass --repo <folder|GitHub URL>). First init runs bettercms derive: it builds the site, runs it, renders every page as a visitor sees it (in your local Chrome, Chromium or Edge, so copy a component fetches in the browser is read too) and sends those pages to BetterCMS, which derives the pages, fields and today's copy from them, as it does from a site it builds. Then the agent converts the code in place: each literal becomes a read of its field, with the markup unchanged. It deploys with bettercms deploy --to <host>, records the site URL (set_front_end), runs derive again so the setup measures what shipped, and stops only when get_next_steps reports receipt.complete (its receipt.frontEnd rows and its gate).

npx @bettercms-ai/cli@latest derive            # in the front end's folder
BETTERCMS_BROWSER=none npx @bettercms-ai/cli@latest derive   # read the HTML as served

init never starts from a starter unless you ask for one: --framework <next|astro|react-ts>, or "starter" at its question, for a project with no front end at all.

status

$ bettercms status
Project   proj_abc123
State     idle
Live      a1b2c3d since 2026-01-01T00:00:00.000Z
Serving   staging — promote pending (production a1b2c3d)
Editor    https://bettercms.ai/acme/projects/acme-site/visual

Serving appears only on projects that use the staging/production split. Live reads nothing deployed yet until the first publish, and State carries the API's own reason when a build failed: State failed — <reason>.

--json prints the API response verbatim — state, lastLiveSha, liveAt, publishing, failed, error, slot, productionSha, awaitingPromote, canvas, editorUrl. It is also non-interactive: if signing in would need a browser it exits 1 with Sign-in needed for <id>. Run the command once without --json to approve it. rather than opening one. Run status once without --json on a new machine, then script it.

status needs the same artifact:write grant as deploying, which the default device grant carries. A narrowed role sees the API's own 403 sentence instead of the status lines.

A Setup line follows when the API reports a setup gate: complete, not judged (nothing is live, or the project is headless), or incomplete: N items left. status --gate prints the whole gate, as init does when the agent exits, and exits 3 while it is incomplete, so a script can wait on it. --json --gate adds the gate to the JSON as setupGate.

deploy

$ bettercms deploy --to vercel
Host      Vercel (acme-site)
URL       https://acme-site-a1b2c3.vercel.app
Env       BCMS_API_URL, BCMS_WORKSPACE, BCMS_PROJECT_ID, PUBLIC_BCMS_API_URL, …
Rebuilds  on every publish (deploy hook registered with BetterCMS)

The Sanity model: the host's own CLI (vercel, netlify, wrangler, run with npx) signs you in through your browser, creates or links the site and deploys this directory. Your host token stays in that CLI's own store; BetterCMS never sees it. Then deploy:

  1. sets the site's BetterCMS address on the host — BCMS_API_URL, BCMS_WORKSPACE, BCMS_PROJECT_ID, unprefixed and under your framework's public prefix (NEXT_PUBLIC_, PUBLIC_, VITE_, …). Published content needs no key, so nothing secret is set;
  2. registers the host's deploy hook with BetterCMS, so every publish rebuilds the site.

| Host | Site | Rebuild on publish | |---|---|---| | Vercel | vercel link (creates the project), vercel git connect | Once the project builds from your GitHub repository. | | Netlify | netlify init in a terminal (creates the site and connects the repository); netlify sites:create without one | Once the site builds from your repository. | | Cloudflare | A local build, then wrangler deploy --assets <dir>: a Worker with static assets (created on the first deploy) | No: a local upload has no deploy hook. Rerun deploy after publishing, or connect the repository to Workers Builds. |

The hook is added beside BetterCMS's own builds, never instead of them: a project BetterCMS hosts keeps building there, so the editor keeps working, and switching the production host stays a dashboard decision. A rerun reuses its hook. Registering one needs the Enterprise plan; without it the site still deploys and deploy says publishes will not rebuild it.

In CI there is no browser: set the host's own token — VERCEL_TOKEN, NETLIFY_AUTH_TOKEN, or CLOUDFLARE_API_TOKEN with CLOUDFLARE_ACCOUNT_ID. Without one, deploy exits 1 and names it. --json prints { provider, site, url, env, hook } and never opens a browser. Cloudflare needs --dir when the framework is not Astro, Vite, Nuxt, Gatsby or Create React App.

Exit codes

| Code | Meaning | |---|---| | 0 | Done. Also whoami when signed out and logout when there was nothing to clear. | | 1 | The command failed — the reason is on the last line of stderr. | | 2 | The command line itself was wrong: unknown command, unknown flag, or a flag missing its value. | | 3 | init and status --gate only: the setup gate is not complete. What is left is printed above, with the command that resumes. | | other | The exit code of git or your package manager, which printed its own error. |

Troubleshooting

  • "git is required to clone …" — install git, then rerun.
  • A private repo asks for credentials — that is git's own prompt; set up an SSH agent or a credential helper and rerun.
  • "You approved a different project …" — the consent screen only pre-selects the project; rerun and pick the one in the command.
  • "This project has no deployed source yet." — connect a GitHub repo or publish once.
  • "status needs --project-id , or run bettercms init first." — nothing is cached on this machine yet. Pass the id, or run init.
  • "Sign-in needed for . Run the command once without --json to approve it." — --json never opens a browser. Run the same command without --json, approve it, then rerun.
  • "Could not reach https://api.bettercms.ai: …" — DNS, TLS, a proxy, or the 30 s request timeout. Check the network, or point --api somewhere reachable.
  • "Unexpected response from the API." — a 2xx body that was not the JSON expected; usually a captive portal or a proxy answering instead of the API.