@lunora/cli 1.0.0-alpha.5 → 1.0.0-alpha.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/bin.mjs +1 -1
  2. package/dist/index.d.mts +50 -10
  3. package/dist/index.d.ts +50 -10
  4. package/dist/index.mjs +5 -5
  5. package/dist/packem_chunks/handler.mjs +2 -2
  6. package/dist/packem_chunks/handler14.mjs +1 -1
  7. package/dist/packem_chunks/handler16.mjs +1 -1
  8. package/dist/packem_chunks/handler18.mjs +1 -1
  9. package/dist/packem_chunks/handler19.mjs +1 -1
  10. package/dist/packem_chunks/handler2.mjs +1 -1
  11. package/dist/packem_chunks/handler21.mjs +1 -1
  12. package/dist/packem_chunks/handler5.mjs +1 -1
  13. package/dist/packem_chunks/handler6.mjs +1 -1
  14. package/dist/packem_chunks/planDevCommand.mjs +4 -48
  15. package/dist/packem_chunks/runDeployCommand.mjs +1 -1
  16. package/dist/packem_chunks/runInitCommand.mjs +459 -35
  17. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +4 -4
  18. package/dist/packem_chunks/runResetCommand.mjs +1 -1
  19. package/dist/packem_shared/{COMMANDS-DXaq12xm.mjs → COMMANDS-Bn8luojF.mjs} +10 -2
  20. package/dist/packem_shared/{commands-B9nASOYd.mjs → commands-DqsEzojt.mjs} +2 -2
  21. package/dist/packem_shared/detect-package-manager-DYp7n3mJ.mjs +61 -0
  22. package/dist/packem_shared/{diffSnapshots-RR2ZE8Ya.mjs → diffSnapshots-BeDvvNiF.mjs} +1 -1
  23. package/dist/packem_shared/{runAddCommand-BF7XreDW.mjs → runAddCommand-G544_v6e.mjs} +1 -1
  24. package/dist/packem_shared/{schemaIrToSnapshot-aBTo7TM5.mjs → schemaIrToSnapshot-DdsljJT-.mjs} +1 -1
  25. package/dist/packem_shared/{tui-prompts-Bm15GPJA.mjs → tui-prompts-XHFxlOg5.mjs} +41 -1
  26. package/package.json +3 -5
  27. /package/dist/packem_shared/{defaultSpawner-DxI3mebw.mjs → createRecordingSpawner-DxI3mebw.mjs} +0 -0
