UNPKG

@lunora/cli

Version:

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

1,312 lines • 107 kB
import{existsSync as m,mkdirSync as M,writeFileSync as v,readFileSync as x,appendFileSync as ot,readdirSync as Y,rmSync as R,lstatSync as ge,mkdtempSync as we,copyFileSync as st,renameSync as lt}from"node:fs";import{tmpdir as ye}from"node:os";import{detectFramework as ct,DEV_VARS_EXAMPLE_FILE as be,DEV_VARS_FILE as dt,isInteractive as b,LUNA_ART as ut,LUNA_NAME as pt,LUNA_SIGNOFF as ht,BADGES as g,paintAnswer as ft,detectLintTools as mt,readProjectDependencies as ke,applyLintIgnores as vt,readProjectManifest as gt}from"@lunora/config";import{readJsonSync as wt,walkSync as yt}from"@visulima/fs";import{relative as D,join as u,dirname as C,basename as T,resolve as xe}from"@visulima/path";import{downloadTemplate as Se}from"giget";import{modify as bt,applyEdits as kt}from"jsonc-parser";import{join as J,dirname as xt}from"node:path";import{installArgsFor as Ce,execArgsFor as St,detectPackageManager as B,detectInstalledManagers as Ct,runScriptCommand as Lt,addArgsFor as At}from"@lunora/config/cli";import{d as Et}from"../packem_shared/command-B6vu47iR.mjs";import{EXIT_CODE as w}from"../packem_shared/EXIT_CODE-DVjUgIaG.mjs";import Mt from"magic-string";import{Project as It,SyntaxKind as y}from"ts-morph";import{P as X}from"../packem_shared/prompt-cancelled-BvsNxg_Q.mjs";import{l as Le,m as _t,n as Ae,o as Ee,a as Tt}from"../packem_shared/resolve-Bdc9KEUv.mjs";import{defaultSpawner as Me}from"../packem_shared/createRecordingSpawner-01ZngIwK.mjs";import{d as $t,e as Nt,f as Rt,a as Ie,t as O,g as _e,h as Ot,b as Te,i as $e,w as Pt,j as Dt,k as Ne,l as Bt}from"../packem_shared/tui-prompts-B3YwUhGw.mjs";import{A as Re,p as Ut,w as jt,M as Vt,r as Ft,E as Oe,k as Wt,e as Ht,c as Pe,l as De,i as zt,f as Kt}from"../packem_shared/storage-BLLoZRJa.mjs";import{r as qt}from"../packem_shared/commands-DC3ZX1yk.mjs";import{findWranglerFile as Gt,readWranglerJsonc as Yt,DEFAULT_OBSERVABILITY as Jt,CLOUDFLARE_TOOLCHAIN_VERSIONS as re}from"@lunora/config/cloudflare";import{logStep as ae}from"../packem_shared/createLogger-4TujevyB.mjs";import{r as Xt}from"../packem_shared/lint-ignore-report-DKZagpqk.mjs";import Qt from"node:dns/promises";const Q={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
        with:
          version: latest
`,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"}},E=(e,t)=>{const r=St(e,"lunora",t);return[r.command,...r.args].join(" ")},ne=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}`,Zt=e=>{const t=Q[e],r=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}\`
${r}
# 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:
${ne(t)}
      # Codegen + wrangler validation gate (no deploy) — fails fast on drift.
      - run: ${E(e,["prepare"])}
      - run: ${E(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:
${ne(t)}
      - run: ${E(e,["deploy","--preview"])}
`},er=e=>{const t=Q[e],r=[...t.gitlabProvisionScript,t.installCmd].map(a=>`    - ${a}`).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:
${r}

# 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.
    - ${E(e,["prepare"])}
    - ${E(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:
    - ${E(e,["deploy","--preview"])}
`},Be={github:{file:J(".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)"}},tr=(e,t)=>e==="github"?Zt(t):er(t),rr=e=>e==="github"||e==="gitlab",ar=(e,t,r,a={})=>{const n=J(e,Be[t].file);return m(n)&&a.overwrite!==!0?{path:n,skipped:!0,written:!1}:(M(xt(n),{recursive:!0}),v(n,tr(t,r),"utf8"),{path:n,skipped:!1,written:!0})},nr=(e,t,r,a,n={})=>{const i=Be[t];try{const s=ar(e,t,r,n);if(s.skipped)a.info(`--ci ${t}: ${i.file} already exists — left unchanged (re-run with overwrite to replace).`);else{const o=Ce(r),l=[o.command,...o.args].join(" "),{installCmd:c,lockfile:d}=Q[r];a.success(`--ci ${t}: wrote ${i.file}`),a.info(`--ci ${t}: set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as ${i.secretsHint} to enable deploys.`),a.info(`--ci ${t}: run \`${l}\` and commit ${d} before pushing — the pipeline runs \`${c}\`.`)}return s}catch(s){const o=s instanceof Error?s.message:String(s);return a.warn(`--ci ${t}: could not write ${i.file} (${o})`),{path:J(e,i.file),skipped:!1,written:!1}}},ir={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"},or=e=>{const t=ct(e);return{...t,adapter:ir[t.framework]}},N="lunora()",ie='import { lunora } from "@lunora/vite";',sr="@lunora/vite",lr="lunora",cr=e=>{for(const t of e.getStatements()){if(t.getKind()!==y.ExportAssignment)continue;const r=t.asKindOrThrow(y.ExportAssignment).getExpression();if(r.getKind()!==y.CallExpression)continue;const a=r.asKindOrThrow(y.CallExpression);if(a.getExpression().getText()!=="defineConfig")continue;const n=a.getArguments()[0];if(n?.getKind()===y.ObjectLiteralExpression)return n.asKindOrThrow(y.ObjectLiteralExpression)}},dr=e=>{for(const t of e.getStatements()){if(t.getKind()!==y.ExportAssignment)continue;const r=t.asKindOrThrow(y.ExportAssignment).getExpression();if(r.getKind()===y.ObjectLiteralExpression)return r.asKindOrThrow(y.ObjectLiteralExpression)}},ur=e=>e.getDescendantsOfKind(y.CallExpression).some(t=>t.getExpression().getText()===lr),pr=e=>e.getImportDeclarations().some(t=>t.getModuleSpecifierValue()===sr),hr=e=>new It({compilerOptions:{allowJs:!0},useInMemoryFileSystem:!0}).createSourceFile("vite.config.ts",e,{overwrite:!0}),fr=(e,t)=>{const r=t.getImportDeclarations().at(-1)?.getEnd()??0;r===0?e.prepend(`${ie}
`):e.appendLeft(r,`
${ie}`)},mr=(e,t)=>{const r=t.getProperty("plugins");if(r===void 0){const s=t.getProperties(),o=t.getStart()+1;if(s.length===0){e.appendLeft(o,` plugins: [${N}] `);return}const l=s[0];if(l===void 0)return"could not locate the start of the Vite config object's first property";e.appendLeft(l.getStart(),`plugins: [${N}],
    `);return}const a=r.getDescendantsOfKind(y.ArrayLiteralExpression)[0];if(a===void 0)return"the Vite config's `plugins` is not an array literal — add `lunora()` to it by hand";const n=a.getElements();if(n.length===0){e.appendLeft(a.getStart()+1,N);return}const i=n[0];if(i===void 0)return"could not locate the first entry of the Vite config's `plugins` array";e.appendLeft(i.getStart(),`${N}, `)},Ue=e=>{const t=hr(e);if(ur(t))return{changed:!1,code:e,reason:"lunora plugin already present"};const r=cr(t)??dr(t);if(r===void 0)return{changed:!1,code:e,reason:"could not locate a Vite config plugins array to patch"};const a=new Mt(e);pr(t)||fr(a,t);const n=mr(a,r);return n!==void 0?{changed:!1,code:e,reason:n}:{changed:!0,code:a.toString()}},vr=/enotfound|eai_again|econnrefused|etimedout|network|fetch failed|getaddrinfo/u,gr=/404|not found|could not find|no such/u,wr=(e,t)=>{const r=e instanceof Error?e.message:String(e),a=r.toLowerCase(),n=`failed to download template "${t.templateType}" from ${t.remote}: ${r}`;return gr.test(a)?{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}: ${r}`}:vr.test(a)?{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:n}:{hints:["If this is a network/offline issue, scaffold from a local root with `lunora init --from <dir>`."],message:n}},je=[{description:"No sign-in yet; add it later with `lunora add auth`",label:"None",value:"none"},{description:"Users and sessions in your own D1, via better-auth (@lunora/auth)",label:"Lunora auth",value:"lunora"},{description:"Clerk owns the session; the Worker verifies its session token",label:"Clerk",value:"clerk"},{description:"WorkOS AuthKit owns the session; the Worker verifies its access token",label:"WorkOS AuthKit",value:"workos"},{description:"Auth0 owns the session; the Worker verifies its access token",label:"Auth0",value:"auth0"}],oe=je.map(e=>e.value),Z={auth0:{clientSnippet:["// @auth0/auth0-react: <Auth0Provider authorizationParams={{ audience: AUTH0_AUDIENCE }} useRefreshTokens>","// const { getAccessTokenSilently, isLoading, user } = useAuth0();","// In a useEffect over [client, getAccessTokenSilently, isLoading, user?.sub]:","if (isLoading) return;","if (!user) return client.setAuthToken(null);","const fetchToken = () => getAccessTokenSilently().catch((error) => {",'    if (["login_required", "missing_refresh_token"].includes(error?.error)) return null; // signed out',"    throw error;","});","return setAuthTokenProvider(client, fetchToken, { subject: user.sub });"].join(`
`),envVars:["AUTH0_DOMAIN","AUTH0_AUDIENCE"],identity:"auth0Identity",identityFile:"auth0",item:"auth-auth0-jwt"},clerk:{clientSnippet:["// @clerk/react: const { getToken, isLoaded, isSignedIn, userId } = useAuth();","// In a useEffect over [client, getToken, isLoaded, isSignedIn, userId]:","if (!isLoaded) return;","if (!isSignedIn) return client.setAuthToken(null);","return setAuthTokenProvider(client, getToken, { subject: userId });"].join(`
`),envVars:["CLERK_PUBLISHABLE_KEY","CLERK_AUTHORIZED_PARTIES"],identity:"clerkIdentity",identityFile:"clerk",item:"auth-clerk-jwt"},workos:{clientSnippet:["// @workos-inc/authkit-react: const { getAccessToken, isLoading, user } = useAuth();","// In a useEffect over [client, getAccessToken, isLoading, user?.id]:","if (isLoading) return;","if (!user) return client.setAuthToken(null);","const fetchToken = () => getAccessToken().catch((error) => {","    if (error instanceof LoginRequiredError) return null; // signed out","    throw error;","});","return setAuthTokenProvider(client, fetchToken, { subject: user.id });"].join(`
`),envVars:["WORKOS_CLIENT_ID"],identity:"workosIdentity",identityFile:"workos",item:"auth-workos-jwt"}},yr=e=>Object.hasOwn(Z,e),br=(e,t)=>{const r=e.trim().toLowerCase();return oe.includes(r)?r:(t(`init: unknown --auth "${e}" — expected ${oe.join(" | ")}; skipping auth.`),"none")},kr=["lunora/app.ts","src/app.ts","lunora/server.ts","src/server.ts","src/server/index.ts","src/server/index.tsx","src/index.ts","src/worker.ts"],xr="virtual:lunora/worker",Sr=["lunora.config.ts","lunora.config.mts","lunora.config.js","lunora.config.mjs"],Cr=["src","app","pages","plugins"],Lr=/\.(?:[jt]sx?|svelte|vue)$/u,H="new LunoraClient({",se=/\n([\t ]*)\.build(?:FrameworkWorker)?\(/uy,Ar=new Set(["(","[","{"]),Er=new Set([")","]","}"]),Mr=(e,t)=>{const r=e[t];if(r==="/"&&e[t+1]==="/"){const n=e.indexOf(`
`,t);return n===-1?e.length:n}if(r==="/"&&e[t+1]==="*"){const n=e.indexOf("*/",t+2);return n===-1?e.length:n+2}if(r!=='"'&&r!=="'"&&r!=="`")return t;let a=t+1;for(;a<e.length&&e[a]!==r;)a+=e[a]==="\\"?2:1;return a+1},Ir=(e,t)=>{let r=0,a=t;for(;a<e.length;){const n=Mr(e,a);if(n!==a){a=n;continue}const i=e[a]??"";se.lastIndex=a;const s=r===0?se.exec(e)??void 0:void 0;if(s!==void 0)return{at:a,indent:s[1]??"    "};if(r===0&&i===";")return{at:a,indent:"    "};r+=Ar.has(i)?1:0,r-=Er.has(i)?1:0,a+=1}},_r=(e,t,r)=>{const a=D(C(u(e,t)),u(e,"lunora","identity",r.identityFile));return`${a.startsWith(".")?a:`./${a}`}.js`},Tr=/\bresolveIdentity\b/u,$r=/^import[^"']*["'][^"'\n]*["'][^\n]*\n/u,Nr=(e,t,r,a)=>{const n=r.indexOf("defineApp<"),i=n===-1?void 0:Ir(r,n);if(i===void 0)return;if(r.includes(`${a.identity}(env)`))return{outcome:"already-wired"};if(Tr.test(r))return{outcome:"has-resolver"};const{at:s,indent:o}=i,l=`
${o}// Verify the provider's token on every request and feed it into ctx.auth (memoized for 5 s).
${o}.extend((env) => ({ resolveIdentity: ${a.identity}(env) }))`,c=`import { ${a.identity} } from "${_r(e,t,a)}";
`,d=r.lastIndexOf(`
import `,n),h=d===-1?void 0:$r.exec(r.slice(d+1)),f=h==null?0:d+1+h[0].length;return{outcome:"wired",patched:r.slice(0,f)+c+r.slice(f,s)+l+r.slice(s)}},Rr=(e,t)=>{for(const r of kr){const a=u(e,r),n=m(a)?Nr(e,r,x(a,"utf8"),t):void 0;if(n?.outcome==="wired"&&v(a,n.patched),n!==void 0)return{file:r,outcome:n.outcome}}},Or=e=>{const t=Gt(e);return t!==void 0&&Yt(t).parsed?.main===xr},Pr=(e,t)=>{if(!(!Or(e)||Sr.some(r=>m(u(e,r)))))return v(u(e,"lunora.config.ts"),`import type { AppBuilder, LunoraConfig } from "./lunora/_generated/app";
import { ${t.identity} } from "./lunora/identity/${t.identityFile}";

export default {
    // Verify the provider's token on every request and feed it into ctx.auth (memoized for 5 s).
    app: (app: AppBuilder<object>): AppBuilder<object> => app.extend((env) => ({ resolveIdentity: ${t.identity}(env) })),
} satisfies LunoraConfig<object>;
`),"lunora.config.ts"},Ve=(e,t=0)=>t>3||!m(e)?[]:Y(e,{withFileTypes:!0}).flatMap(r=>{const a=u(e,r.name);return r.isDirectory()?r.name==="node_modules"?[]:Ve(a,t+1):Lr.test(r.name)?[a]:[]}),Dr=e=>Cr.flatMap(t=>Ve(u(e,t))).filter(t=>{const r=x(t,"utf8");return!r.includes(H)||r.includes("authTokenOnUpgrade")?!1:(v(t,r.replaceAll(H,`${H} authTokenOnUpgrade: true,`)),!0)}),Br=(e,t)=>{const r=u(e,be),a=u(e,dt),n=m(r)?x(r,"utf8"):"",i=m(a)?x(a,"utf8").split(`
`):[],s=t.envVars.filter(c=>!n.split(`
`).some(d=>d.startsWith(`${c}=`)));if(s.length===0)return!1;const o=s.map(c=>i.find(d=>d.startsWith(`${c}=`))??`${c}=`),l=n.length===0||n.endsWith(`
`)?"":`
`;return ot(r,`${l}# ${t.item}: the identity provider the Worker verifies tokens from.
${o.join(`
`)}
`),!0},Ur=(e,t,r)=>{const a=Z[t],n=()=>{const s=Pr(e,a);return s===void 0?void 0:{file:s,outcome:"wired"}},i=Rr(e,a)??n();i===void 0?r.warn(`auth: add \`.extend((env) => ({ resolveIdentity: ${a.identity}(env) }))\` to your app builder (lunora.config.ts \`app\` hook or the Worker entry) — ${a.identity} is in lunora/identity/${a.identityFile}.ts.`):i.outcome==="already-wired"?r.info(`auth: ${i.file} already wires ${a.identity}; left as is.`):i.outcome==="has-resolver"?r.warn(`auth: ${i.file} already sets resolveIdentity, so it was left as is. Compose the two: \`resolveIdentity: composeIdentityResolvers([${a.identity}(env), <your resolver>])\` — ${a.identity} is in lunora/identity/${a.identityFile}.ts.`):r.success(`auth: wired ${a.identity} as resolveIdentity in ${i.file}`),Br(e,a)&&r.success(`auth: listed ${a.envVars.join(", ")} in ${be}`);for(const s of Dr(e))r.success(`auth: turned on authTokenOnUpgrade in ${D(e,s)}`);r.info(`auth: hand the provider's token to Lunora once signed in (setAuthTokenProvider from lunorash/client or @lunora/client):
${a.clientSnippet}
Fill in the env vars the item wrote to .dev.vars.`)},P=async(e,t,r)=>{if(b()){await Nt(g[e],t,r);return}if(process.stdout.write(`
`),r===void 0||r===""){ae(e,t);return}const a=r.split(`
`).map(n=>ft(n)).join(`
`);ae(e,`${t}
${a}`)},jr=async e=>{if(b()){await $t();return}e.info(`
${ut}
${pt}: ${ht}`)},Fe=[{description:"LLMs via Workers AI (summarize, generate, stream)",label:"AI",value:"ai"},{description:"Sign-up / sign-in with @lunora/auth (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"}],le=Fe.map(e=>e.value),ce=Fe.filter(e=>e.value!=="auth"),We=new Set(["auth","auth-ui"]),Vr=e=>e==="email"?Oe:e,Fr=(e,t)=>{const r=[];for(const a of e.split(",").map(n=>n.trim()).filter(Boolean))le.includes(a)?r.includes(a)||r.push(a):t(`init: unknown --add feature "${a}" — expected ${le.join(" | ")}; skipping.`);return r},He=e=>e.resolveAuthUiItem===void 0?"auth-ui-react":e.resolveAuthUiItem(),Wr=async e=>{const t=await Ht(e.select),r=await Pe(e.text,e.projectName);return{label:"auth",names:[t],transformManifest:a=>De(a,r)}},Hr=async e=>{const t=await e.text(Vt,{placeholder:"you@yourdomain.com"}),r=Ft(t,a=>{e.logger.warn(a)});return{label:"email",names:[Oe],transformManifest:r===void 0?void 0:a=>Wt(a,r)}},zr=async(e,t)=>{const r=He(e);if(r===void 0){e.logger.error(`init: ${Re}`);return}if(t)return{label:"auth-ui",names:[r]};const a=await Pe(e.text,e.projectName);return{label:"auth-ui",names:[r],transformManifest:n=>De(n,a)}},Kr=async e=>{const t=await Ut(e.text,e.projectName);return{label:"storage",names:["storage"],transformManifest:r=>jt(r,t)}},qr=(e,t)=>{const r=[];let a=!1;for(const n of t){if(n!=="auth-ui"){r.push({label:n,names:[Vr(n)]});continue}const i=He(e);if(i===void 0){e.logger.error(`init: ${Re}`),a=!0;continue}r.push({label:n,names:[i]})}return{plans:r,refused:a}},ze=e=>e!==void 0&&yr(e)?{externalAuth:e,label:e,names:[Z[e].item]}:void 0,Gr={auth:Wr,"auth-ui":async(e,t)=>zr(e,t.includes("auth")),email:Hr,storage:Kr},Yr=async e=>{const t=ze(e.auth),r=e.auth!==void 0&&e.auth!=="lunora",a=new Set(r?(e.preselected??[]).filter(l=>We.has(l)):[]),n=(e.preselected??[]).filter(l=>!a.has(l));for(const l of a)e.logger.warn(`--auth ${String(e.auth)} rules out Lunora auth; ignoring \`${l}\` from --add.`);e.auth==="lunora"&&!n.includes("auth")&&n.unshift("auth");const{plans:i,refused:s}=qr(e,n);return t!==void 0&&i.unshift(t),await e.applyAll(i)&&!s&&a.size===0},de=async e=>{if(e.preselected!==void 0&&e.preselected.length>0||e.auth!==void 0&&!e.interactive)return Yr(e);if(!e.interactive)return 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>`."),!0;const t=e.auth??await e.selectAuth("Which auth do you want?",je,{default:"none"})??"none",r=ze(t),a=t==="lunora"?ce:ce.filter(o=>!We.has(o.value)),n=await e.multiSelect("Which features do you want to add?",a,{defaults:[]}),i=t==="lunora"&&!n.includes("auth")?["auth",...n]:n;if(i.length===0&&r===void 0)return!0;const s=r===void 0?[]:[r];for(const o of i){const l=Gr[o],c=l?await l(e,i):{label:o,names:[o]};c!==void 0&&s.push(c)}return await e.applyAll(s),!0},Jr=[{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"}],Xr="Which linter/formatter do you use? We'll tell it to skip Lunora's generated files.",Qr=async e=>{const t=e.interactive?await e.multiSelect(Xr,Jr,{defaults:e.detected}):[...e.detected];if(t.length===0)return[];const r=e.apply(t);return Xt(r,e.logger),r},$="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",_=`/* 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; } }
`,Zr=`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="${$}" 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>
    );
}
`,ea=`<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="${$}" 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>
`,ta=`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="${$}" 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>
    );
}
`,ra=`<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="${$}" 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>
`,aa=`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="${$}" 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.subscribe(api.messages.list, { channelId: "channel:demo" }, (result) => {
    count.textContent = String(result.messages.length);
});
`,U="const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;",na=`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.
${U}
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>,
);
`,ia=`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.
${U}
createApp(App).use(createLunora(new LunoraClient({ url }))).mount("#app");
`,oa=`import "./index.css";

import { LunoraProvider } from "@lunora/solid";
import { LunoraClient } from "lunorash/client";
import { render } from "solid-js/web";

import App from "./App";

${U}
const client = new LunoraClient({ url });
const root = document.getElementById("root");

render(
    () => (
        <LunoraProvider client={client}>
            <App />
        </LunoraProvider>
    ),
    root!,
);
`,sa=`<script lang="ts">
    import { setLunoraClient } from "@lunora/svelte";
    import { LunoraClient } from "lunorash/client";

    import App from "./App.svelte";

    ${U}
    setLunoraClient(new LunoraClient({ url }));
<\/script>

<App />
`,la=`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;
`,j={react:{adapter:"@lunora/react",createViteTemplate:"react-ts",files:[{contents:na,path:"src/main.tsx"},{contents:Zr,path:"src/App.tsx"},{contents:_,path:"src/index.css"}],label:"React"},solid:{adapter:"@lunora/solid",createViteTemplate:"solid-ts",files:[{contents:oa,path:"src/index.tsx"},{contents:ta,path:"src/App.tsx"},{contents:_,path:"src/index.css"}],label:"Solid"},svelte:{adapter:"@lunora/svelte",createViteTemplate:"svelte-ts",files:[{contents:sa,path:"src/Root.svelte"},{contents:la,path:"src/main.ts"},{contents:ra,path:"src/App.svelte"},{contents:_,path:"src/app.css"}],label:"Svelte"},vanilla:{adapter:"lunorash/client",createViteTemplate:"vanilla-ts",files:[{contents:aa,path:"src/main.ts"},{contents:_,path:"src/style.css"}],label:"Vanilla"},vue:{adapter:"@lunora/vue",createViteTemplate:"vue-ts",files:[{contents:ia,path:"src/main.ts"},{contents:ea,path:"src/App.vue"},{contents:_,path:"src/style.css"}],label:"Vue"}},V=e=>Object.hasOwn(j,e),ca=`/**
 * Rate-limit schema extension + plugin.
 *
 * Defines the \`ratelimit_buckets\` table used by \`createDbStore\` for durable,
 * DO-backed rate limiting. Included automatically in every Lunora project.
 */
import type { RateLimitConfigMap } from "lunorash/ratelimit";
import { createDbStore, RateLimiter } from "lunorash/ratelimit";
import type { Middleware } from "lunorash/server";
import { definePlugin, defineSchemaExtension, defineTable, v } from "lunorash/server";

/**
 * Named limits this app enforces. This is the one place they live — add your own
 * and reference them by name from \`rateLimit(limiter, "<name>", …)\` (see
 * \`lunora/messages.ts\`).
 */
export const limits = {
    /** Chat writes: 10 per caller per minute, refilling continuously over 60s. */
    send: { kind: "token bucket", period: 60_000, rate: 10 },
} as const satisfies RateLimitConfigMap;

/** The limit names you've configured above. */
export type LimitName = keyof typeof limits;

/**
 * Build a durable {@link RateLimiter} bound to this request's \`ctx.db\`. State is
 * persisted in the \`ratelimit_buckets\` table, so limits survive Durable Object
 * hibernation/eviction and are shared across every call into the same DO. Each
 * \`get\`→\`set\` runs under the DO input gate, so the read-modify-write is atomic.
 */
export const makeRateLimiter = (ctx: { db: unknown }): RateLimiter<LimitName> =>
    new RateLimiter<LimitName>({
        config: limits,
        // The merged table name carries the plugin prefix.
        store: createDbStore({ db: ctx.db as never, table: "ratelimit_buckets" }),
    });

/**
 * Middleware that injects \`ctx.api.ratelimit\` — a per-request limiter helper —
 * for any procedure that opts in with \`.use(ratelimit.middleware)\`. Convention
 * (see \`definePlugin\`): helpers hang off \`ctx.api.<key>\`.
 */
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) },
        },
    });

