UNPKG

@lunora/cli

Version:

The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands

733 lines (680 loc) 92 kB
import{existsSync as m,mkdirSync as M,writeFileSync as v,readFileSync as b,readdirSync as re,rmSync as $,mkdtempSync as oe,cpSync as G,renameSync as Re,lstatSync as Oe}from"node:fs";import{tmpdir as se}from"node:os";import{detectFramework as Te,isInteractive as g,LUNA_ART as Pe,LUNA_NAME as De,LUNA_SIGNOFF as Be,BADGES as f,paintAnswer as je,detectLintTools as Ve,applyLintIgnores as Ue}from"@lunora/config";import{walkSync as Fe}from"@visulima/fs";import{join as u,dirname as A,basename as E,resolve as ne,relative as He}from"@visulima/path";import{downloadTemplate as ie}from"giget";import{modify as We,applyEdits as ze}from"jsonc-parser";import{join as V,dirname as qe}from"node:path";import{i as le,e as Ke,d as U,c as Ge,b as Je,a as Ye}from"../packem_shared/detect-package-manager-DSaAqRof.mjs";import{d as Xe}from"../packem_shared/command-DwC11Cwe.mjs";import Qe from"magic-string";import{Project as Ze,SyntaxKind as w}from"ts-morph";import{P as F}from"../packem_shared/prompt-cancelled-C8Sj7OqI.mjs";import{h as ce,d as de,i as ue,b as et}from"../packem_shared/resolve-DZAGzg67.mjs";import{defaultSpawner as pe}from"../packem_shared/createRecordingSpawner-DJlNacZ6.mjs";import{d as tt,e as at,f as rt,a as he,t as H,g as me,h as ot,b as fe,i as ve,w as st,j as nt,k as ge,l as it}from"../packem_shared/tui-prompts-Dd0pDuNr.mjs";import{E as we,p as lt,w as ct,M as dt,r as ut,k as pt,f as ht,c as ye,l as be,e as mt,g as ft}from"../packem_shared/lint-ignore-report-0sVOnXcO.mjs";import{logStep as J}from"../packem_shared/createLogger-hGafwf-3.mjs";import vt from"node:dns/promises";import{r as gt}from"../packem_shared/commands-B-YnEi2t.mjs";const W={bun:{githubCacheKey:void 0,githubSetupStep:` - uses: oven-sh/setup-bun@v2 `,gitlabImage:"oven/bun:1",gitlabProvisionScript:[],installCmd:"bun install --frozen-lockfile",lockfile:"bun.lockb"},npm:{githubCacheKey:"npm",githubSetupStep:"",gitlabImage:"node:22",gitlabProvisionScript:[],installCmd:"npm ci",lockfile:"package-lock.json"},pnpm:{githubCacheKey:"pnpm",githubSetupStep:` - uses: pnpm/action-setup@v4 `,gitlabImage:"node:22",gitlabProvisionScript:["corepack enable"],installCmd:"pnpm install --frozen-lockfile",lockfile:"pnpm-lock.yaml"},yarn:{githubCacheKey:"yarn",githubSetupStep:"",gitlabImage:"node:22",gitlabProvisionScript:["corepack enable"],installCmd:"yarn install --frozen-lockfile",lockfile:"yarn.lock"}},C=(e,t)=>{const a=Ke(e,"lunora",t);return[a.command,...a.args].join(" ")},Y=e=>` - uses: actions/checkout@v4 ${e.githubSetupStep} - uses: actions/setup-node@v4 with: node-version: 22 ${e.githubCacheKey===void 0?"":` cache: ${e.githubCacheKey} `} - run: ${e.installCmd}`,wt=e=>{const t=W[e],a=t.githubCacheKey===void 0?`# (below) requires it — run \`${e} install\` locally and`:`# (below) and the ${e} cache both require it — run \`${e} install\` locally and`;return`name: Deploy on: push: branches: [main] pull_request: workflow_dispatch: # Prerequisite: commit your ${t.lockfile}. \`${t.installCmd}\` ${a} # commit the lockfile before pushing, or the first CI run fails. # # Set these repository secrets (Settings → Secrets and variables → Actions): # CLOUDFLARE_API_TOKEN — a Workers-scoped API token # CLOUDFLARE_ACCOUNT_ID — your Cloudflare account id env: CLOUDFLARE_API_TOKEN: \${{ secrets.CLOUDFLARE_API_TOKEN }} CLOUDFLARE_ACCOUNT_ID: \${{ secrets.CLOUDFLARE_ACCOUNT_ID }} jobs: # Production deploy on push to the default branch. deploy: if: github.event_name != 'pull_request' runs-on: ubuntu-latest permissions: contents: read steps: ${Y(t)} # Codegen + wrangler validation gate (no deploy) — fails fast on drift. - run: ${C(e,["prepare"])} - run: ${C(e,["deploy"])} # Preview version on every pull request — uploads a versioned preview URL; # production traffic is untouched. preview: if: github.event_name == 'pull_request' runs-on: ubuntu-latest permissions: contents: read steps: ${Y(t)} - run: ${C(e,["deploy","--preview"])} `},yt=e=>{const t=W[e],a=[...t.gitlabProvisionScript,t.installCmd].map(r=>` - ${r}`).join(` `);return`stages: - deploy # Prerequisite: commit your ${t.lockfile}. \`${t.installCmd}\` # (below) requires it — run \`${e} install\` locally and commit the lockfile # before pushing, or the first pipeline fails. # # Set these as masked CI/CD variables (Settings → CI/CD → Variables): # CLOUDFLARE_API_TOKEN — a Workers-scoped API token # CLOUDFLARE_ACCOUNT_ID — your Cloudflare account id .lunora_base: image: ${t.gitlabImage} stage: deploy before_script: ${a} # Production deploy on the default branch. deploy: extends: .lunora_base rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: # Codegen + wrangler validation gate (no deploy) — fails fast on drift. - ${C(e,["prepare"])} - ${C(e,["deploy"])} # Preview version on every merge request (versioned preview URL; production # traffic is untouched). preview: extends: .lunora_base rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" script: - ${C(e,["deploy","--preview"])} `},xe={github:{file:V(".github","workflows","deploy.yml"),secretsHint:"repository secrets (Settings → Secrets and variables → Actions)"},gitlab:{file:".gitlab-ci.yml",secretsHint:"masked CI/CD variables (Settings → CI/CD → Variables)"}},bt=(e,t)=>e==="github"?wt(t):yt(t),xt=e=>e==="github"||e==="gitlab",kt=(e,t,a,r={})=>{const o=V(e,xe[t].file);return m(o)&&r.overwrite!==!0?{path:o,skipped:!0,written:!1}:(M(qe(o),{recursive:!0}),v(o,bt(t,a),"utf8"),{path:o,skipped:!1,written:!0})},Ct=(e,t,a,r,o={})=>{const s=xe[t];try{const n=kt(e,t,a,o);if(n.skipped)r.info(`--ci ${t}: ${s.file} already exists — left unchanged (re-run with overwrite to replace).`);else{const i=le(a),l=[i.command,...i.args].join(" "),{installCmd:d,lockfile:c}=W[a];r.success(`--ci ${t}: wrote ${s.file}`),r.info(`--ci ${t}: set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as ${s.secretsHint} to enable deploys.`),r.info(`--ci ${t}: run \`${l}\` and commit ${c} before pushing — the pipeline runs \`${d}\`.`)}return n}catch(n){const i=n instanceof Error?n.message:String(n);return r.warn(`--ci ${t}: could not write ${s.file} (${i})`),{path:V(e,s.file),skipped:!1,written:!1}}},Lt={astro:"@lunora/react",none:"@lunora/react",nuxt:"@lunora/vue","react-router":"@lunora/react","solid-start":"@lunora/solid",sveltekit:"@lunora/svelte","tanstack-start":"@lunora/react","tanstack-start-solid":"@lunora/solid",vinext:"@lunora/react"},St=e=>{const t=Te(e);return{...t,adapter:Lt[t.framework]}},_="lunora()",X='import { lunora } from "@lunora/vite";',Mt=/\blunora\s*\(/u,At='"@lunora/vite"',Et="'@lunora/vite'",Nt=e=>{for(const t of e.getStatements()){if(t.getKind()!==w.ExportAssignment)continue;const a=t.asKindOrThrow(w.ExportAssignment).getExpression();if(a.getKind()!==w.CallExpression)continue;const r=a.asKindOrThrow(w.CallExpression);if(r.getExpression().getText()!=="defineConfig")continue;const o=r.getArguments()[0];if(o?.getKind()===w.ObjectLiteralExpression)return o.asKindOrThrow(w.ObjectLiteralExpression)}},It=e=>{for(const t of e.getStatements()){if(t.getKind()!==w.ExportAssignment)continue;const a=t.asKindOrThrow(w.ExportAssignment).getExpression();if(a.getKind()===w.ObjectLiteralExpression)return a.asKindOrThrow(w.ObjectLiteralExpression)}},_t=e=>new Ze({compilerOptions:{allowJs:!0},useInMemoryFileSystem:!0}).createSourceFile("vite.config.ts",e,{overwrite:!0}),$t=(e,t)=>{const a=t.getImportDeclarations().at(-1)?.getEnd()??0;a===0?e.prepend(`${X} `):e.appendLeft(a,` ${X}`)},Rt=(e,t)=>{const a=t.getProperty("plugins");if(a===void 0){const s=t.getProperties(),n=t.getStart()+1;if(s.length===0)e.appendLeft(n,` plugins: [${_}] `);else{const i=s[0];i!==void 0&&e.appendLeft(i.getStart(),`plugins: [${_}], `)}return}const r=a.getDescendantsOfKind(w.ArrayLiteralExpression)[0];if(r===void 0)return;const o=r.getElements();if(o.length===0)e.appendLeft(r.getStart()+1,_);else{const s=o[0];s!==void 0&&e.appendLeft(s.getStart(),`${_}, `)}},ke=e=>{if(Mt.test(e))return{changed:!1,code:e,reason:"lunora plugin already present"};const t=_t(e),a=Nt(t)??It(t);if(a===void 0)return{changed:!1,code:e,reason:"could not locate a Vite config plugins array to patch"};const r=new Qe(e);return!e.includes(At)&&!e.includes(Et)&&$t(r,t),Rt(r,a),{changed:!0,code:r.toString()}},Ot=/enotfound|eai_again|econnrefused|etimedout|network|fetch failed|getaddrinfo/u,Tt=/404|not found|could not find|no such/u,Pt=(e,t)=>{const a=e instanceof Error?e.message:String(e),r=a.toLowerCase(),o=`failed to download template "${t.templateType}" from ${t.remote}: ${a}`;return Tt.test(r)?{hints:[`Check the template name "${t.templateType}" and the ref "${t.ref}".`,"List/inspect available templates, or target a branch/tag with `--ref <branch>`."],message:`template "${t.templateType}" not found at ${t.remote}: ${a}`}:Ot.test(r)?{hints:["You appear to be offline or unable to reach GitHub.","To scaffold without a network, point at a local template root: `lunora init --from <dir>`."],message:o}:{hints:["If this is a network/offline issue, scaffold from a local root with `lunora init --from <dir>`."],message:o}},R=async(e,t,a)=>{if(g()){await at(f[e],t,a);return}if(process.stdout.write(` `),a===void 0||a===""){J(e,t);return}const r=a.split(` `).map(o=>je(o)).join(` `);J(e,`${t} ${r}`)},Dt=async e=>{if(g()){await tt();return}e.info(` ${Pe} ${De}: ${Be}`)},Ce=[{description:"LLMs via Workers AI (summarize, generate, stream)",label:"AI",value:"ai"},{description:"Sign-up / sign-in (asks which provider)",label:"Authentication",value:"auth"},{description:"Copy-in auth screens for your framework (sign in/up, reset, 2FA)",label:"Auth UI",value:"auth-ui"},{description:"Snapshot + restore your Durable Object data",label:"Backups",value:"backup"},{description:"Headless browser screenshots + PDFs",label:"Browser rendering",value:"browser"},{description:"Zero Trust identity via Cloudflare Access",label:"Cloudflare Access",value:"cloudflare-access"},{description:"Scheduled jobs via Cron Triggers (@lunora/scheduler)",label:"Cron jobs",value:"crons"},{description:"Cloudflare Email Workers + a dev mail catcher",label:"Transactional email",value:"email"},{description:"OpenFeature feature flags (ctx.flags)",label:"Feature flags",value:"flags"},{description:"External Postgres/MySQL via Hyperdrive",label:"Hyperdrive",value:"hyperdrive"},{description:"Stripe-first payments (checkout, subscription, webhooks)",label:"Payments",value:"payment"},{description:"Live presence / who's-online over hibernated WebSockets",label:"Presence",value:"presence"},{description:"Async message queues (push/pull consumers)",label:"Queues",value:"queue"},{description:"Typed R2 buckets + signed URLs (@lunora/storage)",label:"File storage",value:"storage"},{description:"Durable long-running workflows (step.do, sleep, branch)",label:"Workflows",value:"workflow"}],Q=Ce.map(e=>e.value),Bt=e=>e==="email"?we:e,jt=(e,t)=>{const a=[];for(const r of e.split(",").map(o=>o.trim()).filter(Boolean))Q.includes(r)?a.includes(r)||a.push(r):t(`init: unknown --add feature "${r}" — expected ${Q.join(" | ")}; skipping.`);return a},Vt=async e=>{const t=await ht(e.select),a=await ye(e.text,e.projectName);return{label:"auth",names:[t],transformManifest:r=>be(r,a)}},Ut=async e=>{const t=await e.text(dt,{placeholder:"you@yourdomain.com"}),a=ut(t,r=>{e.logger.warn(r)});return{label:"email",names:[we],transformManifest:a===void 0?void 0:r=>pt(r,a)}},Ft=async(e,t)=>{const a=e.resolveAuthUiItem?.()??"auth-ui-react";if(t)return{label:"auth-ui",names:[a]};const r=await ye(e.text,e.projectName);return{label:"auth-ui",names:[a],transformManifest:o=>be(o,r)}},Ht=async e=>{const t=await lt(e.text,e.projectName);return{label:"storage",names:["storage"],transformManifest:a=>ct(a,t)}},Wt={auth:Vt,"auth-ui":async(e,t)=>Ft(e,t.includes("auth")),email:Ut,storage:Ht},Z=async e=>{if(e.preselected!==void 0&&e.preselected.length>0){await e.applyAll(e.preselected.map(r=>{const o=r==="auth-ui"?e.resolveAuthUiItem?.()??"auth-ui-react":Bt(r);return{label:r,names:[o]}}));return}if(!e.interactive){e.logger.info("tip: add features later with `lunora add <ai|auth|backup|browser|cloudflare-access|crons|email|flags|hyperdrive|payment|presence|queue|storage|workflow>`.");return}const t=await e.multiSelect("Which features do you want to add?",Ce,{defaults:[]});if(t.length===0)return;const a=[];for(const r of t){const o=Wt[r];a.push(o?await o(e,t):{label:r,names:[r]})}await e.applyAll(a)},zt=[{description:"Adds an ignores entry to eslint.config.js",label:"ESLint",value:"eslint"},{description:"Writes .prettierignore",label:"Prettier",value:"prettier"},{description:"Adds negated patterns to biome.json",label:"Biome",value:"biome"},{description:"Writes ignorePatterns to .oxlintrc.json",label:"oxlint (oxc)",value:"oxlint"}],qt="Which linter/formatter do you use? We'll tell it to skip Lunora's generated files.",Kt=async e=>{const t=e.interactive?await e.multiSelect(qt,zt,{defaults:e.detected}):[...e.detected];if(t.length===0)return[];const a=e.apply(t);return mt(a,e.logger),a},N="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",S=`/* The welcome page is a full-bleed starter — reset the browser's default body margin/padding so it sits flush; everything else is scoped under .lunora-welcome (collision-safe). */ body { margin: 0; padding: 0; } .lunora-welcome { --cyan: hsl(186 84% 56%); --violet: hsl(256 72% 68%); --rose: hsl(330 80% 64%); --ribbon: linear-gradient(115deg, var(--cyan), var(--violet) 52%, var(--rose)); --sans: "Geist Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; --mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace; /* NIGHT (default) */ --bg: #0e0e11; --surface: hsl(240 12% 8% / 0.72); --surface-2: hsl(240 11% 11% / 0.82); --line: hsl(0 0% 100% / 0.08); --line-2: hsl(0 0% 100% / 0.14); --t-display: hsl(228 30% 97%); --t-primary: hsl(228 26% 90%); --t-secondary: hsl(228 12% 62%); --t-faint: hsl(228 10% 44%); --logo: hsl(228 30% 97%); --accent: var(--violet); --shot-bg: hsl(240 12% 6%); --glow-1: hsl(256 80% 52% / 0.30); --glow-2: hsl(196 84% 52% / 0.13); --arc: hsl(256 60% 70% / 0.11); position: relative; min-height: 100vh; background: var(--bg); color: var(--t-primary); font-family: var(--sans); line-height: 1.55; -webkit-font-smoothing: antialiased; overflow-x: hidden; transition: background .3s, color .3s; } .lunora-welcome[data-theme="light"] { --bg: hsl(228 32% 97%); --surface: hsl(0 0% 100% / 0.82); --surface-2: hsl(0 0% 100% / 0.95); --line: hsl(228 16% 88%); --line-2: hsl(228 14% 80%); --t-display: hsl(240 14% 10%); --t-primary: hsl(240 12% 18%); --t-secondary: hsl(235 9% 42%); --t-faint: hsl(235 8% 58%); --logo: hsl(240 16% 9%); --accent: hsl(256 58% 56%); --shot-bg: hsl(228 26% 99%); --glow-1: hsl(256 80% 60% / 0.14); --glow-2: hsl(196 84% 58% / 0.08); --arc: hsl(256 40% 55% / 0.12); } .lunora-welcome *, .lunora-welcome *::before, .lunora-welcome *::after { box-sizing: border-box; } .lunora-welcome a { color: inherit; text-decoration: none; } .lunora-welcome button { font-family: inherit; cursor: pointer; } .lunora-welcome ::selection { background: hsl(256 72% 68% / 0.3); } .lunora-welcome code { font-family: var(--mono); } /* glow background */ .lunora-welcome .lw-bg { position: fixed; inset: 0; z-index: 0; pointer-events: none; overflow: hidden; } .lunora-welcome .lw-bg .glow { position: absolute; left: 50%; top: -4%; width: 860px; height: 720px; transform: translateX(-50%); border-radius: 50%; background: radial-gradient(circle at 50% 50%, var(--glow-1), var(--glow-2) 40%, transparent 66%); } .lunora-welcome .lw-bg .arc { position: absolute; left: 50%; border-radius: 50%; border: 1px solid var(--arc); transform: translateX(-50%); } .lunora-welcome .lw-bg .arc.a1 { top: -340px; width: 980px; height: 980px; } .lunora-welcome .lw-bg .arc.a2 { top: -240px; width: 720px; height: 720px; opacity: .7; } /* theme toggle */ .lunora-welcome .lw-toggle { position: fixed; z-index: 5; top: 20px; right: clamp(16px,4vw,36px); display: inline-flex; align-items: center; gap: 7px; border: 1px solid var(--line-2); background: var(--surface); backdrop-filter: blur(12px); color: var(--t-secondary); font-family: var(--mono); font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; padding: 7px 11px; transition: .15s; } .lunora-welcome .lw-toggle:hover { color: var(--t-display); border-color: var(--accent); } .lunora-welcome .lw-toggle svg { width: 13px; height: 13px; } .lunora-welcome .lw-wrap { position: relative; z-index: 2; width: 100%; max-width: 1080px; margin: 0 auto; padding: clamp(44px,8vh,92px) clamp(20px,5vw,48px); display: flex; flex-direction: column; min-height: 100vh; } .lunora-welcome .brand { display: flex; align-items: center; justify-content: center; gap: 15px; margin-bottom: clamp(38px,6vh,68px); color: var(--logo); } .lunora-welcome .brand svg { width: 54px; height: auto; display: block; } .lunora-welcome .brand .word { font-size: 34px; font-weight: 600; letter-spacing: -0.03em; color: var(--t-display); } .lunora-welcome .grid { display: grid; grid-template-columns: 1.06fr 0.94fr; gap: 16px; align-items: stretch; flex: 1; max-height: 580px; } @media (max-width: 820px) { .lunora-welcome .grid { grid-template-columns: 1fr; max-height: none; } } .lunora-welcome .card { border: 1px solid var(--line); background: var(--surface); backdrop-filter: blur(10px); transition: border-color .2s, background .2s; } .lunora-welcome .card:hover { border-color: var(--line-2); background: var(--surface-2); } .lunora-welcome .card:hover .arrow { color: var(--accent); transform: translateX(3px); } .lunora-welcome .arrow { color: var(--t-faint); transition: color .2s, transform .2s; } .lunora-welcome .arrow svg { width: 20px; height: 20px; display: block; } .lunora-welcome .ic { display: grid; place-items: center; color: var(--accent); border: 1px solid color-mix(in oklab, var(--accent) 36%, transparent); background: color-mix(in oklab, var(--accent) 12%, transparent); } .lunora-welcome .ic svg { display: block; } /* left feature card — stretches to fill the box height */ .lunora-welcome .feature { padding: clamp(18px,2vw,24px); display: flex; flex-direction: column; } .lunora-welcome .shot { border: 1px solid var(--line); overflow: hidden; background: var(--shot-bg); -webkit-mask-image: linear-gradient(to bottom, #000 56%, transparent 100%); mask-image: linear-gradient(to bottom, #000 56%, transparent 100%); } .lunora-welcome .shot .top { display: flex; align-items: center; gap: 12px; padding: 11px 14px; border-bottom: 1px solid var(--line); } .lunora-welcome .shot .wm { display: flex; align-items: center; gap: 7px; font-size: 12px; font-weight: 600; color: var(--t-primary); } .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%); } .lunora-welcome .shot .search { flex: 1; height: 22px; border: 1px solid var(--line); border-radius: 4px; } .lunora-welcome .shot .ver { font-family: var(--mono); font-size: 8px; letter-spacing: .08em; color: var(--t-faint); } .lunora-welcome .shot .body { display: grid; grid-template-columns: 92px 1fr; min-height: 224px; } .lunora-welcome .shot .nav { border-right: 1px solid var(--line); padding: 14px 12px; display: flex; flex-direction: column; gap: 11px; } .lunora-welcome .shot .nav i, .lunora-welcome .shot .doc i { height: 5px; border-radius: 2px; background: var(--line); } .lunora-welcome .shot .doc { padding: 16px 18px; display: flex; flex-direction: column; gap: 9px; } .lunora-welcome .shot .doc .h { height: 8px; width: 46%; border-radius: 2px; background: var(--line-2); margin-bottom: 5px; } .lunora-welcome .shot .doc .accent { height: 5px; width: 26%; border-radius: 2px; background: var(--accent); } .lunora-welcome .feature .info { margin-top: auto; padding-top: clamp(18px,2.4vh,28px); } .lunora-welcome .feature .ic { width: 40px; height: 40px; margin-bottom: 15px; } .lunora-welcome .feature .ic svg { width: 19px; height: 19px; } .lunora-welcome .feature h2 { margin: 0 0 10px; font-size: 19px; font-weight: 600; letter-spacing: -0.015em; color: var(--t-display); } .lunora-welcome .feature .row { display: flex; align-items: flex-end; gap: 16px; } .lunora-welcome .feature p { margin: 0; color: var(--t-secondary); font-size: 14px; max-width: 50ch; } .lunora-welcome .feature .row .arrow { margin-left: auto; } /* right stack — smaller cards, spread to align bottoms with the feature */ .lunora-welcome .stack { display: flex; flex-direction: column; gap: 16px; height: 100%; } .lunora-welcome .mini { flex: 1; padding: 15px 17px; display: flex; align-items: center; gap: 16px; min-height: 0; } .lunora-welcome .mini .mc { flex: 1; } .lunora-welcome .mini .ic { width: 32px; height: 32px; margin-bottom: 10px; } .lunora-welcome .mini .ic svg { width: 16px; height: 16px; } .lunora-welcome .mini h3 { margin: 0 0 5px; font-size: 16px; font-weight: 600; letter-spacing: -0.01em; color: var(--t-display); } .lunora-welcome .mini p { margin: 0; color: var(--t-secondary); font-size: 12.5px; line-height: 1.45; } .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); } @media (prefers-reduced-motion: reduce) { .lunora-welcome * { transition: none !important; } } `,Gt=`import { useEffect, useState } from "react"; type Theme = "dark" | "light"; export default function App() { // Default to "dark" for a stable first paint, then reconcile to the OS // preference; the toggle takes over after that. const [theme, setTheme] = useState<Theme>("dark"); useEffect(() => { if (typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: light)").matches) { setTheme("light"); } }, []); const isLight = theme === "light"; return ( <div className="lunora-welcome" data-theme={theme}> <div className="lw-bg"> <div className="arc a1" /> <div className="arc a2" /> <div className="glow" /> </div> <button className="lw-toggle" type="button" aria-label="Toggle color theme" onClick={() => setTheme(isLight ? "dark" : "light")}> {isLight ? ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"> <path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" /> </svg> ) : ( <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="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> )} <span>{isLight ? "Ivory" : "Night"}</span> </button> <div className="lw-wrap"> <div className="brand"> <svg viewBox="0 0 543 446" role="img" aria-label="Lunora"> <path d="${N}" fill="currentColor" fillRule="evenodd" /> </svg> <span className="word">Lunora</span> </div> <div className="grid"> <a className="card feature" href="https://lunora.sh/docs"> <div className="shot" aria-hidden="true"> <div className="top"> <span className="wm"> <i /> Lunora </span> <span className="search" /> <span className="ver">v0.1</span> </div> <div className="body"> <div className="nav"> <i style={{ width: "80%" }} /> <i style={{ width: "60%" }} /> <i style={{ width: "72%" }} /> <i style={{ width: "50%" }} /> <i style={{ width: "66%" }} /> <i style={{ width: "44%" }} /> <i style={{ width: "58%" }} /> </div> <div className="doc"> <span className="h" /> <i style={{ width: "92%" }} /> <i style={{ width: "88%" }} /> <span className="accent" /> <i style={{ width: "80%" }} /> <i style={{ width: "90%" }} /> <i style={{ width: "72%" }} /> <i style={{ width: "84%" }} /> <i style={{ width: "78%" }} /> </div> </div> </div> <div className="info"> <span className="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7"> <path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" /> <path d="M14 3v5h5" /> </svg> </span> <h2>Documentation</h2> <div className="row"> <p> Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC, you'll have a live app fast. </p> <span className="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </div> </div> </a> <div className="stack"> <a className="card mini" href="https://lunora.sh/blog"> <div className="mc"> <span className="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7"> <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" /> </svg> </span> <h3>Blog</h3> <p>Product updates, deep dives, and what's new in Lunora.</p> </div> <span className="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </a> <a className="card mini" href="/_lunora"> <div className="mc"> <span className="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7"> <rect x="3" y="3" width="18" height="18" rx="1" /> <path d="M3 9h18M9 21V9" /> </svg> </span> <h3>Lunora Studio</h3> <p>Local admin for schema, data, logs, and advisors.</p> </div> <span className="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </a> <a className="card mini" href="https://lunora.sh/packages"> <div className="mc"> <span className="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7"> <path d="M12 2 3 7v10l9 5 9-5V7z" /> <path d="M3 7l9 5 9-5M12 12v10" /> </svg> </span> <h3>Cloudflare ecosystem</h3> <p>Auth, mail, storage, AI, payments — one deploy.</p> </div> <span className="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </a> </div> </div> <div className="lw-foot">Running on Lunora · Vite + React</div> </div> </div> ); } `,Jt=`<script setup lang="ts"> import { onMounted, ref } from "vue"; // Default to "dark" for a stable first paint, then reconcile to the OS // preference; the toggle takes over after that. const theme = ref<"dark" | "light">("dark"); onMounted(() => { if (window.matchMedia("(prefers-color-scheme: light)").matches) { theme.value = "light"; } }); const toggle = (): void => { theme.value = theme.value === "light" ? "dark" : "light"; }; <\/script> <template> <div class="lunora-welcome" :data-theme="theme"> <div class="lw-bg"> <div class="arc a1" /> <div class="arc a2" /> <div class="glow" /> </div> <button class="lw-toggle" type="button" aria-label="Toggle color theme" @click="toggle"> <svg v-if="theme === 'light'" 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> <svg v-else 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> <span>{{ theme === "light" ? "Ivory" : "Night" }}</span> </button> <div class="lw-wrap"> <div class="brand"> <svg viewBox="0 0 543 446" role="img" aria-label="Lunora"> <path d="${N}" fill="currentColor" fill-rule="evenodd" /> </svg> <span class="word">Lunora</span> </div> <div class="grid"> <a class="card feature" href="https://lunora.sh/docs"> <div class="shot" aria-hidden="true"> <div class="top"> <span class="wm"><i /> Lunora</span> <span class="search" /> <span class="ver">v0.1</span> </div> <div class="body"> <div class="nav"> <i style="width: 80%" /> <i style="width: 60%" /> <i style="width: 72%" /> <i style="width: 50%" /> <i style="width: 66%" /> <i style="width: 44%" /> <i style="width: 58%" /> </div> <div class="doc"> <span class="h" /> <i style="width: 92%" /> <i style="width: 88%" /> <span class="accent" /> <i style="width: 80%" /> <i style="width: 90%" /> <i style="width: 72%" /> <i style="width: 84%" /> <i style="width: 78%" /> </div> </div> </div> <div class="info"> <span class="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"> <path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" /> <path d="M14 3v5h5" /> </svg> </span> <h2>Documentation</h2> <div class="row"> <p> Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC, you'll have a live app fast. </p> <span class="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </div> </div> </a> <div class="stack"> <a class="card mini" href="https://lunora.sh/blog"> <div class="mc"> <span class="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"> <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" /> </svg> </span> <h3>Blog</h3> <p>Product updates, deep dives, and what's new in Lunora.</p> </div> <span class="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </a> <a class="card mini" href="/_lunora"> <div class="mc"> <span class="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"> <rect x="3" y="3" width="18" height="18" rx="1" /> <path d="M3 9h18M9 21V9" /> </svg> </span> <h3>Lunora Studio</h3> <p>Local admin for schema, data, logs, and advisors.</p> </div> <span class="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </a> <a class="card mini" href="https://lunora.sh/packages"> <div class="mc"> <span class="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"> <path d="M12 2 3 7v10l9 5 9-5V7z" /> <path d="M3 7l9 5 9-5M12 12v10" /> </svg> </span> <h3>Cloudflare ecosystem</h3> <p>Auth, mail, storage, AI, payments — one deploy.</p> </div> <span class="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </a> </div> </div> <div class="lw-foot">Running on Lunora · Vite + Vue</div> </div> </div> </template> `,Yt=`import { createSignal, onMount } from "solid-js"; export default function App() { // Default to "dark" for a stable first paint, then reconcile to the OS // preference; the toggle takes over after that. const [theme, setTheme] = createSignal<"dark" | "light">("dark"); onMount(() => { if (window.matchMedia("(prefers-color-scheme: light)").matches) { setTheme("light"); } }); const isLight = () => theme() === "light"; return ( <div class="lunora-welcome" data-theme={theme()}> <div class="lw-bg"> <div class="arc a1" /> <div class="arc a2" /> <div class="glow" /> </div> <button class="lw-toggle" type="button" aria-label="Toggle color theme" onClick={() => setTheme(isLight() ? "dark" : "light")}> {isLight() ? ( <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> ) : ( <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> )} <span>{isLight() ? "Ivory" : "Night"}</span> </button> <div class="lw-wrap"> <div class="brand"> <svg viewBox="0 0 543 446" role="img" aria-label="Lunora"> <path d="${N}" fill="currentColor" fill-rule="evenodd" /> </svg> <span class="word">Lunora</span> </div> <div class="grid"> <a class="card feature" href="https://lunora.sh/docs"> <div class="shot" aria-hidden="true"> <div class="top"> <span class="wm"> <i /> Lunora </span> <span class="search" /> <span class="ver">v0.1</span> </div> <div class="body"> <div class="nav"> <i style={{ width: "80%" }} /> <i style={{ width: "60%" }} /> <i style={{ width: "72%" }} /> <i style={{ width: "50%" }} /> <i style={{ width: "66%" }} /> <i style={{ width: "44%" }} /> <i style={{ width: "58%" }} /> </div> <div class="doc"> <span class="h" /> <i style={{ width: "92%" }} /> <i style={{ width: "88%" }} /> <span class="accent" /> <i style={{ width: "80%" }} /> <i style={{ width: "90%" }} /> <i style={{ width: "72%" }} /> <i style={{ width: "84%" }} /> <i style={{ width: "78%" }} /> </div> </div> </div> <div class="info"> <span class="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"> <path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" /> <path d="M14 3v5h5" /> </svg> </span> <h2>Documentation</h2> <div class="row"> <p> Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC, you'll have a live app fast. </p> <span class="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </div> </div> </a> <div class="stack"> <a class="card mini" href="https://lunora.sh/blog"> <div class="mc"> <span class="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"> <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" /> </svg> </span> <h3>Blog</h3> <p>Product updates, deep dives, and what's new in Lunora.</p> </div> <span class="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </a> <a class="card mini" href="/_lunora"> <div class="mc"> <span class="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"> <rect x="3" y="3" width="18" height="18" rx="1" /> <path d="M3 9h18M9 21V9" /> </svg> </span> <h3>Lunora Studio</h3> <p>Local admin for schema, data, logs, and advisors.</p> </div> <span class="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </a> <a class="card mini" href="https://lunora.sh/packages"> <div class="mc"> <span class="ic"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"> <path d="M12 2 3 7v10l9 5 9-5V7z" /> <path d="M3 7l9 5 9-5M12 12v10" /> </svg> </span> <h3>Cloudflare ecosystem</h3> <p>Auth, mail, storage, AI, payments — one deploy.</p> </div> <span class="arrow"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <path d="M5 12h14M13 6l6 6-6 6" /> </svg> </span> </a> </div> </div> <div class="lw-foot">Running on Lunora · Vite + Solid</div> </div> </div> ); } `,Xt=`<script lang="ts"> import { onMount } from "svelte"; // Default to "dark" for a stable first paint, then reconcile to the OS // preference; the toggle takes over after that. let theme = $state<"dark" | "light">("dark"); const isLight = $derived(theme === "light"); onMount(() => { if (window.matchMedia("(prefers-color-scheme: light)").matches) { theme = "light"; } }); const toggle = (): void => { theme = theme === "light" ? "dark" : "light"; }; <\/script> <div class="lunora-welcome" data-theme={theme}> <div class="lw-bg"> <div class="arc a1"></div> <div class="arc a2"></div> <div class="glow"></div> </div> <button class="lw-toggle" type="button" aria-label="Toggle color theme" onclick={toggle}> {#if isLight} <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> {:else} <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> {/if} <span>{isLight ? "Ivory" : "Night"}</span> </button> <div class="lw-wrap"> <div class="brand"> <svg viewBox="0 0 543 446" role="img" aria-label="Lunora"> <path d="${N}" fill="currentColor" fill-rule="evenodd" /> </svg> <span class="word">Lunora</span> </div> <div class="grid"> <a class="card feature" href="https://lunora.sh/docs"> <div class="shot" aria-hidden="true"> <div class="top"> <span class="wm"><i></i> Lunora</span> <span class="search"></span> <span class="ver">v0.1</span> </div> <div class="body"> <div class="nav"> <i style="