PackagesCli

@lunora/cli

The standalone `lunora` binary — scaffold, codegen, deploy.

@lunora/cli is the standalone alternative to the Vite plugin. It powers the lunora binary you got when you ran npx lunorash@alpha init. The plugin and the CLI share the same @lunora/codegen core, so the generated files are identical regardless of how they were produced.

Commands

# Project
lunora init [name] [-t <template>]      # scaffold a new project (default: react)
            [-i | --yes]                 #   offer (or skip) adding auth + email
lunora add <auth|email>                 # add a feature to the current project
lunora view [--remote]                  # open the Lunora studio in your browser
lunora docs [section]                   # open the docs site in your browser
lunora info [--json]                    # print versions, wrangler summary, schema overview
lunora doctor                           # preflight the project (bindings, placeholders, secrets)
lunora registry <add|list|view|build>   # component registry
lunora rules <install|check>            # install the AI agent skills into .agents/skills/

# Develop
lunora mcp install [client…]            # wire Lunora's MCP servers into your editor
            [--list] [--print] [--force]
            [--docs-only] [--local-only]
            [--global | --project]
lunora mcp uninstall [client…]          # remove them again
lunora mcp serve [--allow-writes]       # the stdio MCP server your editor spawns
            [--no-docs] [--url u] [--token t]
lunora dev [--port n] [--worker-port n] # wrangler worker + studio + codegen watch
            [--no-studio] [--no-codegen]
lunora codegen [--api-spec <spec>]      # one-shot codegen
            [--format <pretty|json>]
lunora run <fn> [--args <json>]         # send a single RPC to a running worker
            [--shard <key>] [--url <u>]
lunora insights [--shard <key>]         # write-conflict / error / latency hot-spots
            [--limit n] [--json] [--prod --url <u>]
lunora reset [--all] [--yes]            # clear local Miniflare state

# Deploy
lunora prepare [--api-spec <spec>]      # codegen + wrangler validation — for CI
lunora build [--out-dir <dir>]          # bundle the worker to disk without deploying
lunora deploy [--env <name>]            # codegen, validate wrangler, then wrangler deploy
              [--migrate] [--prebuilt]  #   --preview uploads a version (no live traffic)
              [--preview] [--dry-run]   #   --dry-run validates + bundles, never publishes
              [--temporary]             #   --temporary deploys with no account (~60min, then claim)
lunora link --url <url> [--env <name>]  # link this checkout to its deployed worker
lunora deployments <list|inspect|rollback|promote>   # history + traffic control
             [--env <name>] [--yes]
lunora verify [--api-spec <spec>]       # dry-run codegen + tsc --noEmit (no files written)
              [--no-typecheck]
lunora analyze [--json]                 # wrangler dry-run: bundle size + top modules
lunora containers <build|push|images|list|info|delete> [args…]
            [--tag <name:tag>] [--push] [--env <n>]

# Data
lunora logs [worker]                    # stream live Worker logs via wrangler tail
            [--format <pretty|json>]    #   [--status <s>] [--search <q>] [--env <n>]
lunora migrate <generate|create|up|down|status> [name|id]
lunora env <list|get|set|unset|push|diff|doctor>
lunora export [--out <file>] [--tables <t1,t2>] [--url <u>] [--token <tok>]
lunora import <file> [--table <name>] [--batch-size n]
lunora backup <create|list|restore|pitr> [--dir <d>] [--at <iso>]
lunora seed [--table <t>] [--count n]   # deterministic fake data from schema.ts
            [--seed n] [--dry-run] [--reset]
lunora introspect [--url <u>] [--tables <t1,t2>]  # scaffold schema.ts from an
                  [--schema <s>] [--no-procedures] # existing Postgres/MySQL DB

lunora init

Scaffolds a new Lunora project by fetching a template from gh:anolilab/lunora/templates/<type>#<version>. init (and add) resolve that release branch to the immutable commit SHA it currently points at — logged as pinned … → <sha> — so the fetch is reproducible and tamper-evident; if the SHA can't be resolved (offline / rate-limited) it falls back to the branch with a one-line UNPINNED warning. Pass -t / --template to choose the starting point:

