@lunora/cli 1.0.0-alpha.7 → 1.0.0-alpha.70

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 (59) hide show
  1. package/LICENSE.md +6 -0
  2. package/__assets__/package-og.svg +1 -1
  3. package/dist/bin.mjs +1 -1
  4. package/dist/index.d.mts +248 -115
  5. package/dist/index.d.ts +248 -115
  6. package/dist/index.mjs +8 -8
  7. package/dist/packem_chunks/handler.mjs +87 -6
  8. package/dist/packem_chunks/handler10.mjs +8 -14
  9. package/dist/packem_chunks/handler11.mjs +19 -189
  10. package/dist/packem_chunks/handler12.mjs +176 -115
  11. package/dist/packem_chunks/handler13.mjs +118 -52
  12. package/dist/packem_chunks/handler14.mjs +50 -43
  13. package/dist/packem_chunks/handler15.mjs +48 -67
  14. package/dist/packem_chunks/handler16.mjs +74 -37
  15. package/dist/packem_chunks/handler17.mjs +39 -100
  16. package/dist/packem_chunks/handler18.mjs +87 -152
  17. package/dist/packem_chunks/handler19.mjs +148 -67
  18. package/dist/packem_chunks/handler2.mjs +6 -4
  19. package/dist/packem_chunks/handler20.mjs +77 -75
  20. package/dist/packem_chunks/handler21.mjs +71 -288
  21. package/dist/packem_chunks/handler3.mjs +6 -3
  22. package/dist/packem_chunks/handler4.mjs +2 -2
  23. package/dist/packem_chunks/handler5.mjs +7 -4
  24. package/dist/packem_chunks/handler6.mjs +10 -7
  25. package/dist/packem_chunks/handler7.mjs +2 -2
  26. package/dist/packem_chunks/handler8.mjs +1 -1
  27. package/dist/packem_chunks/handler9.mjs +315 -12
  28. package/dist/packem_chunks/planDevCommand.mjs +653 -37
  29. package/dist/packem_chunks/runCodegenCommand.mjs +5 -4
  30. package/dist/packem_chunks/runDeployCommand.mjs +115 -20
  31. package/dist/packem_chunks/runInitCommand.mjs +1602 -172
  32. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +5 -4
  33. package/dist/packem_chunks/runResetCommand.mjs +2 -2
  34. package/dist/packem_chunks/runRpcCommand.mjs +3 -2
  35. package/dist/packem_shared/{COMMANDS-Bn8luojF.mjs → COMMANDS-0BD3K1pb.mjs} +79 -21
  36. package/dist/packem_shared/{DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs → DEFAULT_IMPORT_BATCH_SIZE-CRQmmBZM.mjs} +3 -2
  37. package/dist/packem_shared/{api-spec-CtA6ilu4.mjs → api-spec-Bx0iKbxA.mjs} +3 -1
  38. package/dist/packem_shared/{buildRegistryIndex-BcYe607_.mjs → buildRegistryIndex-CqM5FwGI.mjs} +1 -1
  39. package/dist/packem_shared/codegen-error-DJG-ghs_.mjs +31 -0
  40. package/dist/packem_shared/{command-BDXcJCCJ.mjs → command-lYnl4QyF.mjs} +6 -1
  41. package/dist/packem_shared/{commands-DqsEzojt.mjs → commands-vnRRkhNE.mjs} +123 -15
  42. package/dist/packem_shared/{createLogger-CHPNjFw2.mjs → createLogger-CIWSHrTL.mjs} +40 -8
  43. package/dist/packem_shared/{createRecordingSpawner-DxI3mebw.mjs → createRecordingSpawner-Cw5Iu73G.mjs} +12 -2
  44. package/dist/packem_shared/{detect-package-manager-DYp7n3mJ.mjs → detect-package-manager-DvEthdCw.mjs} +26 -30
  45. package/dist/packem_shared/{insertSchemaExtension-BuzF6-t2.mjs → insertSchemaExtension-DAqbfr9Z.mjs} +15 -10
  46. package/dist/packem_shared/{open-url-Dfq6fAyT.mjs → open-url-4PBLY9X0.mjs} +3 -2
  47. package/dist/packem_shared/{output-format-7gyGR3h8.mjs → output-format-B4642rjE.mjs} +1 -1
  48. package/dist/packem_shared/{parseManifest--vZf2FY1.mjs → parseManifest-BBrXCg9V.mjs} +16 -9
  49. package/dist/packem_shared/prompt-cancelled-APzX1Im-.mjs +9 -0
  50. package/dist/packem_shared/runAddCommand-BCcJzs_k.mjs +4 -0
  51. package/dist/packem_shared/storage-bGfgwOgF.mjs +84 -0
  52. package/dist/packem_shared/tui-prompts-BjEN8XgP.mjs +658 -0
  53. package/dist/packem_shared/wrangler-secrets-Dq_Fkbm-.mjs +49 -0
  54. package/package.json +10 -7
  55. package/skills/lunora-quickstart/SKILL.md +25 -5
  56. package/skills/lunora-setup-storage/SKILL.md +7 -3
  57. package/dist/packem_shared/features-ocSSpZtS.mjs +0 -24
  58. package/dist/packem_shared/runAddCommand-G544_v6e.mjs +0 -4
  59. package/dist/packem_shared/tui-prompts-XHFxlOg5.mjs +0 -269
