@cloudhal/cli

中文文档
0.6.5发布于 2026/11/09Apache-2.0TypeScriptES Module构建来源已验证近 30 天下载 3959

cloudhal — create, run and build Cloudhal sites locally

原始使用文档
文档目录7 节
  1. Schedules (RFC-0008)
  2. Reader comments (RFC-0007)
  3. Site analytics (RFC-0006)
  4. Migrating tags to terms (RFC-0002)
  5. Changing typed fields (RFC-0041)
  6. Site archives (RFC-0039)
  7. Deploying to Cloudflare (ADR-0012)

@cloudhal/cli

Every command and flag is in cloudhal --help and the generated CLI reference. The ones you use most:

cloudhal init <dir> [--name <name>]          create a site project with a seeded local store
cloudhal dev                                 live preview (drafts visible)
cloudhal studio                              API + Studio UI (+ /mcp) for this project
cloudhal mcp [--mode plan|manual|edits|auto] MCP server over stdio for this project
cloudhal deploy                              site + admin Workers on Cloudflare
cloudhal push / cloudhal pull                local site ↔ deployment
cloudhal export / cloudhal import            one site ↔ a .cloudhal file

Studio build. The published CLI ships Studio prebuilt. Run from the monorepo, cloudhal deploy uploads apps/studio/dist and first checks that it is current: when it is missing, or its index.html is older than the newest file in apps/studio/src, apps/studio/index.html or packages/ui/src, it runs pnpm --filter @cloudhal/studio build. With --no-build-studio it never builds and fails on a missing or stale build instead.

Schedules (RFC-0008)

Releases, embargoes and expiries, time zones, automations, recipes for scheduled agents and the Free-plan notes: schedules.

On Cloudflare. cloudhal deploy gives the admin Worker one cron trigger, every minute (* * * * *): the scheduler's tick, which also runs the analytics rollup and the comments job behind hourly watermarks (at :17). It always declares the SQLite-backed SchedulerClock Durable Object (binding SCHEDULER_CLOCK, migration v1) and sets CLOUDHAL_SCHEDULER:

  • cron (default): the trigger ticks;
  • alarm: when Cloudflare refuses the trigger because the account's cron triggers are used up (Workers Free allows 5), the deploy goes on without it and the Durable Object's alarm ticks instead. The choice is kept in .cloudhal/deploy/state.json; --scheduler cron tries the trigger again, --scheduler alarm chooses the clock up front. Deployments that had CLOUDHAL_ANALYTICS_CRON=off or CLOUDHAL_COMMENTS_CRON=off move to alarm and lose those variables.

Every admin request is also a backstop: when the heartbeat is more than two minutes behind, it runs a tick in the background. The migration 0017_schedules runs on the admin Worker's first request.

Guards.

  • deploy refuses to deploy code with a lower schedules level (cloudhal.schedulesLevel of @cloudhal/core-ts) over a deployment that has open releases or gated pages and entries: older code ignores holds and gates. --force-downgrade overrides it.
  • push refuses while the deployment has open releases (their pinned versions point into the graph a push replaces); --cancel-schedules cancels them first.
  • push carries content, not publishing: once the deployment has published, push --replace keeps its published snapshot (an approved publish or rollback in Studio), so it can never move production back to an older version. Publish the pushed content in Studio. A deployment that has never published takes your local one, as the first deploy does.

Tick budget and the automations switch. Every deploy writes CLOUDHAL_TICK_BUDGET and CLOUDHAL_AUTOMATIONS on the admin Worker:

  • CLOUDHAL_TICK_BUDGET=free (default: 40 statements and one release per tick) or paid (900 statements and up to 50 releases): cloudhal deploy --plan paid or cloudhal.json "schedules": { "plan": "paid" }. The plan is remembered in .cloudhal/deploy/state.json; --plan free goes back. Wrangler's login cannot read the account's plan, so the deploy says which budget it used.
  • CLOUDHAL_AUTOMATIONS=off with "schedules": { "automations": false } (else on): automations stop; releases and gates go on. On Node, cloudhal dev, studio, mcp and schedules run-due honour CLOUDHAL_AUTOMATIONS=off from the environment.

Commands:

  • cloudhal schedules status [--remote]: the scheduler's mode and state (running, late, stopped, unknown), the last tick and the next wake, the code's schedules level, and each site's open releases and gated pages and entries.
  • cloudhal schedules list [--remote] [--limit <n>]: the site's releases with their state, time and number of items.
  • cloudhal schedules run-due [--remote]: one tick now. A self-hosted server without a long-running Cloudhal process runs it from the system's cron every minute.

