@lunora/cli 1.0.0-alpha.4 → 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 +4 -3
  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 +3 -5
  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 +5 -48
  15. package/dist/packem_chunks/runDeployCommand.mjs +1 -1
  16. package/dist/packem_chunks/runInitCommand.mjs +488 -46
  17. package/dist/packem_chunks/runMigrateGenerateCommand.mjs +4 -4
  18. package/dist/packem_chunks/runResetCommand.mjs +3 -3
  19. package/dist/packem_shared/{COMMANDS-CHw4zOZ9.mjs → COMMANDS-Bn8luojF.mjs} +10 -2
  20. package/dist/packem_shared/{commands-SUPdjsu5.mjs → commands-DqsEzojt.mjs} +5 -4
  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-Txwh1Xw1.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-XHFxlOg5.mjs +269 -0
  26. package/package.json +6 -5
  27. /package/dist/packem_shared/{defaultSpawner-DxI3mebw.mjs → createRecordingSpawner-DxI3mebw.mjs} +0 -0
@@ -1,15 +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
- import { detectFramework as detectFramework$1, isInteractive, promptSelect, promptMultiSelect } from '@lunora/config';
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-SUPdjsu5.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';
13
16
  import { p as promptAuthProvider, E as EMAIL_ITEM } from '../packem_shared/features-ocSSpZtS.mjs';
14
17
 
15
18
  const GITHUB_CONTENT = `name: Deploy
@@ -284,11 +287,16 @@ const patchViteConfig = (source) => {
284
287
 
285
288
  const STACK_FEATURE_OPTIONS = [
286
289
  { description: "Sign-up / sign-in (asks which provider)", label: "Authentication", value: "auth" },
287
- { 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" }
288
296
  ];
289
297
  const offerRegistryExtras = async (deps) => {
290
298
  if (!deps.interactive) {
291
- 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>`.");
292
300
  return;
293
301
  }
294
302
  const picked = await deps.multiSelect("Which features do you want to add?", STACK_FEATURE_OPTIONS, { defaults: [] });
@@ -297,11 +305,268 @@ const offerRegistryExtras = async (deps) => {
297
305
  const provider = await promptAuthProvider(deps.select);
298
306
  await deps.apply([provider]);
299
307
  } else {
300
- await deps.apply([EMAIL_ITEM]);
308
+ await deps.apply([feature === "email" ? EMAIL_ITEM : feature]);
301
309
  }
302
310
  }
303
311
  };
304
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
+
305
570
  const TEXT_EXTENSIONS = /* @__PURE__ */ new Set([".gitignore", ".html", ".js", ".json", ".jsonc", ".md", ".mjs", ".ts", ".tsx"]);
306
571
  const VITE_CONFIG_CANDIDATES = ["vite.config.ts", "vite.config.mts", "vite.config.js", "vite.config.mjs"];
