@blinkk/root-cms

中文文档
4.3.5发布于 2026/11/06MITTypeScriptES Module构建来源已验证近 30 天下载 1.6万
原始使用文档
文档目录7 节
  1. A note on *@domain role grants
  2. Change proposals
  3. Previewing a proposal
  4. Downloadable agent skill
  5. Headless doc editor (pop-up or iframe)
  6. Headless Root AI (pop-up or iframe)
  7. Field focus from the in-CMS preview pane ("click to edit")

Setting up Firestore

Firestore must be setup as Native Mode and not Datastore Mode

Firestore read/writes will need to be locked down by adding the following to the security rules (in Firebase's Firestore UI):

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if false;
    }

    match /Projects/{project} {
      allow write:
        if isSignedIn() && userIsAdmin();
      allow read:
        if isSignedIn() && userCanRead();

      match /{collection}/{document=**} {
        allow write:
          if isSignedIn() && userCanPublish();
        allow read:
          if isSignedIn() && userCanRead();
      }

      match /Collections/{collectionId}/Drafts/{document=**} {
        allow write:
          if isSignedIn() && userCanEdit();
      }

      function isSignedIn() {
        return request.auth != null;
      }

      function getRoles() {
        return get(/databases/$(database)/documents/Projects/$(project)).data.roles;
      }

      function userCanRead() {
        let roles = getRoles();
        let email = request.auth.token.email;
        let domain = '*@' + email.split('@')[1];
        return (roles[email] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR', 'VIEWER']) || (roles[domain] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR', 'VIEWER']);
      }

      function userCanPublish() {
        let roles = getRoles();
        let email = request.auth.token.email;
        let domain = '*@' + email.split('@')[1];
        return (roles[email] in ['ADMIN', 'EDITOR']) || (roles[domain] in ['ADMIN', 'EDITOR']);
      }

      function userCanEdit() {
        let roles = getRoles();
        let email = request.auth.token.email;
        let domain = '*@' + email.split('@')[1];
        return (roles[email] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR']) || (roles[domain] in ['ADMIN', 'EDITOR', 'CONTRIBUTOR']);
      }

      function userIsAdmin() {
        let roles = getRoles();
        let email = request.auth.token.email;
        let domain = '*@' + email.split('@')[1];
        return (roles[email] == 'ADMIN') || (roles[domain] == 'ADMIN');
      }
    }
  }
}

In Firestore, add a document at Projects/<yourprojectid> with a value of {roles: {"[email protected]": "ADMIN"}}.

Using Firestore Studio:

  • Under Give the collection an ID, set Collection ID to Projects
  • Under Add its first document set Document ID to your project ID
  • For the first record set Field name to roles with a Field type of map
  • In the map set the new Field name to your e-mail, Field type to string and Field value to ADMIN and save.

A note on *@domain role grants

The rules above support a wildcard *@example.com entry in roles that matches every signed-in user whose verified email ends in @example.com. This is convenient for granting access to a whole workspace, but be aware:

  • Anyone Firebase Auth admits with an @example.com email — including newly-created accounts and any account that you no longer control — gains the role immediately, with no opt-in or audit trail.
  • Never use a wildcard for a free-email provider (e.g. *@gmail.com): that grants access to anyone with a Google account.
  • Prefer explicit per-email grants for ADMIN and EDITOR; reserve the wildcard for CONTRIBUTOR or VIEWER if you use it at all.

CLI for AI agents (root-cms client.*)

The root-cms CLI exposes two commands that let an AI coding agent (or any script) drive the RootCMSClient programmatically. Run them from the root of a Root.js project (a directory containing root.config.ts).

Discover the available API:

# Human-readable list of methods + descriptions
root-cms client.methods

# Machine-readable JSON, including referenced type/interface definitions
root-cms client.methods --json --types

Call any method with a JSON array of positional arguments:

# getDoc(collectionId, slug, options)
root-cms client.call getDoc '["Pages", "home", {"mode": "draft"}]'

# A no-arg method (omit the JSON array)
root-cms client.call publishScheduledDocs

# Read large args from stdin by passing `-`
echo '["Pages/home", {"title": "Hello"}]' | root-cms client.call saveDraftData -

client.call prints a single-line JSON envelope to stdout and exits non-zero on failure:

{"ok": true, "result": <value>}
{"ok": false, "error": "<message>"}

Firestore Timestamp, GeoPoint, and DocumentReference values are encoded as {"_seconds","_nanoseconds"}, {"_latitude","_longitude"}, and {"_referencePath"} respectively in both arguments and results.

Change proposals

AI agents can propose content changes instead of writing them straight to Firestore. A proposal is a single Markdown file describing the edits — doc id, field path, old value, new value — that gets committed to your repo and reviewed in a pull request. Nothing touches the database until you apply it.

Each change and op is a heading, and each edited value is a diff code block, so the pull request shows exactly which lines change:

# Refresh the homepage hero

- **id:** `2026-08-20-hero-refresh`
- **version:** 1

## Edit `Pages/home`

### Set `hero.title`

```diff text
-Welcome to Acme
+Build faster with Acme
```

A value block is text (the string, verbatim), yaml, or json. A diff block holds the whole value with each line prefixed by " " (unchanged), - (before), or + (after). Proposals can also be written in YAML (.yaml files), the original format.

