create-kubojs 0.2.0 → 0.2.2

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.
@@ -11,18 +11,61 @@ import path from "node:path";
11
11
  import envPaths from "env-paths";
12
12
  import fs from "fs-extra";
13
13
  import { fileURLToPath } from "node:url";
14
- import { desktopWebFrontends as desktopWebFrontends$3 } from "@kubojs/types";
15
- import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, processAddonTemplates, processAddonsDeps, processNxConfig, processPackageConfigs, processTemplateString, processTurboConfig, processVitePlusConfig } from "@kubojs/template-generator";
14
+ import { desktopWebFrontends as desktopWebFrontends$3, getPaymentCompatibilityIssue, normalizePayments } from "@kubojs/types";
15
+ import { EMBEDDED_TEMPLATES, EMBEDDED_TEMPLATES as EMBEDDED_TEMPLATES$1, GeneratorError, GeneratorError as GeneratorError$1, TEMPLATE_COUNT, VirtualFileSystem, VirtualFileSystem as VirtualFileSystem$1, dependencyVersionMap, generate, generate as generate$1, generateReproducibleCommand, processAddonTemplates, processAddonsDeps, processEnvVariables, processPackageConfigs, processTemplateString, processTurboConfig, processVitePlusConfig } from "@kubojs/template-generator";
16
16
  import { consola, createConsola } from "consola";
17
17
  import { AsyncLocalStorage } from "node:async_hooks";
18
18
  import gradient from "gradient-string";
19
19
  import { $, execa } from "execa";
20
20
  import { writeTree } from "@kubojs/template-generator/fs-writer";
21
- import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, isCancel as isCancel$1 } from "@clack/core";
21
+ import { ConfirmPrompt, GroupMultiSelectPrompt, MultiSelectPrompt, SelectPrompt, TextPrompt, isCancel as isCancel$1, isCancel as isCancel$2 } from "@clack/core";
22
22
  import { applyEdits, modify, parse } from "jsonc-parser";
23
23
  import os from "node:os";
24
24
  import { format } from "oxfmt";
25
25
  import { Generator, configSchema } from "@tanstack/router-generator";
26
+ //#region src/utils/cli-colors.ts
27
+ const KUBO_COLORS = {
28
+ signal: "#c49314",
29
+ bright: "#d6a72b",
30
+ orange: "#e08a2e",
31
+ cream: "#f2ede0"
32
+ };
33
+ const TRUECOLOR_COLORTERM = new Set(["24bit", "truecolor"]);
34
+ function parseForceColor(forceColor) {
35
+ if (forceColor === void 0 || forceColor === "") return void 0;
36
+ if (forceColor === "true") return 1;
37
+ const parsed = Number.parseInt(forceColor, 10);
38
+ return Number.isNaN(parsed) ? 1 : parsed;
39
+ }
40
+ function supportsAnsiColor(env = process.env, terminal = process.stdout) {
41
+ if (env.NO_COLOR !== void 0) return false;
42
+ const forceColor = parseForceColor(env.FORCE_COLOR);
43
+ if (forceColor === 0) return false;
44
+ if (forceColor !== void 0) return true;
45
+ return terminal.isTTY === true;
46
+ }
47
+ function supportsTrueColor(env = process.env, terminal = process.stdout) {
48
+ if (!supportsAnsiColor(env, terminal)) return false;
49
+ const forceColor = parseForceColor(env.FORCE_COLOR);
50
+ if (forceColor !== void 0) return forceColor >= 3;
51
+ const colorTerm = env.COLORTERM?.toLowerCase();
52
+ if (colorTerm !== void 0 && TRUECOLOR_COLORTERM.has(colorTerm)) return true;
53
+ return env.TERM?.toLowerCase().includes("direct") === true;
54
+ }
55
+ function createColor(hex, fallback) {
56
+ if (!supportsTrueColor()) return fallback;
57
+ const red = Number.parseInt(hex.slice(1, 3), 16);
58
+ const green = Number.parseInt(hex.slice(3, 5), 16);
59
+ const blue = Number.parseInt(hex.slice(5, 7), 16);
60
+ return (text) => `\u001B[38;2;${red};${green};${blue}m${text}\u001B[39m`;
61
+ }
62
+ const cliColors = {
63
+ signal: createColor(KUBO_COLORS.signal, pc.yellow),
64
+ bright: createColor(KUBO_COLORS.bright, pc.yellow),
65
+ orange: createColor(KUBO_COLORS.orange, pc.yellow),
66
+ cream: createColor(KUBO_COLORS.cream, pc.white)
67
+ };
68
+ //#endregion
26
69
  //#region src/utils/get-package-manager.ts
27
70
  const getUserPkgManager = () => {
28
71
  const userAgent = process.env.npm_config_user_agent;
@@ -42,7 +85,7 @@ const DEFAULT_CONFIG_BASE = {
42
85
  database: "sqlite",
43
86
  orm: "drizzle",
44
87
  auth: "better-auth",
45
- payments: "none",
88
+ payments: [],
46
89
  observability: ["getmonitor"],
47
90
  communication: "none",
48
91
  addons: ["turborepo"],
@@ -63,6 +106,8 @@ function getDefaultConfig() {
63
106
  projectDir: path.resolve(process.cwd(), DEFAULT_CONFIG_BASE.projectName),
64
107
  packageManager: getUserPkgManager(),
65
108
  frontend: [...DEFAULT_CONFIG_BASE.frontend],
109
+ payments: [...DEFAULT_CONFIG_BASE.payments],
110
+ observability: [...DEFAULT_CONFIG_BASE.observability],
66
111
  addons: [...DEFAULT_CONFIG_BASE.addons],
67
112
  examples: [...DEFAULT_CONFIG_BASE.examples],
68
113
  testing: [...DEFAULT_CONFIG_BASE.testing]
@@ -82,17 +127,12 @@ const ADDON_COMPATIBILITY = {
82
127
  husky: [],
83
128
  lefthook: [],
84
129
  turborepo: [],
85
- nx: [],
86
130
  "vite-plus": [],
87
- starlight: [],
88
- ultracite: [],
89
131
  mcp: [],
90
132
  oxlint: [],
91
- fumadocs: [],
92
133
  opentui: [],
93
- wxt: [],
94
134
  skills: [],
95
- evlog: [],
135
+ "s3-storage": [],
96
136
  none: []
97
137
  };
98
138
  //#endregion
@@ -429,24 +469,28 @@ const TITLE_TEXT = `
429
469
  ██║ ██╗╚██████╔╝██████╔╝╚██████╔╝
430
470
  ╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═════╝
431
471
  `;
432
- const catppuccinTheme = {
433
- pink: "#F5C2E7",
434
- mauve: "#CBA6F7",
435
- red: "#F38BA8",
436
- maroon: "#E78284",
437
- peach: "#FAB387",
438
- yellow: "#F9E2AF",
439
- green: "#A6E3A1",
440
- teal: "#94E2D5",
441
- sky: "#89DCEB",
442
- sapphire: "#74C7EC",
443
- lavender: "#B4BEFE"
444
- };
472
+ const KUBO_TITLE_COLORS = [
473
+ "#D6A72B",
474
+ "#F5D76E",
475
+ "#E0B43E",
476
+ "#E08A2E",
477
+ "#E0B43E",
478
+ "#E8A43A",
479
+ "#E08A2E",
480
+ "#E08A2E",
481
+ "#C49314",
482
+ "#E8C978",
483
+ "#E5D3A5"
484
+ ];
445
485
  const renderTitle = () => {
446
486
  const terminalWidth = process.stdout.columns || 80;
447
487
  const titleLines = TITLE_TEXT.split("\n");
448
- if (terminalWidth < Math.max(...titleLines.map((line) => line.length))) console.log(gradient(Object.values(catppuccinTheme)).multiline(`Kubo`));
449
- else console.log(gradient(Object.values(catppuccinTheme)).multiline(TITLE_TEXT));
488
+ const title = terminalWidth < Math.max(...titleLines.map((line) => line.length)) ? "Kubo" : TITLE_TEXT;
489
+ if (supportsTrueColor()) {
490
+ console.log(gradient([...KUBO_TITLE_COLORS]).multiline(title));
491
+ return;
492
+ }
493
+ console.log(pc.yellow(title));
450
494
  };
451
495
  //#endregion
452
496
  //#region src/commands/history.ts
@@ -493,10 +537,10 @@ async function historyHandler(input) {
493
537
  return;
494
538
  }
495
539
  renderTitle();
496
- intro(pc.magenta(`Project History (${entries.length} entries)`));
540
+ intro(cliColors.signal(`Project History (${entries.length} entries)`));
497
541
  for (const [index, entry] of entries.entries()) {
498
542
  const num = pc.dim(`${index + 1}.`);
499
- const name = pc.cyan(pc.bold(entry.projectName));
543
+ const name = cliColors.bright(pc.bold(entry.projectName));
500
544
  const stack = pc.dim(formatStackSummary(entry));
501
545
  log.message(`${num} ${name}`);
502
546
  log.message(` ${pc.dim("Created:")} ${formatDate(entry.createdAt)}`);
@@ -529,7 +573,7 @@ async function openExternalUrl(url, successMessage) {
529
573
  if ((await Result.tryPromise({
530
574
  try: () => openUrl(url),
531
575
  catch: () => null
532
- })).isOk()) log.success(pc.blue(successMessage));
576
+ })).isOk()) log.success(cliColors.signal(successMessage));
533
577
  else log.message(`Please visit ${url}`);
534
578
  }