307
572
  const MINIMAL_VITE_CONFIG = `import { defineConfig } from "vite";
@@ -404,12 +669,60 @@ const isSafeSource = (source) => {
404
669
  }
405
670
  return source.startsWith("gh:") || source.startsWith("github:") || source.startsWith("https://");
406
671
  };
407
- const logScaffoldSuccess = (logger, written, target, name) => {
672
+ const logScaffoldSuccess = (logger, written, target) => {
408
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";
409
686
  logger.info("next steps:");
410
687
  logger.info(` cd ${name}`);
411
- logger.info(" pnpm install");
412
- 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;
413
726
  };
414
727
  const scaffoldFromLocal = (fromRoot, templateType, target, name, logger) => {
415
728
  const templateDirectory = join$1(fromRoot, templateType);
@@ -418,7 +731,7 @@ const scaffoldFromLocal = (fromRoot, templateType, target, name, logger) => {
418
731
  return { code: 1, files: [], target };
419
732
  }
420
733
  const written = copyTemplate(templateDirectory, target, name);
421
- logScaffoldSuccess(logger, written, target, name);
734
+ logScaffoldSuccess(logger, written, target);
422
735
  return { code: 0, files: written, target };
423
736
  };
424
737
  const scaffoldFromRemote = async (options) => {
@@ -427,22 +740,25 @@ const scaffoldFromRemote = async (options) => {
427
740
  const stagingDirectory = join$1(stagingRoot, "template");
428
741
  try {
429
742
  const remote = resolveTemplateSource(templateType, source, ref);
430
- logger.info(`fetching template from ${remote}`);
431
- const downloaded = await downloadTemplate(remote, {
432
- cwd: stagingRoot,
433
- dir: stagingDirectory,
434
- force: true,
435
- install: false,
436
- silent: true
437
- });
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
+ );
438
753
  const staged = collectFiles(stagingDirectory);
439
- if (downloaded.commit) {
440
- logger.info(`resolved ${downloaded.source} @ ${downloaded.commit} (${String(staged.length)} file(s))`);
441
- } else {
442
- logger.info(`resolved ${downloaded.source} (${String(staged.length)} file(s))`);
443
- }
444
- const written = copyTemplate(stagingDirectory, target, name);
445
- 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);
446
762
  return { code: 0, files: written, target };
447
763
  } catch (error) {
448
764
  const message = error instanceof Error ? error.message : String(error);
@@ -452,6 +768,50 @@ const scaffoldFromRemote = async (options) => {
452
768
  rmSync(stagingRoot, { force: true, recursive: true });
453
769
  }
454
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
+ };
455
815
  const createMinimalViteConfig = (cwd, logger) => {
456
816
  const target = join$1(cwd, "vite.config.ts");
457
817
  try {
@@ -570,33 +930,97 @@ const offerIsInteractive = (options) => options.yes !== true && (options.prompt
570
930
  const maybeOfferExtras = async (options, projectDirectory) => {
571
931
  const interactive = offerIsInteractive(options);
572
932
  const apply = async (names) => {
573
- const result = await runAddCommand({
574
- allowUnsafeSource: options.allowUnsafeSource,
575
- cwd: projectDirectory,
576
- from: options.registryFrom,
577
- logger: options.logger,
578
- names: [...names],
579
- ref: options.ref,
580
- source: options.registrySource,
581
- yes: true
582
- });
933
+ const applyLogger = isInteractive() ? {
934
+ error: (message) => {
935
+ options.logger.error(message);
936
+ },
937
+ info: () => {
938
+ },
939
+ success: () => {
940
+ },
941
+ warn: (message) => {
942
+ options.logger.warn(message);
943
+ }
944
+ } : options.logger;
945
+ const result = await withTuiSpinner(
946
+ `adding ${names.join(", ")}…`,
947
+ () => runAddCommand({
948
+ allowUnsafeSource: options.allowUnsafeSource,
949
+ cwd: projectDirectory,
950
+ from: options.registryFrom,
951
+ logger: applyLogger,
952
+ names: [...names],
953
+ ref: options.ref,
954
+ source: options.registrySource,
955
+ yes: true
956
+ })
957
+ );
583
958
  return result.code === 0;
584
959
  };
960
+ await tuiIntro("let's finish setting up your app");
585
961
  await offerRegistryExtras({
586
962
  apply,
587
963
  interactive,
588
964
  logger: options.logger,
589
- multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => promptMultiSelect(message, choices, settings)),
590
- select: options.prompt?.select ?? ((message, choices, settings) => promptSelect(message, choices, settings))
965
+ multiSelect: options.prompt?.multiSelect ?? ((message, choices, settings) => tuiMultiSelect(message, choices, settings)),
966
+ select: options.prompt?.select ?? ((message, choices, settings) => tuiSelect(message, choices, settings))
591
967
  });
592
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.`;
1014
+ };
593
1015
  const scaffoldNewProject = async (options, cwd) => {
594
- const name = options.name ?? "lunora-app";
595
- const templateType = options.templateType ?? "vite";
596
- if (templateType === "next") {
597
- options.logger.warn('template "next" is not yet available — re-run with `-t vite` 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);
598
1020
  return { code: 1, files: [], target: "" };
599
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);
600
1024
  if (name.includes("/") || name.includes("\\") || name === ".." || name === ".") {
601
1025
  options.logger.error(`init: refusing project name "${name}" — must not contain path separators or be "." / "..".`);
602
1026
  return { code: 1, files: [], target: "" };
@@ -609,6 +1033,19 @@ const scaffoldNewProject = async (options, cwd) => {
609
1033
  return { code: 1, files: [], target };
610
1034
  }
611
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
+ }
612
1049
  if (options.from !== void 0) {
613
1050
  return scaffoldFromLocal(options.from, templateType, target, name, options.logger);
614
1051
  }
@@ -625,13 +1062,18 @@ const runInitCommand = async (options) => {
625
1062
  const result = options.inPlace === true ? runInPlaceInit(cwd, options.logger) : await scaffoldNewProject(options, cwd);
626
1063
  if (result.code === 0 && result.target !== "") {
627
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
+ }
628
1070
  }
629
1071
  if (result.code === 0 && options.ci !== void 0) {
630
1072
  scaffoldCiWorkflow(options.inPlace === true ? cwd : result.target, options.ci, options.logger);
631
1073
  }
632
1074
  return result;
633
1075
  };
634
- const isTemplate = (value) => value === "astro" || value === "next" || value === "nuxt" || value === "standalone" || value === "sveltekit" || value === "tanstack-start-react" || value === "tanstack-start-solid" || value === "vite";
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";
635
1077
  const resolveCiProvider = (raw, logger) => {
636
1078
  if (raw === void 0) {
637
1079
  return void 0;
@@ -643,8 +1085,7 @@ const resolveCiProvider = (raw, logger) => {
643
1085
  return void 0;
644
1086
  };
645
1087
  const execute = defineHandler(({ argument, cwd, logger, options }) => {
646
- const templateRaw = options.template ?? "vite";
647
- const template = isTemplate(templateRaw) ? templateRaw : "vite";
1088
+ const templateType = options.template !== void 0 && isTemplate(options.template) ? options.template : void 0;
648
1089
  return runInitCommand({
649
1090
  allowUnsafeSource: options.allowUnsafeSource === true,
650
1091
  cwd,
@@ -656,7 +1097,8 @@ const execute = defineHandler(({ argument, cwd, logger, options }) => {
656
1097
  name: argument[0],
657
1098
  ref: options.ref,
658
1099
  source: options.source,
659
- templateType: template,
1100
+ templateType,
1101
+ vite: options.vite,
660
1102
  yes: options.yes === true
661
1103
  });
662
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";