Supported change kinds are doc.edit, doc.create, doc.duplicate, release.create, release.update, and translations (headings ## Edit, ## Create, ## Duplicate … to …, ## Create release, ## Update release, and ## Translations). Edit operations (set, insert_item, remove_item) use dotted field paths with zero-based array indices, and apply in order.

Three commands drive the workflow:

# Check the proposal format. No db access, so it works as a CI gate on a PR.
root-cms proposal.check cms-proposals/2026-08-20-hero-refresh.md

# Resolve against the live db, validate against collection schemas, and
# report what it would write.
root-cms proposal.diff cms-proposals/2026-08-20-hero-refresh.md

# Apply it.
root-cms proposal.apply cms-proposals/2026-08-20-hero-refresh.md

Applying writes drafts only — it never publishes, schedules, or deletes — and nothing is written unless every change in the proposal resolves cleanly. Each command prints the same single-line JSON envelope as client.call.

A proposal's before values are documentation for the human reading the diff; they are not checked against the database unless you pass --verify-before.

Previewing a proposal

RootCMSClient can overlay an unapplied proposal on everything it reads, so you can see proposed content rendered on a running site before accepting it:

import {RootCMSClient, parseProposalFile} from '@blinkk/root-cms/core';

const filepath = 'cms-proposals/x.md';
const res = parseProposalFile(fs.readFileSync(filepath, 'utf8'), filepath);
if (res.ok) {
  const client = new RootCMSClient(rootConfig, {proposal: res.proposal});
  // getDoc(), listDocs(), and batch reads now reflect the proposal.
}

The overlay applies to reads only. Writes never fold the proposal in, and getRawDoc() / listDocs({raw: true}) deliberately bypass it so proposed content can never leak into a setRawDoc() round-trip.

Downloadable agent skill

Ready-to-use skills that teach AI coding agents how to use these commands ship with the package: root-cms-cli (the client.* commands), root-cms-propose (authoring change proposals), root-cms-apply (reviewing and applying them), root-cms-theme (generating a theme for the CMS UI), and root-cms-email-setup (deploying the email service and setting up email notifications). Install them with:

root-cms skill.install

Root stays agnostic of any particular AI provider: the command auto-detects existing agent skills directories (e.g. .claude/skills, .agent/skills, or any .*/skills directory already in your project) and installs into them. If none is found, it installs to .agent/skills. Pass an explicit directory to override detection (e.g. root-cms skill.install ./my-agent/skills), or --force to overwrite an existing copy. Pass --skill <name> to install just one of them.

Embedding the CMS (@blinkk/root-cms/browser-client)

@blinkk/root-cms/browser-client is a dependency-free, framework-agnostic browser library for embedding Root CMS into another site. It wraps the underlying iframe/postMessage wiring behind a typed API, so the wire protocol can evolve without breaking your integration.

For pop-up/iframe embedding (the doc editor and Root AI), the embedding page's origin must be allowlisted in the CMS plugin config:

// root.config.ts
cmsPlugin({
  // ...
  allowedIframeOrigins: ['https://app.example.com'],
});

Headless doc editor (pop-up or iframe)

Opens /cms/embed/content/<collection>/<slug>, a minimal version of the doc editor with explicit (non-auto) saving. In popup mode, call from a user gesture (e.g. a click handler) to avoid pop-up blockers. Closing the editor discards unsaved changes.

import {RootCMSBrowserClient} from '@blinkk/root-cms/browser-client';

const root = new RootCMSBrowserClient({cmsOrigin: 'https://cms.example.com'});

const editor = root.openEditor('Pages/about', {
  mode: 'popup', // or 'iframe' (pass `container`)
  deeplink: 'hero.title', // optional: field to scroll to on load
});
editor.on('ready', () => console.log('editor loaded'));
editor.on('saved', () => location.reload());
editor.on('published', () => editor.closeAndReload());
editor.on('close', () => console.log('editor closed'));
editor.focusField('hero.image'); // scroll the editor to a field
editor.reload();
editor.close(); // or editor.closeAndReload()

Headless Root AI (pop-up or iframe)

Opens /cms/embed/ai, the compact Root AI chat, optionally with a doc as context:

const ai = root.openRootAI({docId: 'Pages/about', mode: 'popup'});
ai.on('ready', () => console.log('chat loaded'));
ai.close();

Field focus from the in-CMS preview pane ("click to edit")

When your site is rendered inside the CMS preview pane, use a PreviewConnection to focus editor fields from the page and to highlight page nodes when the editor requests it. The preview pane is same-origin with the CMS, so no config is needed and the connection is inert when the page is not embedded:

import {RootCMSBrowserClient} from '@blinkk/root-cms/browser-client';

const preview = RootCMSBrowserClient.connectPreview();
if (preview.isEmbedded) {
  // "Click to edit": focus the field in the doc editor.
  el.addEventListener('click', () => preview.focusField('hero.title'));
  // Highlight page nodes when the editor hovers/focuses a field.
  preview.on('highlight', ({deepKey, scroll}) => {
    clearHighlights();
    if (deepKey) highlightNode(deepKey, {scroll});
  });
}

版本与发布记录

@blinkk/root-cms 共发布 340 个版本,首次发布于 2022/10/29;过去 12 个月发布了 91 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
4.3.5latest2026/11/062 天前
4.3.32026/11/044 天前
4.3.22026/11/035 天前
4.3.12026/11/035 天前
4.2.22026/10/2910 天前
4.2.12026/10/2810 天前

其他发布通道:rc → 2.0.0-rc.1,debug → 1.3.14-debug.4,beta → 3.0.1-beta.9,alpha → 4.0.0-alpha.8。

关于 @blinkk/root-cms 的常见问题

如何安装 @blinkk/root-cms?

在项目目录运行 npm install @blinkk/root-cms,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:@blinkk/root(4.3.5)、firebase-admin(>=11)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 root-cms,可以用 npx root-cms 直接运行。

@blinkk/root-cms 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@blinkk/root-cms 支持 ESM 还是 CommonJS?

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

@blinkk/root-cms 需要什么版本的 Node.js?

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

@blinkk/root-cms 可以商用吗?

@blinkk/root-cms 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@blinkk/root-cms 的下载量有多少?

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

@blinkk/root-cms 还在维护吗?

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