--remote uses the deployment token and the operator routes /api/admin/schedules/status|run-due and /api/admin/sites/:siteId/schedules/releases.

Locally. cloudhal dev, cloudhal studio and cloudhal mcp run the same tick on a timer of at most 60 seconds, with a catch-up tick at start; several of them on one project are safe.

Reader comments (RFC-0007)

Setup, moderation, agents, spam protection, privacy, erasure and a ready-made privacy paragraph (English and Turkish): reader comments.

Comments are available on a deployment unless cloudhal.json says otherwise, and off for every site until someone turns them on in Studio. A deploy never adds a comment form by surprise.

cloudhal.json → comments:

KeyDefaultMeaning
availabletrue (also when the block is missing)false removes comments from the deployment: no endpoints, no script, no hourly comments work. cloudhal init writes { "available": true }.
rateLimitfalsetrue or { "limit": 10, "namespaceId": "…" }: a Workers Rate Limiting binding (COMMENTS_RL) on the site Worker that limits submissions per client across isolates (limit per 60 s, default 10; the namespace id is derived from the deployment name unless set). On top of the per-isolate and D1 limits, which work without it.

On Cloudflare. cloudhal deploy:

  • generates CLOUDHAL_COMMENTS_KEY once into .cloudhal/deploy/secrets.json (0600) and gives it to both Workers through --secrets-file; it is never printed or written into a config. Losing it makes stored emails and the Turnstile secret unreadable;
  • sets CLOUDHAL_COMMENTS_AVAILABLE on the admin Worker;
  • runs the hourly comments job on the scheduler's minute tick (behind an hourly watermark), even when analytics are off; without a free cron slot the tick is the SchedulerClock alarm (Schedules);
  • sets CLOUDHAL_SITE_HOSTS for a custom domain and, with comments.rateLimit, adds COMMENTS_RL;
  • purges the site's cache once when availability changes.

The migration 0016_comments runs on the admin Worker's first request. Existing deployments get comments on their next deploy, still off for every site.

Data location. A D1 database's jurisdiction is chosen when it is created and cannot be changed:

  • the first cloudhal deploy in a terminal asks "Automatic" or "EU jurisdiction";
  • --jurisdiction eu decides in advance (CI);
  • a non-interactive first deploy without the flag uses the automatic location and says how to choose;
  • once the database exists the flag changes nothing, and the deploy says so;
  • to move, export the comments, deploy under a new --name with --jurisdiction eu, and import.

Commands:

  • cloudhal comments status [--remote]: whether comments are available and the key is there, the site's switch, collections and protection, counts by status, the oldest pending comment, today's stored submissions against the daily cap, and the purge queue with the purge requests of the last minute (Workers Cache allows 5).
  • cloudhal comments export [--remote] [--with-personal-data --out <file>]: JSON lines (a header, then one Comment per line, oldest first) to stdout or --out. --with-personal-data appends commenter lines with their email and needs --out, which is then written owner-only.
  • cloudhal comments import --in <file> [--remote]: idempotent by id and by importer origin, in batches of 50 lines.
    • Each record must name an article of the target site, so push the site first.
    • The site comes from the target, statuses are kept, and emails are encrypted again under the target's key.
    • Replies whose parent arrives later are retried once.
    • Refused lines are listed with their line number, and the command exits 1.

--remote uses the deployment token and the operator routes /api/admin/sites/:siteId/comments/status|export|import; each call is audited as the operator.

Locally. cloudhal dev takes comments on the project's SQLite. The key is CLOUDHAL_COMMENTS_KEY from the environment, or .cloudhal/comments.key (random, created on first use, 0600). cloudhal studio and cloudhal mcp use the same store. Node has no cron, so their scheduler tick runs the hourly comments job (retention, comments of deleted articles, threads closed by time, old salts) behind its watermark, and they empty the local purge queue (there is no edge cache).

Site analytics (RFC-0006)

What is collected, what never is, the unique-visitors mode, retention and a ready-made privacy-policy paragraph (English and Turkish): site analytics and privacy.

Each published page carries one first-party script (/_cloudhal/p.<hash>.js, under 1 KB gzipped) that posts a page view to POST /__cloudhal/e on the site Worker. The Worker checks it, resolves the path to a published node and writes the event to Workers Analytics Engine (WAE). The admin Worker reads WAE through the Analytics SQL binding (no API token) and keeps daily totals in D1. Locally and on Node the same model runs on the project's SQLite.

cloudhal.json → analytics (deployment defaults; per-site settings in Studio override them):

