@lunora/cli 0.0.0 → 1.0.0-alpha.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE.md +105 -0
  2. package/README.md +109 -9
  3. package/__assets__/package-og.svg +14 -0
  4. package/dist/bin.mjs +11 -0
  5. package/dist/index.d.mts +852 -0
  6. package/dist/index.d.ts +852 -0
  7. package/dist/index.mjs +19 -0
  8. package/dist/packem_chunks/handler.mjs +76 -0
  9. package/dist/packem_chunks/handler10.mjs +22 -0
  10. package/dist/packem_chunks/handler11.mjs +192 -0
  11. package/dist/packem_chunks/handler12.mjs +131 -0
  12. package/dist/packem_chunks/handler13.mjs +65 -0
  13. package/dist/packem_chunks/handler14.mjs +58 -0
  14. package/dist/packem_chunks/handler15.mjs +79 -0
  15. package/dist/packem_chunks/handler16.mjs +41 -0
  16. package/dist/packem_chunks/handler17.mjs +105 -0
  17. package/dist/packem_chunks/handler18.mjs +172 -0
  18. package/dist/packem_chunks/handler19.mjs +89 -0
  19. package/dist/packem_chunks/handler2.mjs +114 -0
  20. package/dist/packem_chunks/handler20.mjs +94 -0
  21. package/dist/packem_chunks/handler21.mjs +311 -0
  22. package/dist/packem_chunks/handler3.mjs +204 -0
  23. package/dist/packem_chunks/handler4.mjs +33 -0
  24. package/dist/packem_chunks/handler5.mjs +49 -0
  25. package/dist/packem_chunks/handler6.mjs +91 -0
  26. package/dist/packem_chunks/handler7.mjs +42 -0
  27. package/dist/packem_chunks/handler8.mjs +174 -0
  28. package/dist/packem_chunks/handler9.mjs +16 -0
  29. package/dist/packem_chunks/planDevCommand.mjs +543 -0
  30. package/dist/packem_chunks/runCodegenCommand.mjs +52 -0
  31. package/dist/packem_chunks/runDeployCommand.mjs +504 -0
  32. package/dist/packem_chunks/runInitCommand.mjs +652 -0
  33. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +397 -0
  34. package/dist/packem_chunks/runResetCommand.mjs +41 -0
  35. package/dist/packem_chunks/runRpcCommand.mjs +68 -0
  36. package/dist/packem_shared/COMMANDS-1V_KEx35.mjs +905 -0
  37. package/dist/packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs +244 -0
  38. package/dist/packem_shared/admin-url-4UzT-CI4.mjs +19 -0
  39. package/dist/packem_shared/api-spec-CtA6ilu4.mjs +13 -0
  40. package/dist/packem_shared/buildRegistryIndex-BcYe607_.mjs +38 -0
  41. package/dist/packem_shared/command-BDXcJCCJ.mjs +14 -0
  42. package/dist/packem_shared/createLogger-CHPNjFw2.mjs +73 -0
  43. package/dist/packem_shared/defaultSpawner-DxI3mebw.mjs +43 -0
  44. package/dist/packem_shared/diffSnapshots-RR2ZE8Ya.mjs +161 -0
  45. package/dist/packem_shared/docker-hMQ97KSQ.mjs +21 -0
  46. package/dist/packem_shared/features-ocSSpZtS.mjs +24 -0
  47. package/dist/packem_shared/insertSchemaExtension-BuzF6-t2.mjs +59 -0
  48. package/dist/packem_shared/open-url-Dfq6fAyT.mjs +41 -0
  49. package/dist/packem_shared/output-format-7gyGR3h8.mjs +17 -0
  50. package/dist/packem_shared/parseArgs-YXFuKdEk.mjs +56 -0
  51. package/dist/packem_shared/parseManifest--vZf2FY1.mjs +94 -0
  52. package/dist/packem_shared/resolve-target-qbsJ_5sF.mjs +16 -0
  53. package/dist/packem_shared/runAddCommand-BZGkRnBs.mjs +693 -0
  54. package/dist/packem_shared/schema-drift-gate-BtBt0as0.mjs +79 -0
  55. package/dist/packem_shared/schemaIrToSnapshot-aBTo7TM5.mjs +43 -0
  56. package/dist/packem_shared/wrangler-name-cy4yhm9j.mjs +12 -0
  57. package/package.json +61 -18
  58. package/skills/README.md +29 -0
  59. package/skills/lunora/SKILL.md +83 -0
  60. package/skills/lunora-create-package/SKILL.md +129 -0
  61. package/skills/lunora-deploy/SKILL.md +150 -0
  62. package/skills/lunora-functions/SKILL.md +182 -0
  63. package/skills/lunora-migration-helper/SKILL.md +194 -0
  64. package/skills/lunora-performance-audit/SKILL.md +143 -0
  65. package/skills/lunora-quickstart/SKILL.md +240 -0
  66. package/skills/lunora-realtime/SKILL.md +177 -0
  67. package/skills/lunora-setup-auth/SKILL.md +170 -0
  68. package/skills/lunora-setup-hyperdrive/SKILL.md +154 -0
  69. package/skills/lunora-setup-hyperdrive-global/SKILL.md +171 -0
  70. package/skills/lunora-setup-mail/SKILL.md +151 -0
  71. package/skills/lunora-setup-scheduler/SKILL.md +157 -0
  72. package/skills/lunora-setup-storage/SKILL.md +154 -0