@@ -1,16 +1,18 @@
1
- import { existsSync, mkdirSync, writeFileSync, readdirSync, mkdtempSync, rmSync, readFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, mkdtempSync, cpSync, rmSync, renameSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
3
  import { detectFramework as detectFramework$1, isInteractive } from '@lunora/config';
4
4
  import { walkSync } from '@visulima/fs';
5
- import { resolve, join as join$1, relative, dirname as dirname$1, basename } from '@visulima/path';
5
+ import { join as join$1, dirname as dirname$1, basename, resolve, relative } from '@visulima/path';
6
6
  import { downloadTemplate } from 'giget';
7
7
  import { modify, applyEdits } from 'jsonc-parser';
8
8
  import { join, dirname } from 'node:path';
9
9
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
10
+ import { a as detectInstalledManagers, i as installArgsFor } from '../packem_shared/detect-package-manager-DYp7n3mJ.mjs';
10
11
  import MagicString from 'magic-string';
11
12
  import { Project, SyntaxKind } from 'ts-morph';
12
- import { c as resolveSourceRef, d as resolveDistTag, r as runAddCommand } from '../packem_shared/commands-B9nASOYd.mjs';
13
- import { b as tuiIntro, d as tuiOutro, t as tuiSelect, e as tuiMultiSelect, w as withTuiSpinner } from '../packem_shared/tui-prompts-Bm15GPJA.mjs';
13
+ import { c as resolveDistTag, d as resolveSourceRef, r as runAddCommand } from '../packem_shared/commands-DqsEzojt.mjs';
14
+ import { defaultSpawner } from '../packem_shared/createRecordingSpawner-DxI3mebw.mjs';
15
+ import { b as tuiOutro, d as tuiBanner, e as tuiText, f as tuiIntro, t as tuiSelect, w as withTuiSpinner, a as tuiConfirm, g as tuiMultiSelect } from '../packem_shared/tui-prompts-XHFxlOg5.mjs';
14
16
  import { p as promptAuthProvider, E as EMAIL_ITEM } from '../packem_shared/features-ocSSpZtS.mjs';
15
17
 
16
18
  const GITHUB_CONTENT = `name: Deploy
@@ -285,11 +287,16 @@ const patchViteConfig = (source) => {
285
287
 
286
288
  const STACK_FEATURE_OPTIONS = [
287
289
  { description: "Sign-up / sign-in (asks which provider)", label: "Authentication", value: "auth" },
288
- { description: "Cloudflare Email Workers + a dev mail catcher", label: "Transactional email", value: "email" }
290
+ { description: "Cloudflare Email Workers + a dev mail catcher", label: "Transactional email", value: "email" },
291
+ { description: "Typed R2 buckets + signed URLs (@lunora/storage)", label: "File storage", value: "storage" },
292
+ { description: "Token-bucket / sliding-window limits (@lunora/ratelimit)", label: "Rate limiting", value: "ratelimit" },
293
+ { description: "Scheduled jobs via Cron Triggers (@lunora/scheduler)", label: "Cron jobs", value: "crons" },
294
+ { description: "Live presence / who's-online over hibernated WebSockets", label: "Presence", value: "presence" },
295
+ { description: "Snapshot + restore your Durable Object data", label: "Backups", value: "backup" }
289
296
  ];
290
297
  const offerRegistryExtras = async (deps) => {
291
298
  if (!deps.interactive) {
292
- deps.logger.info("tip: add authentication or email later with `lunora add auth` / `lunora add email`.");
299
+ deps.logger.info("tip: add features later with `lunora add <auth|email|storage|ratelimit|crons|presence|backup>`.");
293
300
  return;
294
301
  }
295
302
  const picked = await deps.multiSelect("Which features do you want to add?", STACK_FEATURE_OPTIONS, { defaults: [] });
@@ -298,11 +305,268 @@ const offerRegistryExtras = async (deps) => {
298
305
  const provider = await promptAuthProvider(deps.select);
299
306
  await deps.apply([provider]);
300
307
  } else {
301
- await deps.apply([EMAIL_ITEM]);
308
+ await deps.apply([feature === "email" ? EMAIL_ITEM : feature]);
302
309
  }
303
310
  }
304
311
  };
305
312
 
313
+ const READ_URL = `const url = (import.meta.env.VITE_LUNORA_URL as string | undefined) ?? globalThis.location.origin;`;
314
+ const REACT_MAIN = `import "./index.css";
315
+
316
+ import { LunoraProvider } from "@lunora/react";
317
+ import { LunoraClient } from "lunorash/client";
318
+ import { StrictMode } from "react";
319
+ import { createRoot } from "react-dom/client";
320
+
321
+ import App from "./App.tsx";
322
+
323
+ // \`@lunora/vite\` runs the Worker on the same origin as Vite, so default to
324
+ // \`location.origin\`. Point \`VITE_LUNORA_URL\` at a deployed Worker to develop
325
+ // the client against production data.
326
+ ${READ_URL}
327
+ const client = new LunoraClient({ url });
328
+
329
+ const root = document.getElementById("root");
330
+
331
+ if (!root) {
332
+ throw new Error("missing #root mount node");
333
+ }
334
+
335
+ createRoot(root).render(
336
+ <StrictMode>
337
+ <LunoraProvider client={client}>
338
+ <App />
339
+ </LunoraProvider>
340
+ </StrictMode>,
341
+ );
342
+ `;
343
+ const VUE_MAIN = `import "./style.css";
344
+
345
+ import { createLunora } from "@lunora/vue";
346
+ import { LunoraClient } from "lunorash/client";
347
+ import { createApp } from "vue";
348
+
349
+ import App from "./App.vue";
350
+
351
+ // Provide one LunoraClient at the app root via the Vue plugin form.
352
+ ${READ_URL}
353
+ createApp(App).use(createLunora(new LunoraClient({ url }))).mount("#app");
354
+ `;
355
+ const SOLID_INDEX = `import "./index.css";
356
+
357
+ import { LunoraContext } from "@lunora/solid";
358
+ import { LunoraClient } from "lunorash/client";
359
+ import { render } from "solid-js/web";
360
+
361
+ import App from "./App";
362
+
363
+ ${READ_URL}
364
+ const client = new LunoraClient({ url });
365
+ const root = document.getElementById("root");
366
+
367
+ render(
368
+ () => (
369
+ <LunoraContext.Provider value={client}>
370
+ <App />
371
+ </LunoraContext.Provider>
372
+ ),
373
+ root!,
374
+ );
375
+ `;
376
+ const SVELTE_ROOT = `<script lang="ts">
377
+ import { setLunoraClient } from "@lunora/svelte";
378
+ import { LunoraClient } from "lunorash/client";
379
+
380
+ import App from "./App.svelte";
381
+
382
+ ${READ_URL}
383
+ setLunoraClient(new LunoraClient({ url }));
384
+ <\/script>
385
+
386
+ <App />
387
+ `;
388
+ const SVELTE_MAIN = `import "./app.css";
389
+
390
+ import { mount } from "svelte";
391
+
392
+ import Root from "./Root.svelte";
393
+
394
+ // Mount \`Root\` (it sets the ambient LunoraClient) rather than \`App\` directly.
395
+ const app = mount(Root, { target: document.getElementById("app")! });
396
+
397
+ export default app;
398
+ `;
399
+ const VANILLA_MAIN = `import "./style.css";
400
+
401
+ import { LunoraClient } from "lunorash/client";
402
+
403
+ import { api } from "../lunora/_generated/api";
404
+
405
+ // Vanilla starter: no framework provider — talk to Lunora through the client
406
+ // directly. \`@lunora/vite\` runs the Worker on the same origin as Vite.
407
+ ${READ_URL}
408
+ const client = new LunoraClient({ url });
409
+
410
+ const root = document.querySelector<HTMLDivElement>("#app")!;
411
+
412
+ const heading = document.createElement("h1");
413
+ heading.textContent = "Vite + Lunora";
414
+
415
+ const output = document.createElement("pre");
416
+ root.replaceChildren(heading, output);
417
+
418
+ const render = (messages: unknown): void => {
419
+ // textContent (not innerHTML) — never inject server data as markup.
420
+ output.textContent = JSON.stringify(messages, null, 2);
421
+ };
422
+
423
+ // Live subscription: the list re-renders on every server delta.
424
+ client.onUpdate(api.messages.list, { channelId: "channel:demo" }, render);
425
+ `;
426
+ const ADAPTERS = {
427
+ react: { adapter: "@lunora/react", createViteTemplate: "react-ts", files: [{ contents: REACT_MAIN, path: "src/main.tsx" }], label: "React" },
428
+ solid: { adapter: "@lunora/solid", createViteTemplate: "solid", files: [{ contents: SOLID_INDEX, path: "src/index.tsx" }], label: "Solid" },
429
+ svelte: {
430
+ adapter: "@lunora/svelte",
431
+ createViteTemplate: "svelte-ts",
432
+ files: [
433
+ { contents: SVELTE_ROOT, path: "src/Root.svelte" },
434
+ { contents: SVELTE_MAIN, path: "src/main.ts" }
435
+ ],
436
+ label: "Svelte"
437
+ },
438
+ vanilla: { adapter: "lunorash/client", createViteTemplate: "vanilla-ts", files: [{ contents: VANILLA_MAIN, path: "src/main.ts" }], label: "Vanilla" },
439
+ vue: { adapter: "@lunora/vue", createViteTemplate: "vue-ts", files: [{ contents: VUE_MAIN, path: "src/main.ts" }], label: "Vue" }
440
+ };
441
+ const isOverlayFramework = (value) => Object.hasOwn(ADAPTERS, value);
442
+
443
+ const LUNORA_SCHEMA = `import { defineSchema, defineTable, v } from "lunorash/server";
444
+
445
+ export default defineSchema({
446
+ messages: defineTable({
447
+ channelId: v.string(),
448
+ text: v.string(),
449
+ })
450
+ .shardBy("channelId")
451
+ .index("by_channel", ["channelId"]),
452
+ });
453
+ `;
454
+ const LUNORA_MESSAGES = `import { mutation, query, v } from "./_generated/server.js";
455
+
456
+ export const list = query.input({ channelId: v.string(), limit: v.optional(v.number()) }).query(async ({ args }) => {
457
+ return { channelId: args.channelId, limit: args.limit ?? 50, messages: [] };
458
+ });
459
+
460
+ export const send = mutation.input({ channelId: v.string(), text: v.string() }).mutation(async ({ args }) => {
461
+ return { channelId: args.channelId, text: args.text };
462
+ });
463
+ `;
464
+ const SERVER_ENTRY = `import type { ShardNamespaceLike } from "lunorash/runtime";
465
+
466
+ import { defineApp } from "../lunora/_generated/app.js";
467
+
468
+ interface Env extends Record<string, unknown> {
469
+ SHARD: ShardNamespaceLike;
470
+ }
471
+
472
+ const app = defineApp<Env>()
473
+ .shard((env) => env.SHARD)
474
+ .build();
475
+
476
+ export const ShardDO = app.ShardDO;
477
+ export default app;
478
+ `;
479
+ const WRANGLER = `{
480
+ "$schema": "node_modules/wrangler/config-schema.json",
481
+ "name": "__NAME__",
482
+ "main": "src/server.ts",
483
+ "compatibility_date": "2026-06-10",
484
+ "compatibility_flags": ["nodejs_compat"],
485
+ "durable_objects": {
486
+ "bindings": [{ "name": "SHARD", "class_name": "ShardDO" }],
487
+ },
488
+ "migrations": [{ "tag": "v1", "new_sqlite_classes": ["ShardDO"] }],
489
+ "observability": { "enabled": true, "head_sampling_rate": 1 },
490
+ }
491
+ `;
492
+ const GITIGNORE_ADDITIONS = [".wrangler", ".lunora/", ".lunora-cache", "lunora/_generated"];
493
+ const COMMON_DEV_DEPENDENCIES = {
494
+ "@cloudflare/workers-types": "^4.20260611.1",
495
+ wrangler: "^4.100.0"
496
+ };
497
+ const writeFile = (target, relativePath, contents, written) => {
498
+ const destination = join$1(target, relativePath);
499
+ mkdirSync(dirname$1(destination), { recursive: true });
500
+ writeFileSync(destination, contents, "utf8");
501
+ written.push(destination);
502
+ };
503
+ const NEWLINE = /\r?\n/;
504
+ const ensureGitignore = (target) => {
505
+ const path = join$1(target, ".gitignore");
506
+ const existing = existsSync(path) ? readFileSync(path, "utf8") : "";
507
+ const missing = GITIGNORE_ADDITIONS.filter((entry) => !existing.split(NEWLINE).includes(entry));
508
+ if (missing.length === 0) {
509
+ return;
510
+ }
511
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
512
+ writeFileSync(path, `${existing}${prefix}
513
+ # Lunora
514
+ ${missing.join("\n")}
515
+ `, "utf8");
516
+ };
517
+ const stampRange = (name, range, distTag) => name === "lunorash" || name.startsWith("@lunora/") ? distTag : range;
518
+ const withDependency = (map, name, range, distTag) => {
519
+ return { ...map, [name]: stampRange(name, range, distTag) };
520
+ };
521
+ const restampLunora = (map, distTag) => Object.fromEntries(Object.entries(map).map(([name, range]) => [name, stampRange(name, range, distTag)]));
522
+ const patchPackageJson = (target, name, adapter, distTag) => {
523
+ const path = join$1(target, "package.json");
524
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
525
+ let dependencies = withDependency(parsed.dependencies ?? {}, "lunorash", distTag, distTag);
526
+ if (adapter.adapter.startsWith("@lunora/")) {
527
+ dependencies = withDependency(dependencies, adapter.adapter, distTag, distTag);
528
+ }
529
+ for (const [depName, range] of Object.entries(adapter.extraDependencies ?? {})) {
530
+ dependencies = withDependency(dependencies, depName, range, distTag);
531
+ }
532
+ let devDependencies = withDependency(parsed.devDependencies ?? {}, "@lunora/vite", distTag, distTag);
533
+ for (const [depName, range] of Object.entries(COMMON_DEV_DEPENDENCIES)) {
534
+ devDependencies = withDependency(devDependencies, depName, range, distTag);
535
+ }
536
+ parsed.name = name;
537
+ parsed.dependencies = restampLunora(dependencies, distTag);
538
+ parsed.devDependencies = restampLunora(devDependencies, distTag);
539
+ parsed.scripts = { ...parsed.scripts, codegen: "lunora codegen", deploy: "vite build && lunora deploy" };
540
+ writeFileSync(path, `${JSON.stringify(parsed, void 0, 4)}
541
+ `, "utf8");
542
+ };
543
+ const patchBaseViteConfig = (target, logger) => {
544
+ const candidate = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"].map((file) => join$1(target, file)).find((path) => existsSync(path));
545
+ if (candidate === void 0) {
546
+ logger.warn("overlay: no vite.config found in the create-vite base — add `lunora()` to your Vite plugins manually.");
547
+ return;
548
+ }
549
+ const result = patchViteConfig(readFileSync(candidate, "utf8"));
550
+ if (result.changed) {
551
+ writeFileSync(candidate, result.code, "utf8");
552
+ }
553
+ };
554
+ const applyLunoraOverlay = (options) => {
555
+ const { adapter, distTag, logger, name, target } = options;
556
+ const written = [];
557
+ writeFile(target, join$1("lunora", "schema.ts"), LUNORA_SCHEMA, written);
558
+ writeFile(target, join$1("lunora", "messages.ts"), LUNORA_MESSAGES, written);
559
+ writeFile(target, join$1("src", "server.ts"), SERVER_ENTRY, written);
560
+ writeFile(target, "wrangler.jsonc", WRANGLER.replaceAll("__NAME__", name), written);
561
+ for (const file of adapter.files) {
562
+ writeFile(target, file.path, file.contents, written);
563
+ }
564
+ patchBaseViteConfig(target, logger);
565
+ patchPackageJson(target, name, adapter, distTag);
566
+ ensureGitignore(target);
567
+ return written;
568
+ };
569
+
306
570
  const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".gitignore", ".html", ".js", ".json", ".jsonc", ".md", ".mjs", ".ts", ".tsx"]);
307
571
  const VITE_CONFIG_CANDIDATES = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
308
572
  const MINIMAL_VITE_CONFIG = `import { defineConfig } from "vite";
@@ -405,12 +669,60 @@ const isSafeSource = (source) => {
405
669
  }
406
670
  return source.startsWith("gh:") || source.startsWith("github:") || source.startsWith("https://");
407
671
  };
