UNPKG

@lunora/cli

Version:

The Lunora CLI: init, dev, deploy, codegen, run, reset, and migrate commands

2,594 lines 115 kB
import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, rmSync, mkdtempSync, lstatSync, cpSync, renameSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { detectFramework as detectFramework$1, isInteractive, LUNA_ART, LUNA_NAME, LUNA_SIGNOFF, paintAnswer, BADGES } from '@lunora/config';
import { walkSync } from '@visulima/fs';
import { join as join$1, dirname as dirname$1, basename, resolve, relative } from '@visulima/path';
import { downloadTemplate } from 'giget';
import { modify, applyEdits } from 'jsonc-parser';
import { join, dirname } from 'node:path';
import { d as defineHandler } from '../packem_shared/command-lYnl4QyF.mjs';
import { d as detectPackageManager, a as detectInstalledManagers, i as installArgsFor, r as runScriptCommand } from '../packem_shared/detect-package-manager-v4hHpQd0.mjs';
import MagicString from 'magic-string';
import { Project, SyntaxKind } from 'ts-morph';
import { P as PromptCancelledError } from '../packem_shared/prompt-cancelled-APzX1Im-.mjs';
import { c as resolveTagVersions, d as resolveSourceRef, e as resolvePinnedSourceRef, f as resolveDistTag, r as runAddCommand } from '../packem_shared/commands-D5Yxt9VY.mjs';
import { defaultSpawner } from '../packem_shared/createRecordingSpawner-WuSn20kb.mjs';
import { d as tuiMascot, e as tuiStep, f as tuiMoonrise, t as tuiText, g as tuiHeadline, h as tuiInfo, a as tuiSelect, b as tuiConfirm, w as withTuiSpinner, i as tuiNextSteps, j as tuiTasks, k as tuiMultiSelect, l as withTuiBadgeProgress } from '../packem_shared/tui-prompts-BjEN8XgP.mjs';
import { logStep } from '../packem_shared/createLogger-CIWSHrTL.mjs';
import { E as EMAIL_ITEM, p as promptBucketName, w as withStorageBucketName, M as MAIL_DESTINATION_PROMPT, r as resolveTypedDestination, f as withMailDestination, e as promptAuthProvider, c as promptDatabaseName, g as withAuthDatabaseName } from '../packem_shared/storage-BXU4ax4O.mjs';
import dns from 'node:dns/promises';

const GITHUB_CONTENT = `name: Deploy

on:
  push:
    branches: [main]
  pull_request:
  workflow_dispatch:

# Prerequisite: commit your pnpm-lock.yaml. \`pnpm install --frozen-lockfile\`
# (below) and the pnpm cache both require it — run \`pnpm install\` locally and
# 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:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      # Codegen + wrangler validation gate (no deploy) — fails fast on drift.
      - run: pnpm exec lunora prepare
      - run: pnpm exec lunora 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:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: pnpm
      - run: pnpm install --frozen-lockfile
      - run: pnpm exec lunora deploy --preview
`;
const GITLAB_CONTENT = `stages:
  - deploy

# Prerequisite: commit your pnpm-lock.yaml. \`pnpm install --frozen-lockfile\`
# (below) requires it — run \`pnpm 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: node:22
  stage: deploy
  before_script:
    - corepack enable
    - pnpm install --frozen-lockfile

# 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.
    - pnpm exec lunora prepare
    - pnpm exec lunora 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:
    - pnpm exec lunora deploy --preview
`;
const WORKFLOWS = {
  github: {
    content: GITHUB_CONTENT,
    file: join(".github", "workflows", "deploy.yml"),
    secretsHint: "repository secrets (Settings → Secrets and variables → Actions)"
  },
  gitlab: {
    content: GITLAB_CONTENT,
    file: ".gitlab-ci.yml",
    secretsHint: "masked CI/CD variables (Settings → CI/CD → Variables)"
  }
};
const isCiProvider = (value) => value === "github" || value === "gitlab";
const writeCiWorkflow = (projectRoot, provider, options = {}) => {
  const spec = WORKFLOWS[provider];
  const path = join(projectRoot, spec.file);
  if (existsSync(path) && options.overwrite !== true) {
    return { path, skipped: true, written: false };
  }
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, spec.content, "utf8");
  return { path, skipped: false, written: true };
};
const scaffoldCiWorkflow = (projectRoot, provider, logger, options = {}) => {
  const spec = WORKFLOWS[provider];
  try {
    const result = writeCiWorkflow(projectRoot, provider, options);
    if (result.skipped) {
      logger.info(`--ci ${provider}: ${spec.file} already exists — left unchanged (re-run with overwrite to replace).`);
    } else {
      logger.success(`--ci ${provider}: wrote ${spec.file}`);
      logger.info(`--ci ${provider}: set CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID as ${spec.secretsHint} to enable deploys.`);
      logger.info(
        `--ci ${provider}: run \`pnpm install\` and commit pnpm-lock.yaml before pushing — the pipeline runs \`pnpm install --frozen-lockfile\`.`
      );
    }
    return result;
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.warn(`--ci ${provider}: could not write ${spec.file} (${message})`);
    return { path: join(projectRoot, spec.file), skipped: false, written: false };
  }
};

const ADAPTER_BY_FRAMEWORK = {
  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"
};
const detectFramework = (root) => {
  const base = detectFramework$1(root);
  return { ...base, adapter: ADAPTER_BY_FRAMEWORK[base.framework] };
};