/**
 * The rate-limit plugin: a schema extension (\`ratelimit_buckets\`) + middleware
 * (\`ctx.api.ratelimit\`). \`lunora/schema.ts\` wires the extension in with
 * \`.extend(ratelimit.extension)\`; attach the middleware yourself with
 * \`c.mutation.use(ratelimit.middleware)\`.
 */
export const ratelimit = definePlugin("ratelimit", {
    extension: defineSchemaExtension("ratelimit", {
        tables: {
            // Bare name — auto-prefixes to \`ratelimit_buckets\` at merge time.
            // \`externallyManaged\`: rows are written by \`createDbStore\` (via
            // \`makeRateLimiter\`), not a discoverable \`ctx.db.insert(...)\`, so the
            // \`table_without_insert\` advisor correctly skips it.
            buckets: defineTable({
                key: v.string(),
                value: v.number(),
                ts: v.number(),
                prev: v.optional(v.number()),
            })
                .index("by_key", ["key"])
                .index("by_ts", ["ts"])
                .externallyManaged(),
        },
    }),
    middleware,
});
`,da=`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);
`,ua=`import { rateLimit } from "lunorash/ratelimit";

import { makeRateLimiter } from "./ratelimit/schema.js";
import type { MutationCtx } from "#lunora/_generated/server.js";
import { mutation, query, v } from "#lunora/_generated/server.js";