@@ -0,0 +1,905 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { createCerebro } from '@visulima/cerebro';
3
+ import completionCommand from '@visulima/cerebro/command/completion';
4
+ import versionCommand from '@visulima/cerebro/command/version';
5
+ import { A as API_SPEC_HELP } from './api-spec-CtA6ilu4.mjs';
6
+ import { createLogger } from './createLogger-CHPNjFw2.mjs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+
10
+ const addCommand = {
11
+ argument: { description: "Feature or registry item: auth | email | storage | crons | presence | ratelimit | backup | …", name: "feature", type: String },
12
+ description: "Add a feature or registry item (auth, email, storage, crons, …) to the current Lunora project",
13
+ examples: [
14
+ ["lunora add auth", "Add authentication (asks which provider)"],
15
+ ["lunora add auth --provider clerk", "Add Clerk auth without prompting"],
16
+ ["lunora add email", "Add transactional email (Cloudflare Email Workers + dev mail catcher)"],
17
+ ["lunora add storage", "Add the R2 storage registry item"],
18
+ ["lunora add crons", "Add the scheduled-jobs registry item"]
19
+ ],
20
+ group: "Project",
21
+ loader: () => import('../packem_chunks/handler.mjs').then((m) => {
22
+ return { default: m.execute };
23
+ }),
24
+ name: "add",
25
+ options: [
26
+ { description: "auth: provider to use without prompting (auth | clerk | auth0)", name: "provider", type: String },
27
+ { description: "Skip the provider prompt and use the default (email & password)", name: "yes", type: Boolean },
28
+ { description: "Local registry root (offline; expects <name>/ subdirs)", name: "from", type: String },
29
+ { description: "Override the remote registry source base (e.g. gh:owner/repo/registry)", name: "source", type: String },
30
+ { description: "Permit --source values outside gh:/github:/https://", name: "allow-unsafe-source", type: Boolean }
31
+ ]
32
+ };
33
+
34
+ const analyzeCommand = {
35
+ description: "Run wrangler dry-run and report bundle size, top modules, and _generated files",
36
+ examples: [["lunora analyze", "Report the worker bundle size + heaviest modules"]],
37
+ group: "Deploy",
38
+ loader: () => import('../packem_chunks/handler2.mjs').then((m) => {
39
+ return { default: m.execute };
40
+ }),
41
+ name: "analyze",
42
+ options: [{ description: "Emit a JSON report instead of human text", name: "json", type: Boolean }]
43
+ };
44
+
45
+ const backupCommand = {
46
+ argument: { description: "create | list | restore <id|file> | pitr", name: "subcommand", type: String },
47
+ description: "Managed snapshot backups (create | list | restore) plus native point-in-time recovery (pitr)",
48
+ examples: [
49
+ ["lunora backup create", "Snapshot every table to a backup file"],
50
+ ["lunora backup list", "List recorded snapshots"],
51
+ ["lunora backup restore <id>", "Restore a snapshot by id"],
52
+ ["lunora backup pitr --at 2026-06-01T00:00:00Z", "Point-in-time recovery (≤30 days)"]
53
+ ],
54
+ group: "Data",
55
+ loader: () => import('../packem_chunks/handler3.mjs').then((m) => {
56
+ return { default: m.execute };
57
+ }),
58
+ name: "backup",
59
+ options: [
60
+ { description: "Backup directory (default .lunora-backups)", name: "dir", type: String },
61
+ { description: "Comma-separated table allowlist (create)", name: "tables", type: String },
62
+ { description: "pitr: time to read/restore to (ISO or epoch-ms, ≤30 days)", name: "at", type: String },
63
+ { description: "pitr --restore: explicit bookmark to restore to (wins over --at)", name: "bookmark", type: String },
64
+ { description: "pitr: perform a restore instead of just reading the bookmark", name: "restore", type: Boolean },
65
+ { description: "pitr --restore: restart the shard now so recovery applies immediately", name: "restart", type: Boolean },
66
+ { description: "pitr: target shard key (default: root shard)", name: "shard", type: String },
67
+ { description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
68
+ { description: "Confirm a production pitr --restore (required with --prod)", name: "yes", type: Boolean },
69
+ { description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
70
+ {
71
+ description: "Admin bearer token (prefer LUNORA_ADMIN_TOKEN; --token is visible to other local processes via the process table)",
72
+ name: "token",
73
+ type: String
74
+ }
75
+ ]
76
+ };
77
+
78
+ const buildCommand = {
79
+ description: "Codegen + validate + bundle the Worker to disk without deploying",
80
+ examples: [
81
+ ["lunora build", "Bundle to .lunora/build without deploying"],
82
+ ["lunora build --out-dir dist-worker", "Bundle to a custom directory"]
83
+ ],
84
+ group: "Deploy",
85
+ loader: () => import('../packem_chunks/handler4.mjs').then((m) => {
86
+ return { default: m.execute };
87
+ }),
88
+ name: "build",
89
+ options: [
90
+ { description: `Which API spec(s) to emit: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
91
+ { description: "Output format: pretty (default) or json", name: "format", type: String },
92
+ { description: "Directory to write the bundled Worker to (default .lunora/build)", name: "out-dir", type: String }
93
+ ]
94
+ };
95
+
96
+ const codegenCommand = {
97
+ description: "Run codegen for lunora/ functions and schema",
98
+ examples: [["lunora codegen", "Generate lunora/_generated/ from your schema + functions"]],
99
+ group: "Develop",
100
+ loader: () => import('../packem_chunks/runCodegenCommand.mjs').then((m) => {
101
+ return { default: m.execute };
102
+ }),
103
+ name: "codegen",
104
+ options: [
105
+ { description: `Which API spec(s) to emit: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
106
+ { description: "Output format: pretty (default) or json", name: "format", type: String }
107
+ ]
108
+ };
109
+
110
+ const containersCommand = {
111
+ argument: { description: "<build|push|images|list|info|delete> [args…]", name: "args", type: String },
112
+ description: "Build/push container images and manage container instances (wraps wrangler containers)",
113
+ examples: [
114
+ ["lunora containers build ./containers/transcoder --tag transcoder:v1", "Build a container image with the local Docker engine"],
115
+ ["lunora containers build ./containers/transcoder --tag transcoder:v1 --push", "Build and push to the Cloudflare Registry in one step"],
116
+ ["lunora containers push transcoder:v1", "Push a locally-tagged image to the Cloudflare Registry"],
117
+ ["lunora containers images list", "List images in your Cloudflare Registry"],
118
+ ["lunora containers images delete transcoder:v1", "Delete an image to free registry storage"]
119
+ ],
120
+ group: "Deploy",
121
+ loader: () => import('../packem_chunks/handler5.mjs').then((m) => {
122
+ return { default: m.execute };
123
+ }),
124
+ name: "containers",
125
+ options: [
126
+ { description: "build: push the image to the Cloudflare Registry after building", name: "push", type: Boolean },
127
+ { description: "build: name:tag for the image (forwarded to wrangler --tag)", name: "tag", type: String },
128
+ { description: "Cloudflare environment name", name: "env", type: String }
129
+ ]
130
+ };
131
+
132
+ const deployCommand = {
133
+ description: "Codegen, validate wrangler, then wrangler deploy",
134
+ examples: [
135
+ ["lunora deploy", "Deploy to Cloudflare"],
136
+ ["lunora deploy --env production", "Deploy to a named environment"],
137
+ ["lunora deploy --dry-run", "Validate + bundle without publishing"],
138
+ ["lunora deploy --migrate", "Deploy, then run pending data migrations"]
139
+ ],
140
+ group: "Deploy",
141
+ loader: () => import('../packem_chunks/runDeployCommand.mjs').then((m) => {
142
+ return { default: m.execute };
143
+ }),
144
+ name: "deploy",
145
+ options: [
146
+ { description: "Override the schema-drift gate (deploy even with breaking schema drift and no migration)", name: "allow-schema-drift", type: Boolean },
147
+ { description: `Which API spec(s) to emit: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
148
+ { description: "Validate, bundle, and run pre-deploy gates without publishing (wrangler deploy --dry-run)", name: "dry-run", type: Boolean },
149
+ { description: "Cloudflare environment name", name: "env", type: String },
150
+ { description: "Output format: pretty (default) or json", name: "format", type: String },
151
+ { description: "After a successful deploy, run pending data migrations against the live worker", name: "migrate", type: Boolean },
152
+ {
153
+ description: "Skip codegen + the schema-drift gate (assumes `lunora build`/`prepare` already ran in this CI run). Wrangler still bundles the worker.",
154
+ name: "prebuilt",
155
+ type: Boolean
156
+ },
157
+ { description: "Admin bearer token for --migrate (falls back to LUNORA_ADMIN_TOKEN)", name: "migrate-token", type: String },
158
+ {
159
+ description: "Worker URL for --migrate (REQUIRED with --migrate; the deploy target URL is not captured automatically)",
160
+ name: "migrate-url",
161
+ type: String
162
+ },
163
+ { description: "Confirm running the production data migration triggered by --migrate (required with --migrate)", name: "migrate-yes", type: Boolean },
164
+ {
165
+ description: "Upload a preview version (wrangler versions upload) instead of going live — prints a preview URL; doesn't shift production traffic",
166
+ name: "preview",
167
+ type: Boolean
168
+ },
169
+ {
170
+ 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.",
171
+ name: "temporary",
172
+ type: Boolean
173
+ },
174
+ {
175
+ description: "Re-bless the committed schema baseline (lunora/.lunora-schema.json) with the current shape",
176
+ name: "update-schema-baseline",
177
+ type: Boolean
178
+ }
179
+ ]
180
+ };
181
+
182
+ const deploymentsCommand = {
183
+ argument: { description: "list | inspect <version-id> | rollback [version-id] | promote <version-id>", name: "subcommand", type: String },
184
+ description: "List deployments and roll back / promote / inspect Worker versions",
185
+ examples: [
186
+ ["lunora deployments list", "Show the 10 most recent deployments"],
187
+ ["lunora deployments inspect <version-id>", "View a specific Worker version"],
188
+ ["lunora deployments rollback --yes", "Roll back to the previous version"],
189
+ ["lunora deployments promote <version-id> --yes", "Send 100% of traffic to a version"]
190
+ ],
191
+ group: "Deploy",
192
+ loader: () => import('../packem_chunks/handler6.mjs').then((m) => {
193
+ return { default: m.execute };
194
+ }),
195
+ name: "deployments",
196
+ options: [
197
+ { description: "Cloudflare environment name", name: "env", type: String },
198
+ { description: "Display `list` output as JSON", name: "json", type: Boolean },
199
+ { description: "Reason/description recorded with a rollback or promote", name: "message", type: String },
200
+ { description: "Confirm a rollback or promote (required — these change live traffic)", name: "yes", type: Boolean }
201
+ ]
202
+ };
203
+
204
+ const devCommand = {
205
+ description: "Run the dev stack: wrangler worker + studio + codegen watch",
206
+ examples: [
207
+ ["lunora dev", "Run the worker + studio + codegen watch"],
208
+ ["lunora dev --no-studio", "Skip the embedded studio server"],
209
+ ["lunora dev --worker-port 8080", "Use a custom wrangler dev port"],
210
+ ["lunora dev --remote", "Proxy D1/KV/R2 to the deployed worker (also LUNORA_REMOTE=1)"]
211
+ ],
212
+ group: "Develop",
213
+ loader: () => import('../packem_chunks/planDevCommand.mjs').then((m) => {
214
+ return { default: m.execute };
215
+ }),
216
+ name: "dev",
217
+ options: [
218
+ { description: `Which API spec(s) codegen emits: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
219
+ { description: "Studio server port (default 6173)", name: "port", type: Number },
220
+ { description: "wrangler dev port (default 8787)", name: "worker-port", type: Number },
221
+ { description: "Don't start the embedded studio server", name: "no-studio", type: Boolean },
222
+ { description: "Don't watch + regenerate codegen", name: "no-codegen", type: Boolean },
223
+ { description: "Proxy D1/KV/R2 bindings to the deployed worker (or set LUNORA_REMOTE=1)", name: "remote", type: Boolean }
224
+ ]
225
+ };
226
+
227
+ const docsCommand = {
228
+ argument: { description: "Optional path under the docs site (e.g. addons/studio)", name: "section", type: String },
229
+ description: "Open the Lunora docs in your browser (optional [section] path)",
230
+ examples: [
231
+ ["lunora docs", "Open the Lunora docs"],
232
+ ["lunora docs addons/studio", "Open a specific docs section"]
233
+ ],
234
+ group: "Project",
235
+ loader: () => import('../packem_chunks/handler7.mjs').then((m) => {
236
+ return { default: m.execute };
237
+ }),
238
+ name: "docs"
239
+ };
240
+
241
+ const doctorCommand = {
242
+ description: "Preflight the current Lunora project (wrangler bindings, placeholders, dev secrets)",
243
+ examples: [["lunora doctor", "Run the project preflight checks"]],
244
+ group: "Project",
245
+ loader: () => import('../packem_chunks/handler8.mjs').then((m) => {
246
+ return { default: m.execute };
247
+ }),
248
+ name: "doctor",
249
+ options: []
250
+ };
251
+
252
+ const envCommand = {
253
+ argument: { description: "list | get <KEY> | set <KEY> <VALUE> | unset <KEY> | push | diff | doctor", name: "subcommand", type: String },
254
+ description: "Manage .dev.vars and sync secrets via wrangler (list | get | set | unset | push | diff | doctor)",
255
+ examples: [
256
+ ["lunora env list", "List .dev.vars keys"],
257
+ ["lunora env set API_KEY secret", "Set a local variable"],
258
+ ["lunora env push --yes", "Upload secrets to Cloudflare"],
259
+ ["lunora env diff", "Compare local .dev.vars keys against Cloudflare"]
260
+ ],
261
+ group: "Data",
262
+ loader: () => import('../packem_chunks/handler21.mjs').then((m) => {
263
+ return { default: m.execute };
264
+ }),
265
+ name: "env",
266
+ options: [
267
+ { description: "Target production for `push` (passes --env production to wrangler)", name: "prod", type: Boolean },
268
+ {
269
+ description: "Push secrets to a temporary-account deployment when unauthenticated (wrangler secret put --temporary). Errors if you're already authenticated.",
270
+ name: "temporary",
271
+ type: Boolean
272
+ },
273
+ { description: "Required for `push` — confirms uploading secrets to Cloudflare", name: "yes", type: Boolean }
274
+ ]
275
+ };
276
+
277
+ const exportCommand = {
278
+ argument: { description: "Optional path (alias for --out)", name: "path", type: String },
279
+ description: "Stream NDJSON of every shard-local + global table from the worker",
280
+ examples: [
281
+ ["lunora export --out backup.ndjson", "Dump every table to an NDJSON file"],
282
+ ["lunora export --tables messages,users", "Export only specific tables"]
283
+ ],
284
+ group: "Data",
285
+ loader: () => import('../packem_chunks/handler9.mjs').then((m) => {
286
+ return { default: m.execute };
287
+ }),
288
+ name: "export",
289
+ options: [
290
+ { description: "Output file path (`-` for stdout, default)", name: "out", type: String },
291
+ { description: "Comma-separated table allowlist", name: "tables", type: String },
292
+ { description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
293
+ { description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
294
+ {
295
+ description: "Admin bearer token (prefer LUNORA_ADMIN_TOKEN; --token is visible to other local processes via the process table)",
296
+ name: "token",
297
+ type: String
298
+ }
299
+ ]
300
+ };
301
+
302
+ const importCommand = {
303
+ argument: { description: "Source NDJSON file", name: "file", type: String },
304
+ description: "Bulk-insert rows from an NDJSON file via the worker's admin endpoint",
305
+ examples: [["lunora import backup.ndjson", "Bulk-insert rows from an NDJSON file"]],
306
+ group: "Data",
307
+ loader: () => import('../packem_chunks/handler10.mjs').then((m) => {
308
+ return { default: m.execute };
309
+ }),
310
+ name: "import",
311
+ options: [
312
+ { description: "Wrap each bare doc as `{table:<name>,doc:...}`", name: "table", type: String },
313
+ { description: "Rows per HTTP request (default 500)", name: "batch-size", type: Number },
314
+ { description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
315
+ { description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
316
+ {
317
+ description: "Admin bearer token (prefer LUNORA_ADMIN_TOKEN; --token is visible to other local processes via the process table)",
318
+ name: "token",
319
+ type: String
320
+ }
321
+ ]
322
+ };
323
+
324
+ const infoCommand = {
325
+ description: "Print resolved project config: @lunora/* versions, wrangler summary, schema overview",
326
+ examples: [
327
+ ["lunora info", "Print resolved project config"],
328
+ ["lunora info --json", "Emit a JSON snapshot"]
329
+ ],
330
+ group: "Project",
331
+ loader: () => import('../packem_chunks/handler11.mjs').then((m) => {
332
+ return { default: m.execute };
333
+ }),
334
+ name: "info",
335
+ options: [{ description: "Emit a JSON snapshot instead of human text", name: "json", type: Boolean }]
336
+ };
337
+
338
+ const initCommand = {
339
+ argument: { description: "Project name", name: "name", type: String },
340
+ description: "Scaffold a new Lunora project",
341
+ examples: [
342
+ ["lunora init my-app", "Scaffold with the default (vite) template"],
343
+ ["lunora init my-app -t tanstack-start-react", "Scaffold a TanStack Start (React) app"],
344
+ ["lunora init my-app -t tanstack-start-solid", "Scaffold a TanStack Start (Solid) app"],
345
+ ["lunora init --here", "Add Lunora to the current project"],
346
+ ["lunora init my-app --ci github", "Scaffold + add a GitHub Actions deploy pipeline"],
347
+ ["lunora init my-app --ci gitlab", "Scaffold + add a GitLab CI deploy pipeline"]
348
+ ],
349
+ group: "Project",
350
+ loader: () => import('../packem_chunks/runInitCommand.mjs').then((m) => {
351
+ return { default: m.execute };
352
+ }),
353
+ name: "init",
354
+ options: [
355
+ {
356
+ alias: "t",
357
+ defaultValue: "vite",
358
+ description: "Template to scaffold (vite | standalone | astro | nuxt | sveltekit | tanstack-start-react | tanstack-start-solid)",
359
+ name: "template",
360
+ type: String
361
+ },
362
+ {
363
+ description: "Local templates root to copy from (offline-friendly; expects <type>/ subdirs)",
364
+ name: "from",
365
+ type: String
366
+ },
367
+ {
368
+ description: "Override the remote template source (e.g. gh:owner/repo/sub#ref)",
369
+ name: "source",
370
+ type: String
371
+ },
372
+ {
373
+ description: "Permit --source values outside gh:/github:/https:// (e.g. local file://)",
374
+ name: "allow-unsafe-source",
375
+ type: Boolean
376
+ },
377
+ {
378
+ description: "Add Lunora to the current project: detect the framework, patch the config, scaffold lunora/, print per-framework wiring steps",
379
+ name: "here",
380
+ type: Boolean
381
+ },
382
+ {
383
+ alias: "i",
384
+ description: "After scaffolding, offer to add auth + email (defaults on when stdin is a TTY)",
385
+ name: "interactive",
386
+ type: Boolean
387
+ },
388
+ {
389
+ alias: "y",
390
+ description: "Skip the auth/email offer; scaffold only",
391
+ name: "yes",
392
+ type: Boolean
393
+ },
394
+ {
395
+ description: "Also scaffold a CI deploy pipeline: github (.github/workflows/deploy.yml) or gitlab (.gitlab-ci.yml)",
396
+ name: "ci",
397
+ type: String
398
+ }
399
+ ]
400
+ };
401
+
402
+ const insightsCommand = {
403
+ description: "Report write-conflict hot-spots, error rates, and latency outliers from a running Worker",
404
+ examples: [
405
+ ["lunora insights", "Report against the local dev worker"],
406
+ ["lunora insights --shard channel:demo", "Scope the report to one shard"],
407
+ ["lunora insights --json", "Emit the raw report as JSON"],
408
+ ["lunora insights --prod --url https://app.example.com --token $LUNORA_ADMIN_TOKEN", "Report against production"]
409
+ ],
410
+ group: "Develop",
411
+ loader: () => import('../packem_chunks/handler12.mjs').then((m) => {
412
+ return { default: m.execute };
413
+ }),
414
+ name: "insights",
415
+ options: [
416
+ { description: "Explicit shard key (defaults to the root shard)", name: "shard", type: String },
417
+ { description: "Max rows per section (default 10)", name: "limit", type: String },
418
+ { description: "Emit a JSON report instead of human text", name: "json", type: Boolean },
419
+ { description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
420
+ { description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
421
+ { description: "Admin bearer token (or LUNORA_ADMIN_TOKEN)", name: "token", type: String }
422
+ ]
423
+ };
424
+
425
+ const linkCommand = {
426
+ description: "Link this checkout to its deployed Worker (writes .lunora/project.json)",
427
+ examples: [
428
+ ["lunora link --url https://app.acme.workers.dev", "Link to a deployed Worker URL"],
429
+ ["lunora link --url https://app.acme.workers.dev --env production", "Link a named environment"],
430
+ ["lunora link --remove", "Remove the link"]
431
+ ],
432
+ group: "Deploy",
433
+ loader: () => import('../packem_chunks/handler13.mjs').then((m) => {
434
+ return { default: m.execute };
435
+ }),
436
+ name: "link",
437
+ options: [
438
+ { description: "Cloudflare environment name to record alongside the link", name: "env", type: String },
439
+ { description: "Worker name (defaults to the `name` in wrangler config)", name: "name", type: String },
440
+ { description: "Remove the existing link (.lunora/project.json)", name: "remove", type: Boolean },
441
+ { description: "Deployed Worker URL to link (e.g. https://app.acme.workers.dev)", name: "url", type: String }
442
+ ]
443
+ };
444
+
445
+ const logsCommand = {
446
+ argument: { description: "Worker name (defaults to the name in wrangler config)", name: "worker", type: String },
447
+ description: "Stream live logs from a deployed Lunora Worker",
448
+ group: "Deploy",
449
+ loader: () => import('../packem_chunks/handler14.mjs').then((m) => {
450
+ return { default: m.execute };
451
+ }),
452
+ name: "logs",
453
+ options: [
454
+ { description: "Cloudflare environment name", name: "env", type: String },
455
+ { description: "Output format: pretty (default) or json", name: "format", type: String },
456
+ { description: "Substring filter on log messages", name: "search", type: String },
457
+ { description: "Filter by invocation status: ok, error, or canceled", name: "status", type: String },
458
+ {
459
+ description: "Tail a temporary-account deployment when unauthenticated (wrangler tail --temporary). Errors if you're already authenticated.",
460
+ name: "temporary",
461
+ type: Boolean
462
+ }
463
+ ]
464
+ };
465
+
466
+ const migrateCommand = {
467
+ argument: { description: "generate | create | up | down | status | d1-to-hyperdrive [name|id]", name: "subcommand", type: String },
468
+ description: "Schema (generate), online data (create | up | down | status), and backend (d1-to-hyperdrive) migrations",
469
+ examples: [
470
+ ["lunora migrate generate", "Diff lunora/schema.ts and emit a SQL migration"],
471
+ ["lunora migrate create add_users_email", "Scaffold a data migration"],
472
+ ["lunora migrate up backfill-names", "Run a data migration across shards"],
473
+ ["lunora migrate status backfill-names", "Report a migration's per-shard status"],
474
+ ["lunora migrate d1-to-hyperdrive --from-url https://old --to-url https://new", "Copy .global() data from D1 to Hyperdrive"]
475
+ ],
476
+ group: "Data",
477
+ loader: () => import('../packem_chunks/runMigrateGenerateCommand.mjs').then((m) => {
478
+ return { default: m.execute };
479
+ }),
480
+ name: "migrate",
481
+ options: [
482
+ { description: "Migration name slug (e.g. add_users_email)", name: "name", type: String },
483
+ { description: "Target table for `create`", name: "table", type: String },
484
+ { description: "Preview a data migration without rewriting rows", name: "dry-run", type: Boolean },
485
+ { description: "Rows per batch for a data migration", name: "batch-size", type: Number },
486
+ { description: "Cap batches processed this run (maps to the runner's maxBatches)", name: "steps", type: Number },
487
+ { description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
488
+ { description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
489
+ {
490
+ description: "Admin bearer token (prefer LUNORA_ADMIN_TOKEN; --token is visible to other local processes via the process table)",
491
+ name: "token",
492
+ type: String
493
+ },
494
+ { description: "Required with --prod for up/down — confirms running against production", name: "yes", type: Boolean },
495
+ // d1-to-hyperdrive backend migration.
496
+ { description: "d1-to-hyperdrive: source (D1) worker URL (defaults to --url)", name: "from-url", type: String },
497
+ { description: "d1-to-hyperdrive: source admin token (defaults to --token / LUNORA_ADMIN_TOKEN)", name: "from-token", type: String },
498
+ { description: "d1-to-hyperdrive: target (Hyperdrive) worker URL (defaults to --url)", name: "to-url", type: String },
499
+ { description: "d1-to-hyperdrive: target admin token (defaults to --token / LUNORA_ADMIN_TOKEN)", name: "to-token", type: String },
500
+ { description: "d1-to-hyperdrive: comma-separated .global() tables to move (default: all global tables)", name: "tables", type: String },
501
+ { description: "d1-to-hyperdrive: keep the intermediate NDJSON dump at this path", name: "out", type: String }
502
+ ]
503
+ };
504
+
505
+ const prepareCommand = {
506
+ description: "Run codegen + binding reconcile + wrangler validation (no Vite) — for CI",
507
+ examples: [["lunora prepare", "Codegen + binding reconcile + validate (CI, before deploy)"]],
508
+ group: "Deploy",
509
+ loader: () => import('../packem_chunks/handler15.mjs').then((m) => {
510
+ return { default: m.execute };
511
+ }),
512
+ name: "prepare",
513
+ options: [
514
+ { description: "Override the schema-drift gate (proceed even with breaking schema drift and no migration)", name: "allow-schema-drift", type: Boolean },
515
+ { description: `Which API spec(s) to emit: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
516
+ {
517
+ description: "Re-bless the committed schema baseline (lunora/.lunora-schema.json) with the current shape",
518
+ name: "update-schema-baseline",
519
+ type: Boolean
520
+ }
521
+ ]
522
+ };
523
+
524
+ const registryCommand = {
525
+ argument: { description: "<add|list|view|build> [item names…]", name: "args", type: String },
526
+ description: "Component registry: add/list/view items, or build the catalog",
527
+ examples: [
528
+ ["lunora registry list", "List available registry items"],
529
+ ["lunora registry add presence", "Scaffold a registry item into lunora/"],
530
+ ["lunora registry build --check", "Verify the committed catalog is current"]
531
+ ],
532
+ group: "Project",
533
+ loader: () => import('../packem_chunks/handler16.mjs').then((m) => {
534
+ return { default: m.execute };
535
+ }),
536
+ name: "registry",
537
+ options: [
538
+ { description: "add: print the plan and stop without writing", name: "dry-run", type: Boolean },
539
+ { description: "add: preview the file changes (content diff) and write nothing", name: "diff", type: Boolean },
540
+ { description: "add: force-overwrite existing files (take the incoming copy)", name: "overwrite", type: Boolean },
541
+ { description: "add: skip the package.json mutation confirmation prompt", name: "yes", type: Boolean },
542
+ { description: "Local registry root (offline; expects <name>/ subdirs)", name: "from", type: String },
543
+ { description: "Override the remote registry source base (e.g. gh:owner/repo/registry)", name: "source", type: String },
544
+ { description: "Permit --source values outside gh:/github:/https://", name: "allow-unsafe-source", type: Boolean },
545
+ { description: "Emit JSON output (add plan / list)", name: "json", type: Boolean },
546
+ { description: "build: output path for the catalog (default <root>/index.json)", name: "out", type: String },
547
+ { description: "build: verify the index is current instead of rewriting it", name: "check", type: Boolean }
548
+ ]
549
+ };
550
+
551
+ const resetCommand = {
552
+ description: "Clear local Miniflare state (and .lunora-cache with --all)",
553
+ examples: [
554
+ ["lunora reset", "Clear local Miniflare state"],
555
+ ["lunora reset --all", "Also remove .lunora-cache"]
556
+ ],
557
+ group: "Develop",
558
+ loader: () => import('../packem_chunks/runResetCommand.mjs').then((m) => {
559
+ return { default: m.execute };
560
+ }),
561
+ name: "reset",
562
+ options: [
563
+ { description: "Also remove .lunora-cache", name: "all", type: Boolean },
564
+ { description: "Skip the confirmation prompt (required when stdin is not a TTY)", name: "yes", type: Boolean }
565
+ ]
566
+ };
567
+
568
+ const rulesCommand = {
569
+ argument: { description: "install | check", name: "subcommand", type: String },
570
+ description: "Install the Lunora agent skills (AI rules) into .agents/skills/, or check they're present",
571
+ examples: [
572
+ ["lunora rules install", "Copy the Lunora agent skills into .agents/skills/"],
573
+ ["lunora rules install --overwrite", "Reinstall, replacing edited skill files"],
574
+ ["lunora rules check", "Report which Lunora skills are installed"],
575
+ ["lunora rules check --strict", "Exit non-zero when rules are missing (CI gate)"]
576
+ ],
577
+ group: "Project",
578
+ loader: () => import('../packem_chunks/handler17.mjs').then((m) => {
579
+ return { default: m.execute };
580
+ }),
581
+ name: "rules",
582
+ options: [
583
+ { description: "install: overwrite skill files that already exist (default: skip them)", name: "overwrite", type: Boolean },
584
+ { description: "check: exit non-zero when the rules are missing (for CI gating)", name: "strict", type: Boolean }
585
+ ]
586
+ };
587
+
588
+ const runCommand = {
589
+ argument: { description: "Function path (e.g. messages:send)", name: "functionPath", type: String },
590
+ description: "Send a single RPC to a running Lunora Worker",
591
+ examples: [
592
+ [`lunora run messages:send --args '{"text":"hi"}'`, "Call a function with JSON args"],
593
+ ["lunora run messages:list --shard channel:demo", "Target a specific shard"]
594
+ ],
595
+ group: "Develop",
596
+ loader: () => import('../packem_chunks/runRpcCommand.mjs').then((m) => {
597
+ return { default: m.execute };
598
+ }),
599
+ name: "run",
600
+ options: [
601
+ { description: "JSON-encoded args object", name: "args", type: String },
602
+ { description: "Explicit shard key", name: "shard", type: String },
603
+ { description: "Worker URL (default http://localhost:8787)", name: "url", type: String }
604
+ ]
605
+ };
606
+
607
+ const seedCommand = {
608
+ description: "Generate deterministic fake data from lunora/schema.ts and bulk-insert it via the worker's admin endpoint",
609
+ examples: [
610
+ ["lunora seed", "Seed every table with the default row count"],
611
+ ["lunora seed --table posts --count 50", "Seed 50 posts; FK-parent tables are seeded automatically"],
612
+ ["lunora seed --reset", "Wipe local .wrangler/state, then seed from scratch"],
613
+ ["lunora seed --seed 7 --dry-run", "Print the NDJSON for seed 7 without inserting"]
614
+ ],
615
+ group: "Data",
616
+ loader: () => import('../packem_chunks/handler18.mjs').then((m) => {
617
+ return { default: m.execute };
618
+ }),
619
+ name: "seed",
620
+ options: [
621
+ { description: "Rows per table (default 10)", name: "count", type: Number },
622
+ { description: "Seed only this table; its FK-parent tables are seeded automatically", name: "table", type: String },
623
+ { description: "Deterministic seed — same value yields identical rows (default 0)", name: "seed", type: Number },
624
+ { description: "Print the generated NDJSON instead of inserting", name: "dry-run", type: Boolean },
625
+ { description: "Wipe local .wrangler/state before seeding (local dev only)", name: "reset", type: Boolean },
626
+ { description: "Rows per HTTP request (default 500)", name: "batch-size", type: Number },
627
+ { description: "Target production — requires an explicit --url", name: "prod", type: Boolean },
628
+ { description: "Worker URL (default http://localhost:8787)", name: "url", type: String },
629
+ {
630
+ description: "Admin bearer token (prefer LUNORA_ADMIN_TOKEN; --token is visible to other local processes via the process table)",
631
+ name: "token",
632
+ type: String
633
+ },
634
+ { description: "Skip the confirmation prompt when seeding a non-local/production target", name: "yes", type: Boolean }
635
+ ]
636
+ };
637
+
638
+ const verifyCommand = {
639
+ description: "Validate wrangler.jsonc + codegen dry-run + tsc --noEmit (no files written)",
640
+ examples: [
641
+ ["lunora verify", "Validate wrangler + codegen + tsc"],
642
+ ["lunora verify --no-typecheck", "Skip the TypeScript type-check"]
643
+ ],
644
+ group: "Deploy",
645
+ loader: () => import('../packem_chunks/handler19.mjs').then((m) => {
646
+ return { default: m.execute };
647
+ }),
648
+ name: "verify",
649
+ options: [
650
+ { description: "Treat breaking schema drift as a warning instead of a failure", name: "allow-schema-drift", type: Boolean },
651
+ { description: `Which API spec(s) to emit: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
652
+ { description: "Output format: pretty (default) or json", name: "format", type: String },
653
+ { description: "Skip the TypeScript type-check step", name: "no-typecheck", type: Boolean }
654
+ ]
655
+ };
656
+
657
+ const viewCommand = {
658
+ description: "Open the Lunora studio in your browser (local dev by default, --remote for production)",
659
+ examples: [
660
+ ["lunora view", "Open the studio for local dev"],
661
+ ["lunora view --remote", "Open the deployed studio"]
662
+ ],
663
+ group: "Project",
664
+ loader: () => import('../packem_chunks/handler20.mjs').then((m) => {
665
+ return { default: m.execute };
666
+ }),
667
+ name: "view",
668
+ options: [{ description: "Open the deployed worker URL instead of localhost", name: "remote", type: Boolean }]
669
+ };
670
+
671
+ const editDistance = (a, b) => {
672
+ const distances = Array.from({ length: b.length + 1 }, (_, index) => index);
673
+ for (let row = 1; row <= a.length; row += 1) {
674
+ let previousDiagonal = distances[0] ?? 0;
675
+ distances[0] = row;
676
+ for (let column = 1; column <= b.length; column += 1) {
677
+ const previousColumn = distances[column] ?? 0;
678
+ const cost = a[row - 1] === b[column - 1] ? 0 : 1;
679
+ distances[column] = Math.min(
680
+ (distances[column - 1] ?? 0) + 1,
681
+ // insertion
682
+ previousColumn + 1,
683
+ // deletion
684
+ previousDiagonal + cost
685
+ // substitution
686
+ );
687
+ previousDiagonal = previousColumn;
688
+ }
689
+ }
690
+ return distances[b.length] ?? 0;
691
+ };
692
+ const closestMatch = (input, candidates) => {
693
+ let best;
694
+ let bestDistance = Number.POSITIVE_INFINITY;
695
+ for (const candidate of candidates) {
696
+ const distance = editDistance(input, candidate);
697
+ if (distance < bestDistance) {
698
+ bestDistance = distance;
699
+ best = candidate;
700
+ }
701
+ }
702
+ const threshold = Math.max(2, Math.ceil(input.length / 3));
703
+ return best !== void 0 && bestDistance <= threshold ? best : void 0;
704
+ };
705
+
706
+ const REGISTRY_URL = "https://registry.npmjs.org/@lunora/cli/latest";
707
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
708
+ const FETCH_TIMEOUT_MS = 1500;
709
+ const DEV_VERSION = "0.0.0";
710
+ const LEADING_V = /^v/u;
711
+ const versionParts = (version) => {
712
+ const core = version.trim().replace(LEADING_V, "").split("-")[0] ?? "";
713
+ const [major, minor, patch] = core.split(".").map((part) => {
714
+ const n = Number.parseInt(part, 10);
715
+ return Number.isFinite(n) ? n : 0;
716
+ });
717
+ return [major ?? 0, minor ?? 0, patch ?? 0];
718
+ };
719
+ const compareVersions = (a, b) => {
720
+ const pa = versionParts(a);
721
+ const pb = versionParts(b);
722
+ for (let index = 0; index < 3; index += 1) {
723
+ if ((pa[index] ?? 0) !== (pb[index] ?? 0)) {
724
+ return (pa[index] ?? 0) > (pb[index] ?? 0) ? 1 : -1;
725
+ }
726
+ }
727
+ return 0;
728
+ };
729
+ const isNewer = (current, latest) => compareVersions(latest, current) > 0;
730
+ const isCacheFresh = (checkedAt, nowMs, ttlMs) => nowMs - checkedAt < ttlMs;
731
+ const formatUpdateNotice = (current, latest) => `Update available for @lunora/cli: ${current} → ${latest} — run \`pnpm add -D @lunora/cli@latest\``;
732
+ const cacheFilePath = (cacheDirectory) => join(cacheDirectory, "lunora-cli-update.json");
733
+ const readCache = (cacheDirectory) => {
734
+ try {
735
+ const parsed = JSON.parse(readFileSync(cacheFilePath(cacheDirectory), "utf8"));
736
+ if (parsed !== null && typeof parsed === "object") {
737
+ const { checkedAt, latest } = parsed;
738
+ if (typeof latest === "string" && typeof checkedAt === "number") {
739
+ return { checkedAt, latest };
740
+ }
741
+ }
742
+ } catch {
743
+ }
744
+ return void 0;
745
+ };
746
+ const writeCache = (cacheDirectory, cache) => {
747
+ try {
748
+ writeFileSync(cacheFilePath(cacheDirectory), `${JSON.stringify(cache)}
749
+ `, "utf8");
750
+ } catch {
751
+ }
752
+ };
753
+ const fetchLatestVersion = async (fetchImpl) => {
754
+ try {
755
+ const response = await fetchImpl(REGISTRY_URL, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
756
+ if (!response.ok) {
757
+ return void 0;
758
+ }
759
+ const body = await response.json();
760
+ const version = body !== null && typeof body === "object" ? body.version : void 0;
761
+ return typeof version === "string" ? version : void 0;
762
+ } catch {
763
+ return void 0;
764
+ }
765
+ };
766
+ const isNotifierDisabled = (current, env, isTty) => current === DEV_VERSION || !isTty || env.CI !== void 0 || env.LUNORA_NO_UPDATE_NOTIFIER !== void 0;
767
+ const maybeNotifyUpdate = async (deps) => {
768
+ const env = deps.env ?? process.env;
769
+ const isTty = deps.isTTY ?? process.stdout.isTTY;
770
+ if (isNotifierDisabled(deps.current, env, isTty)) {
771
+ return;
772
+ }
773
+ const cacheDirectory = deps.cacheDir ?? tmpdir();
774
+ const nowMs = (deps.now ?? Date.now)();
775
+ const ttlMs = deps.ttlMs ?? CACHE_TTL_MS;
776
+ const cache = readCache(cacheDirectory);
777
+ let latest = cache?.latest;
778
+ if (cache === void 0 || !isCacheFresh(cache.checkedAt, nowMs, ttlMs)) {
779
+ const fetched = await fetchLatestVersion(deps.fetchImpl ?? globalThis.fetch);
780
+ if (fetched !== void 0) {
781
+ latest = fetched;
782
+ writeCache(cacheDirectory, { checkedAt: nowMs, latest: fetched });
783
+ }
784
+ }
785
+ if (latest !== void 0 && isNewer(deps.current, latest)) {
786
+ deps.logger.warn(formatUpdateNotice(deps.current, latest));
787
+ }
788
+ };
789
+
790
+ const COMMANDS = [
791
+ "init",
792
+ "add",
793
+ "dev",
794
+ "codegen",
795
+ "build",
796
+ "deploy",
797
+ "containers",
798
+ "prepare",
799
+ "link",
800
+ "deployments",
801
+ "logs",
802
+ "run",
803
+ "insights",
804
+ "reset",
805
+ "migrate",
806
+ "export",
807
+ "import",
808
+ "seed",
809
+ "backup",
810
+ "verify",
811
+ "info",
812
+ "doctor",
813
+ "env",
814
+ "analyze",
815
+ "view",
816
+ "docs",
817
+ "registry",
818
+ "rules"
819
+ ];
820
+ const readCliVersion = () => {
821
+ try {
822
+ const parsed = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
823
+ const version = parsed !== null && typeof parsed === "object" ? parsed.version : void 0;
824
+ return typeof version === "string" && version.length > 0 ? version : "0.0.0";
825
+ } catch {
826
+ return "0.0.0";
827
+ }
828
+ };
829
+ const VERSION = readCliVersion();
830
+ const CLI_COMMANDS = [
831
+ initCommand,
832
+ addCommand,
833
+ devCommand,
834
+ codegenCommand,
835
+ buildCommand,
836
+ deployCommand,
837
+ containersCommand,
838
+ prepareCommand,
839
+ linkCommand,
840
+ deploymentsCommand,
841
+ logsCommand,
842
+ runCommand,
843
+ insightsCommand,
844
+ resetCommand,
845
+ migrateCommand,
846
+ exportCommand,
847
+ importCommand,
848
+ seedCommand,
849
+ backupCommand,
850
+ verifyCommand,
851
+ infoCommand,
852
+ doctorCommand,
853
+ envCommand,
854
+ analyzeCommand,
855
+ viewCommand,
856
+ docsCommand,
857
+ registryCommand,
858
+ rulesCommand
859
+ ];
860
+ const buildCli = (options) => {
861
+ const exitCode = { value: 0 };
862
+ const cli = createCerebro("lunora", {
863
+ argv: options.argv === void 0 ? void 0 : [...options.argv],
864
+ cwd: options.cwd,
865
+ exit: (code) => {
866
+ exitCode.value = typeof code === "number" ? code : 0;
867
+ },
868
+ logger: options.logger,
869
+ packageName: "@lunora/cli",
870
+ packageVersion: VERSION
871
+ });
872
+ for (const command of CLI_COMMANDS) {
873
+ cli.addCommand(command);
874
+ }
875
+ cli.addCommand(versionCommand);
876
+ cli.addCommand(completionCommand);
877
+ return { cli, exitCode };
878
+ };
879
+ const UNKNOWN_COMMAND = /Command "(?<name>[^"]+)" not found/u;
880
+ const reportRunError = (error) => {
881
+ const logger = createLogger();
882
+ const message = error instanceof Error ? error.message : String(error);
883
+ const unknown = UNKNOWN_COMMAND.exec(message);
884
+ if (!unknown?.groups) {
885
+ logger.error(message);
886
+ return;
887
+ }
888
+ const name = unknown.groups.name ?? "";
889
+ const suggestion = closestMatch(name, COMMANDS);
890
+ logger.error(`Unknown command "${name}".${suggestion === void 0 ? "" : ` Did you mean "${suggestion}"?`}`);
891
+ logger.info("Run `lunora --help` to list commands, or `lunora docs` to open the documentation.");
892
+ };
893
+ const runCli = async (options = {}) => {
894
+ const { cli, exitCode } = buildCli(options);
895
+ try {
896
+ await cli.run({ shouldExitProcess: false });
897
+ } catch (error) {
898
+ reportRunError(error);
899
+ return 1;
900
+ }
901
+ await maybeNotifyUpdate({ current: VERSION, logger: createLogger() });
902
+ return exitCode.value;
903
+ };
904
+
905
+ export { COMMANDS, VERSION, runCli };