@bettercms-ai/cli

中文文档
0.16.2发布于 2026/11/02ES Module近 30 天下载 4426

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

原始使用文档
文档目录9 节
  1. Commands
  2. Environments
  3. init
  4. The read key
  5. A headless project
  6. status
  7. deploy
  8. Exit codes
  9. Troubleshooting

@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

CommandWhat 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.
whoamiThe cached sign-in — project, workspace, API, token expiry. Offline.
logoutForgets the cached sign-in. Idempotent.
--version / --helpThe installed version / this usage.
FlagDefaultWhat it is
--project-id <id>init: required · status: the cached sign-inThe project to work on.
--dir <path>the project slugDirectory init creates. Must not already exist.
--api <rung|url>prodA rung by name — prod, dev, local — or a full URL. Also BETTERCMS_API_URL.
--jsonoffstatus only: print the API response verbatim.
--agentthe one installed, else askedinit only: claude, codex, opencode, cursor or gemini (env BETTERCMS_AGENT); remembered in ~/.bettercms/cli-prefs.json.
--frameworkthe project's, else askedinit only, headless with no front end yet: the starter (next, astro, react-ts).
--no-launchoffinit only: print the next steps instead of opening the coding agent.
--no-sectionsoffinit only: leave the page files alone (skip step 7).
--repothe connected repositoryinit only: use this folder or GitHub URL as the site's code.
--push / --no-pushaskedinit only: push BetterCMS's setup commits as it goes (--no-push: never).
--authoringaskedinit only: how editors build pages, components or fields.
--forms-emailaskedinit only: where the site's forms send submissions ("" keeps the site's own handling).
--gateoffstatus 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:

NameAPI
prod (default)https://api.bettercms.ai
devhttps://dev-api.bettercms.ai
localhttp://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.
HostSiteRebuild on publish
Vercelvercel link (creates the project), vercel git connectOnce the project builds from your GitHub repository.
Netlifynetlify init in a terminal (creates the site and connects the repository); netlify sites:create without oneOnce the site builds from your repository.
CloudflareA 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

CodeMeaning
0Done. Also whoami when signed out and logout when there was nothing to clear.
1The command failed — the reason is on the last line of stderr.
2The command line itself was wrong: unknown command, unknown flag, or a flag missing its value.
3init and status --gate only: the setup gate is not complete. What is left is printed above, with the command that resumes.
otherThe 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.

版本与发布记录

@bettercms-ai/cli 共发布 40 个版本,首次发布于 2026/09/07;过去 12 个月发布了 40 个版本。

版本发布日期距今
0.16.2latest2026/11/026 天前
0.16.12026/11/017 天前
0.16.02026/10/318 天前
0.15.112026/10/318 天前
0.15.92026/10/2811 天前
0.15.82026/10/2811 天前

关于 @bettercms-ai/cli 的常见问题

@bettercms-ai/cli 是什么?

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

如何安装 @bettercms-ai/cli?

在项目目录运行 npm install @bettercms-ai/cli,或使用 pnpm add / yarn add / bun add。它还提供命令行工具 bettercms,可以用 npx bettercms 直接运行。

@bettercms-ai/cli 支持 TypeScript 吗?

该包的 package.json 没有声明类型入口。需要类型提示时,可以在 npm 上查找社区维护的 @types/bettercms-ai__cli 是否存在。

@bettercms-ai/cli 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@bettercms-ai/cli 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@bettercms-ai/cli 可以商用吗?

@bettercms-ai/cli 使用 未声明 许可。package.json 没有声明许可证。没有许可证的代码默认保留全部权利,商用前请联系作者或查看仓库中的 LICENSE 文件。以上为概括,不构成法律意见,请以许可原文为准。

@bettercms-ai/cli 的下载量有多少?

根据 npm 官方下载统计,@bettercms-ai/cli 在 2026/10/08 – 2026/11/06 这 30 天内被下载 4,426 次,日均约 148 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@bettercms-ai/cli 还在维护吗?

最新版本 0.16.2 发布于 2026/11/02(6 天前);至今共发布 40 个版本,其中过去 12 个月发布了 40 个;首次发布于 2026/09/07。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。