@lunora/cli 1.0.0-alpha.20 → 1.0.0-alpha.22

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.
package/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CodegenOptions, SchemaIR } from '@lunora/codegen';
2
2
  import '@visulima/cerebro';
3
- import { ensureDevVariables, ensureDevVarsExample, materializeRemoteWranglerConfig } from '@lunora/config';
3
+ import { ensureDevVariables, ensureDevVarsExample, fillDevSecrets, materializeRemoteWranglerConfig } from '@lunora/config';
4
4
  export { REQUIRED_COMPATIBILITY_DATE, REQUIRED_FLAG, type WranglerProjectValidationOptions as WranglerValidationOptions, type WranglerValidationReport, type WranglerProjectValidationResult as WranglerValidationResult, validateWranglerProject as validateWrangler, validateWranglerConfig } from '@lunora/config';
5
5
  /** Every command name the CLI registers (drives the `CommandName` type + tests). */
6
6
  declare const COMMANDS: readonly ["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"];
@@ -429,6 +429,8 @@ interface DevCommandOptions {
429
429
  ensureEnv?: typeof ensureDevVariables;
430
430
  /** Injection seam for tests — defaults to the real `.dev.vars.example` package-aware scaffolder. */
431
431
  ensureExample?: typeof ensureDevVarsExample;
432
+ /** Injection seam for tests — defaults to the real empty-secret/admin-token filler. */
433
+ fillSecrets?: typeof fillDevSecrets;
432
434
  logger: Logger;
433
435
  /** Injection seam for tests — defaults to the real remote-config materializer. */
434
436
  materializeRemote?: typeof materializeRemoteWranglerConfig;
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CodegenOptions, SchemaIR } from '@lunora/codegen';
2
2
  import '@visulima/cerebro';
3
- import { ensureDevVariables, ensureDevVarsExample, materializeRemoteWranglerConfig } from '@lunora/config';
3
+ import { ensureDevVariables, ensureDevVarsExample, fillDevSecrets, materializeRemoteWranglerConfig } from '@lunora/config';
4
4
  export { REQUIRED_COMPATIBILITY_DATE, REQUIRED_FLAG, type WranglerProjectValidationOptions as WranglerValidationOptions, type WranglerValidationReport, type WranglerProjectValidationResult as WranglerValidationResult, validateWranglerProject as validateWrangler, validateWranglerConfig } from '@lunora/config';
5
5
  /** Every command name the CLI registers (drives the `CommandName` type + tests). */
6
6
  declare const COMMANDS: readonly ["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"];
@@ -429,6 +429,8 @@ interface DevCommandOptions {
429
429
  ensureEnv?: typeof ensureDevVariables;
430
430
  /** Injection seam for tests — defaults to the real `.dev.vars.example` package-aware scaffolder. */
431
431
  ensureExample?: typeof ensureDevVarsExample;
432
+ /** Injection seam for tests — defaults to the real empty-secret/admin-token filler. */
433
+ fillSecrets?: typeof fillDevSecrets;
432
434
  logger: Logger;
433
435
  /** Injection seam for tests — defaults to the real remote-config materializer. */
434
436
  materializeRemote?: typeof materializeRemoteWranglerConfig;
@@ -1,5 +1,5 @@
1
1
  import { spawn } from 'node:child_process';
2
- import { detectAgentRules, resolveRemoteEnabled, readProjectRemotePreference, inferLunoraBindings, packageNamesFromBindings, ensureDevVarsExample, ensureDevVariables, isInteractive, DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, claimAgentRulesHint, AGENT_RULES_HINT, materializeRemoteWranglerConfig, formatLunoraEvent } from '@lunora/config';
2
+ import { detectAgentRules, resolveRemoteEnabled, readProjectRemotePreference, inferLunoraBindings, packageNamesFromBindings, ensureDevVarsExample, ensureDevVariables, isInteractive, DEV_VARS_FILE, DEV_VARS_EXAMPLE_FILE, fillDevSecrets, claimAgentRulesHint, AGENT_RULES_HINT, materializeRemoteWranglerConfig, formatLunoraEvent } from '@lunora/config';
3
3
  import { p as parseApiSpec } from '../packem_shared/api-spec-CtA6ilu4.mjs';
4
4
  import { existsSync, watch } from 'node:fs';
5
5
  import { join } from 'node:path';
@@ -409,6 +409,15 @@ const offerDevVariablesScaffold = async (options, cwd) => {
409
409
  `hint: ${DEV_VARS_FILE} was not scaffolded (non-interactive run). Copy ${DEV_VARS_EXAMPLE_FILE} → ${DEV_VARS_FILE} and fill in secrets, or run \`lunora dev\` in an interactive terminal to scaffold automatically.`
410
410
  );
411
411
  }
412
+ try {
413
+ (options.fillSecrets ?? fillDevSecrets)({
414
+ cwd,
415
+ info: (message) => {
416
+ options.logger.info(message);
417
+ }
418
+ });
419
+ } catch {
420
+ }
412
421
  };