/**
 * The limiter comes from \`lunora/ratelimit/schema.ts\` — that file owns the named
 * limits and the durable store, so tuning \`send\` there is what changes this
 * procedure's budget. Build it from the generated \`MutationCtx\` so
 * \`rateLimit(limiter, …)\` infers the full procedure context: \`ctx.auth\`/\`ctx.ip\`
 * in the key callback and a typed \`ctx.db\` in the handler both depend on it.
 */
const limiter = (ctx: MutationCtx) => makeRateLimiter(ctx);

/**
 * One bucket per caller, not one bucket for the whole app.
 *
 * \`ctx.auth.userId\` is \`null\` until you wire auth (\`lunora registry add auth\`),
 * so the \`ctx.ip\` fallback is what actually keys the limit in a fresh project.
 * Keying on \`"anon"\` alone would let one script exhaust the budget for every user.
 *
 * \`ctx.ip\` is Cloudflare's server-side \`CF-Connecting-IP\` and is populated ONLY
 * when the app runs on Cloudflare, where the edge stamps that header over
 * anything the client sent. Deploy this to another target and \`ctx.ip\` is
 * \`undefined\` by design — the runtime will not pass on a header the caller can
 * type — so every anonymous caller falls back to the shared \`"anon"\` bucket.
 * Off Cloudflare, key on something the caller cannot choose (an API key, a
 * signed session) before relying on this limit.
 */