535
579
  async function openDocsCommand() {
@@ -557,17 +601,9 @@ const WEB_FRAMEWORKS = [
557
601
  ];
558
602
  //#endregion
559
603
  //#region src/utils/compatibility-rules.ts
560
- const TASK_RUNNER_ADDONS$1 = [
561
- "turborepo",
562
- "nx",
563
- "vite-plus"
564
- ];
604
+ const TASK_RUNNER_ADDONS$1 = ["turborepo", "vite-plus"];
565
605
  /** Mutually exclusive code-quality linters (one slot). */
566
- const LINTER_ADDONS = [
567
- "biome",
568
- "oxlint",
569
- "ultracite"
570
- ];
606
+ const LINTER_ADDONS = ["biome", "oxlint"];
571
607
  const STATIC_DESKTOP_ADDONS = ["tauri", "electrobun"];
572
608
  function isLinterAddon(addon) {
573
609
  return LINTER_ADDONS.includes(addon);
@@ -639,23 +675,6 @@ const FULLSTACK_FRONTENDS$1 = [
639
675
  "svelte",
640
676
  "astro"
641
677
  ];
642
- const EVLOG_SERVER_BACKENDS = [
643
- "hono",
644
- "express",
645
- "fastify",
646
- "elysia"
647
- ];
648
- const EVLOG_FULLSTACK_FRONTENDS = FULLSTACK_FRONTENDS$1;
649
- const evlogCompatibilityMessage = "evlog addon supports Hono, Express, Fastify, Elysia, or backend self with Next.js, TanStack Start, Nuxt, SvelteKit, or Astro. Convex and backend none are not supported yet.";
650
- function supportsEvlogAddon(frontend = [], backend, _runtime) {
651
- if (!backend) return true;
652
- if (EVLOG_SERVER_BACKENDS.includes(backend)) return true;
653
- if (backend === "self") {
654
- if (frontend.length === 0) return true;
655
- return frontend.some((f) => EVLOG_FULLSTACK_FRONTENDS.includes(f));
656
- }
657
- return false;
658
- }
659
678
  function validateSelfBackendCompatibility(providedFlags, options, config) {
660
679
  const backend = config.backend || options.backend;
661
680
  const frontends = config.frontend || options.frontend || [];
@@ -701,7 +720,7 @@ function isFrontendAllowedWithBackend(frontend, backend, auth) {
701
720
  function supportsConvexBetterAuth(frontends = []) {
702
721
  return frontends.some((frontend) => CONVEX_BETTER_AUTH_SUPPORTED_FRONTENDS.includes(frontend));
703
722
  }
704
- function allowedApisForFrontends(frontends = []) {
723
+ function allowedApisForFrontends(frontends = [], backend) {
705
724
  const includesNuxt = frontends.includes("nuxt");
706
725
  const includesSvelte = frontends.includes("svelte");
707
726
  const includesSolid = frontends.includes("solid");
@@ -711,9 +730,17 @@ function allowedApisForFrontends(frontends = []) {
711
730
  "orpc",
712
731
  "none"
713
732
  ];
714
- if (includesNuxt || includesSvelte || includesSolid || includesAstro) return ["orpc", "none"];
733
+ if (backend === "hono") base.splice(2, 0, "orval");
734
+ if (includesNuxt || includesSvelte || includesSolid || includesAstro) return backend === "hono" ? [
735
+ "orpc",
736
+ "orval",
737
+ "none"
738
+ ] : ["orpc", "none"];
715
739
  return base;
716
740
  }
741
+ function isApiCompatibleWithBackend(api, backend) {
742
+ return api !== "orval" || backend === "hono";
743
+ }
717
744
  function isExampleTodoAllowed(backend, database, api) {
718
745
  if (backend === "convex") return true;
719
746
  if (database === "none" || api === "none") return false;
@@ -773,10 +800,6 @@ function validateDockerWebDeployDesktopAddons(webDeploy, addons, frontend, backe
773
800
  return validationErr$1(`'--web-deploy docker' is not compatible with the ${desktopAddons.join(", ")} addon on '${affected}' because desktop addons switch the web build to a static export, which the docker image cannot serve. Remove the addon or use a static-serving frontend (tanstack-router, solid).`);
774
801
  }
775
802
  function validateAddonCompatibility(addon, frontend, auth, backend, runtime) {
776
- if (addon === "evlog" && !supportsEvlogAddon(frontend, backend, runtime)) return {
777
- isCompatible: false,
778
- reason: evlogCompatibilityMessage
779
- };
780
803
  if (STATIC_DESKTOP_ADDONS.includes(addon) && auth === "clerk" && frontend.includes("react-router")) return {
781
804
  isCompatible: false,
782
805
  reason: `${addon} addon forces React Router into a static export, but Clerk on React Router requires SSR middleware. Remove the addon or use a different auth/frontend.`
@@ -789,9 +812,13 @@ function validateAddonCompatibility(addon, frontend, auth, backend, runtime) {
789
812
  isCompatible: false,
790
813
  reason: "tauri addon is not compatible with Convex Better Auth on Next.js or TanStack Start because those templates use server auth bootstrap and cannot be exported as static desktop assets."
791
814
  };
815
+ if (!(addon in ADDON_COMPATIBILITY)) return {
816
+ isCompatible: false,
817
+ reason: `${addon} is not a supported addon`
818
+ };
792
819
  const compatibleFrontends = ADDON_COMPATIBILITY[addon];
793
820
  if (compatibleFrontends.length > 0) {
794
- if (!frontend.some((f) => compatibleFrontends.includes(f))) return {
821
+ if (!frontend.some((f) => compatibleFrontends.some((compatibleFrontend) => compatibleFrontend === f))) return {
795
822
  isCompatible: false,
796
823
  reason: `${addon} addon requires one of these frontends: ${compatibleFrontends.join(", ")}`
797
824
  };
@@ -808,8 +835,8 @@ function getCompatibleAddons(allAddons, frontend, existingAddons = [], auth, bac
808
835
  });
809
836
  }
810
837
  function validateAddonsAgainstFrontends(addons = [], frontends = [], auth, backend, runtime) {
811
- if (addons.filter((addon) => TASK_RUNNER_ADDONS$1.includes(addon)).length > 1) return validationErr$1("Cannot combine 'turborepo', 'nx', and 'vite-plus' addons. Choose one task runner.");
812
- if ([...new Set(addons.filter((addon) => LINTER_ADDONS.includes(addon)))].length > 1) return validationErr$1("Cannot combine 'biome', 'oxlint', and 'ultracite' addons. Choose one code-quality linter.");
838
+ if (addons.filter((addon) => TASK_RUNNER_ADDONS$1.includes(addon)).length > 1) return validationErr$1("Cannot combine 'turborepo' and 'vite-plus' addons. Choose one task runner.");
839
+ if ([...new Set(addons.filter((addon) => LINTER_ADDONS.includes(addon)))].length > 1) return validationErr$1("Cannot combine 'biome' and 'oxlint' addons. Choose one code-quality linter.");
813
840
  for (const addon of addons) {
814
841
  if (addon === "none") continue;
815
842
  const { isCompatible, reason } = validateAddonCompatibility(addon, frontends, auth, backend, runtime);
@@ -822,21 +849,29 @@ function validateAddonsAgainstConfig(addons = [], config) {
822
849
  }
823
850
  function validateCommunicationCompatibility(communication, backend) {
824
851
  if (!communication || communication === "none") return Result.ok(void 0);
825
- if ((communication === "resend" || communication === "notifique") && backend === "none") return validationErr$1(`${communication === "notifique" ? "Notifique" : "Resend"} communication requires a server backend. Please choose a backend or use '--communication none'.`);
852
+ if ((communication === "resend" || communication === "notifique" || communication === "arara") && backend === "none") return validationErr$1(`${communication === "notifique" ? "Notifique" : communication === "arara" ? "AraraHQ" : "Resend"} communication requires a server backend. Please choose a backend or use '--communication none'.`);
853
+ return Result.ok(void 0);
854
+ }
855
+ function validateAraraRuntimeCompatibility(communication, backend, runtime, serverDeploy) {
856
+ if (communication !== "arara" || backend === "convex") return Result.ok(void 0);
857
+ if (runtime === "workers" || serverDeploy === "cloudflare") return validationErr$1("AraraHQ requires the official Node SDK and is not compatible with Edge/Workers runtimes. Use a Node/Bun server deployment or Convex Node Action.");
826
858
  return Result.ok(void 0);
827
859
  }
828
860
  function validatePaymentsCompatibility(payments, auth, backend, frontends = [], orm, database) {
829
- if (!payments || payments === "none") return Result.ok(void 0);
830
- if (payments === "abacatepay") {
831
- if (backend === "convex") return validationErr$1("AbacatePay payments is not compatible with '--backend convex'. Please use a server backend or backend 'self'.");
832
- if (!frontends.some((frontend) => isWebFrontend(frontend))) return validationErr$1("AbacatePay payments requires a web frontend. Please choose next, tanstack-start, tanstack-router, react-router, nuxt, svelte, solid, or astro.");
833
- if (frontends.length > 0 && frontends.every((frontend) => [
834
- "native-bare",
835
- "native-uniwind",
836
- "native-unistyles"
837
- ].includes(frontend))) return validationErr$1("AbacatePay payments is not compatible with native-only frontends. Please add a supported web frontend or use '--payments none'.");
838
- if (database === "none" || orm === "none") return validationErr$1("AbacatePay payments v1 requires a SQL database with Drizzle or Prisma. Database and ORM cannot be 'none'.");
839
- if (database === "mongodb" || orm === "mongoose") return validationErr$1("AbacatePay payments v1 requires a SQL database with Drizzle or Prisma. MongoDB and Mongoose are not supported.");
861
+ if (!payments || payments.length === 0) return Result.ok(void 0);
862
+ for (const provider of payments) {
863
+ const issue = getPaymentCompatibilityIssue({
864
+ provider,
865
+ backend,
866
+ frontends,
867
+ database,
868
+ orm
869
+ });
870
+ const label = provider === "stripe" ? "Stripe" : "AbacatePay";
871
+ if (issue === "convex-unsupported") return validationErr$1(`${label} payments is not compatible with '--backend convex'. Please use a server backend or backend 'self'.`);
872
+ if (issue === "requires-web-frontend") return validationErr$1(`${label} payments requires a web frontend. Please choose next, tanstack-start, tanstack-router, react-router, nuxt, svelte, solid, or astro.`);
873
+ if (issue === "native-only-unsupported") return validationErr$1(`${label} payments is not compatible with native-only frontends. Please add a supported web frontend or leave payments unselected.`);
874
+ if (issue === "sql-database-required") return validationErr$1("AbacatePay payments v1 requires a SQL database with Drizzle or Prisma. MongoDB and Mongoose are not supported.");
840
875
  }
841
876
  return Result.ok(void 0);
842
877
  }
@@ -871,12 +906,12 @@ function isGoBack(value) {
871
906
  * These prompts return GO_BACK_SYMBOL when 'b' is pressed (instead of canceling)
872
907
  */
873
908
  const unicode = process.platform !== "win32";
874
- const S_STEP_ACTIVE = unicode ? "◆" : "*";
909
+ const S_STEP_ACTIVE$1 = unicode ? "◆" : "*";
875
910
  const S_STEP_CANCEL = unicode ? "■" : "x";
876
911
  const S_STEP_ERROR = unicode ? "▲" : "x";
877
912
  const S_STEP_SUBMIT = unicode ? "◇" : "o";
878
- const S_BAR = unicode ? "│" : "|";
879
- const S_BAR_END = unicode ? "└" : "—";
913
+ const S_BAR$1 = unicode ? "│" : "|";
914
+ const S_BAR_END$1 = unicode ? "└" : "—";
880
915
  const S_RADIO_ACTIVE = unicode ? "●" : ">";
881
916
  const S_RADIO_INACTIVE = unicode ? "○" : " ";
882
917
  const S_CHECKBOX_ACTIVE = unicode ? "◻" : "[•]";
@@ -885,10 +920,10 @@ const S_CHECKBOX_INACTIVE = unicode ? "◻" : "[ ]";
885
920
  function symbol(state) {
886
921
  switch (state) {
887
922
  case "initial":
888
- case "active": return pc.cyan(S_STEP_ACTIVE);
923
+ case "active": return cliColors.orange(S_STEP_ACTIVE$1);
889
924
  case "cancel": return pc.red(S_STEP_CANCEL);
890
925
  case "error": return pc.yellow(S_STEP_ERROR);
891
- case "submit": return pc.green(S_STEP_SUBMIT);
926
+ case "submit": return cliColors.signal(S_STEP_SUBMIT);
892
927
  }
893
928
  }
894
929
  const KEYBOARD_HINT = pc.dim(`${pc.gray("↑/↓")} navigate • ${pc.gray("enter")} confirm • ${pc.gray("b")} back • ${pc.gray("ctrl+c")} cancel`);
@@ -905,6 +940,12 @@ function getMultiHint() {
905
940
  function normalizeValidationMessage(validationMessage) {
906
941
  return validationMessage instanceof Error ? validationMessage.message : validationMessage;
907
942
  }
943
+ function formatKeycap(key) {
944
+ return pc.bold(`<${key}>`);
945
+ }
946
+ function getRequiredMultiselectMessage() {
947
+ return `Please select at least one option.\n${pc.dim(`Press ${formatKeycap("space")} to select, ${formatKeycap("enter")} to submit`)}`;
948
+ }
908
949
  async function runWithNavigation(prompt) {
909
950
  let goBack = false;
910
951
  prompt.on("key", (char) => {
@@ -923,7 +964,7 @@ async function navigableSelect(opts) {
923
964
  switch (state) {
924
965
  case "disabled": return `${pc.gray(S_RADIO_INACTIVE)} ${pc.gray(label)}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
925
966
  case "selected": return `${pc.dim(label)}`;
926
- case "active": return `${pc.green(S_RADIO_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
967
+ case "active": return `${cliColors.bright(S_RADIO_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
927
968
  case "cancelled": return `${pc.strikethrough(pc.dim(label))}`;
928
969
  default: return `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(label)}`;
929
970
  }
@@ -932,14 +973,14 @@ async function navigableSelect(opts) {
932
973
  options: opts.options,
933
974
  initialValue: opts.initialValue,
934
975
  render() {
935
- const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
976
+ const title = `${pc.gray(S_BAR$1)}\n${symbol(this.state)} ${opts.message}\n`;
936
977
  switch (this.state) {
937
- case "submit": return `${title}${pc.gray(S_BAR)} ${opt(this.options[this.cursor], "selected")}`;
938
- case "cancel": return `${title}${pc.gray(S_BAR)} ${opt(this.options[this.cursor], "cancelled")}\n${pc.gray(S_BAR)}`;
978
+ case "submit": return `${title}${pc.gray(S_BAR$1)} ${opt(this.options[this.cursor], "selected")}`;
979
+ case "cancel": return `${title}${pc.gray(S_BAR$1)} ${opt(this.options[this.cursor], "cancelled")}\n${pc.gray(S_BAR$1)}`;
939
980
  default: {
940
- const optionsText = this.options.map((option, i) => opt(option, option.disabled ? "disabled" : i === this.cursor ? "active" : "inactive")).join(`\n${pc.cyan(S_BAR)} `);
941
- const hint = `\n${pc.gray(S_BAR)} ${getHint()}`;
942
- return `${title}${pc.cyan(S_BAR)} ${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
981
+ const optionsText = this.options.map((option, i) => opt(option, option.disabled ? "disabled" : i === this.cursor ? "active" : "inactive")).join(`\n${cliColors.signal(S_BAR$1)} `);
982
+ const hint = `\n${pc.gray(S_BAR$1)} ${getHint()}`;
983
+ return `${title}${cliColors.signal(S_BAR$1)} ${optionsText}\n${cliColors.signal(S_BAR_END$1)}${hint}\n`;
943
984
  }
944
985
  }
945
986
  }
@@ -950,10 +991,10 @@ async function navigableMultiselect(opts) {
950
991
  const opt = (option, state) => {
951
992
  const label = option.label ?? String(option.value);
952
993
  if (state === "disabled") return `${pc.gray(S_CHECKBOX_INACTIVE)} ${pc.strikethrough(pc.gray(label))}${option.hint ? ` ${pc.dim(`(${option.hint ?? "disabled"})`)}` : ""}`;
953
- if (state === "active") return `${pc.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
954
- if (state === "selected") return `${pc.green(S_CHECKBOX_SELECTED)} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
994
+ if (state === "active") return `${cliColors.orange(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
995
+ if (state === "selected") return `${cliColors.bright(S_CHECKBOX_SELECTED)} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
955
996
  if (state === "cancelled") return `${pc.strikethrough(pc.dim(label))}`;
956
- if (state === "active-selected") return `${pc.green(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
997
+ if (state === "active-selected") return `${cliColors.bright(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
957
998
  if (state === "submitted") return `${pc.dim(label)}`;
958
999
  return `${pc.dim(S_CHECKBOX_INACTIVE)} ${pc.dim(label)}`;
959
1000
  };
@@ -962,11 +1003,11 @@ async function navigableMultiselect(opts) {
962
1003
  initialValues: opts.initialValues,
963
1004
  required,
964
1005
  validate(selected) {
965
- if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
1006
+ if (required && (selected === void 0 || selected.length === 0)) return getRequiredMultiselectMessage();
966
1007
  return normalizeValidationMessage(opts.validate?.(selected));
967
1008
  },
968
1009
  render() {
969
- const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
1010
+ const title = `${pc.gray(S_BAR$1)}\n${symbol(this.state)} ${opts.message}\n`;
970
1011
  const value = this.value ?? [];
971
1012
  const styleOption = (option, active) => {
972
1013
  if (option.disabled) return opt(option, "disabled");
@@ -978,21 +1019,21 @@ async function navigableMultiselect(opts) {
978
1019
  switch (this.state) {
979
1020
  case "submit": {
980
1021
  const submitText = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted")).join(pc.dim(", ")) || pc.dim("none");
981
- return `${title}${pc.gray(S_BAR)} ${submitText}`;
1022
+ return `${title}${pc.gray(S_BAR$1)} ${submitText}`;
982
1023
  }
983
1024
  case "cancel": {
984
1025
  const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
985
- return `${title}${pc.gray(S_BAR)} ${label}\n${pc.gray(S_BAR)}`;
1026
+ return `${title}${pc.gray(S_BAR$1)} ${label}\n${pc.gray(S_BAR$1)}`;
986
1027
  }
987
1028
  case "error": {
988
- const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
989
- const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.yellow(S_BAR)} `);
990
- return `${title}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
1029
+ const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END$1)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
1030
+ const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.yellow(S_BAR$1)} `);
1031
+ return `${title}${pc.yellow(S_BAR$1)} ${optionsText}\n${footer}\n`;
991
1032
  }
992
1033
  default: {
993
- const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${pc.cyan(S_BAR)} `);
994
- const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
995
- return `${title}${pc.cyan(S_BAR)} ${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
1034
+ const optionsText = this.options.map((option, i) => styleOption(option, i === this.cursor)).join(`\n${cliColors.signal(S_BAR$1)} `);
1035
+ const hint = `\n${pc.gray(S_BAR$1)} ${getMultiHint()}`;
1036
+ return `${title}${cliColors.signal(S_BAR$1)} ${optionsText}\n${cliColors.signal(S_BAR_END$1)}${hint}\n`;
996
1037
  }
997
1038
  }
998
1039
  }
@@ -1006,14 +1047,14 @@ async function navigableConfirm(opts) {
1006
1047
  inactive,
1007
1048
  initialValue: opts.initialValue ?? true,
1008
1049
  render() {
1009
- const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
1050
+ const title = `${pc.gray(S_BAR$1)}\n${symbol(this.state)} ${opts.message}\n`;
1010
1051
  const value = this.value ? active : inactive;
1011
1052
  switch (this.state) {
1012
- case "submit": return `${title}${pc.gray(S_BAR)} ${pc.dim(value)}`;
1013
- case "cancel": return `${title}${pc.gray(S_BAR)} ${pc.strikethrough(pc.dim(value))}\n${pc.gray(S_BAR)}`;
1053
+ case "submit": return `${title}${pc.gray(S_BAR$1)} ${pc.dim(value)}`;
1054
+ case "cancel": return `${title}${pc.gray(S_BAR$1)} ${pc.strikethrough(pc.dim(value))}\n${pc.gray(S_BAR$1)}`;
1014
1055
  default: {
1015
- const hint = `\n${pc.gray(S_BAR)} ${getHint()}`;
1016
- return `${title}${pc.cyan(S_BAR)} ${this.value ? `${pc.green(S_RADIO_ACTIVE)} ${active}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(active)}`} ${pc.dim("/")} ${!this.value ? `${pc.green(S_RADIO_ACTIVE)} ${inactive}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(inactive)}`}\n${pc.cyan(S_BAR_END)}${hint}\n`;
1056
+ const hint = `\n${pc.gray(S_BAR$1)} ${getHint()}`;
1057
+ return `${title}${cliColors.signal(S_BAR$1)} ${this.value ? `${pc.green(S_RADIO_ACTIVE)} ${active}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(active)}`} ${pc.dim("/")} ${!this.value ? `${pc.green(S_RADIO_ACTIVE)} ${inactive}` : `${pc.dim(S_RADIO_INACTIVE)} ${pc.dim(inactive)}`}\n${cliColors.signal(S_BAR_END$1)}${hint}\n`;
1017
1058
  }
1018
1059
  }
1019
1060
  }
@@ -1026,16 +1067,16 @@ async function navigableGroupMultiselect(opts) {
1026
1067
  const isItem = typeof option.group === "string";
1027
1068
  const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true });
1028
1069
  const isLast = isItem && next && next.group === true;
1029
- const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : "";
1030
- if (state === "active") return `${pc.dim(prefix)}${pc.cyan(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1031
- if (state === "group-active") return `${prefix}${pc.cyan(S_CHECKBOX_ACTIVE)} ${pc.dim(label)}`;
1032
- if (state === "group-active-selected") return `${prefix}${pc.green(S_CHECKBOX_SELECTED)} ${pc.dim(label)}`;
1070
+ const prefix = isItem ? `${isLast ? S_BAR_END$1 : S_BAR$1} ` : "";
1071
+ if (state === "active") return `${pc.dim(prefix)}${cliColors.orange(S_CHECKBOX_ACTIVE)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1072
+ if (state === "group-active") return `${prefix}${cliColors.orange(S_CHECKBOX_ACTIVE)} ${pc.dim(label)}`;
1073
+ if (state === "group-active-selected") return `${prefix}${cliColors.bright(S_CHECKBOX_SELECTED)} ${pc.dim(label)}`;
1033
1074
  if (state === "selected") {
1034
- const selectedCheckbox = isItem ? pc.green(S_CHECKBOX_SELECTED) : "";
1075
+ const selectedCheckbox = isItem ? cliColors.bright(S_CHECKBOX_SELECTED) : "";
1035
1076
  return `${pc.dim(prefix)}${selectedCheckbox} ${pc.dim(label)}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1036
1077
  }
1037
1078
  if (state === "cancelled") return `${pc.strikethrough(pc.dim(label))}`;
1038
- if (state === "active-selected") return `${pc.dim(prefix)}${pc.green(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1079
+ if (state === "active-selected") return `${pc.dim(prefix)}${cliColors.bright(S_CHECKBOX_SELECTED)} ${label}${option.hint ? ` ${pc.dim(`(${option.hint})`)}` : ""}`;
1039
1080
  if (state === "submitted") return `${pc.dim(label)}`;
1040
1081
  const unselectedCheckbox = isItem ? pc.dim(S_CHECKBOX_INACTIVE) : "";
1041
1082
  return `${pc.dim(prefix)}${unselectedCheckbox} ${pc.dim(label)}`;
@@ -1046,24 +1087,24 @@ async function navigableGroupMultiselect(opts) {
1046
1087
  required,
1047
1088
  selectableGroups: true,
1048
1089
  validate(selected) {
1049
- if (required && (selected === void 0 || selected.length === 0)) return `Please select at least one option.\n${pc.reset(pc.dim(`Press ${pc.gray(pc.bgWhite(pc.inverse(" space ")))} to select, ${pc.gray(pc.bgWhite(pc.inverse(" enter ")))} to submit`))}`;
1090
+ if (required && (selected === void 0 || selected.length === 0)) return getRequiredMultiselectMessage();
1050
1091
  return normalizeValidationMessage(opts.validate?.(selected));
1051
1092
  },
1052
1093
  render() {
1053
- const title = `${pc.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;
1094
+ const title = `${pc.gray(S_BAR$1)}\n${symbol(this.state)} ${opts.message}\n`;
1054
1095
  const value = this.value ?? [];
1055
1096
  switch (this.state) {
1056
1097
  case "submit": {
1057
1098
  const selectedOptions = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "submitted"));
1058
1099
  const optionsText = selectedOptions.length === 0 ? "" : ` ${selectedOptions.join(pc.dim(", "))}`;
1059
- return `${title}${pc.gray(S_BAR)}${optionsText}`;
1100
+ return `${title}${pc.gray(S_BAR$1)}${optionsText}`;
1060
1101
  }
1061
1102
  case "cancel": {
1062
1103
  const label = this.options.filter(({ value: optionValue }) => value.includes(optionValue)).map((option) => opt(option, "cancelled")).join(pc.dim(", "));
1063
- return `${title}${pc.gray(S_BAR)} ${label.trim() ? `${label}\n${pc.gray(S_BAR)}` : ""}`;
1104
+ return `${title}${pc.gray(S_BAR$1)} ${label.trim() ? `${label}\n${pc.gray(S_BAR$1)}` : ""}`;
1064
1105
  }
1065
1106
  case "error": {
1066
- const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
1107
+ const footer = this.error.split("\n").map((ln, i) => i === 0 ? `${pc.yellow(S_BAR_END$1)} ${pc.yellow(ln)}` : ` ${ln}`).join("\n");
1067
1108
  const optionsText = this.options.map((option, i, options) => {
1068
1109
  const selected = value.includes(option.value) || option.group === true && this.isGroupSelected(`${option.value}`);
1069
1110
  const active = i === this.cursor;
@@ -1071,8 +1112,8 @@ async function navigableGroupMultiselect(opts) {
1071
1112
  if (active && selected) return opt(option, "active-selected", options);
1072
1113
  if (selected) return opt(option, "selected", options);
1073
1114
  return opt(option, active ? "active" : "inactive", options);
1074
- }).join(`\n${pc.yellow(S_BAR)} `);
1075
- return `${title}${pc.yellow(S_BAR)} ${optionsText}\n${footer}\n`;
1115
+ }).join(`\n${pc.yellow(S_BAR$1)} `);
1116
+ return `${title}${pc.yellow(S_BAR$1)} ${optionsText}\n${footer}\n`;
1076
1117
  }
1077
1118
  default: {
1078
1119
  const optionsText = this.options.map((option, i, options) => {
@@ -1085,10 +1126,10 @@ async function navigableGroupMultiselect(opts) {
1085
1126
  else if (selected) optionText = opt(option, "selected", options);
1086
1127
  else optionText = opt(option, active ? "active" : "inactive", options);
1087
1128
  return `${i !== 0 && !optionText.startsWith("\n") ? " " : ""}${optionText}`;
1088
- }).join(`\n${pc.cyan(S_BAR)}`);
1129
+ }).join(`\n${cliColors.signal(S_BAR$1)}`);
1089
1130
  const optionsPrefix = optionsText.startsWith("\n") ? "" : " ";
1090
- const hint = `\n${pc.gray(S_BAR)} ${getMultiHint()}`;
1091
- return `${title}${pc.cyan(S_BAR)}${optionsPrefix}${optionsText}\n${pc.cyan(S_BAR_END)}${hint}\n`;
1131
+ const hint = `\n${pc.gray(S_BAR$1)} ${getMultiHint()}`;
1132
+ return `${title}${cliColors.signal(S_BAR$1)}${optionsPrefix}${optionsText}\n${cliColors.signal(S_BAR_END$1)}${hint}\n`;
1092
1133
  }
1093
1134
  }
1094
1135
  }
@@ -1108,10 +1149,6 @@ function getAddonDisplay(addon) {
1108
1149
  label = "Turborepo";
1109
1150
  hint = "High-performance build system";
1110
1151
  break;
1111
- case "nx":
1112
- label = "Nx";
1113
- hint = "Smart monorepo orchestration and task graph";
1114
- break;
1115
1152
  case "vite-plus":
1116
1153
  label = "Vite+";
1117
1154
  hint = "Unified Vite toolchain and workspace task runner";
@@ -1136,10 +1173,6 @@ function getAddonDisplay(addon) {
1136
1173
  label = "Oxlint";
1137
1174
  hint = "Oxlint + Oxfmt (linting & formatting)";
1138
1175
  break;
1139
- case "ultracite":
1140
- label = "Ultracite";
1141
- hint = "Zero-config preset for Biome or Oxlint with AI integration";
1142
- break;
1143
1176
  case "lefthook":
1144
1177
  label = "Lefthook";
1145
1178
  hint = "Fast and powerful Git hooks manager";
@@ -1148,22 +1181,10 @@ function getAddonDisplay(addon) {
1148
1181
  label = "Husky";
1149
1182
  hint = "Modern native Git hooks made easy";
1150
1183
  break;
1151
- case "starlight":
1152
- label = "Starlight";
1153
- hint = "Build stellar docs with astro";
1154
- break;
1155
- case "fumadocs":
1156
- label = "Fumadocs";
1157
- hint = "Build excellent documentation site";
1158
- break;
1159
1184
  case "opentui":
1160
1185
  label = "OpenTUI";
1161
1186
  hint = "Build terminal user interfaces";
1162
1187
  break;
1163
- case "wxt":
1164
- label = "WXT";
1165
- hint = "Build browser extensions";
1166
- break;
1167
1188
  case "skills":
1168
1189
  label = "Skills";
1169
1190
  hint = "AI coding agent skills for your stack";
@@ -1172,9 +1193,9 @@ function getAddonDisplay(addon) {
1172
1193
  label = "MCP";
1173
1194
  hint = "Install MCP servers, including kubojs, via add-mcp";
1174
1195
  break;
1175
- case "evlog":
1176
- label = "evlog";
1177
- hint = "Request logging with Better Auth context and AI SDK telemetry";
1196
+ case "s3-storage":
1197
+ label = "S3-compatible Storage";
1198
+ hint = "Add object storage for AWS S3, Cloudflare R2, MinIO, or Backblaze";
1178
1199
  break;
1179
1200
  default:
1180
1201
  label = addon;
@@ -1186,27 +1207,20 @@ function getAddonDisplay(addon) {
1186
1207
  };
1187
1208
  }
1188
1209
  const ADDON_GROUPS = {
1189
- "Monorepo & Tasks": [
1190
- "turborepo",
1191
- "nx",
1192
- "vite-plus"
1193
- ],
1210
+ "Monorepo & Tasks": ["turborepo", "vite-plus"],
1194
1211
  "Code Quality": [
1195
1212
  "biome",
1196
1213
  "oxlint",
1197
- "ultracite",
1198
1214
  "husky",
1199
1215
  "lefthook"
1200
1216
  ],
1201
- Documentation: ["starlight", "fumadocs"],
1202
1217
  "Platform Extensions": [
1203
1218
  "pwa",
1204
1219
  "tauri",
1205
1220
  "electrobun",
1206
1221
  "opentui",
1207
- "wxt"
1222
+ "s3-storage"
1208
1223
  ],
1209
- Observability: ["evlog"],
1210
1224
  "AI & Agent Tools": ["skills", "mcp"]
1211
1225
  };
1212
1226
  function createGroupedOptions() {
@@ -1231,16 +1245,8 @@ function sortAndPruneGroupedOptions(groupedOptions) {
1231
1245
  });
1232
1246
  }
1233
1247
  function validateAddonSelection(selected) {
1234
- if ((selected?.filter((addon) => [
1235
- "turborepo",
1236
- "nx",
1237
- "vite-plus"
1238
- ].includes(addon)) ?? []).length > 1) return "Choose Turborepo, Nx, or Vite+ as your task runner, not more than one.";
1239
- if ((selected?.filter((addon) => [
1240
- "biome",
1241
- "oxlint",
1242
- "ultracite"
1243
- ].includes(addon)) ?? []).length > 1) return "Choose Biome, Oxlint, or Ultracite as your code-quality tool, not more than one.";
1248
+ if ((selected?.filter((addon) => ["turborepo", "vite-plus"].includes(addon)) ?? []).length > 1) return "Choose Turborepo or Vite+ as your task runner, not both.";
1249
+ if ((selected?.filter((addon) => ["biome", "oxlint"].includes(addon)) ?? []).length > 1) return "Choose Biome or Oxlint as your code-quality tool, not both.";
1244
1250
  }
1245
1251
  async function getAddonsChoice(addons, frontends, auth, backend, runtime, previousValue) {
1246
1252
  if (addons !== void 0) return addons;
@@ -1265,7 +1271,7 @@ async function getAddonsChoice(addons, frontends, auth, backend, runtime, previo
1265
1271
  required: false,
1266
1272
  validate: validateAddonSelection
1267
1273
  });
1268
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1274
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1269
1275
  return response;
1270
1276
  }
1271
1277
  async function getAddonsToAdd(config) {
@@ -1288,7 +1294,7 @@ async function getAddonsToAdd(config) {
1288
1294
  required: false,
1289
1295
  validate: validateAddonSelection
1290
1296
  });
1291
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1297
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
1292
1298
  return response;
1293
1299
  }
1294
1300
  //#endregion
@@ -1364,515 +1370,6 @@ const addPackageDependency = async (opts) => {
1364
1370
  await fs.writeJson(pkgJsonPath, pkgJson, { spaces: 2 });
1365
1371
  };
1366
1372
  //#endregion
1367
- //#region src/helpers/addons/evlog-setup.ts
1368
- const evlogBackends = [
1369
- "hono",
1370
- "express",
1371
- "fastify",
1372
- "elysia"
1373
- ];
1374
- const evlogWebFrontends = [
1375
- "next",
1376
- "nuxt",
1377
- "svelte",
1378
- "tanstack-start",
1379
- "astro"
1380
- ];
1381
- function isEvlogBackend(backend) {
1382
- return evlogBackends.includes(backend);
1383
- }
1384
- function getEvlogWebFrontend(frontends) {
1385
- return frontends.find((frontend) => evlogWebFrontends.includes(frontend));
1386
- }
1387
- function shouldIdentifyWebAuth(config) {
1388
- return config.auth === "better-auth" && config.backend === "self";
1389
- }
1390
- function prependMissingImports(content, imports) {
1391
- const missingImports = imports.filter((line) => !content.includes(line));
1392
- if (missingImports.length === 0) return content;
1393
- const importBlock = `${missingImports.join("\n")}\n`;
1394
- const referenceMatch = content.match(/^(?:\/\/\/ <reference[^\n]*>\n)+/);
1395
- if (referenceMatch) return `${referenceMatch[0]}${importBlock}${content.slice(referenceMatch[0].length)}`;
1396
- return `${importBlock}${content}`;
1397
- }
1398
- function addNamedImport(content, moduleName, names) {
1399
- const importRegex = new RegExp(`import \\{([^}]+)\\} from "${moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}";`);
1400
- const match = content.match(importRegex);
1401
- if (!match) return prependMissingImports(content, [`import { ${names.join(", ")} } from "${moduleName}";`]);
1402
- const nextNames = [...match[1].split(",").map((name) => name.trim()).filter(Boolean)];
1403
- for (const name of names) if (!nextNames.includes(name)) nextNames.push(name);
1404
- return content.replace(match[0], `import { ${nextNames.join(", ")} } from "${moduleName}";`);
1405
- }
1406
- function insertBeforeOnce(content, marker, snippet, alreadyPresent) {
1407
- if (content.includes(alreadyPresent)) return content;
1408
- if (!content.includes(marker)) return content;
1409
- return content.replace(marker, `${snippet}${marker}`);
1410
- }
1411
- function insertAfterOnce(content, marker, snippet, alreadyPresent) {
1412
- if (content.includes(alreadyPresent)) return content;
1413
- if (!content.includes(marker)) return content;
1414
- return content.replace(marker, `${marker}${snippet}`);
1415
- }
1416
- async function writeFileIfChanged(filePath, content) {
1417
- if ((await fs.pathExists(filePath) ? await fs.readFile(filePath, "utf-8") : void 0) === content) return;
1418
- await fs.ensureDir(path.dirname(filePath));
1419
- await fs.writeFile(filePath, content);
1420
- }
1421
- async function updateFileIfExists(filePath, update) {
1422
- if (!await fs.pathExists(filePath)) return;
1423
- const content = await fs.readFile(filePath, "utf-8");
1424
- const nextContent = update(content);
1425
- if (nextContent !== content) await fs.writeFile(filePath, nextContent);
1426
- }
1427
- function usesCreateAuthFactory(config) {
1428
- return config.runtime === "workers" || config.serverDeploy === "cloudflare" || config.backend === "self" && config.webDeploy === "cloudflare";
1429
- }
1430
- function getAuthImportLine(config) {
1431
- return usesCreateAuthFactory(config) ? `import { createAuth } from "@${config.projectName}/auth";` : `import { auth } from "@${config.projectName}/auth";`;
1432
- }
1433
- function getAuthExpression(config) {
1434
- return usesCreateAuthFactory(config) ? "createAuth()" : "auth";
1435
- }
1436
- function addEvlogBetterAuthServerSetup(content, backend, authExpression) {
1437
- let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
1438
- const usesAuthFactory = authExpression.endsWith("()");
1439
- const evlogAuthExpression = `${authExpression} as BetterAuthInstance`;
1440
- const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
1441
- const identifySnippet = usesAuthFactory ? "" : `const identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});\n\n`;
1442
- const identifyUserSetup = usesAuthFactory ? `\n\tconst identifyUser = createAuthMiddleware(${evlogAuthExpression}, ${authOptions});` : "";
1443
- if (backend === "hono") {
1444
- nextContent = insertBeforeOnce(nextContent, "const app = new Hono", identifySnippet, "createAuthMiddleware(");
1445
- return insertAfterOnce(nextContent, "app.use(evlog());", `\napp.use("*", async (c, next) => {${identifyUserSetup}\n\tawait identifyUser(c.get("log"), c.req.raw.headers, c.req.path);\n\tawait next();\n});`, "identifyUser(c.get(\"log\")");
1446
- }
1447
- if (backend === "express") {
1448
- nextContent = insertBeforeOnce(nextContent, "const app = express();", identifySnippet, "createAuthMiddleware(");
1449
- return insertAfterOnce(nextContent, "app.use(evlog());", `\napp.use(async (req, _res, next) => {${identifyUserSetup}\n\tawait identifyUser(req.log, req.headers, req.path);\n\tnext();\n});`, "identifyUser(req.log");
1450
- }
1451
- if (backend === "fastify") {
1452
- nextContent = addNamedImport(nextContent, "evlog/fastify", ["useLogger"]);
1453
- nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", identifySnippet, "createAuthMiddleware(");
1454
- return insertAfterOnce(nextContent, "fastify.register(evlog);", `\nfastify.addHook("preHandler", async (request) => {${identifyUserSetup}\n\tawait identifyUser(useLogger(), request.headers, request.url);\n});`, "identifyUser(useLogger()");
1455
- }
1456
- nextContent = insertBeforeOnce(nextContent, "new Elysia", identifySnippet, "createAuthMiddleware(");
1457
- return insertAfterOnce(nextContent, ".use(evlog())", `\n\t.derive(async ({ request, log }) => {${identifyUserSetup.replace(/\n\t/g, "\n ")}\n\t\tawait identifyUser(log, request.headers, new URL(request.url).pathname);\n\t\treturn {};\n\t})`, "identifyUser(log");
1458
- }
1459
- function addEvlogServerSetup(content, backend, serviceName) {
1460
- const initSnippet = `initLogger({\n\tenv: { service: "${serviceName}" },\n});\n\n`;
1461
- if (backend === "hono") {
1462
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog, type EvlogVariables } from \"evlog/hono\";"]);
1463
- nextContent = insertBeforeOnce(nextContent, "const app = new Hono", initSnippet, "initLogger({");
1464
- nextContent = nextContent.replace("const app = new Hono();", "const app = new Hono<EvlogVariables>();");
1465
- nextContent = nextContent.replace("import { logger } from \"hono/logger\";\n", "").replace(/\napp\.use\(logger\(\)\);/, "");
1466
- return insertAfterOnce(nextContent, "const app = new Hono<EvlogVariables>();", "\n\napp.use(evlog());", "app.use(evlog());");
1467
- }
1468
- if (backend === "express") {
1469
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/express\";"]);
1470
- nextContent = insertBeforeOnce(nextContent, "const app = express();", initSnippet, "initLogger({");
1471
- return insertAfterOnce(nextContent, "const app = express();", "\n\napp.use(evlog());", "app.use(evlog());");
1472
- }
1473
- if (backend === "fastify") {
1474
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/fastify\";"]);
1475
- nextContent = insertBeforeOnce(nextContent, "const fastify = Fastify", initSnippet, "initLogger({");
1476
- return insertBeforeOnce(nextContent, "fastify.register(fastifyCors", "fastify.register(evlog);\n", "fastify.register(evlog);");
1477
- }
1478
- let nextContent = prependMissingImports(content, ["import { initLogger } from \"evlog\";", "import { evlog } from \"evlog/elysia\";"]);
1479
- nextContent = insertBeforeOnce(nextContent, "new Elysia", initSnippet, "initLogger({");
1480
- for (const marker of ["new Elysia({ adapter: node() })", "new Elysia()"]) nextContent = insertAfterOnce(nextContent, marker, "\n .use(evlog())", ".use(evlog())");
1481
- return nextContent;
1482
- }
1483
- function addNuxtEvlogSetup(content, serviceName) {
1484
- let nextContent = content;
1485
- if (!nextContent.includes("\"evlog/nuxt\"") && !nextContent.includes("'evlog/nuxt'")) nextContent = nextContent.replace(/modules:\s*\[/, (match) => `${match}\n "evlog/nuxt",`);
1486
- if (!nextContent.includes("evlog:")) nextContent = nextContent.replace(/\n\}\)\s*$/, (match) => {
1487
- const contentBeforeConfigClose = nextContent.slice(0, -match.length);
1488
- return `${!/[,{]\s*$/.test(contentBeforeConfigClose) ? "," : ""}\n evlog: {\n env: { service: "${serviceName}" },\n },\n})`;
1489
- });
1490
- return nextContent;
1491
- }
1492
- function addSvelteViteEvlogSetup(content, serviceName) {
1493
- let nextContent = prependMissingImports(content, ["import evlog from \"evlog/vite\";"]);
1494
- if (nextContent.includes("evlog({")) return nextContent;
1495
- return nextContent.replace("plugins: [tailwindcss(), sveltekit()],", `plugins: [\n tailwindcss(),\n sveltekit(),\n evlog({ service: "${serviceName}" }),\n ],`);
1496
- }
1497
- function addSvelteHooksEvlogSetup(content) {
1498
- let nextContent = prependMissingImports(content, ["import { createEvlogHooks } from \"evlog/sveltekit\";"]);
1499
- if (!nextContent.includes("export const handle") && !nextContent.includes("const authHandle")) {
1500
- if (!nextContent.includes("createEvlogHooks()")) nextContent = `${nextContent.trimEnd()}\n\nexport const { handle, handleError } = createEvlogHooks();\n`;
1501
- return nextContent;
1502
- }
1503
- nextContent = prependMissingImports(nextContent, ["import { sequence } from \"@sveltejs/kit/hooks\";"]);
1504
- if (!nextContent.includes("const { handle: evlogHandle, handleError }")) nextContent = nextContent.replace(/((?:import .+\n)+)/, `$1\nconst { handle: evlogHandle, handleError } = createEvlogHooks();\n\n`);
1505
- nextContent = nextContent.replace(/export const handle(:\s*Handle)?\s*=\s*async/, (_match, typeAnnotation) => `const authHandle${typeAnnotation ?? ""} = async`);
1506
- if (!nextContent.includes("sequence(evlogHandle, authHandle)")) nextContent = `${nextContent.trimEnd()}\n\nexport const handle = sequence(evlogHandle as Handle, authHandle);\nexport { handleError };\n`;
1507
- return nextContent;
1508
- }
1509
- function addSvelteLocalsType(content) {
1510
- let nextContent = prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";"]);
1511
- if (nextContent.includes("log: RequestLogger")) return nextContent;
1512
- if (nextContent.includes("// interface Locals {}")) return nextContent.replace("// interface Locals {}", "interface Locals {\n log: RequestLogger;\n }");
1513
- return nextContent.replace("namespace App {", "namespace App {\n interface Locals {\n log: RequestLogger;\n }\n");
1514
- }
1515
- function addTanstackStartRootEvlogSetup(content) {
1516
- let nextContent = prependMissingImports(content, ["import { createMiddleware } from \"@tanstack/react-start\";", "import { evlogErrorHandler } from \"evlog/nitro/v3\";"]);
1517
- const middlewareEntry = "createMiddleware().server(evlogErrorHandler)";
1518
- if (nextContent.includes(`middleware: [${middlewareEntry}]`)) return nextContent;
1519
- if (nextContent.includes("middleware: [")) return nextContent.replace("middleware: [", `middleware: [${middlewareEntry}, `);
1520
- if (/server:\s*{/.test(nextContent)) return nextContent.replace(/server:\s*{\n/, `server: {\n middleware: [${middlewareEntry}],\n`);
1521
- return nextContent.replace("head: () => ({", `server: {\n middleware: [${middlewareEntry}],\n },\n\n head: () => ({`);
1522
- }
1523
- function addAstroMiddlewareEvlogSetup(content, serviceName) {
1524
- let nextContent = prependMissingImports(content, ["import { createRequestLogger, initLogger } from \"evlog\";"]);
1525
- const initSnippet = `initLogger({\n env: { service: "${serviceName}" },\n});\n\n`;
1526
- nextContent = insertBeforeOnce(nextContent, "export const onRequest", initSnippet, "initLogger({");
1527
- if (nextContent.includes("createRequestLogger({")) return nextContent;
1528
- const contextMarker = "export const onRequest = defineMiddleware(async (context, next) => {";
1529
- if (nextContent.includes(contextMarker)) {
1530
- nextContent = insertAfterOnce(nextContent, contextMarker, `\n const url = new URL(context.request.url);\n const log = createRequestLogger({\n method: context.request.method,\n path: url.pathname,\n });\n\n context.locals.log = log;\n`, "const log = createRequestLogger({");
1531
- return nextContent.replace("return next();", "const response = await next();\n log.emit();\n return response;");
1532
- }
1533
- const localsMarker = "export const onRequest = defineMiddleware(async ({ request, locals }, next) => {";
1534
- if (nextContent.includes(localsMarker)) {
1535
- nextContent = insertAfterOnce(nextContent, localsMarker, `\n const url = new URL(request.url);\n const log = createRequestLogger({\n method: request.method,\n path: url.pathname,\n });\n\n locals.log = log;\n`, "const log = createRequestLogger({");
1536
- return nextContent.replace("return next();", "const response = await next();\n log.emit();\n return response;");
1537
- }
1538
- return nextContent;
1539
- }
1540
- function addAstroLocalsType(content) {
1541
- let nextContent = prependMissingImports(content, ["import type { RequestLogger } from \"evlog\";"]);
1542
- if (nextContent.includes("log: RequestLogger")) return nextContent;
1543
- if (nextContent.includes("interface Locals {")) return nextContent.replace("interface Locals {", "interface Locals {\n log: RequestLogger;");
1544
- if (nextContent.includes("declare namespace App {")) return nextContent.replace("declare namespace App {", "declare namespace App {\n interface Locals {\n log: RequestLogger;\n }\n");
1545
- return `${nextContent.trimEnd()}\n\ndeclare namespace App {\n interface Locals {\n log: RequestLogger;\n }\n}\n`;
1546
- }
1547
- function addNextRouteWrappers(content) {
1548
- let nextContent = prependMissingImports(content, ["import { withEvlog } from \"@/lib/evlog\";"]);
1549
- if (nextContent.includes("withEvlog(handler)") || nextContent.includes("withEvlog(handleRequest)")) return nextContent;
1550
- nextContent = nextContent.replace("export { handler as GET, handler as POST };", "export const GET = withEvlog(handler);\nexport const POST = withEvlog(handler);");
1551
- for (const method of [
1552
- "GET",
1553
- "POST",
1554
- "PUT",
1555
- "PATCH",
1556
- "DELETE"
1557
- ]) nextContent = nextContent.replace(`export const ${method} = handleRequest;`, `export const ${method} = withEvlog(handleRequest);`);
1558
- return nextContent;
1559
- }
1560
- function addNextAiEvlogSetup(content) {
1561
- let nextContent = addNamedImport(content, "@/lib/evlog", ["withEvlog"]);
1562
- if (!nextContent.includes("withEvlog(async (req: Request)")) {
1563
- nextContent = nextContent.replace("export async function POST(req: Request) {", "export const POST = withEvlog(async (req: Request) => {");
1564
- if (nextContent.includes("export const POST = withEvlog(async (req: Request) => {")) nextContent = nextContent.replace(/\n}\s*$/, "\n});\n");
1565
- }
1566
- return nextContent;
1567
- }
1568
- function addNextBetterAuthToRoute(content) {
1569
- let nextContent = addNamedImport(content, "@/lib/evlog-auth", ["identifyEvlogUser"]);
1570
- nextContent = nextContent.replace("function handler(req:", "async function handler(req:");
1571
- for (const marker of [
1572
- "async function handler(req: NextRequest) {",
1573
- "async function handleRequest(req: NextRequest) {",
1574
- "export const POST = withEvlog(async (req: Request) => {"
1575
- ]) nextContent = insertAfterOnce(nextContent, marker, "\n await identifyEvlogUser(req);", "identifyEvlogUser(req)");
1576
- return nextContent;
1577
- }
1578
- function addSvelteBetterAuthEvlogSetup(content, config) {
1579
- if (!content.includes("authHandle") || content.includes("evlogAuthHandle")) return content;
1580
- let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
1581
- if (!nextContent.includes(`@${config.projectName}/auth`)) nextContent = prependMissingImports(nextContent, [getAuthImportLine(config)]);
1582
- if (usesCreateAuthFactory(config) && config.webDeploy === "cloudflare" && !nextContent.includes(`@${config.projectName}/env/server`)) nextContent = prependMissingImports(nextContent, [`import { env as localEnv } from "@${config.projectName}/env/server";`]);
1583
- const authExpression = getAuthExpression(config);
1584
- const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
1585
- const authHandleSnippet = usesCreateAuthFactory(config) && config.webDeploy === "cloudflare" ? `const evlogAuthHandle: Handle = async ({ event, resolve }) => {\n\tif (building) {\n\t\treturn resolve(event);\n\t}\n\n\tconst authEnv = event.platform?.env ?? localEnv;\n\tconst identifyUser = createAuthMiddleware(createAuth(authEnv) as BetterAuthInstance, ${authOptions});\n\tawait identifyUser(event.locals.log, event.request.headers, event.url.pathname);\n\treturn resolve(event);\n};\n\n` : `const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n\nconst evlogAuthHandle: Handle = async ({ event, resolve }) => {\n\tawait identifyUser(event.locals.log, event.request.headers, event.url.pathname);\n\treturn resolve(event);\n};\n\n`;
1586
- nextContent = insertAfterOnce(nextContent, "const { handle: evlogHandle, handleError } = createEvlogHooks();\n\n", authHandleSnippet, "evlogAuthHandle");
1587
- return nextContent.replace("sequence(evlogHandle as Handle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)").replace("sequence(evlogHandle, authHandle)", "sequence(evlogHandle as Handle, evlogAuthHandle, authHandle)");
1588
- }
1589
- function addAstroBetterAuthEvlogSetup(content, config) {
1590
- if (content.includes("createAuthMiddleware(")) return content;
1591
- let nextContent = addNamedImport(content, "evlog/better-auth", ["createAuthMiddleware", "type BetterAuthInstance"]);
1592
- if (!nextContent.includes(`@${config.projectName}/auth`)) nextContent = prependMissingImports(nextContent, [getAuthImportLine(config)]);
1593
- const authExpression = getAuthExpression(config);
1594
- const authOptions = "{ exclude: [\"/api/auth/**\"], maskEmail: true }";
1595
- const usesFactory = usesCreateAuthFactory(config);
1596
- if (!usesFactory) nextContent = insertBeforeOnce(nextContent, "export const onRequest", `const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n\n`, "const identifyUser = createAuthMiddleware(");
1597
- for (const marker of ["context.locals.log = log;", "locals.log = log;"]) {
1598
- if (!nextContent.includes(marker)) continue;
1599
- const requestExpression = marker.startsWith("context") ? "context.request" : "request";
1600
- const identifySnippet = usesFactory ? `\n\n const identifyUser = createAuthMiddleware(${authExpression} as BetterAuthInstance, ${authOptions});\n await identifyUser(log, ${requestExpression}.headers, url.pathname);` : `\n\n await identifyUser(log, ${requestExpression}.headers, url.pathname);`;
1601
- return insertAfterOnce(nextContent, marker, identifySnippet, "identifyUser(log");
1602
- }
1603
- return nextContent;
1604
- }
1605
- function getNextEvlogFile(serviceName) {
1606
- return `import { createEvlog } from "evlog/next";
1607
- import { createInstrumentation } from "evlog/next/instrumentation/create";
1608
-
1609
- export const { withEvlog, useLogger, log, createError } = createEvlog({
1610
- service: "${serviceName}",
1611
- });
1612
-
1613
- export const { register, onRequestError } = createInstrumentation({
1614
- service: "${serviceName}",
1615
- });
1616
- `;
1617
- }
1618
- function getNextInstrumentationFile() {
1619
- return `import { defineNodeInstrumentation } from "evlog/next/instrumentation";
1620
-
1621
- export const { register, onRequestError } = defineNodeInstrumentation(() => import("./src/lib/evlog"));
1622
- `;
1623
- }
1624
- function getNextProxyFile() {
1625
- return `import { evlogMiddleware } from "evlog/next";
1626
-
1627
- export const proxy = evlogMiddleware();
1628
-
1629
- export const config = {
1630
- matcher: ["/api/:path*"],
1631
- };
1632
- `;
1633
- }
1634
- function getNextEvlogAuthFile(config) {
1635
- if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
1636
- import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1637
- import { useLogger } from "@/lib/evlog";
1638
-
1639
- export async function identifyEvlogUser(request: Request) {
1640
- const identifyUser = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1641
- exclude: ["/api/auth/**"],
1642
- maskEmail: true,
1643
- });
1644
- await identifyUser(useLogger(), request.headers, new URL(request.url).pathname);
1645
- }
1646
- `;
1647
- return `${getAuthImportLine(config)}
1648
- import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1649
- import { useLogger } from "@/lib/evlog";
1650
-
1651
- const identifyUser = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1652
- exclude: ["/api/auth/**"],
1653
- maskEmail: true,
1654
- });
1655
-
1656
- export async function identifyEvlogUser(request: Request) {
1657
- await identifyUser(useLogger(), request.headers, new URL(request.url).pathname);
1658
- }
1659
- `;
1660
- }
1661
- function getNitroEvlogAuthPluginFile(config) {
1662
- if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
1663
- import { createAuthIdentifier, type BetterAuthInstance } from "evlog/better-auth";
1664
-
1665
- export default defineNitroPlugin((nitroApp) => {
1666
- nitroApp.hooks.hook("request", async (event) => {
1667
- const identify = createAuthIdentifier(${getAuthExpression(config)} as BetterAuthInstance, {
1668
- exclude: ["/api/auth/**"],
1669
- maskEmail: true,
1670
- });
1671
- await identify(event);
1672
- });
1673
- });
1674
- `;
1675
- return `${getAuthImportLine(config)}
1676
- import { createAuthIdentifier, type BetterAuthInstance } from "evlog/better-auth";
1677
-
1678
- export default defineNitroPlugin((nitroApp) => {
1679
- nitroApp.hooks.hook(
1680
- "request",
1681
- createAuthIdentifier(${getAuthExpression(config)} as BetterAuthInstance, {
1682
- exclude: ["/api/auth/**"],
1683
- maskEmail: true,
1684
- }),
1685
- );
1686
- });
1687
- `;
1688
- }
1689
- function getNuxtEvlogAuthMiddlewareFile(config) {
1690
- if (usesCreateAuthFactory(config)) return `${getAuthImportLine(config)}
1691
- import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1692
-
1693
- export default defineEventHandler(async (event) => {
1694
- if (!event.context.log) return;
1695
- const identify = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1696
- exclude: ["/api/auth/**"],
1697
- maskEmail: true,
1698
- });
1699
- await identify(event.context.log, event.headers, event.path);
1700
- });
1701
- `;
1702
- return `${getAuthImportLine(config)}
1703
- import { createAuthMiddleware, type BetterAuthInstance } from "evlog/better-auth";
1704
-
1705
- const identify = createAuthMiddleware(${getAuthExpression(config)} as BetterAuthInstance, {
1706
- exclude: ["/api/auth/**"],
1707
- maskEmail: true,
1708
- });
1709
-
1710
- export default defineEventHandler(async (event) => {
1711
- if (!event.context.log) return;
1712
- await identify(event.context.log, event.headers, event.path);
1713
- });
1714
- `;
1715
- }
1716
- function getTanstackNitroConfigFile(serviceName) {
1717
- return `import { defineConfig } from "nitro";
1718
- import evlog from "evlog/nitro/v3";
1719
-
1720
- export default defineConfig({
1721
- experimental: {
1722
- asyncContext: true,
1723
- },
1724
- modules: [
1725
- evlog({
1726
- env: { service: "${serviceName}" },
1727
- }),
1728
- ],
1729
- });
1730
- `;
1731
- }
1732
- function getAstroMiddlewareFile(serviceName) {
1733
- return `import { defineMiddleware } from "astro:middleware";
1734
- import { createRequestLogger, initLogger } from "evlog";
1735
-
1736
- initLogger({
1737
- env: { service: "${serviceName}" },
1738
- });
1739
-
1740
- export const onRequest = defineMiddleware(async ({ request, locals }, next) => {
1741
- const url = new URL(request.url);
1742
- const log = createRequestLogger({
1743
- method: request.method,
1744
- path: url.pathname,
1745
- });
1746
-
1747
- locals.log = log;
1748
-
1749
- try {
1750
- const response = await next();
1751
- log.emit();
1752
- return response;
1753
- } catch (error) {
1754
- log.error(error instanceof Error ? error : new Error(String(error)));
1755
- log.emit();
1756
- throw error;
1757
- }
1758
- });
1759
- `;
1760
- }
1761
- function getAstroEnvFile() {
1762
- return `/// <reference types="astro/client" />
1763
-
1764
- import type { RequestLogger } from "evlog";
1765
-
1766
- declare namespace App {
1767
- interface Locals {
1768
- log: RequestLogger;
1769
- }
1770
- }
1771
- `;
1772
- }
1773
- async function setupNextEvlog(config, serviceName) {
1774
- const webDir = path.join(config.projectDir, "apps/web");
1775
- const evlogPath = path.join(webDir, "src/lib/evlog.ts");
1776
- if (!await fs.pathExists(evlogPath)) await writeFileIfChanged(evlogPath, getNextEvlogFile(serviceName));
1777
- const identifyWebAuth = shouldIdentifyWebAuth(config);
1778
- if (identifyWebAuth) {
1779
- const evlogAuthPath = path.join(webDir, "src/lib/evlog-auth.ts");
1780
- if (!await fs.pathExists(evlogAuthPath)) await writeFileIfChanged(evlogAuthPath, getNextEvlogAuthFile(config));
1781
- }
1782
- const instrumentationPath = path.join(webDir, "instrumentation.ts");
1783
- if (!await fs.pathExists(instrumentationPath)) await writeFileIfChanged(instrumentationPath, getNextInstrumentationFile());
1784
- const proxyPath = path.join(webDir, "src/proxy.ts");
1785
- const rootProxyPath = path.join(webDir, "proxy.ts");
1786
- if (!await fs.pathExists(proxyPath) && !await fs.pathExists(rootProxyPath)) await writeFileIfChanged(proxyPath, getNextProxyFile());
1787
- const updateNextApiRoute = (content) => {
1788
- let nextContent = addNextRouteWrappers(content);
1789
- if (identifyWebAuth) nextContent = addNextBetterAuthToRoute(nextContent);
1790
- return nextContent;
1791
- };
1792
- await updateFileIfExists(path.join(webDir, "src/app/api/trpc/[trpc]/route.ts"), updateNextApiRoute);
1793
- await updateFileIfExists(path.join(webDir, "src/app/api/rpc/[[...rest]]/route.ts"), updateNextApiRoute);
1794
- if (config.examples.includes("ai")) await updateFileIfExists(path.join(webDir, "src/app/api/ai/route.ts"), (content) => {
1795
- let nextContent = addNextAiEvlogSetup(content);
1796
- if (identifyWebAuth) nextContent = addNextBetterAuthToRoute(nextContent);
1797
- return nextContent;
1798
- });
1799
- }
1800
- async function setupNuxtEvlog(config, serviceName) {
1801
- const webDir = path.join(config.projectDir, "apps/web");
1802
- await updateFileIfExists(path.join(webDir, "nuxt.config.ts"), (content) => addNuxtEvlogSetup(content, serviceName));
1803
- if (shouldIdentifyWebAuth(config)) {
1804
- const oldAuthPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
1805
- if (await fs.pathExists(oldAuthPluginPath)) {
1806
- if ((await fs.readFile(oldAuthPluginPath, "utf-8")).includes("evlog/better-auth")) await fs.remove(oldAuthPluginPath);
1807
- }
1808
- const authMiddlewarePath = path.join(webDir, "server/middleware/evlog-auth.ts");
1809
- if (!await fs.pathExists(authMiddlewarePath)) await writeFileIfChanged(authMiddlewarePath, getNuxtEvlogAuthMiddlewareFile(config));
1810
- }
1811
- }
1812
- async function setupSvelteEvlog(config, serviceName) {
1813
- const webDir = path.join(config.projectDir, "apps/web");
1814
- await updateFileIfExists(path.join(webDir, "vite.config.ts"), (content) => addSvelteViteEvlogSetup(content, serviceName));
1815
- const hooksPath = path.join(webDir, "src/hooks.server.ts");
1816
- if (await fs.pathExists(hooksPath)) await updateFileIfExists(hooksPath, addSvelteHooksEvlogSetup);
1817
- else await writeFileIfChanged(hooksPath, `import { createEvlogHooks } from "evlog/sveltekit";
1818
-
1819
- export const { handle, handleError } = createEvlogHooks();
1820
- `);
1821
- await updateFileIfExists(path.join(webDir, "src/app.d.ts"), addSvelteLocalsType);
1822
- if (shouldIdentifyWebAuth(config)) await updateFileIfExists(path.join(webDir, "src/hooks.server.ts"), (content) => addSvelteBetterAuthEvlogSetup(content, config));
1823
- }
1824
- async function setupTanstackStartEvlog(config, serviceName) {
1825
- const webDir = path.join(config.projectDir, "apps/web");
1826
- const nitroConfigPath = path.join(webDir, "nitro.config.ts");
1827
- if (!await fs.pathExists(nitroConfigPath)) await writeFileIfChanged(nitroConfigPath, getTanstackNitroConfigFile(serviceName));
1828
- await updateFileIfExists(path.join(webDir, "src/routes/__root.tsx"), addTanstackStartRootEvlogSetup);
1829
- if (shouldIdentifyWebAuth(config)) {
1830
- const authPluginPath = path.join(webDir, "server/plugins/evlog-auth.ts");
1831
- if (!await fs.pathExists(authPluginPath)) await writeFileIfChanged(authPluginPath, getNitroEvlogAuthPluginFile(config));
1832
- }
1833
- }
1834
- async function setupAstroEvlog(config, serviceName) {
1835
- const webDir = path.join(config.projectDir, "apps/web");
1836
- const middlewarePath = path.join(webDir, "src/middleware.ts");
1837
- if (!await fs.pathExists(middlewarePath)) await writeFileIfChanged(middlewarePath, getAstroMiddlewareFile(serviceName));
1838
- else await updateFileIfExists(middlewarePath, (content) => addAstroMiddlewareEvlogSetup(content, serviceName));
1839
- const envPath = path.join(webDir, "src/env.d.ts");
1840
- if (!await fs.pathExists(envPath)) await writeFileIfChanged(envPath, getAstroEnvFile());
1841
- else await updateFileIfExists(envPath, addAstroLocalsType);
1842
- if (shouldIdentifyWebAuth(config)) await updateFileIfExists(middlewarePath, (content) => addAstroBetterAuthEvlogSetup(content, config));
1843
- }
1844
- async function setupEvlogWeb(config) {
1845
- const frontend = getEvlogWebFrontend(config.frontend);
1846
- if (!frontend) return;
1847
- const serviceName = `${config.projectName}-web`;
1848
- if (frontend === "next") await setupNextEvlog(config, serviceName);
1849
- else if (frontend === "nuxt") await setupNuxtEvlog(config, serviceName);
1850
- else if (frontend === "svelte") await setupSvelteEvlog(config, serviceName);
1851
- else if (frontend === "tanstack-start") await setupTanstackStartEvlog(config, serviceName);
1852
- else if (frontend === "astro") await setupAstroEvlog(config, serviceName);
1853
- }
1854
- async function setupEvlog(config) {
1855
- return Result.tryPromise({
1856
- try: async () => {
1857
- if (isEvlogBackend(config.backend)) {
1858
- const serverIndexPath = path.join(config.projectDir, "apps/server/src/index.ts");
1859
- if (await fs.pathExists(serverIndexPath)) {
1860
- const content = await fs.readFile(serverIndexPath, "utf-8");
1861
- let nextContent = addEvlogServerSetup(content, config.backend, `${config.projectName}-server`);
1862
- if (config.auth === "better-auth") nextContent = addEvlogBetterAuthServerSetup(nextContent, config.backend, getAuthExpression(config));
1863
- if (nextContent !== content) await fs.writeFile(serverIndexPath, nextContent);
1864
- }
1865
- }
1866
- await setupEvlogWeb(config);
1867
- },
1868
- catch: (error) => new AddonSetupError({
1869
- addon: "evlog",
1870
- message: `Failed to set up evlog: ${error instanceof Error ? error.message : String(error)}`,
1871
- cause: error
1872
- })
1873
- });
1874
- }
1875
- //#endregion
1876
1373
  //#region src/prompts/navigable-group.ts
1877
1374
  /**
1878
1375
  * Navigable group - a group of prompts that allows going back
@@ -1915,7 +1412,7 @@ async function navigableGroup(prompts, opts) {
1915
1412
  goingBack = false;
1916
1413
  continue;
1917
1414
  }
1918
- if (isCancel$1(result)) {
1415
+ if (isCancel$2(result)) {
1919
1416
  if (typeof opts?.onCancel === "function") {
1920
1417
  results[name] = "canceled";
1921
1418
  opts.onCancel({ results });
@@ -2036,206 +1533,6 @@ function getPackageRunnerPrefix(packageManager) {
2036
1533
  }
2037
1534
  }
2038
1535
  //#endregion
2039
- //#region src/helpers/addons/fumadocs-setup.ts
2040
- const TEMPLATES$2 = {
2041
- "next-mdx": {
2042
- label: "Next.js: Fumadocs MDX",
2043
- hint: "recommended",
2044
- value: "+next+fuma-docs-mdx"
2045
- },
2046
- "next-mdx-static": {
2047
- label: "Next.js Static: Fumadocs MDX",
2048
- value: "+next+fuma-docs-mdx+static"
2049
- },
2050
- waku: {
2051
- label: "Waku: Fumadocs MDX",
2052
- value: "waku"
2053
- },
2054
- "react-router": {
2055
- label: "React Router: Fumadocs MDX (not RSC)",
2056
- value: "react-router"
2057
- },
2058
- "react-router-spa": {
2059
- label: "React Router SPA: Fumadocs MDX (not RSC)",
2060
- hint: "SPA mode allows you to host the site statically, compatible with a CDN.",
2061
- value: "react-router-spa"
2062
- },
2063
- "tanstack-start": {
2064
- label: "Tanstack Start: Fumadocs MDX (not RSC)",
2065
- value: "tanstack-start"
2066
- },
2067
- "tanstack-start-spa": {
2068
- label: "Tanstack Start SPA: Fumadocs MDX (not RSC)",
2069
- hint: "SPA mode allows you to host the site statically, compatible with a CDN.",
2070
- value: "tanstack-start-spa"
2071
- }
2072
- };
2073
- const DEFAULT_TEMPLATE$2 = "next-mdx";
2074
- const DEFAULT_DEV_PORT$1 = 4e3;
2075
- function aiChatDisabledForTemplate(template) {
2076
- return template === "next-mdx-static" || template.endsWith("-spa");
2077
- }
2078
- function getFumadocsLinter(addons) {
2079
- if (addons.includes("oxlint")) return "oxlint";
2080
- if (addons.includes("biome") || addons.includes("ultracite")) return "biome";
2081
- if (addons.includes("vite-plus")) return "oxlint";
2082
- }
2083
- function getFumadocsAddonContext(currentAddons, persistedAddons) {
2084
- return Array.from(new Set([...persistedAddons ?? [], ...currentAddons]));
2085
- }
2086
- async function setupFumadocs(config) {
2087
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
2088
- const { packageManager, projectDir } = config;
2089
- cliLog.info("Setting up Fumadocs...");
2090
- const configuredOptions = config.addonOptions?.fumadocs;
2091
- let template = configuredOptions?.template;
2092
- let search = configuredOptions?.search;
2093
- let ogImage = configuredOptions?.ogImage;
2094
- let aiChat = configuredOptions?.aiChat;
2095
- if (isSilent()) template = template ?? DEFAULT_TEMPLATE$2;
2096
- else {
2097
- const promptResult = await Result.tryPromise({
2098
- try: () => navigableGroup({
2099
- template: async () => {
2100
- if (template !== void 0) return template;
2101
- return navigableSelect({
2102
- message: "Choose a template",
2103
- options: Object.entries(TEMPLATES$2).map(([key, t]) => ({
2104
- value: key,
2105
- label: t.label,
2106
- hint: "hint" in t ? t.hint : void 0
2107
- })),
2108
- initialValue: DEFAULT_TEMPLATE$2
2109
- });
2110
- },
2111
- search: async () => {
2112
- if (search !== void 0) return search;
2113
- return navigableSelect({
2114
- message: "Choose a search solution?",
2115
- options: [{
2116
- value: "orama",
2117
- label: "Default",
2118
- hint: "local search powered by Orama, recommended"
2119
- }, {
2120
- value: "orama-cloud",
2121
- label: "Orama Cloud",
2122
- hint: "3rd party search solution, signup needed"
2123
- }],
2124
- initialValue: "orama"
2125
- });
2126
- },
2127
- ogImage: async ({ results }) => {
2128
- if (ogImage !== void 0) return ogImage;
2129
- if (!(results.template ?? template ?? DEFAULT_TEMPLATE$2).startsWith("next-")) return "skip";
2130
- return navigableSelect({
2131
- message: "Configure Open Graph Image generation?",
2132
- options: [{
2133
- value: "next-og",
2134
- label: "next/og",
2135
- hint: "Next.js built-in solution"
2136
- }, {
2137
- value: "takumi",
2138
- label: "Takumi",
2139
- hint: "Output WebP format, framework-agnostic"
2140
- }],
2141
- initialValue: "next-og"
2142
- });
2143
- },
2144
- aiChat: async ({ results }) => {
2145
- if (aiChat !== void 0) return aiChat;
2146
- if (aiChatDisabledForTemplate(results.template ?? template ?? DEFAULT_TEMPLATE$2)) return "none";
2147
- return navigableSelect({
2148
- message: "Configure AI Chat?",
2149
- options: [
2150
- {
2151
- value: "none",
2152
- label: "No"
2153
- },
2154
- {
2155
- value: "openrouter",
2156
- label: "AI SDK",
2157
- hint: "default to OpenRouter"
2158
- },
2159
- {
2160
- value: "llmgateway",
2161
- label: "LLM Gateway"
2162
- },
2163
- {
2164
- value: "inkeep",
2165
- label: "Inkeep AI",
2166
- hint: "API key required"
2167
- }
2168
- ],
2169
- initialValue: "none"
2170
- });
2171
- }
2172
- }),
2173
- catch: (e) => new AddonSetupError({
2174
- addon: "fumadocs",
2175
- message: `Failed to run Fumadocs prompts: ${e instanceof Error ? e.message : String(e)}`,
2176
- cause: e
2177
- })
2178
- });
2179
- if (promptResult.isErr()) return Result.err(promptResult.error);
2180
- const results = promptResult.value;
2181
- if (results.template === void 0 || results.search === void 0 || results.ogImage === void 0 || results.aiChat === void 0) return userCancelled("Operation cancelled");
2182
- template = results.template;
2183
- search = results.search;
2184
- ogImage = results.ogImage === "skip" ? void 0 : results.ogImage;
2185
- aiChat = results.aiChat === "none" ? void 0 : results.aiChat;
2186
- }
2187
- if (!template) return userCancelled("Operation cancelled");
2188
- const isNextTemplate = template.startsWith("next-");
2189
- if (!isNextTemplate) ogImage = void 0;
2190
- if (aiChatDisabledForTemplate(template)) aiChat = void 0;
2191
- const templateArg = TEMPLATES$2[template].value;
2192
- const devPort = configuredOptions?.devPort ?? DEFAULT_DEV_PORT$1;
2193
- const options = [
2194
- `--template ${templateArg}`,
2195
- `--pm ${packageManager}`,
2196
- "--no-git"
2197
- ];
2198
- if (isNextTemplate) options.push("--src");
2199
- const persistedConfig = await readKubojsConfig(projectDir);
2200
- const linter = getFumadocsLinter(getFumadocsAddonContext(config.addons, persistedConfig?.addons));
2201
- if (linter) options.push(`--linter ${linter}`);
2202
- if (search) options.push(`--search ${search}`);
2203
- if (ogImage) options.push(`--og-image ${ogImage}`);
2204
- if (aiChat) options.push(`--ai-chat ${aiChat}`);
2205
- const args = getPackageExecutionArgs(packageManager, `create-fumadocs-app@latest fumadocs ${options.join(" ")}`);
2206
- const appsDir = path.join(projectDir, "apps");
2207
- await fs.ensureDir(appsDir);
2208
- const s = createSpinner();
2209
- s.start("Running Fumadocs create command...");
2210
- const result = await Result.tryPromise({
2211
- try: async () => {
2212
- await $({
2213
- cwd: appsDir,
2214
- env: { CI: "true" }
2215
- })`${args}`;
2216
- const fumadocsDir = path.join(projectDir, "apps", "fumadocs");
2217
- const packageJsonPath = path.join(fumadocsDir, "package.json");
2218
- if (await fs.pathExists(packageJsonPath)) {
2219
- const packageJson = await fs.readJson(packageJsonPath);
2220
- packageJson.name = "fumadocs";
2221
- if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port=${devPort}`;
2222
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
2223
- }
2224
- },
2225
- catch: (e) => new AddonSetupError({
2226
- addon: "fumadocs",
2227
- message: `Failed to set up Fumadocs: ${e instanceof Error ? e.message : String(e)}`,
2228
- cause: e
2229
- })
2230
- });
2231
- if (result.isErr()) {
2232
- s.stop("Failed to set up Fumadocs");
2233
- return result;
2234
- }
2235
- s.stop("Fumadocs setup complete!");
2236
- return Result.ok(void 0);
2237
- }
2238
- //#endregion
2239
1536
  //#region src/helpers/addons/mcp-setup.ts
2240
1537
  const MCP_AGENTS = [
2241
1538
  {
@@ -2310,7 +1607,7 @@ const MCP_AGENTS = [
2310
1607
  }
2311
1608
  ];
2312
1609
  const DEFAULT_SCOPE$1 = "project";
2313
- const DEFAULT_AGENTS$2 = [
1610
+ const DEFAULT_AGENTS$1 = [
2314
1611
  "cursor",
2315
1612
  "claude-code",
2316
1613
  "vscode"
@@ -2434,7 +1731,6 @@ function getAllMcpServers(config) {
2434
1731
  function getRecommendedMcpServers(config, scope) {
2435
1732
  const serversByKey = new Map(getAllMcpServers(config).map((server) => [server.key, server]));
2436
1733
  const recommendedServerKeys = ["kubojs", "context7"];
2437
- if (scope === "project" && config.addons.includes("nx")) recommendedServerKeys.push("nx");
2438
1734
  if (config.runtime === "workers" || config.webDeploy === "cloudflare" || config.serverDeploy === "cloudflare") recommendedServerKeys.push("cloudflare-docs");
2439
1735
  if (config.backend === "convex") recommendedServerKeys.push("convex");
2440
1736
  if (hasReactBasedFrontend$1(config.frontend)) recommendedServerKeys.push("shadcn");
@@ -2473,7 +1769,7 @@ async function setupMcp(config) {
2473
1769
  selectedServerKeys = configuredServerKeys?.filter((k) => availableServerKeys.has(k)) ?? serverOptions;
2474
1770
  if (selectedServerKeys.length === 0) return Result.ok(void 0);
2475
1771
  const agentOptions = filterAgentsForScope(scope);
2476
- const defaultAgents = uniqueValues$1(DEFAULT_AGENTS$2.filter((a) => agentOptions.some((o) => o.value === a)));
1772
+ const defaultAgents = uniqueValues$1(DEFAULT_AGENTS$1.filter((a) => agentOptions.some((o) => o.value === a)));
2477
1773
  selectedAgents = configuredAgents?.filter((a) => agentOptions.some((o) => o.value === a)) ?? defaultAgents;
2478
1774
  if (selectedAgents.length === 0) return Result.ok(void 0);
2479
1775
  } else {
@@ -2516,7 +1812,7 @@ async function setupMcp(config) {
2516
1812
  if (currentServers !== void 0 && currentServers.length === 0) return [];
2517
1813
  const agentOpts = filterAgentsForScope(currentScope);
2518
1814
  if (agentOpts.length === 0) return [];
2519
- const defaults = uniqueValues$1(DEFAULT_AGENTS$2.filter((a) => agentOpts.some((o) => o.value === a)));
1815
+ const defaults = uniqueValues$1(DEFAULT_AGENTS$1.filter((a) => agentOpts.some((o) => o.value === a)));
2520
1816
  if (configuredAgents !== void 0) return configuredAgents.filter((a) => agentOpts.some((o) => o.value === a));
2521
1817
  return navigableMultiselect({
2522
1818
  message: "Select agents to install MCP servers to",
@@ -2762,7 +2058,7 @@ const AVAILABLE_AGENTS = [
2762
2058
  }
2763
2059
  ];
2764
2060
  const DEFAULT_SCOPE = "project";
2765
- const DEFAULT_AGENTS$1 = [
2061
+ const DEFAULT_AGENTS = [
2766
2062
  "cursor",
2767
2063
  "claude-code",
2768
2064
  "github-copilot"
@@ -2797,8 +2093,6 @@ function getRecommendedSourceKeys(config) {
2797
2093
  if (backend === "elysia") sources.push("elysiajs/skills");
2798
2094
  if (backend === "convex") sources.push("waynesutton/convexskills");
2799
2095
  if (addons.includes("opentui")) sources.push("msmps/opentui-skill");
2800
- if (addons.includes("ultracite")) sources.push("haydenbleasel/ultracite");
2801
- if (addons.includes("evlog")) sources.push("https://www.evlog.dev");
2802
2096
  return sources;
2803
2097
  }
2804
2098
  const CURATED_SKILLS_BY_SOURCE = {
@@ -2910,7 +2204,7 @@ async function setupSkills(config) {
2910
2204
  scope = configuredScope ?? DEFAULT_SCOPE;
2911
2205
  selectedSkills = configuredSelections !== void 0 ? configuredSelections.flatMap((selection) => selection.skills.map((skill) => `${selection.source}::${skill}`)) : allSkillValues;
2912
2206
  if (selectedSkills.length === 0) return Result.ok(void 0);
2913
- selectedAgents = configuredAgents ? [...configuredAgents] : [...DEFAULT_AGENTS$1];
2207
+ selectedAgents = configuredAgents ? [...configuredAgents] : [...DEFAULT_AGENTS];
2914
2208
  if (selectedAgents.length === 0) return Result.ok(void 0);
2915
2209
  } else {
2916
2210
  const results = await navigableGroup({
@@ -2947,7 +2241,7 @@ async function setupSkills(config) {
2947
2241
  message: "Select agents to install skills to",
2948
2242
  options: AVAILABLE_AGENTS,
2949
2243
  required: false,
2950
- initialValues: [...DEFAULT_AGENTS$1]
2244
+ initialValues: [...DEFAULT_AGENTS]
2951
2245
  });
2952
2246
  }
2953
2247
  });
@@ -2996,44 +2290,6 @@ async function setupSkills(config) {
2996
2290
  return Result.ok(void 0);
2997
2291
  }
2998
2292
  //#endregion
2999
- //#region src/helpers/addons/starlight-setup.ts
3000
- async function setupStarlight(config) {
3001
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
3002
- const { packageManager, projectDir } = config;
3003
- const s = createSpinner();
3004
- s.start("Setting up Starlight docs...");
3005
- const args = getPackageExecutionArgs(packageManager, `create-astro@latest ${[
3006
- "docs",
3007
- "--template",
3008
- "starlight",
3009
- "--yes",
3010
- "--no-install",
3011
- "--no-git",
3012
- "--skip-houston"
3013
- ].join(" ")}`);
3014
- const appsDir = path.join(projectDir, "apps");
3015
- await fs.ensureDir(appsDir);
3016
- const result = await Result.tryPromise({
3017
- try: async () => {
3018
- await $({
3019
- cwd: appsDir,
3020
- env: { CI: "true" }
3021
- })`${args}`;
3022
- },
3023
- catch: (e) => new AddonSetupError({
3024
- addon: "starlight",
3025
- message: `Failed to set up Starlight docs: ${e instanceof Error ? e.message : String(e)}`,
3026
- cause: e
3027
- })
3028
- });
3029
- if (result.isErr()) {
3030
- s.stop("Failed to set up Starlight docs");
3031
- return result;
3032
- }
3033
- s.stop("Starlight docs setup successfully!");
3034
- return Result.ok(void 0);
3035
- }
3036
- //#endregion
3037
2293
  //#region src/helpers/addons/tauri-setup.ts
3038
2294
  function getWebFrontend(frontend) {
3039
2295
  return frontend.find((value) => types_exports.desktopWebFrontends.includes(value));
@@ -3114,7 +2370,7 @@ async function setupTauri(config) {
3114
2370
  }
3115
2371
  //#endregion
3116
2372
  //#region src/helpers/addons/tui-setup.ts
3117
- const TEMPLATES$1 = {
2373
+ const TEMPLATES = {
3118
2374
  core: {
3119
2375
  label: "Core",
3120
2376
  hint: "Basic OpenTUI template"
@@ -3128,7 +2384,7 @@ const TEMPLATES$1 = {
3128
2384
  hint: "SolidJS-based OpenTUI template"
3129
2385
  }
3130
2386
  };
3131
- const DEFAULT_TEMPLATE$1 = "core";
2387
+ const DEFAULT_TEMPLATE = "core";
3132
2388
  const TUI_LOCKFILES = [
3133
2389
  "bun.lock",
3134
2390
  "package-lock.json",
@@ -3138,7 +2394,7 @@ const TUI_LOCKFILES = [
3138
2394
  function resolveTuiTemplate(config) {
3139
2395
  const configuredTemplate = config.addonOptions?.opentui?.template;
3140
2396
  if (configuredTemplate) return configuredTemplate;
3141
- if (isSilent()) return DEFAULT_TEMPLATE$1;
2397
+ if (isSilent()) return DEFAULT_TEMPLATE;
3142
2398
  }
3143
2399
  async function setupTui(config) {
3144
2400
  if (shouldSkipExternalCommands()) return Result.ok(void 0);
@@ -3149,14 +2405,14 @@ async function setupTui(config) {
3149
2405
  setIsFirstPrompt(true);
3150
2406
  const selectedTemplate = await navigableSelect({
3151
2407
  message: "Choose a template",
3152
- options: Object.entries(TEMPLATES$1).map(([key, templateOption]) => ({
2408
+ options: Object.entries(TEMPLATES).map(([key, templateOption]) => ({
3153
2409
  value: key,
3154
2410
  label: templateOption.label,
3155
2411
  hint: templateOption.hint
3156
2412
  })),
3157
- initialValue: DEFAULT_TEMPLATE$1
2413
+ initialValue: DEFAULT_TEMPLATE
3158
2414
  });
3159
- if (isCancel$1(selectedTemplate)) return userCancelled("Operation cancelled");
2415
+ if (isCancel$2(selectedTemplate)) return userCancelled("Operation cancelled");
3160
2416
  template = selectedTemplate;
3161
2417
  }
3162
2418
  const args = getPackageExecutionArgs(packageManager, `create-tui@latest --template ${template} --no-git --no-install tui`);
@@ -3234,324 +2490,6 @@ async function postProcessTuiWorkspace(tuiDir) {
3234
2490
  return Result.ok(void 0);
3235
2491
  }
3236
2492
  //#endregion
3237
- //#region src/helpers/addons/ultracite-setup.ts
3238
- const LINTERS = {
3239
- biome: { label: "Biome (Recommended)" },
3240
- oxlint: { label: "Oxlint + Oxfmt" }
3241
- };
3242
- const AGENTS = {
3243
- universal: { label: "Universal (AGENTS.md — covers all agents)" },
3244
- claude: { label: "Claude Code" },
3245
- codex: { label: "Codex" },
3246
- jules: { label: "Jules" },
3247
- replit: { label: "Replit Agent" },
3248
- devin: { label: "Devin" },
3249
- lovable: { label: "Lovable" },
3250
- zencoder: { label: "Zencoder" },
3251
- ona: { label: "Ona" },
3252
- openclaw: { label: "OpenClaw" },
3253
- continue: { label: "Continue" },
3254
- "snowflake-cortex": { label: "Snowflake Cortex" },
3255
- deepagents: { label: "Deepagents" },
3256
- qoder: { label: "Qoder" },
3257
- "kimi-cli": { label: "Kimi CLI" },
3258
- mcpjam: { label: "MCPJam" },
3259
- mux: { label: "Mux" },
3260
- pi: { label: "Pi" },
3261
- adal: { label: "AdaL" },
3262
- copilot: { label: "GitHub Copilot" },
3263
- cline: { label: "Cline" },
3264
- amp: { label: "AMP" },
3265
- aider: { label: "Aider" },
3266
- "firebase-studio": { label: "Firebase Studio" },
3267
- "open-hands": { label: "OpenHands" },
3268
- gemini: { label: "Gemini" },
3269
- junie: { label: "Junie" },
3270
- augmentcode: { label: "Augment Code" },
3271
- bob: { label: "IBM Bob" },
3272
- "kilo-code": { label: "Kilo Code" },
3273
- goose: { label: "Goose" },
3274
- "roo-code": { label: "Roo Code" },
3275
- warp: { label: "Warp" },
3276
- droid: { label: "Droid" },
3277
- opencode: { label: "OpenCode" },
3278
- crush: { label: "Crush" },
3279
- qwen: { label: "Qwen Code" },
3280
- "amazon-q-cli": { label: "Amazon Q CLI" },
3281
- firebender: { label: "Firebender" },
3282
- "cursor-cli": { label: "Cursor CLI" },
3283
- "mistral-vibe": { label: "Mistral Vibe" },
3284
- vercel: { label: "Vercel Agent" }
3285
- };
3286
- const HOOKS = {
3287
- cursor: { label: "Cursor" },
3288
- windsurf: { label: "Windsurf" },
3289
- codebuddy: { label: "CodeBuddy" },
3290
- claude: { label: "Claude Code" },
3291
- copilot: { label: "GitHub Copilot" }
3292
- };
3293
- const ULTRACITE_VERSION = "7.9.3";
3294
- const DEFAULT_LINTER = "biome";
3295
- const DEFAULT_EDITORS = ["vscode"];
3296
- const DEFAULT_AGENTS = ["universal"];
3297
- const DEFAULT_HOOKS = [];
3298
- function getFrameworksFromFrontend(frontend) {
3299
- const frameworkMap = {
3300
- "tanstack-router": ["react", "tanstack"],
3301
- "react-router": ["react", "remix"],
3302
- "tanstack-start": ["react", "tanstack"],
3303
- next: ["react", "next"],
3304
- nuxt: ["vue"],
3305
- "native-bare": ["react"],
3306
- "native-uniwind": ["react"],
3307
- "native-unistyles": ["react"],
3308
- svelte: ["svelte"],
3309
- solid: ["solid"],
3310
- astro: ["astro"]
3311
- };
3312
- const frameworks = /* @__PURE__ */ new Set();
3313
- for (const f of frontend) for (const framework of frameworkMap[f] ?? []) frameworks.add(framework);
3314
- return Array.from(frameworks);
3315
- }
3316
- function buildUltraciteInitArgs({ packageManager, linter, frameworks, editors, agents, hooks, gitHooks }) {
3317
- const ultraciteArgs = [
3318
- "init",
3319
- "--pm",
3320
- packageManager,
3321
- "--linter",
3322
- linter
3323
- ];
3324
- if (frameworks.length > 0) ultraciteArgs.push("--frameworks", ...frameworks);
3325
- if (editors.length > 0) ultraciteArgs.push("--editors", ...editors);
3326
- if (agents.length > 0) ultraciteArgs.push("--agents", ...agents);
3327
- if (hooks.length > 0) ultraciteArgs.push("--hooks", ...hooks);
3328
- if (gitHooks.length > 0) ultraciteArgs.push("--integrations", ...gitHooks);
3329
- return [
3330
- ...getPackageRunnerPrefix(packageManager),
3331
- `ultracite@${ULTRACITE_VERSION}`,
3332
- ...ultraciteArgs,
3333
- "--skip-install",
3334
- "--quiet"
3335
- ];
3336
- }
3337
- async function removeHuskySampleHook(projectDir) {
3338
- const hookPath = path.join(projectDir, ".husky", "pre-commit");
3339
- if (!await fs.pathExists(hookPath)) return;
3340
- const content = await fs.readFile(hookPath, "utf-8");
3341
- const cleaned = content.split("\n").filter((line) => !/^(?:npm|pnpm|yarn|bun|deno)(?: run)? test$/.test(line.trim())).join("\n");
3342
- if (cleaned !== content) await fs.writeFile(hookPath, cleaned);
3343
- }
3344
- async function setupUltracite(config, gitHooks) {
3345
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
3346
- const { packageManager, projectDir, frontend } = config;
3347
- cliLog.info("Setting up Ultracite...");
3348
- const configuredOptions = config.addonOptions?.ultracite;
3349
- let linter = configuredOptions?.linter;
3350
- let editors = configuredOptions?.editors;
3351
- let agents = configuredOptions?.agents;
3352
- let hooks = configuredOptions?.hooks;
3353
- if (!linter || !editors || !agents || !hooks) if (isSilent()) {
3354
- linter = linter ?? DEFAULT_LINTER;
3355
- editors = editors ?? [...DEFAULT_EDITORS];
3356
- agents = agents ?? [...DEFAULT_AGENTS];
3357
- hooks = hooks ?? [...DEFAULT_HOOKS];
3358
- } else {
3359
- const results = await navigableGroup({
3360
- linter: async () => {
3361
- if (linter !== void 0) return linter;
3362
- return navigableSelect({
3363
- message: "Which linter do you want to use?",
3364
- options: Object.entries(LINTERS).map(([key, linterOption]) => ({
3365
- value: key,
3366
- label: linterOption.label
3367
- })),
3368
- initialValue: linter ?? DEFAULT_LINTER
3369
- });
3370
- },
3371
- editors: async () => {
3372
- if (editors !== void 0) return editors;
3373
- return navigableMultiselect({
3374
- message: "Which editors do you want to configure (recommended)?",
3375
- required: false,
3376
- options: [{
3377
- value: "vscode",
3378
- label: "VSCode / Cursor / Windsurf"
3379
- }, {
3380
- value: "zed",
3381
- label: "Zed"
3382
- }]
3383
- });
3384
- },
3385
- agents: async () => {
3386
- if (agents !== void 0) return agents;
3387
- return navigableMultiselect({
3388
- message: "Which agent files do you want to add (optional)?",
3389
- required: false,
3390
- options: Object.entries(AGENTS).map(([key, agent]) => ({
3391
- value: key,
3392
- label: agent.label
3393
- }))
3394
- });
3395
- },
3396
- hooks: async () => {
3397
- if (hooks !== void 0) return hooks;
3398
- return navigableMultiselect({
3399
- message: "Which agent hooks do you want to enable (optional)?",
3400
- required: false,
3401
- options: Object.entries(HOOKS).map(([key, hook]) => ({
3402
- value: key,
3403
- label: hook.label
3404
- }))
3405
- });
3406
- }
3407
- });
3408
- if (results.linter === void 0 || results.editors === void 0 || results.agents === void 0 || results.hooks === void 0) return userCancelled("Operation cancelled");
3409
- linter = results.linter;
3410
- editors = results.editors;
3411
- agents = results.agents;
3412
- hooks = results.hooks;
3413
- }
3414
- const frameworks = getFrameworksFromFrontend(frontend);
3415
- const args = buildUltraciteInitArgs({
3416
- packageManager,
3417
- linter,
3418
- frameworks,
3419
- editors,
3420
- agents,
3421
- hooks,
3422
- gitHooks
3423
- });
3424
- const s = createSpinner();
3425
- s.start("Running Ultracite init command...");
3426
- const initResult = await Result.tryPromise({
3427
- try: async () => {
3428
- await $({
3429
- cwd: projectDir,
3430
- env: { CI: "true" }
3431
- })`${args}`;
3432
- },
3433
- catch: (e) => {
3434
- s.stop(pc.red("Failed to run Ultracite init command"));
3435
- return new AddonSetupError({
3436
- addon: "ultracite",
3437
- message: `Failed to set up Ultracite: ${e instanceof Error ? e.message : String(e)}`,
3438
- cause: e
3439
- });
3440
- }
3441
- });
3442
- if (initResult.isErr()) {
3443
- cliLog.error(pc.red("Failed to set up Ultracite"));
3444
- return initResult;
3445
- }
3446
- if (gitHooks.includes("husky")) await Result.tryPromise({
3447
- try: () => removeHuskySampleHook(projectDir),
3448
- catch: (e) => e
3449
- });
3450
- s.stop("Ultracite setup successfully!");
3451
- return Result.ok(void 0);
3452
- }
3453
- //#endregion
3454
- //#region src/helpers/addons/wxt-setup.ts
3455
- const TEMPLATES = {
3456
- vanilla: {
3457
- label: "Vanilla",
3458
- hint: "Vanilla JavaScript template"
3459
- },
3460
- vue: {
3461
- label: "Vue",
3462
- hint: "Vue.js template"
3463
- },
3464
- react: {
3465
- label: "React",
3466
- hint: "React template"
3467
- },
3468
- solid: {
3469
- label: "Solid",
3470
- hint: "SolidJS template"
3471
- },
3472
- svelte: {
3473
- label: "Svelte",
3474
- hint: "Svelte template"
3475
- }
3476
- };
3477
- const DEFAULT_TEMPLATE = "react";
3478
- const DEFAULT_DEV_PORT = 5555;
3479
- async function setupWxt(config) {
3480
- if (shouldSkipExternalCommands()) return Result.ok(void 0);
3481
- const { packageManager, projectDir } = config;
3482
- cliLog.info("Setting up WXT...");
3483
- const configuredOptions = config.addonOptions?.wxt;
3484
- let template = configuredOptions?.template;
3485
- if (!template) if (isSilent()) template = DEFAULT_TEMPLATE;
3486
- else {
3487
- setIsFirstPrompt(true);
3488
- const selectedTemplate = await navigableSelect({
3489
- message: "Choose a template",
3490
- options: Object.entries(TEMPLATES).map(([key, templateOption]) => ({
3491
- value: key,
3492
- label: templateOption.label,
3493
- hint: templateOption.hint
3494
- })),
3495
- initialValue: DEFAULT_TEMPLATE
3496
- });
3497
- if (isCancel$1(selectedTemplate)) return userCancelled("Operation cancelled");
3498
- template = selectedTemplate;
3499
- }
3500
- const devPort = configuredOptions?.devPort ?? DEFAULT_DEV_PORT;
3501
- const args = getPackageExecutionArgs(packageManager, `wxt@latest init extension --template ${template} --pm ${packageManager}`);
3502
- const appsDir = path.join(projectDir, "apps");
3503
- const ensureDirResult = await Result.tryPromise({
3504
- try: () => fs.ensureDir(appsDir),
3505
- catch: (e) => new AddonSetupError({
3506
- addon: "wxt",
3507
- message: `Failed to create directory: ${e instanceof Error ? e.message : String(e)}`,
3508
- cause: e
3509
- })
3510
- });
3511
- if (ensureDirResult.isErr()) return ensureDirResult;
3512
- const s = createSpinner();
3513
- s.start("Running WXT init command...");
3514
- const initResult = await Result.tryPromise({
3515
- try: async () => {
3516
- await $({
3517
- cwd: appsDir,
3518
- env: { CI: "true" }
3519
- })`${args}`;
3520
- },
3521
- catch: (e) => {
3522
- s.stop(pc.red("Failed to run WXT init command"));
3523
- return new AddonSetupError({
3524
- addon: "wxt",
3525
- message: `Failed to set up WXT: ${e instanceof Error ? e.message : String(e)}`,
3526
- cause: e
3527
- });
3528
- }
3529
- });
3530
- if (initResult.isErr()) {
3531
- cliLog.error(pc.red("Failed to set up WXT"));
3532
- return initResult;
3533
- }
3534
- const extensionDir = path.join(projectDir, "apps", "extension");
3535
- const packageJsonPath = path.join(extensionDir, "package.json");
3536
- if ((await Result.tryPromise({
3537
- try: async () => {
3538
- if (await fs.pathExists(packageJsonPath)) {
3539
- const packageJson = await fs.readJson(packageJsonPath);
3540
- packageJson.name = "extension";
3541
- if (packageJson.scripts?.dev) packageJson.scripts.dev = `${packageJson.scripts.dev} --port ${devPort}`;
3542
- await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
3543
- }
3544
- },
3545
- catch: (e) => new AddonSetupError({
3546
- addon: "wxt",
3547
- message: `Failed to update package.json: ${e instanceof Error ? e.message : String(e)}`,
3548
- cause: e
3549
- })
3550
- })).isErr()) cliLog.warn(pc.yellow("WXT setup completed but failed to update package.json"));
3551
- s.stop("WXT setup complete!");
3552
- return Result.ok(void 0);
3553
- }
3554
- //#endregion
3555
2493
  //#region src/helpers/addons/addons-setup.ts
3556
2494
  async function runSetup(setupFn) {
3557
2495
  const result = await setupFn();
@@ -3572,41 +2510,27 @@ async function runAddonStep(addon, step) {
3572
2510
  if (result.isErr()) cliConsola.error(pc.red(result.error.message));
3573
2511
  }
3574
2512
  async function setupAddons(config) {
3575
- const { addons, testing, frontend, projectDir } = config;
2513
+ const { addons, frontend, projectDir } = config;
3576
2514
  const hasWebFrontend = frontend.some((value) => types_exports.desktopWebFrontends.includes(value));
3577
2515
  if (addons.includes("tauri") && hasWebFrontend) await runSetup(() => setupTauri(config));
3578
- const hasUltracite = addons.includes("ultracite");
3579
- const hasOxlint = !hasUltracite && addons.includes("oxlint");
3580
- const hasBiome = !hasUltracite && !hasOxlint && addons.includes("biome");
2516
+ const hasOxlint = addons.includes("oxlint");
2517
+ const hasBiome = !hasOxlint && addons.includes("biome");
3581
2518
  const hasHusky = addons.includes("husky");
3582
2519
  const hasLefthook = addons.includes("lefthook");
3583
2520
  const hasVitePlus = addons.includes("vite-plus");
3584
- if (hasUltracite) {
3585
- const gitHooks = [];
3586
- if (hasHusky) gitHooks.push("husky");
3587
- if (hasLefthook) gitHooks.push("lefthook");
3588
- await runSetup(() => setupUltracite(config, gitHooks));
3589
- } else {
3590
- if (hasBiome) await runAddonStep("biome", () => setupBiome(projectDir));
3591
- else if (hasOxlint) await runSetup(() => setupOxlint(projectDir, config.packageManager));
3592
- if (hasHusky || hasLefthook) {
3593
- let linter;
3594
- if (hasOxlint) linter = "oxlint";
3595
- else if (hasBiome) linter = "biome";
3596
- else if (hasVitePlus) linter = "vite-plus";
3597
- if (hasHusky) await runAddonStep("husky", () => setupHusky(projectDir, linter));
3598
- if (hasLefthook) await runAddonStep("lefthook", () => setupLefthook(projectDir));
3599
- }
2521
+ if (hasBiome) await runAddonStep("biome", () => setupBiome(projectDir));
2522
+ else if (hasOxlint) await runSetup(() => setupOxlint(projectDir, config.packageManager));
2523
+ if (hasHusky || hasLefthook) {
2524
+ let linter;
2525
+ if (hasOxlint) linter = "oxlint";
2526
+ else if (hasBiome) linter = "biome";
2527
+ else if (hasVitePlus) linter = "vite-plus";
2528
+ if (hasHusky) await runAddonStep("husky", () => setupHusky(projectDir, linter));
2529
+ if (hasLefthook) await runAddonStep("lefthook", () => setupLefthook(projectDir));
3600
2530
  }
3601
- if (addons.includes("starlight")) await runSetup(() => setupStarlight(config));
3602
- if (addons.includes("fumadocs")) await runSetup(() => setupFumadocs(config));
3603
2531
  if (addons.includes("opentui")) await runSetup(() => setupTui(config));
3604
- if (addons.includes("wxt")) await runSetup(() => setupWxt(config));
3605
2532
  if (addons.includes("skills")) await runSetup(() => setupSkills(config));
3606
2533
  if (addons.includes("mcp")) await runSetup(() => setupMcp(config));
3607
- if (addons.includes("evlog")) await runSetup(() => setupEvlog(config));
3608
- if (testing.includes("vitest")) await runAddonStep("vitest", () => setupVitest(projectDir));
3609
- if (testing.includes("playwright")) await runAddonStep("playwright", () => setupPlaywright(projectDir));
3610
2534
  }
3611
2535
  async function setupBiome(projectDir) {
3612
2536
  await addPackageDependency({
@@ -3679,6 +2603,20 @@ async function setupTesting(projectDir, testing) {
3679
2603
  await updateScripts(projectDir, { "test:e2e": "playwright test" });
3680
2604
  }
3681
2605
  }
2606
+ /** Best-effort browser install after deps; never fails project creation. */
2607
+ async function installPlaywrightBrowsers(projectDir, packageManager) {
2608
+ if (shouldSkipExternalCommands()) return;
2609
+ const s = createSpinner();
2610
+ s.start("Installing Playwright browsers...");
2611
+ const args = getPackageExecutionArgs(packageManager, "playwright install");
2612
+ try {
2613
+ await $({ cwd: projectDir })`${args}`;
2614
+ s.stop(pc.green("Playwright browsers installed"));
2615
+ } catch (e) {
2616
+ s.stop(pc.yellow("Playwright browser install skipped (run playwright install later)"));
2617
+ cliLog.info(pc.dim(`Playwright browsers were not installed automatically: ${e instanceof Error ? e.message : String(e)}`));
2618
+ }
2619
+ }
3682
2620
  //#endregion
3683
2621
  //#region src/utils/config-processing.ts
3684
2622
  function processArrayOption(options) {
@@ -3700,7 +2638,7 @@ function processFlags(options, projectName) {
3700
2638
  if (options.database) config.database = options.database;
3701
2639
  if (options.orm) config.orm = options.orm;
3702
2640
  if (options.auth !== void 0) config.auth = options.auth;
3703
- if (options.payments !== void 0) config.payments = options.payments;
2641
+ if (options.payments !== void 0) config.payments = normalizePayments(options.payments);
3704
2642
  if (options.observability !== void 0) config.observability = normalizeObservability(options.observability);
3705
2643
  if (options.disableObservability) config.observability = [];
3706
2644
  if (options.communication !== void 0) config.communication = options.communication;
@@ -3761,6 +2699,7 @@ async function detectProjectConfig(projectDir) {
3761
2699
  frontend: kubojsConfig.frontend,
3762
2700
  addons: kubojsConfig.addons,
3763
2701
  examples: kubojsConfig.examples,
2702
+ testing: kubojsConfig.testing,
3764
2703
  auth: kubojsConfig.auth,
3765
2704
  payments: kubojsConfig.payments,
3766
2705
  observability: normalizeObservability(kubojsConfig.observability),
@@ -3808,9 +2747,8 @@ const ADD_PACKAGE_JSON_PATHS = [
3808
2747
  "apps/web/package.json",
3809
2748
  "apps/native/package.json",
3810
2749
  "apps/desktop/package.json",
3811
- "apps/fumadocs/package.json",
3812
2750
  "apps/docs/package.json",
3813
- "packages/api/package.json",
2751
+ "apps/api/package.json",
3814
2752
  "packages/db/package.json",
3815
2753
  "packages/auth/package.json",
3816
2754
  "packages/backend/package.json",
@@ -3820,17 +2758,21 @@ const ADD_PACKAGE_JSON_PATHS = [
3820
2758
  "packages/ui/package.json"
3821
2759
  ];
3822
2760
  const ADD_TEXT_FILE_PATHS = ["apps/web/vite.config.ts", "lefthook.yml"];
2761
+ const ADD_ENV_FILE_PATHS = [
2762
+ ".env",
2763
+ "apps/web/.env",
2764
+ "apps/server/.env",
2765
+ "apps/native/.env",
2766
+ "packages/backend/.env.local",
2767
+ "packages/infra/.env"
2768
+ ];
3823
2769
  const HOOK_ADDONS = ["husky", "lefthook"];
3824
2770
  const HOOK_LINTER_ADDONS = [
3825
2771
  "biome",
3826
2772
  "oxlint",
3827
2773
  "vite-plus"
3828
2774
  ];
3829
- const TASK_RUNNER_ADDONS = [
3830
- "turborepo",
3831
- "nx",
3832
- "vite-plus"
3833
- ];
2775
+ const TASK_RUNNER_ADDONS = ["turborepo", "vite-plus"];
3834
2776
  function mergeAddonOptions(existingAddonOptions, nextAddonOptions) {
3835
2777
  if (!existingAddonOptions && !nextAddonOptions) return;
3836
2778
  const mergedAddonOptions = { ...existingAddonOptions };
@@ -3868,11 +2810,8 @@ async function cleanupRemovedLinters(projectDir, removedAddons) {
3868
2810
  const packageJson = await fs.readJson(packageJsonPath);
3869
2811
  packageJson.devDependencies = packageJson.devDependencies ?? {};
3870
2812
  for (const removed of removedLinters) {
3871
- if (removed === "biome" || removed === "ultracite") {
3872
- delete packageJson.devDependencies["@biomejs/biome"];
3873
- delete packageJson.devDependencies.ultracite;
3874
- }
3875
- if (removed === "oxlint" || removed === "ultracite") {
2813
+ if (removed === "biome") delete packageJson.devDependencies["@biomejs/biome"];
2814
+ if (removed === "oxlint") {
3876
2815
  delete packageJson.devDependencies.oxlint;
3877
2816
  delete packageJson.devDependencies.oxfmt;
3878
2817
  }
@@ -3880,7 +2819,6 @@ async function cleanupRemovedLinters(projectDir, removedAddons) {
3880
2819
  if (typeof packageJson.scripts?.check === "string" && removedLinters.some((linter) => {
3881
2820
  if (linter === "biome") return packageJson.scripts.check.includes("biome");
3882
2821
  if (linter === "oxlint") return packageJson.scripts.check.includes("oxlint");
3883
- if (linter === "ultracite") return packageJson.scripts.check.includes("ultracite");
3884
2822
  return false;
3885
2823
  })) delete packageJson.scripts.check;
3886
2824
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
@@ -3936,7 +2874,7 @@ async function addHandlerInternal(input) {
3936
2874
  }));
3937
2875
  if (!isSilent()) {
3938
2876
  renderTitle();
3939
- intro(pc.magenta("Add addons to your kubojs project"));
2877
+ intro(cliColors.signal("Add addons to your kubojs project"));
3940
2878
  }
3941
2879
  const existingConfig = await detectProjectConfig(projectDir);
3942
2880
  if (!existingConfig) return Result.err(new CLIError({ message: `No kubojs project found in ${projectDir}. Make sure kubojs.jsonrc exists.` }));
@@ -3969,7 +2907,7 @@ async function addHandlerInternal(input) {
3969
2907
  if (selectedAddons.length === 0) {
3970
2908
  if (!isSilent()) {
3971
2909
  log.info(pc.dim("No addons selected."));
3972
- outro(pc.magenta("Nothing to add."));
2910
+ outro(cliColors.signal("Nothing to add."));
3973
2911
  }
3974
2912
  return Result.ok({
3975
2913
  success: true,
@@ -3980,10 +2918,11 @@ async function addHandlerInternal(input) {
3980
2918
  addonsToAdd = selectedAddons;
3981
2919
  }
3982
2920
  const { updatedAddons, removedAddons } = mergeAddonsExclusive(existingConfig.addons, addonsToAdd);
2921
+ const updatedTesting = existingConfig.testing ?? [];
3983
2922
  const addonsValidationResult = validateAddonsAgainstConfig(updatedAddons, existingConfig);
3984
2923
  if (addonsValidationResult.isErr()) return Result.err(new CLIError({ message: addonsValidationResult.error.message }));
3985
2924
  if (!isSilent()) {
3986
- log.info(pc.cyan(`Adding addons: ${addonsToAdd.join(", ")}`));
2925
+ log.info(cliColors.bright(`Adding addons: ${addonsToAdd.join(", ")}`));
3987
2926
  if (removedAddons.length > 0) log.info(pc.dim(`Replacing exclusive addons: ${removedAddons.join(", ")}`));
3988
2927
  }
3989
2928
  const mergedAddonOptions = mergeAddonOptions(existingConfig.addonOptions, input.addonOptions);
@@ -4003,6 +2942,7 @@ async function addHandlerInternal(input) {
4003
2942
  payments: existingConfig.payments,
4004
2943
  observability: existingConfig.observability,
4005
2944
  communication: existingConfig.communication ?? "none",
2945
+ testing: updatedTesting,
4006
2946
  git: false,
4007
2947
  packageManager: input.packageManager || existingConfig.packageManager,
4008
2948
  install: input.install ?? false,
@@ -4031,10 +2971,17 @@ async function addHandlerInternal(input) {
4031
2971
  vfs.writeFile(filePath, content);
4032
2972
  }
4033
2973
  }
2974
+ for (const filePath of ADD_ENV_FILE_PATHS) {
2975
+ const fullPath = path.join(projectDir, filePath);
2976
+ if (await fs.pathExists(fullPath)) {
2977
+ const content = await fs.readFile(fullPath, "utf-8");
2978
+ vfs.writeFile(filePath, content);
2979
+ }
2980
+ }
4034
2981
  await processAddonTemplates(vfs, EMBEDDED_TEMPLATES, config);
4035
2982
  processAddonsDeps(vfs, config);
2983
+ processEnvVariables(vfs, updatedConfig);
4036
2984
  if (addonsToAdd.includes("turborepo")) processTurboConfig(vfs, updatedConfig);
4037
- if (addonsToAdd.includes("nx")) processNxConfig(vfs, updatedConfig);
4038
2985
  if (addonsToAdd.includes("vite-plus")) processVitePlusConfig(vfs, updatedConfig);
4039
2986
  if (updatedAddons.some((addon) => TASK_RUNNER_ADDONS.includes(addon))) processPackageConfigs(vfs, updatedConfig);
4040
2987
  if (updatedAddons.includes("vite-plus")) updateViteConfigImportsForVitePlus(vfs);
@@ -4049,7 +2996,7 @@ async function addHandlerInternal(input) {
4049
2996
  if (!isSilent()) {
4050
2997
  log.success(pc.green("Dry run validation passed. No addon files were written."));
4051
2998
  log.info(pc.dim(`Planned addon files: ${vfs.getFileCount()}`));
4052
- outro(pc.magenta("Dry run complete."));
2999
+ outro(cliColors.signal("Dry run complete."));
4053
3000
  }
4054
3001
  return Result.ok({
4055
3002
  success: true,
@@ -4087,7 +3034,8 @@ async function addHandlerInternal(input) {
4087
3034
  if (testingSetupResult.isErr()) return Result.err(testingSetupResult.error);
4088
3035
  await updateKubojsConfig(projectDir, {
4089
3036
  addons: updatedAddons,
4090
- addonOptions: updatedConfig.addonOptions
3037
+ addonOptions: updatedConfig.addonOptions,
3038
+ testing: updatedTesting
4091
3039
  });
4092
3040
  if (input.install) {
4093
3041
  if (!isSilent()) log.info(pc.dim("Installing dependencies..."));
@@ -4099,7 +3047,7 @@ async function addHandlerInternal(input) {
4099
3047
  if (!isSilent()) {
4100
3048
  log.success(pc.green(`Successfully added: ${addonsToAdd.join(", ")}`));
4101
3049
  if (!input.install) log.info(pc.yellow(`Run '${config.packageManager === "npm" ? "npm install" : `${config.packageManager} install`}' to install new dependencies.`));
4102
- outro(pc.magenta("Addons added successfully!"));
3050
+ outro(cliColors.signal("Addons added successfully!"));
4103
3051
  }
4104
3052
  return Result.ok({
4105
3053
  success: true,
@@ -4112,7 +3060,7 @@ async function addHandlerInternal(input) {
4112
3060
  //#region src/prompts/api.ts
4113
3061
  async function getApiChoice(Api, frontend, backend, previousValue) {
4114
3062
  if (backend === "convex" || backend === "none") return "none";
4115
- const allowed = allowedApisForFrontends(frontend ?? []);
3063
+ const allowed = allowedApisForFrontends(frontend ?? [], backend);
4116
3064
  if (Api) {
4117
3065
  const compat = validateApiFrontendCompatibility(Api, frontend ?? []);
4118
3066
  if (compat.isErr()) throw compat.error;
@@ -4126,6 +3074,10 @@ async function getApiChoice(Api, frontend, backend, previousValue) {
4126
3074
  value: "orpc",
4127
3075
  label: "oRPC",
4128
3076
  hint: "End-to-end type-safe APIs that adhere to OpenAPI standards"
3077
+ } : a === "orval" ? {
3078
+ value: "orval",
3079
+ label: "Orval",
3080
+ hint: "OpenAPI REST APIs with generated Hono handlers and Fetch clients"
4129
3081
  } : {
4130
3082
  value: "none",
4131
3083
  label: "None",
@@ -4136,7 +3088,7 @@ async function getApiChoice(Api, frontend, backend, previousValue) {
4136
3088
  options: apiOptions,
4137
3089
  initialValue: preferValidInitial(apiOptions, previousValue, apiOptions[0].value)
4138
3090
  });
4139
- if (isCancel$1(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
3091
+ if (isCancel$2(apiType)) throw new UserCancelledError({ message: "Operation cancelled" });
4140
3092
  return apiType;
4141
3093
  }
4142
3094
  //#endregion
@@ -4188,7 +3140,7 @@ async function getAuthChoice(auth, backend, frontend = [], previousValue) {
4188
3140
  options,
4189
3141
  initialValue: preferValidInitial(options, previousValue, options.some((option) => option.value === DEFAULT_CONFIG.auth) ? DEFAULT_CONFIG.auth : "none")
4190
3142
  });
4191
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3143
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4192
3144
  return response;
4193
3145
  }
4194
3146
  //#endregion
@@ -4242,7 +3194,7 @@ async function getBackendFrameworkChoice(backendFramework, frontends, previousVa
4242
3194
  options: backendOptions,
4243
3195
  initialValue: preferValidInitial(backendOptions, previousValue, hasFullstackFrontend ? "self" : DEFAULT_CONFIG.backend)
4244
3196
  });
4245
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3197
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4246
3198
  return response;
4247
3199
  }
4248
3200
  //#endregion
@@ -4265,6 +3217,11 @@ async function getCommunicationChoice(communication, backend, previousValue) {
4265
3217
  value: "notifique",
4266
3218
  label: "Notifique",
4267
3219
  hint: "Omnichannel BR messaging API"
3220
+ },
3221
+ {
3222
+ value: "arara",
3223
+ label: "AraraHQ",
3224
+ hint: "WhatsApp messaging via the official Node SDK"
4268
3225
  }
4269
3226
  ];
4270
3227
  const response = await navigableSelect({
@@ -4272,7 +3229,7 @@ async function getCommunicationChoice(communication, backend, previousValue) {
4272
3229
  options,
4273
3230
  initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.communication)
4274
3231
  });
4275
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3232
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4276
3233
  return response;
4277
3234
  }
4278
3235
  //#endregion
@@ -4312,7 +3269,7 @@ async function getDatabaseChoice(database, backend, runtime, previousValue) {
4312
3269
  options: databaseOptions,
4313
3270
  initialValue: preferValidInitial(databaseOptions, previousValue, DEFAULT_CONFIG.database)
4314
3271
  });
4315
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3272
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4316
3273
  return response;
4317
3274
  }
4318
3275
  //#endregion
@@ -4411,7 +3368,7 @@ async function getDBSetupChoice(databaseType, dbSetup, _orm, backend, runtime, p
4411
3368
  options,
4412
3369
  initialValue: preferValidInitial(options, previousValue, "none")
4413
3370
  });
4414
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3371
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4415
3372
  return response;
4416
3373
  }
4417
3374
  //#endregion
@@ -4438,7 +3395,7 @@ async function getExamplesChoice(examples, database, frontends, backend, api, pr
4438
3395
  required: false,
4439
3396
  initialValues: (previousValue ?? DEFAULT_CONFIG.examples)?.filter((ex) => options.some((o) => o.value === ex))
4440
3397
  });
4441
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3398
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4442
3399
  return response;
4443
3400
  }
4444
3401
  //#endregion
@@ -4474,7 +3431,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4474
3431
  initialValues: previousValue ? [...previousWeb ? ["web"] : [], ...previousNative ? ["native"] : []] : ["web"]
4475
3432
  });
4476
3433
  if (isGoBack(frontendTypes)) return GO_BACK_SYMBOL;
4477
- if (isCancel$1(frontendTypes)) throw new UserCancelledError({ message: "Operation cancelled" });
3434
+ if (isCancel$2(frontendTypes)) throw new UserCancelledError({ message: "Operation cancelled" });
4478
3435
  setIsFirstPrompt(false);
4479
3436
  const result = [];
4480
3437
  let shouldRestart = false;
@@ -4527,7 +3484,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4527
3484
  initialValue: preferValidInitial(webOptions, previousWeb, DEFAULT_CONFIG.frontend[0])
4528
3485
  });
4529
3486
  if (isGoBack(webFramework)) shouldRestart = true;
4530
- else if (isCancel$1(webFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
3487
+ else if (isCancel$2(webFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
4531
3488
  else result.push(webFramework);
4532
3489
  }
4533
3490
  if (shouldRestart) {
@@ -4561,7 +3518,7 @@ async function getFrontendChoice(frontendOptions, backend, auth, previousValue)
4561
3518
  setIsFirstPrompt(wasFirstPrompt);
4562
3519
  continue;
4563
3520
  }
4564
- else if (isCancel$1(nativeFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
3521
+ else if (isCancel$2(nativeFramework)) throw new UserCancelledError({ message: "Operation cancelled" });
4565
3522
  else result.push(nativeFramework);
4566
3523
  }
4567
3524
  if (shouldRestart) {
@@ -4579,7 +3536,7 @@ async function getGitChoice(git, previousValue) {
4579
3536
  message: "Initialize git repository?",
4580
3537
  initialValue: previousValue ?? DEFAULT_CONFIG.git
4581
3538
  });
4582
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3539
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4583
3540
  return response;
4584
3541
  }
4585
3542
  //#endregion
@@ -4590,7 +3547,7 @@ async function getinstallChoice(install, previousValue) {
4590
3547
  message: "Install dependencies?",
4591
3548
  initialValue: previousValue ?? DEFAULT_CONFIG.install
4592
3549
  });
4593
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3550
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4594
3551
  return response;
4595
3552
  }
4596
3553
  //#endregion
@@ -4612,7 +3569,7 @@ async function getObservabilityChoice(observability, previousValue) {
4612
3569
  required: false,
4613
3570
  initialValues: previousValue ?? DEFAULT_CONFIG.observability
4614
3571
  });
4615
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3572
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4616
3573
  return response;
4617
3574
  }
4618
3575
  //#endregion
@@ -4730,7 +3687,7 @@ function validateBackendNoneConstraints(config, providedFlags) {
4730
3687
  if (has("orm") && config.orm !== "none") return validationErr("Backend 'none' requires '--orm none'. Please remove the --orm flag or set it to 'none'.");
4731
3688
  if (has("api") && config.api !== "none") return validationErr("Backend 'none' requires '--api none'. Please remove the --api flag or set it to 'none'.");
4732
3689
  if (has("auth") && config.auth !== "none") return validationErr("Backend 'none' requires '--auth none'. Please remove the --auth flag or set it to 'none'.");
4733
- if (has("payments") && config.payments !== "none") return validationErr("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
3690
+ if (has("payments") && (config.payments?.length ?? 0) > 0) return validationErr("Backend 'none' requires '--payments none'. Please remove the --payments flag or set it to 'none'.");
4734
3691
  if (has("communication") && config.communication !== "none") return validationErr("Backend 'none' requires '--communication none'. Please remove the --communication flag or set it to 'none'.");
4735
3692
  if (has("dbSetup") && config.dbSetup !== "none") return validationErr("Backend 'none' requires '--db-setup none'. Please remove the --db-setup flag or set it to 'none'.");
4736
3693
  if (has("serverDeploy") && config.serverDeploy !== "none") return validationErr("Backend 'none' requires '--server-deploy none'. Please remove the --server-deploy flag or set it to 'none'.");
@@ -4779,6 +3736,8 @@ function validateFrontendConstraints(config, providedFlags) {
4779
3736
  return Result.ok(void 0);
4780
3737
  }
4781
3738
  function validateApiConstraints(config, options) {
3739
+ if (!isApiCompatibleWithBackend(config.api, config.backend)) return validationErr("Orval API requires the Hono backend in this version. Please use '--backend hono' or choose tRPC/oRPC.");
3740
+ if (config.api === "orval" && options.examples?.includes("todo")) return validationErr("The Orval API layer does not support the generated todo example yet. Please remove 'todo' from --examples or choose tRPC/oRPC.");
4782
3741
  if (config.api === "none") {
4783
3742
  if (options.examples?.includes("todo") && options.backend !== "convex" && options.backend !== "none") return validationErr("Cannot use '--examples todo' when '--api' is set to 'none'. The todo example requires an API layer. Please remove 'todo' from --examples or choose an API type.");
4784
3743
  }
@@ -4811,6 +3770,7 @@ function validateFullConfig(config, providedFlags, options) {
4811
3770
  yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
4812
3771
  yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend ?? [], config.orm, config.database);
4813
3772
  yield* validateCommunicationCompatibility(config.communication, config.backend);
3773
+ yield* validateAraraRuntimeCompatibility(config.communication, config.backend, config.runtime, config.serverDeploy);
4814
3774
  return Result.ok(void 0);
4815
3775
  });
4816
3776
  }
@@ -4822,6 +3782,7 @@ function validateConfigForProgrammaticUse(config) {
4822
3782
  yield* validateTestingAgainstFrontends(config.testing ?? [], config.frontend ?? []);
4823
3783
  yield* validatePaymentsCompatibility(config.payments, config.auth, config.backend, config.frontend, config.orm, config.database);
4824
3784
  yield* validateCommunicationCompatibility(config.communication, config.backend);
3785
+ yield* validateAraraRuntimeCompatibility(config.communication, config.backend, config.runtime, config.serverDeploy);
4825
3786
  if (config.addons && config.addons.length > 0) yield* validateAddonsAgainstFrontends(config.addons, config.frontend, config.auth, config.backend, config.runtime);
4826
3787
  yield* validateExamplesCompatibility(config.examples ?? [], config.backend, config.database, config.frontend ?? [], config.api);
4827
3788
  return Result.ok(void 0);
@@ -4860,7 +3821,7 @@ async function getORMChoice(orm, hasDatabase, database, backend, runtime, previo
4860
3821
  options,
4861
3822
  initialValue: preferValidInitial(options, previousValue, database === "mongodb" ? "prisma" : runtime === "workers" ? "drizzle" : DEFAULT_CONFIG.orm)
4862
3823
  });
4863
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3824
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4864
3825
  return response;
4865
3826
  }
4866
3827
  //#endregion
@@ -4889,47 +3850,48 @@ async function getPackageManagerChoice(packageManager, previousValue) {
4889
3850
  ],
4890
3851
  initialValue: previousValue ?? detectedPackageManager
4891
3852
  });
4892
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3853
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4893
3854
  return response;
4894
3855
  }
4895
3856
  //#endregion
4896
3857
  //#region src/prompts/payments.ts
4897
3858
  async function getPaymentsChoice(payments, auth, backend, frontends = [], previousValue) {
4898
3859
  if (payments !== void 0) return payments;
4899
- if (backend === "none") return "none";
3860
+ if (backend === "none") return [];
4900
3861
  const options = [{
4901
- value: "none",
4902
- label: "None",
4903
- hint: "No payments integration"
4904
- }, {
4905
3862
  value: "abacatepay",
4906
3863
  label: "AbacatePay",
4907
3864
  hint: "Hosted checkout with webhook reconciliation"
3865
+ }, {
3866
+ value: "stripe",
3867
+ label: "Stripe",
3868
+ hint: "Embedded Checkout with a fulfillment webhook"
4908
3869
  }];
4909
- const abacatePayDisabledReason = !frontends.some((frontend) => [
4910
- "tanstack-router",
4911
- "react-router",
4912
- "tanstack-start",
4913
- "next",
4914
- "nuxt",
4915
- "svelte",
4916
- "solid",
4917
- "astro"
4918
- ].includes(frontend)) ? "Requires a web frontend" : backend === "convex" ? "Not supported with Convex" : void 0;
4919
- const selectableOptions = options.map((option) => {
4920
- if (option.value !== "abacatepay") return option;
4921
- return {
4922
- ...option,
4923
- hint: abacatePayDisabledReason ?? option.hint,
4924
- disabled: abacatePayDisabledReason !== void 0
4925
- };
4926
- });
4927
- const response = await navigableSelect({
4928
- message: "Select payments provider",
4929
- options: selectableOptions,
4930
- initialValue: preferValidInitial(selectableOptions, previousValue, DEFAULT_CONFIG.payments)
3870
+ const getDisabledReason = (provider) => {
3871
+ const issue = getPaymentCompatibilityIssue({
3872
+ provider,
3873
+ backend,
3874
+ frontends
3875
+ });
3876
+ if (issue === "requires-web-frontend") return "Requires a web frontend";
3877
+ if (issue === "convex-unsupported") return "Not supported with Convex";
3878
+ if (issue === "native-only-unsupported") return "Not supported with native-only frontends";
3879
+ if (issue === "sql-database-required") return "Requires a SQL database with Prisma or Drizzle";
3880
+ };
3881
+ const response = await navigableMultiselect({
3882
+ message: "Select payments providers",
3883
+ options: options.map((option) => {
3884
+ const disabledReason = getDisabledReason(option.value);
3885
+ return {
3886
+ ...option,
3887
+ hint: disabledReason ?? option.hint,
3888
+ disabled: disabledReason !== void 0
3889
+ };
3890
+ }),
3891
+ required: false,
3892
+ initialValues: previousValue ?? DEFAULT_CONFIG.payments
4931
3893
  });
4932
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3894
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4933
3895
  return response;
4934
3896
  }
4935
3897
  //#endregion
@@ -4956,7 +3918,7 @@ async function getRuntimeChoice(runtime, backend, previousValue) {
4956
3918
  options: runtimeOptions,
4957
3919
  initialValue: preferValidInitial(runtimeOptions, previousValue, DEFAULT_CONFIG.runtime)
4958
3920
  });
4959
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3921
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4960
3922
  return response;
4961
3923
  }
4962
3924
  //#endregion
@@ -5014,7 +3976,7 @@ async function getServerDeploymentChoice(deployment, runtime, backend, _webDeplo
5014
3976
  options,
5015
3977
  initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.serverDeploy)
5016
3978
  });
5017
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
3979
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
5018
3980
  return response;
5019
3981
  }
5020
3982
  //#endregion
@@ -5037,7 +3999,7 @@ async function getTestingChoice(testing, frontends, previousValue) {
5037
3999
  required: false,
5038
4000
  initialValues: (previousValue ?? DEFAULT_CONFIG.testing)?.filter((t) => options.some((o) => o.value === t))
5039
4001
  });
5040
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4002
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
5041
4003
  return response;
5042
4004
  }
5043
4005
  //#endregion
@@ -5090,7 +4052,7 @@ async function getDeploymentChoice(deployment, _runtime, backend, frontend = [],
5090
4052
  options,
5091
4053
  initialValue: preferValidInitial(options, previousValue, DEFAULT_CONFIG.webDeploy)
5092
4054
  });
5093
- if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled" });
4055
+ if (isCancel$2(response)) throw new UserCancelledError({ message: "Operation cancelled" });
5094
4056
  return response;
5095
4057
  }
5096
4058
  //#endregion
@@ -5174,6 +4136,32 @@ async function gatherConfig(flags, projectName, projectDir, relativePath) {
5174
4136
  }
5175
4137
  //#endregion
5176
4138
  //#region src/prompts/project-name.ts
4139
+ const S_BAR = "│";
4140
+ const S_BAR_END = "└";
4141
+ const S_STEP_ACTIVE = "◆";
4142
+ function kuboText(options) {
4143
+ return new TextPrompt({
4144
+ validate: options.validate,
4145
+ placeholder: options.placeholder,
4146
+ defaultValue: options.defaultValue,
4147
+ initialValue: options.initialValue,
4148
+ render() {
4149
+ const title = `${cliColors.signal(S_BAR)}\n${cliColors.bright(S_STEP_ACTIVE)} ${options.message}\n`;
4150
+ const placeholder = options.placeholder ? pc.dim(options.placeholder) : pc.dim("_");
4151
+ const input = this.userInput ? this.userInputWithCursor : placeholder;
4152
+ const value = this.value ?? "";
4153
+ switch (this.state) {
4154
+ case "error": {
4155
+ const error = this.error ? ` ${pc.yellow(this.error)}` : "";
4156
+ return `${title.trim()}\n${cliColors.signal(S_BAR)} ${input}\n${pc.yellow(S_BAR_END)}${error}\n`;
4157
+ }
4158
+ case "submit": return `${title}${pc.gray(S_BAR)}${value ? ` ${pc.dim(value)}` : ""}`;
4159
+ case "cancel": return `${title}${pc.gray(S_BAR)}${value ? ` ${pc.strikethrough(pc.dim(value))}` : ""}${value.trim() ? `\n${pc.gray(S_BAR)}` : ""}`;
4160
+ default: return `${title}${cliColors.signal(S_BAR)} ${input}\n${cliColors.signal(S_BAR_END)}\n`;
4161
+ }
4162
+ }
4163
+ }).prompt();
4164
+ }
5177
4165
  function isPathWithinCwd$1(targetPath) {
5178
4166
  const resolved = path.resolve(targetPath);
5179
4167
  const rel = path.relative(process.cwd(), resolved);
@@ -5201,7 +4189,7 @@ async function getProjectName(initialName) {
5201
4189
  counter++;
5202
4190
  }
5203
4191
  while (!isValid) {
5204
- const response = await text({
4192
+ const response = await kuboText({
5205
4193
  message: "Enter your project name or path (relative to current directory)",
5206
4194
  placeholder: defaultName,
5207
4195
  initialValue: initialName,
@@ -5215,7 +4203,7 @@ async function getProjectName(initialName) {
5215
4203
  }
5216
4204
  }
5217
4205
  });
5218
- if (isCancel(response)) throw new UserCancelledError({ message: "Operation cancelled." });
4206
+ if (isCancel$1(response)) throw new UserCancelledError({ message: "Operation cancelled." });
5219
4207
  projectPath = response || defaultName;
5220
4208
  isValid = true;
5221
4209
  }
@@ -5256,48 +4244,48 @@ async function trackProjectCreation(config, disableAnalytics = false) {
5256
4244
  //#region src/utils/display-config.ts
5257
4245
  function displayConfig(config) {
5258
4246
  const configDisplay = [];
5259
- if (config.projectName) configDisplay.push(`${pc.blue("Project Name:")} ${config.projectName}`);
4247
+ if (config.projectName) configDisplay.push(`${cliColors.signal("Project Name:")} ${config.projectName}`);
5260
4248
  if (config.frontend !== void 0) {
5261
4249
  const frontend = Array.isArray(config.frontend) ? config.frontend : [config.frontend];
5262
4250
  const frontendText = frontend.length > 0 && frontend[0] !== void 0 ? frontend.join(", ") : "none";
5263
- configDisplay.push(`${pc.blue("Frontend:")} ${frontendText}`);
5264
- }
5265
- if (config.backend !== void 0) configDisplay.push(`${pc.blue("Backend:")} ${String(config.backend)}`);
5266
- if (config.runtime !== void 0) configDisplay.push(`${pc.blue("Runtime:")} ${String(config.runtime)}`);
5267
- if (config.api !== void 0) configDisplay.push(`${pc.blue("API:")} ${String(config.api)}`);
5268
- if (config.database !== void 0) configDisplay.push(`${pc.blue("Database:")} ${String(config.database)}`);
5269
- if (config.orm !== void 0) configDisplay.push(`${pc.blue("ORM:")} ${String(config.orm)}`);
5270
- if (config.auth !== void 0) configDisplay.push(`${pc.blue("Auth:")} ${String(config.auth)}`);
5271
- if (config.payments !== void 0) configDisplay.push(`${pc.blue("Payments:")} ${String(config.payments)}`);
5272
- if (config.communication !== void 0) configDisplay.push(`${pc.blue("Communication:")} ${String(config.communication)}`);
5273
- if (config.observability !== void 0) configDisplay.push(`${pc.blue("Observability:")} ${config.observability.join(", ") || "none"}`);
4251
+ configDisplay.push(`${cliColors.signal("Frontend:")} ${frontendText}`);
4252
+ }
4253
+ if (config.backend !== void 0) configDisplay.push(`${cliColors.signal("Backend:")} ${String(config.backend)}`);
4254
+ if (config.runtime !== void 0) configDisplay.push(`${cliColors.signal("Runtime:")} ${String(config.runtime)}`);
4255
+ if (config.api !== void 0) configDisplay.push(`${cliColors.signal("API:")} ${String(config.api)}`);
4256
+ if (config.database !== void 0) configDisplay.push(`${cliColors.signal("Database:")} ${String(config.database)}`);
4257
+ if (config.orm !== void 0) configDisplay.push(`${cliColors.signal("ORM:")} ${String(config.orm)}`);
4258
+ if (config.auth !== void 0) configDisplay.push(`${cliColors.signal("Auth:")} ${String(config.auth)}`);
4259
+ if (config.payments !== void 0) configDisplay.push(`${cliColors.signal("Payments:")} ${config.payments.join(", ") || "none"}`);
4260
+ if (config.communication !== void 0) configDisplay.push(`${cliColors.signal("Communication:")} ${String(config.communication)}`);
4261
+ if (config.observability !== void 0) configDisplay.push(`${cliColors.signal("Observability:")} ${config.observability.join(", ") || "none"}`);
5274
4262
  if (config.addons !== void 0) {
5275
4263
  const addons = Array.isArray(config.addons) ? config.addons : [config.addons];
5276
4264
  const addonsText = addons.length > 0 && addons[0] !== void 0 ? addons.join(", ") : "none";
5277
- configDisplay.push(`${pc.blue("Addons:")} ${addonsText}`);
4265
+ configDisplay.push(`${cliColors.signal("Addons:")} ${addonsText}`);
5278
4266
  }
5279
4267
  if (config.examples !== void 0) {
5280
4268
  const examples = Array.isArray(config.examples) ? config.examples : [config.examples];
5281
4269
  const examplesText = examples.length > 0 && examples[0] !== void 0 ? examples.join(", ") : "none";
5282
- configDisplay.push(`${pc.blue("Examples:")} ${examplesText}`);
4270
+ configDisplay.push(`${cliColors.signal("Examples:")} ${examplesText}`);
5283
4271
  }
5284
4272
  if (config.testing !== void 0) {
5285
4273
  const testing = Array.isArray(config.testing) ? config.testing : [config.testing];
5286
4274
  const testingText = testing.length > 0 && testing[0] !== void 0 ? testing.join(", ") : "none";
5287
- configDisplay.push(`${pc.blue("Testing:")} ${testingText}`);
4275
+ configDisplay.push(`${cliColors.signal("Testing:")} ${testingText}`);
5288
4276
  }
5289
4277
  if (config.git !== void 0) {
5290
4278
  const gitText = typeof config.git === "boolean" ? config.git ? "Yes" : "No" : String(config.git);
5291
- configDisplay.push(`${pc.blue("Git Init:")} ${gitText}`);
4279
+ configDisplay.push(`${cliColors.signal("Git Init:")} ${gitText}`);
5292
4280
  }
5293
- if (config.packageManager !== void 0) configDisplay.push(`${pc.blue("Package Manager:")} ${String(config.packageManager)}`);
4281
+ if (config.packageManager !== void 0) configDisplay.push(`${cliColors.signal("Package Manager:")} ${String(config.packageManager)}`);
5294
4282
  if (config.install !== void 0) {
5295
4283
  const installText = typeof config.install === "boolean" ? config.install ? "Yes" : "No" : String(config.install);
5296
- configDisplay.push(`${pc.blue("Install Dependencies:")} ${installText}`);
4284
+ configDisplay.push(`${cliColors.signal("Install Dependencies:")} ${installText}`);
5297
4285
  }
5298
- if (config.dbSetup !== void 0) configDisplay.push(`${pc.blue("Database Setup:")} ${String(config.dbSetup)}`);
5299
- if (config.webDeploy !== void 0) configDisplay.push(`${pc.blue("Web Deployment:")} ${String(config.webDeploy)}`);
5300
- if (config.serverDeploy !== void 0) configDisplay.push(`${pc.blue("Server Deployment:")} ${String(config.serverDeploy)}`);
4286
+ if (config.dbSetup !== void 0) configDisplay.push(`${cliColors.signal("Database Setup:")} ${String(config.dbSetup)}`);
4287
+ if (config.webDeploy !== void 0) configDisplay.push(`${cliColors.signal("Web Deployment:")} ${String(config.webDeploy)}`);
4288
+ if (config.serverDeploy !== void 0) configDisplay.push(`${cliColors.signal("Server Deployment:")} ${String(config.serverDeploy)}`);
5301
4289
  if (configDisplay.length === 0) return pc.yellow("No configuration selected.");
5302
4290
  return configDisplay.join("\n");
5303
4291
  }
@@ -5427,7 +4415,7 @@ const TEMPLATE_PRESETS = {
5427
4415
  frontend: ["react-router"],
5428
4416
  api: "orpc",
5429
4417
  auth: "better-auth",
5430
- payments: "none",
4418
+ payments: [],
5431
4419
  observability: ["getmonitor"],
5432
4420
  communication: "none",
5433
4421
  addons: ["turborepo"],
@@ -5444,7 +4432,7 @@ const TEMPLATE_PRESETS = {
5444
4432
  frontend: ["tanstack-router"],
5445
4433
  api: "trpc",
5446
4434
  auth: "better-auth",
5447
- payments: "none",
4435
+ payments: [],
5448
4436
  observability: ["getmonitor"],
5449
4437
  communication: "none",
5450
4438
  addons: ["turborepo"],
@@ -5461,7 +4449,7 @@ const TEMPLATE_PRESETS = {
5461
4449
  frontend: ["next"],
5462
4450
  api: "trpc",
5463
4451
  auth: "better-auth",
5464
- payments: "none",
4452
+ payments: [],
5465
4453
  observability: ["getmonitor"],
5466
4454
  communication: "none",
5467
4455
  addons: ["biome", "turborepo"],
@@ -5478,7 +4466,7 @@ const TEMPLATE_PRESETS = {
5478
4466
  frontend: ["native-uniwind"],
5479
4467
  api: "none",
5480
4468
  auth: "none",
5481
- payments: "none",
4469
+ payments: [],
5482
4470
  observability: [],
5483
4471
  communication: "none",
5484
4472
  addons: ["none"],
@@ -5614,7 +4602,7 @@ function shouldSkipFile(fileName) {
5614
4602
  }
5615
4603
  function shouldUseBiomeFormatter(addons) {
5616
4604
  if (!addons) return false;
5617
- return addons.includes("biome") || addons.includes("ultracite");
4605
+ return addons.includes("biome");
5618
4606
  }
5619
4607
  async function formatCode(filePath, content) {
5620
4608
  const result = await Result.tryPromise({
@@ -5670,7 +4658,7 @@ async function formatProjectWithBiome(projectDir) {
5670
4658
  }
5671
4659
  /**
5672
4660
  * Post-scaffold format.
5673
- * - biome / ultracite → Biome `check --write` (not oxfmt)
4661
+ * - biome → Biome `check --write` (not oxfmt)
5674
4662
  * - oxlint / none → oxfmt
5675
4663
  * Always skips kubojs.jsonrc, lockfiles, and binary extensions (oxfmt path).
5676
4664
  */
@@ -6118,7 +5106,7 @@ async function writeEnvFile$2(projectDir, backend, config) {
6118
5106
  }
6119
5107
  async function setupWithNeonDb(projectDir, packageManager, backend) {
6120
5108
  const s = createSpinner();
6121
- s.start("Creating Neon database using get-db...");
5109
+ s.start("Creating Neon database using neon-new...");
6122
5110
  const targetApp = backend === "self" ? "apps/web" : "apps/server";
6123
5111
  const targetDir = path.join(projectDir, targetApp);
6124
5112
  const ensureDirResult = await Result.tryPromise({
@@ -6133,17 +5121,17 @@ async function setupWithNeonDb(projectDir, packageManager, backend) {
6133
5121
  s.stop(pc.red("Failed to create directory"));
6134
5122
  return ensureDirResult;
6135
5123
  }
6136
- const packageArgs = getPackageExecutionArgs(packageManager, `get-db@latest --yes --ref "sbA3tIe"`);
5124
+ const packageArgs = getPackageExecutionArgs(packageManager, `neon-new@latest --yes --ref "sbA3tIe"`);
6137
5125
  return Result.tryPromise({
6138
5126
  try: async () => {
6139
5127
  await $({ cwd: targetDir })`${packageArgs}`;
6140
5128
  s.stop(pc.green("Neon database created successfully!"));
6141
5129
  },
6142
5130
  catch: (e) => {
6143
- s.stop(pc.red("Failed to create database with get-db"));
5131
+ s.stop(pc.red("Failed to create database with neon-new"));
6144
5132
  return new DatabaseSetupError({
6145
5133
  provider: "neon",
6146
- message: `Failed to create database with get-db: ${e instanceof Error ? e.message : String(e)}`,
5134
+ message: `Failed to create database with neon-new: ${e instanceof Error ? e.message : String(e)}`,
6147
5135
  cause: e
6148
5136
  });
6149
5137
  }
@@ -6209,7 +5197,7 @@ async function setupNeonPostgres(config, cliInput) {
6209
5197
  const promptedSetupMethod = await select({
6210
5198
  message: "Choose your Neon setup method:",
6211
5199
  options: [{
6212
- label: "Quick setup with get-db",
5200
+ label: "Quick setup with neon-new",
6213
5201
  value: "neondb",
6214
5202
  hint: "fastest, no auth required"
6215
5203
  }, {
@@ -7141,6 +6129,10 @@ async function getDockerStatus(database) {
7141
6129
  }
7142
6130
  //#endregion
7143
6131
  //#region src/helpers/core/post-installation.ts
6132
+ const pc$1 = {
6133
+ ...pc,
6134
+ cyan: cliColors.signal
6135
+ };
7144
6136
  function getDesktopStaticBuildNote(frontend) {
7145
6137
  const staticBuildFrontends = new Map([
7146
6138
  ["tanstack-start", "TanStack Start"],
@@ -7151,7 +6143,7 @@ function getDesktopStaticBuildNote(frontend) {
7151
6143
  ]);
7152
6144
  const staticBuildFrontend = frontend.find((value) => staticBuildFrontends.has(value));
7153
6145
  if (!staticBuildFrontend) return "";
7154
- return `${pc.yellow("NOTE:")} Desktop builds package static web assets.\n ${staticBuildFrontends.get(staticBuildFrontend)} needs a static/export web build before desktop packaging will work.`;
6146
+ return `${pc$1.yellow("NOTE:")} Desktop builds package static web assets.\n ${staticBuildFrontends.get(staticBuildFrontend)} needs a static/export web build before desktop packaging will work.`;
7155
6147
  }
7156
6148
  async function displayPostInstallInstructions(config) {
7157
6149
  const { api, database, relativePath, packageManager, depsInstalled, orm, addons, testing, runtime, frontend, backend, dbSetup, webDeploy, serverDeploy, observability, communication } = config;
@@ -7173,50 +6165,52 @@ async function displayPostInstallInstructions(config) {
7173
6165
  const lintingInstructions = hasGitHooksOrLinting ? getLintingInstructions(runCmd) : "";
7174
6166
  const nativeInstructions = (frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles")) && backend !== "none" ? getNativeInstructions(isConvex, isBackendSelf, frontend || [], runCmd) : "";
7175
6167
  const pwaInstructions = addons?.includes("pwa") && frontend?.includes("react-router") ? getPwaInstructions() : "";
7176
- const starlightInstructions = addons?.includes("starlight") ? getStarlightInstructions(runCmd) : "";
7177
- const playwrightInstructions = testing?.includes("playwright") ? getPlaywrightInstructions(runCmd) : "";
6168
+ const playwrightInstructions = testing?.includes("playwright") ? getPlaywrightInstructions(runCmd, packageManager) : "";
7178
6169
  const clerkInstructions = config.auth === "clerk" ? getClerkInstructions(frontend || [], backend, api) : "";
7179
6170
  const alchemyDeployInstructions = getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend);
7180
6171
  const getMonitorInstructions = observability.includes("getmonitor") ? getGetMonitorInstructions() : "";
7181
6172
  const resendInstructions = communication === "resend" ? getResendInstructions() : "";
7182
6173
  const notifiqueInstructions = communication === "notifique" ? getNotifiqueInstructions() : "";
6174
+ const araraInstructions = communication === "arara" ? getAraraInstructions(backend) : "";
7183
6175
  const hasWeb = frontend?.some((f) => types_exports.desktopWebFrontends.includes(f));
7184
6176
  const hasNative = frontend?.includes("native-bare") || frontend?.includes("native-uniwind") || frontend?.includes("native-unistyles");
7185
6177
  const webPort = (0, types_exports.getWebPort)(frontend ?? []);
7186
6178
  const betterAuthConvexInstructions = isConvex && config.auth === "better-auth" ? getBetterAuthConvexInstructions(hasWeb ?? false, webPort, packageManager, runCmd) : "";
7187
6179
  const bunWebNativeWarning = packageManager === "bun" && hasNative && hasWeb ? getBunWebNativeWarning() : "";
7188
6180
  const noOrmWarning = !isConvex && database !== "none" && orm === "none" ? getNoOrmWarning() : "";
7189
- let output = `${pc.bold("Next steps")}\n${pc.cyan("1.")} ${cdCmd}\n`;
6181
+ let output = `${pc$1.bold("Next steps")}\n${pc$1.cyan("1.")} ${cdCmd}\n`;
7190
6182
  let stepCounter = 2;
7191
- if (!depsInstalled) output += `${pc.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
7192
- if (database === "sqlite" && dbSetup === "turso") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} db:local\n${pc.dim(" (optional - starts local Turso/libsql replica; requires Turso CLI)")}\n`;
6183
+ if (!depsInstalled) output += `${pc$1.cyan(`${stepCounter++}.`)} ${packageManager} install\n`;
6184
+ if (database === "sqlite" && dbSetup === "turso") output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} db:local\n${pc$1.dim(" (optional - starts local Turso/libsql replica; requires Turso CLI)")}\n`;
7193
6185
  if (isConvex) {
7194
- output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc.dim(" (this will guide you through Convex project setup)")}\n`;
7195
- output += `${pc.cyan(`${stepCounter++}.`)} Copy environment variables from\n${pc.white(" packages/backend/.env.local")} to ${pc.white("apps/*/.env")}\n`;
7196
- output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`;
7197
- } else if (isBackendSelf) output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
6186
+ output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} dev:setup\n${pc$1.dim(" (this will guide you through Convex project setup)")}\n`;
6187
+ output += `${pc$1.cyan(`${stepCounter++}.`)} Copy environment variables from\n${pc$1.white(" packages/backend/.env.local")} to ${pc$1.white("apps/*/.env")}\n`;
6188
+ output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} dev\n\n`;
6189
+ } else if (isBackendSelf) output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
7198
6190
  else {
7199
- if (runtime !== "workers") output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
6191
+ if (runtime !== "workers") output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
7200
6192
  if (runtime === "workers") {
7201
- if (dbSetup === "d1") output += `${pc.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
7202
- output += `${pc.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
6193
+ if (dbSetup === "d1") output += `${pc$1.yellow("IMPORTANT:")} Complete D1 database setup first\n (see Database commands below)\n`;
6194
+ output += `${pc$1.cyan(`${stepCounter++}.`)} ${runCmd} dev\n`;
7203
6195
  }
7204
6196
  }
7205
6197
  const hasStandaloneBackend = backend !== "none";
7206
- if (hasWeb || hasStandaloneBackend || addons?.includes("starlight") || addons?.includes("fumadocs")) {
7207
- output += `${pc.bold("Your project will be available at:")}\n`;
7208
- if (hasWeb) output += `${pc.cyan("•")} Frontend: http://localhost:${webPort}\n`;
7209
- else if (!hasNative && !addons?.includes("starlight")) output += `${pc.yellow("NOTE:")} You are creating a backend-only app\n (no frontend selected)\n`;
6198
+ if (hasWeb || hasStandaloneBackend) {
6199
+ output += `${pc$1.bold("Your project will be available at:")}\n`;
6200
+ if (hasWeb) output += `${pc$1.cyan("•")} Frontend: http://localhost:${webPort}\n`;
6201
+ else if (!hasNative) output += `${pc$1.yellow("NOTE:")} You are creating a backend-only app\n (no frontend selected)\n`;
7210
6202
  if (!isConvex && !isBackendSelf && hasStandaloneBackend) {
7211
- output += `${pc.cyan("•")} Backend API: http://localhost:3000\n`;
7212
- if (api === "orpc") output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api-reference\n`;
6203
+ output += `${pc$1.cyan("•")} Backend API: http://localhost:3000\n`;
6204
+ if (api === "orpc") output += `${pc$1.cyan("•")} OpenAPI (Scalar UI): http://localhost:3000/api-reference\n`;
6205
+ if (api === "orval") {
6206
+ output += `${pc$1.cyan("•")} OpenAPI contract: apps/api/openapi.yaml\n`;
6207
+ output += `${pc$1.cyan("•")} Regenerate API code: bun run api:generate\n`;
6208
+ }
7213
6209
  }
7214
6210
  if (isBackendSelf && api === "orpc") {
7215
6211
  const rpcPath = frontend?.includes("next") || frontend?.includes("tanstack-start") ? "/api/rpc" : "/rpc";
7216
- output += `${pc.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}${rpcPath}/api-reference\n`;
6212
+ output += `${pc$1.cyan("•")} OpenAPI (Scalar UI): http://localhost:${webPort}${rpcPath}/api-reference\n`;
7217
6213
  }
7218
- if (addons?.includes("starlight")) output += `${pc.cyan("•")} Docs: http://localhost:4321\n`;
7219
- if (addons?.includes("fumadocs")) output += `${pc.cyan("•")} Fumadocs: http://localhost:4000\n`;
7220
6214
  }
7221
6215
  if (nativeInstructions) output += `\n${nativeInstructions.trim()}\n`;
7222
6216
  if (databaseInstructions) output += `\n${databaseInstructions.trim()}\n`;
@@ -7227,18 +6221,18 @@ async function displayPostInstallInstructions(config) {
7227
6221
  if (vitePlusNativeHooksInstructions) output += `\n${vitePlusNativeHooksInstructions.trim()}\n`;
7228
6222
  if (lintingInstructions) output += `\n${lintingInstructions.trim()}\n`;
7229
6223
  if (pwaInstructions) output += `\n${pwaInstructions.trim()}\n`;
7230
- if (starlightInstructions) output += `\n${starlightInstructions.trim()}\n`;
7231
6224
  if (playwrightInstructions) output += `\n${playwrightInstructions.trim()}\n`;
7232
6225
  if (clerkInstructions) output += `\n${clerkInstructions.trim()}\n`;
7233
6226
  if (betterAuthConvexInstructions) output += `\n${betterAuthConvexInstructions.trim()}\n`;
7234
6227
  if (getMonitorInstructions) output += `\n${getMonitorInstructions.trim()}\n`;
7235
6228
  if (resendInstructions) output += `\n${resendInstructions.trim()}\n`;
7236
6229
  if (notifiqueInstructions) output += `\n${notifiqueInstructions.trim()}\n`;
6230
+ if (araraInstructions) output += `\n${araraInstructions.trim()}\n`;
7237
6231
  if (alchemyDeployInstructions) output += `\n${alchemyDeployInstructions.trim()}\n`;
7238
6232
  if (noOrmWarning) output += `\n${noOrmWarning.trim()}\n`;
7239
6233
  if (bunWebNativeWarning) output += `\n${bunWebNativeWarning.trim()}\n`;
7240
- output += `\n${pc.bold("Like kubojs?")} Please consider giving us a star\n on GitHub:\n`;
7241
- output += pc.cyan("https://github.com/albuquerquesz/kubo");
6234
+ output += `\n${pc$1.bold("Like kubojs?")} Please consider giving us a star\n on GitHub:\n`;
6235
+ output += pc$1.cyan("https://github.com/albuquerquesz/kubo");
7242
6236
  cliConsola.box(output);
7243
6237
  }
7244
6238
  function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
@@ -7247,26 +6241,27 @@ function getNativeInstructions(isConvex, isBackendSelf, frontend, runCmd) {
7247
6241
  const exampleUrl = isConvex ? "https://example.convex.cloud" : isBackendSelf ? `http://<YOUR_LOCAL_IP>:${selfBackendPort}` : "http://<YOUR_LOCAL_IP>:3000";
7248
6242
  const envFileName = ".env";
7249
6243
  const ipNote = isConvex ? "your Convex deployment URL (find after running 'dev:setup')" : "your local IP address";
7250
- let instructions = `${pc.yellow("NOTE:")} For Expo connectivity issues, update\n apps/native/${envFileName} with ${ipNote}:\n ${`${envVar}=${exampleUrl}`}\n`;
7251
- if (isConvex) instructions += `\n${pc.yellow("IMPORTANT:")} When using local development with Convex and native apps,\n ensure you use your local IP address instead of localhost or 127.0.0.1\n for proper connectivity.\n`;
7252
- if (frontend.includes("native-unistyles")) instructions += `\n${pc.yellow("NOTE:")} Unistyles requires a development build.\n cd apps/native and run ${runCmd} android or ${runCmd} ios\n`;
6244
+ let instructions = `${pc$1.yellow("NOTE:")} For Expo connectivity issues, update\n apps/native/${envFileName} with ${ipNote}:\n ${`${envVar}=${exampleUrl}`}\n`;
6245
+ if (isConvex) instructions += `\n${pc$1.yellow("IMPORTANT:")} When using local development with Convex and native apps,\n ensure you use your local IP address instead of localhost or 127.0.0.1\n for proper connectivity.\n`;
6246
+ if (frontend.includes("native-unistyles")) instructions += `\n${pc$1.yellow("NOTE:")} Unistyles requires a development build.\n cd apps/native and run ${runCmd} android or ${runCmd} ios\n`;
7253
6247
  return instructions;
7254
6248
  }
7255
6249
  function getHuskyInstructions(runCmd) {
7256
- return `${pc.bold("Git hooks with Husky:")}\n${pc.cyan("•")} Initialize hooks: ${`${runCmd} prepare`}\n`;
6250
+ return `${pc$1.bold("Git hooks with Husky:")}\n${pc$1.cyan("•")} Initialize hooks: ${`${runCmd} prepare`}\n`;
7257
6251
  }
7258
- function getPlaywrightInstructions(runCmd) {
7259
- return `${pc.bold("End-to-end tests with Playwright:")}\n${pc.cyan("•")} Install browsers: npx playwright install\n${pc.cyan("•")} Run e2e tests: ${`${runCmd} test:e2e`}\n`;
6252
+ function getPlaywrightInstructions(runCmd, packageManager) {
6253
+ const browserInstall = packageManager === "bun" ? "bunx playwright install" : packageManager === "pnpm" ? "pnpm exec playwright install" : "npx playwright install";
6254
+ return `${pc$1.bold("End-to-end tests with Playwright:")}\n${pc$1.cyan("•")} Install browsers (if needed): ${browserInstall}\n${pc$1.cyan("•")} Run e2e tests: ${`${runCmd} test:e2e`}\n`;
7260
6255
  }
7261
6256
  function getLintingInstructions(runCmd) {
7262
- return `${pc.bold("Linting and formatting:")}\n${pc.cyan("•")} Run checks: ${`${runCmd} check`}\n`;
6257
+ return `${pc$1.bold("Linting and formatting:")}\n${pc$1.cyan("•")} Run checks: ${`${runCmd} check`}\n`;
7263
6258
  }
7264
6259
  function getLefthookInstructions(packageManager) {
7265
6260
  const cmd = packageManager === "npm" ? "npx" : packageManager;
7266
- return `${pc.bold("Git hooks with Lefthook:")}\n${pc.cyan("•")} Install hooks: ${cmd} lefthook install\n`;
6261
+ return `${pc$1.bold("Git hooks with Lefthook:")}\n${pc$1.cyan("•")} Install hooks: ${cmd} lefthook install\n`;
7267
6262
  }
7268
6263
  function getVitePlusNativeHooksInstructions(runCmd) {
7269
- return `${pc.bold("Vite+ native Git hooks:")}\n${pc.cyan("•")} Optional hook setup: ${`${runCmd} hooks:setup`}\n${pc.dim(" (runs vp config; hooks install into .vite-hooks and use vp staged)")}\n`;
6264
+ return `${pc$1.bold("Vite+ native Git hooks:")}\n${pc$1.cyan("•")} Optional hook setup: ${`${runCmd} hooks:setup`}\n${pc$1.dim(" (runs vp config; hooks install into .vite-hooks and use vp staged)")}\n`;
7270
6265
  }
7271
6266
  async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup, webDeploy, serverDeploy, backend) {
7272
6267
  const instructions = [];
@@ -7279,62 +6274,63 @@ async function getDatabaseInstructions(database, orm, runCmd, _runtime, dbSetup,
7279
6274
  }
7280
6275
  }
7281
6276
  if (isD1Alchemy) {
7282
- if (orm === "drizzle") instructions.push(`${pc.cyan("•")} Generate migrations: ${`${runCmd} db:generate`}`);
6277
+ if (orm === "drizzle") instructions.push(`${pc$1.cyan("•")} Generate migrations: ${`${runCmd} db:generate`}`);
7283
6278
  else if (orm === "prisma") {
7284
- instructions.push(`${pc.cyan("•")} Generate Prisma client: ${`${runCmd} db:generate`}`);
7285
- instructions.push(`${pc.cyan("•")} Apply migrations: ${`${runCmd} db:migrate`}`);
6279
+ instructions.push(`${pc$1.cyan("•")} Generate Prisma client: ${`${runCmd} db:generate`}`);
6280
+ instructions.push(`${pc$1.cyan("•")} Apply migrations: ${`${runCmd} db:migrate`}`);
7286
6281
  }
7287
6282
  }
7288
6283
  if (dbSetup === "planetscale") {
7289
- if (database === "mysql" && orm === "drizzle") instructions.push(`${pc.yellow("NOTE:")} Enable foreign key constraints in PlanetScale database settings`);
7290
- if (database === "mysql" && orm === "prisma") instructions.push(`${pc.yellow("NOTE:")} How to handle Prisma migrations with PlanetScale:\n https://github.com/prisma/prisma/issues/7292`);
6284
+ if (database === "mysql" && orm === "drizzle") instructions.push(`${pc$1.yellow("NOTE:")} Enable foreign key constraints in PlanetScale database settings`);
6285
+ if (database === "mysql" && orm === "prisma") instructions.push(`${pc$1.yellow("NOTE:")} How to handle Prisma migrations with PlanetScale:\n https://github.com/prisma/prisma/issues/7292`);
7291
6286
  }
7292
- if (dbSetup === "turso" && orm === "prisma") instructions.push(`${pc.yellow("NOTE:")} Follow Turso's Prisma guide for migrations via the Turso CLI:\n https://docs.turso.tech/sdk/ts/orm/prisma`);
6287
+ if (dbSetup === "turso" && orm === "prisma") instructions.push(`${pc$1.yellow("NOTE:")} Follow Turso's Prisma guide for migrations via the Turso CLI:\n https://docs.turso.tech/sdk/ts/orm/prisma`);
7293
6288
  if (orm === "prisma") {
7294
- if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
7295
- if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
6289
+ if (database === "mongodb" && dbSetup === "docker") instructions.push(`${pc$1.yellow("WARNING:")} Prisma + MongoDB + Docker combination\n may not work.`);
6290
+ if (dbSetup === "docker") instructions.push(`${pc$1.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
7296
6291
  if (!isD1Alchemy) {
7297
- instructions.push(`${pc.cyan("•")} Generate Prisma Client: ${`${runCmd} db:generate`}`);
7298
- instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
6292
+ instructions.push(`${pc$1.cyan("•")} Generate Prisma Client: ${`${runCmd} db:generate`}`);
6293
+ instructions.push(`${pc$1.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
7299
6294
  }
7300
- if (!isD1Alchemy) instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
6295
+ if (!isD1Alchemy) instructions.push(`${pc$1.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
7301
6296
  } else if (orm === "drizzle") {
7302
- if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
7303
- if (!isD1Alchemy) instructions.push(`${pc.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
7304
- if (!isD1Alchemy) instructions.push(`${pc.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
6297
+ if (dbSetup === "docker") instructions.push(`${pc$1.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
6298
+ if (!isD1Alchemy) instructions.push(`${pc$1.cyan("•")} Apply schema: ${`${runCmd} db:push`}`);
6299
+ if (!isD1Alchemy) instructions.push(`${pc$1.cyan("•")} Database UI: ${`${runCmd} db:studio`}`);
7305
6300
  } else if (orm === "mongoose") {
7306
- if (dbSetup === "docker") instructions.push(`${pc.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
7307
- } else if (orm === "none") instructions.push(`${pc.yellow("NOTE:")} Manual database schema setup\n required.`);
7308
- return instructions.length ? `${pc.bold("Database commands:")}\n${instructions.join("\n")}` : "";
6301
+ if (dbSetup === "docker") instructions.push(`${pc$1.cyan("•")} Start docker container: ${`${runCmd} db:start`}`);
6302
+ } else if (orm === "none") instructions.push(`${pc$1.yellow("NOTE:")} Manual database schema setup\n required.`);
6303
+ return instructions.length ? `${pc$1.bold("Database commands:")}\n${instructions.join("\n")}` : "";
7309
6304
  }
7310
6305
  function getTauriInstructions(runCmd, frontend) {
7311
6306
  const staticBuildNote = getDesktopStaticBuildNote(frontend);
7312
- return `\n${pc.bold("Desktop app with Tauri:")}\n${pc.cyan("•")} Start desktop app: ${`cd apps/web && ${runCmd} desktop:dev`}\n${pc.cyan("•")} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc.yellow("NOTE:")} Tauri requires Rust and platform-specific dependencies.\n See: https://v2.tauri.app/start/prerequisites/${staticBuildNote ? `\n${staticBuildNote}` : ""}`;
6307
+ return `\n${pc$1.bold("Desktop app with Tauri:")}\n${pc$1.cyan("•")} Start desktop app: ${`cd apps/web && ${runCmd} desktop:dev`}\n${pc$1.cyan("•")} Build desktop app: ${`cd apps/web && ${runCmd} desktop:build`}\n${pc$1.yellow("NOTE:")} Tauri requires Rust and platform-specific dependencies.\n See: https://v2.tauri.app/start/prerequisites/${staticBuildNote ? `\n${staticBuildNote}` : ""}`;
7313
6308
  }
7314
6309
  function getElectrobunInstructions(runCmd, frontend) {
7315
6310
  const staticBuildNote = getDesktopStaticBuildNote(frontend);
7316
- return `\n${pc.bold("Desktop app with Electrobun:")}\n${pc.cyan("•")} Start desktop app with HMR: ${`${runCmd} dev:desktop`}\n${pc.cyan("•")} Build stable desktop app (DMG/App): ${`${runCmd} build:desktop`}\n${pc.cyan("•")} Build canary desktop app: ${`${runCmd} build:desktop:canary`}\n${pc.yellow("NOTE:")} Electrobun wraps your web frontend in a desktop shell.\n See: https://framework.blackboard.sh/electrobun/${staticBuildNote ? `\n${staticBuildNote}` : ""}`;
6311
+ return `\n${pc$1.bold("Desktop app with Electrobun:")}\n${pc$1.cyan("•")} Start desktop app with HMR: ${`${runCmd} dev:desktop`}\n${pc$1.cyan("•")} Build stable desktop app (DMG/App): ${`${runCmd} build:desktop`}\n${pc$1.cyan("•")} Build canary desktop app: ${`${runCmd} build:desktop:canary`}\n${pc$1.yellow("NOTE:")} Electrobun wraps your web frontend in a desktop shell.\n See: https://framework.blackboard.sh/electrobun/${staticBuildNote ? `\n${staticBuildNote}` : ""}`;
7317
6312
  }
7318
6313
  function getPwaInstructions() {
7319
- return `\n${pc.bold("PWA with React Router v7:")}\n${pc.yellow("NOTE:")} There is a known compatibility issue between VitePWA\n and React Router v7. See:\n https://github.com/vite-pwa/vite-plugin-pwa/issues/809`;
7320
- }
7321
- function getStarlightInstructions(runCmd) {
7322
- return `\n${pc.bold("Documentation with Starlight:")}\n${pc.cyan("•")} Start docs site: ${`cd apps/docs && ${runCmd} dev`}\n${pc.cyan("•")} Build docs site: ${`cd apps/docs && ${runCmd} build`}`;
6314
+ return `\n${pc$1.bold("PWA with React Router v7:")}\n${pc$1.yellow("NOTE:")} There is a known compatibility issue between VitePWA\n and React Router v7. See:\n https://github.com/vite-pwa/vite-plugin-pwa/issues/809`;
7323
6315
  }
7324
6316
  function getNoOrmWarning() {
7325
- return `\n${pc.yellow("WARNING:")} Database selected without an ORM. Features requiring\n database access (e.g., examples, auth) need manual setup.`;
6317
+ return `\n${pc$1.yellow("WARNING:")} Database selected without an ORM. Features requiring\n database access (e.g., examples, auth) need manual setup.`;
7326
6318
  }
7327
6319
  function getGetMonitorInstructions() {
7328
- return `${pc.bold("GetMonitor observability:")}\n${pc.cyan("•")} Keys are optional on first run — the app starts without them; capture stays idle until configured\n${pc.cyan("•")} When ready, set GETMONITOR_API_KEY (public gm_xxx) in the web and/or server .env\n${pc.cyan("•")} Ingestion host is fixed (http://ingest.getmonitor.io) — no apiHost env var\n${pc.cyan("•")} Optional: set GETMONITOR_AUTH_TOKEN for Next/Nuxt source-map upload on production builds\n${pc.cyan("•")} Browser/Node capture is automatic; React trees include GetMonitorErrorBoundary\n${pc.cyan("•")} Opt out next time with --disable-observability\n${pc.dim(" https://github.com/get-monitor/getmonitor-js")}`;
6320
+ return `${pc$1.bold("GetMonitor observability:")}\n${pc$1.cyan("•")} Keys are optional on first run — the app starts without them; capture stays idle until configured\n${pc$1.cyan("•")} When ready, set GETMONITOR_API_KEY (public gm_xxx) in the web and/or server .env\n${pc$1.cyan("•")} Ingestion host is fixed (http://ingest.getmonitor.io) — no apiHost env var\n${pc$1.cyan("•")} Optional: set GETMONITOR_AUTH_TOKEN for Next/Nuxt source-map upload on production builds\n${pc$1.cyan("•")} Browser/Node capture is automatic; React trees include GetMonitorErrorBoundary\n${pc$1.cyan("•")} Opt out next time with --disable-observability\n${pc$1.dim(" https://github.com/get-monitor/getmonitor-js")}`;
7329
6321
  }
7330
6322
  function getResendInstructions() {
7331
- return `${pc.bold("Resend communication:")}\n${pc.cyan("•")} Keys are optional on first run — sendEmail throws only when called without RESEND_API_KEY\n${pc.cyan("•")} Set RESEND_API_KEY (and optional RESEND_FROM_EMAIL) in the server .env\n${pc.cyan("•")} Import sendEmail from packages/email (e.g. @project/email)\n${pc.cyan("•")} Replace onboarding@resend.dev with a verified domain sender for production\n${pc.dim(" https://resend.com/docs/send-with-nodejs")}`;
6323
+ return `${pc$1.bold("Resend communication:")}\n${pc$1.cyan("•")} Keys are optional on first run — sendEmail throws only when called without RESEND_API_KEY\n${pc$1.cyan("•")} Set RESEND_API_KEY (and optional RESEND_FROM_EMAIL) in the server .env\n${pc$1.cyan("•")} Import sendEmail from packages/email (e.g. @project/email)\n${pc$1.cyan("•")} Replace onboarding@resend.dev with a verified domain sender for production\n${pc$1.dim(" https://resend.com/docs/send-with-nodejs")}`;
7332
6324
  }
7333
6325
  function getNotifiqueInstructions() {
7334
- return `${pc.bold("Notifique communication:")}\n${pc.cyan("•")} Keys are optional on first run — helpers throw only when called without NOTIFIQUE_API_KEY\n${pc.cyan("•")} Set NOTIFIQUE_API_KEY (sk_live_… / sk_test_…) in the server .env\n${pc.cyan("•")} Import sendSms / sendWhatsAppText / sendEmail from packages/notifique\n${pc.cyan("•")} Auth is Bearer only — do not send x-workspace-id\n${pc.dim(" https://docs.notifique.dev/skill.md")}`;
6326
+ return `${pc$1.bold("Notifique communication:")}\n${pc$1.cyan("•")} Keys are optional on first run — helpers throw only when called without NOTIFIQUE_API_KEY\n${pc$1.cyan("•")} Set NOTIFIQUE_API_KEY (sk_live_… / sk_test_…) in the server .env\n${pc$1.cyan("•")} Import sendSms / sendWhatsAppText / sendEmail from packages/notifique\n${pc$1.cyan("•")} Auth is Bearer only — do not send x-workspace-id\n${pc$1.dim(" https://docs.notifique.dev/skill.md")}`;
6327
+ }
6328
+ function getAraraInstructions(backend) {
6329
+ const envPath = backend === "convex" ? "packages/backend/.env.local" : "the server .env";
6330
+ return `${pc$1.bold("AraraHQ communication:")}\n${pc$1.cyan("•")} Set ARARA_API_KEY in ${envPath}\n${pc$1.cyan("•")} Use @project/arara only from server code; the SDK does not run on Edge/Workers\n${pc$1.cyan("•")} Convex projects use the generated Node Action (api.arara.execute)\n${pc$1.dim(" https://docs.ararahq.com/sdks/node")}`;
7335
6331
  }
7336
6332
  function getBunWebNativeWarning() {
7337
- return `\n${pc.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
6333
+ return `\n${pc$1.yellow("WARNING:")} 'bun' might cause issues with web + native apps in a monorepo.\n Use 'pnpm' if problems arise.`;
7338
6334
  }
7339
6335
  function getClerkQuickstartUrl(frontend) {
7340
6336
  if (frontend.includes("next")) return "https://clerk.com/docs/nextjs/getting-started/quickstart";
@@ -7390,28 +6386,28 @@ function getClerkInstructionLines(frontend, backend, api) {
7390
6386
  }
7391
6387
  function getClerkInstructions(frontend, backend, api) {
7392
6388
  return [
7393
- `${pc.bold("Clerk Authentication Setup:")}`,
7394
- `${pc.cyan("•")} Follow the guide: ${pc.underline(getClerkQuickstartUrl(frontend))}`,
7395
- ...getClerkInstructionLines(frontend, backend, api).map((line) => `${pc.cyan("•")} ${line}`)
6389
+ `${pc$1.bold("Clerk Authentication Setup:")}`,
6390
+ `${pc$1.cyan("•")} Follow the guide: ${pc$1.underline(getClerkQuickstartUrl(frontend))}`,
6391
+ ...getClerkInstructionLines(frontend, backend, api).map((line) => `${pc$1.cyan("•")} ${line}`)
7396
6392
  ].join("\n");
7397
6393
  }
7398
6394
  function getBetterAuthConvexInstructions(hasWeb, webPort, packageManager, runCmd) {
7399
6395
  const cmd = packageManager === "npm" ? "npx" : packageManager;
7400
- return `${pc.bold("Better Auth + Convex Setup:")}\n${pc.cyan("•")} Configure the Convex deployment before setting env vars:\n${pc.white(` ${runCmd} dev:setup`)}\n${pc.cyan("•")} Set environment variables from ${pc.white("packages/backend")}:\n${pc.white(" cd packages/backend")}\n${pc.white(` ${cmd} convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)`)}\n` + (hasWeb ? `${pc.white(` ${cmd} convex env set SITE_URL http://localhost:${webPort}`)}\n` : "");
6396
+ return `${pc$1.bold("Better Auth + Convex Setup:")}\n${pc$1.cyan("•")} Configure the Convex deployment before setting env vars:\n${pc$1.white(` ${runCmd} dev:setup`)}\n${pc$1.cyan("•")} Set environment variables from ${pc$1.white("packages/backend")}:\n${pc$1.white(" cd packages/backend")}\n${pc$1.white(` ${cmd} convex env set BETTER_AUTH_SECRET=$(openssl rand -base64 32)`)}\n` + (hasWeb ? `${pc$1.white(` ${cmd} convex env set SITE_URL http://localhost:${webPort}`)}\n` : "");
7401
6397
  }
7402
6398
  function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend) {
7403
6399
  const instructions = [];
7404
6400
  const isBackendSelf = backend === "self";
7405
6401
  if (webDeploy === "cloudflare" && serverDeploy !== "cloudflare" && !isBackendSelf) {
7406
6402
  const cfDeploy = serverDeploy === "vercel" ? "deploy:web" : "deploy";
7407
- instructions.push(`${pc.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} ${cfDeploy}`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
6403
+ instructions.push(`${pc$1.bold("Deploy web with Cloudflare (Alchemy):")}\n${pc$1.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc$1.cyan("•")} Deploy: ${`${runCmd} ${cfDeploy}`}\n${pc$1.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
7408
6404
  } else if (serverDeploy === "cloudflare" && webDeploy !== "cloudflare" && !isBackendSelf) {
7409
6405
  const cfDeploy = webDeploy === "vercel" ? "deploy:server" : "deploy";
7410
- instructions.push(`${pc.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} ${cfDeploy}`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
7411
- } else if (webDeploy === "cloudflare" && (serverDeploy === "cloudflare" || isBackendSelf)) instructions.push(`${pc.bold("Deploy with Cloudflare (Alchemy):")}\n${pc.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
6406
+ instructions.push(`${pc$1.bold("Deploy server with Cloudflare (Alchemy):")}\n${pc$1.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc$1.cyan("•")} Deploy: ${`${runCmd} ${cfDeploy}`}\n${pc$1.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
6407
+ } else if (webDeploy === "cloudflare" && (serverDeploy === "cloudflare" || isBackendSelf)) instructions.push(`${pc$1.bold("Deploy with Cloudflare (Alchemy):")}\n${pc$1.cyan("•")} Dev: ${`${runCmd} dev`}\n${pc$1.cyan("•")} Deploy: ${`${runCmd} deploy`}\n${pc$1.cyan("•")} Destroy: ${`${runCmd} destroy`}`);
7412
6408
  if (webDeploy === "docker" || serverDeploy === "docker") {
7413
6409
  const dockerTargets = webDeploy === "docker" && serverDeploy === "docker" ? "web + server" : webDeploy === "docker" ? "web" : "server";
7414
- instructions.push(`${pc.bold(`Deploy ${dockerTargets} with Docker Compose:`)}\n${pc.cyan("•")} Start: ${`${runCmd} docker:up`}\n${pc.cyan("•")} Logs: ${`${runCmd} docker:logs`}\n${pc.cyan("•")} Stop: ${`${runCmd} docker:down`}\n${pc.cyan("•")} Config: docker-compose.yml`);
6410
+ instructions.push(`${pc$1.bold(`Deploy ${dockerTargets} with Docker Compose:`)}\n${pc$1.cyan("•")} Start: ${`${runCmd} docker:up`}\n${pc$1.cyan("•")} Logs: ${`${runCmd} docker:logs`}\n${pc$1.cyan("•")} Stop: ${`${runCmd} docker:down`}\n${pc$1.cyan("•")} Config: docker-compose.yml`);
7415
6411
  }
7416
6412
  if (webDeploy === "vercel" || serverDeploy === "vercel") {
7417
6413
  const mixedWithCloudflare = webDeploy !== "none" && serverDeploy !== "none" && webDeploy !== serverDeploy;
@@ -7419,14 +6415,14 @@ function getAlchemyDeployInstructions(runCmd, webDeploy, serverDeploy, backend)
7419
6415
  const vercelEnvScript = "env:production";
7420
6416
  const vercelDeployScript = mixedWithCloudflare ? webDeploy === "vercel" ? "deploy:web:prod" : "deploy:server:prod" : "deploy:prod";
7421
6417
  const vercelTargets = webDeploy === "vercel" && (serverDeploy === "vercel" || isBackendSelf) ? "web + server" : webDeploy === "vercel" ? "web" : "server";
7422
- instructions.push(`${pc.bold(`Deploy ${vercelTargets} with Vercel Services:`)}\n${pc.cyan("•")} Link project: ${`${runCmd} ${vercelSetupScript}`}\n${pc.cyan("•")} Sync env (before first deploy): ${`${runCmd} ${vercelEnvScript}`}\n${pc.cyan("•")} Deploy: ${`${runCmd} ${vercelDeployScript}`}\n${pc.cyan("•")} Guide: https://www.kubojs.dev/docs/guides/vercel`);
6418
+ instructions.push(`${pc$1.bold(`Deploy ${vercelTargets} with Vercel Services:`)}\n${pc$1.cyan("•")} Link project: ${`${runCmd} ${vercelSetupScript}`}\n${pc$1.cyan("•")} Sync env (before first deploy): ${`${runCmd} ${vercelEnvScript}`}\n${pc$1.cyan("•")} Deploy: ${`${runCmd} ${vercelDeployScript}`}\n${pc$1.cyan("•")} Guide: https://www.kubojs.dev/docs/guides/vercel`);
7423
6419
  }
7424
6420
  if (webDeploy === "guaracloud" || serverDeploy === "guaracloud") {
7425
6421
  const deployScript = webDeploy !== "none" && serverDeploy !== "none" && webDeploy !== serverDeploy || webDeploy === "guaracloud" && serverDeploy === "guaracloud" ? `deploy:${webDeploy === "guaracloud" ? "web" : "server"}` : "deploy";
7426
6422
  const linkScript = `${deployScript}:link`;
7427
6423
  const logsScript = `${deployScript}:logs`;
7428
6424
  const guaraTargets = webDeploy === "guaracloud" && (serverDeploy === "guaracloud" || isBackendSelf) ? "web + server" : webDeploy === "guaracloud" ? "web" : "server";
7429
- instructions.push(`${pc.bold(`Deploy ${guaraTargets} with Guara Cloud:`)}\n${pc.cyan("•")} Login: ${`${runCmd} deploy:login`}\n${pc.cyan("•")} Link app directory: ${`${runCmd} ${linkScript}`}\n${pc.cyan("•")} Deploy: ${`${runCmd} ${deployScript}`}\n${pc.cyan("•")} Logs: ${`${runCmd} ${logsScript}`}\n${pc.cyan("•")} Docs: https://guaracloud.com/docs/getting-started/introduction/`);
6425
+ instructions.push(`${pc$1.bold(`Deploy ${guaraTargets} with Guara Cloud:`)}\n${pc$1.cyan("•")} Login: ${`${runCmd} deploy:login`}\n${pc$1.cyan("•")} Link app directory: ${`${runCmd} ${linkScript}`}\n${pc$1.cyan("•")} Deploy: ${`${runCmd} ${deployScript}`}\n${pc$1.cyan("•")} Logs: ${`${runCmd} ${logsScript}`}\n${pc$1.cyan("•")} Docs: https://guaracloud.com/docs/getting-started/introduction/`);
7430
6426
  }
7431
6427
  return instructions.length ? `\n${instructions.join("\n")}` : "";
7432
6428
  }
@@ -7492,10 +6488,13 @@ async function createProject(options, cliInput = {}) {
7492
6488
  }));
7493
6489
  yield* Result.await(formatProject(projectDir, { addons: options.addons }));
7494
6490
  if (!isSilent()) log.success("Project template successfully scaffolded!");
7495
- if (options.install) yield* Result.await(installDependencies({
7496
- projectDir,
7497
- packageManager: options.packageManager
7498
- }));
6491
+ if (options.install) {
6492
+ yield* Result.await(installDependencies({
6493
+ projectDir,
6494
+ packageManager: options.packageManager
6495
+ }));
6496
+ if (options.testing.includes("playwright")) await installPlaywrightBrowsers(projectDir, options.packageManager);
6497
+ }
7499
6498
  yield* Result.await(initializeGit(projectDir, options.git));
7500
6499
  if (!isSilent()) await displayPostInstallInstructions({
7501
6500
  ...options,
@@ -7568,7 +6567,7 @@ function createEmptyResult(timeScaffolded, elapsedTimeMs, error) {
7568
6567
  examples: [],
7569
6568
  testing: [],
7570
6569
  auth: "none",
7571
- payments: "none",
6570
+ payments: [],
7572
6571
  observability: [],
7573
6572
  communication: "none",
7574
6573
  git: false,
@@ -7608,7 +6607,7 @@ async function createProjectHandler(input, options = {}) {
7608
6607
  async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7609
6608
  return Result.gen(async function* () {
7610
6609
  if (!isSilent() && input.renderTitle !== false) renderTitle();
7611
- if (!isSilent()) intro(pc.magenta("Creating a new kubojs project"));
6610
+ if (!isSilent()) intro(cliColors.bright("Creating a new kubojs project"));
7612
6611
  if (!isSilent() && input.yolo) cliConsola.fatal("YOLO mode enabled - skipping checks. Things may break!");
7613
6612
  let currentPathInput;
7614
6613
  if (isSilent()) currentPathInput = yield* Result.await(resolveProjectNameForSilent(input));
@@ -7670,7 +6669,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7670
6669
  const templateName = input.template.toUpperCase();
7671
6670
  const templateDescription = getTemplateDescription(input.template);
7672
6671
  if (!isSilent()) {
7673
- log.message(pc.bold(pc.cyan(`Using template: ${pc.white(templateName)}`)));
6672
+ log.message(pc.bold(cliColors.bright(`Using template: ${pc.white(templateName)}`)));
7674
6673
  log.message(pc.dim(` ${templateDescription}`));
7675
6674
  }
7676
6675
  const userOverrides = {};
@@ -7753,7 +6752,7 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7753
6752
  log.success(pc.green("Dry run validation passed. No files were written."));
7754
6753
  log.message(pc.dim(`Target directory: ${finalResolvedPath}`));
7755
6754
  log.message(pc.dim(`Run without --dry-run to create the project.`));
7756
- outro(pc.magenta("Dry run complete."));
6755
+ outro(cliColors.signal("Dry run complete."));
7757
6756
  }
7758
6757
  return Result.ok({
7759
6758
  success: true,
@@ -7769,14 +6768,14 @@ async function createProjectHandlerInternal(input, startTime, timeScaffolded) {
7769
6768
  manualDb: cliInput.manualDb ?? input.manualDb,
7770
6769
  dbSetupOptions: effectiveDbSetupOptions
7771
6770
  }));
7772
- if (!isSilent()) log.success(pc.blue(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
6771
+ if (!isSilent()) log.success(cliColors.bright(`You can reproduce this setup with the following command:\n${reproducibleCommand}`));
7773
6772
  await trackProjectCreation(config, input.disableAnalytics);
7774
6773
  const historyResult = await addToHistory(config, reproducibleCommand);
7775
6774
  if (historyResult.isErr() && !isSilent()) log.warn(pc.yellow(historyResult.error.message));
7776
6775
  const elapsedTimeMs = Date.now() - startTime;
7777
6776
  if (!isSilent()) {
7778
6777
  const elapsedTimeInSeconds = (elapsedTimeMs / 1e3).toFixed(2);
7779
- outro(pc.magenta(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
6778
+ outro(cliColors.signal(`Project created successfully in ${pc.bold(elapsedTimeInSeconds)} seconds!`));
7780
6779
  }
7781
6780
  return Result.ok({
7782
6781
  success: true,
@@ -8085,6 +7084,8 @@ async function builder() {
8085
7084
  * ```
8086
7085
  */
8087
7086
  async function createVirtual(options) {
7087
+ const rawPayments = options.payments;
7088
+ const rawObservability = options.observability;
8088
7089
  const config = {
8089
7090
  projectName: options.projectName || "my-project",
8090
7091
  projectDir: "/virtual",
@@ -8100,8 +7101,8 @@ async function createVirtual(options) {
8100
7101
  examples: options.examples || [],
8101
7102
  testing: options.testing || [],
8102
7103
  auth: options.auth || "none",
8103
- payments: options.payments || "none",
8104
- observability: options.disableObservability ? [] : options.observability || [],
7104
+ payments: rawPayments === "none" ? [] : Array.isArray(rawPayments) ? rawPayments : rawPayments ? [rawPayments] : [],
7105
+ observability: options.disableObservability ? [] : rawObservability === "none" ? [] : Array.isArray(rawObservability) ? rawObservability : rawObservability ? [rawObservability] : [],
8105
7106
  communication: options.communication || "none",
8106
7107
  git: options.git ?? false,
8107
7108
  packageManager: options.packageManager || "bun",