@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands
1,032 lines (998 loc) • 47.6 kB
JavaScript
import { mkdirSync, readFileSync, lstatSync, writeFileSync } from 'node:fs';
import { resolveHint, isLunoraError, flattenHint, findSolutionByMessage } from '@lunora/errors';
import { createCerebro } from '@visulima/cerebro';
import completionCommand from '@visulima/cerebro/command/completion';
import versionCommand from '@visulima/cerebro/command/version';
import { A as API_SPEC_HELP } from './api-spec-Bx0iKbxA.mjs';
import { createLogger } from './createLogger-CIWSHrTL.mjs';
import { VisulimaError, renderError } from '@visulima/error';
import { homedir } from 'node:os';
import { join } from 'node:path';
const addCommand = {
argument: {
description: "Feature or registry item: ai | auth | email | storage | crons | presence | queue | workflow | flags | backup | …",
name: "feature",
type: String
},
description: "Add a feature or registry item (ai, auth, email, storage, crons, …) to the current Lunora project",
examples: [
["lunora add auth", "Add authentication (asks which provider)"],
["lunora add auth --provider clerk", "Add Clerk auth without prompting"],
["lunora add email", "Add transactional email (Cloudflare Email Workers + dev mail catcher)"],
["lunora add storage", "Add the R2 storage registry item (asks for the bucket name)"],
["lunora add storage --bucket my-app-uploads", "Add storage with a bucket name, no prompt"],
["lunora add crons", "Add the scheduled-jobs registry item"],
["lunora add storage --ref alpha", "Add an item from the alpha branch's registry"]
],
group: "Project",
loader: () => import('../packem_chunks/handler.mjs').then((m) => {
return { default: m.execute };
}),
name: "add",
options: [
{ description: "auth: provider to use without prompting (auth | clerk | auth0)", name: "provider", type: String },
{ description: "auth: D1 database name to use without prompting (lowercase alphanumeric + hyphens)", name: "db", type: String },
{ description: "storage: R2 bucket name to use without prompting (lowercase alphanumeric + hyphens)", name: "bucket", type: String },
{ description: "email: verified destination address to use without prompting", name: "mail-to", type: String },
{ description: "Skip prompts (auth provider, DB name, bucket name, mail destination) and use the defaults", name: "yes", type: Boolean },
{ description: "Local registry root (offline; expects <name>/ subdirs)", name: "from", type: String },
{ description: "Override the remote registry source base (e.g. gh:owner/repo/registry)", name: "source", type: String },
{
description: "Fetch items from a git ref (branch, tag, or commit), e.g. --ref alpha. Overrides the version-derived default",
name: "ref",
type: String
},
{ description: "Permit --source values outside gh:/github:/https://", name: "allow-unsafe-source", type: Boolean },
{ description: "Output format: pretty (default) or json", name: "format", type: String }
]
};
const analyzeCommand = {
description: "Run wrangler dry-run and report bundle size, top modules, and _generated files",
examples: [["lunora analyze", "Report the worker bundle size + heaviest modules"]],
group: "Deploy",
loader: () => import('../packem_chunks/handler2.mjs').then((m) => {
return { default: m.execute };
}),
name: "analyze",
options: [{ description: "Emit a JSON report instead of human text", name: "json", type: Boolean }]
};
const backupCommand = {
argument: { description: "create | list | restore <id|file> | pitr", name: "subcommand", type: String },
description: "Managed snapshot backups (create | list | restore) plus native point-in-time recovery (pitr)",
examples: [
["lunora backup create", "Snapshot every table to a backup file"],
["lunora backup list", "List recorded snapshots"],
["lunora backup restore <id>", "Restore a snapshot by id"],
["lunora backup pitr --at 2026-06-01T00:00:00Z", "Point-in-time recovery (≤30 days)"]
],
group: "Data",
loader: () => import('../packem_chunks/handler3.mjs').then((m) => {
return { default: m.execute };
}),
name: "backup",
options: [
{ description: "Backup directory (default .lunora-backups)", name: "dir", type: String },
{ description: "Comma-separated table allowlist (create)", name: "tables", type: String },
{ description: "pitr: time to read/restore to (ISO or epoch-ms, ≤30 days)", name: "at", type: String },
{ description: "pitr --restore: explicit bookmark to restore to (wins over --at)", name: "bookmark", type: String },
{ description: "pitr: perform a restore instead of just reading the bookmark", name: "restore", type: Boolean },
{ description: "pitr --restore: restart the shard now so recovery applies immediately", name: "restart", type: Boolean },
{ description: "pitr: target shard key (default: root shard)", name: "shard", type: String },
{ description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
{ description: "Confirm a production pitr --restore (required with --prod)", name: "yes", type: Boolean },
{ description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
{
description: "Admin bearer token (prefer LUNORA_ADMIN_TOKEN; --token is visible to other local processes via the process table)",
name: "token",
type: String
}
]
};
const buildCommand = {
description: "Codegen + validate + bundle the Worker to disk without deploying",
examples: [
["lunora build", "Bundle to .lunora/build without deploying"],
["lunora build --out-dir dist-worker", "Bundle to a custom directory"]
],
group: "Deploy",
loader: () => import('../packem_chunks/handler4.mjs').then((m) => {
return { default: m.execute };
}),
name: "build",
options: [
{ description: `Which API spec(s) to emit: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
{ description: "Output format: pretty (default) or json", name: "format", type: String },
{ description: "Directory to write the bundled Worker to (default .lunora/build)", name: "out-dir", type: String }
]
};
const codegenCommand = {
description: "Run codegen for lunora/ functions and schema",
examples: [["lunora codegen", "Generate lunora/_generated/ from your schema + functions"]],
group: "Develop",
loader: () => import('../packem_chunks/runCodegenCommand.mjs').then((m) => {
return { default: m.execute };
}),
name: "codegen",
options: [
{ description: `Which API spec(s) to emit: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
{ description: "Output format: pretty (default) or json", name: "format", type: String }
]
};
const containersCommand = {
argument: { description: "<build|push|images|list|info|delete> [args…]", name: "args", type: String },
description: "Build/push container images and manage container instances (wraps wrangler containers)",
examples: [
["lunora containers build ./containers/transcoder --tag transcoder:v1", "Build a container image with the local Docker engine"],
["lunora containers build ./containers/transcoder --tag transcoder:v1 --push", "Build and push to the Cloudflare Registry in one step"],
["lunora containers push transcoder:v1", "Push a locally-tagged image to the Cloudflare Registry"],
["lunora containers images list", "List images in your Cloudflare Registry"],
["lunora containers images delete transcoder:v1", "Delete an image to free registry storage"]
],
group: "Deploy",
loader: () => import('../packem_chunks/handler5.mjs').then((m) => {
return { default: m.execute };
}),
name: "containers",
options: [
{ description: "build: push the image to the Cloudflare Registry after building", name: "push", type: Boolean },
{ description: "build: name:tag for the image (forwarded to wrangler --tag)", name: "tag", type: String },
{ description: "Cloudflare environment name", name: "env", type: String }
]
};
const deployCommand = {
description: "Codegen, validate wrangler, then wrangler deploy",
examples: [
["lunora deploy", "Deploy to Cloudflare"],
["lunora deploy --env production", "Deploy to a named environment"],
["lunora deploy --dry-run", "Validate + bundle without publishing"],
["lunora deploy --migrate", "Deploy, then run pending data migrations"]
],
group: "Deploy",
loader: () => import('../packem_chunks/runDeployCommand.mjs').then((m) => {
return { default: m.execute };
}),
name: "deploy",
options: [
{ description: "Override the schema-drift gate (deploy even with breaking schema drift and no migration)", name: "allow-schema-drift", type: Boolean },
{ description: `Which API spec(s) to emit: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
{ description: "Validate, bundle, and run pre-deploy gates without publishing (wrangler deploy --dry-run)", name: "dry-run", type: Boolean },
{ description: "Cloudflare environment name", name: "env", type: String },
{ description: "Output format: pretty (default) or json", name: "format", type: String },
{ description: "After a successful deploy, run pending data migrations against the live worker", name: "migrate", type: Boolean },
{
description: "Skip codegen + the schema-drift gate (assumes `lunora build`/`prepare` already ran in this CI run). Wrangler still bundles the worker.",
name: "prebuilt",
type: Boolean
},
{ description: "Admin bearer token for --migrate (falls back to LUNORA_ADMIN_TOKEN)", name: "migrate-token", type: String },
{
description: "Worker URL for --migrate (REQUIRED with --migrate; the deploy target URL is not captured automatically)",
name: "migrate-url",
type: String
},
{ description: "Confirm running the production data migration triggered by --migrate (required with --migrate)", name: "migrate-yes", type: Boolean },
{
description: "Upload a preview version (wrangler versions upload) instead of going live — prints a preview URL; doesn't shift production traffic",
name: "preview",
type: Boolean
},
{
description: "Deploy to a temporary Cloudflare account when unauthenticated (wrangler deploy --temporary; live ~60min, then claim or it's deleted). Wrangler errors if you're already authenticated.",
name: "temporary",
type: Boolean
},
{
description: "Re-bless the committed schema baseline (lunora/.lunora-schema.json) with the current shape",
name: "update-schema-baseline",
type: Boolean
}
]
};
const deploymentsCommand = {
argument: { description: "list | inspect <version-id> | rollback [version-id] | promote <version-id>", name: "subcommand", type: String },
description: "List deployments and roll back / promote / inspect Worker versions",
examples: [
["lunora deployments list", "Show the 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"]
],
group: "Deploy",
loader: () => import('../packem_chunks/handler6.mjs').then((m) => {
return { default: m.execute };
}),
name: "deployments",
options: [
{ description: "Cloudflare environment name", name: "env", type: String },
{ description: "Display `list` output as JSON", name: "json", type: Boolean },
{ description: "Reason/description recorded with a rollback or promote", name: "message", type: String },
{ description: "Confirm a rollback or promote (required — these change live traffic)", name: "yes", type: Boolean }
]
};
const devCommand = {
argument: {
description: "Optional subcommand: stop (shut the running dev server down) | status (report it) | logs (print its captured output)",
name: "args",
type: String
},
description: "Run the dev stack: wrangler worker + studio + codegen watch",
examples: [
["lunora dev", "Run the worker + studio + codegen watch"],
["lunora dev --background", "Run detached: blocks until ready, prints URL + PID, then returns"],
["lunora dev stop", "Stop the background/tracked dev server (idempotent)"],
["lunora dev status", "Report the running dev server (URL, PID, uptime)"],
["lunora dev logs", "Print the captured dev-server log (background runs)"],
["lunora dev --json", "Machine-readable JSON log lines (also LUNORA_LOG_JSON=1)"],
["lunora dev --no-studio", "Skip the embedded studio server"],
["lunora dev --worker-port 8080", "Use a custom wrangler dev port"],
["lunora dev --remote", "Proxy D1/KV/R2 to the deployed worker (also LUNORA_REMOTE=1)"]
],
group: "Develop",
loader: () => import('../packem_chunks/planDevCommand.mjs').then((m) => {
return { default: m.execute };
}),
name: "dev",
// KEEP IN SYNC with `daemonArguments` in `./lifecycle.ts`: a new flag that
// must reach a `--background` daemon has to be forwarded there explicitly.
options: [
{ description: `Which API spec(s) codegen emits: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
{ description: "Studio server port (default 6173)", name: "port", type: Number },
{ description: "wrangler dev port (default 8787)", name: "worker-port", type: Number },
{
description: "Run the dev server as a managed background process (auto-enabled when an AI agent is detected; LUNORA_AGENT_MODE=0 disables)",
name: "background",
type: Boolean
},
{ description: "Emit machine-readable JSON log lines (also LUNORA_LOG_JSON=1; auto-enabled for AI agents)", name: "json", type: Boolean },
{ description: "How many trailing lines `lunora dev logs` prints (default 100, 0 = all)", name: "lines", type: Number },
{ description: "Don't start the embedded studio server", name: "no-studio", type: Boolean },
{ description: "Don't watch + regenerate codegen", name: "no-codegen", type: Boolean },
{ description: "Proxy D1/KV/R2 bindings to the deployed worker (or set LUNORA_REMOTE=1)", name: "remote", type: Boolean }
]
};
const docsCommand = {
argument: { description: "Optional path under the docs site (e.g. addons/studio)", name: "section", type: String },
description: "Open the Lunora docs in your browser (optional [section] path)",
examples: [
["lunora docs", "Open the Lunora docs"],
["lunora docs addons/studio", "Open a specific docs section"]
],
group: "Project",
loader: () => import('../packem_chunks/handler7.mjs').then((m) => {
return { default: m.execute };
}),
name: "docs"
};
const doctorCommand = {
description: "Preflight the current Lunora project (wrangler bindings, placeholders, dev secrets)",
examples: [["lunora doctor", "Run the project preflight checks"]],
group: "Project",
loader: () => import('../packem_chunks/handler8.mjs').then((m) => {
return { default: m.execute };
}),
name: "doctor",
options: []
};
const envCommand = {
argument: { description: "list | get <KEY> | set <KEY> <VALUE> | unset <KEY> | generate [KEY] | push | diff | doctor", name: "subcommand", type: String },
description: "Manage .dev.vars and sync secrets via wrangler (list | get | set | unset | generate | push | diff | doctor)",
examples: [
["lunora env list", "List .dev.vars keys"],
["lunora env set API_KEY secret", "Set a local variable"],
["lunora env generate", "Generate strong values for the project's secrets (print KEY=value)"],
["lunora env generate AUTH_SECRET --set", "Generate one secret and write it to .dev.vars"],
["lunora env push --yes", "Upload secrets to Cloudflare"],
["lunora env diff", "Compare local .dev.vars keys against Cloudflare"]
],
group: "Data",
loader: () => import('../packem_chunks/handler9.mjs').then((m) => {
return { default: m.execute };
}),
name: "env",
options: [
{ description: "Target production for `push` (passes --env production to wrangler)", name: "prod", type: Boolean },
{ description: "For `generate` — write the generated secrets into .dev.vars instead of printing them", name: "set", type: Boolean },
{
description: "Push secrets to a temporary-account deployment when unauthenticated (wrangler secret put --temporary). Errors if you're already authenticated.",
name: "temporary",
type: Boolean
},
{ description: "Required for `push` — confirms uploading secrets to Cloudflare", name: "yes", type: Boolean }
]
};
const exportCommand = {
argument: { description: "Optional path (alias for --out)", name: "path", type: String },
description: "Stream NDJSON of every shard-local + global table from the worker",
examples: [
["lunora export --out backup.ndjson", "Dump every table to an NDJSON file"],
["lunora export --tables messages,users", "Export only specific tables"]
],
group: "Data",
loader: () => import('../packem_chunks/handler10.mjs').then((m) => {
return { default: m.execute };
}),
name: "export",
options: [
{ description: "Output file path (`-` for stdout, default)", name: "out", type: String },
{ description: "Comma-separated table allowlist", name: "tables", type: String },
{ description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
{ description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
{
description: "Admin bearer token (prefer LUNORA_ADMIN_TOKEN; --token is visible to other local processes via the process table)",
name: "token",
type: String
}
]
};
const importCommand = {
argument: { description: "Source NDJSON file", name: "file", type: String },
description: "Bulk-insert rows from an NDJSON file via the worker's admin endpoint",
examples: [["lunora import backup.ndjson", "Bulk-insert rows from an NDJSON file"]],
group: "Data",
loader: () => import('../packem_chunks/handler11.mjs').then((m) => {
return { default: m.execute };
}),
name: "import",
options: [
{ description: "Wrap each bare doc as `{table:<name>,doc:...}`", name: "table", type: String },
{ description: "Rows per HTTP request (default 500)", name: "batch-size", type: Number },
{ description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
{ description: "Confirm bulk-writing production (required with --prod)", name: "yes", type: Boolean },
{ description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
{
description: "Admin bearer token (prefer LUNORA_ADMIN_TOKEN; --token is visible to other local processes via the process table)",
name: "token",
type: String
}
]
};
const infoCommand = {
description: "Print resolved project config: @lunora/* versions, wrangler summary, schema overview",
examples: [
["lunora info", "Print resolved project config"],
["lunora info --json", "Emit a JSON snapshot"]
],
group: "Project",
loader: () => import('../packem_chunks/handler12.mjs').then((m) => {
return { default: m.execute };
}),
name: "info",
options: [{ description: "Emit a JSON snapshot instead of human text", name: "json", type: Boolean }]
};
const initCommand = {
argument: { description: "Project name", name: "name", type: String },
description: "Scaffold a new Lunora project",
examples: [
["lunora init my-app", "Scaffold with the default (vite) template"],
["lunora init my-app -t next", "Scaffold a Next.js app"],
["lunora init my-app -t tanstack-start-react", "Scaffold a TanStack Start (React) app"],
["lunora init my-app -t tanstack-start-solid", "Scaffold a TanStack Start (Solid) app"],
["lunora init my-app --ref alpha", "Scaffold from the alpha branch's templates"],
["lunora init --here", "Add Lunora to the current project"],
["lunora init my-app --ci github", "Scaffold + add a GitHub Actions deploy pipeline"],
["lunora init my-app --ci gitlab", "Scaffold + add a GitLab CI deploy pipeline"]
],
group: "Project",
loader: () => import('../packem_chunks/runInitCommand.mjs').then((m) => {
return { default: m.execute };
}),
name: "init",
options: [
{
alias: "t",
// No default: when omitted, an interactive run shows the framework
// picker (default React overlay) and a non-interactive run errors.
// For React/Vue/Solid/Svelte SPAs use `--vite <framework>` (overlay);
// `-t` selects a bespoke template.
description: "Bespoke template (standalone | astro | next | nuxt | sveltekit | tanstack-start-react | tanstack-start-solid). For an SPA use --vite react|vue|solid|svelte.",
name: "template",
type: String
},
{
description: "Scaffold via the create-vite overlay for a framework (react | vue | solid | svelte | vanilla) — official create-vite base + Lunora layer",
name: "vite",
type: String
},
{
description: "Local templates root to copy from (offline-friendly; expects <type>/ subdirs)",
name: "from",
type: String
},
{
description: "Override the remote template source (e.g. gh:owner/repo/sub#ref)",
name: "source",
type: String
},
{
description: "Fetch templates from a git ref (branch, tag, or commit), e.g. --ref alpha. Overrides the version-derived default",
name: "ref",
type: String
},
{
description: "Permit --source values outside gh:/github:/https:// (e.g. local file://)",
name: "allow-unsafe-source",
type: Boolean
},
{
description: "Add Lunora to the current project: detect the framework, patch the config, scaffold lunora/, print per-framework wiring steps",
name: "here",
type: Boolean
},
{
alias: "i",
description: "After scaffolding, offer to add auth + email (defaults on when stdin is a TTY)",
name: "interactive",
type: Boolean
},
{
alias: "y",
description: "Skip the auth/email offer; scaffold only",
name: "yes",
type: Boolean
},
{
description: "Also scaffold a CI deploy pipeline: github (.github/workflows/deploy.yml) or gitlab (.gitlab-ci.yml)",
name: "ci",
type: String
},
{
description: "Add features non-interactively after scaffolding (comma-separated): ai | auth | backup | browser | cloudflare-access | crons | email | flags | hyperdrive | payment | presence | queue | storage | workflow",
name: "add",
type: String
},
{
description: "Walk through every step (prompts + output) without writing files, installing, or running git",
name: "dry-run",
type: Boolean
}
]
};
const insightsCommand = {
description: "Report write-conflict hot-spots, error rates, and latency outliers from a running Worker",
examples: [
["lunora insights", "Report against the local dev worker"],
["lunora insights --shard channel:demo", "Scope the report to one shard"],
["lunora insights --json", "Emit the raw report as JSON"],
["lunora insights --prod --url https://app.example.com --token $LUNORA_ADMIN_TOKEN", "Report against production"]
],
group: "Develop",
loader: () => import('../packem_chunks/handler13.mjs').then((m) => {
return { default: m.execute };
}),
name: "insights",
options: [
{ description: "Explicit shard key (defaults to the root shard)", name: "shard", type: String },
{ description: "Max rows per section (default 10)", name: "limit", type: String },
{ description: "Emit a JSON report instead of human text", name: "json", type: Boolean },
{ description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
{ description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
{ description: "Admin bearer token (or LUNORA_ADMIN_TOKEN)", name: "token", type: String }
]
};
const linkCommand = {
description: "Link this checkout to its deployed Worker (writes .lunora/project.json)",
examples: [
["lunora link --url https://app.acme.workers.dev", "Link to a deployed Worker URL"],
["lunora link --url https://app.acme.workers.dev --env production", "Link a named environment"],
["lunora link --remove", "Remove the link"]
],
group: "Deploy",
loader: () => import('../packem_chunks/handler14.mjs').then((m) => {
return { default: m.execute };
}),
name: "link",
options: [
{ description: "Cloudflare environment name to record alongside the link", name: "env", type: String },
{ description: "Worker name (defaults to the `name` in wrangler config)", name: "name", type: String },
{ description: "Remove the existing link (.lunora/project.json)", name: "remove", type: Boolean },
{ description: "Deployed Worker URL to link (e.g. https://app.acme.workers.dev)", name: "url", type: String }
]
};
const logsCommand = {
argument: { description: "Worker name (defaults to the name in wrangler config)", name: "worker", type: String },
description: "Stream live logs from a deployed Worker, or read the durable log archive with --durable",
group: "Deploy",
loader: () => import('../packem_chunks/handler21.mjs').then((m) => {
return { default: m.execute };
}),
name: "logs",
options: [
{ description: "Cloudflare environment name", name: "env", type: String },
{ description: "Output format: pretty (default) or json", name: "format", type: String },
{ description: "Substring filter on log messages", name: "search", type: String },
{ description: "Filter by invocation status: ok, error, or canceled", name: "status", type: String },
{
description: "Tail a temporary-account deployment when unauthenticated (wrangler tail --temporary). Errors if you're already authenticated.",
name: "temporary",
type: Boolean
},
// --- durable-archive path (--durable) ---
{ description: "Read the durable log archive (pipelineLogSink → R2) via R2 SQL instead of tailing live", name: "durable", type: Boolean },
{ description: "durable: Iceberg table the Pipeline writes to (required with --durable)", name: "table", type: String },
{ description: "durable: Iceberg namespace (R2 Data Catalog database) the table lives in", name: "namespace", type: String },
{ description: "durable: lower time bound (epoch-millis or ISO 8601), inclusive", name: "since", type: String },
{ description: "durable: upper time bound (epoch-millis or ISO 8601), inclusive", name: "until", type: String },
{ description: "durable: exact severity filter (trace|debug|log|info|warn|error|fatal)", name: "level", type: String },
{ description: "durable: severity floor — this level and every more-severe one", name: "min-level", type: String },
{ description: "durable: keep function paths starting with this prefix (LIKE 'prefix%')", name: "function-prefix", type: String },
{ description: "durable: trace-id filter", name: "trace-id", type: String },
{ description: "durable: shard-key filter", name: "shard-key", type: String },
{ description: "durable: user-id filter", name: "user-id", type: String },
{ description: "durable: max rows (clamped to 1–10000; default 500)", name: "limit", type: String },
{ description: "durable: resume after a prior page — the ts from its nextCursor", name: "cursor", type: String },
{ description: "durable: emit one JSON object per line instead of a table", name: "ndjson", type: Boolean }
]
};
const migrateCommand = {
argument: { description: "generate | create | up | down | status | d1-to-hyperdrive [name|id]", name: "subcommand", type: String },
description: "Schema (generate), online data (create | up | down | status), and backend (d1-to-hyperdrive) migrations",
examples: [
["lunora migrate generate", "Diff lunora/schema.ts and emit a SQL migration"],
["lunora migrate create add_users_email", "Scaffold a data migration"],
["lunora migrate up backfill-names", "Run a data migration across shards"],
["lunora migrate status backfill-names", "Report a migration's per-shard status"],
["lunora migrate d1-to-hyperdrive --from-url https://old --to-url https://new", "Copy .global() data from D1 to Hyperdrive"]
],
group: "Data",
loader: () => import('../packem_chunks/runMigrateGenerateCommand.mjs').then((m) => {
return { default: m.execute };
}),
name: "migrate",
options: [
{ description: "Migration name slug (e.g. add_users_email)", name: "name", type: String },
{ description: "Target table for `create` (prompted for interactively when omitted)", name: "table", type: String },
{ description: "Preview a data migration without rewriting rows", name: "dry-run", type: Boolean },
{ description: "Rows per batch for a data migration", name: "batch-size", type: Number },
{ description: "Cap batches processed this run (maps to the runner's maxBatches)", name: "steps", type: Number },
{ description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
{ description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
{
description: "Admin bearer token (prefer LUNORA_ADMIN_TOKEN; --token is visible to other local processes via the process table)",
name: "token",
type: String
},
{ description: "Required with --prod for up/down — confirms running against production", name: "yes", type: Boolean },
// d1-to-hyperdrive backend migration.
{ description: "d1-to-hyperdrive: source (D1) worker URL (defaults to --url)", name: "from-url", type: String },
{ description: "d1-to-hyperdrive: source admin token (defaults to --token / LUNORA_ADMIN_TOKEN)", name: "from-token", type: String },
{ description: "d1-to-hyperdrive: target (Hyperdrive) worker URL (defaults to --url)", name: "to-url", type: String },
{ description: "d1-to-hyperdrive: target admin token (defaults to --token / LUNORA_ADMIN_TOKEN)", name: "to-token", type: String },
{ description: "d1-to-hyperdrive: comma-separated .global() tables to move (default: all global tables)", name: "tables", type: String },
{ description: "d1-to-hyperdrive: keep the intermediate NDJSON dump at this path", name: "out", type: String }
]
};
const prepareCommand = {
description: "Run codegen + binding reconcile + wrangler validation (no Vite) — for CI",
examples: [["lunora prepare", "Codegen + binding reconcile + validate (CI, before deploy)"]],
group: "Deploy",
loader: () => import('../packem_chunks/handler15.mjs').then((m) => {
return { default: m.execute };
}),
name: "prepare",
options: [
{ description: "Override the schema-drift gate (proceed even with breaking schema drift and no migration)", name: "allow-schema-drift", type: Boolean },
{ description: `Which API spec(s) to emit: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
{
description: "Re-bless the committed schema baseline (lunora/.lunora-schema.json) with the current shape",
name: "update-schema-baseline",
type: Boolean
}
]
};
const registryCommand = {
argument: { description: "<add|list|view|build> [item names…]", name: "args", type: String },
description: "Component registry: add/list/view items, or build the catalog",
examples: [
["lunora registry list", "List available registry items"],
["lunora registry add presence", "Scaffold a registry item into lunora/"],
["lunora registry build --check", "Verify the committed catalog is current"]
],
group: "Project",
loader: () => import('../packem_chunks/handler16.mjs').then((m) => {
return { default: m.execute };
}),
name: "registry",
options: [
{ description: "add: print the plan and stop without writing", name: "dry-run", type: Boolean },
{ description: "add: preview the file changes (content diff) and write nothing", name: "diff", type: Boolean },
{ description: "add: force-overwrite existing files (take the incoming copy)", name: "overwrite", type: Boolean },
{ description: "add: skip the package.json mutation confirmation prompt", name: "yes", type: Boolean },
{ description: "Local registry root (offline; expects <name>/ subdirs)", name: "from", type: String },
{ description: "Override the remote registry source base (e.g. gh:owner/repo/registry)", name: "source", type: String },
{
description: "Fetch items from a git ref (branch, tag, or commit), e.g. --ref alpha. Overrides the version-derived default",
name: "ref",
type: String
},
{ description: "Permit --source values outside gh:/github:/https://", name: "allow-unsafe-source", type: Boolean },
{ description: "Emit JSON output (add plan / list)", name: "json", type: Boolean },
{ description: "build: output path for the catalog (default <root>/index.json)", name: "out", type: String },
{ description: "build: verify the index is current instead of rewriting it", name: "check", type: Boolean }
]
};
const resetCommand = {
description: "Clear local Miniflare state (and .lunora-cache with --all)",
examples: [
["lunora reset", "Clear local Miniflare state"],
["lunora reset --all", "Also remove .lunora-cache"]
],
group: "Develop",
loader: () => import('../packem_chunks/runResetCommand.mjs').then((m) => {
return { default: m.execute };
}),
name: "reset",
options: [
{ description: "Also remove .lunora-cache", name: "all", type: Boolean },
{ description: "Skip the confirmation prompt (required when stdin is not a TTY)", name: "yes", type: Boolean }
]
};
const rulesCommand = {
argument: { description: "install | check", name: "subcommand", type: String },
description: "Install the Lunora agent skills (AI rules) into .agents/skills/, or check they're present",
examples: [
["lunora rules install", "Copy the Lunora agent skills into .agents/skills/"],
["lunora rules install --overwrite", "Reinstall, replacing edited skill files"],
["lunora rules check", "Report which Lunora skills are installed"],
["lunora rules check --strict", "Exit non-zero when rules are missing (CI gate)"]
],
group: "Project",
loader: () => import('../packem_chunks/handler17.mjs').then((m) => {
return { default: m.execute };
}),
name: "rules",
options: [
{ description: "install: overwrite skill files that already exist (default: skip them)", name: "overwrite", type: Boolean },
{ description: "check: exit non-zero when the rules are missing (for CI gating)", name: "strict", type: Boolean }
]
};
const runCommand = {
argument: { description: "Function path (e.g. messages:send)", name: "functionPath", type: String },
description: "Send a single RPC to a running Lunora Worker",
examples: [
[`lunora run messages:send --args '{"text":"hi"}'`, "Call a function with JSON args"],
["lunora run messages:list --shard channel:demo", "Target a specific shard"]
],
group: "Develop",
loader: () => import('../packem_chunks/runRpcCommand.mjs').then((m) => {
return { default: m.execute };
}),
name: "run",
options: [
{ description: "JSON-encoded args object", name: "args", type: String },
{ description: "Explicit shard key", name: "shard", type: String },
{ description: "Worker URL (default http://localhost:8787)", name: "url", type: String }
]
};
const seedCommand = {
description: "Generate deterministic fake data from lunora/schema.ts and bulk-insert it via the worker's admin endpoint",
examples: [
["lunora seed", "Seed every table with the default row count"],
["lunora seed --table posts --count 50", "Seed 50 posts; FK-parent tables are seeded automatically"],
["lunora seed --reset", "Wipe local .wrangler/state, then seed from scratch"],
["lunora seed --seed 7 --dry-run", "Print the NDJSON for seed 7 without inserting"]
],
group: "Data",
loader: () => import('../packem_chunks/handler18.mjs').then((m) => {
return { default: m.execute };
}),
name: "seed",
options: [
{ description: "Rows per table (default 10)", name: "count", type: Number },
{ description: "Seed only this table; its FK-parent tables are seeded automatically", name: "table", type: String },
{ description: "Deterministic seed — same value yields identical rows (default 0)", name: "seed", type: Number },
{ description: "Print the generated NDJSON instead of inserting", name: "dry-run", type: Boolean },
{ description: "Wipe local .wrangler/state before seeding (local dev only)", name: "reset", type: Boolean },
{ description: "Rows per HTTP request (default 500)", name: "batch-size", type: Number },
{ description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
{ description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
{
description: "Admin bearer token (prefer LUNORA_ADMIN_TOKEN; --token is visible to other local processes via the process table)",
name: "token",
type: String
},
{ description: "Skip the confirmation prompt when seeding a non-local/production target", name: "yes", type: Boolean }
]
};
const verifyCommand = {
description: "Validate wrangler.jsonc + codegen dry-run + tsc --noEmit (no files written)",
examples: [
["lunora verify", "Validate wrangler + codegen + tsc"],
["lunora verify --no-typecheck", "Skip the TypeScript type-check"],
["lunora verify --health-url https://my-app.workers.dev", "Also probe the deployment's /_lunora/health"]
],
group: "Deploy",
loader: () => import('../packem_chunks/handler19.mjs').then((m) => {
return { default: m.execute };
}),
name: "verify",
options: [
{ description: "Treat breaking schema drift as a warning instead of a failure", name: "allow-schema-drift", type: Boolean },
{ description: `Which API spec(s) to emit: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
{ description: "Output format: pretty (default) or json", name: "format", type: String },
{
description: "Probe this deployment's /_lunora/health endpoint (off by default; keeps verify offline-safe)",
name: "health-url",
type: String
},
{ description: "Skip the TypeScript type-check step", name: "no-typecheck", type: Boolean }
]
};
const viewCommand = {
description: "Open the Lunora studio in your browser (local dev by default, --remote for production)",
examples: [
["lunora view", "Open the studio for local dev"],
["lunora view --remote", "Open the deployed studio"]
],
group: "Project",
loader: () => import('../packem_chunks/handler20.mjs').then((m) => {
return { default: m.execute };
}),
name: "view",
options: [{ description: "Open the deployed worker URL instead of localhost", name: "remote", type: Boolean }]
};
const NO_STACK = { filterStacktrace: () => false, hideErrorCodeView: true };
const renderLunoraError = (error, options = {}) => {
const message = error instanceof Error ? error.message : String(error);
const hint = resolveHint(isLunoraError(error) ? { code: error.code, hint: error.hint, message } : message);
const rendered = new VisulimaError({
// `renderError` iterates `hint` as lines; the shared flattener returns one
// string, so split it back to lines for the terminal renderer.
hint: hint === void 0 ? void 0 : flattenHint(hint).split("\n"),
message: options.reason === void 0 ? message : `${options.reason}: ${message}`,
name: error instanceof Error && error.name.length > 0 ? error.name : "Error"
});
rendered.stack = "";
return renderError(rendered, NO_STACK);
};
const editDistance = (a, b) => {
const distances = Array.from({ length: b.length + 1 }, (_, index) => index);
for (let row = 1; row <= a.length; row += 1) {
let previousDiagonal = distances[0] ?? 0;
distances[0] = row;
for (let column = 1; column <= b.length; column += 1) {
const previousColumn = distances[column] ?? 0;
const cost = a[row - 1] === b[column - 1] ? 0 : 1;
distances[column] = Math.min(
(distances[column - 1] ?? 0) + 1,
// insertion
previousColumn + 1,
// deletion
previousDiagonal + cost
// substitution
);
previousDiagonal = previousColumn;
}
}
return distances[b.length] ?? 0;
};
const closestMatch = (input, candidates) => {
let best;
let bestDistance = Number.POSITIVE_INFINITY;
for (const candidate of candidates) {
const distance = editDistance(input, candidate);
if (distance < bestDistance) {
bestDistance = distance;
best = candidate;
}
}
const threshold = Math.max(2, Math.ceil(input.length / 3));
return best !== void 0 && bestDistance <= threshold ? best : void 0;
};
const REGISTRY_URL = "https://registry.npmjs.org/@lunora/cli/latest";
const CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
const FETCH_TIMEOUT_MS = 1500;
const DEV_VERSION = "0.0.0";
const LEADING_V = /^v/u;
const versionParts = (version) => {
const core = version.trim().replace(LEADING_V, "").split("-")[0] ?? "";
const [major, minor, patch] = core.split(".").map((part) => {
const n = Number.parseInt(part, 10);
return Number.isFinite(n) ? n : 0;
});
return [major ?? 0, minor ?? 0, patch ?? 0];
};
const compareVersions = (a, b) => {
const pa = versionParts(a);
const pb = versionParts(b);
for (let index = 0; index < 3; index += 1) {
if ((pa[index] ?? 0) !== (pb[index] ?? 0)) {
return (pa[index] ?? 0) > (pb[index] ?? 0) ? 1 : -1;
}
}
return 0;
};
const isNewer = (current, latest) => compareVersions(latest, current) > 0;
const isCacheFresh = (checkedAt, nowMs, ttlMs) => nowMs - checkedAt < ttlMs;
const formatUpdateNotice = (current, latest) => `Update available for @lunora/cli: ${current} → ${latest} — run \`pnpm add -D @lunora/cli@latest\``;
const cacheFilePath = (cacheDirectory) => join(cacheDirectory, "lunora-cli-update.json");
const defaultCacheDirectory = (env) => {
const base = env.XDG_CACHE_HOME && env.XDG_CACHE_HOME.length > 0 ? env.XDG_CACHE_HOME : join(homedir(), ".cache");
const directory = join(base, "lunora");
try {
mkdirSync(directory, { mode: 448, recursive: true });
} catch {
}
return directory;
};
const readCache = (cacheDirectory) => {
try {
const parsed = JSON.parse(readFileSync(cacheFilePath(cacheDirectory), "utf8"));
if (parsed !== null && typeof parsed === "object") {
const { checkedAt, latest } = parsed;
if (typeof latest === "string" && typeof checkedAt === "number") {
return { checkedAt, latest };
}
}
} catch {
}
return void 0;
};
const writeCache = (cacheDirectory, cache) => {
try {
const path = cacheFilePath(cacheDirectory);
try {
if (lstatSync(path).isSymbolicLink()) {
return;
}
} catch {
}
writeFileSync(path, `${JSON.stringify(cache)}
`, "utf8");
} catch {
}
};
const fetchLatestVersion = async (fetchImpl) => {
try {
const response = await fetchImpl(REGISTRY_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
if (!response.ok) {
return void 0;
}
const body = await response.json();
const version = body !== null && typeof body === "object" ? body.version : void 0;
return typeof version === "string" ? version : void 0;
} catch {
return void 0;
}
};
const isNotifierDisabled = (current, env, isTty) => current === DEV_VERSION || !isTty || env.CI !== void 0 || env.LUNORA_NO_UPDATE_NOTIFIER !== void 0;
const maybeNotifyUpdate = async (deps) => {
const env = deps.env ?? process.env;
const isTty = deps.isTTY ?? process.stdout.isTTY;
if (isNotifierDisabled(deps.current, env, isTty)) {
return;
}
const cacheDirectory = deps.cacheDir ?? defaultCacheDirectory(env);
const nowMs = (deps.now ?? Date.now)();
const ttlMs = deps.ttlMs ?? CACHE_TTL_MS;
const cache = readCache(cacheDirectory);
let latest = cache?.latest;
if (cache === void 0 || !isCacheFresh(cache.checkedAt, nowMs, ttlMs)) {
const fetched = await fetchLatestVersion(deps.fetchImpl ?? globalThis.fetch);
if (fetched !== void 0) {
latest = fetched;
writeCache(cacheDirectory, { checkedAt: nowMs, latest: fetched });
}
}
if (latest !== void 0 && isNewer(deps.current, latest)) {
deps.logger.warn(formatUpdateNotice(deps.current, latest));
}
};
const COMMANDS = [
"init",
"add",
"dev",
"codegen",
"build",
"deploy",
"containers",
"prepare",
"link",
"deployments",
"logs",
"run",
"insights",
"reset",
"migrate",
"export",
"import",
"seed",
"backup",
"verify",
"info",
"doctor",
"env",
"analyze",
"view",
"docs",
"registry",
"rules"
];
const readCliVersion = () => {
try {
const parsed = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
const version = parsed !== null && typeof parsed === "object" ? parsed.version : void 0;
return typeof version === "string" && version.length > 0 ? version : "0.0.0";
} catch {
return "0.0.0";
}
};
const VERSION = readCliVersion();
const CLI_COMMANDS = [
initCommand,
addCommand,
devCommand,
codegenCommand,
buildCommand,
deployCommand,
containersCommand,
prepareCommand,
linkCommand,
deploymentsCommand,
logsCommand,
runCommand,
insightsCommand,
resetCommand,
migrateCommand,
exportCommand,
importCommand,
seedCommand,
backupCommand,
verifyCommand,
infoCommand,
doctorCommand,
envCommand,
analyzeCommand,
viewCommand,
docsCommand,
registryCommand,
rulesCommand
];
const buildCli = (options) => {
const exitCode = { value: 0 };
const cli = createCerebro("lunora", {
argv: options.argv === void 0 ? void 0 : [...options.argv],
cwd: options.cwd,
exit: (code) => {
exitCode.value = typeof code === "number" ? code : 0;
},
logger: options.logger,
packageName: "@lunora/cli",
packageVersion: VERSION
});
for (const command of CLI_COMMANDS) {
cli.addCommand(command);
}
cli.addCommand(versionCommand);
cli.addCommand(completionCommand);
return { cli, exitCode };
};
const UNKNOWN_COMMAND = /Command "(?<name>[^"]+)" not found/u;
const reportRunError = (error) => {
const logger = createLogger();
const message = error instanceof Error ? error.message : String(error);
const unknown = UNKNOWN_COMMAND.exec(message);
if (!unknown?.groups) {
if (isLunoraError(error) || findSolutionByMessage(message) !== void 0) {
logger.error(renderLunoraError(error));
} else {
logger.error(message);
}
return;
}
const name = unknown.groups.name ?? "";
const suggestion = closestMatch(name, COMMANDS);
logger.error(`Unknown command "${name}".${suggestion === void 0 ? "" : ` Did you mean "${suggestion}"?`}`);
logger.info("Run `lunora --help` to list commands, or `lunora docs` to open the documentation.");
};
const runCli = async (options = {}) => {
const { cli, exitCode } = buildCli(options);
try {
await cli.run({ shouldExitProcess: false });
} catch (error) {
reportRunError(error);
return 1;
}
await maybeNotifyUpdate({ current: VERSION, logger: createLogger() });
return exitCode.value;
};
export { COMMANDS, VERSION, runCli };