const byCaller = { key: (ctx: { auth: { userId?: null | string }; ip?: string }): string => ctx.auth.userId ?? ctx.ip ?? "anon" };

export const list = query.input({ channelId: v.string().max(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().max(256), text: v.string().max(4096) })
    .use(rateLimit(limiter, "send", byCaller))
    .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 };
    });
`,pa=`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;
`,ha=`{ ${Object.entries(Jt).map(([e,t])=>`${JSON.stringify(e)}: ${JSON.stringify(t)}`).join(", ")} }`,fa=`{
    "$schema": "node_modules/wrangler/config-schema.json",
    "name": "__NAME__",
    "main": "src/server.ts",
    "compatibility_date": "2026-10-01",
    "compatibility_flags": ["nodejs_compat"],
    "durable_objects": {
        "bindings": [{ "name": "SHARD", "class_name": "ShardDO" }],
    },
    "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ShardDO"] }],
    "observability": ${ha},
}
`,ue=[".wrangler",".env",".env.*","!.env.example",".dev.vars",".dev.vars.*","!.dev.vars.example",".lunora/",".lunora-cache","lunora/_generated"],ma=`# 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
`,va={"@cloudflare/workers-types":re["@cloudflare/workers-types"],wrangler:re.wrangler},L=(e,t,r,a)=>{const n=u(e,t);M(C(n),{recursive:!0}),v(n,r,"utf8"),a.push(n)},ga=/\r?\n/,wa=e=>{const t=u(e,".gitignore"),r=m(t)?x(t,"utf8"):"",a=new Set(r.split(ga)),n=ue.filter(o=>!a.has(o));if(n.length===0)return;const i=n.some(o=>!o.startsWith("!"))?[...n.filter(o=>!o.startsWith("!")),...ue.filter(o=>o.startsWith("!"))]:n,s=r.length>0&&!r.endsWith(`
`)?`
`:"";v(t,`${r}${s}
# Lunora
${i.join(`
`)}
`,"utf8")},F=e=>e==="lunorash"||e.startsWith("@lunora/"),ya=(e,t,r,a)=>F(e)?a.get(e)??r:t,A=(e,t,r)=>({...e,[t]:r}),pe=(e,t,r)=>Object.fromEntries(Object.entries(e).map(([a,n])=>[a,ya(a,n,t,r)])),ba=async(e,t,r,a)=>{const n=u(e,"package.json"),i=wt(n);let s=A(i.dependencies??{},"lunorash",a);s=A(s,"@lunora/ratelimit",a),r.adapter.startsWith("@lunora/")&&(s=A(s,r.adapter,a));for(const[d,h]of Object.entries(r.extraDependencies??{}))s=A(s,d,h);let o=A(i.devDependencies??{},"@lunora/vite",a);o=A(o,"@lunora/studio",a);for(const[d,h]of Object.entries(va))o=A(o,d,h);const l=[...Object.keys(s),...Object.keys(o)].filter(d=>F(d)),c=await Le(l,a);i.name=t,i.imports={...i.imports,"#lunora/*":"./lunora/*"},i.dependencies=pe(s,a,c),i.devDependencies=pe(o,a,c),i.scripts={...i.scripts,codegen:"lunora codegen",deploy:"lunora deploy"},v(n,`${JSON.stringify(i,void 0,4)}
`,"utf8")},ka=["vite.config.ts","vite.config.mts","vite.config.js","vite.config.mjs"],Ke=e=>ka.map(t=>u(e,t)).find(t=>m(t)),xa=(e,t)=>{const r=Ke(e);if(r===void 0){t.warn("overlay: no vite.config found in the create-vite base — add `lunora()` to your Vite plugins manually.");return}const a=Ue(x(r,"utf8"));a.changed&&v(r,a.code,"utf8")},Sa=async e=>{const{adapter:t,distTag:r,logger:a,name:n,target:i}=e,s=[];L(i,u("lunora","ratelimit","schema.ts"),ca,s),L(i,u("lunora","schema.ts"),da,s),L(i,u("lunora","messages.ts"),ua,s),L(i,u("src","server.ts"),pa,s),L(i,"wrangler.jsonc",fa.replaceAll("__NAME__",n),s),L(i,".env.example",ma,s);for(const o of t.files)L(i,o.path,o.contents,s);return xa(i,a),await ba(i,n,t,r),wa(i),s},Ca=["lunar","silver","silent","waning","waxing","crescent","cosmic","stellar","orbital","gibbous","twilight","midnight","shimmering","drifting","weightless"],La=["moon","tide","crater","comet","eclipse","halo","orbit","nebula","voyager","lander","rover","beacon","harbor","meadow","fox"],he=e=>e[Math.floor(Math.random()*e.length)],Aa=()=>`${he(Ca)}-${he(La)}`,Ea=async()=>Qt.lookup("github.com").then(()=>!0,()=>!1),Ma=/^(?:gh|github):([^/]+)\/([^#/]+)(?:\/[^#]*)?(?:#(.+))?$/,qe=e=>{const t=Ma.exec(e);if(t===null)return;const[,r,a,n]=t;if(!(r===void 0||a===void 0))return{owner:r,ref:n??"HEAD",repo:a}},Ia=async e=>{const t=qe(e);if(t===void 0)return;const r=`https://codeload.github.com/${t.owner}/${t.repo}/tar.gz/${t.ref}`;try{const a=await fetch(r,{method:"HEAD"});return a.status===404?!1:a.ok?!0:void 0}catch{return}},Ge=async e=>e.isLocal||!(e.source===void 0||qe(e.source)!==void 0)?!0:await Ea()?e.source!==void 0&&await Ia(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),S={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?"},_a=new Set([".gitignore",".html",".js",".json",".jsonc",".md",".mjs",".ts",".tsx"]),Ta=`import { defineConfig } from "vite";
import { lunora } from "@lunora/vite";

