UNPKG

@lunora/cli

Version:

The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands

1,032 lines (998 loc) 47.6 kB
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 };