ValueDescription
reactReact SPA — the official create-vite base + the Lunora layer (default)
vueVue SPA — create-vite base + Lunora
solidSolid SPA — create-vite base + Lunora
svelteSvelte SPA — create-vite base + Lunora
nextNext.js (App Router) — OpenNext on Cloudflare + a standalone Lunora worker
tanstack-start-reactTanStack Start (React) — SSR with live-loader routes
tanstack-start-solidTanStack Start (Solid)
react-routerReact Router v7 (framework mode) — SSR composed into the Lunora worker
astroAstro + a standalone Lunora worker
analogAnalogJS (Angular) — single-worker, Lunora mounted in Nitro
nuxtNuxt (Vue) — single-worker, Lunora mounted in Nitro
sveltekitSvelteKit + a standalone Lunora worker
expoReact Native (Expo) — an iOS/Android/web app + a Lunora worker backend
standaloneWorker only — no frontend

The four create-vite frameworks (react, vue, solid, svelte) scaffold through the overlay engine — the official create-vite base plus the Lunora layer — while the rest are bespoke Lunora templates. Run lunora init with no -t to pick from the same list interactively.

Additional flags:

  • --from <dir> — copy from a local templates root instead of fetching remotely (offline-friendly; expects <type>/ subdirs)
  • --source <ref> — override the remote template source (e.g. gh:owner/repo/sub#ref)
  • --allow-unsafe-source — permit --source values outside gh:/github:/https://
  • --here — add Lunora to an existing project: detect the framework, patch the config, scaffold lunora/, print per-framework wiring steps
  • -i / --interactive — after scaffolding, offer to add authentication and transactional email. Defaults on when stdin is a TTY; never prompts in CI.
  • -y / --yes — skip the auth/email offer and scaffold only.
  • --ci <github\|gitlab> — also scaffold a CI deploy pipeline (.github/workflows/deploy.yml for GitHub Actions, .gitlab-ci.yml for GitLab CI): a production lunora deploy on the default branch and a lunora deploy --preview on every pull / merge request. Set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as the provider's secrets / CI-CD variables.

lunora add

Adds a feature to the current Lunora project (you must be inside one — a lunora/ directory and a wrangler.jsonc). A thin front door over lunora registry add: it maps a feature to its registry item(s), applies them, and prints the next steps.

lunora add auth                 # authentication (asks which provider)
lunora add auth --provider clerk   # Clerk, without prompting (also: auth0, auth)
lunora add auth --yes           # default provider (email & password), no prompt
lunora add email                # transactional email (Cloudflare Email Workers + dev mail catcher)
FeatureInstallsNotes
auththe auth registry item (or auth-clerk / auth-auth0)Adds @lunora/auth + a D1 DB binding; verification / reset mail is captured into the studio Mail tab in dev
emailthe mail registry itemCloudflare Email Workers transport (SEND_EMAIL binding) + the dev mail catcher

Flags: --provider <auth\|clerk\|auth0>, --yes, --from <dir> (local registry root), --source <ref>, --allow-unsafe-source.

lunora view

Opens the Lunora studio in your browser. Defaults to the local dev studio; --remote opens the deployed worker's studio instead (resolved from .lunora/project.json, see lunora link).

lunora view            # local dev studio
lunora view --remote   # the deployed studio

lunora docs

Opens the documentation site in your browser. Pass an optional section path to jump straight to a page.

lunora docs                  # the docs home
lunora docs addons/studio    # a specific section

lunora info

Prints the resolved project configuration: the installed @lunora/* versions, a wrangler.jsonc summary, and an overview of the tables declared in lunora/schema.ts. --json emits the same snapshot machine-readably — useful in bug reports and CI diagnostics.

lunora info
lunora info --json

lunora doctor

A read-only preflight over the current project. It reports pass / warn / fail for:

  • wrangler.jsonc — that it exists, parses as JSONC, and declares the SHARD Durable Object binding.
  • D1 placeholders — a database_id still left at the scaffold placeholder.
  • Email destination — a send_email binding whose destination_address is still a placeholder.
  • .dev.vars — secrets present but left with unfilled values.
  • LUNORA_ADMIN_TOKEN — whether it is set.
  • Containers — every container declared in your config is actually exported by the worker entry.
  • Version skew@lunora/* packages spanning different versions, or mixing release channels (e.g. stable + alpha).
lunora doctor

It writes nothing and exits non-zero when any hard check fails, so it works as a CI gate. See Debugging for how to act on each finding.

lunora dev

Starts three concurrent processes: wrangler dev (Worker), the embedded Lunora studio, and codegen in watch mode. All three reload on file changes. In a project on @lunora/vite it spawns vite dev instead — the Vite plugin already runs the worker, studio, and codegen inside the Vite dev server.

lunora dev                # default ports: studio 6173, worker 8787
lunora dev --port 7000    # custom studio port
lunora dev --no-studio    # Worker + codegen only

Background mode (AI agents)

--background starts the dev server as a managed detached process: the command blocks until the server accepts requests, prints the URL + PID, then returns. A state record at .lunora/dev.json acts as a lockfile — starting again while a server runs reports the existing instance instead of spawning a conflict, and stop / status / logs resolve the running instance from it. Every subcommand is idempotent: stopping when nothing runs succeeds silently.

lunora dev --background   # detach; blocks until ready, prints URL + PID
lunora dev status         # URL, PID, uptime (add --json for a machine-readable doc)
lunora dev logs           # captured output of a background run (--lines n, 0 = all)
lunora dev stop           # SIGTERM, SIGKILL escalation after 10s; clears the record

When an AI coding agent is detected (Claude Code, Cursor, Codex, Gemini CLI, Cline, …) background mode and JSON logging turn on automatically — no flags needed in agent workflows. Set LUNORA_AGENT_MODE=0 to opt out (or =1 to force it). Agents can also poll GET /_lunora/status on the running worker — a public, secret-free health probe answering {"ok":true}.

JSON log lines are available to everyone via lunora dev --json or LUNORA_LOG_JSON=1.

lunora deploy

Runs codegen, validates wrangler.jsonc, then invokes wrangler deploy. Pass --migrate to apply pending data migrations against the live worker immediately after a successful deploy.

lunora deploy
lunora deploy --env staging
lunora deploy --migrate --migrate-token $LUNORA_ADMIN_TOKEN
lunora deploy --temporary    # no Cloudflare account needed
lunora deploy --dry-run      # run every pre-deploy gate, publish nothing

You don't need a Cloudflare account to try a deploy. --temporary ships to a temporary account (wrangler deploy --temporary): the Worker is live for about 60 minutes, then you either claim it into an account or it's deleted. An account is only required to keep a deployment. (Wrangler errors if you're already authenticated, so drop --temporary once you've signed in.)

--dry-run runs the full pre-deploy pipeline — codegen, the schema-drift gate, wrangler.jsonc validation, and the wrangler bundle — without publishing.

A successful first deploy auto-writes .lunora/project.json (see lunora link) from the deployed URL, so follow-up commands don't need --url.

--preview uploads a new Worker version (wrangler versions upload) and prints a preview URL instead of going live — production traffic is untouched, and the post-deploy steps (migrations, baseline re-bless, auto-link) are skipped. The --ci pipelines use it to deploy a preview on every pull / merge request.

lunora build

Runs the full pre-deploy pipeline (codegen, the schema-drift gate, wrangler.jsonc validation) and writes the bundled Worker to disk without publishing (wrangler deploy --dry-run --outdir). This is the build half of a build/deploy split — produce a verified artifact in one CI step, then ship it with lunora deploy --prebuilt in another.

lunora build                  # bundle to .lunora/build
lunora build --out-dir dist-worker

lunora deploy --prebuilt skips codegen + the schema-drift gate (trusting the prior build / prepare); wrangler still bundles the Worker.

Records the deployed Worker's name + public URL in a gitignored .lunora/project.json, so commands that target a live worker stop needing --url on every invocation.

lunora link --url https://my-app.acme.workers.dev
lunora link --url https://my-app.acme.workers.dev --env production
lunora link --remove

Once linked, lunora run, lunora logs, and lunora deploy --migrate resolve the worker from the link automatically. The bulk / destructive commands (export, import, migrate, backup, seed, insights) use the link only under --prod, so a production link never silently becomes the target of an unguarded write. The link carries only public identifiers — never secrets.

lunora deployments

Inspect deployment history and move traffic between Worker versions (wraps wrangler versions / wrangler rollback):

lunora deployments list                       # 10 most recent deployments
lunora deployments inspect <version-id>       # view a specific Worker version
lunora deployments rollback --yes             # roll back to the previous version
lunora deployments promote <version-id> --yes # send 100% of traffic to a version

rollback and promote change live traffic, so they require --yes.

lunora prepare

Same pipeline as lunora deploy but stops before wrangler deploy — no Vite step, no network traffic. Use it in CI to catch codegen drift and wrangler.jsonc validation errors before the deploy job runs.

lunora verify

Validates wrangler.jsonc, runs a codegen dry-run, and type-checks the project with tsc --noEmit. Nothing is written to disk. Exits non-zero on any error so you can gate merges on it.

lunora migrate

Manages both schema migrations (D1 SQL) and online data migrations (row transforms that run against a live worker):

lunora migrate generate add_email_index   # diff schema.ts → emit D1 SQL
lunora migrate create --name backfill_at  # scaffold a data-migration stub
lunora migrate up                         # run pending data migrations (dev)
lunora migrate up --prod --url <url>      # run against production (requires --yes)
lunora migrate status                     # show pending / applied / failed

generate parses lunora/schema.ts, filters to .global() tables (sharded tables live in per-DO SQLite — no migration needed), diffs against lunora/migrations/.snapshot.json, and emits a timestamped SQL file. Commit both the SQL and the snapshot — they are deterministic.

lunora run

Send a single RPC to a running Worker without spinning up the client SDK:

lunora run messages:send --args '{"channelId":"general","text":"hi"}'

--shard overrides shard routing; --url lets you point at a deployed Worker instead of http://localhost:8787.

lunora codegen

Runs codegen once and exits — the one-shot form of what lunora dev and the Vite plugin do in watch mode. It reads lunora/schema.ts plus your function files and writes lunora/_generated/.

lunora codegen
lunora codegen --api-spec both     # also emit openapi.json and openrpc.json
lunora codegen --format json       # machine-readable output

--api-spec selects which API description to emit alongside the generated modules: openapi (the default) writes openapi.json, openrpc writes openrpc.json, both writes both, and none writes neither.

lunora insights

Reports per-function metrics from a running worker, ranked into three sections:

  • Write-conflict hot-spots — functions that lose OCC retries most often. These are your sharding candidates: a high conflict rate means many writers are contending on one Durable Object.
  • Error hot-spots — functions by error rate, with the most recent error message.
  • Latency outliers — slowest single call, plus the mean, per function.
lunora insights                      # against the local dev worker
lunora insights --shard channel:demo # scope to one shard
lunora insights --limit 25           # more rows per section (default 10)
lunora insights --json               # raw report
lunora insights --prod --url https://app.example.com --token $LUNORA_ADMIN_TOKEN

Targeting production requires an explicit --url. See Sharding for what to do about a write-conflict hot-spot, and Performance for the latency side.

lunora reset

Clears local Miniflare state — the simulated Durable Objects, D1, R2, and KV that lunora dev persists under .wrangler/state. This is the "start from an empty database" button for local development; it never touches a deployed worker.

lunora reset          # clear Miniflare state (prompts to confirm)
lunora reset --all    # also remove .lunora-cache
lunora reset --yes    # skip the prompt (required when stdin is not a TTY)

lunora env

Manage .dev.vars (local secrets) and push them to Cloudflare via wrangler secret:

lunora env list              # list all keys in .dev.vars
lunora env get DATABASE_URL  # read a single key
lunora env set FOO bar       # write a key
lunora env unset FOO         # remove a key
lunora env push              # upload to Cloudflare (prompts unless --yes)
lunora env push --prod --yes # push to the production environment
lunora env diff              # compare local .dev.vars keys against Cloudflare
lunora env doctor            # validate .dev.vars against wrangler.jsonc bindings

diff reads the deployed Worker's secret names via wrangler secret list and reports which keys are local-only (need a push), remote-only, or in both. Cloudflare never returns secret values (they are write-only), so diff compares names — not values. Pass --prod to target the production environment.

lunora export / lunora import

Bulk data transfer between workers — mirrors Convex's convex export / convex import:

lunora export --out ./backup.ndjson
lunora export --tables messages,channels --url https://my-worker.workers.dev --token $TOKEN

lunora import ./backup.ndjson
lunora import ./users.ndjson --table users   # wrap bare docs as {table,doc} envelopes

lunora backup

Managed snapshot backups and native point-in-time recovery (PITR):

lunora backup create                   # snapshot the running shard
lunora backup list                     # list available snapshots
lunora backup restore <id>             # restore a snapshot
lunora backup pitr --at 2024-06-01T12:00:00Z   # read PITR bookmark
lunora backup pitr --at 2024-06-01T12:00:00Z --restore --yes   # restore to that point

See Backups, export & import for the full picture — snapshot format, restore drills, and moving data between deployments.

lunora seed

Generates deterministic fake data from lunora/schema.ts and bulk-inserts it through the worker's admin endpoint. Rows are derived from a seed number, so the same --seed always produces identical data — which makes seeded fixtures reproducible across machines and CI runs.

lunora seed                          # every table, default 10 rows each
lunora seed --table posts --count 50 # 50 posts; FK parents seeded automatically
lunora seed --seed 7 --dry-run       # print the NDJSON for seed 7, insert nothing
lunora seed --reset                  # wipe local .wrangler/state, then seed

Seeding respects foreign keys: naming one table with --table also seeds the tables it references. --batch-size (default 500) sets rows per HTTP request. Targeting a non-local worker needs --prod plus an explicit --url, and prompts unless you pass --yes; prefer LUNORA_ADMIN_TOKEN over --token, which is visible to other local processes through the process table. See @lunora/seed for using the same generator inside tests.

lunora introspect

Reads an existing Postgres or MySQL database and scaffolds lunora/schema.ts from it, plus a list/get procedure module per table. Use it when you're adopting Lunora on top of a database that already exists, instead of transcribing the schema by hand.

lunora introspect --url postgres://localhost/shop  # every base table
lunora introspect --tables users,orders            # just these (reads $DATABASE_URL)
lunora introspect --dry-run                        # print, write nothing
lunora introspect --no-procedures --force          # schema only, overwrite

The command is read-only against the source database — it queries information_schema (and pg_index on Postgres) and never writes.

Re-runs merge, they don't clobber. The first run writes a whole lunora/schema.ts; after that the file is yours, and a second run folds only what's new — tables, columns, indexes — into it as additive edits, preserving your formatting, comments, and any validator you tightened. That goes through the same ts-morph editor the Studio schema editor uses, so two of its rules apply: a column added to a table that already exists lands v.optional(...) (a required one needs a backfill migration), and names that aren't bare identifiers are reported and skipped. Nothing is ever removed — a column dropped upstream stays, because deleting it would drop rows. Pass --force to overwrite instead of merge, and --dry-run to see the plan first.

What it emits is a starting point you own, not a build artifact: review it before shipping. Specifically:

  • Every table is .global({ backend: "hyperdrive" }), because the rows live in the external database. Point the HYPERDRIVE binding at it — see @lunora/hyperdrive.
  • Lunora mints its own _id, so the source primary key is carried over as a unique index rather than replacing it.
  • Foreign keys become v.id("<target>"); a type with no direct validator becomes v.any() with a TODO beside it, and is reported as a warning.
  • The emitted procedures are RPC-only. Publishing one over REST stays an explicit .expose({ rest: true }) decision you make after adding whatever auth or RLS the table needs — introspect cannot know who may read your data.
  • list is built on defineListArgs, so only index-backed columns are filterable and paging is keyset-based. That bounds which columns a caller can reach, not the cost of every operator over them — review the generated filter list before exposing the procedure.

The driver is loaded on demand and is not a CLI dependency: install pg or mysql2 in your project first.

lunora registry

Fetch and manage reusable Lunora components (queries, mutations, UI widgets):

lunora registry list
lunora registry view auth/session-token
lunora registry add auth/session-token
lunora registry build   # regenerate the local catalog index.json

lunora rules

Installs the Lunora agent skills — portable instructions that teach AI coding agents (Claude Code, Cursor, Copilot) how to use Lunora — into the project's .agents/skills/. lunora dev, the Vite plugin, and the studio nudge you to run this when the rules are missing.

lunora rules install              # copy the skills into .agents/skills/ (skips edited files)
lunora rules install --overwrite  # reinstall, replacing local edits
lunora rules check                # report which skills are present
lunora rules check --strict       # exit non-zero when missing (CI gate)

lunora mcp

Connects AI editors to Lunora over the Model Context Protocol. Where lunora rules teaches an agent how Lunora works, this gives it live tools: documentation search, dev-server status and logs, and typed access to your app's functions.

lunora mcp install                      # every MCP client already configured here
lunora mcp install claude-code cursor   # or name them
lunora mcp install --list               # supported clients and their config files
lunora mcp install --print              # show the config without writing it
lunora mcp install --force              # replace entries that already exist
lunora mcp install --docs-only          # skip this project's local server

install writes two servers, and they go to different places by default: lunora-docs, the hosted documentation server at https://lunora.sh/mcp, is the same URL in every project, so it lands in the client's machine-wide config; lunora, which runs lunora mcp serve for this app, only means anything inside the project, so it lands in the project config. Pass --global or --project to force both one way. Where a client only has one of the two (Zed has no project config, VS Code no global one), it falls back rather than skipping the client.

lunora mcp uninstall removes both again, from both scopes and every client by default — it touches only the two entries we wrote, leaving the rest of the file and its comments alone. It knows each client's own config file, top-level key, and entry shape, so you don't have to remember that VS Code says servers where Cursor says mcpServers:

ClientConfig file
claude-code.mcp.json
cursor.cursor/mcp.json
vscode.vscode/mcp.json
gemini.gemini/settings.json
claude-desktopthe OS application-data directory
windsurf~/.codeium/windsurf/mcp_config.json
codex~/.codex/config.toml (snippet printed to paste)

Existing entries are left alone unless you pass --force, comments in a JSONC config survive the edit, and a file that doesn't parse is reported rather than overwritten.

lunora mcp serve is the stdio server those entries spawn — you rarely run it by hand. It takes no required configuration: the dev server's URL comes from .lunora/dev.json and the admin token from .dev.vars, both re-read per tool call, so starting lunora dev after your editor is already open just works. It exposes the documentation tools, lunora_dev_status / lunora_dev_logs, and the read-only deployment tools; --allow-writes adds the mutation and action tools.

lunora analyze

Runs a wrangler dry-deploy and reports bundle size, the heaviest modules, and the state of _generated/ files. Pass --json to pipe the output to a CI artifact.

lunora containers

Thin wrappers over wrangler containers …, so container image and instance management lives under the same CLI as the rest of the deploy workflow. The split matters in CI: build and push the image in one step, then ship the worker with lunora deploy in another.

lunora containers build ./containers/transcoder --tag transcoder:v1
lunora containers build ./containers/transcoder --tag transcoder:v1 --push
lunora containers push transcoder:v1
lunora containers images list
lunora containers images delete transcoder:v1

build uses your local Docker engine; --push uploads to the Cloudflare Registry in the same step. --env selects the Cloudflare environment. See @lunora/container for declaring containers in the first place.

lunora logs

Streams live logs from a deployed Lunora Worker by wrapping wrangler tail:

lunora logs
lunora logs my-worker --format json --status error
lunora logs --search "auth" --env staging

Writing Lunora functions by hand

Queries, mutations, and actions are plain TypeScript files under lunora/. There is no scaffolding command — create the file yourself:

lunora/messages.ts
import { mutation, query, v } from "@/lunora/_generated/server";

export const list = query.input({ channelId: v.id("channels"), limit: v.optional(v.number()) }).query(async ({ ctx, args: { channelId, limit } }) => {
    return ctx.db
        .query("messages")
        .withIndex("by_channel", (q) => q.eq("channelId", channelId))
        .order("desc")
        .take(limit ?? 50);
});

export const send = mutation.input({ channelId: v.id("channels"), text: v.string() }).mutation(async ({ ctx, args: { channelId, text } }) => {
    await ctx.db.insert("messages", {
        channelId,
        userId: ctx.auth.userId!,
        text,
        createdAt: Date.now(),
    });
});

After saving, lunora dev picks up the change and re-runs codegen automatically. Run lunora codegen once manually if you are outside the dev loop.

Codegen output

lunora/_generated/:

  • api.ts — the typed api.<file>.<function> namespace
  • server.tsinternalQuery, internalMutation exports re-typed against your schema
  • dataModel.tsDoc<"messages">, Id<"users">, etc.

These files are deterministic — commit them. CI re-runs codegen and diffs the result to catch drift.

See also