export default defineConfig({ plugins: [lunora()] });
`,$a=`import { defineSchema, defineTable, v } from "@lunora/server";

export default defineSchema({
    messages: defineTable({
        channelId: v.string(),
        text: v.string(),
    })
        .shardBy("channelId")
        .index("by_channel", ["channelId"]),
});
`,Na=`import { mutation, query, v } from "./_generated/server";

export const list = query
    .input({ channelId: v.string(), limit: v.optional(v.number()) })
    .query(async ({ args }) => {
        return { channelId: args.channelId, limit: args.limit ?? 50, messages: [] };
    });

export const send = mutation
    .input({ channelId: v.string(), text: v.string() })
    .mutation(async ({ args }) => {
        return { channelId: args.channelId, text: args.text };
    });
`,Ra="gh:anolilab/lunora/templates",Oa=e=>{const t=e.lastIndexOf(".");return t===-1?!1:_a.has(e.slice(t))},Pa=/^[a-z0-9_][a-z0-9\-_]*$/u,Da=/([a-z0-9])([A-Z])/gu,Ba=/[^a-z0-9_]/gu,Ua=e=>{const t=e.replaceAll(Da,"$1-$2").toLowerCase().replaceAll(Ba,"-").split("-").filter(r=>r.length>0);return t.length>0?t.join("-"):"my-app"},ja=(e,t)=>e.replaceAll("{{name}}",t),Va=async(e,t)=>{const r=new Set;for(const a of e)if(T(a)==="package.json")for(const n of Object.keys(ke(C(a))))F(n)&&r.add(n);return Le(r,t)},Fa=(e,t,r)=>{let a;try{a=JSON.parse(e)}catch{return e}let n=e;for(const i of["dependencies","devDependencies"])for(const s of Object.keys(a[i]??{})){if(!F(s))continue;const o=r.get(s)??t,l=bt(n,[i,s],o,{formattingOptions:{insertSpaces:!0,tabSize:4}});n=kt(n,l)}return n},Wa=["@parcel/watcher","esbuild","lmdb","msgpackr-extract","rs-module-lexer","sharp","unrs-resolver","vue-demi","workerd"],Ha=["cpu-features","protobufjs","ssh2"],Ye="pnpm-workspace.yaml",za=()=>["# 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:",...Wa.map(e=>`    '${e}': true`),...Ha.map(e=>`    '${e}': false`),""].join(`
`),Je=e=>{const t=u(e,Ye);m(t)||v(t,za(),"utf8")},W=e=>{const t=[];for(const r of yt(e,{includeDirs:!1,includeFiles:!0}))ge(r.path).isSymbolicLink()||t.push(r.path);return t},fe="vitejs/vite",me=(e,t)=>{for(const r of W(e)){const a=u(t,D(e,r));M(C(a),{recursive:!0}),st(r,a)}},Xe=async(e,t,r)=>{const a=W(e),n=[],i=Ae(),s=await Va(a,i);for(const o of a){const l=D(e,o),c=u(t,l);M(C(c),{recursive:!0});const d=x(o);let h=Oa(o)?ja(d.toString("utf8"),r):void 0;h!==void 0&&T(o)==="package.json"&&(h=Fa(h,i,s)),h===void 0?v(c,d):v(c,h,"utf8"),n.push(c)}return n},q=(e,t,r)=>t!==void 0&&t.length>0?t:`${Ra}/${e}#${Ee(r)}`,Ka=e=>e.includes("..")?!1:e.startsWith("gh:")||e.startsWith("github:")||e.startsWith("https://"),I=(e,t)=>{e.info(`[dry-run] would ${t}`)},Qe=(e,t,r)=>(e.error(`init: the template at ${r} contains no files — nothing was scaffolded. Check the template name, \`--source\` layout and \`--ref\`.`),{code:w.NOT_FOUND,files:[],target:t}),Ze="Template files copied.",ee=(e,t,r)=>{b()&&process.stdout.write(`
`),e.success(`scaffolded ${String(t.length)} files into ${r}`)},qa=(e,t)=>{const{args:r,command:a}=At(e,t);return`${a} ${r.join(" ")}`},et=(e,t)=>{let r=xe(e);for(;;){if(t(r))return!0;const a=C(r);if(a===r)return!1;r=a}},Ga=e=>m(u(e,Ye))?!0:gt(e)?.workspaces!==void 0,te=e=>et(e,Ga),Ya=e=>et(e,t=>m(u(t,".git"))),Ja=async(e,t)=>{if(e.yes===!0||!b()||Ya(C(t)))return;if(!await Te(S.git,{badge:g.git,defaultYes:!1})){await $e("Sounds good! You can always run git init manually.");return}if(e.dryRun===!0){I(e.logger,"initialize a git repository");return}const r=e.spawner??Me;(await Pt("Initializing a git repository…",()=>r({args:["init"],command:"git",cwd:t}))).code===0?await P("git","Initialized an empty git repository."):e.logger.warn("`git init` failed — initialize it yourself later with `git init`.")},Xa=async(e,t,r,a)=>{const n=[{code:`cd ./${e}`,lead:"Enter your project directory using"}];t===void 0&&n.push({code:`${r} install`,lead:"Install dependencies with",tail:a?" from the workspace root":void 0}),n.push({code:Lt(r,"dev"),lead:"Run",tail:" to start the dev server."},{code:"lunora add",lead:"Add features like auth or storage using"});const i=[{code:"https://lunora.sh/docs",lead:"Read the docs at"},{code:"https://lunora.sh/chat",lead:"Stuck? Join the chat at"}];if(b()){await Dt(g.next,S.nextHeader,n,i);return}const s=n.map(o=>`${o.lead} ${o.code}${o.tail??""}`);s.push("",...i.map(o=>`${o.lead} ${o.code}${o.tail??""}`)),await P("next",S.nextHeader,s.join(`
`))},Qa=e=>e.yes!==!0&&(e.installPrompt!==void 0||b()),Za=async(e,t)=>{if(!Qa(e)||te(C(t)))return;const r=Ct(e.packageManagerProbe),[a]=r;if(a===void 0)return;if(!await(e.installPrompt?.confirmInstall??(async()=>Te(S.install,{badge:g.deps,defaultYes:!0})))()){await $e("No problem! Remember to install dependencies after setup.");return}let i=a;if(r.length>1&&(i=e.installPrompt?await e.installPrompt.selectManager(r):await O(S.packageManager,r.map(d=>({label:d,value:d})),{badge:g.deps,default:a})??a),e.dryRun===!0){I(e.logger,`install dependencies with ${i}`);return}i==="pnpm"&&Je(t);const s=e.spawner??Me,{args:o,command:l}=Ce(i);await P("deps",`Installing dependencies with ${i}…`);const c=await s({args:o,command:l,cwd:t});if(c.code!==0){e.logger.warn(`\`${l} install\` exited with code ${String(c.code)} — run it yourself in ${T(t)}/.`);return}return await P("deps",`Dependencies installed with ${i}.`),i},en=async(e,t,r,a,n,i)=>{const s=u(e,t);if(!m(s))return n.error(`template not found in local source: ${s}`),{code:w.NOT_FOUND,files:[],target:r};const o=await Xe(s,r,a);return o.length===0?Qe(n,r,s):(i(),ee(n,o,r),{code:0,files:o,target:r})},tn=async e=>{const{logger:t,markComplete:r,name:a,ref:n,source:i,target:s,templateType:o}=e,l=we(u(ye(),"lunora-init-fetch-")),c=u(l,"template");try{const d=i!==void 0&&i.length>0?n:await Tt(n,t),h=q(o,i,d);let f,p=[];if(await Ne([{label:`${o} template fetched`,run:async()=>{f=await Se(h,{cwd:l,dir:c,force:!0,install:!1,silent:!0})}},{label:`files copied into ${a}/`,run:async()=>{p=await Xe(c,s,a)}}],{end:Ze,start:"Project initializing…"}),p.length===0)return Qe(t,s,f?.source??h);r();const k=W(c);return b()&&process.stdout.write(`
`),t.info(f?.commit?`template: ${f.source} @ ${f.commit} (${String(k.length)} files)`:`template: ${f?.source??h} (${String(k.length)} files)`),ee(t,p,s),{code:0,files:p,target:s}}catch(d){if(d instanceof X)throw d;const{hints:h,message:f}=wr(d,{ref:Ee(n),remote:q(o,i,n),templateType:o});t.error(f);for(const p of h)t.warn(p);return{code:1,files:[],target:s}}finally{R(l,{force:!0,recursive:!0})}},rn=e=>{for(const t of["_gitignore","_npmrc","_gitattributes"]){const r=u(e,t);m(r)&&lt(r,u(e,`.${t.slice(1)}`))}},an=async e=>{const{framework:t,logger:r,markComplete:a,name:n,overlayBaseFrom:i,target:s}=e,o=j[t],l=we(u(ye(),"lunora-vite-base-"));try{let c;if(i!==void 0&&(c=u(i,`template-${o.createViteTemplate}`),!m(c)))return r.error(`create-vite base not found on disk: ${c}`),{code:w.NOT_FOUND,files:[],target:s};const d=async()=>{if(c!==void 0){me(c,s);return}const f=u(l,"base"),p=await _t(fe,"main",r),k=`github:${fe}/packages/create-vite/template-${o.createViteTemplate}#${p}`;await Se(k,{cwd:l,dir:f,force:!0,install:!1,silent:!0}),rn(f),me(f,s)};let h=[];return await Ne([{label:`create-vite (${o.label}) base ready`,run:d},{label:`Lunora overlay applied (${o.label})`,run:async()=>{h=await Sa({adapter:o,distTag:Ae(),logger:r,name:n,target:s})}}],{end:Ze,start:"Project initializing…"}),a(),ee(r,h,s),{code:0,files:[...W(s)],target:s}}catch(c){if(c instanceof X)throw c;const d=c instanceof Error?c.message:String(c);return r.error(`failed to scaffold the ${o.label} base: ${d}`),{code:1,files:[],target:s}}finally{R(l,{force:!0,recursive:!0})}},G=(e,t,r,a,n)=>(n.error(`init --in-place: could not ${e} ${t}: ${r instanceof Error?r.message:String(r)}`),{code:1,files:[],target:a}),nn=(e,t)=>{const r=u(e,"vite.config.ts");try{v(r,Ta,"utf8")}catch(a){return G("write",r,a,e,t)}return t.success(`created ${r} with lunora() plugin`),{code:0,files:[r],target:e}},on=(e,t,r)=>{let a;try{a=x(e,"utf8")}catch(i){return G("read",e,i,t,r)}const n=Ue(a);if(!n.changed)return r.info(`${e}: ${n.reason??"no changes needed"}`),{code:0,files:[],target:t};try{v(e,n.code,"utf8")}catch(i){return G("write",e,i,t,r)}return r.success(`patched ${e} — added lunora() plugin`),{code:0,files:[e],target:t}},sn=(e,t)=>{const r=u(e,"lunora"),a=u(r,"schema.ts");if(m(a))return t.info(`lunora/ already present — left ${a} untouched`),[];const n=[];try{M(r,{recursive:!0}),v(a,$a,"utf8"),n.push(a);const i=u(r,"messages.ts");m(i)||(v(i,Na,"utf8"),n.push(i)),t.success(`scaffolded lunora/ (${String(n.length)} file(s))`)}catch(i){const s=i instanceof Error?i.message:String(i);t.error(`init --here: could not scaffold lunora/: ${s}`)}return n},tt=e=>e==="sveltekit"||e==="nuxt"||e==="astro",ln=(e,t,r)=>{const{adapter:a,class:n,framework:i}=e;r.info(""),r.info(`detected framework: ${i} (class ${n})`),r.info("next steps:");const s=[a,"@lunora/client","@lunora/runtime","@lunora/server"];tt(i)||s.push("@lunora/vite"),r.info(`  1. install the adapter:  ${qa(t,s)}`),r.info("  2. run codegen:          lunora codegen"),n==="A"?(r.info("  3. compose one worker:   wrap your worker entry with"),r.info("       createWorker({ httpRouter: <your framework SSR handler>, shardDO: ShardDO, ... })"),r.info(`  4. add the provider:     mount the ${a} provider in your root layout/route`),r.info("  5. make a loader live:   preloadQuery() in a loader, usePreloadedQuery() in the component"),r.info("     see https://lunora.sh/docs/frameworks/reactive-loaders")):n==="B"?(r.info("  3. inject Lunora:        mount Lunora realtime under /_lunora/* in your server hook"),r.info(`       (${i} owns its Cloudflare adapter — Lunora composes into its server entry)`),r.info(`  4. add the provider:     mount the ${a} provider in your root layout`),r.info("  5. read the guide:       https://lunora.sh/docs/frameworks/deploy")):(r.info("  3. add the provider:     wrap your app with the LunoraProvider from @lunora/react"),r.info("  4. read the guide:       https://lunora.sh/docs/frameworks/bring-your-framework")),r.info("")},cn=(e,t,r)=>{if(tt(t))return r.info(`${t} wires Lunora through its server entry, not a lunora() Vite plugin — leaving the Vite config alone (see next steps)`),{code:0,files:[],target:e};const a=Ke(e);return a===void 0?nn(e,r):on(a,e,r)},dn=(e,t)=>{const r=or(e),a=cn(e,r.framework,t);if(a.code!==0)return a;const n=sn(e,t);return ln(r,B(e),t),{code:0,files:[...a.files,...n],target:e}},un=e=>e.yes!==!0&&(e.prompt!==void 0||(e.interactive??b())),ve=async(e,t,r)=>{r.dryRun!==!0&&await Qr({apply:a=>vt(e,a),detected:mt(e),interactive:t,logger:r.logger,multiSelect:r.lintPrompt??((a,n,i)=>_e(a,n,{...i,badge:g.add}))})},pn=async(e,t)=>{const r=un(e),a=e.add===void 0?[]:Fr(e.add,o=>{e.logger.warn(o)}),n=async o=>{if(o.length===0)return!0;if(e.dryRun===!0)return I(e.logger,`add ${o.map(p=>p.externalAuth===void 0?p.label:`${p.label} (and wire resolveIdentity)`).join(", ")}`),!0;const l=[],c=b()?{error:p=>{l.push({level:"error",message:p})},info:()=>{},success:()=>{},warn:p=>{l.push({level:"warn",message:p})}}:e.logger,d=o.map(p=>({running:`adding ${p.label}…`,task:()=>qt({allowUnsafeSource:e.allowUnsafeSource,cwd:t,from:e.registryFrom,logger:c,names:[...p.names],ref:e.ref,source:e.registrySource,transformManifest:p.transformManifest,yes:!0})})),h=`added ${o.map(p=>p.label).join(", ")}`,f=await Bt(g.add,d,h);for(const{level:p,message:k}of l)e.logger[p](k);for(const[p,k]of o.entries())k.externalAuth!==void 0&&f[p]?.code===0&&Ur(t,k.externalAuth,e.logger);return f.every(p=>p.code===0)},i=e.auth===void 0?void 0:br(e.auth,o=>{e.logger.warn(o)}),s={applyAll:n,auth:i,interactive:r,logger:e.logger,multiSelect:e.prompt?.multiSelect??((o,l,c)=>_e(o,l,{...c,badge:g.add})),preselected:a.length>0?a:void 0,projectName:T(t),resolveAuthUiItem:()=>{const o=ke(t);if(!zt(o))return Kt(o)??"auth-ui-react"},select:e.prompt?.select??((o,l,c)=>O(o,l,{...c,badge:g.add})),selectAuth:e.prompt===void 0?(o,l,c)=>O(o,l,{...c,badge:g.add}):e.prompt.selectAuth??(()=>Promise.resolve("none")),text:e.prompt?.text??((o,l)=>Ie(o,{...l,badge:g.add}))};if(a.length>0||i!==void 0&&!r){const o=await de(s);return await ve(t,r,e),o}return b()&&await Ot(S.extras),await de(s),await ve(t,r,e),!0},z="react",rt=[{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:"Solid 2.0 SPA — the Solid 2 line (@solidjs/web, vite-plugin-solid 3)",label:"Solid 2",value:"solid-v2"},{description:"Svelte SPA — create-vite base + Lunora",label:"Svelte",value:"svelte"},{description:"React SPA on Rsbuild — the Worker runs under wrangler dev behind the dev server",label:"React · Rsbuild",value:"rspack-react"},{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 (React) on Rsbuild — SSR composed into the Lunora worker",label:"TanStack Start · React · Rsbuild",value:"tanstack-start-react-rspack"},{description:"SaaS kit — multi-tenant app on TanStack Start: organizations, projects, activity, admin",label:"SaaS kit",value:"saas"},{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:"Astro (islands) — single-worker, Lunora composed into the adapter 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 — single-worker, Lunora composed into the adapter 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"}],at=Object.keys(j).join("|"),nt=rt.filter(e=>!V(e.value)).map(e=>e.value),it=nt.join("|"),hn=e=>V(e)?{framework:e,kind:"overlay"}:{kind:"template",templateType:e},fn=async e=>e.vite!==void 0?{framework:e.vite,kind:"overlay"}:e.templateType!==void 0?{kind:"template",templateType:e.templateType}:!b()||e.yes===!0?{framework:z,kind:"overlay"}:hn(await O(S.framework,rt,{badge:g.tmpl,default:z})??z),mn=e=>{if(b()||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 <${at}>\` for an SPA, or \`-t <${it}>\` 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.`},vn=async(e,t,r,a,n)=>V(t)?await Ge({isLocal:e.overlayBaseFrom!==void 0,logger:e.logger})?(M(a,{recursive:!0}),an({framework:t,logger:e.logger,markComplete:n,name:r,overlayBaseFrom:e.overlayBaseFrom,target:a})):{code:1,files:[],target:a}:(e.logger.error(`init: unknown framework "${t}". Supported overlays: ${Object.keys(j).join(", ")}.`),{code:w.USAGE,files:[],target:a}),gn=async(e,t,r,a,n)=>e.from!==void 0?await en(e.from,t,a,r,e.logger,n):e.source!==void 0&&e.source.length>0&&!e.allowUnsafeSource&&!Ka(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:w.USAGE,files:[],target:a}):await Ge({isLocal:!1,logger:e.logger,source:q(t,e.source,e.ref)})?tn({logger:e.logger,markComplete:n,name:r,ref:e.ref,source:e.source,target:a,templateType:t}):{code:1,files:[],target:a},wn=(e,t)=>{if(te(t))return;let r;try{r=B(e)}catch{return}r==="pnpm"&&Je(e)},yn=async(e,t,r)=>{await Rt("realtime backend on Cloudflare Workers + Durable Objects");const a=mn(e);if(a!==void 0)return e.logger.error(a),{code:w.USAGE,files:[],target:""};const n=Aa(),i=e.name??await Ie(S.name,{badge:g.dir,default:n,placeholder:n}),s=await fn(e),o=i.trim();if(o.length===0)return e.logger.error("init: refusing an empty project name — pass a directory name (e.g. `lunora init my-app`)."),{code:w.USAGE,files:[],target:""};if(o.includes("/")||o.includes("\\")||o===".."||o===".")return e.logger.error(`init: refusing project name "${o}" — must not contain path separators or be "." / "..".`),{code:w.USAGE,files:[],target:""};if(!Pa.test(o))return e.logger.error(`init: refusing project name "${o}" — a Cloudflare Worker name must be lowercase letters, digits, "-" or "_" (starting with a letter, digit or "_"), and wrangler rejects anything else. Try \`${Ua(o)}\`.`),{code:w.USAGE,files:[],target:""};const l=xe(t,o),c=ge(l,{throwIfNoEntry:!1});if(c?.isSymbolicLink()===!0)return e.logger.error(`init: refusing to scaffold into "${l}" — it is a symlink. Remove it, or scaffold into a different directory name.`),{code:w.CONFLICT,files:[],target:l};const d=c!==void 0;if(d&&Y(l).length>0)return e.logger.error(`target directory not empty: ${l}`),{code:w.CONFLICT,files:[],target:l};if(e.dryRun===!0){const f=s.kind==="overlay"?`the ${s.framework} create-vite overlay`:`the ${s.templateType} template`;return I(e.logger,`scaffold ${f} into ${l}`),{code:0,files:[],target:l}}r.record(l,d);const h=s.kind==="overlay"?await vn(e,s.framework,o,l,r.complete):await gn(e,s.templateType,o,l,r.complete);return h.code===0&&wn(l,t),h},K=(e,t)=>{const{target:r,targetPreExisted:a}=e;if(!(r===void 0||!m(r))){if(a===!0)for(const n of Y(r))R(u(r,n),{force:!0,recursive:!0});else R(r,{force:!0,recursive:!0});t.info(`removed the partially-created project at ${r}`)}},bn=async(e,t,r)=>e.inPlace!==!0?yn(e,t,r):e.dryRun===!0?(I(e.logger,`configure Lunora into ${t}`),{code:0,files:[],target:t}):dn(t,e.logger),kn=async(e,t,r)=>{const a=await pn(e,t.target),n=e.inPlace===!0?void 0:await Za(e,t.target);if(e.inPlace!==!0){await Ja(e,t.target);const i=n??B(t.target);await Xa(T(t.target),n,i,te(r)),await jr(e.logger)}return a},xn=(e,t,r)=>{if(t.code!==0||e.ci===void 0)return;if(e.dryRun===!0){I(e.logger,`scaffold a ${e.ci} CI deploy pipeline`);return}const a=e.inPlace===!0?r:t.target;try{const n=B(a);nr(a,e.ci,n,e.logger)}catch{e.logger.warn(`--ci ${e.ci}: could not detect a package manager for this project — skipped the CI pipeline.`)}},Sn=async e=>{const t=e.cwd??process.cwd(),r={};let a;try{a=await bn(e,t,{complete:()=>{r.target=void 0},record:(n,i)=>{r.target=n,r.targetPreExisted=i}}),a.code===0&&a.target!==""?(r.target=void 0,await kn(e,a,t)||(a={...a,code:1})):a.code!==0&&K(r,e.logger)}catch(n){if(n instanceof X)return K(r,e.logger),process.stdout.write("\n  ✖  Setup cancelled — run `lunora init` again whenever you're ready. 🌙\n"),{code:130,files:[],target:""};throw K(r,e.logger),n}return xn(e,a,t),a},Cn=e=>typeof e=="string"&&nt.includes(e),Ln=(e,t)=>{if(e!==void 0){if(rr(e))return e;t.warn(`init: unknown --ci "${e}" — expected github | gitlab; skipping CI scaffold.`)}},An=e=>e===void 0?{}:Cn(e)?{templateType:e}:V(e)?{vite:e}:{error:`init: unknown --template "${e}" — expected a bespoke template (${it}) or an overlay framework (${at}).`},Jn=Et(({argument:e,cwd:t,logger:r,options:a})=>{const n=An(a.template);return"error"in n?(r.error(n.error),{code:w.USAGE}):Sn({add:a.add,auth:a.auth,allowUnsafeSource:a.allowUnsafeSource===!0,cwd:t,ci:Ln(a.ci,r),dryRun:a.dryRun===!0,from:a.from,inPlace:a.here===!0,interactive:a.interactive===!0?!0:void 0,logger:r,name:e[0],ref:a.ref,source:a.source,templateType:n.templateType,vite:a.vite??n.vite,yes:a.yes===!0})});export{rt as FRAMEWORK_CHOICES,Jn as execute,Cn as isTemplate,An as resolveTemplateFlag,q as resolveTemplateSource,Sn as runInitCommand};