const LUNORA_CALL = "lunora()";
const LUNORA_IMPORT = 'import { lunora } from "@lunora/vite";';
const LUNORA_CALL_RE = /\blunora\s*\(/u;
const LUNORA_VITE_DOUBLE = '"@lunora/vite"';
const LUNORA_VITE_SINGLE = "'@lunora/vite'";
const findDefineConfigObject = (sf) => {
  for (const statement of sf.getStatements()) {
    if (statement.getKind() !== SyntaxKind.ExportAssignment) {
      continue;
    }
    const expr = statement.asKindOrThrow(SyntaxKind.ExportAssignment).getExpression();
    if (expr.getKind() !== SyntaxKind.CallExpression) {
      continue;
    }
    const call = expr.asKindOrThrow(SyntaxKind.CallExpression);
    if (call.getExpression().getText() !== "defineConfig") {
      continue;
    }
    const argument = call.getArguments()[0];
    if (argument?.getKind() === SyntaxKind.ObjectLiteralExpression) {
      return argument.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
    }
  }
  return void 0;
};
const findPlainExportObject = (sf) => {
  for (const statement of sf.getStatements()) {
    if (statement.getKind() !== SyntaxKind.ExportAssignment) {
      continue;
    }
    const expr = statement.asKindOrThrow(SyntaxKind.ExportAssignment).getExpression();
    if (expr.getKind() === SyntaxKind.ObjectLiteralExpression) {
      return expr.asKindOrThrow(SyntaxKind.ObjectLiteralExpression);
    }
  }
  return void 0;
};
const findConfigObject = (sourceText) => {
  const project = new Project({
    compilerOptions: { allowJs: true },
    useInMemoryFileSystem: true
  });
  const sf = project.createSourceFile("vite.config.ts", sourceText, { overwrite: true });
  return findDefineConfigObject(sf) ?? findPlainExportObject(sf);
};
const importInsertPosition = (sourceText) => {
  const project = new Project({
    compilerOptions: { allowJs: true },
    useInMemoryFileSystem: true
  });
  const sf = project.createSourceFile("vite.config.ts", sourceText, { overwrite: true });
  const imports = sf.getImportDeclarations();
  if (imports.length === 0) {
    return 0;
  }
  const last = imports[imports.length - 1];
  if (last === void 0) {
    return 0;
  }
  return last.getEnd();
};
const addImport = (ms, source) => {
  const insertAt = importInsertPosition(source);
  if (insertAt === 0) {
    ms.prepend(`${LUNORA_IMPORT}
`);
  } else {
    ms.appendLeft(insertAt, `
${LUNORA_IMPORT}`);
  }
};
const patchPluginsArray = (ms, configObject) => {
  const pluginsProp = configObject.getProperty("plugins");
  if (pluginsProp === void 0) {
    const properties = configObject.getProperties();
    const openBrace = configObject.getStart() + 1;
    if (properties.length === 0) {
      ms.appendLeft(openBrace, ` plugins: [${LUNORA_CALL}] `);
    } else {
      const firstProp = properties[0];
      if (firstProp !== void 0) {
        ms.appendLeft(firstProp.getStart(), `plugins: [${LUNORA_CALL}],
    `);
      }
    }
    return;
  }
  const arrayLit = pluginsProp.getDescendantsOfKind(SyntaxKind.ArrayLiteralExpression)[0];
  if (arrayLit === void 0) {
    return;
  }
  const elements = arrayLit.getElements();
  if (elements.length === 0) {
    ms.appendLeft(arrayLit.getStart() + 1, LUNORA_CALL);
  } else {
    const firstElement = elements[0];
    if (firstElement !== void 0) {
      ms.appendLeft(firstElement.getStart(), `${LUNORA_CALL}, `);
    }
  }
};
const patchViteConfig = (source) => {
  if (LUNORA_CALL_RE.test(source)) {
    return { changed: false, code: source, reason: "lunora plugin already present" };
  }
  const configObject = findConfigObject(source);
  if (configObject === void 0) {
    return { changed: false, code: source, reason: "could not locate a Vite config plugins array to patch" };
  }
  const ms = new MagicString(source);
  const alreadyImported = source.includes(LUNORA_VITE_DOUBLE) || source.includes(LUNORA_VITE_SINGLE);
  if (alreadyImported) {
    patchPluginsArray(ms, configObject);
  } else {
    addImport(ms, source);
    patchPluginsArray(ms, configObject);
  }
  return { changed: true, code: ms.toString() };
};

const NETWORK_ERROR_PATTERN = /enotfound|eai_again|econnrefused|etimedout|network|fetch failed|getaddrinfo/u;
const NOT_FOUND_ERROR_PATTERN = /404|not found|could not find|no such/u;
const describeDownloadFailure = (error, context) => {
  const raw = error instanceof Error ? error.message : String(error);
  const lower = raw.toLowerCase();
  const genericMessage = `failed to download template "${context.templateType}" from ${context.remote}: ${raw}`;
  if (NOT_FOUND_ERROR_PATTERN.test(lower)) {
    return {
      hints: [
        `Check the template name "${context.templateType}" and the ref "${context.ref}".`,
        "List/inspect available templates, or target a branch/tag with `--ref <branch>`."
      ],
      message: `template "${context.templateType}" not found at ${context.remote}: ${raw}`
    };
  }
  if (NETWORK_ERROR_PATTERN.test(lower)) {
    return {
      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: genericMessage
    };
  }
  return {
    hints: ["If this is a network/offline issue, scaffold from a local root with `lunora init --from <dir>`."],
    message: genericMessage
  };
};

const emitStep = async (type, message, answer) => {
  if (isInteractive()) {
    await tuiStep(BADGES[type], message, answer);
    return;
  }
  process.stdout.write("\n");
  if (answer === void 0 || answer === "") {
    logStep(type, message);
    return;
  }
  const dimmed = answer.split("\n").map((line) => paintAnswer(line)).join("\n");
  logStep(type, `${message}
${dimmed}`);
};
const emitMascot = async (logger) => {
  if (isInteractive()) {
    await tuiMascot();
    return;
  }
  logger.info(`
${LUNA_ART}
${LUNA_NAME}: ${LUNA_SIGNOFF}`);
};

const STACK_FEATURE_OPTIONS = [
  { description: "LLMs via Workers AI (summarize, generate, stream)", label: "AI", value: "ai" },
  { description: "Sign-up / sign-in (asks which provider)", label: "Authentication", value: "auth" },
  { description: "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" }
];
const STACK_FEATURE_VALUES = STACK_FEATURE_OPTIONS.map((option) => option.value);
const featureItem = (feature) => feature === "email" ? EMAIL_ITEM : feature;
const parseFeatureList = (raw, warn) => {
  const features = [];
  for (const part of raw.split(",").map((entry) => entry.trim()).filter(Boolean)) {
    if (STACK_FEATURE_VALUES.includes(part)) {
      if (!features.includes(part)) {
        features.push(part);
      }
    } else {
      warn(`init: unknown --add feature "${part}" — expected ${STACK_FEATURE_VALUES.join(" | ")}; skipping.`);
    }
  }
  return features;
};
const collectAuthFeature = async (deps) => {
  const provider = await promptAuthProvider(deps.select);
  const databaseName = await promptDatabaseName(deps.text, deps.projectName);
  return { label: "auth", names: [provider], transformManifest: (manifest) => withAuthDatabaseName(manifest, databaseName) };
};
const collectEmailFeature = async (deps) => {
  const answer = await deps.text(MAIL_DESTINATION_PROMPT, { placeholder: "you@yourdomain.com" });
  const destination = resolveTypedDestination(answer, (message) => {
    deps.logger.warn(message);
  });
  return {
    label: "email",
    names: [EMAIL_ITEM],
    transformManifest: destination === void 0 ? void 0 : (manifest) => withMailDestination(manifest, destination)
  };
};
const collectStorageFeature = async (deps) => {
  const bucketName = await promptBucketName(deps.text, deps.projectName);
  return { label: "storage", names: ["storage"], transformManifest: (manifest) => withStorageBucketName(manifest, bucketName) };
};
const FEATURE_COLLECTORS = {
  auth: collectAuthFeature,
  email: collectEmailFeature,
  storage: collectStorageFeature
};
const offerRegistryExtras = async (deps) => {
  if (deps.preselected !== void 0 && deps.preselected.length > 0) {
    await deps.applyAll(
      deps.preselected.map((feature) => {
        return { label: feature, names: [featureItem(feature)] };
      })
    );
    return;
  }
  if (!deps.interactive) {
    deps.logger.info(
      // eslint-disable-next-line no-secrets/no-secrets -- the pipe-separated feature list in this tip is a UI prompt, not a credential
      "tip: add features later with `lunora add <ai|auth|backup|browser|cloudflare-access|crons|email|flags|hyperdrive|payment|presence|queue|storage|workflow>`."
    );
    return;
  }
  const picked = await deps.multiSelect("Which features do you want to add?", STACK_FEATURE_OPTIONS, { defaults: [] });
  if (picked.length === 0) {
    return;
  }
  const plans = [];
  for (const feature of picked) {
    const collect = FEATURE_COLLECTORS[feature];
    plans.push(collect ? await collect(deps) : { label: feature, names: [feature] });
  }
  await deps.applyAll(plans);
};

const LOGO_PATH = "M 259.500 10.552 C 220.080 15.859, 182.424 32.566, 152.500 58.025 C 110.179 94.031, 85.380 137.183, 77.518 188.500 C 75.410 202.255, 74.569 225.677, 75.796 236.466 C 76.757 244.917, 76.683 245.692, 74.518 249.966 C 63.118 272.466, 53.141 303.876, 51.382 322.799 L 50.718 329.943 71.960 320.471 C 83.643 315.262, 93.326 311, 93.478 311 C 93.630 311, 96.547 316.063, 99.959 322.250 C 103.371 328.438, 107.249 334.850, 108.577 336.500 L 110.990 339.500 110.981 336 C 110.977 334.075, 111.499 324.991, 112.143 315.813 L 113.312 299.127 121.406 293.336 C 132.495 285.403, 149.593 271.554, 161 261.268 C 171.556 251.748, 189.116 235, 188.540 235 C 188.337 235, 183.069 238.648, 176.835 243.106 C 142.318 267.789, 68.537 314, 63.646 314 C 61.843 314, 72.791 281.179, 80.905 262.259 C 92.233 235.845, 107.473 212.389, 132.106 183.453 L 138.451 176 148.268 176 C 176.192 176, 197.512 187.154, 212.868 209.797 C 216.470 215.108, 217.035 216.595, 216.477 219.297 C 211.386 243.968, 202.359 274.496, 193.797 296 C 183.898 320.861, 167.147 352.101, 152.395 373.215 L 147.004 380.930 152.891 385.830 C 161.400 392.911, 165.563 396, 166.594 395.998 C 167.092 395.998, 168.772 391.641, 170.327 386.317 C 176.279 365.934, 188.422 338.749, 200.942 317.778 C 223.060 280.731, 256.432 244.369, 294.500 215.836 C 309.956 204.252, 313.937 201.603, 314.719 202.385 C 315.116 202.783, 315.449 213.096, 315.460 225.304 C 315.474 241.855, 315.021 250.405, 313.680 258.924 C 307.009 301.272, 291.175 336.677, 263.112 372 C 255.259 381.883, 227.182 410.673, 218.516 417.727 L 213.532 421.783 223.439 424.880 C 281.705 443.093, 349.165 436.018, 398.616 406.508 C 446.728 377.797, 483.322 331.466, 497.366 281.481 C 503.381 260.075, 504.480 250.741, 504.491 221 C 504.501 191.997, 503.598 184.047, 497.987 163.732 C 484.768 115.871, 452.505 72.708, 407.718 42.964 C 381.051 25.254, 352.818 14.828, 319.695 10.460 C 305.932 8.645, 273.298 8.695, 259.500 10.552";
const WELCOME_CSS = `/* 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; } }
`;
const REACT_APP = `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="${LOGO_PATH}" 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>
    );
}
`;
const VUE_APP = `<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="${LOGO_PATH}" 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>
`;
const SOLID_APP = `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="${LOGO_PATH}" 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>
    );
}
`;
const SVELTE_APP = `<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="${LOGO_PATH}" 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>
`;
const VANILLA_MAIN = `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="${LOGO_PATH}" fill="currentColor" fill-rule="evenodd" />
            </svg>
            <span class="word">Lunora</span>
        </div>

        <div class="grid">
            <a class="card feature" href="https://lunora.sh/docs">
                <div class="shot" aria-hidden="true">
                    <div class="top">
                        <span class="wm"><i></i> Lunora</span>
                        <span class="search"></span>
                        <span class="ver">v0.1</span>
                    </div>
                    <div class="body">
                        <div class="nav">
                            <i style="width: 80%"></i>
                            <i style="width: 60%"></i>
                            <i style="width: 72%"></i>
                            <i style="width: 50%"></i>
                            <i style="width: 66%"></i>
                            <i style="width: 44%"></i>
                            <i style="width: 58%"></i>
                        </div>
                        <div class="doc">
                            <span class="h"></span>
                            <i style="width: 92%"></i>
                            <i style="width: 88%"></i>
                            <span class="accent"></span>
                            <i style="width: 80%"></i>
                            <i style="width: 90%"></i>
                            <i style="width: 72%"></i>
                            <i style="width: 84%"></i>
                            <i style="width: 78%"></i>
                        </div>
                    </div>
                </div>
                <div class="info">
                    <span class="ic">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
                            <path d="M4 5a2 2 0 0 1 2-2h9l5 5v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2z" />
                            <path d="M14 3v5h5" />
                        </svg>
                    </span>
                    <h2>Documentation</h2>
                    <div class="row">
                        <p>
                            Schemas, queries, live subscriptions, sharding, and edge deploy — start to finish. New here or coming from Convex or tRPC,
                            you'll have a live app fast.
                        </p>
                        <span class="arrow">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <path d="M5 12h14M13 6l6 6-6 6" />
                            </svg>
                        </span>
                    </div>
                </div>
            </a>

            <div class="stack">
                <a class="card mini" href="https://lunora.sh/blog">
                    <div class="mc">
                        <span class="ic">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
                                <path d="M5 4h11a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1H6a2 2 0 0 1-2-2V5a1 1 0 0 1 1-1zM8 8h7M8 12h7M8 16h4" />
                            </svg>
                        </span>
                        <h3>Blog</h3>
                        <p>Product updates, deep dives, and what's new in Lunora.</p>
                    </div>
                    <span class="arrow">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <path d="M5 12h14M13 6l6 6-6 6" />
                        </svg>
                    </span>
                </a>
                <a class="card mini" href="/_lunora">
                    <div class="mc">
                        <span class="ic">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
                                <rect x="3" y="3" width="18" height="18" rx="1" />
                                <path d="M3 9h18M9 21V9" />
                            </svg>
                        </span>
                        <h3>Lunora Studio</h3>
                        <p>Local admin for schema, data, logs, and advisors.</p>
                    </div>
                    <span class="arrow">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <path d="M5 12h14M13 6l6 6-6 6" />
                        </svg>
                    </span>
                </a>
                <a class="card mini" href="https://lunora.sh/packages">
                    <div class="mc">
                        <span class="ic">
                            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7">
                                <path d="M12 2 3 7v10l9 5 9-5V7z" />
                                <path d="M3 7l9 5 9-5M12 12v10" />
                            </svg>
                        </span>
                        <h3>Cloudflare ecosystem</h3>
                        <p>Auth, mail, storage, AI, payments — one deploy.</p>
                    </div>
                    <span class="arrow">
                        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <path d="M5 12h14M13 6l6 6-6 6" />
                        </svg>
                    </span>
                </a>
            </div>
        </div>

        <div class="lw-foot">Running on Lunora · Vite + Vanilla · <span id="lw-count">0</span> messages</div>
    </div>
\`;

const root = document.querySelector<HTMLDivElement>("#app")!;
root.classList.add("lunora-welcome");
root.innerHTML = welcomeHtml;

// Theme toggle: flip the root's data-theme + swap the button's icon/label.
const toggleButton = root.querySelector<HTMLButtonElement>(".lw-toggle")!;

const paintToggle = (theme: "dark" | "light"): void => {
    root.dataset.theme = theme;
    toggleButton.innerHTML = \`\${theme === "light" ? MOON_ICON : SUN_ICON}<span>\${theme === "light" ? "Ivory" : "Night"}</span>\`;
};

paintToggle(window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark");
toggleButton.addEventListener("click", () => {
    paintToggle(root.dataset.theme === "light" ? "dark" : "light");
});

// Live demo: the message count of a demo channel re-renders on every delta.
const count = document.querySelector<HTMLSpanElement>("#lw-count")!;

client.onUpdate(api.messages.list, { channelId: "channel:demo" }, (result) => {
    count.textContent = String(result.messages.length);
});
`;

const READ_URL = `const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;`;
const REACT_MAIN = `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.
${READ_URL}
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>,
);
`;
const VUE_MAIN = `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.
${READ_URL}
createApp(App).use(createLunora(new LunoraClient({ url }))).mount("#app");
`;
const SOLID_INDEX = `import "./index.css";

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

import App from "./App";

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

render(
    () => (
        <LunoraContext.Provider value={client}>
            <App />
        </LunoraContext.Provider>
    ),
    root!,
);
`;
const SVELTE_ROOT = `<script lang="ts">
    import { setLunoraClient } from "@lunora/svelte";
    import { LunoraClient } from "lunorash/client";

    import App from "./App.svelte";

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

<App />
`;
const SVELTE_MAIN = `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;
`;
const ADAPTERS = {
  react: {
    adapter: "@lunora/react",
    createViteTemplate: "react-ts",
    files: [
      { contents: REACT_MAIN, path: "src/main.tsx" },
      { contents: REACT_APP, path: "src/App.tsx" },
      { contents: WELCOME_CSS, path: "src/index.css" }
    ],
    label: "React"
  },
  solid: {
    adapter: "@lunora/solid",
    createViteTemplate: "solid-ts",
    files: [
      { contents: SOLID_INDEX, path: "src/index.tsx" },
      { contents: SOLID_APP, path: "src/App.tsx" },
      { contents: WELCOME_CSS, path: "src/index.css" }
    ],
    label: "Solid"
  },
  svelte: {
    adapter: "@lunora/svelte",
    createViteTemplate: "svelte-ts",
    files: [
      { contents: SVELTE_ROOT, path: "src/Root.svelte" },
      { contents: SVELTE_MAIN, path: "src/main.ts" },
      { contents: SVELTE_APP, path: "src/App.svelte" },
      { contents: WELCOME_CSS, path: "src/app.css" }
    ],
    label: "Svelte"
  },
  vanilla: {
    adapter: "lunorash/client",
    createViteTemplate: "vanilla-ts",
    files: [
      { contents: VANILLA_MAIN, path: "src/main.ts" },
      { contents: WELCOME_CSS, path: "src/style.css" }
    ],
    label: "Vanilla"
  },
  vue: {
    adapter: "@lunora/vue",
    createViteTemplate: "vue-ts",
    files: [
      { contents: VUE_MAIN, path: "src/main.ts" },
      { contents: VUE_APP, path: "src/App.vue" },
      { contents: WELCOME_CSS, path: "src/style.css" }
    ],
    label: "Vue"
  }
};
const isOverlayFramework = (value) => Object.hasOwn(ADAPTERS, value);

const RATELIMIT_SCHEMA = `import type { Middleware } from "lunorash/server";
import { defineSchemaExtension, defineTable, definePlugin, v } from "lunorash/server";
import { createDbStore, RateLimiter } from "lunorash/ratelimit";
import type { RateLimitConfigMap } from "lunorash/ratelimit";

export const limits = {
    default: { kind: "token bucket", period: 60_000, rate: 10 },
} as const satisfies RateLimitConfigMap;

export type LimitName = keyof typeof limits;

export const makeRateLimiter = (ctx: { db: unknown }): RateLimiter<LimitName> =>
    new RateLimiter<LimitName>({
        config: limits,
        store: createDbStore({ db: ctx.db as never, table: "ratelimit_buckets" }),
    });

const middleware: Middleware<{ api?: Record<string, unknown>; db: unknown }, { api: Record<string, unknown>; db: unknown }> = ({ ctx, next }) =>
    next({
        ctx: {
            ...ctx,
            api: { ...ctx.api, ratelimit: makeRateLimiter(ctx) },
        },
    });

export const ratelimit = definePlugin("ratelimit", {
    extension: defineSchemaExtension("ratelimit", {
        tables: {
            buckets: defineTable({
                key: v.string(),
                value: v.number(),
                ts: v.number(),
                prev: v.optional(v.number()),
            })
                .index("by_key", ["key"])
                .externallyManaged(),
        },
    }),
    middleware,
});
`;
const LUNORA_SCHEMA = `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);
`;
const LUNORA_MESSAGES = `import { RateLimiter, rateLimit, createDbStore } from "lunorash/ratelimit";

import { mutation, query, v } from "#lunora/_generated/server.js";

const limiter = (ctx: { db: unknown }) => new RateLimiter({
    config: {
        send: { kind: "token bucket", period: 60_000, rate: 30 },
    },
    store: createDbStore({ db: ctx.db as never, table: "ratelimit_buckets" }),
});

export const list = query.input({ channelId: v.string().meta({ schema: { maxLength: 256 } }), limit: v.optional(v.number()) }).query(async ({ args, ctx }) => {
    const messages = await ctx.db
        .query("messages")
        .withIndex("by_channel", (q) => q.eq("channelId", args.channelId))
        .take(args.limit ?? 50);

    return { channelId: args.channelId, messages };
});

export const send = mutation
    .input({ channelId: v.string().meta({ schema: { maxLength: 256 } }), text: v.string().meta({ schema: { maxLength: 4096 } }) })
    .use(rateLimit(limiter, "send", { key: (ctx) => ctx.auth.userId ?? "anon" }))
    .mutation(async ({ args, ctx }) => {
        const id = await ctx.db.insert("messages", { channelId: args.channelId, text: args.text });

        return { channelId: args.channelId, id, text: args.text };
    });
`;
const SERVER_ENTRY = `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;
`;
const WRANGLER = `{
    "$schema": "node_modules/wrangler/config-schema.json",
    "name": "__NAME__",
    "main": "src/server.ts",
    "compatibility_date": "2026-06-10",
    "compatibility_flags": ["nodejs_compat"],
    "durable_objects": {
        "bindings": [{ "name": "SHARD", "class_name": "ShardDO" }],
    },
    "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ShardDO"] }],
    "observability": { "enabled": true, "head_sampling_rate": 1 },
}
`;
const GITIGNORE_ADDITIONS = [".wrangler", ".env", ".env.*", "!.env.example", ".lunora/", ".lunora-cache", "lunora/_generated"];
const ENV_EXAMPLE = `# 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
`;
const COMMON_DEV_DEPENDENCIES = {
  "@cloudflare/workers-types": "^4.20260611.1",
  wrangler: "^4.100.0"
};
const writeFile = (target, relativePath, contents, written) => {
  const destination = join$1(target, relativePath);
  mkdirSync(dirname$1(destination), { recursive: true });
  writeFileSync(destination, contents, "utf8");
  written.push(destination);
};
const NEWLINE = /\r?\n/;
const ensureGitignore = (target) => {
  const path = join$1(target, ".gitignore");
  const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
  const missing = GITIGNORE_ADDITIONS.filter((entry) => !existing.split(NEWLINE).includes(entry));
  if (missing.length === 0) {
    return;
  }
  const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
  writeFileSync(path, `${existing}${prefix}
# Lunora
${missing.join("\n")}
`, "utf8");
};
const isLunoraDep$1 = (name) => name === "lunorash" || name.startsWith("@lunora/");
const stampRange = (name, range, distTag, versions) => isLunoraDep$1(name) ? versions?.get(name) ?? distTag : range;
const withDependency = (map, name, range, distTag) => {
  return { ...map, [name]: stampRange(name, range, distTag) };
};
const restampLunora = (map, distTag, versions) => Object.fromEntries(Object.entries(map).map(([name, range]) => [name, stampRange(name, range, distTag, versions)]));
const patchPackageJson = async (target, name, adapter, distTag) => {
  const path = join$1(target, "package.json");
  const parsed = JSON.parse(readFileSync(path, "utf8"));
  let dependencies = withDependency(parsed.dependencies ?? {}, "lunorash", distTag, distTag);
  dependencies = withDependency(dependencies, "@lunora/ratelimit", distTag, distTag);
  if (adapter.adapter.startsWith("@lunora/")) {
    dependencies = withDependency(dependencies, adapter.adapter, distTag, distTag);
  }
  for (const [depName, range] of Object.entries(adapter.extraDependencies ?? {})) {
    dependencies = withDependency(dependencies, depName, range, distTag);
  }
  let devDependencies = withDependency(parsed.devDependencies ?? {}, "@lunora/vite", distTag, distTag);
  devDependencies = withDependency(devDependencies, "@lunora/studio", distTag, distTag);
  for (const [depName, range] of Object.entries(COMMON_DEV_DEPENDENCIES)) {
    devDependencies = withDependency(devDependencies, depName, range, distTag);
  }
  const lunoraNames = [...Object.keys(dependencies), ...Object.keys(devDependencies)].filter((depName) => isLunoraDep$1(depName));
  const versions = await resolveTagVersions(lunoraNames, distTag);
  parsed.name = name;
  parsed.imports = { ...parsed.imports, "#lunora/*": "./lunora/*" };
  parsed.dependencies = restampLunora(dependencies, distTag, versions);
  parsed.devDependencies = restampLunora(devDependencies, distTag, versions);
  parsed.scripts = { ...parsed.scripts, codegen: "lunora codegen", deploy: "vite build && lunora deploy" };
  writeFileSync(path, `${JSON.stringify(parsed, void 0, 4)}
`, "utf8");
};
const patchBaseViteConfig = (target, logger) => {
  const candidate = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"].map((file) => join$1(target, file)).find((path) => existsSync(path));
  if (candidate === void 0) {
    logger.warn("overlay: no vite.config found in the create-vite base — add `lunora()` to your Vite plugins manually.");
    return;
  }
  const result = patchViteConfig(readFileSync(candidate, "utf8"));
  if (result.changed) {
    writeFileSync(candidate, result.code, "utf8");
  }
};
const applyLunoraOverlay = async (options) => {
  const { adapter, distTag, logger, name, target } = options;
  const written = [];
  writeFile(target, join$1("lunora", "ratelimit", "schema.ts"), RATELIMIT_SCHEMA, written);
  writeFile(target, join$1("lunora", "schema.ts"), LUNORA_SCHEMA, written);
  writeFile(target, join$1("lunora", "messages.ts"), LUNORA_MESSAGES, written);
  writeFile(target, join$1("src", "server.ts"), SERVER_ENTRY, written);
  writeFile(target, "wrangler.jsonc", WRANGLER.replaceAll("__NAME__", name), written);
  writeFile(target, ".env.example", ENV_EXAMPLE, written);
  for (const file of adapter.files) {
    writeFile(target, file.path, file.contents, written);
  }
  patchBaseViteConfig(target, logger);
  await patchPackageJson(target, name, adapter, distTag);
  ensureGitignore(target);
  return written;
};

const ADJECTIVES = [
  "lunar",
  "silver",
  "silent",
  "waning",
  "waxing",
  "crescent",
  "cosmic",
  "stellar",
  "orbital",
  "gibbous",
  "twilight",
  "midnight",
  "shimmering",
  "drifting",
  "weightless"
];
const NOUNS = [
  "moon",
  "tide",
  "crater",
  "comet",
  "eclipse",
  "halo",
  "orbit",
  "nebula",
  "voyager",
  "lander",
  "rover",
  "beacon",
  "harbor",
  "meadow",
  "fox"
];
const pick = (items) => (
  // eslint-disable-next-line sonarjs/pseudo-random -- cosmetic default project name, not a security decision.
  items[Math.floor(Math.random() * items.length)]
);
const generateProjectName = () => `${pick(ADJECTIVES)}-${pick(NOUNS)}`;

const isOnline = async () => dns.lookup("github.com").then(
  () => true,
  () => false
);
const GITHUB_SOURCE = /^(?:gh|github):([^/]+)\/([^#/]+)(?:\/[^#]*)?(?:#(.+))?$/;
const parseGitHubSource = (source) => {
  const match = GITHUB_SOURCE.exec(source);
  if (match === null) {
    return void 0;
  }
  const [, owner, repo, ref] = match;
  if (owner === void 0 || repo === void 0) {
    return void 0;
  }
  return { owner, ref: ref ?? "HEAD", repo };
};
const templateRefExists = async (source) => {
  const parsed = parseGitHubSource(source);
  if (parsed === void 0) {
    return void 0;
  }
  const url = `https://codeload.github.com/${parsed.owner}/${parsed.repo}/tar.gz/${parsed.ref}`;
  try {
    const response = await fetch(url, { method: "HEAD" });
    if (response.status === 404) {
      return false;
    }
    return response.ok ? true : void 0;
  } catch {
    return void 0;
  }
};
const verifyRemoteTemplate = async (params) => {
  if (params.isLocal) {
    return true;
  }
  const isGitHubBacked = params.source === void 0 || parseGitHubSource(params.source) !== void 0;
  if (!isGitHubBacked) {
    return true;
  }
  if (!await isOnline()) {
    params.logger.error("you appear to be offline — connect to the internet and try again, or scaffold from a local template with `--from <dir>`.");
    return false;
  }
  if (params.source !== void 0 && await templateRefExists(params.source) === false) {
    params.logger.error(`template source not found: ${params.source} — double-check --ref / --source, or browse the templates at https://lunora.sh/docs.`);
    return false;
  }
  return true;
};

const COPY = {
  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?"
};
const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".gitignore", ".html", ".js", ".json", ".jsonc", ".md", ".mjs", ".ts", ".tsx"]);
const VITE_CONFIG_CANDIDATES = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
const MINIMAL_VITE_CONFIG = `import { defineConfig } from "vite";
import { lunora } from "@lunora/vite";

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

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

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

export const send = mutation
    .input({ channelId: v.id("channels"), text: v.string() })
    .mutation(async ({ args }) => {
        return { channelId: args.channelId, text: args.text };
    });
`;
const DEFAULT_SOURCE_BASE = "gh:anolilab/lunora/templates";
const isTextFile = (filePath) => {
  const lastDot = filePath.lastIndexOf(".");
  if (lastDot === -1) {
    return false;
  }
  return TEXT_EXTENSIONS.has(filePath.slice(lastDot));
};
const substitute = (content, name) => content.replaceAll("{{name}}", name);
const isLunoraDep = (name) => name === "lunorash" || name.startsWith("@lunora/");
const resolveLunoraVersions = async (files, distTag) => {
  const names = /* @__PURE__ */ new Set();
  for (const file of files) {
    if (basename(file) !== "package.json") {
      continue;
    }
    try {
      const parsed = JSON.parse(readFileSync(file, "utf8"));
      for (const section of ["dependencies", "devDependencies"]) {
        for (const name of Object.keys(parsed[section] ?? {})) {
          if (isLunoraDep(name)) {
            names.add(name);
          }
        }
      }
    } catch {
    }
  }
  return resolveTagVersions(names, distTag);
};
const stampLunoraDeps = (packageJsonText, distTag, versions) => {
  let parsed;
  try {
    parsed = JSON.parse(packageJsonText);
  } catch {
    return packageJsonText;
  }
  let text = packageJsonText;
  for (const section of ["dependencies", "devDependencies"]) {
    for (const name of Object.keys(parsed[section] ?? {})) {
      if (!isLunoraDep(name)) {
        continue;
      }
      const pin = versions.get(name) ?? distTag;
      const edits = modify(text, [section, name], pin, { formattingOptions: { insertSpaces: true, tabSize: 4 } });
      text = applyEdits(text, edits);
    }
  }
  return text;
};
const PNPM_BUILT_DEPENDENCIES = [
  "@parcel/watcher",
  "esbuild",
  "lmdb",
  "msgpackr-extract",
  "rs-module-lexer",
  "sharp",
  "unrs-resolver",
  "workerd"
];
const PNPM_DENIED_BUILD_DEPENDENCIES = ["cpu-features", "protobufjs", "ssh2"];
const PNPM_WORKSPACE_FILENAME = "pnpm-workspace.yaml";
const pnpmWorkspaceYaml = () => [
  "# 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:",
  ...PNPM_BUILT_DEPENDENCIES.map((name) => `    '${name}': true`),
  ...PNPM_DENIED_BUILD_DEPENDENCIES.map((name) => `    '${name}': false`),
  ""
].join("\n");
const collectFiles = (directory) => {
  const out = [];
  for (const entry of walkSync(directory, { includeDirs: false, includeFiles: true })) {
    if (lstatSync(entry.path).isSymbolicLink()) {
      continue;
    }
    out.push(entry.path);
  }
  return out;
};
const copyTemplate = async (sourceDirectory, target, name) => {
  const files = collectFiles(sourceDirectory);
  const written = [];
  const distTag = resolveDistTag();
  const versions = await resolveLunoraVersions(files, distTag);
  for (const source of files) {
    const relativePath = relative(sourceDirectory, source);
    const destination = join$1(target, relativePath);
    mkdirSync(dirname$1(destination), { recursive: true });
    const raw = readFileSync(source);
    let text = isTextFile(source) ? substitute(raw.toString("utf8"), name) : void 0;
    if (text !== void 0 && basename(source) === "package.json") {
      text = stampLunoraDeps(text, distTag, versions);
    }
    if (text === void 0) {
      writeFileSync(destination, raw);
    } else {
      writeFileSync(destination, text, "utf8");
    }
    written.push(destination);
  }
  return written;
};
const resolveTemplateSource = (templateType, source, ref) => {
  if (source !== void 0 && source.length > 0) {
    return source;
  }
  return `${DEFAULT_SOURCE_BASE}/${templateType}#${resolveSourceRef(ref)}`;
};
const isSafeSource = (source) => {
  if (source.includes("..")) {
    return false;
  }
  return source.startsWith("gh:") || source.startsWith("github:") || source.startsWith("https://");
};
const logWould = (logger, action) => {
  logger.info(`[dry-run] would ${action}`);
};
const logScaffoldSuccess = (logger, written, target) => {
  if (isInteractive()) {
    process.stdout.write("\n");
  }
  logger.success(`scaffolded ${String(written.length)} files into ${target}`);
};
const installCommand = (manager, packages) => {
  const verb = manager === "npm" ? "install" : "add";
  return `${manager} ${verb} ${packages.join(" ")}`;
};
const isInsideMonorepo = (startDirectory) => {
  let directory = resolve(startDirectory);
  for (; ; ) {
    if (existsSync(join$1(directory, "pnpm-workspace.yaml"))) {
      return true;
    }
    const packagePath = join$1(directory, "package.json");
    if (existsSync(packagePath)) {
      try {
        const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
        if (parsed.workspaces !== void 0) {
          return true;
        }
      } catch {
      }
    }
    const parent = dirname$1(directory);
    if (parent === directory) {
      return false;
    }
    directory = parent;
  }
};
const isInsideGitRepo = (startDirectory) => {
  let directory = resolve(startDirectory);
  for (; ; ) {
    if (existsSync(join$1(directory, ".git"))) {
      return true;
    }
    const parent = dirname$1(directory);
    if (parent === directory) {
      return false;
    }
    directory = parent;
  }
};
const maybeOfferGit = async (options, target) => {
  if (options.yes === true || !isInteractive() || isInsideGitRepo(dirname$1(target))) {
    return;
  }
  if (!await tuiConfirm(COPY.git, { badge: BADGES.git, defaultYes: false })) {
    await tuiInfo("Sounds good! You can always run git init manually.");
    return;
  }
  if (options.dryRun === true) {
    logWould(options.logger, "initialize a git repository");
    return;
  }
  const spawner = options.spawner ?? defaultSpawner;
  const result = await withTuiSpinner("Initializing a git repository…", () => spawner({ args: ["init"], command: "git", cwd: target }));
  if (result.code === 0) {
    await emitStep("git", "Initialized an empty git repository.");
  } else {
    options.logger.warn("`git init` failed — initialize it yourself later with `git init`.");
  }
};
const printNextSteps = async (name, installed, manager, insideMonorepo) => {
  const steps = [{ code: `cd ./${name}`, lead: "Enter your project directory using" }];
  if (installed === void 0) {
    steps.push({ code: `${manager} install`, lead: "Install dependencies with", tail: insideMonorepo ? " from the workspace root" : void 0 });
  }
  steps.push(
    { code: runScriptCommand(manager, "dev"), lead: "Run", tail: " to start the dev server." },
    { code: "lunora add", lead: "Add features like auth or storage using" }
  );
  const help = [
    { code: "https://lunora.sh/docs", lead: "Read the docs at" },
    { code: "https://lunora.sh/chat", lead: "Stuck? Join the chat at" }
  ];
  if (isInteractive()) {
    await tuiNextSteps(BADGES.next, COPY.nextHeader, steps, help);
    return;
  }
  const lines = steps.map((step) => `${step.lead} ${step.code}${step.tail ?? ""}`);
  lines.push("", ...help.map((line) => `${line.lead} ${line.code}${line.tail ?? ""}`));
  await emitStep("next", COPY.nextHeader, lines.join("\n"));
};
const offerInstallIsInteractive = (options) => options.yes !== true && (options.installPrompt !== void 0 || isInteractive());
const maybeOfferInstall = async (options, target) => {
  if (!offerInstallIsInteractive(options)) {
    return void 0;
  }
  if (isInsideMonorepo(dirname$1(target))) {
    return void 0;
  }
  const managers = detectInstalledManagers(options.packageManagerProbe);
  const [defaultManager] = managers;
  if (defaultManager === void 0) {
    return void 0;
  }
  const confirm = options.installPrompt?.confirmInstall ?? (async () => tuiConfirm(COPY.install, { badge: BADGES.deps, defaultYes: true }));
  if (!await confirm()) {
    await tuiInfo("No problem! Remember to install dependencies after setup.");
    return void 0;
  }
  let manager = defaultManager;
  if (managers.length > 1) {
    manager = options.installPrompt ? await options.installPrompt.selectManager(managers) : await tuiSelect(
      COPY.packageManager,
      managers.map((candidate) => {
        return { label: candidate, value: candidate };
      }),
      { badge: BADGES.deps, default: defaultManager }
    ) ?? defaultManager;
  }
  if (options.dryRun === true) {
    logWould(options.logger, `install dependencies with ${manager}`);
    return void 0;
  }
  if (manager === "pnpm") {
    const workspacePath = join$1(target, PNPM_WORKSPACE_FILENAME);
    if (!existsSync(workspacePath)) {
      writeFileSync(workspacePath, pnpmWorkspaceYaml(), "utf8");
    }
  }
  const spawner = options.spawner ?? defaultSpawner;
  const { args, command } = installArgsFor(manager);
  await emitStep("deps", `Installing dependencies with ${manager}…`);
  const result = await spawner({ args, command, cwd: target });
  if (result.code !== 0) {
    options.logger.warn(`\`${command} install\` exited with code ${String(result.code)} — run it yourself in ${basename(target)}/.`);
    return void 0;
  }
  await emitStep("deps", `Dependencies installed with ${manager}.`);
  return manager;
};
const scaffoldFromLocal = async (fromRoot, templateType, target, name, logger) => {
  const templateDirectory = join$1(fromRoot, templateType);
  if (!existsSync(templateDirectory)) {
    logger.error(`template not found in local source: ${templateDirectory}`);
    return { code: 1, files: [], target };
  }
  const written = await copyTemplate(templateDirectory, target, name);
  logScaffoldSuccess(logger, written, target);
  return { code: 0, files: written, target };
};
const scaffoldFromRemote = async (options) => {
  const { logger, name, ref, source, target, templateType } = options;
  const stagingRoot = mkdtempSync(join$1(tmpdir(), "lunora-init-fetch-"));
  const stagingDirectory = join$1(stagingRoot, "template");
  try {
    const pinnedRef = source !== void 0 && source.length > 0 ? ref : await resolvePinnedSourceRef(ref, logger);
    const remote = resolveTemplateSource(templateType, source, pinnedRef);
    let downloaded;
    let written = [];
    await tuiTasks(
      [
        {
          label: `${templateType} template fetched`,
          run: async () => {
            downloaded = await downloadTemplate(remote, {
              cwd: stagingRoot,
              dir: stagingDirectory,
              force: true,
              install: false,
              silent: true
            });
          }
        },
        {
          label: `files copied into ${name}/`,
          run: async () => {
            written = await copyTemplate(stagingDirectory, target, name);
          }
        }
      ],
      { end: "Project initialized!", start: "Project initializing…" }
    );
    const staged = collectFiles(stagingDirectory);
    if (isInteractive()) {
      process.stdout.write("\n");
    }
    logger.info(
      downloaded?.commit ? `template: ${downloaded.source} @ ${downloaded.commit} (${String(staged.length)} files)` : `template: ${downloaded?.source ?? remote} (${String(staged.length)} files)`
    );
    logScaffoldSuccess(logger, written, target);
    return { code: 0, files: written, target };
  } catch (error) {
    if (error instanceof PromptCancelledError) {
      throw error;
    }
    const { hints, message } = describeDownloadFailure(error, {
      ref: resolveSourceRef(ref),
      remote: resolveTemplateSource(templateType, source, ref),
      templateType
    });
    logger.error(message);
    for (const hint of hints) {
      logger.warn(hint);
    }
    return { code: 1, files: [], target };
  } finally {
    rmSync(stagingRoot, { force: true, recursive: true });
  }
};
const renameCreateViteDotfiles = (directory) => {
  for (const file of ["_gitignore", "_npmrc", "_gitattributes"]) {
    const from = join$1(directory, file);
    if (existsSync(from)) {
      renameSync(from, join$1(directory, `.${file.slice(1)}`));
    }
  }
};
const scaffoldViteOverlay = async (options) => {
  const { framework, logger, name, overlayBaseFrom, target } = options;
  const adapter = ADAPTERS[framework];
  const stagingRoot = mkdtempSync(join$1(tmpdir(), "lunora-vite-base-"));
  try {
    let localBase;
    if (overlayBaseFrom !== void 0) {
      localBase = join$1(overlayBaseFrom, `template-${adapter.createViteTemplate}`);
      if (!existsSync(localBase)) {
        logger.error(`create-vite base not found on disk: ${localBase}`);
        return { code: 1, files: [], target };
      }
    }
    const copyBase = async () => {
      if (localBase !== void 0) {
        cpSync(localBase, target, { recursive: true });
        return;
      }
      const stagingDirectory = join$1(stagingRoot, "base");
      const remote = `github:vitejs/vite/packages/create-vite/template-${adapter.createViteTemplate}#main`;
      await downloadTemplate(remote, { cwd: stagingRoot, dir: stagingDirectory, force: true, install: false, silent: true });
      renameCreateViteDotfiles(stagingDirectory);
      cpSync(stagingDirectory, target, { recursive: true });
    };
    let written = [];
    await tuiTasks(
      [
        { label: `create-vite (${adapter.label}) base ready`, run: copyBase },
        {
          label: `Lunora overlay applied (${adapter.label})`,
          run: async () => {
            written = await applyLunoraOverlay({ adapter, distTag: resolveDistTag(), logger, name, target });
          }
        }
      ],
      { end: "Project initialized!", start: "Project initializing…" }
    );
    logScaffoldSuccess(logger, written, target);
    return { code: 0, files: [...collectFiles(target)], target };
  } catch (error) {
    if (error instanceof PromptCancelledError) {
      throw error;
    }
    const message = error instanceof Error ? error.message : String(error);
    logger.error(`failed to scaffold the ${adapter.label} base: ${message}`);
    return { code: 1, files: [], target };
  } finally {
    rmSync(stagingRoot, { force: true, recursive: true });
  }
};
const createMinimalViteConfig = (cwd, logger) => {
  const target = join$1(cwd, "vite.config.ts");
  try {
    writeFileSync(target, MINIMAL_VITE_CONFIG, "utf8");
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error(`init --in-place: could not write ${target}: ${message}`);
    return { code: 1, files: [], target: cwd };
  }
  logger.success(`created ${target} with lunora() plugin`);
  return { code: 0, files: [target], target: cwd };
};
const patchExistingViteConfig = (viteConfigPath, cwd, logger) => {
  let source;
  try {
    source = readFileSync(viteConfigPath, "utf8");
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error(`init --in-place: could not read ${viteConfigPath}: ${message}`);
    return { code: 1, files: [], target: cwd };
  }
  const result = patchViteConfig(source);
  if (!result.changed) {
    logger.info(`${viteConfigPath}: ${result.reason ?? "no changes needed"}`);
    return { code: 0, files: [], target: cwd };
  }
  try {
    writeFileSync(viteConfigPath, result.code, "utf8");
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error(`init --in-place: could not write ${viteConfigPath}: ${message}`);
    return { code: 1, files: [], target: cwd };
  }
  logger.success(`patched ${viteConfigPath} — added lunora() plugin`);
  return { code: 0, files: [viteConfigPath], target: cwd };
};
const scaffoldLunoraDirectory = (cwd, logger) => {
  const lunoraDirectory = join$1(cwd, "lunora");
  const schemaPath = join$1(lunoraDirectory, "schema.ts");
  if (existsSync(schemaPath)) {
    logger.info(`lunora/ already present — left ${schemaPath} untouched`);
    return [];
  }
  const written = [];
  try {
    mkdirSync(lunoraDirectory, { recursive: true });
    writeFileSync(schemaPath, SAMPLE_SCHEMA, "utf8");
    written.push(schemaPath);
    const functionPath = join$1(lunoraDirectory, "messages.ts");
    if (!existsSync(functionPath)) {
      writeFileSync(functionPath, SAMPLE_FUNCTION, "utf8");
      written.push(functionPath);
    }
    logger.success(`scaffolded lunora/ (${String(written.length)} file(s))`);
  } catch (error) {
    const message = error instanceof Error ? error.message : String(error);
    logger.error(`init --here: could not scaffold lunora/: ${message}`);
  }
  return written;
};
const printFrameworkNextSteps = (detection, manager, logger) => {
  const { adapter, class: frameworkClass, framework } = detection;
  logger.info("");
  logger.info(`detected framework: ${framework} (class ${frameworkClass})`);
  logger.info("next steps:");
  logger.info(`  1. install the adapter:  ${installCommand(manager, [adapter, "@lunora/client", "@lunora/runtime", "@lunora/server"])}`);
  logger.info("  2. run codegen:          lunora codegen");
  if (frameworkClass === "A") {
    logger.info("  3. compose one worker:   wrap your worker entry with");
    logger.info("       createWorker({ httpRouter: <your framework SSR handler>, shardDO: ShardDO, ... })");
    logger.info(`  4. add the provider:     mount the ${adapter} provider in your root layout/route`);
    logger.info("  5. make a loader live:   preloadQuery() in a loader, usePreloadedQuery() in the component");
    logger.info("     see https://lunora.sh/docs/frameworks/reactive-loaders");
  } else if (frameworkClass === "B") {
    logger.info("  3. inject Lunora:        mount Lunora realtime under /_lunora/* in your server hook");
    logger.info(`       (${framework} owns its Cloudflare adapter — Lunora composes into its server entry)`);
    logger.info(`  4. add the provider:     mount the ${adapter} provider in your root layout`);
    logger.info("  5. read the guide:       https://lunora.sh/docs/frameworks/deploy");
  } else {
    logger.info("  3. add the provider:     wrap your app with the LunoraProvider from @lunora/react");
    logger.info("  4. read the guide:       https://lunora.sh/docs/frameworks/bring-your-framework");
  }
  logger.info("");
};
const findExistingViteConfig = (cwd) => {
  for (const candidate of VITE_CONFIG_CANDIDATES) {
    const full = join$1(cwd, candidate);
    if (existsSync(full)) {
      return full;
    }
  }
  return void 0;
};
const patchOrCreateViteConfig = (cwd, framework, logger) => {
  const viteConfigPath = findExistingViteConfig(cwd);
  if (viteConfigPath === void 0) {
    if (framework === "sveltekit" || framework === "nuxt" || framework === "astro") {
      logger.info(`no Vite config found — ${framework} wires Lunora through its server entry (see next steps)`);
      return { code: 0, files: [], target: cwd };
    }
    return createMinimalViteConfig(cwd, logger);
  }
  return patchExistingViteConfig(viteConfigPath, cwd, logger);
};
const runInPlaceInit = (cwd, logger) => {
  const detection = detectFramework(cwd);
  const viteResult = patchOrCreateViteConfig(cwd, detection.framework, logger);
  if (viteResult.code !== 0) {
    return viteResult;
  }
  const scaffolded = scaffoldLunoraDirectory(cwd, logger);
  printFrameworkNextSteps(detection, detectPackageManager(cwd), logger);
  return { code: 0, files: [...viteResult.files, ...scaffolded], target: cwd };
};
const offerIsInteractive = (options) => options.yes !== true && (options.prompt !== void 0 || (options.interactive ?? isInteractive()));
const maybeOfferExtras = async (options, projectDirectory) => {
  const interactive = offerIsInteractive(options);
  const preselected = options.add === void 0 ? [] : parseFeatureList(options.add, (message) => {
    options.logger.warn(message);
  });
  const applyAll = async (plans) => {
    if (plans.length === 0) {
      return true;
    }
    if (options.dryRun === true) {
      logWould(options.logger, `add ${plans.map((plan) => plan.label).join(", ")}`);
      return true;
    }
    const buffered = [];
    const applyLogger = isInteractive() ? {
      error: (message) => {
        buffered.push({ level: "error", message });
      },
      info: () => {
      },
      success: () => {
      },
      warn: (message) => {
        buffered.push({ level: "warn", message });
      }
    } : options.logger;
    const steps = plans.map((plan) => {
      return {
        running: `adding ${plan.label}…`,
        task: () => runAddCommand({
          allowUnsafeSource: options.allowUnsafeSource,
          cwd: projectDirectory,
          from: options.registryFrom,
          logger: applyLogger,
          names: [...plan.names],
          ref: options.ref,
          source: options.registrySource,
          transformManifest: plan.transformManifest,
          yes: true
        })
      };
    });
    const done = `added ${plans.map((plan) => plan.label).join(", ")}`;
    const results = await withTuiBadgeProgress(BADGES.add, steps, done);
    for (const { level, message } of buffered) {
      options.logger[level](message);
    }
    return results.every((result) => result.code === 0);
  };
  const deps = {
    applyAll,
    interactive,
    logger: options.logger,
    multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => tuiMultiSelect(message, choices, { ...settings, badge: BADGES.add })),
    preselected: preselected.length > 0 ? preselected : void 0,
    projectName: basename(projectDirectory),
    select: options.prompt?.select ?? ((message, choices, settings) => tuiSelect(message, choices, { ...settings, badge: BADGES.add })),
    text: options.prompt?.text ?? ((message, settings) => tuiText(message, { ...settings, badge: BADGES.add }))
  };
  if (preselected.length > 0) {
    await offerRegistryExtras(deps);
    return;
  }
  if (isInteractive()) {
    await tuiHeadline(COPY.extras);
  }
  await offerRegistryExtras(deps);
};
const DEFAULT_FRAMEWORK = "react";
const FRAMEWORK_CHOICES = [
  { description: "React SPA — official create-vite base + the Lunora layer (the default)", label: "React", value: "react" },
  { description: "Vue SPA — create-vite base + Lunora", label: "Vue", value: "vue" },
  { description: "Solid SPA — create-vite base + Lunora", label: "Solid", value: "solid" },
  { description: "Svelte SPA — create-vite base + Lunora", label: "Svelte", value: "svelte" },
  { description: "Next.js (App Router) — OpenNext on Cloudflare + a standalone Lunora worker", label: "Next.js", value: "next" },
  { description: "TanStack Start (React) — SSR with live-loader routes", label: "TanStack Start · React", value: "tanstack-start-react" },
  { description: "TanStack Start (Solid)", label: "TanStack Start · Solid", value: "tanstack-start-solid" },
  { description: "React Router (v7, framework mode) — SSR composed into the Lunora worker", label: "React Router", value: "react-router" },
  { description: "Astro + a standalone Lunora worker", label: "Astro", value: "astro" },
  { description: "AnalogJS (Angular) — single-worker, Lunora mounted in Nitro", label: "Analog", value: "analog" },
  { description: "Nuxt (Vue) — single-worker, Lunora mounted in Nitro", label: "Nuxt", value: "nuxt" },
  { description: "SvelteKit + a standalone Lunora worker", label: "SvelteKit", value: "sveltekit" },
  { description: "React Native (Expo) — an iOS/Android/web app + a Lunora worker backend", label: "React Native · Expo", value: "expo" },
  { description: "Worker only — no frontend", label: "Standalone", value: "standalone" }
];
const OVERLAY_VALUES = Object.keys(ADAPTERS).join("|");
const TEMPLATE_VALUES = FRAMEWORK_CHOICES.filter((choice) => !isOverlayFramework(choice.value)).map((choice) => choice.value).join("|");
const toScaffoldChoice = (value) => isOverlayFramework(value) ? { framework: value, kind: "overlay" } : { kind: "template", templateType: value };
const resolveScaffoldChoice = async (options) => {
  if (options.vite !== void 0) {
    return { framework: options.vite, kind: "overlay" };
  }
  if (options.templateType !== void 0) {
    return { kind: "template", templateType: options.templateType };
  }
  if (!isInteractive() || options.yes === true) {
    return { framework: DEFAULT_FRAMEWORK, kind: "overlay" };
  }
  return toScaffoldChoice(await tuiSelect(COPY.framework, FRAMEWORK_CHOICES, { badge: BADGES.tmpl, default: DEFAULT_FRAMEWORK }) ?? DEFAULT_FRAMEWORK);
};
const nonInteractiveInitError = (options) => {
  if (isInteractive() || options.yes === true) {
    return void 0;
  }
  const missing = [];
  if (options.name === void 0) {
    missing.push("a project name (`lunora init <name>`)");
  }
  if (options.templateType === void 0 && options.vite === void 0) {
    missing.push(`a framework — \`--vite <${OVERLAY_VALUES}>\` for an SPA, or \`-t <${TEMPLATE_VALUES}>\` for a bespoke template`);
  }
  if (missing.length === 0) {
    return void 0;
  }
  return `lunora init can't prompt in a non-interactive terminal — provide ${missing.join(" and ")}, or pass --yes to accept the defaults.`;
};
const scaffoldOverlayPath = async (options, framework, name, target) => {
  if (!isOverlayFramework(framework)) {
    options.logger.error(`init: unknown framework "${framework}". Supported overlays: ${Object.keys(ADAPTERS).join(", ")}.`);
    return { code: 1, files: [], target };
  }
  if (!await verifyRemoteTemplate({ isLocal: options.overlayBaseFrom !== void 0, logger: options.logger })) {
    return { code: 1, files: [], target };
  }
  mkdirSync(target, { recursive: true });
  return scaffoldViteOverlay({ framework, logger: options.logger, name, overlayBaseFrom: options.overlayBaseFrom, target });
};
const scaffoldTemplatePath = async (options, templateType, name, target) => {
  if (options.from !== void 0) {
    return await scaffoldFromLocal(options.from, templateType, target, name, options.logger);
  }
  if (options.source !== void 0 && options.source.length > 0 && !options.allowUnsafeSource && !isSafeSource(options.source)) {
    options.logger.error(
      `init: refusing --source ${options.source} — only gh:, github:, or https:// sources are allowed (and may not contain ".."). Re-run with --allow-unsafe-source if you really want this.`
    );
    return { code: 1, files: [], target };
  }
  if (!await verifyRemoteTemplate({ isLocal: false, logger: options.logger, source: resolveTemplateSource(templateType, options.source, options.ref) })) {
    return { code: 1, files: [], target };
  }
  return scaffoldFromRemote({ logger: options.logger, name, ref: options.ref, source: options.source, target, templateType });
};
const scaffoldNewProject = async (options, cwd, recordTarget) => {
  await tuiMoonrise("realtime backend on Cloudflare Workers + Durable Objects");
  const blocked = nonInteractiveInitError(options);
  if (blocked !== void 0) {
    options.logger.error(blocked);
    return { code: 1, files: [], target: "" };
  }
  const suggestedName = generateProjectName();
  const rawName = options.name ?? await tuiText(COPY.name, { badge: BADGES.dir, default: suggestedName, placeholder: suggestedName });
  const choice = await resolveScaffoldChoice(options);
  const name = rawName.trim();
  if (name.length === 0) {
    options.logger.error(`init: refusing an empty project name — pass a directory name (e.g. \`lunora init my-app\`).`);
    return { code: 1, files: [], target: "" };
  }
  if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
    options.logger.error(`init: refusing project name "${name}" — must not contain path separators or be "." / "..".`);
    return { code: 1, files: [], target: "" };
  }
  const target = resolve(cwd, name);
  const targetPreExisted = existsSync(target);
  if (targetPreExisted) {
    const entries = readdirSync(target);
    if (entries.length > 0) {
      options.logger.error(`target directory not empty: ${target}`);
      return { code: 1, files: [], target };
    }
  }
  if (options.dryRun === true) {
    const what = choice.kind === "overlay" ? `the ${choice.framework} create-vite overlay` : `the ${choice.templateType} template`;
    logWould(options.logger, `scaffold ${what} into ${target}`);
    return { code: 0, files: [], target };
  }
  recordTarget(target, targetPreExisted);
  return choice.kind === "overlay" ? scaffoldOverlayPath(options, choice.framework, name, target) : scaffoldTemplatePath(options, choice.templateType, name, target);
};
const resetScaffoldOnCancel = (cleanup, logger) => {
  const { target, targetPreExisted } = cleanup;
  if (target === void 0 || !existsSync(target)) {
    return;
  }
  if (targetPreExisted === true) {
    for (const entry of readdirSync(target)) {
      rmSync(join$1(target, entry), { force: true, recursive: true });
    }
  } else {
    rmSync(target, { force: true, recursive: true });
  }
  logger.info(`removed the partially-created project at ${target}`);
};
const runScaffoldStep = async (options, cwd, recordTarget) => {
  if (options.inPlace !== true) {
    return scaffoldNewProject(options, cwd, recordTarget);
  }
  if (options.dryRun === true) {
    logWould(options.logger, `configure Lunora into ${cwd}`);
    return { code: 0, files: [], target: cwd };
  }
  return runInPlaceInit(cwd, options.logger);
};
const runPostScaffold = async (options, result, cwd) => {
  await maybeOfferExtras(options, result.target);
  const installedManager = options.inPlace === true ? void 0 : await maybeOfferInstall(options, result.target);
  if (options.inPlace !== true) {
    await maybeOfferGit(options, result.target);
    const manager = installedManager ?? detectPackageManager(result.target);
    await printNextSteps(basename(result.target), installedManager, manager, isInsideMonorepo(cwd));
    await emitMascot(options.logger);
  }
};
const scaffoldCiPipeline = (options, result, cwd) => {
  if (result.code !== 0 || options.ci === void 0) {
    return;
  }
  if (options.dryRun === true) {
    logWould(options.logger, `scaffold a ${options.ci} CI deploy pipeline`);
    return;
  }
  scaffoldCiWorkflow(options.inPlace === true ? cwd : result.target, options.ci, options.logger);
};
const runInitCommand = async (options) => {
  const cwd = options.cwd ?? process.cwd();
  const cleanup = {};
  let result;
  try {
    result = await runScaffoldStep(options, cwd, (target, preExisted) => {
      cleanup.target = target;
      cleanup.targetPreExisted = preExisted;
    });
    if (result.code === 0 && result.target !== "") {
      cleanup.target = void 0;
      await runPostScaffold(options, result, cwd);
    }
  } catch (error) {
    if (error instanceof PromptCancelledError) {
      resetScaffoldOnCancel(cleanup, options.logger);
      process.stdout.write("\n  ✖  Setup cancelled — run `lunora init` again whenever you're ready. 🌙\n");
      return { code: 130, files: [], target: "" };
    }
    throw error;
  }
  scaffoldCiPipeline(options, result, cwd);
  return result;
};
const isTemplate = (value) => value === "analog" || value === "astro" || value === "expo" || value === "next" || value === "nuxt" || value === "react-router" || value === "standalone" || value === "sveltekit" || value === "tanstack-start-react" || value === "tanstack-start-solid";
const resolveCiProvider = (raw, logger) => {
  if (raw === void 0) {
    return void 0;
  }
  if (isCiProvider(raw)) {
    return raw;
  }
  logger.warn(`init: unknown --ci "${raw}" — expected github | gitlab; skipping CI scaffold.`);
  return void 0;
};
const execute = defineHandler(({ argument, cwd, logger, options }) => {
  const templateType = options.template !== void 0 && isTemplate(options.template) ? options.template : void 0;
  return runInitCommand({
    add: options.add,
    allowUnsafeSource: options.allowUnsafeSource === true,
    cwd,
    ci: resolveCiProvider(options.ci, logger),
    dryRun: options.dryRun === true,
    from: options.from,
    inPlace: options.here === true,
    interactive: options.interactive === true ? true : void 0,
    logger,
    name: argument[0],
    ref: options.ref,
    source: options.source,
    templateType,
    vite: options.vite,
    yes: options.yes === true
  });
});

export { execute, isTemplate, resolveTemplateSource, runInitCommand };