408
- const logScaffoldSuccess = (logger, written, target, name) => {
672
+ const logScaffoldSuccess = (logger, written, target) => {
409
673
  logger.success(`scaffolded ${String(written.length)} files into ${target}`);
674
+ };
675
+ const runScriptCommand = (manager, script) => {
676
+ if (manager === "npm") {
677
+ return `npm run ${script}`;
678
+ }
679
+ if (manager === "bun") {
680
+ return `bun run ${script}`;
681
+ }
682
+ return `${manager} ${script}`;
683
+ };
684
+ const printNextSteps = (logger, name, installed) => {
685
+ const manager = installed ?? "pnpm";
410
686
  logger.info("next steps:");
411
687
  logger.info(` cd ${name}`);
412
- logger.info(" pnpm install");
413
- logger.info(" pnpm dev");
688
+ if (installed === void 0) {
689
+ logger.info(` ${manager} install`);
690
+ }
691
+ logger.info(` ${runScriptCommand(manager, "dev")}`);
692
+ };
693
+ const offerInstallIsInteractive = (options) => options.yes !== true && (options.installPrompt !== void 0 || isInteractive());
694
+ const maybeOfferInstall = async (options, target) => {
695
+ if (!offerInstallIsInteractive(options)) {
696
+ return void 0;
697
+ }
698
+ const managers = detectInstalledManagers(options.packageManagerProbe);
699
+ const [defaultManager] = managers;
700
+ if (defaultManager === void 0) {
701
+ return void 0;
702
+ }
703
+ const confirm = options.installPrompt?.confirmInstall ?? (async () => tuiConfirm("Install dependencies now?", { defaultYes: true }));
704
+ if (!await confirm()) {
705
+ return void 0;
706
+ }
707
+ let manager = defaultManager;
708
+ if (managers.length > 1) {
709
+ manager = options.installPrompt ? await options.installPrompt.selectManager(managers) : await tuiSelect(
710
+ "Which package manager?",
711
+ managers.map((candidate) => {
712
+ return { label: candidate, value: candidate };
713
+ }),
714
+ { default: defaultManager }
715
+ ) ?? defaultManager;
716
+ }
717
+ const spawner = options.spawner ?? defaultSpawner;
718
+ const { args, command } = installArgsFor(manager);
719
+ const result = await withTuiSpinner(`Installing dependencies with ${manager}…`, () => spawner({ args, command, cwd: target }));
720
+ if (result.code !== 0) {
721
+ options.logger.warn(`\`${command} install\` exited with code ${String(result.code)} — run it yourself in ${basename(target)}/.`);
722
+ return void 0;
723
+ }
724
+ options.logger.success(`installed dependencies with ${manager}`);
725
+ return manager;
414
726
  };
415
727
  const scaffoldFromLocal = (fromRoot, templateType, target, name, logger) => {
416
728
  const templateDirectory = join$1(fromRoot, templateType);
@@ -419,7 +731,7 @@ const scaffoldFromLocal = (fromRoot, templateType, target, name, logger) => {
419
731
  return { code: 1, files: [], target };
420
732
  }
421
733
  const written = copyTemplate(templateDirectory, target, name);
422
- logScaffoldSuccess(logger, written, target, name);
734
+ logScaffoldSuccess(logger, written, target);
423
735
  return { code: 0, files: written, target };
424
736
  };
425
737
  const scaffoldFromRemote = async (options) => {
@@ -428,22 +740,25 @@ const scaffoldFromRemote = async (options) => {
428
740
  const stagingDirectory = join$1(stagingRoot, "template");
429
741
  try {
430
742
  const remote = resolveTemplateSource(templateType, source, ref);
431
- logger.info(`fetching template from ${remote}`);
432
- const downloaded = await downloadTemplate(remote, {
433
- cwd: stagingRoot,
434
- dir: stagingDirectory,
435
- force: true,
436
- install: false,
437
- silent: true
438
- });
743
+ const downloaded = await withTuiSpinner(
744
+ `Fetching the ${templateType} template…`,
745
+ () => downloadTemplate(remote, {
746
+ cwd: stagingRoot,
747
+ dir: stagingDirectory,
748
+ force: true,
749
+ install: false,
750
+ silent: true
751
+ })
752
+ );
439
753
  const staged = collectFiles(stagingDirectory);
440
- if (downloaded.commit) {
441
- logger.info(`resolved ${downloaded.source} @ ${downloaded.commit} (${String(staged.length)} file(s))`);
442
- } else {
443
- logger.info(`resolved ${downloaded.source} (${String(staged.length)} file(s))`);
444
- }
445
- const written = copyTemplate(stagingDirectory, target, name);
446
- logScaffoldSuccess(logger, written, target, name);
754
+ logger.info(
755
+ downloaded.commit ? `template: ${downloaded.source} @ ${downloaded.commit} (${String(staged.length)} files)` : `template: ${downloaded.source} (${String(staged.length)} files)`
756
+ );
757
+ const written = await withTuiSpinner(
758
+ `Scaffolding ${String(staged.length)} files into ${name}/…`,
759
+ () => Promise.resolve(copyTemplate(stagingDirectory, target, name))
760
+ );
761
+ logScaffoldSuccess(logger, written, target);
447
762
  return { code: 0, files: written, target };
448
763
  } catch (error) {
449
764
  const message = error instanceof Error ? error.message : String(error);
@@ -453,6 +768,50 @@ const scaffoldFromRemote = async (options) => {
453
768
  rmSync(stagingRoot, { force: true, recursive: true });
454
769
  }
455
770
  };
771
+ const renameCreateViteDotfiles = (directory) => {
772
+ for (const file of ["_gitignore", "_npmrc", "_gitattributes"]) {
773
+ const from = join$1(directory, file);
774
+ if (existsSync(from)) {
775
+ renameSync(from, join$1(directory, `.${file.slice(1)}`));
776
+ }
777
+ }
778
+ };
779
+ const scaffoldViteOverlay = async (options) => {
780
+ const { framework, logger, name, overlayBaseFrom, target } = options;
781
+ const adapter = ADAPTERS[framework];
782
+ const stagingRoot = mkdtempSync(join$1(tmpdir(), "lunora-vite-base-"));
783
+ try {
784
+ if (overlayBaseFrom === void 0) {
785
+ const stagingDirectory = join$1(stagingRoot, "base");
786
+ const remote = `github:vitejs/vite/packages/create-vite/template-${adapter.createViteTemplate}#main`;
787
+ await withTuiSpinner(
788
+ `Fetching the ${adapter.label} (create-vite) base…`,
789
+ () => downloadTemplate(remote, { cwd: stagingRoot, dir: stagingDirectory, force: true, install: false, silent: true })
790
+ );
791
+ renameCreateViteDotfiles(stagingDirectory);
792
+ cpSync(stagingDirectory, target, { recursive: true });
793
+ } else {
794
+ const localBase = join$1(overlayBaseFrom, `template-${adapter.createViteTemplate}`);
795
+ if (!existsSync(localBase)) {
796
+ logger.error(`create-vite base not found on disk: ${localBase}`);
797
+ return { code: 1, files: [], target };
798
+ }
799
+ cpSync(localBase, target, { recursive: true });
800
+ }
801
+ const written = await withTuiSpinner(
802
+ `Applying the Lunora overlay (${adapter.label})…`,
803
+ () => Promise.resolve(applyLunoraOverlay({ adapter, distTag: resolveDistTag(), logger, name, target }))
804
+ );
805
+ logScaffoldSuccess(logger, written, target);
806
+ return { code: 0, files: [...collectFiles(target)], target };
807
+ } catch (error) {
808
+ const message = error instanceof Error ? error.message : String(error);
809
+ logger.error(`failed to scaffold the ${adapter.label} base: ${message}`);
810
+ return { code: 1, files: [], target };
811
+ } finally {
812
+ rmSync(stagingRoot, { force: true, recursive: true });
813
+ }
814
+ };
456
815
  const createMinimalViteConfig = (cwd, logger) => {
457
816
  const target = join$1(cwd, "vite.config.ts");
458
817
  try {
@@ -606,15 +965,62 @@ const maybeOfferExtras = async (options, projectDirectory) => {
606
965
  multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => tuiMultiSelect(message, choices, settings)),
607
966
  select: options.prompt?.select ?? ((message, choices, settings) => tuiSelect(message, choices, settings))
608
967
  });
609
- await tuiOutro("you're all set — run `pnpm dev` to start.");
968
+ };
969
+ const DEFAULT_FRAMEWORK = "react";
970
+ const FRAMEWORK_CHOICES = [
971
+ { description: "React SPA — official create-vite base + the Lunora layer (the default)", label: "React", value: "react" },
972
+ { description: "Vue SPA — create-vite base + Lunora", label: "Vue", value: "vue" },
973
+ { description: "Solid SPA — create-vite base + Lunora", label: "Solid", value: "solid" },
974
+ { description: "Svelte SPA — create-vite base + Lunora", label: "Svelte", value: "svelte" },
975
+ { description: "TanStack Start (React) — SSR with live-loader routes", label: "TanStack Start · React", value: "tanstack-start-react" },
976
+ { description: "TanStack Start (Solid)", label: "TanStack Start · Solid", value: "tanstack-start-solid" },
977
+ { description: "React Router (v7, framework mode) — SSR composed into the Lunora worker", label: "React Router", value: "react-router" },
978
+ { description: "Astro + a standalone Lunora worker", label: "Astro", value: "astro" },
979
+ { description: "AnalogJS (Angular) — single-worker, Lunora mounted in Nitro", label: "Analog", value: "analog" },
980
+ { description: "Nuxt (Vue) — single-worker, Lunora mounted in Nitro", label: "Nuxt", value: "nuxt" },
981
+ { description: "SvelteKit + a standalone Lunora worker", label: "SvelteKit", value: "sveltekit" },
982
+ { description: "Worker only — no frontend", label: "Standalone", value: "standalone" }
983
+ ];
984
+ const OVERLAY_VALUES = Object.keys(ADAPTERS).join("|");
985
+ const TEMPLATE_VALUES = FRAMEWORK_CHOICES.filter((choice) => !isOverlayFramework(choice.value)).map((choice) => choice.value).join("|");
986
+ const toScaffoldChoice = (value) => isOverlayFramework(value) ? { framework: value, kind: "overlay" } : { kind: "template", templateType: value };
987
+ const resolveScaffoldChoice = async (options) => {
988
+ if (options.vite !== void 0) {
989
+ return { framework: options.vite, kind: "overlay" };
990
+ }
991
+ if (options.templateType !== void 0) {
992
+ return { kind: "template", templateType: options.templateType };
993
+ }
994
+ if (!isInteractive() || options.yes === true) {
995
+ return { framework: DEFAULT_FRAMEWORK, kind: "overlay" };
996
+ }
997
+ return toScaffoldChoice(await tuiSelect("Which framework would you like?", FRAMEWORK_CHOICES, { default: DEFAULT_FRAMEWORK }) ?? DEFAULT_FRAMEWORK);
998
+ };
999
+ const nonInteractiveInitError = (options) => {
1000
+ if (isInteractive() || options.yes === true) {
1001
+ return void 0;
1002
+ }
1003
+ const missing = [];
1004
+ if (options.name === void 0) {
1005
+ missing.push("a project name (`lunora init <name>`)");
1006
+ }
1007
+ if (options.templateType === void 0 && options.vite === void 0) {
1008
+ missing.push(`a framework — \`--vite <${OVERLAY_VALUES}>\` for an SPA, or \`-t <${TEMPLATE_VALUES}>\` for a bespoke template`);
1009
+ }
1010
+ if (missing.length === 0) {
1011
+ return void 0;
1012
+ }
1013
+ return `lunora init can't prompt in a non-interactive terminal — provide ${missing.join(" and ")}, or pass --yes to accept the defaults.`;
610
1014
  };
611
1015
  const scaffoldNewProject = async (options, cwd) => {
612
- const name = options.name ?? "lunora-app";
613
- const templateType = options.templateType ?? "vite-react";
614
- if (templateType === "next") {
615
- options.logger.warn('template "next" is not yet available — re-run with `-t vite-react` or `-t standalone`.');
1016
+ await tuiBanner("realtime backend on Cloudflare Workers + Durable Objects");
1017
+ const blocked = nonInteractiveInitError(options);
1018
+ if (blocked !== void 0) {
1019
+ options.logger.error(blocked);
616
1020
  return { code: 1, files: [], target: "" };
617
1021
  }
1022
+ const name = options.name ?? await tuiText("What should we call your project?", { default: "lunora-app", placeholder: "lunora-app" });
1023
+ const choice = await resolveScaffoldChoice(options);
618
1024
  if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
619
1025
  options.logger.error(`init: refusing project name "${name}" — must not contain path separators or be "." / "..".`);
620
1026
  return { code: 1, files: [], target: "" };
@@ -627,6 +1033,19 @@ const scaffoldNewProject = async (options, cwd) => {
627
1033
  return { code: 1, files: [], target };
628
1034
  }
629
1035
  }
1036
+ if (choice.kind === "overlay") {
1037
+ if (!isOverlayFramework(choice.framework)) {
1038
+ options.logger.error(`init: unknown framework "${choice.framework}". Supported overlays: ${Object.keys(ADAPTERS).join(", ")}.`);
1039
+ return { code: 1, files: [], target };
1040
+ }
1041
+ mkdirSync(target, { recursive: true });
1042
+ return scaffoldViteOverlay({ framework: choice.framework, logger: options.logger, name, overlayBaseFrom: options.overlayBaseFrom, target });
1043
+ }
1044
+ const { templateType } = choice;
1045
+ if (templateType === "next") {
1046
+ options.logger.warn('template "next" is not yet available — re-run with `--vite react` or `-t standalone`.');
1047
+ return { code: 1, files: [], target };
1048
+ }
630
1049
  if (options.from !== void 0) {
631
1050
  return scaffoldFromLocal(options.from, templateType, target, name, options.logger);
632
1051
  }
@@ -643,13 +1062,18 @@ const runInitCommand = async (options) => {
643
1062
  const result = options.inPlace === true ? runInPlaceInit(cwd, options.logger) : await scaffoldNewProject(options, cwd);
644
1063
  if (result.code === 0 && result.target !== "") {
645
1064
  await maybeOfferExtras(options, result.target);
1065
+ const installedManager = options.inPlace === true ? void 0 : await maybeOfferInstall(options, result.target);
1066
+ if (options.inPlace !== true) {
1067
+ await tuiOutro("you're all set 🎉");
1068
+ printNextSteps(options.logger, basename(result.target), installedManager);
1069
+ }
646
1070
  }
647
1071
  if (result.code === 0 && options.ci !== void 0) {
648
1072
  scaffoldCiWorkflow(options.inPlace === true ? cwd : result.target, options.ci, options.logger);
649
1073
  }
650
1074
  return result;
651
1075
  };
652
- const isTemplate = (value) => value === "astro" || value === "next" || value === "nuxt" || value === "standalone" || value === "sveltekit" || value === "tanstack-start-react" || value === "tanstack-start-solid" || value === "vite-react";
1076
+ const isTemplate = (value) => value === "analog" || value === "astro" || value === "next" || value === "nuxt" || value === "react-router" || value === "standalone" || value === "sveltekit" || value === "tanstack-start-react" || value === "tanstack-start-solid";
653
1077
  const resolveCiProvider = (raw, logger) => {
654
1078
  if (raw === void 0) {
655
1079
  return void 0;
@@ -661,8 +1085,7 @@ const resolveCiProvider = (raw, logger) => {
661
1085
  return void 0;
662
1086
  };
663
1087
  const execute = defineHandler(({ argument, cwd, logger, options }) => {
664
- const templateRaw = options.template ?? "vite-react";
665
- const template = isTemplate(templateRaw) ? templateRaw : "vite-react";
1088
+ const templateType = options.template !== void 0 && isTemplate(options.template) ? options.template : void 0;
666
1089
  return runInitCommand({
667
1090
  allowUnsafeSource: options.allowUnsafeSource === true,
668
1091
  cwd,
@@ -674,7 +1097,8 @@ const execute = defineHandler(({ argument, cwd, logger, options }) => {
674
1097
  name: argument[0],
675
1098
  ref: options.ref,
676
1099
  source: options.source,
677
- templateType: template,
1100
+ templateType,
1101
+ vite: options.vite,
678
1102
  yes: options.yes === true
679
1103
  });
680
1104
  });
@@ -1,13 +1,13 @@
1
- import { existsSync, mkdirSync, writeFileSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, writeFileSync, mkdtempSync, rmSync, readFileSync } from 'node:fs';
2
2
  import { tmpdir } from 'node:os';
3
- import { discoverMigrations, discoverSchema } from '@lunora/codegen';
3
+ import { discoverSchema, discoverMigrations } from '@lunora/codegen';
4
4
  import { join } from '@visulima/path';
5
5
  import { Project } from 'ts-morph';
6
6
  import { r as resolveAdminBaseUrl } from '../packem_shared/admin-url-4UzT-CI4.mjs';
7
7
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
8
- import { diffSnapshots, renderMigrationFile } from '../packem_shared/diffSnapshots-RR2ZE8Ya.mjs';
8
+ import { diffSnapshots, renderMigrationFile } from '../packem_shared/diffSnapshots-BeDvvNiF.mjs';
9
9
  import { a as resolveProductionWorkerUrl } from '../packem_shared/resolve-target-qbsJ_5sF.mjs';
10
- import schemaIrToSnapshot from '../packem_shared/schemaIrToSnapshot-aBTo7TM5.mjs';
10
+ import schemaIrToSnapshot from '../packem_shared/schemaIrToSnapshot-DdsljJT-.mjs';
11
11
  import { runExportCommand, runImportCommand } from '../packem_shared/DEFAULT_IMPORT_BATCH_SIZE-Ck-2bU08.mjs';
12
12
 
13
13
  const SNAPSHOT_FILENAME = ".snapshot.json";
@@ -1,7 +1,7 @@
1
1
  import { existsSync, rmSync } from 'node:fs';
2
2
  import { join } from '@visulima/path';
3
3
  import { d as defineHandler } from '../packem_shared/command-BDXcJCCJ.mjs';
4
- import { a as tuiConfirm } from '../packem_shared/tui-prompts-Bm15GPJA.mjs';
4
+ import { a as tuiConfirm } from '../packem_shared/tui-prompts-XHFxlOg5.mjs';
5
5
 
6
6
  const runResetCommand = async (options) => {
7
7
  const cwd = options.cwd ?? process.cwd();
@@ -361,11 +361,19 @@ const initCommand = {
361
361
  options: [
362
362
  {
363
363
  alias: "t",
364
- defaultValue: "vite-react",
365
- description: "Template to scaffold (vite-react | standalone | astro | nuxt | sveltekit | tanstack-start-react | tanstack-start-solid)",
364
+ // No default: when omitted, an interactive run shows the framework
365
+ // picker (default React overlay) and a non-interactive run errors.
366
+ // For React/Vue/Solid/Svelte SPAs use `--vite <framework>` (overlay);
367
+ // `-t` selects a bespoke template.
368
+ description: "Bespoke template (standalone | astro | nuxt | sveltekit | tanstack-start-react | tanstack-start-solid). For an SPA use --vite react|vue|solid|svelte.",
366
369
  name: "template",
367
370
  type: String
368
371
  },
372
+ {
373
+ description: "Scaffold via the create-vite overlay for a framework (react | vue | solid | svelte | vanilla) — official create-vite base + Lunora layer",
374
+ name: "vite",
375
+ type: String
376
+ },
369
377
  {
370
378
  description: "Local templates root to copy from (offline-friendly; expects <type>/ subdirs)",
371
379
  name: "from",