KeyDefaultMeaning
enabledon when the block is therefalse turns analytics off for the whole deployment: no bindings, no cron, no script. Without the block, analytics are off. cloudhal init writes { "enabled": true }.
uniqueVisitorsfalseDaily-salted visitor hash (opt-in; see the guide).
vitalsOneIn5Core Web Vitals from 1 in N page views: 0 (off), 1, 2, 5, 10, 20.
queryBudget8000Uncached WAE queries per UTC day; past it, live numbers pause until midnight UTC and complete days still come from D1.
rawRetentionDays90Raw events kept in SQLite (local and Node only; WAE keeps 3 months).
devfalseRecord cloudhal dev traffic locally (it is your own).
rateLimitfalsetrue or { "limit": 120, "namespaceId": "…" }: adds a Workers Rate Limiting binding (ANALYTICS_RL) that limits beacons per client across isolates (limit per 60 s, default 120; the namespace id is derived from the deployment name unless set). On top of the per-isolate limits; whether Workers Free offers the binding is unconfirmed.
{ "siteId": "site_…", "analytics": { "enabled": true, "vitalsOneIn": 5 } }

On Cloudflare. With analytics on, cloudhal deploy adds:

  • to the site Worker, the WAE dataset <deployment>_events (binding ANALYTICS; Cloudflare creates the dataset on the first write) and the vars CLOUDHAL_ANALYTICS=on, CLOUDHAL_ANALYTICS_DEFAULTS and, with a custom domain, CLOUDHAL_SITE_HOSTS;
  • to the admin Worker, the Analytics SQL binding (ANALYTICS_SQL), the vars CLOUDHAL_ANALYTICS_DATASET, CLOUDHAL_ANALYTICS_BUDGET and CLOUDHAL_ANALYTICS_DEFAULTS, and the hourly rollup on the scheduler's minute tick (behind a watermark at :17), which rolls finished days into D1, deletes old daily salts and applies retention.

It needs wrangler 4.145 or newer (the analytics config key); the deploy stops and says so when the project's wrangler is older. The migration 0014_analytics runs on the admin Worker's first request. When analytics switch on or off, the deploy purges the site's cache once so pages gain or lose the script tag. Workers Free allows 5 cron triggers per account: when none is free, the admin Worker is deployed without one and the SchedulerClock Durable Object's alarm runs the tick instead, so daily totals are still rolled up on schedule (Schedules).

Turning it on for an existing deployment. Projects from before analytics have no analytics key. The next interactive cloudhal deploy asks once ("Turn on cookie-less analytics for ? (Y/n)") and writes the answer to cloudhal.json; a non-interactive deploy (CI) keeps analytics off and prints how to turn them on. To decide up front, add "analytics": { "enabled": true } (or false) and deploy. Adding a script to a live site changes what its privacy policy must say, so update it first (guide). Agent keys created before analytics do not get analytics:read automatically; allow it per key on Studio's Agents page.