413
422
  const runDevCommand = async (options) => {
414
423
  const plan = planDevCommand(options);
@@ -385,6 +385,920 @@ const offerRegistryExtras = async (deps) => {
385
385
  await deps.applyAll(plans);
386
386
  };
387
387
 
388
+ const LOGO_PATH = "M 259.500 10.552 C 220.080 15.859, 182.424 32.566, 152.500 58.025 C 110.179 94.031, 85.380 137.183, 77.518 188.500 C 75.410 202.255, 74.569 225.677, 75.796 236.466 C 76.757 244.917, 76.683 245.692, 74.518 249.966 C 63.118 272.466, 53.141 303.876, 51.382 322.799 L 50.718 329.943 71.960 320.471 C 83.643 315.262, 93.326 311, 93.478 311 C 93.630 311, 96.547 316.063, 99.959 322.250 C 103.371 328.438, 107.249 334.850, 108.577 336.500 L 110.990 339.500 110.981 336 C 110.977 334.075, 111.499 324.991, 112.143 315.813 L 113.312 299.127 121.406 293.336 C 132.495 285.403, 149.593 271.554, 161 261.268 C 171.556 251.748, 189.116 235, 188.540 235 C 188.337 235, 183.069 238.648, 176.835 243.106 C 142.318 267.789, 68.537 314, 63.646 314 C 61.843 314, 72.791 281.179, 80.905 262.259 C 92.233 235.845, 107.473 212.389, 132.106 183.453 L 138.451 176 148.268 176 C 176.192 176, 197.512 187.154, 212.868 209.797 C 216.470 215.108, 217.035 216.595, 216.477 219.297 C 211.386 243.968, 202.359 274.496, 193.797 296 C 183.898 320.861, 167.147 352.101, 152.395 373.215 L 147.004 380.930 152.891 385.830 C 161.400 392.911, 165.563 396, 166.594 395.998 C 167.092 395.998, 168.772 391.641, 170.327 386.317 C 176.279 365.934, 188.422 338.749, 200.942 317.778 C 223.060 280.731, 256.432 244.369, 294.500 215.836 C 309.956 204.252, 313.937 201.603, 314.719 202.385 C 315.116 202.783, 315.449 213.096, 315.460 225.304 C 315.474 241.855, 315.021 250.405, 313.680 258.924 C 307.009 301.272, 291.175 336.677, 263.112 372 C 255.259 381.883, 227.182 410.673, 218.516 417.727 L 213.532 421.783 223.439 424.880 C 281.705 443.093, 349.165 436.018, 398.616 406.508 C 446.728 377.797, 483.322 331.466, 497.366 281.481 C 503.381 260.075, 504.480 250.741, 504.491 221 C 504.501 191.997, 503.598 184.047, 497.987 163.732 C 484.768 115.871, 452.505 72.708, 407.718 42.964 C 381.051 25.254, 352.818 14.828, 319.695 10.460 C 305.932 8.645, 273.298 8.695, 259.500 10.552";
389
+ const WELCOME_CSS = `/* The welcome page is a full-bleed starter — reset the browser's default
390
+ body margin/padding so it sits flush; everything else is scoped under
391
+ .lunora-welcome (collision-safe). */
392
+ body {
393
+ margin: 0;
394
+ padding: 0;
395
+ }
396
+
397
+ .lunora-welcome {
398
+ --cyan: hsl(186 84% 56%); --violet: hsl(256 72% 68%); --rose: hsl(330 80% 64%);
399
+ --ribbon: linear-gradient(115deg, var(--cyan), var(--violet) 52%, var(--rose));
400
+ --sans: "Geist Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
401
+ --mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
402
+ /* NIGHT (default) */
403
+ --bg: #0e0e11; --surface: hsl(240 12% 8% / 0.72); --surface-2: hsl(240 11% 11% / 0.82);
404
+ --line: hsl(0 0% 100% / 0.08); --line-2: hsl(0 0% 100% / 0.14);
405
+ --t-display: hsl(228 30% 97%); --t-primary: hsl(228 26% 90%); --t-secondary: hsl(228 12% 62%); --t-faint: hsl(228 10% 44%);
406
+ --logo: hsl(228 30% 97%); --accent: var(--violet); --shot-bg: hsl(240 12% 6%);
407
+ --glow-1: hsl(256 80% 52% / 0.30); --glow-2: hsl(196 84% 52% / 0.13); --arc: hsl(256 60% 70% / 0.11);
408
+
409
+ position: relative; min-height: 100vh; background: var(--bg); color: var(--t-primary);
410
+ font-family: var(--sans); line-height: 1.55; -webkit-font-smoothing: antialiased; overflow-x: hidden;
411
+ transition: background .3s, color .3s;
412
+ }
413
+ .lunora-welcome[data-theme="light"] {
414
+ --bg: hsl(228 32% 97%); --surface: hsl(0 0% 100% / 0.82); --surface-2: hsl(0 0% 100% / 0.95);
415
+ --line: hsl(228 16% 88%); --line-2: hsl(228 14% 80%);
416
+ --t-display: hsl(240 14% 10%); --t-primary: hsl(240 12% 18%); --t-secondary: hsl(235 9% 42%); --t-faint: hsl(235 8% 58%);
417
+ --logo: hsl(240 16% 9%); --accent: hsl(256 58% 56%); --shot-bg: hsl(228 26% 99%);
418
+ --glow-1: hsl(256 80% 60% / 0.14); --glow-2: hsl(196 84% 58% / 0.08); --arc: hsl(256 40% 55% / 0.12);
419
+ }
420
+ .lunora-welcome *, .lunora-welcome *::before, .lunora-welcome *::after { box-sizing: border-box; }
421
+ .lunora-welcome a { color: inherit; text-decoration: none; }
422
+ .lunora-welcome button { font-family: inherit; cursor: pointer; }
423
+ .lunora-welcome ::selection { background: hsl(256 72% 68% / 0.3); }
424
+ .lunora-welcome code { font-family: var(--mono); }
425
+
426
+ /* glow background */
427
+ .lunora-welcome .lw-bg { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; }
428
+ .lunora-welcome .lw-bg .glow { position: absolute; left: 50%; top: -4%; width: 860px; height: 720px; transform: translateX(-50%);
429
+ border-radius: 50%; background: radial-gradient(circle at 50% 50%, var(--glow-1), var(--glow-2) 40%, transparent 66%); }
430
+ .lunora-welcome .lw-bg .arc { position: absolute; left: 50%; border-radius: 50%; border: 1px solid var(--arc); transform: translateX(-50%); }
431
+ .lunora-welcome .lw-bg .arc.a1 { top: -340px; width: 980px; height: 980px; }
432
+ .lunora-welcome .lw-bg .arc.a2 { top: -240px; width: 720px; height: 720px; opacity: .7; }
433
+
434
+ /* theme toggle */
435
+ .lunora-welcome .lw-toggle { position: fixed; z-index: 5; top: 20px; right: clamp(16px,4vw,36px); display: inline-flex; align-items: center;
436
+ gap: 7px; border: 1px solid var(--line-2); background: var(--surface); backdrop-filter: blur(12px); color: var(--t-secondary);
437
+ font-family: var(--mono); font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; padding: 7px 11px; transition: .15s; }
438
+ .lunora-welcome .lw-toggle:hover { color: var(--t-display); border-color: var(--accent); }
439
+ .lunora-welcome .lw-toggle svg { width: 13px; height: 13px; }
440
+
441
+ .lunora-welcome .lw-wrap { position: relative; z-index: 2; width: 100%; max-width: 1080px; margin: 0 auto;
442
+ padding: clamp(44px,8vh,92px) clamp(20px,5vw,48px); display: flex; flex-direction: column; min-height: 100vh; }
443
+ .lunora-welcome .brand { display: flex; align-items: center; justify-content: center; gap: 15px; margin-bottom: clamp(38px,6vh,68px); color: var(--logo); }
444
+ .lunora-welcome .brand svg { width: 54px; height: auto; display: block; }
445
+ .lunora-welcome .brand .word { font-size: 34px; font-weight: 600; letter-spacing: -0.03em; color: var(--t-display); }
446
+
447
+ .lunora-welcome .grid { display: grid; grid-template-columns: 1.06fr 0.94fr; gap: 16px; align-items: stretch; flex: 1; max-height: 580px; }
448
+ @media (max-width: 820px) { .lunora-welcome .grid { grid-template-columns: 1fr; max-height: none; } }
449
+
450
+ .lunora-welcome .card { border: 1px solid var(--line); background: var(--surface); backdrop-filter: blur(10px); transition: border-color .2s, background .2s; }
451
+ .lunora-welcome .card:hover { border-color: var(--line-2); background: var(--surface-2); }
452
+ .lunora-welcome .card:hover .arrow { color: var(--accent); transform: translateX(3px); }
453
+ .lunora-welcome .arrow { color: var(--t-faint); transition: color .2s, transform .2s; }
454
+ .lunora-welcome .arrow svg { width: 20px; height: 20px; display: block; }
455
+ .lunora-welcome .ic { display: grid; place-items: center; color: var(--accent);
456
+ border: 1px solid color-mix(in oklab, var(--accent) 36%, transparent); background: color-mix(in oklab, var(--accent) 12%, transparent); }
457
+ .lunora-welcome .ic svg { display: block; }
458
+
459
+ /* left feature card — stretches to fill the box height */
460
+ .lunora-welcome .feature { padding: clamp(18px,2vw,24px); display: flex; flex-direction: column; }
461
+ .lunora-welcome .shot { border: 1px solid var(--line); overflow: hidden; background: var(--shot-bg);
462
+ -webkit-mask-image: linear-gradient(to bottom, #000 56%, transparent 100%); mask-image: linear-gradient(to bottom, #000 56%, transparent 100%); }
463
+ .lunora-welcome .shot .top { display: flex; align-items: center; gap: 12px; padding: 11px 14px; border-bottom: 1px solid var(--line); }
464
+ .lunora-welcome .shot .wm { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 600; color: var(--t-primary); }
465
+ .lunora-welcome .shot .wm i { width: 11px; height: 11px; background: var(--ribbon); -webkit-mask: radial-gradient(circle,#000 60%,transparent 62%); mask: radial-gradient(circle,#000 60%,transparent 62%); }
466
+ .lunora-welcome .shot .search { flex: 1; height: 22px; border: 1px solid var(--line); border-radius: 4px; }
467
+ .lunora-welcome .shot .ver { font-family: var(--mono); font-size: 8px; letter-spacing: .08em; color: var(--t-faint); }
468
+ .lunora-welcome .shot .body { display: grid; grid-template-columns: 92px 1fr; min-height: 224px; }
469
+ .lunora-welcome .shot .nav { border-right: 1px solid var(--line); padding: 14px 12px; display: flex; flex-direction: column; gap: 11px; }
470
+ .lunora-welcome .shot .nav i, .lunora-welcome .shot .doc i { height: 5px; border-radius: 2px; background: var(--line); }
471
+ .lunora-welcome .shot .doc { padding: 16px 18px; display: flex; flex-direction: column; gap: 9px; }
472
+ .lunora-welcome .shot .doc .h { height: 8px; width: 46%; border-radius: 2px; background: var(--line-2); margin-bottom: 5px; }
473
+ .lunora-welcome .shot .doc .accent { height: 5px; width: 26%; border-radius: 2px; background: var(--accent); }
474
+ .lunora-welcome .feature .info { margin-top: auto; padding-top: clamp(18px,2.4vh,28px); }
475
+ .lunora-welcome .feature .ic { width: 40px; height: 40px; margin-bottom: 15px; }
476
+ .lunora-welcome .feature .ic svg { width: 19px; height: 19px; }
477
+ .lunora-welcome .feature h2 { margin: 0 0 10px; font-size: 19px; font-weight: 600; letter-spacing: -0.015em; color: var(--t-display); }
478
+ .lunora-welcome .feature .row { display: flex; align-items: flex-end; gap: 16px; }
479
+ .lunora-welcome .feature p { margin: 0; color: var(--t-secondary); font-size: 14px; max-width: 50ch; }
480
+ .lunora-welcome .feature .row .arrow { margin-left: auto; }
481
+
482
+ /* right stack — smaller cards, spread to align bottoms with the feature */
483
+ .lunora-welcome .stack { display: flex; flex-direction: column; gap: 16px; height: 100%; }
484
+ .lunora-welcome .mini { flex: 1; padding: 15px 17px; display: flex; align-items: center; gap: 16px; min-height: 0; }
485
+ .lunora-welcome .mini .mc { flex: 1; }
486
+ .lunora-welcome .mini .ic { width: 32px; height: 32px; margin-bottom: 10px; }
487
+ .lunora-welcome .mini .ic svg { width: 16px; height: 16px; }
488
+ .lunora-welcome .mini h3 { margin: 0 0 5px; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; color: var(--t-display); }
489
+ .lunora-welcome .mini p { margin: 0; color: var(--t-secondary); font-size: 12.5px; line-height: 1.45; }
490
+
491
+ .lunora-welcome .lw-foot { text-align: center; padding-top: 26px; font-family: var(--mono); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--t-faint); }
492
+
493
+ @media (prefers-reduced-motion: reduce) { .lunora-welcome * { transition: none !important; } }
494
+ `;
495
+ const REACT_APP = `import { useEffect, useState } from "react";
496
+
497
+ type Theme = "dark" | "light";
498
+
499
+ export default function App() {
500
+ // Default to "dark" for a stable first paint, then reconcile to the OS
501
+ // preference; the toggle takes over after that.
502
+ const [theme, setTheme] = useState<Theme>("dark");
503
+
504
+ useEffect(() => {
505
+ if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: light)").matches) {
506
+ setTheme("light");
507
+ }
508
+ }, []);
509
+
510
+ const isLight = theme === "light";
511
+
512
+ return (
513
+ <div className="lunora-welcome" data-theme={theme}>
514
+ <div className="lw-bg">
515
+ <div className="arc a1" />
516
+ <div className="arc a2" />
517
+ <div className="glow" />
518
+ </div>
519
+
520
+ <button className="lw-toggle" type="button" aria-label="Toggle color theme" onClick={() => setTheme(isLight ? "dark" : "light")}>
521
+ {isLight ? (
522
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
523
+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
524
+ </svg>
525
+ ) : (
526
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8">
527
+ <circle cx="12" cy="12" r="4" />
528
+ <path d="M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19" />
529
+ </svg>
530
+ )}
531
+ <span>{isLight ? "Ivory" : "Night"}</span>
532
+ </button>
533
+
534
+ <div className="lw-wrap">
535
+ <div className="brand">
536
+ <svg viewBox="0 0 543 446" role="img" aria-label="Lunora">
537
+ <path d="${LOGO_PATH}" fill="currentColor" fillRule="evenodd" />
538
+ </svg>
539
+ <span className="word">Lunora</span>
540
+ </div>
541
+
542
+ <div className="grid">
543
+ <a className="card feature" href="https://lunora.sh/docs">
544
+ <div className="shot" aria-hidden="true">
545
+ <div className="top">
546
+ <span className="wm">
547
+ <i /> Lunora
548
+ </span>
549
+ <span className="search" />
550
+ <span className="ver">v0.1</span>
551
+ </div>
552
+ <div className="body">
553
+ <div className="nav">
554
+ <i style={{ width: "80%" }} />
555
+ <i style={{ width: "60%" }} />
556
+ <i style={{ width: "72%" }} />
557
+ <i style={{ width: "50%" }} />
558
+ <i style={{ width: "66%" }} />
559
+ <i style={{ width: "44%" }} />
560
+ <i style={{ width: "58%" }} />
561
+ </div>
562
+ <div className="doc">
563
+ <span className="h" />
564
+ <i style={{ width: "92%" }} />
565
+ <i style={{ width: "88%" }} />
566
+ <span className="accent" />
567
+ <i style={{ width: "80%" }} />
568
+ <i style={{ width: "90%" }} />
569
+ <i style={{ width: "72%" }} />
570
+ <i style={{ width: "84%" }} />
571
+ <i style={{ width: "78%" }} />
572
+ </div>
573
+ </div>
574
+ </div>
575
+ <div className="info">
576
+ <span className="ic">
577
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7">
578
+ <path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
579
+ <path d="M14 3v5h5" />
580
+ </svg>
581
+ </span>
582
+ <h2>Documentation</h2>
583
+ <div className="row">
584
+ <p>
585
+ Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC,
586
+ you'll have a live app fast.
587
+ </p>
588
+ <span className="arrow">
589
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
590
+ <path d="M5 12h14M13 6l6 6-6 6" />
591
+ </svg>
592
+ </span>
593
+ </div>
594
+ </div>
595
+ </a>
596
+
597
+ <div className="stack">
598
+ <a className="card mini" href="https://lunora.sh/blog">
599
+ <div className="mc">
600
+ <span className="ic">
601
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7">
602
+ <path d="M5 4h11a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1zM8 8h7M8 12h7M8 16h4" />
603
+ </svg>
604
+ </span>
605
+ <h3>Blog</h3>
606
+ <p>Product updates, deep dives, and what's new in Lunora.</p>
607
+ </div>
608
+ <span className="arrow">
609
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
610
+ <path d="M5 12h14M13 6l6 6-6 6" />
611
+ </svg>
612
+ </span>
613
+ </a>
614
+ <a className="card mini" href="/_lunora">
615
+ <div className="mc">
616
+ <span className="ic">
617
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7">
618
+ <rect x="3" y="3" width="18" height="18" rx="1" />
619
+ <path d="M3 9h18M9 21V9" />
620
+ </svg>
621
+ </span>
622
+ <h3>Lunora Studio</h3>
623
+ <p>Local admin for schema, data, logs, and advisors.</p>
624
+ </div>
625
+ <span className="arrow">
626
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
627
+ <path d="M5 12h14M13 6l6 6-6 6" />
628
+ </svg>
629
+ </span>
630
+ </a>
631
+ <a className="card mini" href="https://lunora.sh/packages">
632
+ <div className="mc">
633
+ <span className="ic">
634
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7">
635
+ <path d="M12 2 3 7v10l9 5 9-5V7z" />
636
+ <path d="M3 7l9 5 9-5M12 12v10" />
637
+ </svg>
638
+ </span>
639
+ <h3>Cloudflare ecosystem</h3>
640
+ <p>Auth, mail, storage, AI, payments — one deploy.</p>
641
+ </div>
642
+ <span className="arrow">
643
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
644
+ <path d="M5 12h14M13 6l6 6-6 6" />
645
+ </svg>
646
+ </span>
647
+ </a>
648
+ </div>
649
+ </div>
650
+
651
+ <div className="lw-foot">Running on Lunora · Vite + React</div>
652
+ </div>
653
+ </div>
654
+ );
655
+ }
656
+ `;
657
+ const VUE_APP = `<script setup lang="ts">
658
+ import { onMounted, ref } from "vue";
659
+
660
+ // Default to "dark" for a stable first paint, then reconcile to the OS
661
+ // preference; the toggle takes over after that.
662
+ const theme = ref<"dark" | "light">("dark");
663
+
664
+ onMounted(() => {
665
+ if (window.matchMedia("(prefers-color-scheme: light)").matches) {
666
+ theme.value = "light";
667
+ }
668
+ });
669
+
670
+ const toggle = (): void => {
671
+ theme.value = theme.value === "light" ? "dark" : "light";
672
+ };
673
+ <\/script>
674
+
675
+ <template>
676
+ <div class="lunora-welcome" :data-theme="theme">
677
+ <div class="lw-bg">
678
+ <div class="arc a1" />
679
+ <div class="arc a2" />
680
+ <div class="glow" />
681
+ </div>
682
+
683
+ <button class="lw-toggle" type="button" aria-label="Toggle color theme" @click="toggle">
684
+ <svg v-if="theme === 'light'" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
685
+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
686
+ </svg>
687
+ <svg v-else viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
688
+ <circle cx="12" cy="12" r="4" />
689
+ <path d="M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19" />
690
+ </svg>
691
+ <span>{{ theme === "light" ? "Ivory" : "Night" }}</span>
692
+ </button>
693
+
694
+ <div class="lw-wrap">
695
+ <div class="brand">
696
+ <svg viewBox="0 0 543 446" role="img" aria-label="Lunora">
697
+ <path d="${LOGO_PATH}" fill="currentColor" fill-rule="evenodd" />
698
+ </svg>
699
+ <span class="word">Lunora</span>
700
+ </div>
701
+
702
+ <div class="grid">
703
+ <a class="card feature" href="https://lunora.sh/docs">
704
+ <div class="shot" aria-hidden="true">
705
+ <div class="top">
706
+ <span class="wm"><i /> Lunora</span>
707
+ <span class="search" />
708
+ <span class="ver">v0.1</span>
709
+ </div>
710
+ <div class="body">
711
+ <div class="nav">
712
+ <i style="width: 80%" />
713
+ <i style="width: 60%" />
714
+ <i style="width: 72%" />
715
+ <i style="width: 50%" />
716
+ <i style="width: 66%" />
717
+ <i style="width: 44%" />
718
+ <i style="width: 58%" />
719
+ </div>
720
+ <div class="doc">
721
+ <span class="h" />
722
+ <i style="width: 92%" />
723
+ <i style="width: 88%" />
724
+ <span class="accent" />
725
+ <i style="width: 80%" />
726
+ <i style="width: 90%" />
727
+ <i style="width: 72%" />
728
+ <i style="width: 84%" />
729
+ <i style="width: 78%" />
730
+ </div>
731
+ </div>
732
+ </div>
733
+ <div class="info">
734
+ <span class="ic">
735
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
736
+ <path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
737
+ <path d="M14 3v5h5" />
738
+ </svg>
739
+ </span>
740
+ <h2>Documentation</h2>
741
+ <div class="row">
742
+ <p>
743
+ Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC,
744
+ you'll have a live app fast.
745
+ </p>
746
+ <span class="arrow">
747
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
748
+ <path d="M5 12h14M13 6l6 6-6 6" />
749
+ </svg>
750
+ </span>
751
+ </div>
752
+ </div>
753
+ </a>
754
+
755
+ <div class="stack">
756
+ <a class="card mini" href="https://lunora.sh/blog">
757
+ <div class="mc">
758
+ <span class="ic">
759
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
760
+ <path d="M5 4h11a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1zM8 8h7M8 12h7M8 16h4" />
761
+ </svg>
762
+ </span>
763
+ <h3>Blog</h3>
764
+ <p>Product updates, deep dives, and what's new in Lunora.</p>
765
+ </div>
766
+ <span class="arrow">
767
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
768
+ <path d="M5 12h14M13 6l6 6-6 6" />
769
+ </svg>
770
+ </span>
771
+ </a>
772
+ <a class="card mini" href="/_lunora">
773
+ <div class="mc">
774
+ <span class="ic">
775
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
776
+ <rect x="3" y="3" width="18" height="18" rx="1" />
777
+ <path d="M3 9h18M9 21V9" />
778
+ </svg>
779
+ </span>
780
+ <h3>Lunora Studio</h3>
781
+ <p>Local admin for schema, data, logs, and advisors.</p>
782
+ </div>
783
+ <span class="arrow">
784
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
785
+ <path d="M5 12h14M13 6l6 6-6 6" />
786
+ </svg>
787
+ </span>
788
+ </a>
789
+ <a class="card mini" href="https://lunora.sh/packages">
790
+ <div class="mc">
791
+ <span class="ic">
792
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
793
+ <path d="M12 2 3 7v10l9 5 9-5V7z" />
794
+ <path d="M3 7l9 5 9-5M12 12v10" />
795
+ </svg>
796
+ </span>
797
+ <h3>Cloudflare ecosystem</h3>
798
+ <p>Auth, mail, storage, AI, payments — one deploy.</p>
799
+ </div>
800
+ <span class="arrow">
801
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
802
+ <path d="M5 12h14M13 6l6 6-6 6" />
803
+ </svg>
804
+ </span>
805
+ </a>
806
+ </div>
807
+ </div>
808
+
809
+ <div class="lw-foot">Running on Lunora · Vite + Vue</div>
810
+ </div>
811
+ </div>
812
+ </template>
813
+ `;
814
+ const SOLID_APP = `import { createSignal, onMount } from "solid-js";
815
+
816
+ export default function App() {
817
+ // Default to "dark" for a stable first paint, then reconcile to the OS
818
+ // preference; the toggle takes over after that.
819
+ const [theme, setTheme] = createSignal<"dark" | "light">("dark");
820
+
821
+ onMount(() => {
822
+ if (window.matchMedia("(prefers-color-scheme: light)").matches) {
823
+ setTheme("light");
824
+ }
825
+ });
826
+
827
+ const isLight = () => theme() === "light";
828
+
829
+ return (
830
+ <div class="lunora-welcome" data-theme={theme()}>
831
+ <div class="lw-bg">
832
+ <div class="arc a1" />
833
+ <div class="arc a2" />
834
+ <div class="glow" />
835
+ </div>
836
+
837
+ <button class="lw-toggle" type="button" aria-label="Toggle color theme" onClick={() => setTheme(isLight() ? "dark" : "light")}>
838
+ {isLight() ? (
839
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
840
+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
841
+ </svg>
842
+ ) : (
843
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
844
+ <circle cx="12" cy="12" r="4" />
845
+ <path d="M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19" />
846
+ </svg>
847
+ )}
848
+ <span>{isLight() ? "Ivory" : "Night"}</span>
849
+ </button>
850
+
851
+ <div class="lw-wrap">
852
+ <div class="brand">
853
+ <svg viewBox="0 0 543 446" role="img" aria-label="Lunora">
854
+ <path d="${LOGO_PATH}" fill="currentColor" fill-rule="evenodd" />
855
+ </svg>
856
+ <span class="word">Lunora</span>
857
+ </div>
858
+
859
+ <div class="grid">
860
+ <a class="card feature" href="https://lunora.sh/docs">
861
+ <div class="shot" aria-hidden="true">
862
+ <div class="top">
863
+ <span class="wm">
864
+ <i /> Lunora
865
+ </span>
866
+ <span class="search" />
867
+ <span class="ver">v0.1</span>
868
+ </div>
869
+ <div class="body">
870
+ <div class="nav">
871
+ <i style={{ width: "80%" }} />
872
+ <i style={{ width: "60%" }} />
873
+ <i style={{ width: "72%" }} />
874
+ <i style={{ width: "50%" }} />
875
+ <i style={{ width: "66%" }} />
876
+ <i style={{ width: "44%" }} />
877
+ <i style={{ width: "58%" }} />
878
+ </div>
879
+ <div class="doc">
880
+ <span class="h" />
881
+ <i style={{ width: "92%" }} />
882
+ <i style={{ width: "88%" }} />
883
+ <span class="accent" />
884
+ <i style={{ width: "80%" }} />
885
+ <i style={{ width: "90%" }} />
886
+ <i style={{ width: "72%" }} />
887
+ <i style={{ width: "84%" }} />
888
+ <i style={{ width: "78%" }} />
889
+ </div>
890
+ </div>
891
+ </div>
892
+ <div class="info">
893
+ <span class="ic">
894
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
895
+ <path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
896
+ <path d="M14 3v5h5" />
897
+ </svg>
898
+ </span>
899
+ <h2>Documentation</h2>
900
+ <div class="row">
901
+ <p>
902
+ Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC,
903
+ you'll have a live app fast.
904
+ </p>
905
+ <span class="arrow">
906
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
907
+ <path d="M5 12h14M13 6l6 6-6 6" />
908
+ </svg>
909
+ </span>
910
+ </div>
911
+ </div>
912
+ </a>
913
+
914
+ <div class="stack">
915
+ <a class="card mini" href="https://lunora.sh/blog">
916
+ <div class="mc">
917
+ <span class="ic">
918
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
919
+ <path d="M5 4h11a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1zM8 8h7M8 12h7M8 16h4" />
920
+ </svg>
921
+ </span>
922
+ <h3>Blog</h3>
923
+ <p>Product updates, deep dives, and what's new in Lunora.</p>
924
+ </div>
925
+ <span class="arrow">
926
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
927
+ <path d="M5 12h14M13 6l6 6-6 6" />
928
+ </svg>
929
+ </span>
930
+ </a>
931
+ <a class="card mini" href="/_lunora">
932
+ <div class="mc">
933
+ <span class="ic">
934
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
935
+ <rect x="3" y="3" width="18" height="18" rx="1" />
936
+ <path d="M3 9h18M9 21V9" />
937
+ </svg>
938
+ </span>
939
+ <h3>Lunora Studio</h3>
940
+ <p>Local admin for schema, data, logs, and advisors.</p>
941
+ </div>
942
+ <span class="arrow">
943
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
944
+ <path d="M5 12h14M13 6l6 6-6 6" />
945
+ </svg>
946
+ </span>
947
+ </a>
948
+ <a class="card mini" href="https://lunora.sh/packages">
949
+ <div class="mc">
950
+ <span class="ic">
951
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
952
+ <path d="M12 2 3 7v10l9 5 9-5V7z" />
953
+ <path d="M3 7l9 5 9-5M12 12v10" />
954
+ </svg>
955
+ </span>
956
+ <h3>Cloudflare ecosystem</h3>
957
+ <p>Auth, mail, storage, AI, payments — one deploy.</p>
958
+ </div>
959
+ <span class="arrow">
960
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
961
+ <path d="M5 12h14M13 6l6 6-6 6" />
962
+ </svg>
963
+ </span>
964
+ </a>
965
+ </div>
966
+ </div>
967
+
968
+ <div class="lw-foot">Running on Lunora · Vite + Solid</div>
969
+ </div>
970
+ </div>
971
+ );
972
+ }
973
+ `;
974
+ const SVELTE_APP = `<script lang="ts">
975
+ import { onMount } from "svelte";
976
+
977
+ // Default to "dark" for a stable first paint, then reconcile to the OS
978
+ // preference; the toggle takes over after that.
979
+ let theme = $state<"dark" | "light">("dark");
980
+ const isLight = $derived(theme === "light");
981
+
982
+ onMount(() => {
983
+ if (window.matchMedia("(prefers-color-scheme: light)").matches) {
984
+ theme = "light";
985
+ }
986
+ });
987
+
988
+ const toggle = (): void => {
989
+ theme = theme === "light" ? "dark" : "light";
990
+ };
991
+ <\/script>
992
+
993
+ <div class="lunora-welcome" data-theme={theme}>
994
+ <div class="lw-bg">
995
+ <div class="arc a1"></div>
996
+ <div class="arc a2"></div>
997
+ <div class="glow"></div>
998
+ </div>
999
+
1000
+ <button class="lw-toggle" type="button" aria-label="Toggle color theme" onclick={toggle}>
1001
+ {#if isLight}
1002
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
1003
+ <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />
1004
+ </svg>
1005
+ {:else}
1006
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
1007
+ <circle cx="12" cy="12" r="4" />
1008
+ <path d="M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19" />
1009
+ </svg>
1010
+ {/if}
1011
+ <span>{isLight ? "Ivory" : "Night"}</span>
1012
+ </button>
1013
+
1014
+ <div class="lw-wrap">
1015
+ <div class="brand">
1016
+ <svg viewBox="0 0 543 446" role="img" aria-label="Lunora">
1017
+ <path d="${LOGO_PATH}" fill="currentColor" fill-rule="evenodd" />
1018
+ </svg>
1019
+ <span class="word">Lunora</span>
1020
+ </div>
1021
+
1022
+ <div class="grid">
1023
+ <a class="card feature" href="https://lunora.sh/docs">
1024
+ <div class="shot" aria-hidden="true">
1025
+ <div class="top">
1026
+ <span class="wm"><i></i> Lunora</span>
1027
+ <span class="search"></span>
1028
+ <span class="ver">v0.1</span>
1029
+ </div>
1030
+ <div class="body">
1031
+ <div class="nav">
1032
+ <i style="width: 80%"></i>
1033
+ <i style="width: 60%"></i>
1034
+ <i style="width: 72%"></i>
1035
+ <i style="width: 50%"></i>
1036
+ <i style="width: 66%"></i>
1037
+ <i style="width: 44%"></i>
1038
+ <i style="width: 58%"></i>
1039
+ </div>
1040
+ <div class="doc">
1041
+ <span class="h"></span>
1042
+ <i style="width: 92%"></i>
1043
+ <i style="width: 88%"></i>
1044
+ <span class="accent"></span>
1045
+ <i style="width: 80%"></i>
1046
+ <i style="width: 90%"></i>
1047
+ <i style="width: 72%"></i>
1048
+ <i style="width: 84%"></i>
1049
+ <i style="width: 78%"></i>
1050
+ </div>
1051
+ </div>
1052
+ </div>
1053
+ <div class="info">
1054
+ <span class="ic">
1055
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
1056
+ <path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
1057
+ <path d="M14 3v5h5" />
1058
+ </svg>
1059
+ </span>
1060
+ <h2>Documentation</h2>
1061
+ <div class="row">
1062
+ <p>
1063
+ Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC,
1064
+ you'll have a live app fast.
1065
+ </p>
1066
+ <span class="arrow">
1067
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1068
+ <path d="M5 12h14M13 6l6 6-6 6" />
1069
+ </svg>
1070
+ </span>
1071
+ </div>
1072
+ </div>
1073
+ </a>
1074
+
1075
+ <div class="stack">
1076
+ <a class="card mini" href="https://lunora.sh/blog">
1077
+ <div class="mc">
1078
+ <span class="ic">
1079
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
1080
+ <path d="M5 4h11a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1zM8 8h7M8 12h7M8 16h4" />
1081
+ </svg>
1082
+ </span>
1083
+ <h3>Blog</h3>
1084
+ <p>Product updates, deep dives, and what's new in Lunora.</p>
1085
+ </div>
1086
+ <span class="arrow">
1087
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1088
+ <path d="M5 12h14M13 6l6 6-6 6" />
1089
+ </svg>
1090
+ </span>
1091
+ </a>
1092
+ <a class="card mini" href="/_lunora">
1093
+ <div class="mc">
1094
+ <span class="ic">
1095
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
1096
+ <rect x="3" y="3" width="18" height="18" rx="1" />
1097
+ <path d="M3 9h18M9 21V9" />
1098
+ </svg>
1099
+ </span>
1100
+ <h3>Lunora Studio</h3>
1101
+ <p>Local admin for schema, data, logs, and advisors.</p>
1102
+ </div>
1103
+ <span class="arrow">
1104
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1105
+ <path d="M5 12h14M13 6l6 6-6 6" />
1106
+ </svg>
1107
+ </span>
1108
+ </a>
1109
+ <a class="card mini" href="https://lunora.sh/packages">
1110
+ <div class="mc">
1111
+ <span class="ic">
1112
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
1113
+ <path d="M12 2 3 7v10l9 5 9-5V7z" />
1114
+ <path d="M3 7l9 5 9-5M12 12v10" />
1115
+ </svg>
1116
+ </span>
1117
+ <h3>Cloudflare ecosystem</h3>
1118
+ <p>Auth, mail, storage, AI, payments — one deploy.</p>
1119
+ </div>
1120
+ <span class="arrow">
1121
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1122
+ <path d="M5 12h14M13 6l6 6-6 6" />
1123
+ </svg>
1124
+ </span>
1125
+ </a>
1126
+ </div>
1127
+ </div>
1128
+
1129
+ <div class="lw-foot">Running on Lunora · Vite + Svelte</div>
1130
+ </div>
1131
+ </div>
1132
+ `;
1133
+ const VANILLA_MAIN = `import "./style.css";
1134
+
1135
+ import { LunoraClient } from "lunorash/client";
1136
+
1137
+ import { api } from "#lunora/_generated/api.js";
1138
+
1139
+ // \`@lunora/vite\` runs the Worker on the same origin as Vite, so default to
1140
+ // \`location.origin\`. Point \`VITE_LUNORA_URL\` at a deployed Worker to develop
1141
+ // the client against production data.
1142
+ const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;
1143
+ const client = new LunoraClient({ url });
1144
+
1145
+ const MOON_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" /></svg>';
1146
+ const SUN_ICON =
1147
+ '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="4" /><path d="M12 2v2M12 20v2M4 12H2M22 12h-2M5 5l1.5 1.5M17.5 17.5 19 19M19 5l-1.5 1.5M6.5 17.5 5 19" /></svg>';
1148
+
1149
+ const welcomeHtml = \`
1150
+ <div class="lw-bg">
1151
+ <div class="arc a1"></div>
1152
+ <div class="arc a2"></div>
1153
+ <div class="glow"></div>
1154
+ </div>
1155
+
1156
+ <button class="lw-toggle" type="button" aria-label="Toggle color theme"></button>
1157
+
1158
+ <div class="lw-wrap">
1159
+ <div class="brand">
1160
+ <svg viewBox="0 0 543 446" role="img" aria-label="Lunora">
1161
+ <path d="${LOGO_PATH}" fill="currentColor" fill-rule="evenodd" />
1162
+ </svg>
1163
+ <span class="word">Lunora</span>
1164
+ </div>
1165
+
1166
+ <div class="grid">
1167
+ <a class="card feature" href="https://lunora.sh/docs">
1168
+ <div class="shot" aria-hidden="true">
1169
+ <div class="top">
1170
+ <span class="wm"><i></i> Lunora</span>
1171
+ <span class="search"></span>
1172
+ <span class="ver">v0.1</span>
1173
+ </div>
1174
+ <div class="body">
1175
+ <div class="nav">
1176
+ <i style="width: 80%"></i>
1177
+ <i style="width: 60%"></i>
1178
+ <i style="width: 72%"></i>
1179
+ <i style="width: 50%"></i>
1180
+ <i style="width: 66%"></i>
1181
+ <i style="width: 44%"></i>
1182
+ <i style="width: 58%"></i>
1183
+ </div>
1184
+ <div class="doc">
1185
+ <span class="h"></span>
1186
+ <i style="width: 92%"></i>
1187
+ <i style="width: 88%"></i>
1188
+ <span class="accent"></span>
1189
+ <i style="width: 80%"></i>
1190
+ <i style="width: 90%"></i>
1191
+ <i style="width: 72%"></i>
1192
+ <i style="width: 84%"></i>
1193
+ <i style="width: 78%"></i>
1194
+ </div>
1195
+ </div>
1196
+ </div>
1197
+ <div class="info">
1198
+ <span class="ic">
1199
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
1200
+ <path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
1201
+ <path d="M14 3v5h5" />
1202
+ </svg>
1203
+ </span>
1204
+ <h2>Documentation</h2>
1205
+ <div class="row">
1206
+ <p>
1207
+ Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC,
1208
+ you'll have a live app fast.
1209
+ </p>
1210
+ <span class="arrow">
1211
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1212
+ <path d="M5 12h14M13 6l6 6-6 6" />
1213
+ </svg>
1214
+ </span>
1215
+ </div>
1216
+ </div>
1217
+ </a>
1218
+
1219
+ <div class="stack">
1220
+ <a class="card mini" href="https://lunora.sh/blog">
1221
+ <div class="mc">
1222
+ <span class="ic">
1223
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
1224
+ <path d="M5 4h11a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1zM8 8h7M8 12h7M8 16h4" />
1225
+ </svg>
1226
+ </span>
1227
+ <h3>Blog</h3>
1228
+ <p>Product updates, deep dives, and what's new in Lunora.</p>
1229
+ </div>
1230
+ <span class="arrow">
1231
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1232
+ <path d="M5 12h14M13 6l6 6-6 6" />
1233
+ </svg>
1234
+ </span>
1235
+ </a>
1236
+ <a class="card mini" href="/_lunora">
1237
+ <div class="mc">
1238
+ <span class="ic">
1239
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
1240
+ <rect x="3" y="3" width="18" height="18" rx="1" />
1241
+ <path d="M3 9h18M9 21V9" />
1242
+ </svg>
1243
+ </span>
1244
+ <h3>Lunora Studio</h3>
1245
+ <p>Local admin for schema, data, logs, and advisors.</p>
1246
+ </div>
1247
+ <span class="arrow">
1248
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1249
+ <path d="M5 12h14M13 6l6 6-6 6" />
1250
+ </svg>
1251
+ </span>
1252
+ </a>
1253
+ <a class="card mini" href="https://lunora.sh/packages">
1254
+ <div class="mc">
1255
+ <span class="ic">
1256
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
1257
+ <path d="M12 2 3 7v10l9 5 9-5V7z" />
1258
+ <path d="M3 7l9 5 9-5M12 12v10" />
1259
+ </svg>
1260
+ </span>
1261
+ <h3>Cloudflare ecosystem</h3>
1262
+ <p>Auth, mail, storage, AI, payments — one deploy.</p>
1263
+ </div>
1264
+ <span class="arrow">
1265
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1266
+ <path d="M5 12h14M13 6l6 6-6 6" />
1267
+ </svg>
1268
+ </span>
1269
+ </a>
1270
+ </div>
1271
+ </div>
1272
+
1273
+ <div class="lw-foot">Running on Lunora · Vite + Vanilla · <span id="lw-count">0</span> messages</div>
1274
+ </div>
1275
+ \`;
1276
+
1277
+ const root = document.querySelector<HTMLDivElement>("#app")!;
1278
+ root.classList.add("lunora-welcome");
1279
+ root.innerHTML = welcomeHtml;
1280
+
1281
+ // Theme toggle: flip the root's data-theme + swap the button's icon/label.
1282
+ const toggleButton = root.querySelector<HTMLButtonElement>(".lw-toggle")!;
1283
+
1284
+ const paintToggle = (theme: "dark" | "light"): void => {
1285
+ root.dataset.theme = theme;
1286
+ toggleButton.innerHTML = \`\${theme === "light" ? MOON_ICON : SUN_ICON}<span>\${theme === "light" ? "Ivory" : "Night"}</span>\`;
1287
+ };
1288
+
1289
+ paintToggle(window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
1290
+ toggleButton.addEventListener("click", () => {
1291
+ paintToggle(root.dataset.theme === "light" ? "dark" : "light");
1292
+ });
1293
+
1294
+ // Live demo: the message count of a demo channel re-renders on every delta.
1295
+ const count = document.querySelector<HTMLSpanElement>("#lw-count")!;
1296
+
1297
+ client.onUpdate(api.messages.list, { channelId: "channel:demo" }, (result) => {
1298
+ count.textContent = String(result.messages.length);
1299
+ });
1300
+ `;
1301
+
388
1302
  const READ_URL = `const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;`;
389
1303
  const REACT_MAIN = `import "./index.css";
390
1304
 
@@ -471,47 +1385,57 @@ const app = mount(Root, { target: document.getElementById("app")! });
471
1385
 
472
1386
  export default app;
473
1387
  `;
474
- const VANILLA_MAIN = `import "./style.css";
475
-
476
- import { LunoraClient } from "lunorash/client";
477
-
478
- import { api } from "../lunora/_generated/api";
479
-
480
- // Vanilla starter: no framework provider — talk to Lunora through the client
481
- // directly. \`@lunora/vite\` runs the Worker on the same origin as Vite.
482
- ${READ_URL}
483
- const client = new LunoraClient({ url });
484
-
485
- const root = document.querySelector<HTMLDivElement>("#app")!;
486
-
487
- const heading = document.createElement("h1");
488
- heading.textContent = "Vite + Lunora";
489
-
490
- const output = document.createElement("pre");
491
- root.replaceChildren(heading, output);
492
-
493
- const render = (messages: unknown): void => {
494
- // textContent (not innerHTML) — never inject server data as markup.
495
- output.textContent = JSON.stringify(messages, null, 2);
496
- };
497
-
498
- // Live subscription: the list re-renders on every server delta.
499
- client.onUpdate(api.messages.list, { channelId: "channel:demo" }, render);
500
- `;
501
1388
  const ADAPTERS = {
502
- react: { adapter: "@lunora/react", createViteTemplate: "react-ts", files: [{ contents: REACT_MAIN, path: "src/main.tsx" }], label: "React" },
503
- solid: { adapter: "@lunora/solid", createViteTemplate: "solid", files: [{ contents: SOLID_INDEX, path: "src/index.tsx" }], label: "Solid" },
1389
+ react: {
1390
+ adapter: "@lunora/react",
1391
+ createViteTemplate: "react-ts",
1392
+ files: [
1393
+ { contents: REACT_MAIN, path: "src/main.tsx" },
1394
+ { contents: REACT_APP, path: "src/App.tsx" },
1395
+ { contents: WELCOME_CSS, path: "src/index.css" }
1396
+ ],
1397
+ label: "React"
1398
+ },
1399
+ solid: {
1400
+ adapter: "@lunora/solid",
1401
+ createViteTemplate: "solid",
1402
+ files: [
1403
+ { contents: SOLID_INDEX, path: "src/index.tsx" },
1404
+ { contents: SOLID_APP, path: "src/App.tsx" },
1405
+ { contents: WELCOME_CSS, path: "src/index.css" }
1406
+ ],
1407
+ label: "Solid"
1408
+ },
504
1409
  svelte: {
505
1410
  adapter: "@lunora/svelte",
506
1411
  createViteTemplate: "svelte-ts",
507
1412
  files: [
508
1413
  { contents: SVELTE_ROOT, path: "src/Root.svelte" },
509
- { contents: SVELTE_MAIN, path: "src/main.ts" }
1414
+ { contents: SVELTE_MAIN, path: "src/main.ts" },
1415
+ { contents: SVELTE_APP, path: "src/App.svelte" },
1416
+ { contents: WELCOME_CSS, path: "src/app.css" }
510
1417
  ],
511
1418
  label: "Svelte"
512
1419
  },
513
- vanilla: { adapter: "lunorash/client", createViteTemplate: "vanilla-ts", files: [{ contents: VANILLA_MAIN, path: "src/main.ts" }], label: "Vanilla" },
514
- vue: { adapter: "@lunora/vue", createViteTemplate: "vue-ts", files: [{ contents: VUE_MAIN, path: "src/main.ts" }], label: "Vue" }
1420
+ vanilla: {
1421
+ adapter: "lunorash/client",
1422
+ createViteTemplate: "vanilla-ts",
1423
+ files: [
1424
+ { contents: VANILLA_MAIN, path: "src/main.ts" },
1425
+ { contents: WELCOME_CSS, path: "src/style.css" }
1426
+ ],
1427
+ label: "Vanilla"
1428
+ },
1429
+ vue: {
1430
+ adapter: "@lunora/vue",
1431
+ createViteTemplate: "vue-ts",
1432
+ files: [
1433
+ { contents: VUE_MAIN, path: "src/main.ts" },
1434
+ { contents: VUE_APP, path: "src/App.vue" },
1435
+ { contents: WELCOME_CSS, path: "src/style.css" }
1436
+ ],
1437
+ label: "Vue"
1438
+ }
515
1439
  };
516
1440
  const isOverlayFramework = (value) => Object.hasOwn(ADAPTERS, value);
517
1441
 
@@ -526,15 +1450,39 @@ export default defineSchema({
526
1450
  .index("by_channel", ["channelId"]),
527
1451
  });
528
1452
  `;
529
- const LUNORA_MESSAGES = `import { mutation, query, v } from "./_generated/server.js";
1453
+ const LUNORA_MESSAGES = `import { RateLimiter, rateLimit } from "@lunora/ratelimit";
1454
+
1455
+ import { mutation, query, v } from "#lunora/_generated/server.js";
530
1456
 
531
- export const list = query.input({ channelId: v.string(), limit: v.optional(v.number()) }).query(async ({ args }) => {
532
- return { channelId: args.channelId, limit: args.limit ?? 50, messages: [] };
1457
+ /**
1458
+ * One in-memory limiter so the public \`send\` mutation isn't an open flood target
1459
+ * out of the box. The default store is in-memory (per-isolate, resets on
1460
+ * eviction) — fine for a starter; run \`lunora add ratelimit\` for the durable,
1461
+ * \`ctx.db\`-backed store when you ship to production.
1462
+ */
1463
+ const limiter = new RateLimiter({
1464
+ config: {
1465
+ send: { kind: "token bucket", period: 60_000, rate: 30 },
1466
+ },
533
1467
  });
534
1468
 
535
- export const send = mutation.input({ channelId: v.string(), text: v.string() }).mutation(async ({ args }) => {
536
- return { channelId: args.channelId, text: args.text };
1469
+ export const list = query.input({ channelId: v.string().meta({ schema: { maxLength: 256 } }), limit: v.optional(v.number()) }).query(async ({ args, ctx }) => {
1470
+ const messages = await ctx.db
1471
+ .query("messages")
1472
+ .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
1473
+ .take(args.limit ?? 50);
1474
+
1475
+ return { channelId: args.channelId, messages };
537
1476
  });
1477
+
1478
+ export const send = mutation
1479
+ .input({ channelId: v.string().meta({ schema: { maxLength: 256 } }), text: v.string().meta({ schema: { maxLength: 4096 } }) })
1480
+ .use(rateLimit(limiter, "send", { key: (ctx) => ctx.auth.userId ?? "anon" }))
1481
+ .mutation(async ({ args, ctx }) => {
1482
+ const id = await ctx.db.insert("messages", { channelId: args.channelId, text: args.text });
1483
+
1484
+ return { channelId: args.channelId, id, text: args.text };
1485
+ });
538
1486
  `;
539
1487
  const SERVER_ENTRY = `import type { ShardNamespaceLike } from "lunorash/runtime";
540
1488
 
@@ -605,6 +1553,7 @@ const patchPackageJson = async (target, name, adapter, distTag) => {
605
1553
  const path = join$1(target, "package.json");
606
1554
  const parsed = JSON.parse(readFileSync(path, "utf8"));
607
1555
  let dependencies = withDependency(parsed.dependencies ?? {}, "lunorash", distTag, distTag);
1556
+ dependencies = withDependency(dependencies, "@lunora/ratelimit", distTag, distTag);
608
1557
  if (adapter.adapter.startsWith("@lunora/")) {
609
1558
  dependencies = withDependency(dependencies, adapter.adapter, distTag, distTag);
610
1559
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/cli",
3
- "version": "1.0.0-alpha.20",
3
+ "version": "1.0.0-alpha.22",
4
4
  "description": "The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands",
5
5
  "keywords": [
6
6
  "agent-skills",
@@ -53,7 +53,7 @@
53
53
  "dependencies": {
54
54
  "@bomb.sh/tab": "0.0.16",
55
55
  "@lunora/codegen": "1.0.0-alpha.7",
56
- "@lunora/config": "1.0.0-alpha.11",
56
+ "@lunora/config": "1.0.0-alpha.12",
57
57
  "@lunora/container": "1.0.0-alpha.1",
58
58
  "@lunora/d1": "1.0.0-alpha.4",
59
59
  "@lunora/seed": "1.0.0-alpha.2",