@lunora/cli
Version:
The Lunora CLI: init, dev, deploy, codegen, migrate, seed, doctor, insights, logs, registry, and the rest of the project commands
1,229 lines • 92 kB
JavaScript
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="width: 80%"></i>
<i style="width: 60%"></i>
<i style="width: 72%"></i>
<i style="width: 50%"></i>
<i style="width: 66%"></i>
<i style="width: 44%"></i>
<i style="width: 58%"></i>
</div>
<div class="doc">
<span class="h"></span>
<i style="width: 92%"></i>
<i style="width: 88%"></i>
<span class="accent"></span>
<i style="width: 80%"></i>
<i style="width: 90%"></i>
<i style="width: 72%"></i>
<i style="width: 84%"></i>
<i style="width: 78%"></i>
</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 + Svelte</div>
</div>
</div>
`,Qt=`import "./style.css";
import { LunoraClient } from "lunorash/client";
import { api } from "#lunora/_generated/api.js";
// \`@lunora/vite\` runs the Worker on the same origin as Vite, so default to
// \`location.origin\`. Point \`VITE_LUNORA_URL\` at a deployed Worker to develop
// the client against production data.
const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;
const client = new LunoraClient({ url });
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>';
const SUN_ICON =
'<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>';
const welcomeHtml = \`
<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"></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="width: 80%"></i>
<i style="width: 60%"></i>
<i style="width: 72%"></i>
<i style="width: 50%"></i>
<i style="width: 66%"></i>
<i style="width: 44%"></i>
<i style="width: 58%"></i>
</div>
<div class="doc">
<span class="h"></span>
<i style="width: 92%"></i>
<i style="width: 88%"></i>
<span class="accent"></span>
<i style="width: 80%"></i>
<i style="width: 90%"></i>
<i style="width: 72%"></i>
<i style="width: 84%"></i>
<i style="width: 78%"></i>
</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 + Vanilla · <span id="lw-count">0</span> messages</div>
</div>
\`;
const root = document.querySelector<HTMLDivElement>("#app")!;
root.classList.add("lunora-welcome");
root.innerHTML = welcomeHtml;
// Theme toggle: flip the root's data-theme + swap the button's icon/label.
const toggleButton = root.querySelector<HTMLButtonElement>(".lw-toggle")!;
const paintToggle = (theme: "dark" | "light"): void => {
root.dataset.theme = theme;
toggleButton.innerHTML = \`\${theme === "light" ? MOON_ICON : SUN_ICON}<span>\${theme === "light" ? "Ivory" : "Night"}</span>\`;
};
paintToggle(window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
toggleButton.addEventListener("click", () => {
paintToggle(root.dataset.theme === "light" ? "dark" : "light");
});
// Live demo: the message count of a demo channel re-renders on every delta.
const count = document.querySelector<HTMLSpanElement>("#lw-count")!;
client.onUpdate(api.messages.list, { channelId: "channel:demo" }, (result) => {
count.textContent = String(result.messages.length);
});
`,O="const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;",Zt=`import "./index.css";
import { LunoraProvider } from "@lunora/react";
import { LunoraClient } from "lunorash/client";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
// \`@lunora/vite\` runs the Worker on the same origin as Vite, so default to
// \`location.origin\`. Point \`VITE_LUNORA_URL\` at a deployed Worker to develop
// the client against production data.
${O}
const client = new LunoraClient({ url });
const root = document.getElementById("root");
if (!root) {
throw new Error("missing #root mount node");
}
createRoot(root).render(
<StrictMode>
<LunoraProvider client={client}>
<App />
</LunoraProvider>
</StrictMode>,
);
`,ea=`import "./style.css";
import { createLunora } from "@lunora/vue";
import { LunoraClient } from "lunorash/client";
import { createApp } from "vue";
import App from "./App.vue";
// Provide one LunoraClient at the app root via the Vue plugin form.
${O}
createApp(App).use(createLunora(new LunoraClient({ url }))).mount("#app");
`,ta=`import "./index.css";
import { LunoraContext } from "@lunora/solid";
import { LunoraClient } from "lunorash/client";
import { render } from "solid-js/web";
import App from "./App";
${O}
const client = new LunoraClient({ url });
const root = document.getElementById("root");
render(
() => (
<LunoraContext.Provider value={client}>
<App />
</LunoraContext.Provider>
),
root!,
);
`,aa=`<script lang="ts">
import { setLunoraClient } from "@lunora/svelte";
import { LunoraClient } from "lunorash/client";
import App from "./App.svelte";
${O}
setLunoraClient(new LunoraClient({ url }));
<\/script>
<App />
`,ra=`import "./app.css";
import { mount } from "svelte";
import Root from "./Root.svelte";
// Mount \`Root\` (it sets the ambient LunoraClient) rather than \`App\` directly.
const app = mount(Root, { target: document.getElementById("app")! });
export default app;
`,T={react:{adapter:"@lunora/react",createViteTemplate:"react-ts",files:[{contents:Zt,path:"src/main.tsx"},{contents:Gt,path:"src/App.tsx"},{contents:S,path:"src/index.css"}],label:"React"},solid:{adapter:"@lunora/solid",createViteTemplate:"solid-ts",files:[{contents:ta,path:"src/index.tsx"},{contents:Yt,path:"src/App.tsx"},{contents:S,path:"src/index.css"}],label:"Solid"},svelte:{adapter:"@lunora/svelte",createViteTemplate:"svelte-ts",files:[{contents:aa,path:"src/Root.svelte"},{contents:ra,path:"src/main.ts"},{contents:Xt,path:"src/App.svelte"},{contents:S,path:"src/app.css"}],label:"Svelte"},vanilla:{adapter:"lunorash/client",createViteTemplate:"vanilla-ts",files:[{contents:Qt,path:"src/main.ts"},{contents:S,path:"src/style.css"}],label:"Vanilla"},vue:{adapter:"@lunora/vue",createViteTemplate:"vue-ts",files:[{contents:ea,path:"src/main.ts"},{contents:Jt,path:"src/App.vue"},{contents:S,path:"src/style.css"}],label:"Vue"}},z=e=>Object.hasOwn(T,e),oa=`import type { Middleware } from "lunorash/server";
import { defineSchemaExtension, defineTable, definePlugin, v } from "lunorash/server";
import { createDbStore, RateLimiter } from "lunorash/ratelimit";
import type { RateLimitConfigMap } from "lunorash/ratelimit";
export const limits = {
default: { kind: "token bucket", period: 60_000, rate: 10 },
} as const satisfies RateLimitConfigMap;
export type LimitName = keyof typeof limits;
export const makeRateLimiter = (ctx: { db: unknown }): RateLimiter<LimitName> =>
new RateLimiter<LimitName>({
config: limits,
store: createDbStore({ db: ctx.db as never, table: "ratelimit_buckets" }),
});
const middleware: Middleware<{ api?: Record<string, unknown>; db: unknown }, { api: Record<string, unknown>; db: unknown }> = ({ ctx, next }) =>
next({
ctx: {
...ctx,
api: { ...ctx.api, ratelimit: makeRateLimiter(ctx) },
},
});
export const ratelimit = definePlugin("ratelimit", {
extension: defineSchemaExtension("ratelimit", {
tables: {
buckets: defineTable({
key: v.string(),
value: v.number(),
ts: v.number(),
prev: v.optional(v.number()),
})
.index("by_key", ["key"])
.externallyManaged(),
},
}),
middleware,
});
`,sa=`import { ratelimit } from "./ratelimit/schema.js";
import { defineSchema, defineTable, v } from "lunorash/server";
export default defineSchema({
messages: defineTable({
channelId: v.string(),
text: v.string(),
})
.shardBy("channelId")
.index("by_channel", ["channelId"]),
}).extend(ratelimit.extension);
`,na=`import { RateLimiter, rateLimit, createDbStore } from "lunorash/ratelimit";
import { mutation, query, v } from "#lunora/_generated/server.js";
const limiter = (ctx: { db: unknown }) => new RateLimiter({
config: {
send: { kind: "token bucket", period: 60_000, rate: 30 },
},
store: createDbStore({ db: ctx.db as never, table: "ratelimit_buckets" }),
});
export const list = query.input({ channelId: v.string().meta({ schema: { maxLength: 256 } }), limit: v.optional(v.number()) }).query(async ({ args, ctx }) => {
const messages = await ctx.db
.query("messages")
.withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
.take(args.limit ?? 50);
return { channelId: args.channelId, messages };
});
export const send = mutation
.input({ channelId: v.string().meta({ schema: { maxLength: 256 } }), text: v.string().meta({ schema: { maxLength: 4096 } }) })
.use(rateLimit(limiter, "send", { key: (ctx) => ctx.auth.userId ?? "anon" }))
.mutation(async ({ args, ctx }) => {
const id = await ctx.db.insert("messages", { channelId: args.channelId, text: args.text });
return { channelId: args.channelId, id, text: args.text };
});
`,ia=`import type { ShardNamespaceLike } from "lunorash/runtime";
import { defineApp } from "../lunora/_generated/app.js";
interface Env extends Record<string, unknown> {
SHARD: ShardNamespaceLike;
}
const app = defineApp<Env>()
.shard((env) => env.SHARD)
.build();
export const ShardDO = app.ShardDO;
export default app;
`,la=`{
"$schema": "node_modules/wrangler/config-schema.json",
"name": "__NAME__",
"main": "src/server.ts",
"compatibility_date": "2026-06-10",
"compatibility_flags": ["nodejs_compat"],
"durable_objects": {
"bindings": [{ "name": "SHARD", "class_name": "ShardDO" }],
},
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["ShardDO"] }],
"observability": { "enabled": true, "head_sampling_rate": 1 },
}
`,ca=[".wrangler",".env",".env.*","!.env.example",".dev.vars",".dev.vars.*","!.dev.vars.example",".lunora/",".lunora-cache","lunora/_generated"],da=`# Lunora endpoint for the browser client.
# Vite statically replaces \`import.meta.env.VITE_LUNORA_URL\` at \`vite dev\` / build.
# Leave it unset to use the page origin; set it to point at a deployed Worker:
#
# VITE_LUNORA_URL=https://my-app.example.workers.dev
`,ua={"@cloudflare/workers-types":"^4.20260611.1",wrangler:"^4.100.0"},x=(e,t,a,r)=>{const o=u(e,t);M(A(o),{recursive:!0}),v(o,a,"utf8"),r.push(o)},pa=/\r?\n/,ha=e=>{const t=u(e,".gitignore"),a=m(t)?b(t,"utf8"):"",r=new Set(a.split(pa)),o=ca.filter(n=>!r.has(n));if(o.length===0)return;const s=a.length>0&&!a.endsWith(`
`)?`
`:"";v(t,`${a}${s}
# Lunora
${o.join(`
`)}
`,"utf8")},P=e=>e==="lunorash"||e.startsWith("@lunora/"),ma=(e,t,a,r)=>P(e)?r.get(e)??a:t,k=(e,t,a)=>({...e,[t]:a}),ee=(e,t,a)=>Object.fromEntries(Object.entries(e).map(([r,o])=>[r,ma(r,o,t,a)])),fa=async(e,t,a,r)=>{const o=u(e,"package.json"),s=JSON.parse(b(o,"utf8"));let n=k(s.dependencies??{},"lunorash",r);n=k(n,"@lunora/ratelimit",r),a.adapter.startsWith("@lunora/")&&(n=k(n,a.adapter,r));for(const[c,p]of Object.entries(a.extraDependencies??{}))n=k(n,c,p);let i=k(s.devDependencies??{},"@lunora/vite",r);i=k(i,"@lunora/studio",r);for(const[c,p]of Object.entries(ua))i=k(i,c,p);const l=[...Object.keys(n),...Object.keys(i)].filter(c=>P(c)),d=await ce(l,r);s.name=t,s.imports={...s.imports,"#lunora/*":"./lunora/*"},s.dependencies=ee(n,r,d),s.devDependencies=ee(i,r,d),s.scripts={...s.scripts,codegen:"lunora codegen",deploy:"vite build && lunora deploy"},v(o,`${JSON.stringify(s,void 0,4)}
`,"utf8")},va=["vite.config.ts","vite.config.mts","vite.config.js","vite.config.mjs"],Le=e=>va.map(t=>u(e,t)).find(t=>m(t)),ga=(e,t)=>{const a=Le(e);if(a===void 0){t.warn("overlay: no vite.config found in the create-vite base — add `lunora()` to your Vite plugins manually.");return}const r=ke(b(a,"utf8"));r.changed&&v(a,r.code,"utf8")},wa=async e=>{const{adapter:t,distTag:a,logger:r,name:o,target:s}=e,n=[];x(s,u("lunora","ratelimit","schema.ts"),oa,n),x(s,u("lunora","schema.ts"),sa,n),x(s,u("lunora","messages.ts"),na,n),x(s,u("src","server.ts"),ia,n),x(s,"wrangler.jsonc",la.replaceAll("__NAME__",o),n),x(s,".env.example",da,n);for(const i of t.files)x(s,i.path,i.contents,n);return ga(s,r),await fa(s,o,t,a),ha(s),n},ya=["lunar","silver","silent","waning","waxing","crescent","cosmic","stellar","orbital","gibbous","twilight","midnight","shimmering","drifting","weightless"],ba=["moon","tide","crater","comet","eclipse","halo","orbit","nebula","voyager","lander","rover","beacon","harbor","meadow","fox"],te=e=>e[Math.floor(Math.random()*e.length)],xa=()=>`${te(ya)}-${te(ba)}`,ka=async()=>vt.lookup("github.com").then(()=>!0,()=>!1),Ca=/^(?:gh|github):([^/]+)\/([^#/]+)(?:\/[^#]*)?(?:#(.+))?$/,Se=e=>{const t=Ca.exec(e);if(t===null)return;const[,a,r,o]=t;if(!(a===void 0||r===void 0))return{owner:a,ref:o??"HEAD",repo:r}},La=async e=>{const t=Se(e);if(t===void 0)return;const a=`https://codeload.github.com/${t.owner}/${t.repo}/tar.gz/${t.ref}`;try{const r=await fetch(a,{method:"HEAD"});return r.status===404?!1:r.ok?!0:void 0}catch{return}},Me=async e=>e.isLocal||!(e.source===void 0||Se(e.source)!==void 0)?!0:await ka()?e.source!==void 0&&await La(e.source)===!1?(e.logger.error(`template source not found: ${e.source} — double-check --ref / --source, or browse the templates at https://lunora.sh/docs.`),!1):!0:(e.logger.error("you appear to be offline — connect to the internet and try again, or scaffold from a local template with `--from <dir>`."),!1),y={extras:"Let's finish setting up your app.",framework:"Which framework should we launch?",git:"Initialize a new git repository? (optional)",install:"Install dependencies now?",name:"Where should we land your project?",nextHeader:"Liftoff confirmed — explore your project!",packageManager:"Which package manager?"},Sa=new Set([".gitignore",".html",".js",".json",".jsonc",".md",".mjs",".ts",".tsx"]),Ma=`import { defineConfig } from "vite";
import { lunora } from "@lunora/vite";
export default defineConfig({ plugins: [lunora()] });
`,Aa=`import { defineSchema, defineTable, v } from "@lunora/server";
export default defineSchema({
messages: defineTable({
channelId: v.id("channels"),
text: v.string(),
})
.shardBy("channelId")
.index("by_channel", ["channelId"]),
});
`,Ea=`import { mutation, query, v } from "./_generated/server";
export const list = query
.input({ channelId: v.id("channels"), limit: v.optional(v.number()) })
.query(async ({ args }) => {
return { channelId: args.channelId, limit: args.limit ?? 50, messages: [] };
});
export const send = mutation
.input({ channelId: v.id("channels"), text: v.string() })
.mutation(async ({ args }) => {
return { channelId: args.channelId, text: args.text };
});
`,Na="gh:anolilab/lunora/templates",Ia=e=>{const t=e.lastIndexOf(".");return t===-1?!1:Sa.has(e.slice(t))},_a=(e,t)=>e.replaceAll("{{name}}",t),$a=async(e,t)=>{const a=new Set;for(const r of e)if(E(r)==="package.json")try{const o=JSON.parse(b(r,"utf8"));for(const s of["dependencies","devDependencies"])for(const n of Object.keys(o[s]??{}))P(n)&&a.add(n)}catch{}return ce(a,t)},Ra=(e,t,a)=>{let r;try{r=JSON.parse(e)}catch{return e}let o=e;for(const s of["dependencies","devDependencies"])for(const n of Object.keys(r[s]??{})){if(!P(n))continue;const i=a.get(n)??t,l=We(o,[s,n],i,{formattingOptions:{insertSpaces:!0,tabSize:4}});o=ze(o,l)}return o},Oa=["@parcel/watcher","esbuild","lmdb","msgpackr-extract","rs-module-lexer","sharp","unrs-resolver","vue-demi","workerd"],Ta=["cpu-features","protobufjs","ssh2"],Ae="pnpm-workspace.yaml",Pa=()=>["# pnpm reads its settings from here (the package.json `pnpm` field is no longer read).","# Pre-approve the toolchain's native build scripts so `pnpm install` runs them","# without the interactive `pnpm approve-builds` step; deny the optional native","# builds a scaffold doesn't need (so no C/C++ toolchain is required).","allowBuilds:",...Oa.map(e=>` '${e}': true`),...Ta.map(e=>` '${e}': false`),""].join(`
`),q=e=>{const t=[];for(const a of Fe(e,{includeDirs:!1,includeFiles:!0}))Oe(a.path).isSymbolicLink()||t.push(a.path);return t},Ee=async(e,t,a)=>{const r=q(e),o=[],s=de(),n=await $a(r,s);for(const i of r){const l=He(e,i),d=u(t,l);M(A(d),{recursive:!0});const c=b(i);let p=Ia(i)?_a(c.toString("utf8"),a):void 0;p!==void 0&&E(i)==="package.json"&&(p=Ra(p,s,n)),p===void 0?v(d,c):v(d,p,"utf8"),o.push(d)}return o},B=(e,t,a)=>t!==void 0&&t.length>0?t:`${Na}/${e}#${ue(a)}`,Da=e=>e.includes("..")?!1:e.startsWith("gh:")||e.startsWith("github:")||e.startsWith("https://"),L=(e,t)=>{e.info(`[dry-run] would ${t}`)},K=(e,t,a)=>{g()&&process.stdout.write(`
`),e.success(`scaffolded ${String(t.length)} files into ${a}`)},Ba=(e,t)=>{const{args:a,command:r}=Je(e,t);return`${r} ${a.join(" ")}`},Ne=(e,t)=>{let a=ne(e);for(;;){if(t(a))return!0;const r=A(a);if(r===a)return!1;a=r}},ja=e=>{if(m(u(e,Ae)))return!0;const t=u(e,"package.json");if(!m(t))return!1;try{return JSON.parse(b(t,"utf8")).workspaces!==void 0}catch{return!1}},Ie=e=>Ne(e,ja),Va=e=>Ne(e,t=>m(u(t,".git"))),Ua=async(e,t)=>{if(e.yes===!0||!g()||Va(A(t)))return;if(!await fe(y.git,{badge:f.git,defaultYes:!1})){await ve("Sounds good! You can always run git init manually.");return}if(e.dryRun===!0){L(e.logger,"initialize a git repository");return}const a=e.spawner??pe;(await st("Initializing a git repository…",()=>a({args:["init"],command:"git",cwd:t}))).code===0?await R("git","Initialized an empty git repository."):e.logger.warn("`git init` failed — initialize it yourself later with `git init`.")},Fa=async(e,t,a,r)=>{const o=[{code:`cd ./${e}`,lead:"Enter your project directory using"}];t===void 0&&o.push({code:`${a} install`,lead:"Install dependencies with",tail:r?" from the workspace root":void 0}),o.push({code:Ye(a,"dev"),lead:"Run",tail:" to start the dev server."},{code:"lunora add",lead:"Add features like auth or storage using"});const s=[{code:"https://lunora.sh/docs",lead:"Read the docs at"},{code:"https://lunora.sh/chat",lead:"Stuck? Join the chat at"}];if(g()){await nt(f.next,y.nextHeader,o,s);return}const n=o.map(i=>`${i.lead} ${i.code}${i.tail??""}`);n.push("",...s.map(i=>`${i.lead} ${i.code}${i.tail??""}`)),await R("next",y.nextHeader,n.join(`
`))},Ha=e=>e.yes!==!0&&(e.installPrompt!==void 0||g()),Wa=async(e,t)=>{if(!Ha(e)||Ie(A(t)))return;const a=Ge(e.packageManagerProbe),[r]=a;if(r===void 0)return;if(!await(e.installPrompt?.confirmInstall??(async()=>fe(y.install,{badge:f.deps,defaultYes:!0})))()){await ve("No problem! Remember to install dependencies after setup.");return}let s=r;if(a.length>1&&(s=e.installPrompt?await e.installPrompt.selectManager(a):await H(y.packageManager,a.map(c=>({label:c,value:c})),{badge:f.deps,default:r})??r),e.dryRun===!0){L(e.logger,`install dependencies with ${s}`);return}if(s==="pnpm"){const c=u(t,Ae);m(c)||v(c,Pa(),"utf8")}const n=e.spawner??pe,{args:i,command:l}=le(s);await R("deps",`Installing dependencies with ${s}…`);const d=await n({args:i,command:l,cwd:t});if(d.code!==0){e.logger.warn(`\`${l} install\` exited with code ${String(d.code)} — run it yourself in ${E(t)}/.`);return}return await R("deps",`Dependencies installed with ${s}.`),s},za=async(e,t,a,r,o)=>{const s=u(e,t);if(!m(s))return o.error(`template not found in local source: ${s}`),{code:1,files:[],target:a};const n=await Ee(s,a,r);return K(o,n,a),{code:0,files:n,target:a}},qa=async e=>{const{logger:t,name:a,ref:r,source:o,target:s,templateType:n}=e,i=oe(u(se(),"lunora-init-fetch-")),l=u(i,"template");try{const d=o!==void 0&&o.length>0?r:await et(r,t),c=B(n,o,d);let p,h=[];await ge([{label:`${n} template fetched`,run:async()=>{p=await ie(c,{cwd:i,dir:l,force:!0,install:!1,silent:!0})}},{label:`files copied into ${a}/`,run:async()=>{h=await Ee(l,s,a)}}],{end:"Project initialized!",start:"Project initializing…"});const I=q(l);return g()&&process.stdout.write(`
`),t.info(p?.commit?`template: ${p.source} @ ${p.commit} (${String(I.length)} files)`:`template: ${p?.source??c} (${String(I.length)} files)`),K(t,h,s),{code:0,files:h,target:s}}catch(d){if(d instanceof F)throw d;const{hints:c,message:p}=Pt(d,{ref:ue(r),remote:B(n,o,r),templateType:n});t.error(p);for(const h of c)t.warn(h);return{code:1,files:[],target:s}}finally{$(i,{force:!0,recursive:!0})}},Ka=e=>{for(const t of["_gitignore","_npmrc","_gitattributes"]){const a=u(e,t);m(a)&&Re(a,u(e,`.${t.slice(1)}`))}},Ga=async e=>{const{framework:t,logger:a,name:r,overlayBaseFrom:o,target:s}=e,n=T[t],i=oe(u(se(),"lunora-vite-base-"));try{let l;if(o!==void 0&&(l=u(o,`template-${n.createViteTemplate}`),!m(l)))return a.error(`create-vite base not found on disk: ${l}`),{code:1,files:[],target:s};const d=async()=>{if(l!==void 0){G(l,s,{recursive:!0});return}const p=u(i,"base"),h=`github:vitejs/vite/packages/create-vite/template-${n.createViteTemplate}#main`;await ie(h,{cwd:i,dir:p,force:!0,install:!1,silent:!0}),Ka(p),G(p,s,{recursive:!0})};let c=[];return await ge([{label:`create-vite (${n.label}) base ready`,run:d},{label:`Lunora overlay applied (${n.label})`,run:async()=>{c=await wa({adapter:n,distTag:de(),logger:a,name:r,target:s})}}],{end:"Project initialized!",start:"Project initializing…"}),K(a,c,s),{code:0,files:[...q(s)],target:s}}catch(l){if(l instanceof F)throw l;const d=l instanceof Error?l.message:String(l);return a.error(`failed to scaffold the ${n.label} base: ${d}`),{code:1,files:[],target:s}}finally{$(i,{force:!0,recursive:!0})}},j=(e,t,a,r,o)=>(o.error(`init --in-place: could not ${e} ${t}: ${a instanceof Error?a.message:String(a)}`),{code:1,files:[],target:r}),Ja=(e,t)=>{const a=u(e,"vite.config.ts");try{v(a,Ma,"utf8")}catch(r){return j("write",a,r,e,t)}return t.success(`created ${a} with lunora() plugin`),{code:0,files:[a],target:e}},Ya=(e,t,a)=>{let r;try{r=b(e,"utf8")}catch(s){return j("read",e,s,t,a)}const o=ke(r);if(!o.changed)return a.info(`${e}: ${o.reason??"no changes needed"}`),{code:0,files:[],target:t};try{v(e,o.code,"utf8")}catch(s){return j("write",e,s,t,a)}return a.success(`patched ${e} — added lunora() plugin`),{code:0,files:[e],target:t}},Xa=(e,t)=>{const a=u(e,"lunora"),r=u(a,"schema.ts");if(m(r))return t.info(`lunora/ already present — left ${r} untouched`),[];const o=[];try{M(a,{recursive:!0}),v(r,Aa,"utf8"),o.push(r);const s=u(a,"messages.ts");m(s)||(v(s,Ea,"utf8"),o.push(s)),t.success(`scaffolded lunora/ (${String(o.length)} file(s))`)}catch(s){const n=s instanceof Error?s.message:String(s);t.error(`init --here: could not scaffold lunora/: ${n}`)}return o},Qa=(e,t,a)=>{const{adapter:r,class:o,framework:s}=e;a.info(""),a.info(`detected framework: ${s} (class ${o})`),a.info("next steps:"),a.info(` 1. install the adapter: ${Ba(t,[r,"@lunora/client","@lunora/runtime","@lunora/server"])}`),a.info(" 2. run codegen: lunora codegen"),o==="A"?(a.info(" 3. compose one worker: wrap your worker entry with"),a.info(" createWorker({ httpRouter: <your framework SSR handler>, shardDO: ShardDO, ... })"),a.info(` 4. add the provider: mount the ${r} provider in your root layout/route`),a.info(" 5. make a loader live: preloadQuery() in a loader, usePreloadedQuery() in the component"),a.info(" see https://lunora.sh/docs/frameworks/reactive-loaders")):o==="B"?(a.info(" 3. inject Lunora: mount Lunora realtime under /_lunora/* in your server hook"),a.info(` (${s} owns its Cloudflare adapter — Lunora composes into its server entry)`),a.info(` 4. add the provider: mount the ${r} provider in your root layout`),a.info(" 5. read the guide: https://lunora.sh/docs/frameworks/deploy")):(a.info(" 3. add the provider: wrap your app with the LunoraProvider from @lunora/react"),a.info(" 4. read the guide: https://lunora.sh/docs/frameworks/bring-your-framework")),a.info("")},Za=(e,t,a)=>{const r=Le(e);return r===void 0?t==="sveltekit"||t==="nuxt"||t==="astro"?(a.info(`no Vite config found — ${t} wires Lunora through its server entry (see next steps)`),{code:0,files:[],target:e}):Ja(e,a):Ya(r,e,a)},er=(e,t)=>{const a=St(e),r=Za(e,a.framework,t);if(r.code!==0)return r;const o=Xa(e,t);return Qa(a,U(e),t),{code:0,files:[...r.files,...o],target:e}},tr=e=>e.yes!==!0&&(e.prompt!==void 0||(e.interactive??g())),ae=async(e,t,a)=>{a.dryRun!==!0&&await Kt({apply:r=>Ue(e,r),detected:Ve(e),interactive:t,logger:a.logger,multiSelect:a.lintPrompt??((r,o,s)=>me(r,o,{...s,badge:f.add}))})},ar=async(e,t)=>{const a=tr(e),r=e.add===void 0?[]:jt(e.add,n=>{e.logger.warn(n)}),s={applyAll:async n=>{if(n.length===0)return!0;if(e.dryRun===!0)return L(e.logger,`add ${n.map(h=>h.label).join(", ")}`),!0;const i=[],l=g()?{error:h=>{i.push({level:"error",message:h})},info:()=>{},success:()=>{},warn:h=>{i.push({level:"warn",message:h})}}:e.logger,d=n.map(h=>({running:`adding ${h.label}…`,task:()=>gt({allowUnsafeSource:e.allowUnsafeSource,cwd:t,from:e.registryFrom,logger:l,names:[...h.names],ref:e.ref,source:e.registrySource,transformManifest:h.transformManifest,yes:!0})})),c=`added ${n.map(h=>h.label).join(", ")}`,p=await it(f.add,d,c);for(const{level:h,message:I}of i)e.logger[h](I);return p.every(h=>h.code===0)},interactive:a,logger:e.logger,multiSelect:e.prompt?.multiSelect??((n,i,l)=>me(n,i,{...l,badge:f.add})),preselected:r.length>0?r:void 0,projectName:E(t),resolveAuthUiItem:()=>{try{const n=JSON.parse(b(u(t,"package.json"),"utf8"));return ft({...n.dependencies,...n.devDependencies})??"auth-ui-react"}catch{return"auth-ui-react"}},select:e.prompt?.select??((n,i,l)=>H(n,i,{...l,badge:f.add})),text:e.prompt?.text??((n,i)=>he(n,{...i,badge:f.add}))};if(r.length>0){await Z(s),await ae(t,a,e);return}g()&&await ot(y.extras),await Z(s),await ae(t,a,e)},D="react",_e=[{description:"React SPA — official create-vite base + the Lunora layer (the default)",label:"React",value:"react"},{description:"Vue SPA — create-vite base + Lunora",label:"Vue",value:"vue"},{description:"Solid SPA — create-vite base + Lunora",label:"Solid",value:"solid"},{description:"Svelte SPA — create-vite base + Lunora",label:"Svelte",value:"svelte"},{description:"Next.js (App Router) — OpenNext on Cloudflare + a standalone Lunora worker",label:"Next.js",value:"next"},{description:"TanStack Start (React) — SSR with live-loader routes",label:"TanStack Start · React",value:"tanstack-start-react"},{description:"TanStack Start (Solid)",label:"TanStack Start · Solid",value:"tanstack-start-solid"},{description:"Next.js App Router on Vite (vinext) — composed into the Lunora worker (experimental)",label:"vinext · App Router",value:"vinext"},{description:"Next.js Pages Router on Vite (vinext) — composed into one worker (experimental)",label:"vinext · Pages Router",value:"vinext-pages"},{description:"React Router (v7, framework mode) — SSR composed into the Lunora worker",label:"React Router",value:"react-router"},{description:"Astro + a standalone Lunora worker",label:"Astro",value:"astro"},{description:"AnalogJS (Angular) — single-worker, Lunora mounted in Nitro",label:"Analog",value:"analog"},{description:"Nuxt (Vue) — single-worker, Lunora mounted in Nitro",label:"Nuxt",value:"nuxt"},{description:"SvelteKit + a standalone Lunora worker",label:"SvelteKit",value:"sveltekit"},{description:"React Native (Expo) — an iOS/Android/web app + a Lunora worker backend",label:"React Native · Expo",value:"expo"},{description:"Worker only — no frontend",label:"Standalone",value:"standalone"}],rr=Object.keys(T).join("|"),$e=_e.filter(e=>!z(e.value)).map(e=>e.value),or=$e.join("|"),sr=e=>z(e)?{framework:e,kind:"overlay"}:{kind:"template",templateType:e},nr=async e=>e.vite!==void 0?{framework:e.vite,kind:"overlay"}:e.templateType!==void 0?{kind:"template",templateType:e.templateType}:!g()||e.yes===!0?{framework:D,kind:"overlay"}:sr(await H(y.framework,_e,{badge:f.tmpl,default:D})??D),ir=e=>{if(g()||e.yes===!0)return;const t=[];if(e.name===void 0&&t.push("a project name (`lunora init <name>`)"),e.templateType===void 0&&e.vite===void 0&&t.push(`a framework — \`--vite <${rr}>\` for an SPA, or \`-t <${or}>\` for a bespoke template`),t.length!==0)return`lunora init can't prompt in a non-interactive terminal — provide ${t.join(" and ")}, or pass --yes to accept the defaults.`},lr=async(e,t,a,r)=>z(t)?await Me({isLocal:e.overlayBaseFrom!==void 0,logger:e.logger})?(M(r,{recursive:!0}),Ga({framework:t,logger:e.logger,name:a,overlayBaseFrom:e.overlayBaseFrom,target:r})):{code:1,files:[],target:r}:(e.logger.error(`init: unknown framework "${t}". Supported overlays: ${Object.keys(T).join(", ")}.`),{code:1,files:[],target:r}),cr=async(e,t,a,r)=>e.from!==void 0?await za(e.from,t,r,a,e.logger):e.source!==void 0&&e.source.length>0&&!e.allowUnsafeSource&&!Da(e.source)?(e.logger.error(`init: refusing --source ${e.source} — only gh:, github:, or https:// sources are allowed (and may not contain ".."). Re-run with --allow-unsafe-source if you really want this.`),{code:1,files:[],target:r}):await Me({isLocal:!1,logger:e.logger,source:B(t,e.source,e.ref)})?qa({logger:e.logger,name:a,ref:e.ref,source:e.source,target:r,templateType:t}):{code:1,files:[],target:r},dr=async(e,t,a)=>{await rt("realtime backend on Cloudflare Workers + Durable Objects");const r=ir(e);if(r!==void 0)return e.logger.error(r),{code:1,files:[],target:""};const o=xa(),s=e.name??await he(y.name,{badge:f.dir,default:o,placeholder:o}),n=await nr(e),i=s.trim();if(i.length===0)return e.logger.error("init: refusing an empty project name — pass a directory name (e.g. `lunora init my-app`)."),{code:1,files:[],target:""};if(i.includes("/")||i.includes("\\")||i===".."||i===".")return e.logger.error(`init: refusing project name "${i}" — must not contain path separators or be "." / "..".`),{code:1,files:[],target:""};const l=ne(t,i),d=m(l);if(d&&re(l).length>0)return e.logger.error(`target directory not empty: ${l}`),{code:1,files:[],target:l};if(e.dryRun===!0){const c=n.kind==="overlay"?`the ${n.framework} create-vite overlay`:`the ${n.templateType} template`;return L(e.logger,`scaffold ${c} into ${l}`),{code:0,files:[],target:l}}return a(l,d),n.kind==="overlay"?lr(e,n.framework,i,l):cr(e,n.templateType,i,l)},ur=(e,t)=>{const{target:a,targetPreExisted:r}=e;if(!(a===void 0||!m(a))){if(r===!0)for(const o of re(a))$(u(a,o),{force:!0,recursive:!0});else $(a,{force:!0,recursive:!0});t.info(`removed the partially-created project at ${a}`)}},pr=async(e,t,a)=>e.inPlace!==!0?dr(e,t,a):e.dryRun===!0?(L(e.logger,`configure Lunora into ${t}`),{code:0,files:[],target:t}):er(t,e.logger),hr=async(e,t,a)=>{await ar(e,t.target);const r=e.inPlace===!0?void 0:await Wa(e,t.target);if(e.inPlace!==!0){await Ua(e,t.target);const o=r??U(t.target);await Fa(E(t.target),r,o,Ie(a)),await Dt(e.logger)}},mr=(e,t,a)=>{if(t.code!==0||e.ci===void 0)return;if(e.dryRun===!0){L(e.logger,`scaffold a ${e.ci} CI deploy pipeline`);return}const r=e.inPlace===!0?a:t.target;try{const o=U(r);Ct(r,e.ci,o,e.logger)}catch{e.logger.warn(`--ci ${e.ci}: could not detect a package manager for this project — skipped the CI pipeline.`)}},fr=async e=>{const t=e.cwd??process.cwd(),a={};let r;try{r=await pr(e,t,(o,s)=>{a.target=o,a.targetPreExisted=s}),r.code===0&&r.target!==""&&(a.target=void 0,await hr(e,r,t))}catch(o){if(o instanceof F)return ur(a,e.logger),process.stdout.write("\n ✖ Setup cancelled — run `lunora init` again whenever you're ready. 🌙\n"),{code:130,files:[],target:""};throw o}return mr(e,r,t),r},vr=e=>typeof e=="string"&&$e.includes(e),gr=(e,t)=>{if(e!==void 0){if(xt(e))return e;t.warn(`init: unknown --ci "${e}" — expected github | gitlab; skipping CI scaffold.`)}},Br=Xe(({argument:e,cwd:t,logger:a,options:r})=>{const o=r.template!==void 0&&vr(r.template)?r.template:void 0;return fr({add:r.add,allowUnsafeSource:r.allowUnsafeSource===!0,cwd:t,ci:gr(r.ci,a),dryRun:r.dryRun===!0,from:r.from,inPlace:r.here===!0,interactive:r.interactive===!0?!0:void 0,logger:a,name:e[0],ref:r.ref,source:r.source,templateType:o,vite:r.vite,yes:r.yes===!0})});export{Br as execute,vr as isTemplate,B as resolveTemplateSource,fr as runInitCommand};