Limits and costs (Cloudflare's published numbers as of 2026-10; WAE is not billed yet, its Paid prices are announced):

Workers FreeWorkers Paid ($5/month)
Worker requests100,000/day, cache hits included10 M/month included, then $0.30 per million
WAE writes100,000/day10 M/month included, then $0.25 per million
WAE queries10,000/day1 M/month included, then $1.00 per million
D1 rows written100,000/day (rollups use ≈ 200–2,200 a day)see D1 pricing
Cron triggers5 per account (Cloudhal uses one, or a Durable Object alarm)see Workers limits

Each page view adds one Worker request (the beacon) plus one per sampled vitals view; the script itself is a free static asset. Rough sizes, assuming about 3 cached asset requests per page: a site with 1,000 views a day uses about 5 % of Free's requests; at 20,000 views a day a Free account is over its limit with or without analytics (Free carries about 25,000 views a day without analytics and 19,000 with, 21,000 with vitals off); 200,000 views a day needs Paid, about $11–12 a month in total, of which about $2.20 is analytics. On Free, Studio warns when yesterday's traffic approaches the limit (set "Cloudflare plan" in Site settings › Analytics). Whether the Analytics SQL binding works on Workers Free, and whether its queries count against the 10,000 a day, is not verified yet (RFC-0006 step 0): if it does not, visits are still recorded and Studio says it can't read them yet.

Commands:

  • cloudhal analytics status [--remote]: effective settings, the store and reader, the last rolled-up day, the query budget and yesterday's request estimate; --remote asks the deployment (deployment token).
  • cloudhal analytics rollup [--days <n>] [--remote] [--redo]: compute daily totals for finished days. --remote --days 90 backfills a deployment from WAE (it keeps 3 months), a few site-days per request until none are pending. --redo recomputes days already done (local only).
  • cloudhal analytics seed [--days <n>] [--views <n>]: deterministic demo traffic (default 30 days, about 120 views a day) over the published pages, in the local SQLite only; never touches a deployment.
  • cloudhal analytics export [--from <date> --to <date>] [--remote] [--out <file>]: daily totals as CSV (default: the last 30 days). There is nothing per visitor to export.

Locally, cloudhal studio reads the project's SQLite (analytics_events), rolls up finished days at start and hourly, and prunes raw events older than rawRetentionDays; cloudhal mcp reads the same data for the analytics tools. cloudhal dev records only with analytics.dev.

Migrating tags to terms (RFC-0002)

cloudhal migrate tags-to-terms moves the deprecated ContentEntry.tags of the local site into terms of the tag taxonomy. It snapshots the site first (before tags-to-terms; its id is the migration id), creates the default category and tag taxonomies if they are missing (category is not required on an existing site), creates one active tag per distinct slug, rewrites entries in batches of 200 and snapshots again (tags-to-terms). An interrupted run resumes; a second run does nothing. Nothing goes live until you publish. --dry-run reports what would change; --revert <id> restores the tags of entries nobody edited since, then removes the terms and taxonomies the run created that are no longer used.

Changing typed fields (RFC-0041)

cloudhal fields changes a collection's typed fields together with the values entries hold, which a plain edit of the definitions refuses (field_change_needs_operation):

cloudhal fields rename events speaker presenter --dry-run   # report only; also lists theme files reading fields.speaker
cloudhal fields rename events speaker presenter
cloudhal fields retype events seats number [--clear]          # closed conversion table; --clear drops values it cannot convert
cloudhal fields retype events format select --options talk,workshop=Workshop [--multiple]
cloudhal fields localize events notes off                     # translations' own copies go; the original's value is read
cloudhal fields remove events notes
cloudhal fields revert events <operationId>

Each run snapshots the site first (before rename_field events.speaker; its id is the operation id), writes the new definition with a migration marker so every reader stays consistent, rewrites entries in guarded batches of 200, drops the marker and snapshots again. Translations that were up to date stay up to date. An interrupted run resumes with cloudhal fields continue <collection> <id>; one operation per collection runs at a time. --remote runs it on the deployment with the deployment token. Only the draft graph changes: nothing goes live until you publish.

Site archives (RFC-0039)

Backups, moving a site without a deployment, content kept in git, templates, the file's layout and its security checks: site archives.

cloudhal export [--out <file>] [--site <id>] [--no-media] [--latest-only]   # one site as a .cloudhal file
cloudhal import <file> [--replace] [--dry-run]                             # verified in full before anything is written
cloudhal import --revert <snapshot-id> [--dry-run]                         # undo a --replace

An archive holds the site's graph, snapshots, media and takedown ledger, never user accounts, comments, analytics or secrets; its history names who made each change. Import keeps the site's id. When --replace changes a site the project holds, it saves that site first as a before import <file> snapshot and prints the cloudhal import --revert <id> that brings it back. For an archive of another site it switches cloudhal.json's siteId, leaving the old site in the store.

Deploying to Cloudflare (ADR-0012)

cloudhal deploy uses your wrangler login (npx wrangler login, or CLOUDFLARE_API_TOKEN); Cloudhal never reads those credentials. It creates the D1 database and R2 bucket if needed (with a lifecycle rule that expires the snapshot bundles under bundles/ after 30 days, RFC-0010), generates the deployment secrets once into .cloudhal/deploy/secrets.json (0600 — keep it out of git), builds and deploys the site Worker (Astro on demand, edge-cached) and the admin Worker (Studio, API, auth, MCP), and on the first deploy pushes your local site.

From Git (Workers Builds). With WORKERS_CI=1, which Cloudflare sets in Workers Builds, cloudhal deploy works from a fresh clone. It takes the names from the committed wrangler.jsonc, seeds the starter site when the checkout has none, keeps the secrets on the Workers and does not print the setup link. This is what the Deploy to Cloudflare button runs: see docs/guides/deploy-button.md.

Custom domain. cloudhal domain set news.example.com --studio studio.example.com records the hostnames in cloudhal.json; the next cloudhal deploy routes them to the Workers as Cloudflare Custom Domains (the zone must be in the same Cloudflare account — Cloudflare creates the DNS record and certificate) and makes the site's one its primary hostname on the deployment. domain set makes it primary in the local store right away, so cloudhal dev, render and build write it into canonical links, the sitemap and feeds before any deploy. The workers.dev addresses keep working. cloudhal domain remove and a deploy undo it. --redirect www.news.example.com (repeatable) adds hosts that answer with a 301 to the site's domain, path and query kept: the next deploy routes them to the site Worker as Custom Domains too, so Cloudflare creates their DNS records and certificates. cloudhal domain remove redirects drops them.

Email (RFC-0011). Add "mail": { "from": "[email protected]", "fromName": "Example Studio" } to cloudhal.json and the next cloudhal deploy gives the admin Worker a Cloudflare Email Service send_email binding locked to that address. Invites can then be emailed, and staff can reset a forgotten password from the sign-in screen. Onboard the address's domain in Email Service first. Sending to any recipient needs Workers Paid (3,000 emails a month included); on Free, Email Service delivers only to verified destination addresses of your account. Locally, cloudhal studio prints these emails in the terminal instead of sending them.

In-editor AI (RFC-0016). Add "ai": {} to cloudhal.json and the next cloudhal deploy binds Workers AI to the admin Worker. No key is needed; it bills to the account, with 10,000 free neurons a day. Each site's owner then turns AI on in Studio › Settings › AI, picks the model and sets a daily limit. An optional AI Gateway adds logs and caching, and reaches other providers' models with keys stored in the gateway. Locally, cloudhal studio reaches Workers AI over REST when CLOUDFLARE_ACCOUNT_ID and CLOUDHAL_AI_TOKEN (a token with Workers AI permissions) are set in the environment.

Operations and hardening. Both Workers send logs to Workers Logs with query strings redacted. In cloudhal.json:

  • "observability": { "logs": 0.25, "traces": 0.05 } samples logs and switches on Workers Traces (off by default);
  • "placement": "smart" tries Smart Placement on the admin Worker.

Static builds get the site's Content-Security-Policy in dist/_headers. docs/guides/cloudflare-hardening.md covers costs, how to measure Smart Placement, and a WAF baseline for custom domains: the Free Managed Ruleset and one Free rate limiting rule.

After that the deployment is the source of truth — agents edit it there. cloudhal push refuses to overwrite content that changed remotely; cloudhal pull brings it home. --dry-run bundles both Workers with the real wrangler without an account.

Publishing and rollback never rebuild anything: the site Worker follows the published snapshot pointer and the admin Worker purges its cache. Preview links (?cloudhal-preview=…) show drafts on the real site.

cloudhal mcp is what you register in Claude Code / Cursor / any MCP client:

{ "mcpServers": { "cloudhal": { "command": "cloudhal", "args": ["mcp"], "cwd": "/path/to/site" } } }

The local developer owns the project, so the default scopes are *; publish, rollback and delete still stop for approval inside the client (or hand back a confirmToken when the client cannot elicit). --scopes site:read,content:write narrows what the client may do. Skills come from <project>/skills or the bundled skills/official.

Everything runs offline: the project's .cloudhal/ holds data.db (SQLite) and blobs/. cloudhal.json records the site id.

Inside the monorepo the bin runs as TypeScript on Node 24 (type stripping): node packages/cli/bin/cloudhal.ts init examples/my-site. Projects created under examples/ are workspace-linked; elsewhere, workspace:* dependencies are rewritten to published versions.

cloudhal dev cannot run inside a Vitest worker (Astro disables its dev middleware when VITEST is set); the e2e test spawns the bin as a child process.

版本与发布记录

@cloudhal/cli 共发布 26 个版本,首次发布于 2026/10/20;过去 12 个月发布了 26 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.6.5latest2026/11/091 天前
0.6.42026/11/081 天前
0.6.32026/11/081 天前
0.6.22026/10/2614 天前
0.6.12026/10/2417 天前
0.6.02026/10/2317 天前

关于 @cloudhal/cli 的常见问题

@cloudhal/cli 是什么?

cloudhal — create, run and build Cloudhal sites locally

如何安装 @cloudhal/cli?

在项目目录运行 npm install @cloudhal/cli,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:astro(^7.4.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。它还提供命令行工具 cloudhal,可以用 npx cloudhal 直接运行。

@cloudhal/cli 支持 TypeScript 吗?

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

@cloudhal/cli 支持 ESM 还是 CommonJS?

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

@cloudhal/cli 需要什么版本的 Node.js?

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

@cloudhal/cli 可以商用吗?

@cloudhal/cli 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@cloudhal/cli 的下载量有多少?

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

@cloudhal/cli 还在维护吗?

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