@@ -1,14 +1,15 @@
1
1
  import { existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync, readFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { discoverSchema, discoverMigrations } from '@lunora/codegen';
4
+ import { LunoraError } from '@lunora/errors';
4
5
  import { join } from '@visulima/path';
5
6
  import { Project } from 'ts-morph';
6
7
  import { r as resolveAdminBaseUrl } from '../packem_shared/admin-url-4UzT-CI4.mjs';
7
- import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
8
+ import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
8
9
  import { diffSnapshots, renderMigrationFile } from '../packem_shared/diffSnapshots-BeDvvNiF.mjs';
9
10
  import { a as resolveProductionWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs';
10
11
  import schemaIrToSnapshot from '../packem_shared/schemaIrToSnapshot-DdsljJT-.mjs';
11
- import { runExportCommand, runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs';
12
+ import { runExportCommand, runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-CRQmmBZM.mjs';
12
13
 
13
14
  const SNAPSHOT_FILENAME = ".snapshot.json";
14
15
  const NON_ALPHANUMERIC = /[^\da-z]+/gu;
@@ -41,12 +42,12 @@ const loadSnapshot = (path) => {
41
42
  const raw = readFileSync(path, "utf8");
42
43
  const parsed = JSON.parse(raw);
43
44
  if (parsed.version !== 1) {
44
- throw new Error(`unsupported snapshot version: ${parsed.version}`);
45
+ throw new LunoraError("INTERNAL", `unsupported snapshot version: ${parsed.version}`);
45
46
  }
46
47
  return parsed;
47
48
  } catch (error) {
48
49
  const message = error instanceof Error ? error.message : String(error);
49
- throw new Error(`failed to read ${path}: ${message}`, { cause: error });
50
+ throw new LunoraError("INTERNAL", `failed to read ${path}: ${message}`, { cause: error });
50
51
  }
51
52
  };
52
53
  const runMigrateGenerateCommand = (options) => {
@@ -1,7 +1,7 @@
1
1
  import { existsSync, rmSync } from 'node:fs';
2
2
  import { join } from '@visulima/path';
3
- import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
4
- import { a as tuiConfirm } from '../packem_shared/tui-prompts-XHFxlOg5.mjs';
3
+ import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
4
+ import { b as tuiConfirm } from '../packem_shared/tui-prompts-BjEN8XgP.mjs';
5
5
 
6
6
  const runResetCommand = async (options) => {
7
7
  const cwd = options.cwd ?? process.cwd();
@@ -1,4 +1,5 @@
1
- import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
1
+ import { LunoraError } from '@lunora/errors';
2
+ import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
2
3
  import { r as resolveWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs';
3
4
 
4
5
  const parseArgsJson = (raw) => {
@@ -9,7 +10,7 @@ const parseArgsJson = (raw) => {
9
10
  return JSON.parse(raw);
10
11
  } catch (error) {
11
12
  const message = error instanceof Error ? error.message : String(error);
12
- throw new Error(`failed to parse --args as JSON: ${message}`, { cause: error });
13
+ throw new LunoraError("INTERNAL", `failed to parse --args as JSON: ${message}`, { cause: error });
13
14
  }
14
15
  };
15
16
  const TRAILING_SLASH = /\/$/u;
@@ -1,9 +1,11 @@
1
1
  import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { resolveHint, isLunoraError, flattenHint, findSolutionByMessage } from '@lunora/errors';
2
3
  import { createCerebro } from '@visulima/cerebro';
3
4
  import completionCommand from '@visulima/cerebro/command/completion';
4
5
  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';
6
+ import { A as API_SPEC_HELP } from './api-spec-Bx0iKbxA.mjs';
7
+ import { createLogger } from './createLogger-CIWSHrTL.mjs';
8
+ import { VisulimaError, renderError } from '@visulima/error';
7
9
  import { tmpdir } from 'node:os';
8
10
  import { join } from 'node:path';
9
11
 
@@ -14,7 +16,8 @@ const addCommand = {
14
16
  ["lunora add auth", "Add authentication (asks which provider)"],
15
17
  ["lunora add auth --provider clerk", "Add Clerk auth without prompting"],
16
18
  ["lunora add email", "Add transactional email (Cloudflare Email Workers + dev mail catcher)"],
17
- ["lunora add storage", "Add the R2 storage registry item"],
19
+ ["lunora add storage", "Add the R2 storage registry item (asks for the bucket name)"],
20
+ ["lunora add storage --bucket my-app-uploads", "Add storage with a bucket name, no prompt"],
18
21
  ["lunora add crons", "Add the scheduled-jobs registry item"],
19
22
  ["lunora add storage --ref alpha", "Add an item from the alpha branch's registry"]
20
23
  ],
@@ -25,7 +28,10 @@ const addCommand = {
25
28
  name: "add",
26
29
  options: [
27
30
  { description: "auth: provider to use without prompting (auth | clerk | auth0)", name: "provider", type: String },
28
- { description: "Skip the provider prompt and use the default (email & password)", name: "yes", type: Boolean },
31
+ { description: "auth: D1 database name to use without prompting (lowercase alphanumeric + hyphens)", name: "db", type: String },
32
+ { description: "storage: R2 bucket name to use without prompting (lowercase alphanumeric + hyphens)", name: "bucket", type: String },
33
+ { description: "email: verified destination address to use without prompting", name: "mail-to", type: String },
34
+ { description: "Skip prompts (auth provider, DB name, bucket name, mail destination) and use the defaults", name: "yes", type: Boolean },
29
35
  { description: "Local registry root (offline; expects <name>/ subdirs)", name: "from", type: String },
30
36
  { description: "Override the remote registry source base (e.g. gh:owner/repo/registry)", name: "source", type: String },
31
37
  {
@@ -33,7 +39,8 @@ const addCommand = {
33
39
  name: "ref",
34
40
  type: String
35
41
  },
36
- { description: "Permit --source values outside gh:/github:/https://", name: "allow-unsafe-source", type: Boolean }
42
+ { description: "Permit --source values outside gh:/github:/https://", name: "allow-unsafe-source", type: Boolean },
43
+ { description: "Output format: pretty (default) or json", name: "format", type: String }
37
44
  ]
38
45
  };
39
46
 
@@ -208,9 +215,19 @@ const deploymentsCommand = {
208
215
  };
209
216
 
210
217
  const devCommand = {
218
+ argument: {
219
+ description: "Optional subcommand: stop (shut the running dev server down) | status (report it) | logs (print its captured output)",
220
+ name: "args",
221
+ type: String
222
+ },
211
223
  description: "Run the dev stack: wrangler worker + studio + codegen watch",
212
224
  examples: [
213
225
  ["lunora dev", "Run the worker + studio + codegen watch"],
226
+ ["lunora dev --background", "Run detached: blocks until ready, prints URL + PID, then returns"],
227
+ ["lunora dev stop", "Stop the background/tracked dev server (idempotent)"],
228
+ ["lunora dev status", "Report the running dev server (URL, PID, uptime)"],
229
+ ["lunora dev logs", "Print the captured dev-server log (background runs)"],
230
+ ["lunora dev --json", "Machine-readable JSON log lines (also LUNORA_LOG_JSON=1)"],
214
231
  ["lunora dev --no-studio", "Skip the embedded studio server"],
215
232
  ["lunora dev --worker-port 8080", "Use a custom wrangler dev port"],
216
233
  ["lunora dev --remote", "Proxy D1/KV/R2 to the deployed worker (also LUNORA_REMOTE=1)"]
@@ -220,10 +237,19 @@ const devCommand = {
220
237
  return { default: m.execute };
221
238
  }),
222
239
  name: "dev",
240
+ // KEEP IN SYNC with `daemonArguments` in `./lifecycle.ts`: a new flag that
241
+ // must reach a `--background` daemon has to be forwarded there explicitly.
223
242
  options: [
224
243
  { description: `Which API spec(s) codegen emits: ${API_SPEC_HELP} (default openapi)`, name: "api-spec", type: String },
225
244
  { description: "Studio server port (default 6173)", name: "port", type: Number },
226
245
  { description: "wrangler dev port (default 8787)", name: "worker-port", type: Number },
246
+ {
247
+ description: "Run the dev server as a managed background process (auto-enabled when an AI agent is detected; LUNORA_AGENT_MODE=0 disables)",
248
+ name: "background",
249
+ type: Boolean
250
+ },
251
+ { description: "Emit machine-readable JSON log lines (also LUNORA_LOG_JSON=1; auto-enabled for AI agents)", name: "json", type: Boolean },
252
+ { description: "How many trailing lines `lunora dev logs` prints (default 100, 0 = all)", name: "lines", type: Number },
227
253
  { description: "Don't start the embedded studio server", name: "no-studio", type: Boolean },
228
254
  { description: "Don't watch + regenerate codegen", name: "no-codegen", type: Boolean },
229
255
  { description: "Proxy D1/KV/R2 bindings to the deployed worker (or set LUNORA_REMOTE=1)", name: "remote", type: Boolean }
@@ -256,21 +282,24 @@ const doctorCommand = {
256
282
  };
257
283
 
258
284
  const envCommand = {
259
- argument: { description: "list | get <KEY> | set <KEY> <VALUE> | unset <KEY> | push | diff | doctor", name: "subcommand", type: String },
260
- description: "Manage .dev.vars and sync secrets via wrangler (list | get | set | unset | push | diff | doctor)",
285
+ argument: { description: "list | get <KEY> | set <KEY> <VALUE> | unset <KEY> | generate [KEY] | push | diff | doctor", name: "subcommand", type: String },
286
+ description: "Manage .dev.vars and sync secrets via wrangler (list | get | set | unset | generate | push | diff | doctor)",
261
287
  examples: [
262
288
  ["lunora env list", "List .dev.vars keys"],
263
289
  ["lunora env set API_KEY secret", "Set a local variable"],
290
+ ["lunora env generate", "Generate strong values for the project's secrets (print KEY=value)"],
291
+ ["lunora env generate AUTH_SECRET --set", "Generate one secret and write it to .dev.vars"],
264
292
  ["lunora env push --yes", "Upload secrets to Cloudflare"],
265
293
  ["lunora env diff", "Compare local .dev.vars keys against Cloudflare"]
266
294
  ],
267
295
  group: "Data",
268
- loader: () => import('../packem_chunks/handler21.mjs').then((m) => {
296
+ loader: () => import('../packem_chunks/handler9.mjs').then((m) => {
269
297
  return { default: m.execute };
270
298
  }),
271
299
  name: "env",
272
300
  options: [
273
301
  { description: "Target production for `push` (passes --env production to wrangler)", name: "prod", type: Boolean },
302
+ { description: "For `generate` — write the generated secrets into .dev.vars instead of printing them", name: "set", type: Boolean },
274
303
  {
275
304
  description: "Push secrets to a temporary-account deployment when unauthenticated (wrangler secret put --temporary). Errors if you're already authenticated.",
276
305
  name: "temporary",
@@ -288,7 +317,7 @@ const exportCommand = {
288
317
  ["lunora export --tables messages,users", "Export only specific tables"]
289
318
  ],
290
319
  group: "Data",
291
- loader: () => import('../packem_chunks/handler9.mjs').then((m) => {
320
+ loader: () => import('../packem_chunks/handler10.mjs').then((m) => {
292
321
  return { default: m.execute };
293
322
  }),
294
323
  name: "export",
@@ -310,7 +339,7 @@ const importCommand = {
310
339
  description: "Bulk-insert rows from an NDJSON file via the worker's admin endpoint",
311
340
  examples: [["lunora import backup.ndjson", "Bulk-insert rows from an NDJSON file"]],
312
341
  group: "Data",
313
- loader: () => import('../packem_chunks/handler10.mjs').then((m) => {
342
+ loader: () => import('../packem_chunks/handler11.mjs').then((m) => {
314
343
  return { default: m.execute };
315
344
  }),
316
345
  name: "import",
@@ -334,7 +363,7 @@ const infoCommand = {
334
363
  ["lunora info --json", "Emit a JSON snapshot"]
335
364
  ],
336
365
  group: "Project",
337
- loader: () => import('../packem_chunks/handler11.mjs').then((m) => {
366
+ loader: () => import('../packem_chunks/handler12.mjs').then((m) => {
338
367
  return { default: m.execute };
339
368
  }),
340
369
  name: "info",
@@ -415,6 +444,16 @@ const initCommand = {
415
444
  description: "Also scaffold a CI deploy pipeline: github (.github/workflows/deploy.yml) or gitlab (.gitlab-ci.yml)",
416
445
  name: "ci",
417
446
  type: String
447
+ },
448
+ {
449
+ description: "Add features non-interactively after scaffolding (comma-separated): auth | email | storage | ratelimit | crons | presence | backup",
450
+ name: "add",
451
+ type: String
452
+ },
453
+ {
454
+ description: "Walk through every step (prompts + output) without writing files, installing, or running git",
455
+ name: "dry-run",
456
+ type: Boolean
418
457
  }
419
458
  ]
420
459
  };
@@ -428,7 +467,7 @@ const insightsCommand = {
428
467
  ["lunora insights --prod --url https://app.example.com --token $LUNORA_ADMIN_TOKEN", "Report against production"]
429
468
  ],
430
469
  group: "Develop",
431
- loader: () => import('../packem_chunks/handler12.mjs').then((m) => {
470
+ loader: () => import('../packem_chunks/handler13.mjs').then((m) => {
432
471
  return { default: m.execute };
433
472
  }),
434
473
  name: "insights",
@@ -450,7 +489,7 @@ const linkCommand = {
450
489
  ["lunora link --remove", "Remove the link"]
451
490
  ],
452
491
  group: "Deploy",
453
- loader: () => import('../packem_chunks/handler13.mjs').then((m) => {
492
+ loader: () => import('../packem_chunks/handler14.mjs').then((m) => {
454
493
  return { default: m.execute };
455
494
  }),
456
495
  name: "link",
@@ -466,7 +505,7 @@ const logsCommand = {
466
505
  argument: { description: "Worker name (defaults to the name in wrangler config)", name: "worker", type: String },
467
506
  description: "Stream live logs from a deployed Lunora Worker",
468
507
  group: "Deploy",
469
- loader: () => import('../packem_chunks/handler14.mjs').then((m) => {
508
+ loader: () => import('../packem_chunks/handler15.mjs').then((m) => {
470
509
  return { default: m.execute };
471
510
  }),
472
511
  name: "logs",
@@ -526,7 +565,7 @@ const prepareCommand = {
526
565
  description: "Run codegen + binding reconcile + wrangler validation (no Vite) — for CI",
527
566
  examples: [["lunora prepare", "Codegen + binding reconcile + validate (CI, before deploy)"]],
528
567
  group: "Deploy",
529
- loader: () => import('../packem_chunks/handler15.mjs').then((m) => {
568
+ loader: () => import('../packem_chunks/handler16.mjs').then((m) => {
530
569
  return { default: m.execute };
531
570
  }),
532
571
  name: "prepare",
@@ -550,7 +589,7 @@ const registryCommand = {
550
589
  ["lunora registry build --check", "Verify the committed catalog is current"]
551
590
  ],
552
591
  group: "Project",
553
- loader: () => import('../packem_chunks/handler16.mjs').then((m) => {
592
+ loader: () => import('../packem_chunks/handler17.mjs').then((m) => {
554
593
  return { default: m.execute };
555
594
  }),
556
595
  name: "registry",
@@ -600,7 +639,7 @@ const rulesCommand = {
600
639
  ["lunora rules check --strict", "Exit non-zero when rules are missing (CI gate)"]
601
640
  ],
602
641
  group: "Project",
603
- loader: () => import('../packem_chunks/handler17.mjs').then((m) => {
642
+ loader: () => import('../packem_chunks/handler18.mjs').then((m) => {
604
643
  return { default: m.execute };
605
644
  }),
606
645
  name: "rules",
@@ -638,7 +677,7 @@ const seedCommand = {
638
677
  ["lunora seed --seed 7 --dry-run", "Print the NDJSON for seed 7 without inserting"]
639
678
  ],
640
679
  group: "Data",
641
- loader: () => import('../packem_chunks/handler18.mjs').then((m) => {
680
+ loader: () => import('../packem_chunks/handler19.mjs').then((m) => {
642
681
  return { default: m.execute };
643
682
  }),
644
683
  name: "seed",
@@ -667,7 +706,7 @@ const verifyCommand = {
667
706
  ["lunora verify --no-typecheck", "Skip the TypeScript type-check"]
668
707
  ],
669
708
  group: "Deploy",
670
- loader: () => import('../packem_chunks/handler19.mjs').then((m) => {
709
+ loader: () => import('../packem_chunks/handler20.mjs').then((m) => {
671
710
  return { default: m.execute };
672
711
  }),
673
712
  name: "verify",
@@ -686,13 +725,28 @@ const viewCommand = {
686
725
  ["lunora view --remote", "Open the deployed studio"]
687
726
  ],
688
727
  group: "Project",
689
- loader: () => import('../packem_chunks/handler20.mjs').then((m) => {
728
+ loader: () => import('../packem_chunks/handler21.mjs').then((m) => {
690
729
  return { default: m.execute };
691
730
  }),
692
731
  name: "view",
693
732
  options: [{ description: "Open the deployed worker URL instead of localhost", name: "remote", type: Boolean }]
694
733
  };
695
734
 
735
+ const NO_STACK = { filterStacktrace: () => false, hideErrorCodeView: true };
736
+ const renderLunoraError = (error, options = {}) => {
737
+ const message = error instanceof Error ? error.message : String(error);
738
+ const hint = resolveHint(isLunoraError(error) ? { code: error.code, hint: error.hint, message } : message);
739
+ const rendered = new VisulimaError({
740
+ // `renderError` iterates `hint` as lines; the shared flattener returns one
741
+ // string, so split it back to lines for the terminal renderer.
742
+ hint: hint === void 0 ? void 0 : flattenHint(hint).split("\n"),
743
+ message: options.reason === void 0 ? message : `${options.reason}: ${message}`,
744
+ name: error instanceof Error && error.name.length > 0 ? error.name : "Error"
745
+ });
746
+ rendered.stack = "";
747
+ return renderError(rendered, NO_STACK);
748
+ };
749
+
696
750
  const editDistance = (a, b) => {
697
751
  const distances = Array.from({ length: b.length + 1 }, (_, index) => index);
698
752
  for (let row = 1; row <= a.length; row += 1) {
@@ -907,7 +961,11 @@ const reportRunError = (error) => {
907
961
  const message = error instanceof Error ? error.message : String(error);
908
962
  const unknown = UNKNOWN_COMMAND.exec(message);
909
963
  if (!unknown?.groups) {
910
- logger.error(message);
964
+ if (isLunoraError(error) || findSolutionByMessage(message) !== void 0) {
965
+ logger.error(renderLunoraError(error));
966
+ } else {
967
+ logger.error(message);
968
+ }
911
969
  return;
912
970
  }
913
971
  const name = unknown.groups.name ?? "";
@@ -1,5 +1,6 @@
1
1
  import { createWriteStream, createReadStream } from 'node:fs';
2
2
  import { unlink, stat } from 'node:fs/promises';
3
+ import { LunoraError } from '@lunora/errors';
3
4
  import { r as resolveAdminBaseUrl } from './admin-url-4UzT-CI4.mjs';
4
5
 
5
6
  const EXPORT_ENDPOINT_PATH = "/_lunora/admin/export";
@@ -183,7 +184,7 @@ const runImportCommand = async (options) => {
183
184
  });
184
185
  if (!response.ok) {
185
186
  const text = await response.text().catch(() => "<no body>");
186
- throw new Error(`import batch failed (HTTP ${String(response.status)}): ${text}`);
187
+ throw new LunoraError("INTERNAL", `import batch failed (HTTP ${String(response.status)}): ${text}`);
187
188
  }
188
189
  const json = await response.json();
189
190
  if (json.inserted) {
@@ -213,7 +214,7 @@ const runImportCommand = async (options) => {
213
214
  parsedDocument = JSON.parse(trimmed);
214
215
  } catch (error) {
215
216
  const message = error instanceof Error ? error.message : String(error);
216
- throw new Error(`invalid JSON on line ${String(lineNumber)}: ${message}`, { cause: error });
217
+ throw new LunoraError("INTERNAL", `invalid JSON on line ${String(lineNumber)}: ${message}`, { cause: error });
217
218
  }
218
219
  batch.push(JSON.stringify({ doc: parsedDocument, table: options.table }));
219
220
  };
@@ -1,3 +1,5 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const API_SPEC_VALUES = ["both", "none", "openapi", "openrpc"];
2
4
  const API_SPEC_HELP = API_SPEC_VALUES.join(" | ");
3
5
  const parseApiSpec = (value) => {
@@ -7,7 +9,7 @@ const parseApiSpec = (value) => {
7
9
  if (API_SPEC_VALUES.includes(value)) {
8
10
  return value;
9
11
  }
10
- throw new Error(`invalid --api-spec "${value}" — expected one of: ${API_SPEC_HELP}`);
12
+ throw new LunoraError("INTERNAL", `invalid --api-spec "${value}" — expected one of: ${API_SPEC_HELP}`);
11
13
  };
12
14
 
13
15
  export { API_SPEC_HELP as A, parseApiSpec as p };
@@ -1,6 +1,6 @@
1
1
  import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
2
2
  import { join } from '@visulima/path';
3
- import parseManifest from './parseManifest--vZf2FY1.mjs';
3
+ import parseManifest from './parseManifest-BBrXCg9V.mjs';
4
4
 
5
5
  const CONTROL_CHARS = /[\u0000-\u001F\u007F-\u009F]/gu;
6
6
  const stripControlChars = (value) => value === void 0 ? void 0 : value.replaceAll(CONTROL_CHARS, "");
@@ -0,0 +1,31 @@
1
+ import { findLunoraSolution } from '@lunora/codegen';
2
+ import { VisulimaError, renderError } from '@visulima/error';
3
+
4
+ const flattenSolutionBody = (solution) => solution.body.split("\n").filter((line) => !line.startsWith("```")).join("\n").replaceAll(/\*\*(.+?)\*\*/gu, "$1").replaceAll(/`([^`]+)`/gu, "$1");
5
+ const NO_STACK = { filterStacktrace: () => false, hideErrorCodeView: true };
6
+ const renderCodegenFailure = (error, reason) => {
7
+ const message = error instanceof Error ? error.message : String(error);
8
+ const solution = findLunoraSolution(message);
9
+ const rendered = new VisulimaError({
10
+ hint: solution ? [solution.header, "", flattenSolutionBody(solution)] : void 0,
11
+ message: reason === void 0 ? `codegen failed: ${message}` : `codegen failed (${reason}): ${message}`,
12
+ name: "CodegenError"
13
+ });
14
+ rendered.stack = "";
15
+ return renderError(rendered, NO_STACK);
16
+ };
17
+ const renderCodegenHint = (message) => {
18
+ const solution = findLunoraSolution(message);
19
+ if (solution === void 0) {
20
+ return void 0;
21
+ }
22
+ const rendered = new VisulimaError({
23
+ hint: [flattenSolutionBody(solution)],
24
+ message: solution.header,
25
+ name: "Hint"
26
+ });
27
+ rendered.stack = "";
28
+ return renderError(rendered, NO_STACK);
29
+ };
30
+
31
+ export { renderCodegenHint as a, renderCodegenFailure as r };
@@ -1,4 +1,5 @@
1
- import { createLogger } from './createLogger-CHPNjFw2.mjs';
1
+ import { createLogger } from './createLogger-CIWSHrTL.mjs';
2
+ import { P as PromptCancelledError, a as PROMPT_CANCEL_EXIT_CODE } from './prompt-cancelled-APzX1Im-.mjs';
2
3
 
3
4
  const defineHandler = (body) => async (toolbox) => {
4
5
  const logger = createLogger();
@@ -6,6 +7,10 @@ const defineHandler = (body) => async (toolbox) => {
6
7
  const { code } = await body({ argument: toolbox.argument, cwd: toolbox.process.cwd, logger, options: toolbox.options });
7
8
  toolbox.process.exit(code);
8
9
  } catch (error) {
10
+ if (error instanceof PromptCancelledError) {
11
+ toolbox.process.exit(PROMPT_CANCEL_EXIT_CODE);
12
+ return;
13
+ }
9
14
  logger.error(error instanceof Error ? error.message : String(error));
10
15
  toolbox.process.exit(1);
11
16
  }
@@ -3,13 +3,14 @@ import { dirname, join } from '@visulima/path';
3
3
  import { DEV_VARS_FILE, parseDevVariableEntries } from '@lunora/config';
4
4
  import { modify, applyEdits, parse } from 'jsonc-parser';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { a as tuiConfirm } from './tui-prompts-XHFxlOg5.mjs';
7
- import { collectCatalog, buildRegistryIndex } from './buildRegistryIndex-BcYe607_.mjs';
8
- import { insertSchemaExtension } from './insertSchemaExtension-BuzF6-t2.mjs';
6
+ import { LunoraError } from '@lunora/errors';
7
+ import { b as tuiConfirm } from './tui-prompts-BjEN8XgP.mjs';
8
+ import { collectCatalog, buildRegistryIndex } from './buildRegistryIndex-CqM5FwGI.mjs';
9
+ import { insertSchemaExtension } from './insertSchemaExtension-DAqbfr9Z.mjs';
9
10
  import { createHash } from 'node:crypto';
10
11
  import { tmpdir } from 'node:os';
11
12
  import { downloadTemplate } from 'giget';
12
- import parseManifest from './parseManifest--vZf2FY1.mjs';
13
+ import parseManifest from './parseManifest-BBrXCg9V.mjs';
13
14
 
14
15
  const DEFAULT_SOURCE_REF_FALLBACK = "alpha";
15
16
  const STABLE_BRANCH = "main";
@@ -54,17 +55,88 @@ const resolveVersionRef = (version) => {
54
55
  const resolveSourceRef = (ref) => {
55
56
  if (ref !== void 0 && ref.length > 0) {
56
57
  if (!isSafeRef(ref)) {
57
- throw new Error(`invalid --ref "${ref}" — a ref may contain letters, digits, ".", "_", "-", "/", "@" and must not contain "..".`);
58
+ throw new LunoraError("INTERNAL", `invalid --ref "${ref}" — a ref may contain letters, digits, ".", "_", "-", "/", "@" and must not contain "..".`);
58
59
  }
59
60
  return ref;
60
61
  }
61
62
  return resolveVersionRef(resolveCliVersion());
62
63
  };
64
+ const SOURCE_REPO = "anolilab/lunora";
65
+ const COMMIT_SHA = /^[0-9a-f]{40}$/iu;
66
+ const SEMVER_BODY = String.raw`\d+\.\d+\.\d+(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)+)?(?:\+[0-9A-Za-z.-]+)?`;
67
+ const LEADING_VERSION_TAG = new RegExp(String.raw`^v?${SEMVER_BODY}$`, "u");
68
+ const PACKAGE_VERSION_TAG = new RegExp(String.raw`^(?:@[\w.-]+\/)?[\w.-]+@${SEMVER_BODY}$`, "u");
69
+ const isImmutableRef = (ref) => COMMIT_SHA.test(ref) || LEADING_VERSION_TAG.test(ref) || PACKAGE_VERSION_TAG.test(ref);
70
+ const githubAuthHeaders = () => {
71
+ const token = process.env["GITHUB_TOKEN"] ?? process.env["GH_TOKEN"];
72
+ return token !== void 0 && token.length > 0 ? { authorization: `Bearer ${token}` } : {};
73
+ };
74
+ const fetchBranchSha = async (branch) => {
75
+ try {
76
+ const response = await fetch(`https://api.github.com/repos/${SOURCE_REPO}/commits/${encodeURIComponent(branch)}`, {
77
+ headers: { accept: "application/vnd.github+json", "user-agent": "lunora-cli", ...githubAuthHeaders() },
78
+ signal: AbortSignal.timeout(1e4)
79
+ });
80
+ if (!response.ok) {
81
+ return void 0;
82
+ }
83
+ const body = await response.json();
84
+ return typeof body.sha === "string" && COMMIT_SHA.test(body.sha) ? body.sha : void 0;
85
+ } catch {
86
+ return void 0;
87
+ }
88
+ };
89
+ const resolvePinnedSourceRef = async (ref, logger) => {
90
+ const resolved = resolveSourceRef(ref);
91
+ if (isImmutableRef(resolved)) {
92
+ return resolved;
93
+ }
94
+ const sha = await fetchBranchSha(resolved);
95
+ if (sha === void 0) {
96
+ logger.warn(`could not pin ${SOURCE_REPO}#${resolved} to a commit — fetching the UNPINNED branch (set GITHUB_TOKEN if rate-limited).`);
97
+ return resolved;
98
+ }
99
+ logger.info(`pinned ${SOURCE_REPO}#${resolved} → ${sha}`);
100
+ return sha;
101
+ };
63
102
  const STABLE_DIST_TAG = "latest";
64
103
  const resolveDistTag = () => {
65
104
  const ref = resolveVersionRef(resolveCliVersion());
66
105
  return ref === STABLE_BRANCH ? STABLE_DIST_TAG : ref;
67
106
  };
107
+ const DEFAULT_REGISTRY = "https://registry.npmjs.org";
108
+ const registryBase = () => {
109
+ const configured = process.env["npm_config_registry"];
110
+ const base = configured !== void 0 && configured.length > 0 ? configured : DEFAULT_REGISTRY;
111
+ return base.endsWith("/") ? base.slice(0, -1) : base;
112
+ };
113
+ const resolveTagVersion = async (packageName, tag) => {
114
+ try {
115
+ const response = await fetch(`${registryBase()}/${packageName.replace("/", "%2F")}`, {
116
+ headers: { accept: "application/vnd.npm.install-v1+json" },
117
+ signal: AbortSignal.timeout(1e4)
118
+ });
119
+ if (!response.ok) {
120
+ return void 0;
121
+ }
122
+ const packument = await response.json();
123
+ return packument["dist-tags"]?.[tag];
124
+ } catch {
125
+ return void 0;
126
+ }
127
+ };
128
+ const resolveTagVersions = async (names, tag) => {
129
+ const resolved = /* @__PURE__ */ new Map();
130
+ await Promise.all(
131
+ [...new Set(names)].map(async (name) => {
132
+ const version = await resolveTagVersion(name, tag);
133
+ if (version !== void 0) {
134
+ resolved.set(name, version);
135
+ }
136
+ })
137
+ );
138
+ return resolved;
139
+ };
68
140
 
69
141
  const resolveDepRange = (range) => {
70
142
  if (!range.startsWith("workspace:")) {
@@ -386,11 +458,12 @@ export const schema = defineSchema({});
386
458
  return { kind: "skipped", path: schemaPath };
387
459
  }
388
460
  if (result.reason === "invalid-identifier") {
389
- throw new Error(
461
+ throw new LunoraError(
462
+ "INTERNAL",
390
463
  `schema-extension item "${itemKey}" is not a valid JS identifier — it is spliced into lunora/schema.ts as \`import { ${itemKey} }\` / \`.extend(${itemKey}.extension)\`. Rename the item to a valid identifier (no leading digit, no "-").`
391
464
  );
392
465
  }
393
- throw new Error(`schema-extension merge failed for "${itemKey}": ${result.reason}`);
466
+ throw new LunoraError("INTERNAL", `schema-extension merge failed for "${itemKey}": ${result.reason}`);
394
467
  };
395
468
  const previewWholeFile = (file, current, incoming, exists, logger) => {
396
469
  const lines = renderDiff(current, incoming);
@@ -478,7 +551,8 @@ const DEFAULT_SOURCE_BASE = "gh:anolilab/lunora/registry";
478
551
  const VALID_ITEM_NAME = /^[A-Za-z0-9][\w-]*$/u;
479
552
  const assertSafeItemName = (name) => {
480
553
  if (!VALID_ITEM_NAME.test(name)) {
481
- throw new Error(
554
+ throw new LunoraError(
555
+ "INTERNAL",
482
556
  `invalid registry item name "${name}" — names must match ${VALID_ITEM_NAME.source} (letters, digits, "-", "_"; no path separators or "..")`
483
557
  );
484
558
  }
@@ -515,29 +589,39 @@ const fetchToStaging = async (remote, label, logger) => {
515
589
  throw error;
516
590
  }
517
591
  };
592
+ const remoteRefCache = /* @__PURE__ */ new WeakMap();
593
+ const resolveRemoteRef = async (options) => {
594
+ const cached = remoteRefCache.get(options);
595
+ if (cached !== void 0) {
596
+ return cached;
597
+ }
598
+ const pending = options.source !== void 0 && options.source.length > 0 ? Promise.resolve(resolveSourceRef(options.ref)) : resolvePinnedSourceRef(options.ref, options.logger);
599
+ remoteRefCache.set(options, pending);
600
+ return pending;
601
+ };
518
602
  const resolveItemDirectory = async (name, options) => {
519
603
  assertSafeItemName(name);
520
604
  if (options.from !== void 0) {
521
605
  const directory = join(options.from, name);
522
606
  if (!existsSync(directory)) {
523
- throw new Error(`registry item not found in local source: ${directory}`);
607
+ throw new LunoraError("INTERNAL", `registry item not found in local source: ${directory}`);
524
608
  }
525
609
  return { cleanup: () => {
526
610
  }, directory };
527
611
  }
528
612
  const base = options.source ?? DEFAULT_SOURCE_BASE;
529
- return fetchToStaging(`${base}/${name}#${resolveSourceRef(options.ref)}`, "item", options.logger);
613
+ return fetchToStaging(`${base}/${name}#${await resolveRemoteRef(options)}`, "item", options.logger);
530
614
  };
531
615
  const resolveRegistryRoot = async (options) => {
532
616
  if (options.from !== void 0) {
533
617
  if (!existsSync(options.from)) {
534
- throw new Error(`registry root not found: ${options.from}`);
618
+ throw new LunoraError("INTERNAL", `registry root not found: ${options.from}`);
535
619
  }
536
620
  return { cleanup: () => {
537
621
  }, root: options.from };
538
622
  }
539
623
  const base = options.source ?? DEFAULT_SOURCE_BASE;
540
- const { cleanup, directory } = await fetchToStaging(`${base}#${resolveSourceRef(options.ref)}`, "registry", options.logger);
624
+ const { cleanup, directory } = await fetchToStaging(`${base}#${await resolveRemoteRef(options)}`, "registry", options.logger);
541
625
  return { cleanup, root: directory };
542
626
  };
543
627
  const readManifest = (itemDirectory, name) => {
@@ -554,7 +638,7 @@ const resolvePlan = async (names, options) => {
554
638
  return;
555
639
  }
556
640
  if (inProgress.has(name)) {
557
- throw new Error(`cyclic registry dependency detected at "${name}"`);
641
+ throw new LunoraError("INTERNAL", `cyclic registry dependency detected at "${name}"`);
558
642
  }
559
643
  inProgress.add(name);
560
644
  const { cleanup, directory } = await resolveItemDirectory(name, options);
@@ -583,6 +667,26 @@ const resolvePlan = async (names, options) => {
583
667
  const emptyResult = () => {
584
668
  return { bindings: [], code: 0, deps: [], skipped: [], written: [] };
585
669
  };
670
+ const setBindingField = (manifest, section, match, field, fieldValue) => {
671
+ if (!manifest.bindings) {
672
+ return manifest;
673
+ }
674
+ return {
675
+ ...manifest,
676
+ bindings: manifest.bindings.map((binding) => {
677
+ if (binding.path[0] !== section || !Array.isArray(binding.value)) {
678
+ return binding;
679
+ }
680
+ const entries = binding.value;
681
+ return {
682
+ ...binding,
683
+ value: entries.map(
684
+ (entry) => typeof entry === "object" && entry !== null && entry[match.key] === match.value ? { ...entry, [field]: fieldValue } : entry
685
+ )
686
+ };
687
+ })
688
+ };
689
+ };
586
690
 
587
691
  const printPlan = (logger, manifest) => {
588
692
  const label = manifest.title ?? manifest.description;
@@ -688,8 +792,12 @@ const runAddCommand = async (options) => {
688
792
  }
689
793
  let cleanups = [];
690
794
  try {
691
- const { cleanups: planCleanups, items } = await resolvePlan(options.names, options);
795
+ const { cleanups: planCleanups, items: resolvedItems } = await resolvePlan(options.names, options);
692
796
  cleanups = planCleanups;
797
+ const { transformManifest } = options;
798
+ const items = transformManifest ? resolvedItems.map((item) => {
799
+ return { ...item, manifest: transformManifest(item.manifest) };
800
+ }) : resolvedItems;
693
801
  for (const { manifest } of items) {
694
802
  printPlan(options.logger, manifest);
695
803
  }
@@ -785,4 +893,4 @@ const runBuildIndexCommand = async (options) => {
785
893
  return empty;
786
894
  };
787
895
 
788
- export { runBuildIndexCommand as a, runRegistryViewCommand as b, resolveDistTag as c, resolveSourceRef as d, runListCommand as e, runAddCommand as r };
896
+ export { runBuildIndexCommand as a, runRegistryViewCommand as b, resolveTagVersions as c, resolveSourceRef as d, resolvePinnedSourceRef as e, resolveDistTag as f, runListCommand as g, runAddCommand as r, setBindingField as s };